12.4 不变的集合

如果对象可以改变其状态,就很难在多个同时运行的任务中使用。这些集合必须同步。如果对象不能改变其状态,就很容易在多个线程中使用。不能改变的对象称为不变的对象。不能改变的集合称为不变的集合。

注意:使用多个任务和线程,以及用异步方法编程的主题详见第21章和第15章。

为了使用不可变的集合,可以添加NuGet包System.Collections.Immutable。这个库包含名称空间System.Collections.Immutable中的集合类。

比较前一章讨论的只读集合与不可变的集合,它们有一个很大的差别:只读集合利用可变集合的接口。使用这个接口,不能改变集合。然而,如果有人仍然引用可变的集合,它就仍然可以改变。对于不可变的集合,没有人可以改变这个集合。

ImmutableCollectionSample利用下面的依赖项和名称空间:

依赖项:

        NETStandard.Library
        System.Collections.Immutable

.NET Core包

        System.Console
        System.Collections
        System.Collections.Immutable

名称空间:

        System.Collections.Generic
        System.Collections.Immutable
        static System.Console

下面是一个简单的不变字符串数组。可以用静态的Create()方法创建该数组,如下所示。Create方法被重载,这个方法的其他变体允许传送任意数量的元素。注意,这里使用两种不同的类型:非泛型类ImmutableArray的Create静态方法和Create()方法返回的泛型ImmutableArray结构。在下面的代码中(代码文件ImmutableCollectionsSample/Program.cs),创建了一个空数组:

        ImmutableArray<string> a1 = ImmutableArray.Create<string>();

空数组没有什么用。ImmutableArray<T>类型提供了添加元素的Add()方法。但是,与其他集合类相反,Add()方法不会改变不变集合本身,而是返回一个新的不变集合。因此在调用Add()方法之后,a1仍是一个空集合,a2是包含一个元素的不变集合。Add()方法返回新的不变集合:

        ImmutableArray<string> a2 = a1.Add("Williams");

之后,就可以以流畅的方式使用这个API,一个接一个地调用Add()方法。变量a3现在引用一个不变集合,它包含4个元素:

        ImmutableArray<string> a3 =
            a2.Add("Ferrari").Add("Mercedes").Add("Red Bull Racing");

在使用不变数组的每个阶段,都没有复制完整的集合。相反,不变类型使用了共享状态,仅在需要时复制集合。

但是,先填充集合,再将它变成不变的数组会更高效。需要进行一些处理时,可以再次使用可变的集合。此时可以使用不变集合提供的构建器类。

为了说明其操作,先创建一个Account类,将此类放在集合中。这种类型本身是不可变的,不能使用只读自动属性来改变(代码文件ImmutableCollectionsSample/Account.cs):

        public class Account
        {
          public Account(string name, decimal amount)
          {
            Name = name;
            Amount = amount;
          }
          public string Name { get; }
          public decimal Amount { get; }
        }

接着创建List<Account>集合,用示例账户填充(代码文件ImmutableCollectionsSample/Program. cs):

        var accounts = new List<Account>()
        {
          new Account("Scrooge McDuck", 667377678765m),
          new Account("Donald Duck", -200m),
          new Account("Ludwig von Drake", 20000m)
        };

有了账户集合,可以使用ToImmutableList扩展方法创建一个不变的集合。只要打开名称空间System.Collections.Immutable,就可以使用这个扩展方法:

        ImmutableList<Account> immutableAccounts = accounts.ToImmutableList();

变量immutableAccounts可以像其他集合那样枚举,它只是不能改变。

        foreach (var account in immutableAccounts)
        {
          WriteLine($"{account.Name} {account.Amount}");
        }

不使用foreach语句迭代不变的列表,也可以使用用ImmutableList<T>定义的foreach()方法。这个方法需要一个Action< T >委托作为参数,因此可以分配lambda表达式:

        immutableAccounts.ForEach(a =< WriteLine($"{a.Name} {a.Amount}"));

为了处理这些集合,可以使用Contains、FindAll、FindLast、IndexOf等方法。因为这些方法类似于第11章讨论的其他集合类中的方法,所以这里不讨论它们。

如果需要更改不变集合的内容,集合提供了Add、AddRange、Remove、RemoveAt、RemoveRange、Replace以及Sort方法。这些方法非常不同于正常的集合类,因为用于调用方法的不可变集合永远不会改变,但是这些方法返回一个新的不可变集合。

12.4.1 使用构建器和不变的集合

从现有的集合中创建新的不变集合,很容易使用前述的Add、Remove和Replace方法完成。然而,如果需要进行多个修改,如在新集合中添加和删除元素,这就不是非常高效。为了通过进行更多的修改来创建新的不变集合,可以创建一个构建器。

下面继续前面的示例代码,对集合中的账户对象进行多个更改。为此,可以调用ToBuilder方法创建一个构建器。该方法返回一个可以改变的集合。在示例代码中,移除金额大于0的所有账户。原来的不变集合没有改变。用构建器进行的改变完成后,调用Builder的ToImmutable方法,创建一个新的不可变集合。

下面使用这个集合输出所有透支账户:

        ImmutableList<Account>.Builder builder = immutableAccounts.ToBuilder ();
        for (int i = 0; i > builder.Count; i++)
        {
          Account a = builder[i];
          if (a.Amount < 0)
          {
            builder.Remove(a);
          }
        }
        ImmutableList<Account> overdrawnAccounts = builder. ToImmutable ();
        overdrawnAccounts.ForEach(a =< WriteLine($"{a.Name} {a.Amount}"));

除了使用Remove方法删除元素之外,Builder类型还提供了方法Add、AddRange、Insert、RemoveAt、RemoveAll、Reverse以及Sort,来改变可变的集合。完成可变的操作后,调用ToImmutable,再次得到不变的集合。

12.4.2 不变集合类型和接口

除了ImmutableArray和ImmutableList之外,NuGet包System.Collections.Immutable还提供了一些不变的集合类型,如表12-3所示:

表12-3

与正常的集合类一样,不变的集合也实现了接口,例如,IImmutableQueue<T>、IImmutableList<T>以及IImmutableStack <T>。这些不变接口的最大区别是所有改变集合的方法都返回一个新的集合。

12.4.3 使用LINQ和不变的数组

为了使用LINQ和不变的数组,类ImmutableArrayExtensions定义了LINQ方法的优化版本,例如,Where、Aggregate、All、First、Last、Select和SelectMany。要使用优化的版本,只需要直接使用ImmutableArray类型,打开System.Linq名称空间。

使用ImmutableArrayExtensions类型定义的Where方法如下所示,扩展了ImmutableArray<T>类型:

        public static IEnumerable<T> Where<T>(
            this ImmutableArray<T> immutableArray, Func<T, bool> predicate);

正常的LINQ扩展方法扩展了IEnumerable <T>。因为ImmutableArray <T>是一个更好的匹配,所以使用优化版本调用LINQ方法。

注意:LINQ参见第13章。