C# 特殊集合
C#中的特殊集合主要有:
- 不可变的集合
- 并发的集合
- 位数组合位矢量
- 可观察的集合
不变的集合
如果对象可以改变其状态,就很难在多个同时运行的任务中使用。这些集合必须同步。如果对象不能改变其状态,就很容易在多个线程中使用。不能改变的对象称为不变的对象;不能改变的集合称为不变的集合。
为了使用不可变的集合,需要添加NuGget包System.Collections.Immutalbe,关于此命名空间下的详细介绍,请点击此处进行查看,本文只对其进行简单的示例说明。
ImmutableArray
该类提供创建不可变数组的方法。例如:
ImmutableArray<string> a1= ImmutableArray.Create<string>();
上述语句用于创建一个string类型的不可变数组,注意,上述虽然都是ImmutableArray,但是却是两种不同的类型:非泛型类ImmutableArray调用Create()静态方法返回泛型ImmutableArray结构。其中,Create方法被重载,这个方法的其他变体允许传送任意数量的元素。
可以使用Add()方法添加新的元素,Add()方法不会改变不变集合本身,而是返回一个新的不变集合。
ImmutableArray<string> a2= a1.Add("java");
上述语句执行之后,a1仍然是一个空集合,a2是包含一个元素的不变集合。可以链式的重复调用Add()方法,最终返回一个集合:
ImmutableArray<string> a3 = a2.Add("c#").Add("python").Add("php");
在使用不变数组的每个阶段,都没有复制完整的集合。相反,不变类型使用了共享状态,仅在需要时复制集合。
通常,先填充集合,再将它变成不变的数组会更高效。当需要进行一些处理时,可以再次使用可变的集合。
ImmutableList<T>
表示不可变列表,它是可由索引访问的强类型对象列表。
示例说明,先定义一个简单的类:
1internal class Account 2{ 3 public string Name { get; } 4 public decimal Amount { get; } 5 6 public Account(string name, decimal amount) 7 { 8 this.Name = name; 9 this.Amount = amount; 10 } 11}
接着创建List<Account>集合,使用ToImmutableList方法将其转换为不变的集合。
1var accounts = new List<Account> 2{ 3 new Account("图书",424.2m), 4 new Account("文具",1243.5m), 5 new Account("篮球",243.3m) 6}; 7//将List转换为不可变集合 8ImmutableList<Account> immutableAccounts = accounts.ToImmutableList(); 9//输出每一项的内容 10immutableAccounts.ForEach(a => Console.WriteLine(a.Name + "--" + a.Amount));
如果需要更改不变集合的内容,可以使用不变集合的Add、AddRange、Remove、RemoveAt、RemoveRange、Replace以及Sort等方法,这些方法都不是直接改变了原来的不变集合,而是返回一个新的不可变集合。虽然上述这些方法可以创建新的不变集合,但是如果对集合频繁的进行多次修改和删除元素,这就不是非常高效。可以使用ImmutableList<T>的ToBuilder() 方法,创建一个构建器,该方法返回一个可以改变的集合。例如:
1 var accounts = new List<Account> 2 { 3 new Account("图书",424.2m), 4 new Account("文具",1243.5m), 5 new Account("篮球",243.3m) 6 }; 7 //先得到不可变集合 8 ImmutableList<Account> immutableAccounts = accounts.ToImmutableList(); 9 //调用ToBuilder()方法将不可变集合创建为可变集合 10 ImmutableList<Account>.Builder builder = immutableAccounts.ToBuilder(); 11 for (int i = 0; i < builder.Count; i++) 12 { 13 Account a = builder[i]; 14 if (a.Amount > 1000) 15 { 16 builder.Remove(a); 17 } 18 } 19 //将新创建的可变集合调用ToImmutable()方法得到不可变集合 20 ImmutableList<Account> overdrawnAccounts = builder.ToImmutable(); 21 overdrawnAccounts.ForEach(b => Console.WriteLine(b.Name + "=" + b.Amount));
除了ImmutableArray和ImmutableList之外,该命名空间下还提供了其他一些不变的集合类型。如:
ImmutableArray<T>:ImmutableArray<T>是一个结构,它在内部使用数组类型,当不允许更改底层类型,这个结构实现了接口IImmutableList<T>。ImmutableList<T>:ImmutableList<T>在内部使用一个二叉树来映射对象,以实现接口IImmutableList<T>。ImmutableQueue<T>:ImmutableQueue<T>实现了接口IImmutableQueue<T>,允许使用Enqueue、Dequeue和Peek以先进先出的方式访问元素。ImmutableStack<T>:ImmutableStack<T>实现了接口IImmutableStack<T>,允许使用Push、Pop和Peek以先进后出的方式访问元素。ImmutableDictionary<TKey,TValue>:ImmutableDictionary<TKey,TValue>是一个键和值不可变的集合,其无序的键/值对元素实现了接口IImmutableDictionary<TKey,TValue>。ImmutableSortedDictionary<TKey,TValue>:ImmutableSortedDictionary<TKey,TValue>是一个不可变的排序字典。其实现了接口IImmutableDictionary<TKey,TValue>。ImmutableHashSet<T>:表示不可变的无序哈希集 ,实现了接口IImmutableSet<T>。ImmutableSortedSet<T>:表示不可变的有序集合,实现了接口IImmutableSet<T>。
上述的这些不变的集合都实现了对应的接口,与正常集合相比,这些不变接口的最大区别是所有改变集合的方法都返回一个新的集合。
并发集合
在命名空间System.Collections.Concurrent 中,提供了几个线程安全的集合类,线程安全的集合可以防止多个线程以相互冲突的方式访问集合。下面列出了System.Collections.Concurrent命名空间中常用的类及其功能。
ConcurrentQueue<T>:表示线程安全的先进先出(FIFO)集合。 这个集合类用一种免锁定的算法实现,使用在内部合并到一个链表中的32项数组。访问队列元素的方法有Enqueue(T)、TryDequeue(T)和TryPeek(T)。这些方法的命名和前面的Queue<T>类的方法很像,只是给可能调用失败的方法加上了前缀Try。因为这个类实现了IProducerConsumerCollection<T>接口,所以TryAdd()和TryTake()方法仅调用Enqueue()和TryDequeue()方法。ConcurrentStack<T>:表示线程安全的后进先出(LIFO)集合。 和ConcurrentQueue<T>类似,只是访问元素的方法不同。ConcurrentStack<T>类定义了Push(T)、PushRange()、TryPeek(T)、TryPop(T)和TryPopRange(T[])方法。该类也实现了IProducerConsumerCollection<T>接口。ConcurrentBag<T>:表示线程安全,无序的对象集合。 该类没有定义添加或提取项的任何顺序。这个类使用一个把线程映射到内部使用的数组上的概念,因此尝试减少锁定。访问元素的方法有Add(T)、TryPeek(T)和TryTake(T)。该类也实现了IProducerConsumerCollection<T>接口。ConcurrentDictionary<TKey,TValue>:表示可以由多个线程同时访问的键/值对的线程安全集合。TryAdd(TKey, TValue)、TryGetValue(TKey, TValue)、TryRemove(TKey, TValue)和TryUpdate(TKey, TValue, TValue)方法以非阻塞的方式访问成员。因为元素基于键和值,所以ConcurrentDictionary<TKey,TValue>类没有实现IProducerConsumerCollection<T>接口。BlockingCollection<T>:为实现IProducerConsumerCollection的线程安全集合提供阻塞和绑定功能。这个集合可以在添加或提取元素之前,会阻塞线程并一直等待。BlockingCollection<T>集合提供了一个接口,以使用Add(T)和Take()方法来添加和删除元素。这些方法会阻塞线程,一直等到任务可以执行为止。Add()方法有一个重载版本,其中可以给该重载版本传递一个cancellationToken令牌,这个令牌允许取消被阻塞的调用。如果不希望线程无限期的等待下去,且不希望从外部取消调用,就可以使用TryAdd(T)和TryTake(T)方法,在这些方法中,也可以指定一个超时值,它表示在调用失败之前应阻塞线程和等待的最长时间。
上述类中,有的实现了IProducerConsumerCollection<T>接口,IProducerConsumerCollection<T>接口提供了TryAdd(T) 和TryTake(T) 方法。TryAdd()方法尝试给集合添加一项,返回布尔值;TryTake()方法尝试从集合中删除并返回一个项。
以ConcurrentXXX形式的集合是线程安全的,如果某个动作不适用于线程的当前状态,它们就返回
false。在继续之前,总是需要确认添加或提取元素是否成功。不能相信集合 会完成任务。
BlockingCollection<T>是对实现了IProducerConsumerCollection<T>接口的任意类的修饰器 ,它默认使用ConcurrentQueue<T>类。还可以给构造函数传递任何其他实现了IProducerConsumerCollection<T>接口的类,例如,ConcurrentBag<T>和ConcurrentStack<T>。
下面将使用一个完整的示例说明并发集合的应用。该示例基于管道,即一个任务向一个集合类写入一些内容,同时另一个任务从该集合中读取内容。首先定义一个基本类:
1public class Info 2{ 3 public string Word { get; set; } 4 public int Count { get; set; } 5 public string Color { get; set; } 6 public override string ToString() 7 { 8 return $"Word:{Word},Count:{Count},Color:{Color}"; 9 } 10}
定义向控制台输出的类,使用同步来避免返回颜色错误的输出:
1public static class ColoredConsole 2{ 3 private static object syncOutput = new object(); 4 5 public static void WriteLine(string message) 6 { 7 lock (syncOutput) 8 { 9 Console.WriteLine(message); 10 } 11 } 12 13 public static void WriteLine(string message, string color) 14 { 15 lock (syncOutput) 16 { 17 Console.ForegroundColor = (ConsoleColor)Enum.Parse( 18 typeof(ConsoleColor), color); 19 Console.WriteLine(message); 20 Console.ResetColor(); 21 } 22 } 23}
接着定义具体的管道实现,详细说明请参加代码中的注释:
1public static class PipeLineStages 2{ 3 public static Task ReadFilenamesAsync(string path, BlockingCollection<string> output) 4 { 5 //第一个阶段 6 return Task.Factory.StartNew(() => 7 { 8 //读取文件名,并把它们添加到队列中 9 foreach (string filename in Directory.EnumerateFiles( 10 path, "*.cs", SearchOption.AllDirectories)) 11 { 12 //添加到BlockingCollection<T>中 13 output.Add(filename); 14 ColoredConsole.WriteLine($"stage 1: added {filename}"); 15 } 16 //通知所有读取器不应再等待集合中的任何额外项 17 output.CompleteAdding(); //该方法必不可少 18 },TaskCreationOptions.LongRunning); 19 } 20 21 public static async Task LoadContentAsync(BlockingCollection<string> input, 22 BlockingCollection<string> output) 23 { 24 //第二个阶段:从队列中读取文件名并加载它们的内容,并把内容写入到另一个队列 25 //如果不调用GetConsumingEnumerable()方法,而是直接迭代集合,将不会迭代之后添加的项 26 foreach (var filename in input.GetConsumingEnumerable()) 27 { 28 using (FileStream stream = File.OpenRead(filename)) 29 { 30 var reader = new StreamReader(stream); 31 string line = null; 32 while ((line = await reader.ReadLineAsync()) != null) 33 { 34 output.Add(line); 35 ColoredConsole.WriteLine("stage 2: added " + line); 36 } 37 } 38 } 39 output.CompleteAdding(); 40 } 41 42 public static Task ProcessContentAsync(BlockingCollection<string> input, 43 ConcurrentDictionary<string, int> output) 44 { 45 return Task.Factory.StartNew(() => 46 { 47 //第三个阶段:读取第二个阶段中写入内容的队列,并将结果写入到一个字典中 48 foreach (var line in input.GetConsumingEnumerable()) 49 { 50 string[] words = line?.Split(' ', ';', '\t', '{', '}', '(', ')', ':', ',', '"'); 51 if (words == null) continue; 52 foreach (var word in words?.Where(w => !string.IsNullOrEmpty(w))) 53 { 54 //如果键没有添加到字典中,第二个参数就定义应该设置的值 55 //如果 键已经存在于字典中,updateValueFactory就定义值的改变方式,++i 56 output.AddOrUpdate(key: word, addValue: 1, 57 updateValueFactory: (s, i) => ++i); 58 ColoredConsole.WriteLine("stage 3: added " + word); 59 } 60 } 61 }, TaskCreationOptions.LongRunning); 62 } 63 64 public static Task transFerContentAsync(ConcurrentDictionary<string, int> input, 65 BlockingCollection<Info> output) 66 { 67 //第四个阶段:从字典中读取内容,转换数据,将其写入队列中 68 return Task.Factory.StartNew(() => 69 { 70 foreach (var word in input.Keys) 71 { 72 int value; 73 if (input.TryGetValue(word, out value)) 74 { 75 var info = new Info { Word = word, Count = value }; 76 output.Add(info); 77 ColoredConsole.WriteLine("stage 4: added " + info); 78 } 79 } 80 output.CompleteAdding(); 81 }, TaskCreationOptions.LongRunning); 82 } 83 84 public static Task AddColorAsync(BlockingCollection<Info> input, 85 BlockingCollection<Info> output) 86 { 87 //第五个阶段:读取队列信息,并添加颜色信息,同时写入另一个队列 88 return Task.Factory.StartNew(() => 89 { 90 foreach (var item in input.GetConsumingEnumerable()) 91 { 92 if (item.Count > 40) 93 { 94 item.Color = "Red"; 95 } 96 else if (item.Count > 20) 97 { 98 item.Color = "Yellow"; 99 } 100 else 101 { 102 item.Color = "Green"; 103 } 104 output.Add(item); 105 ColoredConsole.WriteLine("Stage 5: added color " + item.Color + " to " + item); 106 } 107 output.CompleteAdding(); 108 }, TaskCreationOptions.LongRunning); 109 } 110 111 public static Task ShowContentAsync(BlockingCollection<Info> input) 112 { 113 //第六个阶段:显示最终的队列信息 114 return Task.Factory.StartNew(() => 115 { 116 foreach (var item in input.GetConsumingEnumerable()) 117 { 118 ColoredConsole.WriteLine("Stage 6:" + item, item.Color); 119 } 120 }, TaskCreationOptions.LongRunning); 121 } 122}
最终的调用代码为:
1public static async Task StartPipelineAsync() 2{ 3 var fileNames = new BlockingCollection<string>(); 4 //启动第一个阶段任务,读取文件名,并写入到队列fileNames中 5 Task t1 = PipeLineStages.ReadFilenamesAsync(@"../../", fileNames); 6 ColoredConsole.WriteLine("started stage 1"); 7 8 var lines = new BlockingCollection<string>(); 9 //启动第二个阶段任务,将队列中的文件名进行读取,获取该文件的内容并写入到lines队列中 10 Task t2 = PipeLineStages.LoadContentAsync(fileNames, lines); 11 ColoredConsole.WriteLine("started stage 2"); 12 13 var words = new ConcurrentDictionary<string, int>(); 14 //启动第三个阶段任务,读取lines队列中内容并写入到words中 15 Task t3 = PipeLineStages.ProcessContentAsync(lines, words); 16 17 //同时启动1、2、3三个阶段的任务,并发执行 18 await Task.WhenAll(t1, t2, t3); 19 ColoredConsole.WriteLine("stages 1,2,3 completed"); 20 21 var items = new BlockingCollection<Info>(); 22 //启动第四个阶段任务,将words字典中的数据进行读取,写入到items中 23 Task t4 = PipeLineStages.transFerContentAsync(words, items); 24 25 var coloredItems = new BlockingCollection<Info>(); 26 //启动第五个阶段任务,将items的数据进行读取和修改,将结果写入到coloredItems中 27 Task t5 = PipeLineStages.AddColorAsync(items, coloredItems); 28 29 //启动第六个阶段任务,将最终的结果显示出来 30 Task t6 = PipeLineStages.ShowContentAsync(coloredItems); 31 32 ColoredConsole.WriteLine("stages 4,5,6 started"); 33 //同时启动4、5、6三个阶段的任务 34 await Task.WhenAll(t4, t5, t6); 35 ColoredConsole.WriteLine("all sages finished"); 36}
处理位的集合
如果需要处理的数字有许多位,可以使用 BitArray类和BitVector32结构。这两种类型最重要的区别是:BitArray类可以重新设置大小,如果事先不知道需要的位数,可以使用BitArray类,它可以包含非常多的位。BitVector32结构是基于栈的,因此比较快。BitVector32结构仅包含32位,它们存储在一个整数中。
BitArray类
BitArray 类是一个引用类型,当它的构造函数传入的是int[]时,每一个int类型的整数都将使用32个连续位进行表示。
1public static void Run() 2{ 3 //创建一个包含8位的数组,其索引是0~7 4 var bits1 = new BitArray(8); 5 //把8位都设置为true 6 bits1.SetAll(true); 7 //把对应于1的位设置为false 8 bits1.Set(1, false); 9 bits1[5] = false; 10 bits1[7] = false; 11 DisplayBits(bits1); 12 Console.WriteLine(); 13 14 //Not()方法对所有的位取反 15 bits1.Not(); 16 DisplayBits(bits1); 17 Console.WriteLine(); 18 19 var bits2 = new BitArray(bits1); 20 bits2[0] = true; 21 bits2[1] = false; 22 bits2[4] = true; 23 DisplayBits(bits1); 24 Console.Write (" Or "); 25 DisplayBits(bits2); 26 Console.Write (" = "); 27 //比较两个数组上的同一个位置上的位,如果有一个为true,结果就为true 28 bits1.Or(bits2); 29 DisplayBits(bits1); 30 Console.WriteLine(); 31 32 DisplayBits(bits2); 33 Console.Write(" and "); 34 DisplayBits(bits1); 35 Console.Write (" = " ); 36 //如果两个数组上的同一个位置的位都为true,结果才为true 37 bits2.And(bits1); 38 DisplayBits(bits2); 39 Console.WriteLine(); 40 41 DisplayBits(bits1); 42 Console.Write (" xor "); 43 DisplayBits(bits2); 44 //比较两个数组上的同一个位置上的位,只有一个(不能是二个)设置为1,结果才是1 45 bits1.Xor(bits2); 46 Console.Write(" = "); 47 DisplayBits(bits1); 48 Console.WriteLine(); 49} 50 51public static void DisplayBits(BitArray bits) 52{ 53 foreach (bool bit in bits) 54 { 55 Console.Write(bit ? 1 : 0); 56 } 57}
BitVector32结构
如果事先知道需要的位数,留可以使用BitVector32 结构代替BitArray类。BitVector32结构效率较高,因为它是一个值类型,在整数栈上存储位。一个整数可以存储32位,如果需要更多的位,就可以使用多个BitVector32值或BitArray类。BitArray类可以根据需要增大,但BitVector32结构不能。
BitVector32结构中常用成员:
Data:以整数形式获取BitVector32的值。Data属性把BitVector32结构中的数据返回为整数。Item[]:BitVector32的值可以使用索引器设置,索引器是重载的——可以使用掩码或BitVector32.Section类型的片段来获取和设置值。CreateMask():该方法有多个重载版本,它是以静态方法,用于为访问BitVector32结构中的特定位创建掩码。CreateSection(Int16, BitVector32+Section):该方法有多个重载版本,也是一个静态方法,用于创建32位中的几个片段。
1public static void Run() 2{ 3 //使用默认构造函数创建一个BitVactor32结构,默认每一位都是false。 4 var bits1 = new BitVector32(); 5 //调用CreateMask()方法创建用来访问第一位的一个掩码,bit1被设置为1 6 int bit1 = BitVector32.CreateMask(); 7 //再次调用CreateMask()方法,并将一个掩码作为参数进行传递,返回第二位掩码 8 int bit2 = BitVector32.CreateMask(bit1); 9 int bit3 = BitVector32.CreateMask(bit2); 10 int bit4 = BitVector32.CreateMask(bit3); 11 int bit5 = BitVector32.CreateMask(bit4); 12 //使用掩码和索引器访问位矢量中的位,并设置值 13 bits1[bit1] = true; 14 bits1[bit2] = false; 15 bits1[bit3] = true; 16 bits1[bit4] = true; 17 bits1[bit5] = true; 18 Console.WriteLine(bits1); 19 bits1[0xabcdef] = true; 20 Console.WriteLine(bits1); 21 22 int received = 0x79abcdef; 23 //直接传入十六进制数来创建掩码 24 BitVector32 bits2 = new BitVector32(received); 25 Console.WriteLine(bits2); 26 27 //分割片段 28 BitVector32.Section sectionA = BitVector32.CreateSection(0xfff); 29 BitVector32.Section sectionB = BitVector32.CreateSection(0xff, sectionA); 30 BitVector32.Section sectionC = BitVector32.CreateSection(0xf, sectionB); 31 BitVector32.Section sectionD = BitVector32.CreateSection(0x7, sectionC); 32 BitVector32.Section sectionE = BitVector32.CreateSection(0x7, sectionD); 33 BitVector32.Section sectionF = BitVector32.CreateSection(0x3, sectionE); 34 Console.WriteLine("Section A:" + IntToBinaryString(bits2[sectionA], true)); 35 Console.WriteLine("Section B:" + IntToBinaryString(bits2[sectionB], true)); 36 Console.WriteLine("Section C:" + IntToBinaryString(bits2[sectionC], true)); 37 Console.WriteLine("Section D:" + IntToBinaryString(bits2[sectionD], true)); 38 Console.WriteLine("Section E:" + IntToBinaryString(bits2[sectionE], true)); 39 Console.WriteLine("Section F:" + IntToBinaryString(bits2[sectionF], true)); 40 41 42} 43 44public static string IntToBinaryString(int bits, bool removeTrailingZero) 45{ 46 var sb = new StringBuilder(32); 47 for (int i = 0; i < 32; i++) 48 { 49 if ((bits & 0x80000000) != 0) 50 { 51 sb.Append("1"); 52 } 53 else 54 { 55 sb.Append("0"); 56 } 57 bits = bits << 1; 58 } 59 string s = sb.ToString(); 60 if (removeTrailingZero) 61 { 62 return s.TrimStart('0'); 63 } 64 else 65 { 66 return s; 67 } 68}
可观察的集合
使用ObservableCollection<T> 集合类,可以在集合元素进行添加、删除、移动、修改等操作时,提供通知信息。它可以触发CollectionChanged 事件,可以在该事件中,进行相关的操作。
1public static void Run() 2{ 3 var data = new ObservableCollection<string>(); 4 data.CollectionChanged += Data_CollectionChanged; 5 data.Add("one"); 6 data.Add("tow"); 7 data.Insert(1, "Three"); 8 data.Remove("one"); 9} 10 11private static void Data_CollectionChanged(object sender, 12 System.Collections.Specialized.NotifyCollectionChangedEventArgs e) 13{ 14 Console.WriteLine("action" + e.Action.ToString()); 15 16 if (e.OldItems != null) 17 { 18 Console.WriteLine("OldStartingIndex:" + e.OldStartingIndex); 19 Console.WriteLine("old item(s):"); 20 foreach (var item in e.OldItems) 21 { 22 Console.WriteLine(item); 23 } 24 } 25 if (e.NewItems != null) 26 { 27 Console.WriteLine("NewStartingIndex:" + e.NewStartingIndex); 28 Console.WriteLine("new items:"); 29 foreach (var item in e.NewItems) 30 { 31 Console.WriteLine(item); 32 } 33 } 34 Console.WriteLine(); 35}
