.NET Core提供了一些基本的具体集合类,这些类实现了一系列集合接口。和集合接口一样,每一种集合类型都可以选择使用泛型或非泛型进行实现。在灵活性和性能方面,泛型类更具有优势,而它们的非泛型冗余版本则是为了向后兼容。这与集合接口不同,非泛型集合接口在某些情形下是有其作用的。而泛型List类是最常用的。
List<T>和ArrayList
泛型List
和非泛型ArrayList
类都提供了一种可动态调整大小的对象数组实现,是集合类中使用最广泛的。ArrayList
实现了IList
,而List<T>
既实现了IList
又实现了IList<T>
。与数组不同,所有的接口都是公开实现的,而且其方法如Add和Remove也都是公开可用的。
List<T>
和ArrayList
在内部都维护着一个对象数组,并在超出容量时替换为一个更大的数组。在集合中追加元素的效率很高(因为数组末尾一般都有空闲的位置),而插入元素的速度会慢一些(因为插入位置之后的所有元素都必须向后移动才能留出插入空间),移除元素同样速度较慢(尤其是移除起始元素时)。
List<T>
和ArrayList
具有可以接受现有集合的构造器,它们会将现有集合中的每一个元素都复制到新的List<T>
或ArrayList
中。
public class List<T> : ICollection<T>, IEnumerable<T>, IEnumerable, IList<T>,
IReadOnlyCollection<T>, IReadOnlyList<T>, ICollection, IList
{
public List();
public List(IEnumerable<T> collection);
public List(int capacity);
// 追加、插入
public void Add(T item);
public void AddRange(IEnumerable<T> collection);
public void Insert(int index, T item);
public void InsertRange(int index, IEnumerable<T> collection);
// 删除
public bool Remove(T item);
public void RemoveAt(int index);
public void RemoveRange(int index, int count);
public int RemoveAll(Predicate<T> match);
// 索引
public T this[int index] { get; set; }
public List<T> GetRange(int index, int count);
public List<T>.Enumerator GetEnumerator();
// 复制、转换
public T[] ToArray();
public void CopyTo(T[] array, int arrayIndex);
public void CopyTo(T[] array);
public void CopyTo(int index, T[] array, int arrayIndex, int count);
public ReadOnlyCollection<T> AsReadOnly();
public List<TOutput> ConvertAll<TOutput>(Converter<T, TOutput> converter);
public void Reverse(int index, int count);
public void Reverse();
public int Capacity { get; set; }
public void TrimExcess();
public void Clear();
}
除了上述成员,List<T>
还提供了Array类中所有搜索和排序方法的实例版本。
List<string> words = new List<string>();
words.Add ("melon");
words.Add ("avocado");
words.AddRange (new[] { "banana", "plum" } );
words.Insert (0, "lemon"); // 开头插入
words.InsertRange (0, new[] { "peach", "nashi" }); // 开头插入
words.Remove ("melon");
words.RemoveAt (3); // 删除第四个
words.RemoveRange (0, 2); // 从第一个开始删除2个
words.RemoveAll (s => s.StartsWith ("n")); // 删除开头有n的
Console.WriteLine (words [0]); // first word
Console.WriteLine (words [words.Count - 1]); // last word
foreach (string s in words) Console.WriteLine (s); // all words
List<string> subset = words.GetRange (1, 2); // 2nd->3rd words
string[] wordsArray = words.ToArray();
// 将前两个元素复制到现有数组的末尾
string[] existing = new string [1000];
words.CopyTo (0, existing, 998, 2);
List<string> upperCaseWords = words.ConvertAll (s => s.ToUpper());
List<int> lengths = words.ConvertAll (s => s.Length);
使用非泛型的ArrayList类往往需要进行烦琐的转换,如下所示:
ArrayList al = new ArrayList();
al.Add ("hello");
string first = (string) al [0];
string[] strArr = (string[]) al.ToArray (typeof (string));
编译器无法验证这些转换,所以像int first = (int)al[0];
这个例子会在运行时出错。
如果导入了System.Linq
命名空间,那么可以先调用Cast
再调用ToList
将一个ArrayList
转换为一个泛型List
。Cast和ToList都是System.Linq.Enumerable类的扩展方法。
ArrayList al = new ArrayList();
al.AddRange (new[] {1,5,9});
List<int> list = al.Cast<int>().ToList();
LinkedList<T>
LinkedList<T>
是一个泛型的双向链表。
双向链表是一系列相互引用的节点,每一个节点都引用前一个节点、后一个节点以及实际存储的数据元素。它的主要优点是元素总能够高效地插入到链表的任意位置,因为插入节点只需要创建一个新节点,然后修改引用值。然而查找插入节点的位置会比较慢,因为链表本身并没有直接索引的内在机制。必须遍历每一个节点,并且无法执行二分搜索。
LinkedList<T>
实现了IEnumerable<T>
和ICollection<T>
(及它们的非泛型版本),但是没有实现IList<T>
,因为它不支持索引访问。链表节点是通过下面的类实现的:
public sealed class LinkedListNode<T>
{
public LinkedListNode(T value);
public LinkedList<T>? List { get; }
public LinkedListNode<T>? Next { get; }
public LinkedListNode<T>? Previous { get; }
public T Value { get; set; }
public ref T ValueRef { get; }
}
当添加一个节点时,可以指定它相对于其他节点的位置,或者指定它位于链表的开始/结束位置。可以使用以下方法为LinkedList<T>
添加节点:
public void AddAfter(LinkedListNode<T> node, LinkedListNode<T> newNode);
public LinkedListNode<T> AddAfter(LinkedListNode<T> node, T value);
public void AddBefore(LinkedListNode<T> node, LinkedListNode<T> newNode);
public LinkedListNode<T> AddBefore(LinkedListNode<T> node, T value);
public void AddFirst(LinkedListNode<T> node);
public LinkedListNode<T> AddFirst(T value);
public void AddLast(LinkedListNode<T> node);
public LinkedListNode<T> AddLast(T value);
LinkedList<T>
内部的一些字段记录了链表元素的个数以及链表的头部和尾部。可以通过下面的公有属性访问这些信息:
public LinkedListNode<T>? Last { get; }
public LinkedListNode<T>? First { get; }
public int Count { get; }
也支持以下搜索方法:
public bool Contains(T value);
public LinkedListNode<T>? Find(T value);
public LinkedListNode<T>? FindLast(T value)
最后,可以将LinkedList<T>
的元素复制到一个数组中,以便支持索引处理。Linked-List<T>
也支持foreach语句所需的枚举器:
public void CopyTo(T[] array, int index);
public LinkedList<T>.Enumerator GetEnumerator();
以下代码演示了LinkedList<String>
的用法:
var tune = new LinkedList<string>();
tune.AddFirst ("do"); // do
tune.AddLast ("so"); // do - so
tune.AddAfter (tune.First, "re"); // do - re- so
tune.AddAfter (tune.First.Next, "mi"); // do - re - mi- so
tune.AddBefore (tune.Last, "fa"); // do - re - mi - fa- so
tune.RemoveFirst(); // re - mi - fa - so
tune.RemoveLast(); // re - mi - fa
LinkedListNode<string> miNode = tune.Find ("mi");
tune.Remove (miNode); // re - fa
tune.AddFirst (miNode);
Queue<T>和Queue
Queue<T>
和Queue
是先进先出(FIFO)的数据结构。
它们提供了Enqueue(将一个元素添加到队列末尾)和Dequeue(取出并删除队列的第一个元素)方法。它们还包括一个只返回而不删除队列第一个元素的Peek方法以及一个Count属性(可在取出元素前检查该元素是否存在于队列中)。
public class Queue<T> : IEnumerable<T>, IEnumerable, IReadOnlyCollection<T>, ICollection
{
public Queue();
public Queue(IEnumerable<T> collection);
public Queue(int capacity);
public int Count { get; }
public void Clear();
public bool Contains(T item);
public void CopyTo(T[] array, int arrayIndex);
public T Dequeue(); // 取出并删除队列的第一个元素
public void Enqueue(T item); // 将一个元素添加到队列末尾
public Queue<T>.Enumerator GetEnumerator();
public T Peek();
public T[] ToArray();
public void TrimExcess();
}
虽然队列是可枚举的,但是它并没有实现IList<T>
和IList
,因为我们无法直接通过索引访问其成员。然而,可以使用ToArray
方法将其中的元素复制到一个数组中,而后进行随机访问:
var q = new Queue<int>();
q.Enqueue (10);
q.Enqueue (20);
int[] data = q.ToArray(); // 转为一个数组
Console.WriteLine (q.Count); // "2"
Console.WriteLine (q.Peek()); // "10"
Console.WriteLine (q.Dequeue()); // "10"
Console.WriteLine (q.Dequeue()); // "20"
Console.WriteLine (q.Dequeue()); // 抛出一个异常(队列为空)
队列的实现和泛型List类相似,都在内部使用了一个可根据需要进行大小调整的数组。队列具有一个直接指向头部和尾部元素的索引,因此其入队和出队的操作速度非常快。
Stack<T>和Stack
Stack<T>
和Stack
是后进先出(LIFO)的数据结构。
它们提供了Push(向栈的顶部添加一个元素)和Pop(从栈顶取出并删除一个元素)方法,也提供了一个只读取而不删除元素的Peek方法、Count属性,以及可以导出数据并进行随机访问的ToArray方法:
public class Stack<T> : IEnumerable<T>, IEnumerable, IReadOnlyCollection<T>, ICollection
{
public Stack();
public Stack(IEnumerable<T> collection);
public Stack(int capacity);
public int Count { get; }
public void Clear();
public bool Contains(T item);
public void CopyTo(T[] array, int arrayIndex);
public Stack<T>.Enumerator GetEnumerator();
public T Peek(); // 从栈顶取出一个元素
public T Pop(); // 从栈顶取出并删除一个元素
public void Push(T item); // 向栈的顶部添加一个元素
public T[] ToArray();
public void TrimExcess();
}
使用方法:
var s = new Stack<int>();
s.Push(1);
s.Push(2);
s.Push(3);
Console.WriteLine(s.Count); // 输出 3
Console.WriteLine(s.Peek()); // 1,2,3 输出 3
Console.WriteLine(s.Pop()); // 1,2 输出 3
Console.WriteLine(s.Pop()); // 1 输出 2
Console.WriteLine(s.Pop()); // <empty> 输出 1
Console.WriteLine(s.Pop()); // 报错
栈和Queue<T>
、List<T>
一样,其内部也是用一个可以根据需要调整大小的数组实现的。
BitArray
BitArray是一个压缩保存bool值的可动态调整大小的集合。
由于它使用一位(而不是一般的一字节)来存储一个bool值,因此比起简单的bool数组和以bool为类型参数的泛型List,BitArray具有更高的内存使用效率。BitArray的索引器可以读写每一位:
var bits = new BitArray(2);
bits[1] = true;
它提供了四种按位操作的运算符方法:And、Or、Xor和Not。除最后一个方法外,其他的方法都接受一个BitArray作为参数:
bits.Xor(bits); // 与自身按位异或
Console.WriterLine(bits[1]); // false
HashSet<T>和SortedSet<T>
HashSet<T>
和SortedSet<T>
分别是在.NET Framework 3.5和4.0版本新增的泛型集合类型。
它们都具有以下特点:
- 它们的Contains方法均使用散列查找,因而执行速度很快。
- 它们都不保存重复元素,并且都忽略添加重复值的请求。
- 无法根据位置访问元素。
SortedSet<T>
按一定顺序保存元素,而HashSet<T>
则不是。
HashSet<T>
是通过使用只存储键的散列表实现的,而SortedSet<T>
则是通过一个红黑树实现的。
两个集合都实现ICollection<T>
并提供了一些常用的方法,例如Contains、Add和Remove。此外还提供了一个基于谓词的删除元素的方法:RemoveWhere。
var letters = new HashSet<char> ("the quick brown fox");
// 是否包含某些成员
Console.WriteLine (letters.Contains ('t')); // true
Console.WriteLine (letters.Contains ('j')); // false
真正有意思的方法是集合的各种操作。以下集合操作是破坏性的,即它们会修改集合:
// 将第二个集合的所有元素添加到原始集合上(不包含重复元素)
public void UnionWith(IEnumerable<T> other);
// 将不属于两个集合共有的元素删除
public void IntersectWith(IEnumerable<T> other);
// 删除源集合中的指定元素
public void ExceptWith(IEnumerable<T> other);
// 删除两个集合中共有的元素
public void SymmetricExceptWith(IEnumerable<T> other);
下面的方法仅仅对集合进行查询,因而是非破坏性的:
public bool IsProperSubsetOf(IEnumerable<T> other);
public bool IsProperSupersetOf(IEnumerable<T> other);
public bool IsSubsetOf(IEnumerable<T> other);
public bool IsSupersetOf(IEnumerable<T> other);
public bool Overlaps(IEnumerable<T> other);
public bool SetEquals(IEnumerable<T> other);
HashSet<T>
和SortedSet<T>
均实现了IEnumerable<T>
,因此也可以使用另外一种Set类型或者集合类型作为集合操作方法的参数。SortedSet<T>
拥有HashSet<T>
的所有成员。除此之外,还有如下的成员:
public virtual SortedSet<T> GetViewBetween(T? lowerValue, T? upperValue);
public IEnumerable<T> Reverse();
public T? Min { get; }
public T? Max { get; }
SortedSet<T>
的构造器还可接受一个可选的IComparer<T>
参数(而非一个相等比较器)。
下面的例子会把相同的字符加载到SortedSet<char>
中,获得f与j之间的字符。
var letters = new SortedSet<char> ("the quick brown fox");
foreach (char c in letters)
Console.Write (c); // bcefhiknoqrtuwx
foreach (char c in letters.GetViewBetween ('f', 'i'))
Console.Write (c); // fhi