学编程,来源栈;先学习,再交钱
当前系列: C#语法 修改讲义

复习:J&C:集合概述 / 迭代器模式 / ER模型 / 仓储模式


概述

C#几乎已经完全不会使用非泛型的集合了。所有泛型集合都在该命名空间下:

using System.Collections.Generic;


List<T>

List<string> student = new List<string>();//一个空的List
List<string> student = new List<string> { "wf", "xr", "jym" };//初始化List的同时装入3个元素

C#中List数组的使用方式非常相像

student[0] = "dfg";//赋值(或更改)
Console.WriteLine(student[0]);//取值

那为什么要引入List呢,数组不香么?数组真不香,^_^:

更多内置方法

student.Add("xm");   //添加到最后
student.Insert(3, "xm");   //插入到指定位置

student.Remove("xr");
student.RemoveAt(0);
student.clear();

改:同数组,用[],但@想一想@:为什么可以在一个对象上使用方括号呢?复习:索引器,源代码查看演示

查:

Console.WriteLine(student.BinarySearch("dfg"));//二分查找 Console.WriteLine(student.IndexOf("jym"));
Console.WriteLine(student.Contains("jym"));

这里特别要注意的是BinarySearch(),需要List中的元素是有序排列的!

Count和Capacity

复习:为什么用C#“把质数装到数组很难实现?

#体会#:数组,长度是固定的;List,可以一直Add()或Insert()的……

以下结合源代码演示:

  1. List内部仍然使用数组_items存储元素
    private T[] _items;
    索引器中的证据
            public T this[int index] {
                get {
                    // ......
                    return _items[index]; 
  2. 但这个数组的长度是可以改变的,或者准确的说,是可以“更替”的。演示源代码:Add()方法会进行“扩容”
    if (_size == _items.Length) EnsureCapacity(_size + 1);
    说明:List中用_capacity来确定数组的长度,(并且通过属性Capacity暴露
    _items = new T[capacity];
    默认为4,
    private const int _defaultCapacity = 4;
    当数组无法继续装下更多元素时,就会“双倍扩容”,设置新的capacity:
    int newCapacity = _items.Length == 0 ? DefaultCapacity : _items.Length * 2;

演示:Count和Capacity的关系


Dictionary<TKey, TValue>

字典(键值对集合),可以使用自定义的TValue类来无限扩展

实例化一个空的Dictionary

Dictionary<string, int> scores = new Dictionary<string, int>();
构建键值对:
scores["atai"] = 85;
scores["zm"] = 95;
scores["wpz"] = 90;
或者在实例化的同时存入:
    scores = new Dictionary<string, int>
    {
        {"atai", 85 },
        {"zm", 95 }
    };

@试一试@:如果出现重复的键,会发生什么情况?

其他常用方法:(都是围绕key展开的)

scores.Add("wpz", 90);
scores.Remove("wpz");
scores.ContainsKey("zm");

Console.WriteLine(scores["atai"]);


其他集合

HashSet

C#中用得不多,因为它比较“怪异”:当元素出现重复的时候复习,它自动的把重复元素“吞”了,不做任何“提示”:

HashSet<int> ages = new HashSet<int> { 16, 23, 25, 3, 3};
Console.WriteLine(ages.Count);  //4,因为3和3重复
ages.Add(5);
Console.WriteLine(ages.Count);  //5,5是非重复的
ages.Add(16);  //不会报错!
Console.WriteLine(ages.Count); //还是5

其他

  • LinkedList<T>:双向链表
  • Stack<T>:栈
  • Queue<T>:队列
  • SortedXXX<T>:有序集合
  • ……

想不想打人?O(∩_∩)O哈哈~


继承结构

演示:List和Dictionary的祖先类:

  • IList/IDictionary:索引器,根据下标/键进行操作的各种方法
  • ICollection:Count/Add/Remove/Contains……
  • IEnumerable

#体会:继承/抽象的力量#

  • 便于归纳记忆
  • 便于多态:用基类/接口做类型(比如方法参数),就可以被更广泛的使用

小插曲:LinkedList实现了ICollection,但没有Add()方法,为什么?(复习:接口的显式实现

void ICollection<T>.Add(T value)

IEnumerable

复习:迭代器模式

所有集合元素都可以被遍历

ISet<int> set = new HashSet<int> { 1, 7, 3, 12, 8 };
IEnumerator<int> enumerator = set.GetEnumerator();
while (enumerator.MoveNext())
{
    Console.WriteLine(enumerator.Current);
}

这样写代码有点“累赘”,所以C#引入了一种新的书写方式(语法糖):

foreach

foreach (var id in set)//id代表每一次遍历时的集合元素 {
    Console.WriteLine(id);
}

上述foreach在编译后还是会变成while形式。

ILDASM演示:get_Current() 和MoveNext() 被调用

    IL_006f:  br.s       IL_0086
    IL_0071:  ldloc.1
    IL_0072:  callvirt   instance !0 class [System.Runtime]System.Collections.Generic.IEnumerator`1<class CSharp.Student>::get_Current()
    IL_0077:  stloc.2
    IL_0078:  nop
    IL_0079:  ldloc.2
    IL_007a:  callvirt   instance string CSharp.Person::get_Name()
    IL_007f:  call       void [System.Console]System.Console::WriteLine(string)
    IL_0084:  nop
    IL_0085:  nop
    IL_0086:  ldloc.1
    IL_0087:  callvirt   instance bool [System.Runtime]System.Collections.IEnumerator::MoveNext()
    IL_008c:  brtrue.s   IL_0071
    IL_008e:  leave.s    IL_009b

自定义集合

任何一个类,只要实现了IEnumerable,就可以被foreach。演示:

foreach (var id in student)
{
    Console.WriteLine(id);
public class Student : IEnumerable<double>
要实现IEnumerable,就需要一个IEnumerator的对象:
public IEnumerator<double> GetEnumerator()

这个对象得我们自己声明,通常就放在当前类中,形成一个内部类:

class ScoreEnumerator : IEnumerator<double>{

接下来关键是要实现:

  • 一个属性
    public double Current => scores[index];
  • 一个方法
    public bool MoveNext()
    {
        index++;
        return index < scores.Length;

其中,scores是由构造函数传入的,index是我们自己声明的:

private double[] scores;
private int index;
public ScoreEnumerator(double[] scores)
{
    index = 0;
    this.scores = scores;

演示:foreach循环,会依次调用:GetEnumerator() -> MoveNext() -> Current

说明:以上代码
  1. 并不完善:比如会漏掉数组中第一个元素……
  2. 省略掉非泛型部分内容

foreach vs for

#常见面试题:注意和List集合for循环的区别#

  • for循环的迭代基础是累加器和下标(i++)
  • foreach循环的基础是迭代器的MoveNext()
  • foreach里的item不能被赋值(assign)
    foreach (var student in students)
    {
        //student = new Student();
    但是,要区分:
    student = new Student();//原有的student对象被彻底替换
    student.Name += "fg";//仍然保留原来的student对象,只是改变它的Name


yield

当我们不愿意很麻烦的实现IEnumerable时,可以利用:yield

static IEnumerable<int> GetNumbers()
{
    yield return 0;
    yield return 1;
    yield return 2;
    yield return 3;
}

注意:

  • 方法返回的是一个匿名的(系统生成的)IEnumerable<int>实例,实例中按顺序存储上述数据
  • 方法体中多个yield return不会冲突,但不能同时有return和yield return
  • 还可以使用 yield break; 中断循环
    for (int i = 0; i < 5; i++)
    {
        if (i%2 == 0)
        {
            yield break;
        }
        yield return i;
    }


作业

见:J&C:集合概述 / 迭代器模式 / ER模型 / 仓储模式 1-2题

  1. 双向链表能foreach的基础上,为其声明一个扩展方法Max():返回之前双向链表中存贮着最大值的节点。


学习笔记
源栈学历
键盘敲烂,月薪过万作业不做,等于没学

作业

觉得很 ,不要忘记分享哟!

任何问题,都可以直接加 QQ群:273534701

在当前系列 C#语法 中继续学习:

多快好省!前端后端,线上线下,名师精讲

  • 先学习,后付费;
  • 不满意,不要钱。
  • 编程培训班,我就选源栈

更多了解 加:

QQ群:273534701

答疑解惑,远程debug……

B站 源栈-小九 的直播间

写代码要保持微笑 (๑•̀ㅂ•́)و✧

公众号:源栈一起帮

二维码