C#笔记7:集合类
本章概要:
1:为什么要使用泛型集合
2:集合的线程安全
2.1:Syncronized静态方法和SyncRoot实例属性的用法
3:选择集合类
4:位集合
1:为什么要使用泛型集合
针对 .NET Framework 的 2.0 版和更高版本的应用程序应当使用 System.Collections.Generic 命名空间中的泛型集合类,与对应的非泛型类相比,这些类提供了更高的类型安全性和效率。为什么这么说呢,看下面的例子:
// The .NET Framework 1.1 way to create a list: System.Collections.ArrayList list1 = new System.Collections.ArrayList(); list1.Add(3); list1.Add(105); System.Collections.ArrayList list2 = new System.Collections.ArrayList(); list2.Add("It is raining in Redmond."); list2.Add("It is snowing in the mountains.");
添加到 ArrayList 中的任何引用或值类型都将隐式地向上强制转换为 Object。 如果项是值类型,则必须在将其添加到列表中时进行装箱操作,在检索时进行取消装箱操作。 强制转换以及装箱和取消装箱操作都会降低性能;在必须对大型集合进行循环访问的情况下,装箱和取消装箱的影响非常明显。
另一个限制是缺少编译时类型检查;因为 ArrayList 会将所有项都强制转换为 Object,所以在编译时无法防止客户端代码执行类似如下的操作:
System.Collections.ArrayList list = new System.Collections.ArrayList(); // Add an integer to the list. list.Add(3); // Add a string to the list. This will compile, but may cause an error later. list.Add("It is raining in Redmond."); int t = 0; // This causes an InvalidCastException to be returned. foreach (int x in list) { t += x; }
ArrayList 和其他相似类真正需要的是:客户端代码基于每个实例指定这些类要使用的具体数据类型的方式。 这样将不再需要向上强制转换为 T:System.Object,同时,也使得编译器可以进行类型检查。 换句话说,ArrayList 需要一个类型参数。 这正是泛型所能提供的。
2:集合的线程安全
.NET Framework 1.0 中的集合,如常用的 ArrayList 和 Hashtable通过 Synchronized 属性(此属性返回与集合有关的线程安全包装)提供某种线程安全性。该包装的工作原理是:对每个添加或移除操作锁定整个集合。因此,每个尝试访问集合的线程必须一直等待,直到轮到它来获取锁。这是无法进行伸缩的,并且对于大型集合而言,将会导致性能显著降低。此外,这一设计并不能完全防止出现争用情况。
在 System.Collections.Generic 命名空间中 .NET Framework 2.0 中引入的集合类。这些集合类包括 List<(Of <(T>)>)、Dictionary<(Of <(TKey, TValue>)>) 等。与 .NET Framework 1.0 类相比,这些类提供的类型安全性和性能会更高。不过,.NET Framework 2.0 集合类不提供任何线程同步;当同时在多个线程上添加或移除项时,用户代码必须提供所有同步。
枚举整个集合本质上不是一个线程安全的过程。若要确保枚举过程中的线程安全性,可以在整个枚举过程中锁定集合。若要允许多个线程访问集合以进行读写操作,则必须实现自己的同步或使用 System.Collections.Concurrent 命名空间中的线程安全集合类之一。System.Collections.Concurrent..::.ConcurrentQueue<(Of <(T>)>) 和 System.Collections.Concurrent..::.ConcurrentStack<(Of <(T>)>) 类在枚举元素之前将为元素拍摄快照,以防止转变成另一个线程上的集合。System.Collections.Concurrent..::.ConcurrentDictionary<(Of <(TKey, TValue>)>) 类不拍摄快照。
System.Collections.Concurrent..::.BlockingCollection<(Of <(T>)>) 类提供一个名为 GetConsumingEnumerable 的枚举器方法,该方法通过在枚举项时将项从集合中移除来转变集合。
那么,说到底,到底如何来实现自己的线程安全呢。可以提供一个static object synObj = new object(); 然后lock(synObj)。
2.1:Syncronized静态方法和SyncRoot实例属性的用法
在许多的集合类中,都能看到Syncronized静态方法和SyncRoot实例属性,这两个单词的sync就显而易见的说明了是用来实现同步的,那么,它们内在的机制是什么?
Syncronized方法用来创造一个新的对象的线程安全包装,例如:
HashTable ht = HashTable.Syncronized(new HashTable());
使用这个方法创造的ht对象保证了在多线程环境下,进行对象的添加、删除和解析的时候,系统自动为它创建锁定区域,这样,省去了手工进行线程安全的设置。
但是,使用这个方法并不能保证枚举的同步,例如,一个线程正在删除或添加集合项,而另一个线程同时进行枚举,这时枚举将会抛出异常。所以,在枚举的时候,你必须明确锁定这个集合。这是,我们要锁定并不是集合类对象本身,我们要锁定的是它的SyncRoot对象,这是为什么呢?
举例来说,一个集合类将数据存放在某些类型的内部数据结构,如果,这个类给予这个外面对这个数据结构的访问权,那么仅仅锁定集合对象是无用的,当然,大部分情况下,返回的SyncRoot就是它本身,但是,还是有一些例外,所以,我们如果我们并不能确定集合对象和SyncRoot之间的关系,那么我们还是锁定SyncRoot为最佳选择;
lock(ht.SyncRoot)
{
//你可以安全的对ht进行枚举了
}
3:选择集合类
考虑以下问题:
-
您是否需要一个序列列表,其中的元素通常在检索其值后被放弃?
-
如果需要,那么在需要先进先出 (FIFO) 行为时请考虑使用 Queue 类或 Queue<(Of <(T>)>) 泛型类。在需要后进先出 (LIFO) 行为时请考虑使用 Stack 类或 Stack<(Of <(T>)>) 泛型类。若要从多个线程进行安全访问,请使用并发版本 ConcurrentQueue<(Of <(T>)>) 和 ConcurrentStack<(Of <(T>)>)。
-
如果不需要,请考虑使用其他集合。
-
-
是否需要以某种顺序访问元素,例如 FIFO、LIFO 或随机访问?
-
Queue 类和 Queue<(Of <(T>)>) 或 ConcurrentQueue<(Of <(T>)>) 泛型类提供 FIFO 访问。有关更多信息,请参见何时使用线程安全集合。
-
Stack 类和 Stack<(Of <(T>)>) 或 ConcurrentStack<(Of <(T>)>) 泛型类提供 LIFO 访问。有关更多信息,请参见何时使用线程安全集合。
-
LinkedList<(Of <(T>)>) 泛型类允许从开头到末尾或从末尾到开头按顺序访问。
-
-
是否需要通过索引访问每一元素?
-
ArrayList 和 StringCollection 类以及 List<(Of <(T>)>) 泛型类通过元素的从零开始的索引提供对元素的访问。
-
Hashtable、SortedList、ListDictionary 和 StringDictionary 类以及 Dictionary<(Of <(TKey, TValue>)>) 和 SortedDictionary<(Of <(TKey, TValue>)>) 泛型类通过元素的键提供对元素的访问。
-
NameObjectCollectionBase 和 NameValueCollection 类以及 KeyedCollection<(Of <(TKey, TItem>)>) 和 SortedList<(Of <(TKey, TValue>)>) 泛型类通过其元素的从零开始的索引或者通过其元素的键提供对元素的访问。
-
-
每一元素将包含一个值、一个键和一个值的组合还是一个键和多个值的组合?
-
一个值:使用任何基于 IList 接口或 IList<(Of <(T>)>) 泛型接口的集合。
-
一个键和一个值:使用任何基于 IDictionary 接口或 IDictionary<(Of <(TKey, TValue>)>) 泛型接口的集合。
-
带有嵌入的键的一个值:使用 KeyedCollection<(Of <(TKey, TItem>)>) 泛型类。
-
一个键和多个值:使用 NameValueCollection 类。
-
-
是否需要用与输入元素方式不同的方式对元素排序?
-
Hashtable 类按其元素的哈希代码对元素排序。
-
SortedList 类以及 SortedDictionary<(Of <(TKey, TValue>)>) 和 SortedList<(Of <(TKey, TValue>)>) 泛型类根据 IComparer 接口和 IComparer<(Of <(T>)>) 泛型接口的实现按键对元素排序。
-
ArrayList 提供 Sort 方法,该方法接受 IComparer 实现作为参数。其对应的泛型类(List<(Of <(T>)>) 泛型类)提供 Sort 方法,该方法接受 IComparer<(Of <(T>)>) 泛型接口的实现作为参数。
-
-
是否需要信息的快速搜索和检索?
-
对于小集合(10 项或更少),ListDictionary 比 Hashtable 快。Dictionary<(Of <(TKey, TValue>)>) 泛型类提供比 SortedDictionary<(Of <(TKey, TValue>)>) 泛型类更快的查找。多线程实现是 ConcurrentDictionary<(Of <(TKey, TValue>)>)。ConcurrentBag<(Of <(T>)>) 提供针对未排序的数据的快速多线程插入。有关这两种多线程类型的更多信息,请参见何时使用线程安全集合。
-
-
是否需要只接受字符串的集合?
-
StringCollection(基于 IList)和 StringDictionary(基于 IDictionary)都位于 System.Collections.Specialized 命名空间中。
-
此外,通过为泛型类型参数指定 String 类,可以使用 System.Collections.Generic 命名空间中的任何泛型集合类作为强类型字符串集合。
-
4:位集合
位集合是其元素为位标志的集合。 因为每一元素都是一位,而不是一个对象,所以这些集合的行为与其他集合稍有不同。
位集合非常适合用来存储布尔值类型。
BitArray 类是一个集合类,该类中的容量始终与计数相同。 可通过增加 Length 属性将元素添加到 BitArray 中;通过减少 Length 属性来删除元素。 BitArray 类提供一些在其他集合中未提供的方法,包括允许使用筛选器一次修改多个元素的那些方法,例如 And、Or、Xor、Not 和 SetAll。
BitVector32 类是一种结构,它提供与 BitArray 相同的功能,但性能更快。 BitVector32 的性能较快的原因是:它是值类型,因而在堆栈上分配空间;而 BitArray 是引用类型,因而在堆上分配空间。
BitVector32 可存储正好 32 位的数据,而 BitArray 可存储可变位数的数据。 BitVector32 既可存储位标志又可存储小整数,因此很适合不向用户公开的数据。 但是,如果所需位标志的数目未知、可变或大于 32,则可用 BitArray 取而代之。
用默认构造函数创建一个BitVector32,其中所有的32位都初始化为false。构造函数传入的值如果为1,则结果会再加1,相当于结果没有变化,所以还是false,如果传入-1来表示结果做了取反的变化,其结果则为true。
练习:
1.You are writing a custom dictionary. The custom-dictionary class is named MyDictionary. You need to ensure
that the dictionary is type safe. Which code segment should you use?
A. class MyDictionary : Dictionary<string, string>
B. class MyDictionary : HashTable
C. class MyDictionary : IDictionary
D. class MyDictionary { ... }
Dictionary<string, string> t = new Dictionary<string, string>();
MyDictionary dictionary = (MyDictionary)t;
Answer: A,因为因为泛型集合Dictionary<string, string> 可以进行编译器类型检查,是类型安全的。
2.You are developing an application to assist the user in conducting electronic surveys. The survey consists of 25
true-or-false questions. You need to perform the following tasks: Initialize each answer to true.Minimize the
amount of memory used by each survey. Which storage option should you choose?
A. BitVector32 answers = new BitVector32(1);
B. BitVector32 answers = new BitVector32(-1);
C. BitArray answers = new BitArray (1);
D. BitArray answers = new BitArray(-1);
Answer: B
3.You are writing a method that returns an ArrayList named al. You need to ensure that changes to the ArrayList
are performed in a thread-safe manner. Which code segment should you use?
A. ArrayList al = new ArrayList();lock (al.SyncRoot){ return al;}
B. ArrayList al = new ArrayList();lock (al.SyncRoot.GetType()){ return al;}
C. ArrayList al = new ArrayList();Monitor.Enter(al);Monitor.Exit(al);return al;
D. ArrayList al = new ArrayList();ArrayList sync_al = ArrayList.Synchronized(al);return sync_al;
Answer: D,因为使用Synchronized方法可以返回一个线程安全的集合,A,B,C选项中的al对象并不是一个线程安全的集合。
4.You are creating an undo buffer that stores data modifications. You need to ensure that the undo functionality
undoes the most recent data modifications first. You also need to ensure that the undo buffer permits the storage of strings only. Which code segment should you use?
A. Stack<string> undoBuffer = new Stack<string>();
B. Stack undoBuffer = new Stack();
C. Queue<string> undoBuffer = new Queue<string>();
D. Queue undoBuffer = new Queue();
Answer: A ,Stack<string> 为先进后出,并且是通过编译器检查类型string的集合,先进后出保证了撤销的顺序,后进的先撤销。
5.You need to create a method to clear a Queue named q. Which code segment should you use?
A. foreach (object e in q) { q.Dequeue();}
B. foreach (object e in q) { Enqueue(null);}
C. q.Clear();
D. q.Dequeue();
Answer: C
6.You need to select a class that is optimized for key-based item retrieval from both small and large collections.
Which class should you choose?
A. OrderedDictionary class
B. HybridDictionary class
C. ListDictionary class
D. Hashtable class
Answer: B
备注:建议将HybridDictionary用于字典中的元素数量未知的情况。它利用了 ListDictionary 处理小集合时性能改善的优点,同时也可灵活地切换到处理较大集合时能力比 ListDictionary 更好的 Hashtable。
7:List<T> 相比ArrayList有什么优点。
答:添加到 ArrayList 中的任何引用或值类型都将隐式地向上强制转换为 Object。 如果项是值类型,则必须在将其添加到列表中时进行装箱操作,在检索时进行取消装箱操作。强制转换以及装箱和取消装箱操作都会降低性能;在必须对大型集合进行循环访问的情况下,装箱和取消装箱的影响非常明显。List<T>使用泛型指定了集合的类型,所以它是类型安全的,故不存在装箱和拆箱,效率更高。
8:对集合来说,什么是线程安全。不要照抄MSDN,举例说明。
答:如果多个线程同时需要访问同一个集合,那么这个集合需要当作互斥资源来对待,该集合同时只允许一个访问者对其进行访问,否者就不是一个线程安全的访问。
集合的线程安全分两类,一类是传统集合,如ArrayList,举例如下:
ArrayList list = new ArrayList();
void ThreadSafeFun()
{
for (int i = 0; i < 50; i++)
{
list.Add("abc");
}
for (int i = 0; i < 50; i++)
{
Thread tadd = new Thread(this.Add);
tadd.Start();
Thread tdel = new Thread(this.Del);
tdel.Start();
}
}
void Add()
{
while (true)
{
lock (list.SyncRoot)
{
list.Add("abc");
}
Thread.Sleep(50);
}
}
void Del()
{
while (true)
{
lock (list.SyncRoot)
{
if (list.Count > 0)
{
list.RemoveAt(0);
}
}
Thread.Sleep(50);
}
}
一类是泛型集合,举例如下:
List<string> tList = new List<string>();
object lockObj = new object();
void TThreadSafeFun()
{
for (int i = 0; i < 50; i++)
{
tList.Add("abc");
}
for (int i = 0; i < 50; i++)
{
Thread tAdd = new Thread(this.TAdd);
tAdd.Start();
Thread tDel = new Thread(this.TDel);
tDel.Start();
}
}
void TAdd()
{
while (true)
{
lock (lockObj)
{
tList.Add("abc");
}
Thread.Sleep(50);
}
}
void TDel()
{
while (true)
{
lock (lockObj)
{
if (tList.Count > 0)
{
tList.RemoveAt(0);
}
}
Thread.Sleep(50);
}
}
9:写代码说明,在删除ArrayList中其中一项的时候,如何锁定集合。
答:查看第8题;
10:写代码说明,在删除List<T>其中一项的时候,如何锁定集合。
答:查看第8题;
11:Syncronized静态方法和SyncRoot实例属性的区别。
答:查看本文描述