教程(数组方法类型元素字典)「数组元素数据类型」

.NET 提供了一组用于存储和管理对象集合的标准类型
其中包括可调整大小的列表、链表、排序和未排序的字典以及数组
其中,只有数组构成 C# 语言的一部分;其余集合只是您像其他任何集合一样实例化的类
我们可以将集合的 .NET BCL 中的类型分为以下:定义标准收集协议的接口即用型集合类(列表、字典等)用于编写特定于应用程序的集合的基类本章涵盖了这些类别中的每一个,并增加了一个关于用于确定元素相等性和顺序的类型的部分
集合命名空间如下所示:Namespace包含系统.集合非泛型集合类和接口系统.集合.专业强类型非泛型集合类System.Collections.Generic泛型集合类和接口System.Collections.ObjectModel自定义集合的代理和基础System.Collections.Parallel线程安全集合(请参阅)列举在计算中,有许多不同类型的集合,从简单的数据结构(如数组或链表)到更复杂的数据结构(如红/黑树和哈希表)
尽管这些数据结构的内部实现和外部差异很大,但遍历集合内容的能力几乎是普遍的需求
.NET BCL 通过一对接口(IEnumerable、IEnumerator 及其泛型对应项)支持这一需求,这些接口允许不同的数据结构公开通用遍历 API
这些是 中所示的一组更大的集合接口的一部分
集合接口IEnumerable 和 IEnumeratorIEnumerator 接口定义基本的低级协议,通过该协议以只进方式遍历或枚举集合中的元素
其声明如下:public interface IEnumerator{ bool MoveNext(); object Current { get; } void Reset();}MoveNext 将当前元素或“光标”前进到下一个位置,如果集合中没有更多元素,则返回 false
Current 返回当前位置的元素(通常从对象强制转换为更具体的类型)
在检索第一个元素之前必须调用 MoveNext,这是为了允许空集合
Reset 方法(如果实现)将移回起始位置,从而允许再次枚举集合
重置主要用于组件对象模型 (COM) 互操作性;通常避免直接调用它,因为它不是普遍支持的(并且是不必要的,因为实例化新的枚举器通常同样容易)
集合通常不枚举器;相反,它们接口 IEnumerable 提供枚举器:public interface IEnumerable{ IEnumerator GetEnumerator();}通过定义重新调整枚举器的单个方法,IEnumerable 提供了灵活性,因为迭代逻辑可以复制到另一个类
此外,这意味着多个使用者可以一次枚举集合,而不会相互干扰
您可以将 IEnumerable 视为“IEnumeratorProvider”,它是集合类实现的最基本的接口
以下示例说明了 IEnumerable 和 IEnumerator 的低级用法:string s = "Hello";// Because string implements IEnumerable, we can call GetEnumerator():IEnumerator rator = s.GetEnumerator();while (rator.MoveNext()){ char c = (char) rator.Current; Console.Write (c + ".");}// Output: H.e.l.l.o.但是,很少以这种方式直接调用枚举器上的方法,因为 C# 提供了一个语法快捷方式:foreach 语句
这是使用 foreach 重写的相同示例:string s = "Hello"; // The String class implements IEnumerableforeach (char c in s) Console.Write (c + ".");IEnumerable<T> 和 IEnumerator<T>IEnumerator 和 IEnumerable 几乎总是与其扩展泛型版本一起实现:public interface IEnumerator<T> : IEnumerator, IDisposable{ T Current { get; }}public interface IEnumerable<T> : IEnumerable{ IEnumerator<T> GetEnumerator();}通过定义 Current 和 GetEnumerator 的类型化版本,这些接口增强了静态类型安全性,避免了使用值类型元素装箱的开销,并且对使用者来说更方便
数组自动实现 IEnumerable<T>(其中 T 是数组的成员类型)
由于改进了静态类型安全性,使用字符数组调用以下方法将生成编译时错误:void Test (IEnumerable<int> numbers) { ... }集合类的标准做法是公开 IEnumerable<T>同时通过显式接口实现“隐藏”非泛型 IEnumerable
这样,如果您直接调用 GetEnumerator(),您将获得类型安全的泛型 IEnumerator<T>
但是,有时由于向后兼容性的原因,此规则会被破坏(泛型在 C# 2.0 之前不存在)
一个很好的例子是数组 - 它们必须返回非泛型(很好的表达方式是“经典”)IEnumerator,以防止破坏早期代码
要获取泛型 IEnumerator<T> ,必须强制转换以公开显式接口:int[] data = { 1, 2, 3 };var rator = ((IEnumerable <int>)data).GetEnumerator();幸运的是,由于 foreach ,您很少需要编写此类代码
IEnumerable<T> 和 IDisposableIEnumerator<T> 继承自 IDisposable
这允许枚举器保存对资源(如数据库连接)的引用,并确保在枚举完成(或中途放弃)时释放这些资源
foreach 语句可识别此详细信息并翻译以下内容foreach (var element in somethingEnumerable) { ... }进入逻辑等价物:using (var rator = somethingEnumerable.GetEnumerator()) while (rator.MoveNext()) { var element = rator.Current; ... }何时使用非泛型接口鉴于泛型集合接口(如 IEnumerable<T> 的额外类型安全性,问题出现了:您是否需要使用非泛型 IEnumerable(或 ICollection 或 IList)?在 IEnumerable 的情况下,您必须将此接口与 IEnumerable<T> 一起实现,因为后者源自前者
但是,真正从头开始实现这些接口的情况非常罕见:在几乎所有情况下,都可以采用使用迭代器方法、Collection<T> 和 LINQ 的更高级别的方法
那么,作为消费者呢?在几乎所有情况下,您都可以完全使用通用接口进行管理
不过,非泛型接口偶尔仍然很有用,因为它们能够跨所有元素类型的集合提供类型统一
例如,以下方法以计算任何集合中的元素:public static int Count (IEnumerable e){ int count = 0; foreach (object element in e) { var subCollection = element as IEnumerable; if (subCollection != null) count += Count (subCollection); else count++; } return count;}由于 C# 提供泛型接口的协方差,因此让此方法改为接受 IEnumerable<object> 似乎是有效的
但是,对于值类型元素和未实现 IEnumerable<T 的旧集合,这将失败> - 一个例子是 Windows 窗体中的 ControlCollection
(稍微切线,您可能已经注意到我们示例中的潜在错误:引用将导致无限递归并使方法崩溃
我们可以通过使用 HashSet 最容易地解决这个问题(参见)
使用块确保处置 - 有关IDisposable的更多信息,请参阅
实现枚举接口出于以下一个或多个原因,您可能希望实现 IEnumerable 或 IEnumerable<T>:支持 foreach 语句与任何期望标准集合的内容进行互操作满足更复杂的集合接口的要求支持集合初始值设定项若要实现 IEnumerable/IEnumerable<T> ,必须提供一个枚举器
您可以通过以下三种方式之一执行此操作:如果类正在“包装”另一个集合,则通过返回包装集合的枚举器通过使用收益回报的迭代器通过实例化您自己的 IEnumerator / IEnumerator<T> 实现注意您还可以对现有集合进行子类化:Collection<T> 就是为此目的而设计的(请参阅)
另一种方法是使用 LINQ 查询运算符,我们将在第 中介绍
返回另一个集合的枚举器只是在内部集合上调用 GetEnumerator 的问题
但是,这仅在最简单的方案中可行,其中内部集合中的项正是必需的
更灵活的方法是使用 C# 的 yield return 语句编写迭代器
是一种 C# 语言功能,可帮助编写集合,其方式与 语句帮助使用集合的方式相同
迭代器自动处理 IEnumerable 和 IEnumerator 或其泛型版本的实现
下面是一个简单的示例:public class MyCollection : IEnumerable{ int[] data = { 1, 2, 3 }; public IEnumerator GetEnumerator() { foreach (int i in data) yield return i; }}注意“黑魔法”:GetEnumerator 似乎根本不返回枚举器
分析yield return 语句后,编译器在后台编写一个隐藏的嵌套枚举器类,然后重构 GetEnumerator 以实例化并返回该类
迭代器功能强大且简单(广泛用于实现 LINQ-to-Object 的标准查询运算符)
按照这种方法,我们还可以实现通用接口 IEnumerable<T> :public class MyGenCollection : IEnumerable<int>{ int[] data = { 1, 2, 3 }; public IEnumerator<int> GetEnumerator() { foreach (int i in data) yield return i; } // Explicit implementation keeps it hidden: IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();}因为 IEnumerable<T> 继承自 IEnumerable,我们必须同时实现 GetEnumerator 的泛型和非泛型版本
根据,我们已经显式实现了非通用版本
它可以调用通用 GetEnumerator,因为 IEnumerator<T> 继承自
我们刚刚编写的类适合作为编写更复杂的集合的基础
但是,如果我们不需要简单的 IEnumerable<T> 实现,则 yield return 语句允许更容易的变化
您可以将迭代逻辑移动到返回泛型 IEnumerable<T 的方法中,而不是编写类>让编译器处理其余的工作
下面是一个示例:public static IEnumerable <int> GetSomeIntegers(){ yield return 1; yield return 2; yield return 3;}这是我们使用的方法:foreach (int i in Test.GetSomeIntegers()) Console.WriteLine (i);编写 GetEnumerator 的最后一种方法是编写一个直接实现 IEnumerator 的类
这正是编译器在后台解析迭代器时所做的
(幸运的是,你很少需要自己走这么远
下面的示例定义一个经过硬编码以包含整数 1、2 和 3 的集合:public class MyIntList : IEnumerable{ int[] data = { 1, 2, 3 }; public IEnumerator GetEnumerator() => new Enumerator (this); class Enumerator : IEnumerator // Define an inner class { // for the enumerator. MyIntList collection; int currentIndex = -1; public Enumerator (MyIntList items) => this.collection = items; public object Current { get { if (currentIndex == -1) throw new InvalidOperationException ("Enumeration not started!"); if (currentIndex == collection.data.Length) throw new InvalidOperationException ("Past end of list!"); return collection.data [currentIndex]; } } public bool MoveNext() { if (currentIndex >= collection.data.Length - 1) return false; return ++currentIndex < collection.data.Length; } public void Reset() => currentIndex = -1; }}注意实现重置是可选的 - 您可以改为抛出 NotSupportedException
请注意,对 MoveNext 的第一次调用应移动到列表中的第一个(而不是第二个)项
为了在功能上与迭代器相提并论,我们还必须实现 IEnumerator<T> .下面是一个示例,为简洁起见,省略了边界检查:class MyIntList : IEnumerable<int>{ int[] data = { 1, 2, 3 }; // The generic enumerator is compatible with both IEnumerable and // IEnumerable<T>. We implement the nongeneric GetEnumerator method // explicitly to avoid a naming conflict. public IEnumerator<int> GetEnumerator() => new Enumerator(this); IEnumerator IEnumerable.GetEnumerator() => new Enumerator(this); class Enumerator : IEnumerator<int> { int currentIndex = -1; MyIntList collection; public Enumerator (MyIntList items) => this.items = items; public int Current => collection.data [currentIndex]; object IEnumerator.Current => Current; public bool MoveNext() => ++currentIndex < collection.data.Length; public void Reset() => currentIndex = -1; // Given we don't need a Dispose method, it's good practice to // implement it explicitly, so it's hidden from the public interface. void IDisposable.Dispose() {} }}泛型示例更快,因为 IEnumerator<int>
当前不需要从 int 转换为对象,因此避免了装箱的开销
ICollection 和 IList 接口尽管枚举接口为集合的只进迭代提供了协议,但它们不提供确定集合大小、按索引访问成员、搜索或修改集合的机制
对于此类功能,.NET 定义了 ICollection 、IList 和 IDictionary 接口
每个都有通用和非通用版本;但是,非通用版本主要用于旧版支持
显示了这些接口的继承层次结构
总结它们的最简单方法如下:IEnumerable<T> (和 IEnumerable)提供最少的功能(仅限枚举)ICollection<T> (和ICollection)提供中等功能(例如,Count 属性)IList<T> IDictionary<K,V>及其非通用版本提供最大功能(包括按索引/键进行“随机”访问)注意很少需要这些接口中的任何一个
在几乎所有需要编写集合类的情况下,都可以改为子类 Collection<T>(请参阅)
LINQ 提供了另一个涵盖许多的选项
通用和非通用版本在超出预期的方式上有所不同,尤其是在 ICollection 的情况下
造成这种情况的原因主要是历史的:因为泛型后来出现,泛型接口是在事后诸葛亮的情况下开发的,导致了不同的(和更好的)成员选择
因此,ICollection<T> 不扩展 ICollection,IList<T> 不扩展 IList < 和 IDictionaryTKey, TValue> 不扩展 IDictionary
当然,如果有益的话,集合类本身可以自由地实现接口的两个版本(通常是这样)
注意IList<T>不扩展IList的另一个更微妙的原因是,强制转换为IList<T>将返回一个同时具有Add(T)和Add(object)成员的接口
这将有效地破坏静态类型安全性,因为您可以使用任何类型的对象调用 Add
本节介绍 ICollection<T> 、IList<T> 及其非通用版本;接口
注意在整个 .NET 库中应用单词和的方式没有的基本原理
例如,由于 IList<T> 是 ICollection<T> 的更实用版本,您可能期望类 List<T> 相应地比类 Collection<T> 功能更强大
事实并非如此
最好将术语和视为广义同义词,除非涉及特定类型
ICollection<T> 和 ICollectionICollection<T> 是可数对象集合的标准接口
它提供了确定集合大小(Count)、确定集合中是否存在项(包含)、将集合复制到数组(ToArray)以及确定集合是否为只读(IsReadOnly)的功能
对于可写集合,还可以从集合中添加、删除和清除项目
并且因为它扩展了 IEnumerable<T> ,它也可以通过 foreach 遍历:public interface ICollection<T> : IEnumerable<T>, IEnumerable{ int Count { get; } bool Contains (T item); void CopyTo (T[] array, int arrayIndex); bool IsReadOnly { get; } void Add(T item); bool Remove (T item); void Clear();}非泛型 ICollection 在提供可计数集合方面类似,但它不提供更改列表或检查元素的功能:public interface ICollection : IEnumerable{ int Count { get; } bool IsSynchronized { get; } object SyncRoot { get; } void CopyTo (Array array, int index);}非泛型接口还定义了帮助同步的属性()— 这些属性在泛型版本中被转储,因为线程安全不再被视为集合的固有属性
这两个接口都相当容易实现
如果实现只读 ICollection<T> ,则 添加、删除 和 Clear 方法应引发 NotSupportedException
这些接口通常与 IList 或 IDictionary 接口一起实现
IList<T> 和 IListIList<T> 是可按位置索引的集合的标准接口
除了继承自ICollection<T>和IEnumerable<T>的功能外,它还提供了按位置(通过索引器)读取或写入元素以及按位置插入/删除元素的功能:public interface IList<T> : ICollection<T>, IEnumerable<T>, IEnumerable{ T this [int index] { get; set; } int IndexOf (T item); void Insert (int index, T item); void RemoveAt (int index);}IndexOf 方法对列表执行线性搜索,如果未找到指定的项,则返回 −1
IList 的非泛型版本具有更多成员,因为它从 ICollection 继承较少:public interface IList : ICollection, IEnumerable{ object this [int index] { get; set } bool IsFixedSize { get; } bool IsReadOnly { get; } int Add (object value); void Clear(); bool Contains (object value); int IndexOf (object value); void Insert (int index, object value); void Remove (object value); void RemoveAt (int index);}非泛型 IList 接口上的 Add 方法返回一个整数,这是新添加项的索引
相比之下,ICollection<T> 上的 Add 方法具有 void 返回类型
通用 List<T> 类是 IList<T> 和 IList 的典型实现
C# 数组还同时实现泛型和非泛型 IList (尽管添加或删除元素的方法通过显式接口实现隐藏,并在调用时引发 NotSupportedException)
警告如果您尝试通过 IList 的索引器访问多维数组,则会引发 ArgumentException
在编写如下方法时,这是一个陷阱:public object FirstOrNull (IList list){ if (list == null || list.Count == 0) return null; return list[0];}这可能看起来很无懈可击,但如果使用多维数组调用,它将引发异常
您可以使用此表达式在运行时测试多维数组(详见):list.GetType().IsArray && list.GetType().GetArrayRank()>1IReadOnlyCollection<T> 和 IReadOnlyList<T>.NET 还定义了集合和列表接口,这些接口仅公开只读操作所需的成员:public interface IReadOnlyCollection<out T> : IEnumerable<T>, IEnumerable{ int Count { get; }}public interface IReadOnlyList<out T> : IReadOnlyCollection<T>, IEnumerable<T>, IEnumerable{ T this[int index] { get; }}由于这些接口的类型参数仅用于输出位置,因此将其标记为
例如,这允许将猫列表视为只读的动物列表
相反,T 没有被标记为与 ICollection<T> 和 IList<T> 的协变,因为 T 同时用于输入和输出位置
注意这些接口表示集合或列表的只读;基础实现可能仍然是可写的
大多数可写()集合同时实现只读和读/写接口
除了允许您协变地使用集合之外,只读接口还允许类公开私有可写集合的只读视图
我们在”中演示了这一点,以及更好的解决方案
IReadOnlyList<T>映射到Windows运行时类型IVectorView<T>
数组类Array 类是所有单维和多维数组的隐式基类,它是实现标准集合接口的最基本类型之一
Array 类提供类型统一,因此一组通用方法可用于所有数组,无论其声明或基础元素类型如何
由于数组是如此基本,因此 C# 为它们的声明和初始化提供了显式语法,我们在第 章和第 章中对此进行了介绍
使用 C# 的语法声明数组时,CLR 会隐式子类型 Array 类,从而合成适合数组维度和元素类型的
此伪类型实现类型化泛型集合接口,如 IList<string>
CLR 还在构造时专门处理数组类型,在内存中为它们分配一个连续的空间
这使得索引到数组中非常高效,但可以防止以后调整它们的大小
Array 以泛型和非泛型形式实现 IList<T> 的集合接口
IList<T>本身是显式实现的,但是,为了使数组的公共接口中没有诸如Add或Move之类的方法,这些方法会在固定长度的集合(如数组)上引发异常
Array 类实际上提供了一个静态 Resize 方法,尽管这种方法的工作原理是创建一个新数组,然后复制每个元素
除了效率低下之外,程序中其他位置对数组的引用仍将指向原始版本
对于可调整大小的集合,更好的解决方案是使用 List<T> 类(在下一节中介绍)
数组可以包含值类型或引用类型元素
值类型元素存储在数组中,因此三个长整数(每个 8 个字节)的数组将占用 24 个字节的连续内存
但是,引用类型元素在数组中占用的空间仅与引用一样多(4 位环境中为 32 个字节,8 位环境中为 64 个字节)
说明了以下程序在内存中的效果:StringBuilder[] builders = new StringBuilder [5];builders [0] = new StringBuilder ("builder1");builders [1] = new StringBuilder ("builder2");builders [2] = new StringBuilder ("builder3");long[] numbers = new long [3];numbers [0] = 12345;numbers [1] = 54321;内存中的数组因为 Array 是一个类,所以数组始终是(本身)引用类型,而不管数组的元素类型如何
这意味着语句 arrayB = arrayA 会产生两个引用同一数组的变量
同样,两个不同的数组总是无法通过相等性测试,除非您使用来比较数组的每个元素:object[] a1 = { "string", 123, true };object[] a2 = { "string", 123, true };Console.WriteLine (a1 == a2); // FalseConsole.WriteLine (a1.Equals (a2)); // FalseIStructuralEquatable se1 = a1;Console.WriteLine (se1.Equals (a2, StructuralComparisons.StructuralEqualityComparer)); // True数组可以通过调用 Clone 方法复制:arrayB = arrayA.Clone()
但是,这会导致浅层克隆,这意味着仅复制数组本身表示的内存
如果数组包含值类型对象,则复制值本身;如果数组包含引用类型对象,则仅复制引用(导致两个数组的成员引用相同的对象)
演示了将以下代码添加到示例中的效果:StringBuilder[] builders2 = builders;StringBuilder[] shallowClone = (StringBuilder[]) builders.Clone();浅拷贝阵列要创建深层副本(为其复制引用类型子对象),必须遍历数组并手动克隆每个元素
相同的规则适用于其他 .NET 集合类型
尽管 Array 主要设计用于 32 位索引器,但它对 64 位索引器的支持也有限(允许数组理论上最多寻址 264 elements) via several methods that accept both Int32 and Int64 parameters. These overloads are useless in practice because the CLR does not permit any object—including arrays—to exceed two gigabytes in size (whether running on a 32- or 64-bit environment).注意Array 类上许多期望作为实例方法的方法实际上是静态方法
这是一个奇怪的设计决策,这意味着在 Array 上查找方法时,您应该同时检查静态方法和实例方法
构造和索引创建和索引数组的最简单方法是通过 C# 的语言构造:int[] myArray = { 1, 2, 3 };int first = myArray [0];int last = myArray [myArray.Length - 1];或者,您可以通过调用 Array.CreateInstance 来动态实例化数组
这允许您在运行时指定元素类型和秩(维数),以及通过指定下限来允许非零数组
非零数组与 .NET 公共语言规范 (CLS) 不兼容,不应作为库中的公共成员公开,而库中可能由用 F# 或 Visual Basic 编写的程序使用
GetValue 和 SetValue 方法允许您访问动态创建的数组中的元素(它们也适用于普通数组): // Create a string array 2 elements in length: Array a = Array.CreateInstance (typeof(string), 2); a.SetValue ("hi", 0); // → a[0] = "hi"; a.SetValue ("there", 1); // → a[1] = "there"; string s = (string) a.GetValue (0); // → s = a[0]; // We can also cast to a C# array as follows: string[] cSharpArray = (string[]) a; string s2 = cSharpArray [0];动态创建的零索引数组可以强制转换为匹配或兼容类型(与标准数组方差规则兼容)的 C# 数组
例如,如果 Apple 子类 Fruit ,则可以将 Apple[] 转换为 Fruit[]
这就引出了为什么 object[] 没有用作统一数组类型而不是 Array 类的问题
答案是 object[] 与多维和值类型数组(以及非基于零的数组)都不兼容
int[] 数组不能强制转换为对象 []
因此,我们需要 Array 类来实现完整类型统一
GetValue 和 SetValue 也适用于编译器创建的数组,它们在编写可以处理任何类型和等级的数组的方法时很有用
对于多维数组,它们接受索引器:public object GetValue (params int[] indices)public void SetValue (object value, params int[] indices)以下方法打印任何数组的第一个元素,而不考虑等级: void WriteFirstValue (Array a) { Console.Write (a.Rank + "-dimensional; "); // The indexers array will automatically initialize to all zeros, so // passing it into GetValue or SetValue will get/set the zero-based // (i.e., first) element in the array. int[] indexers = new int[a.Rank]; Console.WriteLine ("First value is " + a.GetValue (indexers)); } void Demo() { int[] oneD = { 1, 2, 3 }; int[,] twoD = { {5,6}, {8,9} }; WriteFirstValue (oneD); // 1-dimensional; first value is 1 WriteFirstValue (twoD); // 2-dimensional; first value is 5 }注意对于使用未知类型但已知等级的数组,泛型提供了一种更简单、更有效的解决方案:void WriteFirstValue<T> (T[] array){ Console.WriteLine (array[0]);}如果元素的类型与数组不兼容,则 SetValue 将引发异常
当一个数组被实例化时,无论是通过语言语法还是Array.CreateInstance,它的元素都会被自动初始化
对于具有引用类型元素的数组,这意味着写入 null;对于具有值类型元素的数组,这意味着调用值类型的默认构造函数(有效地将成员“清零”)
Array 类还通过 Clear 方法按需提供此功能:public static void Clear (Array array, int index, int length);此方法不会影响数组的大小
这与通常使用Clear(例如在ICollection<T>中)形成鲜明对比
清除),从而集合减少到零元素
列举数组很容易用 foreach 语句枚举:int[] myArray = { 1, 2, 3};foreach (int val in myArray) Console.WriteLine (val);还可以使用静态 Array.ForEach 方法进行枚举,该方法定义如下:public static void ForEach<T> (T[] array, Action<T> action);这将使用具有以下签名的操作委托:public delegate void Action<T> (T obj);下面是用 Array.ForEach 重写的第一个示例:Array.ForEach (new[] { 1, 2, 3 }, Console.WriteLine);长度和等级数组提供以下用于查询长度和秩的方法和属性:public int GetLength (int dimension);public long GetLongLength (int dimension);public int Length { get; }public long LongLength { get; }public int GetLowerBound (int dimension);public int GetUpperBound (int dimension);public int Rank { get; } // Returns number of dimensions in arrayGetLength 和 GetLongLength 返回给定维度的长度(一维数组为 0),Length 和 LongLength 返回数组中元素的总数 — 包括所有维度
GetLowerBound 和 GetUpperBound 对于非零索引数组很有用
对于任何给定维度,GetUpperBound 返回的结果与将 GetLowerBound 添加到 GetLength 的结果相同
搜索Array 类提供了一系列用于在一维数组中查找元素的方法:二进制搜索方法用于快速搜索特定项目的排序数组索引/上一个索引方法用于搜索特定项目的未排序数组Find / FindLast / FindIndex / FindLastIndex / FindAll / Exists / TrueForAll用于在未排序数组中搜索满足给定谓词<T的项目>如果未找到指定的值,则任何数组搜索方法都不会引发异常
相反,如果未找到项目,则返回整数的方法返回 −1(假设为零索引数组),返回泛型类型的方法返回类型的默认值(例如,0 表示 int,或 null 表示字符串)
二叉搜索方法很快,但它们仅适用于排序数组,并且要求比较元素的,而不仅仅是
为此,二叉搜索方法可以接受 IComparer 或 IComparer<T> 对象对排序决策进行仲裁(参见)
这必须与最初对数组进行排序时使用的任何比较器一致
如果未提供比较器,则类型的默认排序算法将根据其实现 IComparable/IComparable<T> 应用
IndexOf 和 LastIndexOf 方法对数组执行简单的枚举,返回与给定值匹配的第一个(或最后一个)元素的位置
基于谓词的搜索方法允许方法委托或 lambda 表达式对给定元素是否为“匹配”进行仲裁
谓词只是一个接受对象并返回 true 或 false 的委托:public delegate bool Predicate<T> (T object);在下面的示例中,我们在字符串数组中搜索包含字母“a”的名称:string[] names = { "Rodney", "Jack", "Jill" };string match = Array.Find (names, ContainsA);Console.WriteLine (match); // JackContainsA (string name) { return name.Contains ("a"); }下面是使用 lambda 表达式缩短的相同代码:string[] names = { "Rodney", "Jack", "Jill" };string match = Array.Find (names, n => n.Contains ("a")); // JackFindAll 返回满足谓词的所有项的数组
事实上,它等效于 System.Linq 命名空间中的 Enumerable.Where,只是 FindAll 返回匹配项的数组,而不是相同的 IEnumerable<T>
如果任何数组成员满足给定谓词,则 Exists 返回 true,并且等效于 System.Linq.Enumerable 中的 Any
如果所有项都满足谓词,则 TrueForAll 返回 true,并且等效于 System.Linq.Enumerable 中的 All
排序数组具有以下内置排序方法:// For sorting a single array:public static void Sort<T> (T[] array);public static void Sort (Array array);// For sorting a pair of arrays:public static void Sort<TKey,TValue> (TKey[] keys, TValue[] items);public static void Sort (Array keys, Array items);这些方法中的每一个都还重载以接受以下内容:int index // Starting index at which to begin sortingint length // Number of elements to sortIComparer<T> comparer // Object making ordering decisionsComparison<T> comparison // Delegate making ordering decisions下面说明了排序的最简单用法:int[] numbers = { 3, 2, 1 };Array.Sort (numbers); // Array is now { 1, 2, 3 }接受一对数组的方法通过重新排列每个数组的项目来工作,将排序决策基于第一个数组
在下一个示例中,数字及其相应的单词都按数字顺序排序:int[] numbers = { 3, 2, 1 };string[] words = { "three", "two", "one" };Array.Sort (numbers, words);// numbers array is now { 1, 2, 3 }// words array is now { "one", "two", "three" }Array.Sort要求数组中的元素实现IComparable(参见中的)
这意味着可以对大多数内置 C# 类型(如前面示例中的整数)进行排序
如果元素在本质上不具有可比性,或者您希望覆盖默认排序,则必须为 Sort 提供用于报告两个元素的相对位置的自定义比较提供程序
有一些方法可以做到这一点:通过实现 IComparer / IComparer<T>的帮助器对象(请参阅)通过比较委托:public delegate int Comparison<T> (T x, T y);比较委托遵循与 IComparer<T> 相同的语义
比较:如果 x 在 y 之前,则返回一个负整数;如果 x 在 y 之后,则返回一个正整数;如果 x 和 y 具有相同的排序位置,则返回 0
在下面的示例中,我们对整数数组进行排序,使奇数排在第一位:int[] numbers = { 1, 2, 3, 4, 5 };Array.Sort (numbers, (x, y) => x % 2 == y % 2 ? 0 : x % 2 == 1 ? -1 : 1);// numbers array is now { 1, 3, 5, 2, 4 }注意作为调用 Sort 的替代方法,您可以使用 LINQ 的 OrderBy 和 ThenBy 运算符
与 Array.Sort 不同,LINQ 运算符不会更改原始数组,而是以新的 IEnumerable<T> 序列发出排序结果
反转元素以下 Array 方法反转数组中所有(或部分)元素的顺序:public static void Reverse (Array array);public static void Reverse (Array array, int index, int length);复制Array 提供了四种执行浅拷贝的方法:克隆、复制到、拷贝和约束拷贝
前两个是实例方法;后两个是静态方法
Clone 方法返回一个全新的(浅拷贝)数组
CopyTo 和 Copy 方法复制数组的连续子集
复制多维矩形数组需要将多维索引映射到线性索引
例如,1 × 1 数组中的中间正方形(位置 [3,3]) 用索引 4 表示,计算结果为 :1 3 + 1
源范围和目标范围可以重叠而不会引起问题
ConscuredCopy 执行操作:如果无法成功复制所有请求的元素(例如,由于类型错误),则操作将回滚
Array 还提供了一个 AsReadOnly 方法,该方法返回一个包装器,以防止元素被重新分配
转换和调整大小Array.ConvertAll创建并返回一个元素类型为TOutput的新数组,调用提供的转换器委托来复制元素
转换器定义如下:public delegate TOutput Converter<TInput,TOutput> (TInput input)下面将浮点数数组转换为整数数组:float[] reals = { 1.3f, 1.5f, 1.8f };int[] wholes = Array.ConvertAll (reals, r => Convert.ToInt32 (r));// wholes array is { 1, 2, 2 }Resize 方法的工作原理是创建一个新数组并复制元素,通过引用参数返回新数组
但是,其他对象中对原始数组的任何引用将保持不变
注意System.Linq 命名空间提供了适用于数组转换的扩展方法的附加自助餐
这些方法返回一个IEnumerable<T>,你可以通过Enumerable的ToArray方法将其转换回数组
列表、队列、堆栈和集.NET 提供了一组基本的具体集合类,用于实现本章中所述的接口
本节重点介绍类似列表的集合(相对于我们在中介绍的集合)
与我们之前讨论的接口一样,您通常可以选择每种类型的泛型或非泛型版本
在灵活性和性能方面,泛型类胜出,使其非泛型类除了向后兼容性外是多余的
这与集合接口的情况不同,对于集合接口,非泛型版本偶尔仍然有用
在本节中描述的类中,泛型 List 类是最常用的
List<T> 和 ArrayList泛型 List 类和非泛型 ArrayList 类提供动态大小的对象数组,是最常用的集合类之一
ArrayList 实现了 IList ,而 List<T> 同时实现了 IList 和 IList<T>(以及只读版本,IReadOnlyList<T> )
与数组不同,所有接口都是公开实现的,并且 Add 和 Remove 等方法公开并按预期工作
在内部,List<T> 和 ArrayList 通过维护一个内部对象数组来工作,在达到容量时替换为更大的数组
附加元素是有效的(因为末尾通常有一个空闲插槽),但插入元素可能很慢(因为插入点之后的所有元素都必须移动才能形成空闲插槽),删除元素(尤其是在开始附近)也是如此
与数组一样,如果对已排序的列表使用 BinarySearch 方法,则搜索是有效的,但在其他方面效率低下,因为必须单独检查每个项目
注意如果 T 是值类型,则 List<T> 比 ArrayList 快几倍,因为 List<T> 避免了装箱和取消装箱元素的开销
List<T> 和 ArrayList 提供了接受现有元素集合的构造函数:这些构造函数将现有集合中的每个元素复制到新的 List<T> 或 ArrayList 中:public class List<T> : IList<T>, IReadOnlyList<T>{ public List (); public List (IEnumerable<T> collection); public List (int capacity); // Add+Insert 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); // Remove public bool Remove (T item); public void RemoveAt (int index); public void RemoveRange (int index, int count); public int RemoveAll (Predicate<T> match); // Indexing public T this [int index] { get; set; } public List<T> GetRange (int index, int count); public Enumerator<T> GetEnumerator(); // Exporting, copying and converting: public T[] ToArray(); public void CopyTo (T[] array); public void CopyTo (T[] array, int arrayIndex); public void CopyTo (int index, T[] array, int arrayIndex, int count); public ReadOnlyCollection<T> AsReadOnly(); public List<TOutput> ConvertAll<TOutput> (Converter <T,TOutput> converter); // Other: public void Reverse(); // Reverses order of elements in list. public int Capacity { get;set; } // Forces expansion of internal array. public void TrimExcess(); // Trims internal array back to size. public void Clear(); // Removes all elements, so Count=0.}public delegate TOutput Converter <TInput, TOutput> (TInput input);除了这些成员之外,List<T>还提供了所有Array搜索和排序方法的实例版本
下面的代码演示了 List 的属性和方法(有关搜索和排序的示例,请参阅):var words = new List<string>(); // New string-typed listwords.Add ("melon");words.Add ("avocado");words.AddRange (new[] { "banana", "plum" } );words.Insert (0, "lemon"); // Insert at startwords.InsertRange (0, new[] { "peach", "nashi" }); // Insert at startwords.Remove ("melon");words.RemoveAt (3); // Remove the 4th elementwords.RemoveRange (0, 2); // Remove first 2 elements// Remove all strings starting in 'n':words.RemoveAll (s => s.StartsWith ("n"));Console.WriteLine (words [0]); // first wordConsole.WriteLine (words [words.Count - 1]); // last wordforeach (string s in words) Console.WriteLine (s); // all wordsList<string> subset = words.GetRange (1, 2); // 2nd->3rd wordsstring[] wordsArray = words.ToArray(); // Creates a new typed array// Copy first two elements to the end of an existing array: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]; // Runtime exception注意ArrayList 在功能上类似于 List<object>
当您需要共享不共享公共基类型(对象除外)的混合类型元素列表时,两者都很有用
在这种情况下,选择ArrayList的一个可能的优点是,如果您需要使用反射来处理列表()
使用非泛型 ArrayList 进行反射比使用 List<object> 更容易
如果导入 System.Linq 命名空间,则可以通过调用 Cast 然后调用 ToList 将 ArrayList 转换为泛型列表:ArrayList al = new ArrayList();al.AddRange (new[] { 1, 5, 9 } );List<int> list = al.Cast<int>().ToList();Cast 和 ToList 是 System.Linq.Enumerable 类中的扩展方法
链接列表<T>LinkedList<T> 是一个通用的双向链表(见)
双向链表是节点链,其中每个节点引用前面的节点、之后的节点和实际的元素
它的主要优点是元素始终可以有效地插入列表中的任何位置,因为它只涉及创建一个新节点并更新一些引用
但是,首先查找将节点插入的位置可能会很慢,因为没有直接索引到链表中的内在机制;必须遍历每个节点,并且无法进行二进制切碎搜索
LinkedList<T> 实现了 IEnumerable<T> 和 ICollection<T>(及其非通用版本),但不是 IList<T>因为不支持按索引访问
列表节点通过以下类实现:public sealed class LinkedListNode<T>{ public LinkedList<T> List { get; } public LinkedListNode<T> Next { get; } public LinkedListNode<T> Previous { get; } public T Value { get; set; }}添加节点时,可以指定其相对于另一个节点的位置,也可以指定其在列表的开头/结尾的位置
LinkedList<T>为此提供了以下方法:public void AddFirst(LinkedListNode<T> node);public LinkedListNode<T> AddFirst (T value);public void AddLast (LinkedListNode<T> node);public LinkedListNode<T> AddLast (T value);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 Clear();public void RemoveFirst();public void RemoveLast();public bool Remove (T value);public void Remove (LinkedListNode<T> node);LinkedList<T>具有内部字段来跟踪列表中的元素数量以及列表的头部和尾部
这些属性在以下公共属性中公开:public int Count { get; } // Fastpublic LinkedListNode<T> First { get; } // Fastpublic LinkedListNode<T> Last { get; } // FastLinkedList<T> 还支持以下搜索方法(每种方法都需要在内部枚举列表):public bool Contains (T value);public LinkedListNode<T> Find (T value);public LinkedListNode<T> FindLast (T value);最后,LinkedList<T> 支持复制到数组进行索引处理,并获取一个枚举器来支持 foreach 语句:public void CopyTo (T[] array, int index);public Enumerator<T> GetEnumerator();以下是使用 LinkedList<string> 的演示:var tune = new LinkedList<string>();tune.AddFirst ("do"); // dotune.AddLast ("so"); // do - sotune.AddAfter (tune.First, "re"); // do - re- sotune.AddAfter (tune.First.Next, "mi"); // do - re - mi- sotune.AddBefore (tune.Last, "fa"); // do - re - mi - fa- sotune.RemoveFirst(); // re - mi - fa - sotune.RemoveLast(); // re - mi - faLinkedListNode<string> miNode = tune.Find ("mi");tune.Remove (miNode); // re - fatune.AddFirst (miNode); // mi- re - faforeach (string s in tune) Console.WriteLine (s);队列<T>和队列队列<T> 和队列是先进先出 (FIFO) 数据结构,提供排队(将项目添加到队列尾部)和取消排队(检索并删除队列头部的项目)的方法
还提供了 Peek 方法,用于返回队列头部的元素而不删除它,以及一个 Count 属性(在出列之前检查元素是否存在很有用)
虽然队列是可枚举的,但它们不实现 IList<T> / IList ,因为成员不能通过索引直接访问
但是,提供了一个 ToArray 方法,用于将元素复制到可以从中随机访问它们的数组:public class Queue<T> : IEnumerable<T>, ICollection, IEnumerable{ public Queue(); public Queue (IEnumerable<T> collection); // Copies existing elements public Queue (int capacity); // To lessen auto-resizing public void Clear(); public bool Contains (T item); public void CopyTo (T[] array, int arrayIndex); public int Count { get; } public T Dequeue(); public void Enqueue (T item); public Enumerator<T> GetEnumerator(); // To support foreach public T Peek(); public T[] ToArray(); public void TrimExcess();}以下是使用 Queue<int> 的示例:var q = new Queue<int>();q.Enqueue (10);q.Enqueue (20);int[] data = q.ToArray(); // Exports to an arrayConsole.WriteLine (q.Count); // "2"Console.WriteLine (q.Peek()); // "10"Console.WriteLine (q.Dequeue()); // "10"Console.WriteLine (q.Dequeue()); // "20"Console.WriteLine (q.Dequeue()); // throws an exception (queue empty)队列是使用根据需要调整大小的数组在内部实现的,与泛型 List 类非常相似
队列维护直接指向头和尾元素的索引;因此,排队和取消排队是非常快速的操作(除非需要内部调整大小)
堆叠<T>和堆叠堆栈<T> 和堆栈是后进先出 (LIFO) 数据结构,提供推送(将项目添加到堆栈顶部)和 Pop(从堆栈顶部检索和删除元素)的方法
还提供了非破坏性 Peek 方法,以及用于导出数据以进行随机访问的 Count 属性和 ToArray 方法:public class Stack<T> : IEnumerable<T>, ICollection, IEnumerable{ public Stack(); public Stack (IEnumerable<T> collection); // Copies existing elements public Stack (int capacity); // Lessens auto-resizing public void Clear(); public bool Contains (T item); public void CopyTo (T[] array, int arrayIndex); public int Count { get; } public Enumerator<T> GetEnumerator(); // To support foreach public T Peek(); public T Pop(); public void Push (T item); public T[] ToArray(); public void TrimExcess();}以下示例演示了 Stack<int> :var s = new Stack<int>();s.Push (1); // Stack = 1s.Push (2); // Stack = 1,2s.Push (3); // Stack = 1,2,3Console.WriteLine (s.Count); // Prints 3Console.WriteLine (s.Peek()); // Prints 3, Stack = 1,2,3Console.WriteLine (s.Pop()); // Prints 3, Stack = 1,2Console.WriteLine (s.Pop()); // Prints 2, Stack = 1Console.WriteLine (s.Pop()); // Prints 1, Stack = <empty>Console.WriteLine (s.Pop()); // throws exception堆栈在内部使用根据需要调整大小的数组实现,如 Queue<T> 和 List<T>
位数组位数组是压缩布尔值的动态大小集合
它比简单的布尔数组和通用布尔列表的内存效率更高,因为它对每个值只使用一个位,而布尔类型则为每个值占用一个字节
BitArray 的索引器读取和写入单个位:var bits = new BitArray(2);bits[1] = true;有四种按位运算符方法(和、或、异或、和不是)
除了最后一个之外,所有都接受另一个位数组:bits.Xor (bits); // Bitwise exclusive-OR bits with itselfConsole.WriteLine (bits[1]); // FalseHashSet<T> 和 SortedSet<T>HashSet<T> 和 SortedSet<T>具有以下显著特征:它们的 Contains 方法使用基于哈希的查找快速执行
它们不存储重复的元素,并以静默方式忽略添加的请求
不能按位置访问元素
SortedSet<T> 使元素保持有序,而 HashSet<T> 则不然
HashSet<T> 和 SortedSet<T> 类型的通用性由接口 ISet<T> 捕获
从 .NET 5 开始,这些类还实现了一个名为 IReadOnlySet<T> 的接口,该接口也由不可变集类型实现(请参阅)
HashSet<T> 是用一个只存储键的哈希表实现的;SortedSet<T> 是用红/黑树实现的
这两个集合都实现了 ICollection<T> 并提供您期望的方法,例如 包含 、 添加 和 删除
此外,还有一种基于谓词的删除方法,称为 删除位置 .下面从现有集合构造 HashSet<char>,测试成员资格,然后枚举集合(请注意没有重复项):var letters = new HashSet<char> ("the quick brown fox");Console.WriteLine (letters.Contains ('t')); // trueConsole.WriteLine (letters.Contains ('j')); // falseforeach (char c in letters) Console.Write (c); // the quickbrownfx(我们可以将字符串传递到 HashSet<char> 的构造函数中的原因是该字符串实现了 IEnumerable<char>
真正有趣的方法是集合操作
以下集合操作具有,因为它们会修改集合public void UnionWith (IEnumerable<T> other); // Addspublic void IntersectWith (IEnumerable<T> other); // Removespublic void ExceptWith (IEnumerable<T> other); // Removespublic void SymmetricExceptWith (IEnumerable<T> other); // Removes而以下方法只是查询集合,因此是非破坏性的:public bool IsSubsetOf (IEnumerable<T> other);public bool IsProperSubsetOf (IEnumerable<T> other);public bool IsSupersetOf (IEnumerable<T> other);public bool IsProperSupersetOf (IEnumerable<T> other);public bool Overlaps (IEnumerable<T> other);public bool SetEquals (IEnumerable<T> other);UnionWith 将第二个集合中的所有元素添加到原始集合中(不包括重复项)
相交删除不在两个集中的元素
我们可以从我们的字符集中提取所有元音,如下所示:var letters = new HashSet<char> ("the quick brown fox");letters.IntersectWith ("aeiou");foreach (char c in letters) Console.Write (c); // euioExceptWith 从源代码集中删除指定的元素
在这里,我们从集合中剥离所有元音:var letters = new HashSet<char> ("the quick brown fox");letters.ExceptWith ("aeiou");foreach (char c in letters) Console.Write (c); // th qckbrwnfxSymmetricExceptWith 删除除一个集合或另一个集合所特有的元素之外的所有元素:var letters = new HashSet<char> ("the quick brown fox");letters.SymmetricExceptWith ("the lazy brown fox");foreach (char c in letters) Console.Write (c); // quicklazy请注意,由于 HashSet<T> 和 SortedSet<T> 实现了 IEnumerable<T>,因此您可以使用其他类型的集合(或集合)作为任何集合操作方法的参数
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 中的示例>: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字典是一个集合,其中每个元素都是一个键/值对
字典最常用于查找和排序列表
.NET 通过接口 IDictionary 和 IDictionary<TKey、TValue 以及一组通用字典类为字典定义了一个标准协议>
每个类在以下方面有所不同:项目是否按排序顺序存储是否可以按位置(索引)和键访问项目无论是通用还是非通用从大型字典中按键检索项目是快还是慢总结了每个字典类以及它们在这些方面的区别
性能时间以毫秒为单位,基于在 50.000 GHz PC 上使用整数键和值的字典执行 1,5 次操作
(使用相同基础集合结构的泛型和非泛型对应项之间的性能差异是由于装箱造成的,并且仅显示值类型元素
字典类类型内部结构按索引检索?内存开销(每个项目的平均字节数)速度:随机插入速度:顺序插入速度:按键检索排序字典 <K,V>哈希表不22303020哈希表哈希表不38505030列表字典链表不3650,00050,00050,000有序词典哈希表 + 数组是的59707040排序排序词典 <K,V>红/黑树不20130100120排序列表 <K,V>2x阵列是的23,3003040排序列表2x阵列是的274,500100180在 Big-O 表示法中,按键检索时间如下:O(1) 表示哈希表、字典和有序字典O(log ) 表示 SortedDictionary 和 SortedListO() 表示 ListDictionary(以及非字典类型,如 List<T>)是集合中的元素数
IDictionary<TKey,TValue>IDictionary<TKey,TValue> 为所有基于键/值的集合定义了标准协议
它通过添加方法和属性来访问基于任意类型的键的元素,从而扩展了ICollection<T>:public interface IDictionary <TKey, TValue> : ICollection <KeyValuePair <TKey, TValue>>, IEnumerable{ bool ContainsKey (TKey key); bool TryGetValue (TKey key, out TValue value); void Add (TKey key, TValue value); bool Remove (TKey key); TValue this [TKey key] { get; set; } // Main indexer - by key ICollection <TKey> Keys { get; } // Returns just keys ICollection <TValue> Values { get; } // Returns just values}注意还有一个名为IReadOnlyDictionary<TKey,TValue>的接口,它定义了字典成员的只读子集
若要将项添加到字典,请调用 Add 或使用索引的 set 访问器 - 如果键尚不存在,后者会将项添加到字典中(如果存在,则更新该项)
所有字典实现中都禁止重复键,因此使用相同的键调用 Add 两次会引发异常
若要从字典中检索项,请使用索引器或 TryGetValue 方法
如果键不存在,索引器将引发异常,而 TryGetValue 返回 false
您可以通过调用 包含密钥 来显式测试成员资格;但是,如果您随后检索项目,则会产生两次查找的费用
直接在 IDictionary<TKey,TValue> 上枚举返回一系列 KeyValuePair 结构:public struct KeyValuePair <TKey, TValue>{ public TKey Key { get; } public TValue Value { get; }}您可以通过字典的键/值属性仅枚举键或值
我们将在下一节中演示如何将此接口与泛型 Dictionary 类一起使用
目录非通用IDictionary接口在原则上与IDictionary<TKey,TValue>相同,除了两个重要的功能差异
了解这些差异非常重要,因为 IDictionary 出现在旧代码中(包括 .NET BCL 本身的某些地方):通过索引器检索不存在的键将返回 null(而不是引发异常)
包含成员资格测试,而不是包含密钥
枚举非泛型 IDictionary 将返回一系列 DictionaryEntry 结构:public struct DictionaryEntry{ public object Key { get; set; } public object Value { get; set; }}字典< TKey,TValue>和哈希表泛型字典类是最常用的集合之一(与 List<T> 集合一起)
它使用哈希表数据结构来存储键和值,并且快速高效
注意Dictionary<TKey,TValue>的非通用版本称为Hashtable ;没有称为字典的非泛型类
当我们简单地提到字典时,我们指的是通用的字典<TKey,TValue>类
字典实现了通用和非通用 IDictionary 接口,通用 IDictionary 公开
词典其实是通用词典的“教科书”实现
以下是使用它的方法:var d = new Dictionary<string, int>();d.Add("One", 1);d["Two"] = 2; // adds to dictionary because "two" not already presentd["Two"] = 22; // updates dictionary because "two" is now presentd["Three"] = 3;Console.WriteLine (d["Two"]); // Prints "22"Console.WriteLine (d.ContainsKey ("One")); // true (fast operation)Console.WriteLine (d.ContainsValue (3)); // true (slow operation)int val = 0;if (!d.TryGetValue ("onE", out val)) Console.WriteLine ("No val"); // "No val" (case sensitive)// Three different ways to enumerate the dictionary:foreach (KeyValuePair<string, int> kv in d) // One; 1 Console.WriteLine (kv.Key + "; " + kv.Value); // Two; 22 // Three; 3foreach (string s in d.Keys) Console.Write (s); // OneTwoThreeConsole.WriteLine();foreach (int i in d.Values) Console.Write (i); // 1223其基础哈希表的工作原理是将每个元素的键转换为整数哈希代码(伪唯一值),然后应用算法将哈希代码转换为哈希键
此哈希键在内部用于确定条目属于哪个“存储桶”
如果存储桶包含多个值,则会对存储桶执行线性搜索
一个好的哈希函数不会努力返回严格唯一的哈希码(这通常是不可能的);它努力返回均匀分布在 32 位整数空间中的哈希码
这可以防止最终得到几个非常大(且效率低下)的存储桶的情况
字典可以处理任何类型的键,前提是它能够确定键之间的相等性并获取哈希码
默认情况下,相等性是通过键的对象确定的
等于方法,伪唯一哈希代码是通过密钥的 GetHashCode 方法获取的
可以通过重写这些方法或在构造字典时提供 IEqualityComparer 对象来更改此行为
这样做的一个常见应用是在使用字符串键时指定不区分大小写的相等比较器:var d = new Dictionary<string, int> (StringComparer.OrdinalIgnoreCase);我们将在中进一步讨论这个问题
与许多其他类型的集合一样,可以通过在构造函数中指定集合的预期大小来稍微提高字典的性能,从而避免或减少对内部调整大小操作的需求
非泛型版本名为 Hashtable,除了由于它公开前面讨论的非泛型 IDictionary 接口而产生的差异之外,它在功能上是相似的
字典和哈希表的缺点是项目未排序
此外,不保留添加项目的原始顺序
与所有字典一样,不允许使用重复的键
注意当泛型集合在2005年被引入时,CLR团队选择根据它们所代表的内容(字典,列表)而不是它们内部实现的方式(Hashtable,ArrayList)来命名它们
虽然这很好,因为它给了他们以后更改实现的自由,但这也意味着(通常是选择一种集合而不是另一种集合的最重要标准)不再包含在名称中
有序词典OrderedDictionary 是一种非泛型字典,它以与添加元素相同的顺序维护元素
使用 OrderedDictionary ,您可以按索引和键访问元素
注意排序字典不是字典
OrderedDictionary是Hashtable和ArrayList的组合
这意味着它具有哈希表的所有功能,以及诸如 和整数索引器之类的函数
它还公开按原始顺序返回元素的键和值属性
此类是在 .NET 2.0 中引入的,但奇怪的是,没有泛型版本
列表词典和混合词典ListDictionary使用单向链表来存储基础数据
它不提供排序,尽管它保留了项目的原始输入顺序
ListDictionary 对于大型列表来说非常慢
它唯一真正的“名声”是它对非常小的列表(少于 10 个项目)的效率
HybridDictionary是一个ListDictionary,在达到一定大小时会自动转换为哈希表,以解决ListDictionary的性能问题
这个想法是在字典较小时获得低内存占用,在字典较大时获得良好的性能
但是,考虑到从一个词典转换到另一个词典的开销,以及词典在这两种情况下都不会过重或过慢的事实,一开始使用都不会受到不合理的影响
这两个类都仅以非泛型形式出现
排序词典.NET BCL 提供两个内部结构化的字典类,以便其内容始终按键排序:排序词典<TKey,TValue>排序列表<噶吱吱>吱��1(在本节中,我们将<TKey,TValue>缩写为<,>
SortedDictionary<,> 使用红/黑树:一种数据结构,旨在在任何插入或检索场景中始终如一地执行良好性能
SortedList<,> 在内部使用有序数组对实现,提供快速检索(通过二进制切碎搜索),但插入性能较差(因为需要移动现有值以便为新条目腾出空间)
SortedDictionary<,> 在随机序列中插入元素(尤其是大型列表)方面比 SortedList<,> 快得多
但是,SortedList<,> 具有额外的功能:按索引和键访问项目
使用排序列表,可以直接转到排序序列中的个元素(通过键/值属性上的索引器)
若要对 SortedDictionary<,> 执行相同的操作,必须手动枚举 个项目
(或者,您可以编写一个将排序字典与列表类组合在一起的类
这三个集合中没有一个允许重复键(就像所有一样)
下面的示例使用反射将 System.Object 中定义的所有方法加载到按名称键的排序列表中,然后枚举它们的键和值:// MethodInfo is in the System.Reflection namespacevar sorted = new SortedList <string, MethodInfo>();foreach (MethodInfo m in typeof (object).GetMethods()) sorted [m.Name] = m;foreach (string name in sorted.Keys) Console.WriteLine (name);foreach (MethodInfo m in sorted.Values) Console.WriteLine (m.Name + " returns a " + m.ReturnType);下面是第一个枚举的结果:EqualsGetHashCodeGetTypeReferenceEqualsToString下面是第二个枚举的结果:Equals returns a System.BooleanGetHashCode returns a System.Int32GetType returns a System.TypeReferenceEquals returns a System.BooleanToString returns a System.String请注意,我们通过其索引器填充了字典
如果我们改用 Add 方法,它将引发异常,因为我们反映的对象类重载 Equals 方法,并且您不能将相同的键添加到字典中两次
通过使用索引器,后面的条目将覆盖前面的条目,从而防止此错误
注意您可以通过将每个值元素设置为列表来存储同一键的多个成员:SortedList <string, List<MethodInfo>>扩展我们的示例,下面检索键为“GetHashCode”的 MethodInfo ,就像普通字典一样:Console.WriteLine (sorted ["GetHashCode"]); // Int32 GetHashCode()到目前为止,我们所做的一切都可以使用SortedDictionary<,>
但是,以下两行检索最后一个键和值,仅适用于排序列表:Console.WriteLine (sorted.Keys [sorted.Count - 1]); // ToStringConsole.WriteLine (sorted.Values[sorted.Count - 1].IsVirtual); // True可定制的集合和代理前面各节中讨论的集合类很方便,因为您可以直接实例化它们,但它们不允许您控制在集合中添加或删除项时发生的情况
对于应用程序中的强类型集合,有时需要此控件
例如:在添加或删除项目时触发事件由于添加或删除的项而更新属性检测“非法”添加/删除操作并引发异常(例如,如果操作违反业务规则).NET BCL 在 System.Collections.ObjectModel 命名空间中为此提供了集合类
这些本质上是代理或包装器,通过将方法转发到基础集合来实现 IList<T> 或 IDictionary<,>
每个“添加”、“删除”或“清除”操作都通过一个虚拟方法进行路由,该方法在被覆盖时充当“网关”
可自定义的集合类通常用于公开的集合;例如,在 System.Windows.Form 类上公开的控件集合
Collection<T> 和 CollectionBaseCollection<T> 类是 List<T> 的可自定义包装器
除了实现 IList<T> 和 IList 之外,它还定义了四个额外的虚拟方法和一个受保护的属性,如下所示:public class Collection<T> : IList<T>, ICollection<T>, IEnumerable<T>, IList, ICollection, IEnumerable{ // ... protected virtual void ClearItems(); protected virtual void InsertItem (int index, T item); protected virtual void RemoveItem (int index); protected virtual void SetItem (int index, T item); protected IList<T> Items { get; }}虚拟方法提供了一个网关,您可以通过该网关“挂钩”来更改或增强列表的正常行为
受保护的 Items 属性允许实现者直接访问“内部列表”,该列表用于在内部进行更改,而无需触发虚拟方法
虚拟方法不需要被覆盖;在需要更改列表的默认行为之前,可以将它们单独保留
以下示例演示了 Collection<T 的典型“框架”用法>:Zoo zoo = new Zoo();zoo.Animals.Add (new Animal ("Kangaroo", 10));zoo.Animals.Add (new Animal ("Mr Sea Lion", 20));foreach (Animal a in zoo.Animals) Console.WriteLine (a.Name);public class Animal{ public string Name; public int Popularity; public Animal (string name, int popularity) { Name = name; Popularity = popularity; }}public class AnimalCollection : Collection <Animal>{ // AnimalCollection is already a fully functioning list of animals. // No extra code is required.}public class Zoo // The class that will expose AnimalCollection.{ // This would typically have additional members. public readonly AnimalCollection Animals = new AnimalCollection();}就目前而言,动物收藏并不比一个简单的列表更实用<动物> ;它的作用是为将来的扩展提供基础
为了说明这一点,现在让我们将一个 Zoo 属性添加到 Animal,以便它可以引用它所在的 Zoo 并重写 Collection<Animal 中的每个虚拟方法>以维护该属性:public class Animal{ public string Name; public int Popularity; public Zoo Zoo { get; internal set; } public Animal(string name, int popularity) { Name = name; Popularity = popularity; }}public class AnimalCollection : Collection <Animal>{ Zoo zoo; public AnimalCollection (Zoo zoo) { this.zoo = zoo; } protected override void InsertItem (int index, Animal item) { base.InsertItem (index, item); item.Zoo = zoo; } protected override void SetItem (int index, Animal item) { base.SetItem (index, item); item.Zoo = zoo; } protected override void RemoveItem (int index) { this [index].Zoo = null; base.RemoveItem (index); } protected override void ClearItems() { foreach (Animal a in this) a.Zoo = null; base.ClearItems(); }}public class Zoo{ public readonly AnimalCollection Animals; public Zoo() { Animals = new AnimalCollection (this); }}Collection<T> 也有一个接受现有 IList<T> 的构造函数
与其他集合类不同,提供的列表是而不是的,这意味着后续更改将反映在包装 Collection<T> 中(尽管触发 Collection<T> 的虚拟方法)
相反,通过集合<T>所做的更改将更改基础列表
收藏基地CollectionBase 是 Collection<T> 的非通用版本
这提供了与 Collection<T 相同的大多数功能>但使用起来更笨拙
与模板方法InsertItem 、RemoveItem 、SetItem 和 ClearItem 不同,CollectionBase 具有“钩子”方法,使所需方法的数量增加了一倍:OnInsert 、OnInsertComplete 、OnSet 、OnSetComplete 、OnRemove 、OnRemove Complete 、OnClear 和 OnClearComplete
由于 CollectionBase 是非泛型的,因此在对其进行子类化时还必须实现类型化方法,至少要实现类型化索引器和 Add 方法
KeyedCollection<TKey,TItem> and DictionaryBaseKeyedCollection<TKey,TItem> subclasses Collection<TItem> .它既增加又减去功能
它增加了按键访问项目的能力,就像字典一样
它减去的是代理你自己的内部列表的能力
键控集合与 OrderedDictionary 有一些相似之处,因为它将线性列表与哈希表组合在一起
然而,与OrderedDictionary不同的是,它不实现IDictionary,也不支持键/值的概念
键是从项目本身获取的:通过抽象的 GetKeyForItem 方法
这意味着枚举键控集合就像枚举普通列表一样
你可以最好地将KeyedCollection<TKey,TItem>视为Collection<TItem>加上按键快速查找
因为它子类 Collection<> ,键控集合继承了 Collection<> 的所有功能,除了在构造中指定现有列表的能力
它定义的其他成员如下所示:public abstract class KeyedCollection <TKey, TItem> : Collection <TItem> // ... protected abstract TKey GetKeyForItem(TItem item); protected void ChangeItemKey(TItem item, TKey newKey); // Fast lookup by key - this is in addition to lookup by index. public TItem this[TKey key] { get; } protected IDictionary<TKey, TItem> Dictionary { get; }}GetKeyForItem 是实现者重写的内容,以便从基础对象获取项的键
如果项的键属性发生更改,则必须调用 ChangeItemKey 方法,以便更新内部字典
Dictionary 属性返回用于实现查找的内部字典,该字典是在添加第一项时创建的
可以通过在构造函数中指定创建阈值来更改此行为,延迟创建内部字典直到达到阈值(在此期间,如果按键请求项,则执行线性搜索)
不指定创建阈值的一个很好的理由是,拥有有效的字典对于通过字典的 Keys 属性获取密钥的 ICollection<> 很有用
然后,可以将此集合传递给公共属性
KeyedCollection<,> 最常见的用途是提供可按索引和名称访问的项集合
为了演示这一点,让我们重新访问动物园,这次将 AnimalCollection 实现为 keyedCollection<string,Animal> :public class Animal{ string name; public string Name { get { return name; } set { if (Zoo != null) Zoo.Animals.NotifyNameChange (this, value); name = value; } } public int Popularity; public Zoo Zoo { get; internal set; } public Animal (string name, int popularity) { Name = name; Popularity = popularity; }}public class AnimalCollection : KeyedCollection <string, Animal>{ Zoo zoo; public AnimalCollection (Zoo zoo) { this.zoo = zoo; } internal void NotifyNameChange (Animal a, string newName) => this.ChangeItemKey (a, newName); protected override string GetKeyForItem (Animal item) => item.Name; // The following methods would be implemented as in the previous example protected override void InsertItem (int index, Animal item)... protected override void SetItem (int index, Animal item)... protected override void RemoveItem (int index)... protected override void ClearItems()...}public class Zoo{ public readonly AnimalCollection Animals; public Zoo() { Animals = new AnimalCollection (this); }}以下代码演示了它的用法:Zoo zoo = new Zoo();zoo.Animals.Add (new Animal ("Kangaroo", 10));zoo.Animals.Add (new Animal ("Mr Sea Lion", 20));Console.WriteLine (zoo.Animals [0].Popularity); // 10Console.WriteLine (zoo.Animals ["Mr Sea Lion"].Popularity); // 20zoo.Animals ["Kangaroo"].Name = "Mr Roo";Console.WriteLine (zoo.Animals ["Mr Roo"].Popularity); // 10词典库KeyedCollection的非通用版本称为DictionaryBase
这个遗留类采用了一种非常不同的方法,因为它实现了 IDictionary 并使用笨拙的钩子方法,如 CollectionBase : OnInsert , OnInsertComplete , OnSet , OnSetComplete , OnRemove , OnRemoveComplete , OnClear 和 OnClearComplete(以及另外的 OnGet)
与采用 KeyedCollection 方法相比,实现 IDictionary 的主要优点是,您无需对其进行子类化即可获取密钥
但是由于DictionaryBase的目的是被子类化,所以它根本没有优势
KeyedCollection 中改进的模型几乎可以肯定是因为它是在几年后编写的,事后诸葛亮
DictionaryBase 最好被认为是对向后兼容性有用的
只读集合<T>ReadOnlyCollection<T> 是一个包装器或,它提供集合的只读视图
这对于允许类公开对集合的只读访问权限非常有用,该集合仍然可以在内部更新
只读集合在其构造函数中接受输入集合,并维护对该集合的永久引用
它不采用输入集合的静态副本,因此对输入集合的后续更改可通过只读包装器查看
为了说明这一点,假设您的类希望提供对名为 Names 的字符串列表的只读公共访问
我们可以按如下方式执行此操作:public class Test{ List<string> names = new List<string>(); public IReadOnlyList<string> Names => names;}尽管 Names 返回只读接口,但使用者仍然可以在运行时向下转换为 List<string> 或 IList<string>然后在列表中调用 Add 、Remove 或 Clear
ReadOnlyCollection<T> 提供了更强大的解决方案:public class Test{ List<string> names = new List<string>(); public ReadOnlyCollection<string> Names { get; private set; } public Test() => Names = new ReadOnlyCollection<string> (names); public void AddInternally() => names.Add ("test");}现在,只有 Test 类中的成员才能更改名称列表:Test t = new Test();Console.WriteLine (t.Names.Count); // 0t.AddInternally();Console.WriteLine (t.Names.Count); // 1t.Names.Add ("test"); // Compiler error((IList<string>) t.Names).Add ("test"); // NotSupportedException不可变集合我们刚刚描述了 ReadOnlyCollection<T> 如何创建集合的只读视图
限制写入()集合或任何其他对象的能力可以简化软件并减少错误
集合通过提供初始化后根本无法修改的集合来扩展此原则
如果需要将项添加到不可变集合,则必须实例化新集合,而不动旧集合
不变性是的标志,具有以下:它消除了与更改状态相关的大量错误
它极大地简化了并行性和多线程性,避免了我们在、 章中描述的大多数线程安全问题
它使代码更容易推理
不可变性的缺点是,当您需要进行更改时,必须创建一个全新的对象
这会导致性能下降,尽管我们在本节中讨论了缓解策略,包括重用原始结构部分的能力
不可变集合是 .NET 的一部分(在 .NET Framework 中,它们可通过 NuGet 包获得)
所有集合都在 System.Collections.Immutable 命名空间中定义:类型内部结构不可变数组<T>数组不可变列表<T>AVL 树不可变字典<K,V>AVL 树ImmutableHashSet<T>AVL 树ImmutableSortedDictionary<K,V>AVL 树ImmutableSortedSet<T>AVL 树不可变堆栈<T>链表不可变队列<T>链表ImmutableArray<T> 和 ImmutableList<T> 类型都是 List<T> 的不可变版本
两者都执行相同的工作,但具有不同的性能特征,我们在中讨论过
不可变集合公开一个类似于其可变对应项的公共接口
主要区别在于,似乎用于更改集合的方法(例如 添加或删除 )不会更改原始集合;相反,它们返回一个新集合,其中包含添加或删除请求的项
注意不可变集合可防止添加和删除项;它们不会阻止物品发生变异
若要获得不可变性的全部好处,需要确保只有不可变项才能在不可变集合中结束
创建不可变集合每个不可变集合类型都提供一个 Create<T>() 方法,该方法接受可选的初始值并返回初始化的不可变集合:ImmutableArray<int> array = ImmutableArray.Create<int> (1, 2, 3);每个集合还提供一个 CreateRange<T> 方法,该方法与 Create<T 执行相同的工作> ;不同之处在于它的参数类型是 IEnumerable<T>而不是参数 T[]
您还可以使用适当的扩展方法(ToImmutableArray、ToImmutableList、ToImmutableDictionary等)从现有的IEnumerable<T>创建不可变集合:var list = new[] { 1, 2, 3 }.ToImmutableList();操作不可变集合Add 方法返回一个包含现有元素和新元素的新集合:var oldList = ImmutableList.Create<int> (1, 2, 3);ImmutableList<int> newList = oldList.Add (4);Console.WriteLine (oldList.Count); // 3 (unaltered)Console.WriteLine (newList.Count); // 4Remove 方法以相同的方式运行,返回已删除项的新集合
以这种方式重复添加或删除元素效率低下,因为会为每个添加或删除操作创建一个新的不可变集合
更好的解决方案是调用 AddRange(或 RemoveRange),它接受 IEnumerable<T> 项,这些项都是一次性添加或删除的:var anotherList = oldList.AddRange (new[] { 4, 5, 6 });不可变列表和数组还定义了用于在特定索引处插入元素的 Insert 和 InsertRange 方法、用于在索引处删除的 RemoveAt 方法以及基于谓词删除的 RemoveAll
建设者对于更复杂的初始化需求,每个不可变集合类定义一个对应项
生成器是在功能上等效于可变集合的类,具有类似的性能特征
数据初始化后,调用 .构建器上的 ToImmutable() 返回一个不可变的集合
ImmutableArray<int>.Builder builder = ImmutableArray.CreateBuilder<int>();builder.Add (1);builder.Add (2);builder.Add (3);builder.RemoveAt (0);ImmutableArray<int> myImmutable = builder.ToImmutable();还可以使用生成器对现有不可变更新:var builder2 = myImmutable.ToBuilder();builder2.Add (4); // Efficientbuilder2.Remove (2); // Efficient... // More changes to builder...// Return a new immutable collection with all the changes applied:ImmutableArray<int> myImmutable2 = builder2.ToImmutable();不可变集合和性能大多数不可变集合在内部使用 ,这允许添加/删除操作重用原始内部结构的部分内容,而不必从头开始重新创建整个内容
这将添加/删除操作的开销从潜在的(具有大型集合)减少到,但代价是读取操作速度变慢
最终结果是,大多数不可变集合在读取和写入方面都比可变集合慢
受影响最严重的是 ImmutableList<T> ,对于读取和添加操作,它比 List<T 慢 10 到 200 倍>(取决于列表的大小)
这就是ImmutableArray<T>存在的原因:通过在内部使用数组,它避免了读取操作的开销(其性能可与普通可变数组相媲美)
另一方面,对于添加操作,它比(甚至)ImmutableList<T>慢,因为原始结构都不能重用
因此,当您想要不受阻碍的性能并且不希望后续调用添加或删除(不使用构建器)时,ImmutableArray<> 是可取的
类型读取性能提高性能不可变列表<T>慢慢不可变数组<T>非常快非常慢注意在 ImmutableArray 上调用 Remove 比在 List<T 上调用 Remove 更昂贵>即使在删除第一个元素的最坏情况下也是如此,因为分配新集合会给垃圾回收器带来额外的负载
尽管不可变集合作为一个整体会产生潜在的显著性能成本,但保持总体量级非常重要
在典型的笔记本电脑上,对具有一百万个元素的不可变列表执行 Add 操作仍可能在不到一微秒的时间内发生,而读取操作则在不到 100 纳秒的时间内发生
而且,如果您需要在循环中执行写入操作,则可以使用构建器避免累积成本
以下因素也有助于降低成本:不变性允许轻松并发和并行化(),因此您可以使用所有可用的内核
与可变状态并行化很容易导致错误,并且需要使用锁或并发集合,这两者都会损害性能
通过不可变性,您无需“防御性复制”集合或数据结构来防止意外更改
这是支持在编写Visual Studio的最新部分时使用不可变集合的一个因素
在大多数典型程序中,很少有集合有足够的项目来区分差异
除了Visual Studio之外,性能良好的Microsoft Roslyn工具链也是用不可变的集合构建的,展示了好处如何超过成本
插入平等和秩序在第 的“相等比较”和部分中,我们描述了使类型、可哈希和可比较的标准 .NET 协议
实现这些协议的类型可以在字典或“开箱即用”的排序列表中正常运行
更具体地说:Equals 和 GetHashCode 返回有意义的结果的类型可以用作字典或哈希表中的键
实现 IComparable / IComparable<T> 的类型可以用作任何字典或列表中的键
类型的默认等价或比较实现通常反映该类型最“自然”的内容
但是,有时默认行为不是您想要的
您可能需要一个字典,其字符串类型键的处理不考虑大小写
或者,您可能需要按每个客户的邮政编码排序的客户排序列表
因此,.NET 还定义了一组匹配的“插件”协议
插件协议实现了两件事:它们允许您切换替代等同或比较行为
它们允许您使用具有本质上不相等或可比的键类型的字典或排序集合
插件协议由以下接口组成:IEqualityComparer 和 IEqualityComparer<T>执行插件由哈希表和字典识别IComparer 和 IComparer<T>执行插件被排序的词典和集合识别;另外,数组排序每个接口都有通用和非通用形式
IEqualityComparer 接口在一个名为 EqualityComparer 的类中也有默认实现
此外,还有称为 IStructuralEquatable 和 IStructuralComparable 的接口,它们允许对类和数组进行结构比较的选项
IEqualityComparer 和 EqualComparer相等比较器切换非默认相等和哈希行为,主要用于字典和哈希表类
回想一下基于哈希表的字典的要求
对于任何给定的键,它需要回答两个问题:和另一个一样吗?它的整数哈希码是什么?相等比较器通过实现 IEqualityComparer 接口来回答这些问题:public interface IEqualityComparer<T>{ bool Equals (T x, T y); int GetHashCode (T obj);}public interface IEqualityComparer // Nongeneric version{ bool Equals (object x, object y); int GetHashCode (object obj);}若要编写自定义比较器,请实现其中一个或两个接口(实现这两个接口可提供最大的互操作性)
因为这有点乏味,所以另一种方法是对抽象的 EqualityComparer 类进行子类类,定义如下:public abstract class EqualityComparer<T> : IEqualityComparer, IEqualityComparer<T>{ public abstract bool Equals (T x, T y); public abstract int GetHashCode (T obj); bool IEqualityComparer.Equals (object x, object y); int IEqualityComparer.GetHashCode (object obj); public static EqualityComparer<T> Default { get; }}EqualityComparer 实现了这两个接口;你的工作只是重写这两个抽象方法
Equals 和 GetHashCode 的语义遵循相同的对象规则
等于和反对
GetHashCode ,在第 中描述
在下面的示例中,我们定义一个包含两个字段的 Customer 类,然后编写一个与名字和姓氏匹配的相等比较器:public class Customer{ public string LastName; public string FirstName; public Customer (string last, string first) { LastName = last; FirstName = first; }}public class LastFirstEqComparer : EqualityComparer <Customer>{ public override bool Equals (Customer x, Customer y) => x.LastName == y.LastName && x.FirstName == y.FirstName; public override int GetHashCode (Customer obj) => (obj.LastName + ";" + obj.FirstName).GetHashCode();}为了说明其工作原理,让我们创建两个客户:Customer c1 = new Customer ("Bloggs", "Joe");Customer c2 = new Customer ("Bloggs", "Joe");因为我们没有覆盖对象
等于,正常引用类型相等语义适用:Console.WriteLine (c1 == c2); // FalseConsole.WriteLine (c1.Equals (c2)); // False在中使用这些客户而不指定相等比较器时,相同的默认相等语义适用:var d = new Dictionary<Customer, string>();d [c1] = "Joe";Console.WriteLine (d.ContainsKey (c2)); // False现在,使用自定义相等比较器:var eqComparer = new LastFirstEqComparer();var d = new Dictionary<Customer, string> (eqComparer);d [c1] = "Joe";Console.WriteLine (d.ContainsKey (c2)); // True在此示例中,我们必须小心不要在字典中使用客户的名字或姓氏时更改它;否则,它的哈希代码会改变,字典会中断
平等比较<T>
违约调用 EqualityComparer<T>
默认值返回一个通用相等比较器,您可以将其用作静态对象的替代对象
等于方法
优点是它首先检查 T 是否实现了 IEquatable<T>,如果是,它会调用该实现,避免装箱开销
这在泛型方法中特别有用:static bool Foo<T> (T x, T y){ bool same = EqualityComparer<T>.Default.Equals (x, y); ...ReferenceEqualityComparer.Instance (.NET 5+)从 .NET 5 开始,ReferenceEqualityComparer.Instance 返回始终应用引用相等的相等比较器
对于值类型,其 Equals 方法始终返回 false
IComparer 和 Comparer比较器用于切换已排序词典和集合的自定义排序逻辑
请注意,比较器对于未排序的字典(如字典和哈希表)毫无用处 - 这些字典需要IEqualityComparer来获取哈希码
同样,相等比较器对于排序词典和集合也毫无用处
以下是 IComparer 接口定义:public interface IComparer{ int Compare(object x, object y);}public interface IComparer <in T>{ int Compare(T x, T y);}与相等比较器一样,有一个抽象类可以子类型化而不是实现接口:public abstract class Comparer<T> : IComparer, IComparer<T>{ public static Comparer<T> Default { get; } public abstract int Compare (T x, T y); // Implemented by you int IComparer.Compare (object x, object y); // Implemented for you}下面的示例演示了一个描述愿望的类以及一个按优先级对愿望进行排序的比较器:class Wish{ public string Name; public int Priority; public Wish (string name, int priority) { Name = name; Priority = priority; }}class PriorityComparer : Comparer<Wish>{ public override int Compare (Wish x, Wish y) { if (object.Equals (x, y)) return 0; // Optimization if (x == null) return -1; if (y == null) return 1; return x.Priority.CompareTo (y.Priority); }}对象
等于检查确保我们永远不会与等于方法相矛盾
调用静态对象
在这种情况下,等于方法比调用 x.Equals 更好,因为如果 x 为空,它仍然有效
以下是我们的 PriorityComparer 如何用于对列表进行排序:var wishList = new List<Wish>();wishList.Add (new Wish ("Peace", 2));wishList.Add (new Wish ("Wealth", 3));wishList.Add (new Wish ("Love", 2));wishList.Add (new Wish ("3 more wishes", 1));wishList.Sort (new PriorityComparer());foreach (Wish w in wishList) Console.Write (w.Name + " | ");// OUTPUT: 3 more wishes | Love | Peace | Wealth |在下一个示例中,SurnameComparer 允许您按适合电话簿列表的顺序对姓氏字符串进行排序:class SurnameComparer : Comparer <string>{ string Normalize (string s) { s = s.Trim().ToUpper(); if (s.StartsWith ("MC")) s = "MAC" + s.Substring (2); return s; } public override int Compare (string x, string y) => Normalize (x).CompareTo (Normalize (y));}以下是排序词典中使用的姓氏比较器:var dic = new SortedDictionary<string,string> (new SurnameComparer());dic.Add ("MacPhail", "second!");dic.Add ("MacWilliam", "third!");dic.Add ("McDonald", "first!");foreach (string s in dic.Values) Console.Write (s + " "); // first! second! third!StringComparerStringComparer 是一个预定义的插件类,用于等同和比较字符串,允许您指定语言和区分大小写
StringComparer同时实现了IEqualityComparer和IComparer(及其通用版本),因此您可以将其与任何类型的字典或排序集合一起使用
因为 StringComparer 是抽象的,所以您可以通过其静态属性获取实例
StringComparer.Ordinal 镜像字符串相等比较的默认行为,StringComparer.CurrentCulture 镜像顺序比较的默认行为
以下是其所有静态成员:public static StringComparer CurrentCulture { get; }public static StringComparer CurrentCultureIgnoreCase { get; }public static StringComparer InvariantCulture { get; }public static StringComparer InvariantCultureIgnoreCase { get; }public static StringComparer Ordinal { get; }public static StringComparer OrdinalIgnoreCase { get; }public static StringComparer Create (CultureInfo culture, bool ignoreCase);在下面的示例中,创建了一个不区分大小写的序号字典,使得 dict[“Joe”] 和 dict[“JOE”] 的含义相同:var dict = new Dictionary<string, int> (StringComparer.OrdinalIgnoreCase);在下一个示例中,使用澳大利亚英语对名称数组进行排序:string[] names = { "Tom", "HARRY", "sheila" };CultureInfo ci = new CultureInfo ("en-AU");Array.Sort<string> (names, StringComparer.Create (ci, false));最后一个例子是我们在上一节中编写的 SurnameComparer 的文化感知版本(用于比较适合电话簿列表的名称):class SurnameComparer : Comparer<string>{ StringComparer strCmp; public SurnameComparer (CultureInfo ci) { // Create a case-sensitive, culture-sensitive string comparer strCmp = StringComparer.Create (ci, false); } string Normalize (string s) { s = s.Trim(); if (s.ToUpper().StartsWith ("MC")) s = "MAC" + s.Substring (2); return s; } public override int Compare (string x, string y) { // Directly call Compare on our culture-aware StringComparer return strCmp.Compare (Normalize (x), Normalize (y)); }}等同和等同正如我们中所讨论的,结构默认实现结构比较:如果两个的所有字段都相等,则它们相等
但是,有时结构相等和顺序比较作为其他类型(如数组)的插件选项也很有用
以下接口对此有所帮助:public interface IStructuralEquatable{ bool Equals (object other, IEqualityComparer comparer); int GetHashCode (IEqualityComparer comparer);}public interface IStructuralComparable{ int CompareTo (object other, IComparer comparer);}您传入的 IEqualityComparer / IComparer 将应用于复合对象中的每个单独元素
我们可以通过使用数组来演示这一点
在下面的示例中,我们比较两个数组的相等性,首先使用默认的 Equals 方法,然后使用 IStructuralEquatable 的版本:int[] a1 = { 1, 2, 3 };int[] a2 = { 1, 2, 3 };IStructuralEquatable se1 = a1;Console.Write (a1.Equals (a2)); // FalseConsole.Write (se1.Equals (a2, EqualityComparer<int>.Default)); // True这是另一个示例:string[] a1 = "the quick brown fox".Split();string[] a2 = "THE QUICK BROWN FOX".Split();IStructuralEquatable se1 = a1;bool isTrue = se1.Equals (a2, StringComparer.InvariantCultureIgnoreCase);
教程(数组方法类型元素字典)
(图片来源网络,侵删)

联系我们

在线咨询:点击这里给我发消息