C# 数据与集合相关

内容主要来自:B站:IT萌叔Jack ,《C# 7.0核心技术指南》,《CLR via C# 第4版》

——↓ 引自《C# 7.0核心技术指南》——————————————————

.NET Framework提供了一系列标准的存储和管理对象集合的类型。其中包括可变大小的列表、链表、排序或非排序字典以及数组。在这些类型中,只有数组是C#语言的一部分,而其余的集合只是一些类,我们可以和其他类一样将其实例化。

Framework中的集合类型可以分为以下类别:
· 定义标准集合协议的接口
· 开箱即用的集合类(列表、字典等)
· 编写应用程序特有集合的基类



7.1 枚举

计算中会涉及很多不同类型的集合,有简单数据结构如数组或链表,也有复杂的红黑树和散列表。虽然这些数据结构的内部实现和外部特征差异很大,但几乎都需要实现遍历集合内容这一功能。Framework通过一系列接口(IEnumerable、IEnumerator及其泛型接口)来支持该需求,允许不同的数据结构使用一组通用的遍历API。图7-1展示了部分集合接口:

7.1.1 IEnumerable和IEnumerator

IEnumerator接口定义了以前向方式遍历或枚举集合元素的基本底层协议。其声明如下:

        public interface IEnumerator
        {
          bool MoveNext();
          object Current { get; }
          void Reset();
        }

MoveNext将当前元素或“游标”向前移动到下一个位置,如果集合没有更多的元素,那么它会返回false。

Current返回当前位置的元素(通常需要从object转换为更具体的类型)。在取得第一个元素之前必须先调用MoveNext,即使是空集合也支持这个操作。如果实现了Reset方法,那么它的作用就是将当前位置移回起点,并允许再一次枚举集合。

Reset方法存在的目的主要是COM互操作;而其他情况应当尽量避免直接调用该方法因为它并未得到广泛支持(另外,调用用该方法并没有太大必要,因为完全可以重新实例化一个枚举器来达到相同效果)。 PS:(COM(Component Object Model)互操作是指在不同的应用程序或系统之间交换数据和功能的能力。在.NET中,COM互操作通常指运行时调用实现了COM接口的本地代码或使用本地COM组件来完成某些任务。

Reset方法是IEnumerator接口定义的一种方法,它的主要作用是将枚举器对象重置为其初始状态,以便再次遍历枚举器对象的元素集合。在许多情况下,这种方法可能不被经常使用。但是,在与COM组件交互时,由于COM组件支持枚举器模式,因此Reset方法在这种情况下非常重要,因为它能够确保枚举器对象始终处于正确的状态。同时,由于COM具有相对较低的性能,因此Reset方法还可以通过减少内存和资源消耗来提高性能。)

通常,集合本身并不实现枚举器,而是通过IEnumerable接口提供枚举器:

  public interface IEnumerable
    {
    IEnumerator GetEnumerator();
     }

通过定义一个返回枚举器的方法,IEnumerable灵活地将迭代逻辑转移到了另一个类上。此外,多个消费者可以同时遍历同一个集合而不互相影响。IEnumerable可以看作是“IEnumerator的提供者”,它是所有集合类型需要实现的最基础接口。

以下的例子演示了IEnumerable和IEnumerator的最基本用法:

        string s = "Hello";

        // 由于C#中的字符串类型实现了IEnumerable接口,所以我们可以在对字符串进行操作时调用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 IEnumerable

        foreach (char c in s)
          Console.Write (c + ".");

7.1.2 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指的是数组成员的类型)。

这段内容在原书中还有其他部分,请参阅原书以获取完整信息。在此处,我只是引用了其中的一些段落,如果您需要更全面的了解,请查阅原书。


7.2 ICollection和IList接口

虽然枚举接口提供了一种向前迭代集合的协议,但是它们并没有提供确定集合大小、根据索引访问成员、搜索以及修改集合的机制。为了实现这些功能,.NET Framework定义了ICollection、IList和IDictionary接口。这些接口都支持泛型和非泛型版本。然而,非泛型版本的存在只是为了兼容遗留代码。

这些接口的继承层次如图7-1所示。可以简单总结为:

· IEnumerable<T> (和IEnumerable):提供了最少的功能支持(仅支持元素枚举)
· ICollection<T> (和ICollection):提供一般的功能(例如Count属性)
· IList<T>/IDictionary<K, V> 及其非泛型版本:支持最多的功能(包括根据索引/键进行“随机”访问)

🐧大多数情况下我们不需要实现这些接口。当需要编写一个集合类时,往往会从Collection<T>(请参见7.6节)派生。LINQ还提供了另一种适合于多个场景的方法。上述接口的泛型和非泛型版本的差异很大,特别是ICollection。这其中很多是由历史原因造成的:泛型出现在后,由于借鉴了之前经验,导致在成员的选择上和之前出现了差异(比之前更好了)。因此,ICollection<T>并没有实现ICollection;而IList<T>也没有实现IList;相应的IDictionary<TKey, TValue>也没有实现IDictionary。当然,在有利的情况下,集合类通常同时实现了两种版本的接口。

🐧 若IList<T>实现IList,则当类型转换为IList<T>接口时,会得到一个同时含有Add(T)和Add(object)成员的接口。而这显著破坏了静态类型安全性,因为我们可以将任意类型作为Add方法的参数。本节将介绍ICollection<T>、IList<T>及其非泛型版本;字典相关的接口将在7.5节介绍。

.NET FrameWork并未统一“集合”(collection)和“列表”(list)这两个词汇的使用方式。例如IList<T>接口比ICollection<T>接口的功能更多,因此很容易认为List<T>类比Collection<T>类的功能更强。但事实并非如此。因此,一般认为“集合”和“列表”这两个术语大体上含义是相同的,只在涉及具体类型时例外。

7.2.1 ICollection<T>和ICollection

ICollection<T>标准集合接口可以对其中的对象进行计数。
它可以确定集合大小(Count),
确定集合中是否存在某个元素(Contains),
将集合复制到一个数组(ToArray)以及确定集合是否为只读(IsReadOnly)。
对于可写集合,还可以对集合元素进行添加(Add)、删除(Remove)以及清空(Clear)操作。
由于它实现了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();
        }

7.2.2 IList<T>和IList

IList<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的Add方法的返回类型为void。

通用的List<T>类实现了IList<T>和IList两种接口。C#的数组同样实现了泛型和非泛型版本的IList接口(需要注意,添加和删除元素的方法使用显式接口实现对外隐藏。如果调用这些方法则会抛出NotSupportedException)。

这段内容在原书中还有其他部分,请参阅原书以获取完整信息。在此处,我只是引用了其中的一些段落,如果您需要更全面的了解,请查阅原书。

——↑ 引自《C# 7.0核心技术指南》——————————————————

视频学习:【029-数组】 B站:IT萌叔Jack

数组



数组的添加操作

        public static void Main(string[] args)
        {

            int[] targetArr = { 3, 5, 7, 9, 11 };
            int addIndex = 3;
            int addValue = 55;
            int[] addArray = new int[targetArr.Length + 1];
            for (int i = 0, j = 0; i < targetArr.Length; i++, j++)
            {
                if (i == addIndex)
                {
                    addArray[j] = addValue;
                    j++;
                }
                addArray[j] = targetArr[i];

            }

            for (int i = 0; i < addArray.Length; i++)
            {
                Console.Write($"{addArray[i]}、");
            }
            Console.WriteLine();
        }



数组的删除操作

通过 j 来进行 deleteArray数组的维护


多维数组

多维数组只需要更多的逗号,例如:
[,,,] ;
double [,,,] nums;

二维数组的初试化

 
public static void Main(string[] args)
{
int [,] nums;

int [,] value=new int [3,3]{{1,2,3},{4,5,6},{7,8,9}};

 int [,] array={{1,2,3},{4,5,6},{7,8,9}};

 Console.WriteLine("value[0,0]="+value[0,0]);
}


获取⼆维数组⾏列


int[,] nums = new int[2, 5];

//结果:⾏:2 列:5
Console.WriteLine($"⾏:{nums.GetLength(0)} 列:{nums.GetLength(1)}");

//遍历
int[,] values = { { 1, 2 }, { 4, 5 }, { 7, 8 } };

for (int i = 0; i < values.GetLength(0); i++)
 {
    for (int j = 0; j < values.GetLength(1);j++)
     {
    Console.Write(values[i,j] + " ");
     }
Console.WriteLine();
 }


GetLength(0) 是一个用于获取多维数组第一维长度的方法。在这个特定的例子中,它被用来获取 values 数组中的行数。 由于 values 是一个有着 3 行 2 列的二维数组,调用 GetLength(0) 方法将返回值 3。

交错数组

视频讲解 032-交错数组 B站:IT萌叔Jack

 //交错数组 可以简单理解为数组中的数组 一个数组中存储了多个数组,即:交错数组
public static void Main(string[] args)
{  //定义并初始化交错数组
    int [][] arr = new int[3][];  //定义了一个交错数组,数组中有3个元素,每个元素都是一个数组
    arr[0] = new int[2] {1,2};  //第一个元素是一个数组,数组中有2个元素
    arr[1] = new int[3] {3,4,5};  //第二个元素是一个数组,数组中有3个元素
    arr[2] = new int[4] {6,7,8,9};  //第三个元素是一个数组,数组中有4个元素
    //遍历交错数组
    for (int i = 0; i < arr.Length; i++)
    {
        for (int j = 0; j < arr[i].Length; j++)
        {
            Console.Write(arr[i][j] + " ");
        }
        Console.WriteLine();
    }
}


    //也可以这样定义并初始化交错数组
    int [][] arr2 = new int[3][]
    {
        new int[] {1,2},
        new int[] {3,4,5},
        new int[] {6,7,8,9}
    };



Array类

7.3 Array类

Array类是所有一维和多维数组的隐式基类,它是实现标准集合接口的最基本类型之一。
Array类提供了类型统一性,所以所有的数组对象都能够访问它的一套公共的方法,而与它们的声明或实际的元素类型无关。正是由于数组是如此的基础,因此C#提供了显式的数组声明和初始化语法(请参见第2章、第3章)。

当使用C#语法声明数组时,CLR会在内部将其转换为Array类型的子类,合成一个对应该数组维度和元素类型的伪类型。这个伪类型实现了类型化的泛型集合接口,例如IList<string> 。CLR也会特别处理数组类型的创建,它将数组类型分配到一块连续内存空间中。这样数据的索引就非常高效了,同时不允许在创建后修改数组的大小。

Array实现了泛型接口IList<T>及其非泛型版本。但是IList<T>是显式实现的,以保证Array的公开接口中不包含其中的一些方法,如Add和Remove。这些方法会在固定长度的集合(如数组)上抛出异常。Array类实例也提供了一个静态的Resize方法。但是它实际上是创建一个新数组,并将每一个元素复制到新数组中。Resize方法是很低效的,而且程序中其他地方的数组引用仍然指向原始版本的数组。对于可调整大小的集合,一个更好的方式是使用List<T>类(将在下一节介绍)。

数组可以包含值类型或引用类型的元素。值类型元素存储在数组中,所以一个有三个long整数(每一个8字节)的数组将会占用24个字节的连续内存空间。然而,引用类型在数组中只占用一个引用所需的空间(32位环境是4字节,而64位环境则为8字节)。图7-2说明了下面这个程序在内存中的作用效果:

        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的结果是两个变量引用同一数组。类似的,除非使用自定义相等比较器,否则两个不同的数组在相等比较中总是不相等的。
Framework 4.0提供了一种比较数组元素的比较器,可以通过StructuralComparisons类型访问它:

        object[] a1 = { "string", 123, true };
        object[] a2 = { "string", 123, true };

        Console.WriteLine (a1 == a2);                               // False
        Console.WriteLine (a1.Equals (a2));                         // False

        IStructuralEquatable se1 = a1;
        Console.WriteLine (se1.Equals (a2,
          StructuralComparisons.StructuralEqualityComparer));       // True

数组可以通过Clone方法进行复制,例如arrayB =arrayA.Clone()。
但是,其结果是一个浅表副本(shallow clone),即表示数组本身的内存会被复制。如果数组中包含的是值类型的对象,那么这些值也会被复制;但如果包含的是引用类型的对象,那么只有引用会被复制(结果就是两个数组的元素都引用了相同的对象)。
图7-3演示了以下代码的效果:

        StringBuilder[] builders2 = builders;
        StringBuilder[] shallowClone = (StringBuilder[]) builders.Clone();

如果要进行深度复制即复制引用类型子对象,必须遍历整个数组,然后手动克隆每一个元素。相同的规则也适用于其他.NET集合类型。虽然Array主要是针对32位索引器设计的,但是它也通过一些能够接受Int32和Int64参数的方法实现对64位索引器的部分支持(即令数组在理论上支持多至264个元素)。这些重载方法在实际中的作用是很小的,因为CLR不允许任何对象(包括数组)在大小上超过2GB(不论是32位还是64位运行环境都如此)。

可以通过调用Array.CreateInstance动态创建一个数组实例。该方法可以在运行时指定元素类型、维数,以及通过指定数组下界来实现非零开始的数组。非零开始的数组不符合CLS的规定(Common Language Specification,公共语言规范)。可以使用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[]数组不能够转换为object[],因此,我们需要Array类实现彻底的类型统一。

查找数据 Array

 #region  Array类
//System.Array 该类主要提供一个静态的操作数组的方法
    #endregion

public static void Main(string[] args)
{  
      #region 查找数据 
            int [] score=new int[]{1,2,3,4,5,6,7,8,9,10};

            int index=Array.IndexOf(score,5); // public static int IndexOf<T>(T[] array, T value);    传入数组和值 返回索引下标   如果没找到该值,返回-1
            Console.WriteLine(index);

           int LastIndex = Array.LastIndexOf(scores,10);
            //从后向前找 第一个值为10的下标  结果:10
            Console.WriteLine(LastIndex);

     #endregion

}


Array 拷贝数组

         #region  拷贝数组
            int[] score = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 10 };
            int[] score2 = new int[5];
            Array.Copy(score, score2, 5);
            foreach (var item in score2)
            {
                Console.Write($"{item}、");
            }
            #endregion
//输出结果为 1、2、3、4、5、

——↓ 引自《C# 7.0核心技术指南》——————————————————

7.3.7 复制数组

Array 提供了4个对数组进行浅表复制的方法:Clone、CopyTo、Copy和ConstrainedCopy。 Clone、CopyTo是实例方法;Copy和ConstrainedCopy为静态方法。

Clone方法返回一个全新的(浅表复制的)数组,CopyTo和Copy方法复制数组中的若干连续元素。若复制多维数组则需要将多维数组的索引映射为线性索引。例如,一个3×3数组的中间矩阵(position[1, 1])的索引可以用4表示。其计算方法是1*3+1。这些方法允许源与目标范围重叠而不会造成任何问题。

ConstrainedCopy执行一个原子操作:如果所有请求的元素无法成功复制(例如类型错误)那么操作将会回滚。Array还提供了一个AsReadOnly方法来包装数组以防止其中的元素被重新赋值。


Array 排序数据

 #region  排序数据
            int[] scores = new int[] { 99, 20, 55, 20, 30, 94, 93, 92, 91, 90 };
            Array.Sort(scores); //排序 默认升序
                                //输出  
            Console.WriteLine("Sort排序后:");
            for (int i = 0; i < scores.Length; i++)
            {
                Console.Write($"{ scores[i] }、 ");
            }
            Console.WriteLine();

            Array.Sort(scores, new Comparison<int>((i1, i2) => { return i2 - i1; })); //降序

            Array.Reverse(scores); //反转


            Array.Clear(scores, 0, scores.Length);        //清空 ()      
            #endregion



   //二分查找
        Console.WriteLine("二分查找:");
        int index = Array.BinarySearch(scores, 20);
        Console.WriteLine($"index:{index}");

Array 清空数据

Array.Clear是一个用于清空数组元素的静态方法。使用它可以将一个数组中指定范围内的元素设置为默认值(数组元素类型的默认值)。

以下是Array.Clear方法的语法:

public static void Clear(Array array, int index, int length);

其中,array表示要清空的数组;index表示需要清空的起始位置;length表示需要清空的元素个数。
调用Array.Clear方法后,数组中从起始位置开始的一定数量的元素会被设置为数组元素类型的默认值,例如int数组中的元素会被设置为0,对象数组中的元素会被设置为null,bool数组中的元素会被设置为false等等。

以下是一个示例:


int[] arr = new int[5] { 1, 2, 3, 4, 5 };
Array.Clear(arr, 1, 3);
// 现在 arr 数组的元素为 {1, 0, 0, 0, 5}

在上述示例中,arr数组的第二个、第三个和第四个元素都被设置为默认值0。



集合

System.Collections 名称空间中的几个接口提供了基本的组合功能:
IEnumerable 可以迭代集合中的项。
ICollection(继承于 IEnumerable)可以获取集合中项的个数,并能把项复制到一个简单的数
组类型中。
IList(继承于 IEnumerable 和 ICollection)提供了集合的项列表,允许访问这些项,并提供其
他一些与项列表相关的基本功能。
IDictionary(继承于 IEnumerable 和 ICollection)类似于 IList,但提供了可通过键值(而不是索引)访问的项列表。

System.Array 类实现 IList、ICollection 和 IEnumerable,但不支持 IList 的一些更高级的功能,它表示大小固定的项列表。



视频学习:【079-ArrayList】 B站:IT萌叔Jack

ArrayList


// 创建容器对象 
ArrayList arrayList = new ArrayList(); 
// 增 
arrayList.Add("北京"); 
ArrayList list = new ArrayList(); 
list.Add("上海"); 
list.Add("⼴州"); 
arrayList.AddRange(list); 
arrayList.Insert(1,"深圳"); 
Print(arrayList); // 结果:北京, 深圳, 上海, ⼴州, 
// 查 
Console.WriteLine($"容器容量:{arrayList.Capacity}"); // 结果:容器容量:4 
Console.WriteLine($"数据⻓度:{arrayList.Count}"); // 结果:数据⻓度:4 
Console.WriteLine(arrayList[0]); // 结果:北京 
// 改 
arrayList[0] = "⾹港"; 
Print(arrayList); // 结果:⾹港, 深圳, 上海, ⼴州, 
// 删 
arrayList.Remove("上海"); 
Print(arrayList); // 结果:⾹港, 深圳, ⼴州, 
arrayList.RemoveAt(arrayList.Count - 1);

Print(arrayList); // 结果:⾹港, 深圳 
// foreach遍历 
foreach (string city in arrayList) 
{ 
Console.Write(city + ", "); 
} 
Console.WriteLine(); 
// 迭代器遍历 
IEnumerator enumerator = arrayList.GetEnumerator(); 
while (enumerator.MoveNext()) { 
Console.Write(enumerator.Current + ", "); 
} 
Console.WriteLine(); 
} 
// 定义⼀个遍历集合的⽅法 
public static void Print(ArrayList list) { 
for (int i = 0; i < list.Count; i!%) 
{ 
Console.Write(list[i] + ", "); 
} 
Console.WriteLine(); 
}



视频学习:【082-Hashtable】 B站:IT萌叔Jack

Hashtable

可以通过Key遍历,也可以通过Value遍历,也可以键值对输出。

问题: 如果自定义类作为Hashtable的Key,如何判断是否是同一个key呢?

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace 核心技术指南
{

    class Key
    {
        public char key;
        public Key(char key)
        {
            this.key = key;
        }
        // 重写⽅法
        public override bool Equals(object obj)
        {
            return this.key == (obj as Key).key;
        }
        public override int GetHashCode()
        {
            return this.key.GetHashCode();
        }


        public static void Main(string[] args)
        {
            Hashtable table = new Hashtable();
            table.Add(new Key('H'), "按键H");
            //此时不会报错,因为hashtable不认为两个Key相同,重写Key类的Equals()和GetHashCode()⽅法就会报错
            //重写之后,再次添加会报错
            //table.Add(new Key('H'), "按键H");
            Console.WriteLine(table[new Key('H')]);
        }


    }

}


——↓ 引自《C# 7.0核心技术指南》——————————————————

6.11.3.4重写GetHashCode

GetHashCode是Object类型中的一个虚方法。也许在System.Object这个只拥有很少预定义成员的类型中定义这个应用范围狭窄且用途特定的方法很怪异。因为它只服务于以下两种类型:

 System.Collections.Hashtable
 System.Collections.Generic.Dictionary<TKey, TValue>

这些类型都是散列表(hashtable),即一些使用键来存储和获取元素的集合。散列表支持一个基于键的高效分配元素的方法。 它要求每一个键都是Int32整数,或者称为散列码。 散列码对于每个键来说不需要唯一,但是为了实现最佳的散列表性能,它要尽可能保持差异性。 哈希表在系统中的地位是非常重要的,因此在System.Object中定义了GetHashCode方法,令每一种类型都能够生成散列码。

我们将在7.5节详细介绍散列表。引用类型和值类型都有默认的GetHashCode的实现。这意味只要不重写Equals,就不用重写GetHashCode(如果重写了GetHashCode那么几乎可以肯定Equals方法也会被重写)。下面是重写object.GetHashCode的其他规则:
· 它必须在Equals方法返回true的两个对象上返回相同的值。因此,GetHashCode和Equals通常成对地重写。
· 它不能抛出异常。
· 如果重复调用相同的对象,那么必须返回相同的值(除非对象改变)。

为了实现最佳的散列表性能,GetHashCode应当尽可能避免为两个不同的值返回相同的散列码。
这也是方才在结构体上重写GetHashCode和Equals的第三个原因,其目的就是为了实现更高效的散列算法。
结构体上的默认散列算法是在运行时生成的,它基于结构体中的每一个字段值来计算散列码。

相反,类的默认GetHashCode的实现是基于一个内部对象标识的。这个标识,基于目前的CLR实现,在所有实例中是唯一的。


  如果一个对象作为键添加到字典后其散列码发生了变化,那么这个对象在字典中将不可访问。因此可以基于不可变的字段进行散列码的计算以避免这个问题。我们随后将用一个完整的例子展示重写GetHashCode的方法。

6.11.3.5 重写Equals

object.Equals的逻辑如下:
· 对象不可能是null(除非它是一个可空类型)
· 相等是自反性的(对象与其本身相等)
· 相等是可交换的(如果a.Equals(b)那么b.Equals(a))
· 相等是可传递的(如果a.Equals(b)且b.Equals(c)那么a.Equals(c))
· 相等比较操作是可以重复并且可靠的(它不会抛出异常)。

——↑ 引自《C# 7.0核心技术指南》——————————————————



泛型集合

* List<T>
* Stack<T>
* Queue<T>
* Dictionary<K,V>
* LinkedList<T>
*  自定义泛型
*  泛型约束

——↓ 引自《C# 7.0核心技术指南》——————————————————

7.4.1 List<T>和ArrayList

泛型List和非泛型ArrayList类都提供了一种可动态调整大小的对象数组实现。它们是集合类中使用最广泛的类型,ArrayList实现了IList而List<T>既实现了IList又实现了IList<T>。与数组不同,所有的接口都是公开实现的,而且其方法例如Add和Remove也都是公开可用的。
List<T>和ArrayList在内部都维护了一个对象数组,并在超出容量的时候替换为一个更大的数组。

在集合中追加元素的效率很高(因为数组末尾一般都有空闲的位置),而插入元素的速度会慢一些(因为插入位置之后的所有元素都必须向后移动才能留出插入空间)。与数组一样,对排序的列表执行BinarySearch是非常高效的。但其他情况下就需要检查每一个元素,因而效率就不是那么高了。

🦢如果T是一种值类型,那么List的速度会比ArrayList快好几倍,因为List不需要对元素执行装箱和拆箱操作。

List和ArrayList具有可以接受一个现有集合的构造器,它们会将现有集合中的每一个元素都复制到新的List或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();             // 反转列表中的所有元素。
          public int Capacity { get; set; }  // 强制扩展内部数组的大小。
          public void TrimExcess();          // 将内部数组的大小调整为集合当前包含的元素数。
            public void Clear();               // 删除所有元素,使 Count=0。
        }
        public delegate TOutput Converter <TInput, TOutput>  (TInput input);

除了上述成员,List还提供了Array类型中所有搜索和排序的实例方法。

🦢ArrayList从功能上和List类型相似。当需要一个不共享任何相同基类(object除外)的混合类型元素列表时,这两种类型都是很有用的。在这种情况下,如果需要使用反射机制(见第19章)处理列表,那么选择使用ArrayList更有优势。这是因为相比于List ,反射机制更容易处理非泛型的ArrayList。

这段内容在原书中还有其他部分,请参阅原书以获取完整信息。在此处,我只是引用了其中的一些段落,如果您需要更全面的了解,请查阅原书。



字典

字典是一种集合,其包含的元素均为键值对。字典通常用于查找或用作排序列表。 Framework通过IDictionary和`IDictionary`接口以及一系列通用的字典类定义了标准字典协议。 这些通用的字典类在以下几个方面互不相同: · 元素是否按照有序序列存储 · 元素是否可以以位置(索引)或键进行访问 · 是否为泛型 · 从大字典中用键获得元素值时的快慢

表7-1总结了所有字典类及其在上述方面的区别。我们在1.5GHz的PC上,对一个键值类型均为整数类型的字典进行50000次操作,并对每一种操作进行计时,其性能以毫秒为单位。(使用相同底层结构的集合类型的泛型和非泛型版本的性能差异主要是由装箱造成的,并且仅仅在值类型元素的集合中才会出现。)

以大O表示法来看,用键检索元素的时间复杂度为:
· 对于HashTable、Dictionary、OrderedDictionary为O(1)
· 对于SortedDictionary和SortedList为O(log n)
· 对于ListDictionary(和其非字典的类型如List<T>)为O(n)
其中,n是集合中的元素个数。

7.5.1 IDictionary<TKey, TValue>

IDictionary<TKey, TValue>定义了所有基于键值的集合的标准协议。它扩展了ICollection<T>接口,并增加了方法和属性以便使用任何类型的键进行元素访问:

          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
        }

7.5.3 Dictionary<TKey, TValue> 和HashTable

泛型的Dictionary类(和List集合一样)是使用最广泛的集合之一。它使用一个散列表数据结构来存储键和值,而且快速、高效。

Hashtable(没有非泛型的Dictionary类)是非泛型的Dictionary<TKey, TValue> 。因此当我们提到Dictionary的时候,我们指的是泛型的Dictionary<TKey, TValue>类。
Dictionary同时实现了泛型和非泛型的IDictionary接口。其中,泛型的IDictionary是公开接口。
事实上,Dictionary是泛型IDictionary接口的一个标准实现。

下面的程序演示了它的用法:

        var d = new Dictionary<string, int> ();

        d.Add("One", 1);
        d["Two"] = 2;      // adds to dictionary because "two" not already present
        d["Two"] = 22;     // updates dictionary because "two" is now present
        d["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 ; 3

        foreach (string s in d.Keys) Console.Write (s);       // OneTwoThree
        Console.WriteLine();
        foreach (int i in d.Values) Console.Write (i);        // 1223

字典的底层散列表会将每一个元素的键转换为一个整数散列码,即一个伪的唯一值,然后使用算法将散列码转换为一个散列键。这个散列键会在内部决定元素属于哪一个“桶”。如果这个“桶”包含了不止一个值,那么散列表会在其中执行线性搜索。一个优秀的散列函数不会强求返回唯一的散列码(事实上也不可能做到),而是尽可能地令散列码均匀分布在32位整数范围内,以避免出现元素过度集中(低效)的桶。

字典可以支持任意类型的键,只要可以对键进行相等比较并获得散列码即可。
默认情况下,相等性将由键的object.Equals方法确定,而伪唯一的散列码则通过键的GetHashCode方法获得。如果要改变这种行为,可以重写这些方法,或在创建字典的时候提供一个IEqualityComparer对象。一个常见的应用就是在创建以字符串为键的字典时指定一个不区分大小写的相等比较器:

var d = new Dictionary<string,int>(StringComparer.OrdinalIgnoreCase);

和其他类型的集合一样,字典也可以通过在构造器中指定集合的预期大小来减少或避免进行内部大小调整的操作,从而改善性能。

非泛型版本的字典称为Hashtable。它们在功能上类似,其区别在于其提供的是非泛型的IDictionary接口。我们之前已经讨论过它们的不同之处。

Dictionary和Hashtable的缺点是其中的元素是无序的。而且在添加元素的时候并不保存原始顺序。此外,所有的字典类型都不允许出现重复的键。


这段内容在原书中还有其他部分,请参阅原书以获取完整信息。在此处,我只是引用了其中的一些段落,如果您需要更全面的了解,请查阅原书。

——↑ 引自《C# 7.0核心技术指南》——————————————————


Dictionary

视频学习:【087-Dictionary】 B站:IT萌叔Jack

Dictionary在 System.Collections.Generic命名空间下

简单的基本用法:


Dictionary 深入一点



            //Dictionary 
            //创建空容器
            Dictionary<int, string> dictionary = new Dictionary<int, string>();
            //增
            dictionary.Add(1, "jack");
            dictionary.Add(2, "tom");
            //查
            Console.WriteLine(dictionary.ContainsKey(1));
            Console.WriteLine(dictionary.ContainsValue("jack"));
            //改
            dictionary[1] = "rose";
            //删
            dictionary.Remove(1);
            //遍历
            foreach (KeyValuePair<int, string> item in dictionary)
            {
                Console.WriteLine(item.Key + ":" + item.Value);
            }
            //清空
            dictionary.Clear();





LinkedList

双向链表

视频学习:【088-LinkedList】 B站:IT萌叔Jack

 public LinkedListNode<T>? Find(T value);
//  ?是什么的,怎么用的。 返回值是LinkedListNode 类型的嘛?

?代表为可空类型的标记,它可以用于任何值类型或引用类型。在这里 LinkedListNode<T>? 表示该方法返回一个可为空的 LinkedListNode 类型对象。如果查找到了指定值的节点,则会返回该节点,否则将返回null。

下面是一个例子:

LinkedList<int> linkedList = new LinkedList<int>();
linkedList.AddLast(1);
linkedList.AddLast(2);
linkedList.AddLast(3);

LinkedListNode<int>? node = linkedList.Find(4);
if (node != null)
{
    Console.WriteLine(node.Value);
}
else
{
    Console.WriteLine("not found");
}

在上面的例子中,我们通过 Find() 方法搜索值为4的节点。由于链表中不存在该节点,因此返回值为null。因此,程序会输出 "not found"。

如果要使用可空类型对象的值,通常需要使用“?.“ 和 ”!.“ 操作符进行安全的空值判断和直接获取值。例如,在上面的示例中,我们可以使用代码 node?.Value 来获取node节点的值,node不为空时才进行获取操作,否则返回null。


       static void Main(string[] args)
            {
                LinkedList<string> list = new LinkedList<string>();

                list.AddFirst("jack");
                list.AddLast("tom");

            //public void AddBefore(LinkedListNode<T> node, LinkedListNode<T> newNode); 
           
            list.AddBefore(list.Find("tom"),"Lucy");

            Console.WriteLine(list.Count);   //现在里面是  jack Lucy tom

            Console.WriteLine(list.First);
            //输出: System.Collections.Generic.LinkedListNode`1[System.String]


            list.AddLast("sam");
            Console.WriteLine(list.First.Value);

            Console.WriteLine(list.Last.Previous.Value);  //最后一个节点的前面一个节点 的值

            //改
            list.First.Value = "Peter";

            list.RemoveFirst();
            list.RemoveLast();
            list.Remove("Peter");


            Console.WriteLine("__________");

            //遍历
            foreach (string  name in list)
            {
                Console.WriteLine(name);
            }

        }


——↓ 引自《C# 7.0核心技术指南》——————————————————

7.4.2 LinkedList<T>

LinkedList<T>是一个泛型的双向链表(见图7-4)。双向链表是一系列相互引用的节点,每一个节点都引用前一个节点、后一个节点以及实际存储数据的元素。它的主要优点是元素总能够高效插入到链表的任意位置,因为插入节点只需要创建一个新节点,然后修改引用值。然而查找插入节点的位置会比较慢,因为链表本身并没有直接索引的内在机制。我们必须遍历每一个节点,并且无法执行二分搜索。

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; }
        }

7.4.3 Queue<T>和Queue

Queue<T>和Queue是一种先进先出(FIFO)的数据结构,
它们提供了Enqueue(将一个元素添加到队列末尾)和Dequeue(取出并删除队列的第一个元素)方法。
它们还包括一个只返回而不删除队列第一个元素的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的使用方法:

        var q = new Queue<int> ();
        q.Enqueue (10);
        q.Enqueue (20);
        int[] data = q.ToArray();          // Exports to an array
        Console.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类相似,在内部都使用了一个可根据需要进行大小调整的数组。队列具有一个直接指向头部和尾部元素的索引,因此(除非需要调整内部数组的大小)其入队和出队的操作速度非常快。

7.4.4 Stack<T>和Stack

Stack和Stack是后进先出(LIFO)的数据结构,它们提供了Push(向栈的顶部添加一个元素)和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的使用方法:

        var s = new Stack<int> ();
        s.Push (1);                          // Stack = 1
        s.Push (2);                          // Stack = 1,2
        s.Push (3);                          // Stack = 1,2,3
        Console.WriteLine (s.Count);         // Prints 3
        Console.WriteLine (s.Peek());        // Prints 3,  Stack = 1,2,3
        Console.WriteLine (s.Pop());         // Prints 3,  Stack = 1,2
        Console.WriteLine (s.Pop());         // Prints 2,  Stack = 1
        Console.WriteLine (s.Pop());         // Prints 1,  Stack = <empty>
        Console.WriteLine (s.Pop());         // throws exception

数组和链表数据结构的区别? 数组:地址连续、长度不可变,适合查询 链表:地址不连续,长度可变化;使用的时候更适合增删改查的操作。

自定义泛型

泛型类

泛型接口



协变out和逆变in

语法:协变out和逆变in是用于修饰泛型的
使用:只能用于接口和委托,主要用于限定泛型的使用场景

视频学习:【094-协变out和逆变in】 B站:IT萌叔Jack

using System;

namespace 协变out和逆变in
{

    //接口
    interface I_Out<out T>
    {
        T Move();  //返回值为T类型
    }

    interface I_In<in T>
    {
        void Run(T speed);  //参数为T类型
    }

    delegate R CustomDelegate<in V, in E, out R>(V v, E e);


    class Program
    {
        //根据委托的类型 定义存储函数

        public static string End(string str, char ch)
        {
            return str + ch;
        }

        static void Main(string[] args)
        {
            CustomDelegate<string, char, string> custom = End;
            Console.WriteLine(custom("jack", '$'));

        }
    }
}
 

posted @ 2023-03-28 00:52  专心Coding的程侠  阅读(53)  评论(0编辑  收藏  举报