• C#.Net筑基-集合知识全解


    01、集合基础知识

    .Net 中提供了一系列的管理对象集合的类型,数组、可变列表、字典等。从类型安全上集合分为两类,泛型集合 和 非泛型集合,传统的非泛型集合存储为Object,需要类型转。而泛型集合提供了更好的性能、编译时类型安全,推荐使用。

    .Net中集合主要集中在下面几个命名空间中:

    1.1、集合的起源:接口关系

    • 天赋技能 —— foreach:几乎所有集合都可以用foreach循环操作,是因为他们都继承自IEnumerable接口,由枚举器(IEnumerator)提供枚举操作。

    • 几乎所有集合都提供添加、删除、计数,来自基础接口 ICollectionICollection

    • IListIList 提供了数组的索引器、查找、插入等操作,几乎所有具体的集合类型都实现了该接口。

    • Array 是一个抽象类,是所有数组T[]的基类,她是类型安全的。

    • 推荐尽量使用数组T[]、泛型版的集合,提供了更好的类型安全和性能。

    image.png

    1.2、非泛型集合—— 还有什么存在的价值?

    • 非泛型的Hashtable,Key、Value都是Object类型的,Dictionary 是泛型版本的 Hashtable。

    • ArrayList 是非泛型版本的 List,基本很少使用,也尽量不用。

    ❓既然非泛型版本类型不安全,性能还差,为什么还存在呢?

    主要是历史原因,泛型是.Net2.0 引入的,因此为了向后兼容,依然保留的非泛型版本集合。在接口实现时,非泛型接口一般都是显示实现的,因此基本不会用到。不过在有些场景下,非泛型接口、集合还是有点用的,如类型不固定的集合,或者用接口作为约束条件或类型判断。

    1. ArrayList arr = new ArrayList();
    2. arr.Add(1);
    3. arr.Add("sam");
    4. arr.Add(new Point());
    5. if (arr is IList) {}
    6. class User<T> where T :IList {}

    1.3、CollectionList有何不同?

    ❓两者比较相似,他们到底有什么区别呢?该如何选择?

    • Collection 作为自定义集合基类,内部提供了一些virtual的实现,便于继承实现自己的集合类型。其内部集合用的就是List,如下部分源码 Collection.cs。

    • List 作为集合使用,是最常用的可变长集合类型了,他优化了性能,但是丢失了可扩展性,没有提供任何可以override的成员。

    1. public class Collection<T>
    2. {
    3. public Collection()
    4. {
    5. items = new List();
    6. }
    7. protected virtual void InsertItem(int index, T item)
    8. {
    9. items.Insert(index, item);
    10. }
    11. }

    02、枚举器——foreach的秘密!

    foreach 用来循环迭代可枚举对象,用一种非常简洁、优雅的姿势访问可枚举元素。常用于数组、集合,当然不仅限于集合,只要符合要求枚举要求的都可以。

    image

    image.png

    2.1、IEnumerator枚举器

    枚举可以foreach 枚举的密码是他们都继承自IEnumerable接口,而更重要的是其内部的枚举器 —— IEnumerator。枚举器IEnumerator定义了向前遍历集合元素的基本协议,其申明如下:

    1. public interface IEnumerator
    2. {
    3. object Current { get; }
    4. bool MoveNext();
    5. void Reset(); //这个方法是非必须的,用于重置游标,可不实现
    6. }
    7. public interface IEnumerator<out T> : IDisposable, IEnumerator
    8. {
    9. new T Current { get; }
    10. }

    • MoveNext() 移动当前元素到下一个位置,Current获取当前元素,如果没有元素了,则MoveNext()返回false。注意MoveNext()会先调用,因此首次MoveNext()是把位置移动到第一个位置。

    • Reset()用于重置到起点,主要用于COM互操作,使用很少,可不用实现(直接抛出 NotSupportedException)。

    📢 该接口不是必须的,只要实现了公共的Current、无参MoveNext()成员就可进行枚举操作。

    实现一个获取偶数的枚举器:

    1. void Main()
    2. {
    3. var evenor = new EvenNumbersEnumerator(1, 10);
    4. while (evenor.MoveNext())
    5. {
    6. Console.WriteLine(evenor.Current); //2 4 6 8 10
    7. }
    8. }
    9. //获取偶数的枚举器
    10. public struct EvenNumbersEnumerator : IEnumerator<int> //不继承IEnumerator接口,效果也是一样的
    11. {
    12. private int _start;
    13. private int _end;
    14. private int _position = int.MinValue;
    15. public EvenNumbersEnumerator(int start, int end)
    16. {
    17. _start = start;
    18. _end = end;
    19. }
    20. public int Current => _position;
    21. object IEnumerator.Current => Current; //显示实现非泛型接口,然后隐藏起来
    22. public bool MoveNext()
    23. {
    24. if (_position == int.MinValue)
    25. _position = (int.IsEvenInteger(_start) ? _start : _start + 1) - 2;
    26. _position += 2;
    27. return (_position <= _end);
    28. }
    29. public void Reset() => throw new NotSupportedException();
    30. public void Dispose() { } //IEnumerator 是实现了 IDisposable接口的
    31. }

    2.2、IEnumerable可枚举集合

    IEnumerableIEnumerable是所有集合的基础接口,其核心方法就是 GetEnumerator() 获取一个枚举器。

    1. public interface IEnumerable
    2. {
    3. IEnumerator GetEnumerator();
    4. }
    5. public interface IEnumerable<out T> : IEnumerable
    6. {
    7. new IEnumerator GetEnumerator();
    8. }

    📢 该接口也不是必须的,只要包含public的“GetEnumerator()”方法也是一样的。

    有了 GetEnumerator(),就可以使用foreach来枚举元素了,这里foreach会被编译为 while (evenor.MoveNext()){} 形式的代码。在上面 偶数枚举器的基础上实现 一个偶数类型。

    1. void Main()
    2. {
    3. var evenNumber = new EvenNumbers();
    4. foreach (var n in evenNumber)
    5. {
    6. Console.WriteLine(n); //2 4 6 8 10
    7. }
    8. }
    9. public class EvenNumbers : IEnumerable<int> //不用必须继承接口,只要有GetEnumerator()即可
    10. {
    11. public IEnumerator<int> GetEnumerator()
    12. {
    13. return new EvenNumbersEnumerator(1, 10);
    14. }
    15. IEnumerator IEnumerable.GetEnumerator() //显示实现非泛型接口,然后隐藏起来
    16. {
    17. return GetEnumerator();
    18. }
    19. }

    foreach 迭代其实就是调用其GetEnumerator()CurrentMoveNext()实现的,因此接口并不是必须的,只要有对应的成员即可。

    1. foreach (var n in evenNumber)
    2. {
    3. Console.WriteLine(n); //2 4 6 8 10
    4. }
    5. /************** 上面代码编译后的效果如下:*****************/
    6. IEnumerator<int> enumerator = evenNumber.GetEnumerator();
    7. try
    8. {
    9. while (enumerator.MoveNext ())
    10. {
    11. int i = enumerator.Current;
    12. Console.WriteLine (i);
    13. }
    14. }
    15. finally
    16. {
    17. if (enumerator != null)
    18. {
    19. enumerator.Dispose ();
    20. }
    21. }

    2.3、yield 迭代器

    yield return 是一个用于实现迭代器的专用语句,它允许你一次返回一个元素,而不是一次性返回整个集合。常来用来实现自定义的简单迭代器,非常方便,无需实现IEnumerator接口。

    🔸惰性执行:元素是按需生成的,这可以提高性能并减少内存占用(当然这个要看具体情况),特别是在处理大型集合或复杂的计算时。迭代器方法在被调用时,不会立即执行,而是在MoveNext()时,才会执行对应yield return的语句,并返回该语句的结果。📢Linq里的很多操作也是惰性的。

    🔸简化代码:使用yield return可以避免手动编写迭代器的繁琐过程。

    🔸状态保持yield return自动处理状态保持,使得在每次迭代中保存当前状态变得非常简单。每一条yield return语句执行完后,代码的控制权会交还给调用者,由调用者控制继续。

    yield迭代器方法会被会被编译为一个实现了IEnumerator 接口的私有类,可以看做是一个高级的语法糖,有一些限制(要求):

    • 迭代器的返回类型可以是IEnumerableIEnumerator或他们的泛型版本。还可以用 IAsyncEnumerable 来实现异步的迭代器。

    • yield break 语句提前退出迭代器,不可直接用return,是非法的。

    • yield语句不能和try...catch一起使用。

    1. void Main()
    2. {
    3. var us = new User();
    4. foreach (string name in us)
    5. {
    6. Console.WriteLine(name); //sam kwong
    7. }
    8. foreach (string name in us.GetEnumerator1())
    9. {
    10. Console.WriteLine(name); //1 sam 2
    11. }
    12. foreach (string name in us.GetEnumerator2())
    13. {
    14. Console.WriteLine(name);//KWONG
    15. }
    16. }
    17. public class User
    18. {
    19. private string firstName = "sam";
    20. private string lastName = "Kwong";
    21. public IEnumerator GetEnumerator()
    22. {
    23. yield return firstName;
    24. yield return lastName;
    25. }
    26. public IEnumerable GetEnumerator1() //返回IEnumerable
    27. {
    28. Console.WriteLine("1");
    29. yield return firstName; //第一次执行到这里
    30. Console.WriteLine("2");
    31. yield break; //第二次执行到这里,也是最后一次了
    32. yield return lastName;
    33. }
    34. public IEnumerable<string> GetEnumerator2() //返回IEnumerable
    35. {
    36. yield return lastName.ToUpper();
    37. }
    38. }

    03、集合!装逼了!

    3.1、⭐常用集合类型

    1. ArrayList arr2 = new ArrayList();
    2. arr2.Add(null);
    3. arr2.Add("sam");
    4. arr2.Add(1);
    5. Console.WriteLine(arr2[1]);

    3.2、⭐数组Array[]

    Array 数组是一种有序的集合,通过唯一索引编号进行访问。数组T[]是最常用的数据集合了,几乎支持创建任意类型的数组。Array是所有的数组T[]的(隐式)基类,包括一维、多维数组。CLR会将数组隐式转换为 Array 的子类,生成一个伪类型。

    • 索引从0开始。

    • 定长:数组在申明时必须指定长度,超出长度访问会抛出IndexOutOfRangeException异常。

    • 内存连续:为了高效访问,数组元素在内存中总是连续存储的。如果是值类型数组,值和数组是存储在一起的;如果是引用类型数组,则数组值存储其引用对象的(堆内存)地址。因此数组的访问是非常高效的!

    • 多维数组:矩阵数组 用逗号隔开,int[,] arr = {{1,2},{3,4}};

    • 多维数组:锯齿形数组(数组的数组),int[][] arr =new int[3][];

    1. int[] arr = new int[100]; //申请长度100的int数组
    2. int[] arr2 = new int[]{1,2,3}; //申请并赋值,长度为3
    3. int[] arr3 = {1,2,3}; //同上,前面已制定类型,后面可省略
    4. arr[1] = 1;
    5. Console.WriteLine(arr[2]); //未赋值,默认为0

    📢 几乎大部分编程语言的数组索引都是从0开始的,如C、Java、Python、JavaScript等。当然也有从1开始的,如MATLAB、R、Lua。

    📢 通过上表发现,Array 的很多方法都是静态方法,而不是实例方法,这一点有点困惑,造成了使用不便。而且大部分方法都可以用Linq的扩展来代替。

    image.png

    3.3、Linq扩展

    LINQ to Objects (C#) 提供了大量的对集合操作的扩展,可以使用 LINQ 来查询任何可枚举的集合(IEnumerable)。扩展实现主要集中在 代码 Enumerable 类(源码 Enumerable.cs),涵盖了查询、排序、分组、统计等各种功能,非常强大。

    • 简洁、易读,可以链式操作,简单的代码即可实现丰富的筛选、排序和分组功能。

    • 延迟执行,只有在ToList、ToArray时才会正式执行,和yeild一样的效果。

    1. var arr = Enumerable.Range(1, 100).ToArray(); //生成一个数组
    2. var evens = arr.Where(n => int.IsEvenInteger(n)); //并没有执行
    3. var arr2 = arr.GroupBy(n => n % 10).ToArray();

    04、集合的一些小技巧

    4.1、集合初始化器{}

    同类的初始化器类似,用{}来初始化设置集合值,支持数组、字典。

    1. //数组
    2. int[] arr1 = new int[3] { 1, 2, 3 };
    3. int[] arr2 = new int[] { 1, 2, 3 };
    4. int[] arr4 = { 1, 2, 3 };
    5. //字典
    6. Dictionary<int, string> dict1 = new() { { 1, "sam" }, { 2, "william" } };
    7. Dictionary<int, string> dict2 = new() { [5] = "sam", [6] = "zhangsan" }; //索引器写法
    8. var dict3 = new Dictionary<int, string> { { 1, "sam" }, { 2, "william" } };

    4.2、集合表达式[]

    集合表达式 简化了集合的申明和赋值,直接用[]赋值,比初始化器更简洁,语法形式和JavaScript差不多了。可用于数组、Sapn、List,还可以自定义集合生成器

    1. int[] iarr1 = new int[] { 1, 2, 3, 4 }; //完整的申明方式
    2. int[] iarr2 = { 1, 2, 3, 4 }; //前面声明有类型int[],可省略new
    3. int[] iarr3 = [1, 2, 3, 4]; //简化版的集合表达式
    4. List<string> list = ["a1", "b1", "c1"];
    5. Span<char> sc = ['a', 'b', 'c'];
    6. HashSet<string> set = ["a2", "b2", "c2"];
    7. //..展开运算符,把集合中的元素展开
    8. List<string> list2 = [.. list,..set, "ccc"]; //a1 b1 c1 a2 b2 c2 ccc

    4.3、范围运算符..

    a..b表示a到b的范围(不含b),其本质是 System.Range 类型数据,表示一个索引范围,常用与集合操作。

    • 可省略ab,缺省则表示到边界。

    • 可结合倒数^使用。

    1. int[] arr = new[] { 0, 1, 2, 3, 4, 5 };
    2. Console.WriteLine(arr[1..3]); //1 2 //索引12
    3. Console.WriteLine(arr[3..]); //3 4 5 //索引3到结尾
    4. Console.WriteLine(arr[..]); //全部
    5. Console.WriteLine(arr[^2..]); //4 5 //倒数到2到结尾
    6. var r = 1..3;
    7. Console.WriteLine(r.GetType()); //System.Range

    自定义的索引器也可用用范围Range作为范围参数。

    05、提高集合性能的一些实践

    🚩尽量给集合一个合适的“容量”( capacity),几乎所有可变长集合的“动态变长”其实都是有代价的。他们内部会有一个定长的“数组”,当添加元素较多(大于容量)时,就会自动扩容(如倍增),然后把原有“数组”数据拷贝(搬运)到新“数组“中。

    • 因此在使用可变长集合时,尽量给一个合适的大小,可减少频繁扩容带来的性能影响。当然也不可盲目设置一个比较大的容量,这就很浪费内存空间了。stringBuilder也是一样的道理。

    • 可变长集合的插入、删除效率都不高,因为会移动其后续元素。

    下面测试一下List,当创建一个长度为1000的List时,设置容量(1000)和不设置容量(默认4)的对比。

    1. int max = 10000;
    2. public void List_AutoLength(){
    3. List<int> arr = new List<int>();
    4. for (int i = 0; i < max; i++)
    5. {
    6. arr.Add(i);
    7. }
    8. }
    9. public void List_FixedLength()
    10. {
    11. List<int> arr = new List<int>(max);
    12. for (int i = 0; i < max; i++)
    13. {
    14. arr.Add(i);
    15. }
    16. }

    image.png

    很明显,自动长度的List速度更慢,也消耗了更多的内存。

    image.png

    🚩尽量不创建新数组,使用一些数组方法时需要注意尽量不要创建新的数组,如下面示例代码:

    1. var arr = Enumerable.Range(1, 100).ToArray();
    2. // 需求:对arr进行反序操作
    3. var arr2 = arr.Reverse().ToArray(); //用Linq,创建了新数组
    4. Array.Reverse(arr); //使用Array的静态方法,原地反序,没有创建新对象

    比较一下上面两种反序的性能:

    image.png

    文章转载自:安木夕

    原文链接:https://www.cnblogs.com/anding/p/18229596

    体验地址:引迈 - JNPF快速开发平台_低代码开发平台_零代码开发平台_流程设计器_表单引擎_工作流引擎_软件架构

  • 相关阅读:
    安装mysql详细教程(windows 10安装mysql详细教程新手必看)
    深入理解计算机系统——第四章 Processor Architecture
    java计算机毕业设计web智慧医疗平台设计与实现源码+mysql数据库+系统+lw文档+部署
    深入浅出Spring Boot接口
    高阶函数的简单写法
    python装饰器详解
    Java(十)(网络编程,UDP,TCP)
    Java当中的栈
    spark如何配置checkpoint
    大数据之Kafka
  • 原文地址:https://blog.csdn.net/sdgfafg_25/article/details/139824142