C# 系列 -- 进阶知识

3,377 阅读41分钟

上一篇:C# 系列 -- 基础知识

泛型

泛型顾名思义就是允许我们编写一个可以与 任何数据类型 一起工作的类或方法

泛型类

什么是泛型类

泛型类是一种可以存储任何类型的类。泛型类使用尖括号 <T> 来表示类型参数。例如,您可以创建一个泛型类 MyClass<T>,如下所示:

public class MyClass<T>
{
    private T data;

    public MyClass(T value)
    {
        data = value;
    }

    public T GetData()
    {
        return data;
    }
}

我们可以使用任何类型创建 MyClass<T> 的实例,例如 MyClass<int>``MyClass<string> 或 MyClass<MyCustomType>

特点

  • 泛型类增加了可重用性。 类型参数越多意味着它变得越可重用。 但是,过多的泛化会使代码难以理解和维护
  • 泛型类可以是其他泛型或非泛型类或抽象类的基类
  • 泛型类可以从其他泛型或非泛型接口、类或抽象类派生

实践

单个泛型参数

先定义一个类:MyList

public class MyList<T> // 通过 T 把类型传进来
{
    private T[] list = new T[4]; // 当 T 是 int 的时这就相当于 int[] list = new int[]

    public T this[int index]
    {
        get => list[index];
        set => list[index] = value;
    }

    public T this[string index]
    {
        get
        {
            var i = Convert.ToInt32(index);
            return list[i];
        }
        set
        {
            var i = Convert.ToInt32(index);
            list[i] = value;
        }
    }
}

可以用作数组,也可以用作字符串数组:

class Program
{
    static void Main(string[] args)
    {
        var list1 = new MyList<int>(); 
        list1[0] = 1;
        list1[1] = 2;
        list1[2] = 3;
        list1[3] = 4;
        for (int i = 0; i < 4; i++)
        {
            Console.Write(list1[i.ToString()] + " ");
        }
        Console.Write('\n');
        var list2 = new MyList<string>();
        // 可以把 int 改成 string, 当然也可以是其它类型如 float decimal 等
        list2[0] = "a";
        list2[1] = "b";
        list2[2] = "c";
        list2[3] = "d";
        for (int i = 0; i < 4; i++)
        {
            Console.Write(list2[i.ToString()] + " ");
        }
    }
}

多个泛型参数

KeyValuePair 是 一个系统类。我们看到它使用了两个类型参数,并且它的类型参数名不在是T了,变成了 TKey,TValue。

class KeyValuePair<TKey, TValue>
{
    public TKey Key { get; set; }
    public TValue Value { get; set; }
}

再来看其它的一个例子, 在 Restful的Api当中,我们有时候会返回一个特定的格式。我们就可以把返回类型 定义在泛型类型参数里。

class Program
{
    static void Main(string[] args)
    {
        var vm = new ResultVm<DateTime>(DateTime.Now);
        Console.WriteLine(vm.PlayLoad);

    }
}

public class ResultVm<T1>
{
    public ResultVm(T1 playLoad) //构造器这边不需要<>这个东西
    {
        this.PlayLoad = playLoad;
    }

    public T1 PlayLoad { get; set; }

    public List<string> ErrorMessages { get; set; }
}

泛型方法

什么是泛型方法

泛型方法是一种可以使用任何类型的参数的方法。泛型方法使用尖括号 <T> 来表示类型参数。例如,您可以创建一个泛型方法 Swap<T>,如下所示:

public static void Swap<T>(ref T a, ref T b)
{
    T temp = a;
    a = b;
    b = temp;
}

您可以使用任何类型调用 Swap<T> 方法,例如 Swap<int>(ref a, ref b)Swap<string>(ref a, ref b) 或 Swap<MyCustomType>(ref a, ref b)

实践

先定义一个类:MyList

class MyList<T>
{
    private T[] _data = new T[10];
    
    public void AddOrUpdate(int index, T item)
    {
        if(index >= 0 && index < 10)
            _data[index] = item;
    }

    public T GetData(int index)
    {
        if(index >= 0 && index < 10)
            return _data[index];
        else 
            return default(T);
    }
}

上面的 AddorUpdate() 和 GetData() 方法是泛型方法

item 参数的实际数据类型将在实例化 DataStore 类时指定:

class Program
{
    static void Main(string[] args)
    {
        var names = new MyList<string>();
        names.AddOrUpdate(0, "Ma");
        names.AddOrUpdate(1, "Lema");
        names.AddOrUpdate(2, ".net");

        var empIds = new MyList<int>();
        empIds.AddOrUpdate(0, 50);
        empIds.AddOrUpdate(1, 65);
        empIds.AddOrUpdate(2, 89);
        var id = empIds.GetData(0);
    }
}

泛型参数类型可以与带或不带非泛型参数和返回类型的多个参数一起使用。 以下是有效的泛型方法重载

public void AddOrUpdate(int index, T data) { }
public void AddOrUpdate(T data1, T data2) { }
public void AddOrUpdate<U>(T data1, U data2) { }
public void AddOrUpdate(T data) { }

泛型接口

什么是泛型接口

泛型接口是一种可以使用任何类型的接口。泛型接口使用尖括号 <T> 来表示类型参数。例如,您可以创建一个泛型接口 IRepository<T>,如下所示:

public interface IRepository<T>
{
    void Add(T item);
    void Remove(T item);
    T Find(Func<T, bool> predicate);
}

您可以使用任何类型实现 IRepository<T> 接口,例如 IRepository<int>IRepository<string> 或 IRepository<MyCustomType>

泛型约束

什么是泛型约束

C# 允许我们使用约束来限制客户端代码在实例化泛型类型时 指定某些类型。 如果您尝试使用指定约束不允许的类型来实例化泛型类型,则会出现 编译时错误

可以使用泛型类型名称后的 where 子句对泛型类型指定 一个或多个 约束。

GenericTypeName<T> where T  : contraint1, constraint2

以下示例演示了在实例化泛型类时具有引用类型约束的泛型类

class ResultVm<T> where T : class
{
    public T Payload { get; set; }
}

一些约束

约束描述
where T : class指定泛型类型参数 T 必须是引用类型(如类、接口、委托或数组)
where T : struct指定泛型类型参数 T 必须是值类型(如结构体或枚举)
where T : new()指定泛型类型参数 T 必须具有无参数的公共构造函数
where T : base-class-name指定泛型类型参数 T 必须从指定的基类(如 MyBaseClass)继承
where T : interface-name指定泛型类型参数 T 必须实现指定的接口(如 IMyInterface

实践

public class MyClass<T> where T : class, new()
{
    private T data;

    public MyClass()
    {
        data = new T(); // 使用无参数的公共构造函数创建 T 类型的实例
    }

    public void SetData(T value)
    {
        data = value;
    }

    public T GetData()
    {
        return data;
    }
}

在上面的示例中,我们定义了一个泛型类 MyClass<T>,并为泛型类型参数 T 添加了两个约束:T 必须是引用类型,并且具有无参数的公共构造函数。这样,我们可以在 MyClass<T> 类中使用 new T() 创建 T 类型的实例。

集合

集合分类

C# 包括存储一系列值或对象的专用类,称为集合。 C# 中有三种类型的集合:非泛型集合,泛型集合和并发集合。 System.Collections 命名空间包含非泛型集合类型,System.Collections.Generic 命名空间包含泛型集合类型。 System.Collections.Concurrent 下面包含并发集合(都是泛型了)

在大多数情况下,建议使用泛型集合,因为它们比非泛型集合执行得更快,还可以得到强类型的好处。(打点,编译出错提示)

另外,在有并发多线程,Task的场景下可以使用 并发集合

非泛型集合

非泛型集合描述
ArrayListArrayList 存储任何类型的对象,如数组。但是,不需要像数组一样指定 ArrayList 的大小,因为它会自动增长。
HashtableHashtable 存储键值对。它通过比较键的哈希值来检索值。
SortedListSortedList 存储键值对。默认情况下,它会自动按键的升序排列元素。
QueueQueue以 FIFO 样式(先进先出)存储值。它保持添加值的顺序。它提供了一个 Enqueue() 方法来添加值和一个 Dequeue() 方法来从集合中检索值。 C# 包括泛型和非泛型队列。
StackStack 以 LIFO 样式(后进先出)存储值。它提供了 Push() 方法来添加值和 Pop() 和 Peek() 方法来检索值。 C# 包括泛型和非泛型堆栈。
BitArrayBitArray 管理一个紧凑的位值数组,这些值表示为布尔值,其中 true 表示该位打开 (1),false 表示该位关闭 (0)。

泛型集合

C# 在 System.Collections.Generic 命名空间中包含以下泛型集合类。

泛型集合描述
List泛型 List 包含指定类型的元素。 当您在其中添加元素时,它会自动增长。
Dictionary<TKey,TValue>Dictionary<TKey,TValue> 包含键值对。
SortedList<TKey,TValue>SortedList<TKey,TValue> 存储键值对。 默认情况下,它会自动按键的升序添加元素。
QueueQueue 以 FIFO 样式(先进先出)存储值。 它保持添加值的顺序。 它提供了一个 Enqueue() 方法来添加值和一个 Dequeue() 方法来从集合中检索值。
StackStack 将值存储为 LIFO(后进先出)。 它提供了 Push() 方法来添加值和 Pop() 和 Peek() 方法来检索值。
HashsetHashset 包含非重复元素。 它消除了重复的元素。

并发集合

并发集合描述
ConcurrentBag<List 的并发版本
ConcurrentDictionary<TKey,TValue>Dictionary<TKey,TValue> 的并发版本
ConcurrentQueueQueue 的并发版本
ConcurrentStackStack 的并发版本
BlockingCollection先进先出,主要用来做生产消费者模式,类似于 Goang channel
Channel先进先出,主要用来做生产消费者模式,跟 Goang channel更像了

列表 List

List 是一个强类型对象的集合,可以通过索引访问,并具有排序、搜索和修改列表的方法。 它是 System.Collection.Generic 命名空间下的 ArrayList 的泛型版本。 有了这个之后我们基本不在用ArrayList了。

特征

  • List 等效于 ArrayList
  • List 可以包含 指定类型 的元素。 它提供编译时类型检查并且不执行装箱拆箱,因为它是通用的。
  • 可以使用 Add()、AddRange() 方法或集合初始值设定项语法 添加元素
  • 可以通过传递索引来访问元素,例如 我的列表[0]。 索引从零开始
  • List 比 ArrayList 执行得 更快,更不容易出错

构造函数

构造函数描述
List()初始化 List 类的新实例,该实例为空并且具有默认初始容量。
List(IEnumerable)初始化 List 类的新实例,该实例包含从指定集合复制的元素并且具有足够的容量来容纳所复制的元素。
List(Int32)初始化 List 类的新实例,该实例为空并且具有指定的初始容量。

属性

属性描述
Capacity获取或设置该内部数据结构在不调整大小的情况下能够容纳的元素总数。
Count获取 List 中包含的元素数。
Item[Int32]获取或设置指定索引处的元素。

方法

方法功能描述
Add(T)将对象添加到 List 的结尾处。
AddRange(IEnumerable)将指定集合的元素添加到 List 的末尾。
AsReadOnly()返回当前集合的只读 ReadOnlyCollection 包装器。
BinarySearch(Int32, Int32, T, IComparer)使用指定的比较器在已排序 List 的某个元素范围中搜索元素,并返回该元素从零开始的索引。
BinarySearch(T)使用默认的比较器在整个已排序的 List 中搜索元素,并返回该元素从零开始的索引。
BinarySearch(T, IComparer)使用指定的比较器在整个已排序的 List 中搜索元素,并返回该元素从零开始的索引。
Clear()从 List 中移除所有元素。
Contains(T)确定某元素是否在 List 中。
ConvertAll(Converter<T,TOutput>)将当前 List 中的元素转换为另一种类型,并返回包含已转换元素的列表。
CopyTo(Int32, T[], Int32, Int32)从目标数组的指定索引处开始,将元素的范围从 List 复制到兼容的一维数组。
CopyTo(T[])从目标数组的开头开始,将整个 List 复制到兼容的一维数组。
CopyTo(T[], Int32)从目标数组的指定索引处开始,将整个 List 复制到兼容的一维数组。
Equals(Object)确定指定对象是否等于当前对象。(继承自 Object)
Find(Predicate)搜索与指定谓词所定义的条件相匹配的元素,并返回整个 List 中的第一个匹配元素。
FindAll(Predicate)检索与指定谓词定义的条件匹配的所有元素。
FindIndex(Int32, Int32, Predicate)搜索与指定谓词所定义的条件相匹配的一个元素,并返回 List 中从指定的索引开始、包含指定元素个数的元素范围内第一个匹配项的从零开始的索引。
FindIndex(Int32, Predicate)搜索与指定谓词所定义的条件相匹配的元素,并返回 List 中从指定索引到最后一个元素的元素范围内第一个匹配项的从零开始的索引。
FindIndex(Predicate)搜索与指定谓词所定义的条件相匹配的元素,并返回整个 List 中第一个匹配元素的从零开始的索引。
FindLast(Predicate)搜索与指定谓词所定义的条件相匹配的元素,并返回整个 List 中的最后一个匹配元素。
FindLastIndex(Int32, Int32, Predicate)搜索与指定谓词所定义的条件相匹配的元素,并返回 List 中包含指定元素个数、到指定索引结束的元素范围内最后一个匹配项的从零开始的索引。
FindLastIndex(Int32, Predicate)搜索与由指定谓词定义的条件相匹配的元素,并返回 List 中从第一个元素到指定索引的元素范围内最后一个匹配项的从零开始的索引。
FindLastIndex(Predicate)搜索与指定谓词所定义的条件相匹配的元素,并返回整个 List 中最后一个匹配元素的从零开始的索引。
ForEach(Action)对 List 的每个元素执行指定操作。
GetEnumerator()返回循环访问 List 的枚举数。
GetHashCode()作为默认哈希函数。(继承自 Object)
GetType()获取当前实例的 Type。(继承自 Object)
IndexOf(T, Int32)搜索指定对象并返回 List 中从指定索引到最后一个元素这部分元素中第一个匹配项的从零开始索引。
IndexOf(T, Int32, Int32)搜索指定对象并返回 List 中从指定索引开始并包含指定元素数的这部分元素中第一个匹配项的从零开始索引。
Insert(Int32, T)将元素插入 List 的指定索引处。
InsertRange(Int32, IEnumerable)将集合中的元素插入 List 的指定索引处。
LastIndexOf(T)搜索指定对象并返回整个 List 中最后一个匹配项的从零开始索引。
LastIndexOf(T, Int32)搜索指定对象并返回 List 中从第一个元素到指定索引这部分元素中最后一个匹配项的从零开始的索引。
LastIndexOf(T, Int32, Int32)搜索指定对象并返回 List 中到指定索引为止包含指定元素数的这部分元素中最后一个匹配项的从零开始索引。
MemberwiseClone()创建当前 Object 的浅表副本。(继承自 Object)
RemoveAll(Predicate)移除与指定的谓词所定义的条件相匹配的所有元素。
RemoveAt(Int32)移除 List 的指定索引处的元素。
RemoveRange(Int32, Int32)从 List 中移除一系列元素。
Reverse()将整个 List 中元素的顺序反转。
Reverse(Int32, Int32)将指定范围中元素的顺序反转。
Sort()使用默认比较器对整个 List 中的元素进行排序。
Sort(Comparison)使用指定的 Comparison,对整个 List 中的元素进行排序。
Sort(IComparer)使用指定的比较器对整个 List 中的元素进行排序。
Sort(Int32, Int32, IComparer)使用指定的比较器对 List 中某个范围内的元素进行排序。
ToArray()将 List 的元素复制到新数组中。
ToString()返回表示当前对象的字符串。(继承自 Object)
TrueForAll(Predicate)确定 List 中的每个元素是否都与指定谓词定义的条件匹配。

实践

创建一个 List

List<T> 是一个泛型集合,因此您需要为其可以存储的数据类型指定一个类型参数。 以下示例展示如何创建列表和添加元素。

static void Main(string[] args)
{
    var numberList = new List<int>();
    numberList.Add(1); // 通用 add添加元素
    numberList.Add(3);
    numberList.Add(5);
    numberList.Add(7);

    var cities = new List<string>();
    cities.Add("北京");
    cities.Add("广州");
    cities.Add("上海");
    cities.Add("厦门");
    cities.Add(null); // null是允许的

    // 使用 集合初始值设定项 语法来添加元素 
    var bigCities = new List<string>()
        {
            "北京",
            "广州",
            "上海",
            "厦门"
        };
}

在上面的例子中,我们使用 var numberList = new List<int>(); 来创建一个 int 类型的集合。 同样,我们也创建了cities 和 bigCities 都是字符串类型集合。另外,可以使用 Add() 方法或集合初始值设定项语法在列表中添加元素。

我们还可以使用集合初始值设定项语法添加自定义类的元素。 下面在 List 中添加 Student 类的对象。

public class Student
{
    public int Id { get; set; }

    public string Name { get; set; }

}
class Program
{
    static void Main(string[] args)
    {
        var students = new List<Student>() 
        {
            new Student(){ Id = 1, Name="Ma"},
            new Student(){ Id = 2, Name="Le"},
            new Student(){ Id = 3, Name="Ma2"},
            new Student(){ Id = 4, Name="Hello"}
        };

    }
}

添加一组数据到 List 当中

使用 AddRange(IEnumerable<T> collection) 方法将数组或其他集合中的所有元素添加到 List。

static void Main(string[] args)
{
    var names = new List<string> { "baidu", "qq", "malema" };
    var popularNames = new List<string>();
    popularNames.AddRange(names);
}

访问 List 元素

可以通过索引、for/foreach 循环和使用 LINQ 查询访问List。 List的索引从零开始。 与数组相同可以通过索引器来访问单个元素。 也可以使用 foreach 或 for 循环迭代 List 集合。

var names = new List<string> { "baidu", "qq", "malema" };
var popularNames = new List<string>();
popularNames.AddRange(names);
Console.WriteLine(names[0]); //baidu
Console.WriteLine(names[1]); //qq
foreach (var item in popularNames)
{
    Console.WriteLine(item);
}
popularNames.ForEach(it => Console.WriteLine(it));

for (int i = 0; i < names.Count; i++)
    Console.WriteLine(names[i]); //省略了大括号,不推荐

使用 Linq 语法来检索

例子 1

static void Main(string[] args)
{
    var names = new List<string> { "baidu", "qq", "malema", "tencent", "alibaba" };

    var hasAList = names.Where(x => x.Contains("a"));
    //string.Join是可以把集合合拼起来。
    Console.WriteLine(string.Join(",", hasAList)); // baidu,malema,alibaba

    var query = from it in hasAList where it.Contains('a') select it; // 跟sql有类似的语法
    Console.WriteLine(string.Join(",", hasAList)); // baidu,malema,alibaba
}

例子 2

static void Main(string[] args)
{
    var students = new List<Student>()
    {
        new Student(){ Id = 1, Name="Ma"},
        new Student(){ Id = 2, Name="Le"},
        new Student(){ Id = 3, Name="Ma2"},
        new Student(){ Id = 4, Name="Hello"},
        new Student(){ Id = 5, Name="linq"}
    };

    var list = students.Where(it => it.Id > 3).Select(x => x.Name);
    Console.WriteLine(string.Join(",", list));  // Hello,linq
}

在任意位置插入一个新的元素

下面的代码就将 student 插入到了第一个 student Ma 之前了

static void Main(string[] args)
{
    var students = new List<Student>()
    {
        new Student(){ Id = 1, Name="Ma"},
        new Student(){ Id = 2, Name="Le"},
        new Student(){ Id = 3, Name="Ma2"},
        new Student(){ Id = 4, Name="Hello"},
        new Student(){ Id = 5, Name="linq"}
    };

    var student = new Student() { Name = "m-a-l-e-m-a" };
    students.Insert(0, student);
}

从 List 当中移除一个元素

我们可以使用RemoveAt(index) Remove(T item) 如下我们通过两种方式来移除元素

static void Main(string[] args)
{
    var students = new List<Student>()
    {
        new Student(){ Id = 1, Name="Ma"},
        new Student(){ Id = 2, Name="Le"},
        new Student(){ Id = 3, Name="Ma2"},
        new Student(){ Id = 4, Name="Hello"},
        new Student(){ Id = 5, Name="linq"}
    };

    students.RemoveAt(0); //移除第一个元素
    Console.WriteLine(string.Join(",", students.Select(x => x.Name)));//Le,Ma2,Hello,linq
    students.Remove(students[0]);// 再次移除第一个元素
    Console.WriteLine(string.Join(",", students.Select(x => x.Name)));//Ma2,Hello,linq 

    //先找出要移除的元素
    var linqStudent = students.FirstOrDefault(it => it.Name == "linq");
    students.Remove(linqStudent);
    Console.WriteLine(string.Join(",", students.Select(x => x.Name)));//Ma2,Hello
}

传统方法 移除所有符合条件的

static void Main(string[] args)
{
    var students = new List<Student>()
    {
        new Student(){ Id = 1, Name="Ma"},
        new Student(){ Id = 2, Name="Le"},
        new Student(){ Id = 3, Name="Ma2"},
        new Student(){ Id = 4, Name="Hello"},
        new Student(){ Id = 5, Name="linq"}
    };

    foreach (var item in students.ToList())
    {
        // 为什么要在students后面加上.ToList因为不加的话会报错
        // System.InvalidOperationException:“Collection was modified; enumeration operation may not execute.”
        if (item.Name == "linq")
        {
            students.Remove(item);
        }
    }
}

可以用 students.RemoveAll(it => it.Name == "linq"); 代替上面的移除代码

判断元素是否存在

  • 简单类型
var numbers = new List<int>() { 10, 20, 30, 40 };
numbers.Contains(10); // returns true
numbers.Contains(11); // returns false
numbers.Contains(20); // returns true
  • 复杂类型
static void Main(string[] args)
{
    var students = new List<Student>()
    {
        new Student(){ Id = 1, Name="Ma"},
        new Student(){ Id = 2, Name="Le"},
        new Student(){ Id = 3, Name="Ma2"},
        new Student(){ Id = 4, Name="Hello"},
        new Student(){ Id = 5, Name="linq"}
    };

    var has = students.Any(x => x.Name == "linq"); //true
}

有序列表 SortedList

特征

  • SortedList<TKey, TValue> 是按键排序的键值对数组。
  • 添加元素后立即对其进行排序。 根据 IComparer 按升序对原始类型键和对象键进行排序。
  • 必须是唯一的,不能为空。
  • 值可以为 重复
  • 可以通过在索引器 mySortedList[key] 中传递关联的键来访问值
  • 包含 KeyValuePair<TKey, TValue> 类型的元素
  • 它比 SortedDictionary<TKey,TValue> 使用更少的内存
  • 排序后的数据 检索速度更快,而 SortedDictionary<TKey, TValue> 在插入和删除键值对方面更快。

属性

属性描述
Capacity获取或设置排序列表的容量
Count获取包含在排序列表元素的数量
IsFixedSize获取一个值,指示排序列表是否具有固定大小
IsReadOnly获取一个值,指示排序列表是否为只读
Item获取和设置与SortedList中的特定键关联的值
Keys获取的排序列表的键
Values获取的排序列表(SortedList)中的值

方法

方法描述
Add( object key, object value )将带有指定键和值到排序列表的元素
Clear()将删除 SortedList 的所有元素
ContainsKey( object key )确定 SortedList 中是否包含一个特定的键
ContainsKey( object key )确定 SortedList 中是否包含一个特定的键
ContainsValue( object value )确定 SortedList 是否包含特定的值
GetByIndex( int index )获取 SortedList 中指定索引处的值
GetKey( int index )获取 SortedList 中指定索引处的键
GetKeyList()获取 SortedList 的键
GetValueList()获取 SortedList 中的值
IndexOfKey( object key )返回在排序列表中指定键从零开始的索引
IndexOfValue( object value )返回在 SortedList 中指定的值第一次出现的从零开始的索引
Remove( object key )删除从 SortedList 表中指定键的元素
RemoveAt( int index )删除 SortedList 中指定索引处的元素
TrimToSize()设置在 SortedList 元素的实际数

实践

创建一个 SortedList

以下示例演示如何创建泛型 SortedList<TKey, TValue>,并在其中添加键值对。

static void Main(string[] args)
{
    var sortedList = new SortedList<int, string>(); 
    sortedList.Add(22, "value22");
    sortedList.Add(11, "value11");
    sortedList.Add(2, "value2");
    sortedList.Add(1, "value1");
    sortedList.Add(13, "value13");

    Console.WriteLine(string.Join(",", sortedList.Select(it => it.Value)));
    // value1,value2,value11,value13,value22
    
    // 使用 foreach 循环 列出所有的 key 和 value
    foreach (var item in sortedList) // item 是 KeyValuePair<int, string> 类型 
    {
        Console.WriteLine($"{item.Key}={item.Value}");
    }
    // 1=value1
    // 2=value2
    // 11=value11
    // 13=value13
    // 22=value22
}

访问 SortedList

在索引器 sortedList[key] 中指定一个键,以获取或设置 SortedList 中的值。

static void Main(string[] args)
{
    var sortedList = new SortedList<int, string>()
    {
        [22] = "value22",
        [23] = "value23",
        [3] = "value3",
    };

    Console.WriteLine(sortedList[22]);//"value22"
    sortedList[3] = "value3 new"; // 更改了值
    sortedList[33] = "value33"; // 添加了一个新元素
    // Console.WriteLine(sortedList[65]);
    // 报错:System.Collections.Generic.KeyNotFoundException:“The given key '65' was not present in the dictionary.”

}

上面, sortedList[65] 将抛出 KeyNotFoundException 因为指定的键 65 在排序列表中不存在。 为防止出现此异常,请使用 ContainsKey() 或 TryGetValue() 方法,如下所示。

static void Main(string[] args)
{
    var sortedList = new SortedList<int, string>()
    {
        [22] = "value22",
        [23] = "value23",
        [3] = "value3",
    };

    if (sortedList.ContainsKey(65)) Console.WriteLine(sortedList[65]); // 不会执行到
    if (sortedList.TryGetValue(3, out string value)) Console.WriteLine(value); // value3
}

sortedList还支持Keys和Values。

static void Main(string[] args)
{
    var sortedList = new SortedList<int, string>()
    {
        [22] = "value22",
        [23] = "value23",
        [3] = "value3",
    };

    foreach (var item in sortedList.Keys) //.Values 也是可以输出所有的 Values
    {
        Console.Write(item + ",");
    }
    // 3,22,23,
}

从 SortedList 中移除元素

使用 Remove(key) 和 RemoveAt(index) 方法从 SortedList 中删除键值对。

static void Main(string[] args)
{
    var sortedList = new SortedList<int, string>()
    {
        [22] = "value22",
        [23] = "value23",
        [3] = "value3",
        [4] = "value4",
        [5] = "value5",
        [6] = "value5",
    };

    sortedList.RemoveAt(2); // 移掉了 [5] = "value5",
    sortedList.Remove(23); // 移掉了 [23] = "value23",
    Console.WriteLine(string.Join(",", sortedList.Keys)); // 3,4,6,22
}

字典 Dictionary

Dictionary<TKey, TValue> 是一个通用字典集合,它以没有特定顺序存储键值对。 (跟SortedList比它不排序)

特征

  • Dictionary<TKey, TValue> 存储键值对
  • 实现 IDictionary<TKey, TValue> 接口。
  • 必须是唯一的,不能为空。
  • 值可以为重复
  • 可以通过在索引器中传递关联的键来访问值,例如 我的字典[键]
  • 元素存储为 KeyValuePair<TKey, TValue> 对象。
  • 内部使用 hash 算法

属性

属性描述
Keys字典键集合
Values字典值集合
Count字典内元素数量

方法

属性描述
Add()新增字典元素,当字典键存在时,抛出异常
TryAdd()尝试将指定的键和值添加到字典中,字典键存在时返回false表示新增失败且不会抛出异常
Remove()移除字典集合内指定键数据
Reverse()将字典集合反序排序
ContainsKey()搜索字典键是否存在于字典集合中,存在返回true不存在返回false
ContainsValue()搜索字典值是否存在于字典集合中,存在返回true不存在返回false
First()获取字典集合内第一个键、值元素;若字典集合长度为0会抛出异常Sequence contains no matching element,支持lambda表达式查询
FirstOrDefault()获取字典集合内第一个键、值元素;若字典集合长度为0不抛出异常并且返回null值,支持lambda表达式查询
Where()按条件搜索字典集合中符合条件的结果集,支持lambda表达式查询
ElementAt()获取字典集合中指定下标键、值元素
Except()与另一个同类型结构的字典结果集比较并获取两个字典结果集之间的差集
Intersect()与另一个同类型结构的字典结果集比较并获取两个字典结果集之间的交集
Union()与另一个同类型结构的字典结果集比较并获取两个字典结果集之间的并集

Dictionary常用的属性、方法已经列举完毕,接下来附上常用属性、方法的使用代码供大家参考。

实践

创建 Dictionary

您可以通过传递符合要求的键值对来创建 Dictionary<TKey, TValue> 对象。 以下示例显示如何创建字典并添加键值对

static void Main(string[] args)
{
    var dict0 = new Dictionary<int, string>()
    {
        [1] = "a",
        [2] = "b"
    };

    // C#6.0 之后 提供的 Index Initializers 索引初始化器
    var dict1 = new Dictionary<string, string>()
    {
        ["xm"] = "厦门",
        ["bj"] = "北京",
        ["sh"] = "上海",
    };

    var dict2 = new Dictionary<string, string>()
    {
        { "xm" ,"厦门"},
        { "bj" ,"北京"},
        { "sh" ,"上海" },
    };

    var dict3 = new Dictionary<string, string>();
    dict3.Add("xm", "厦门");
    dict3.Add("bj", "北京");
    dict3.Add("sh", "上海");

    foreach (var kvp in dict3)
        Console.WriteLine("Key: {0}, Value: {1}", kvp.Key, kvp.Value);

    // Key: xm, Value: 厦门
    // Key: bj, Value: 北京
    // Key: sh, Value: 上海
}

在上面的例子中,dict0 是一个 Dictionary<int, string> 类型的字典,所以它可以存储 int 键和字符串值。 同样的,dict1,dict2,dict3是一个 Dictionary<string, string> 类型的字典,所以它可以存储字符串键和字符串值。 字典不能包含重复键或空键,而值可以是重复键或空键。 键必须是唯一的,否则会抛出运行时异常。

访问 Dictionary

我们可以使用索引器访问字典。 通过一个Key来获取相关联的值。 也还可以使用 ElementAt() 方法从指定的索引中获取 KeyValuePair。

static void Main(string[] args)
{
    var dict1 = new Dictionary<string, string>()
    {
        ["xm"] = "厦门",
        ["bj"] = "北京",
        ["sh"] = "上海",
    };

    Console.WriteLine(dict1["xm"]);  // 输出 厦门
    Console.WriteLine(dict1["bj"]);  // 输出 北京
    // Console.WriteLine(dict1["malema"]);
    // 报错:System.Collections.Generic.KeyNotFoundException

    if (dict1.ContainsKey(".net"))
    {
        // 没有这个 key 不会输出
        Console.WriteLine(dict1[".net"]);
    }

    // use TryGetValue() to get a value of unknown key
    string result;

    if (dict1.TryGetValue("xm", out result))
    {
        Console.WriteLine(result);//输出 厦门
    }

    for (int i = 0; i < dict1.Count; i++)
    {
        Console.WriteLine($"Key: {dict1.ElementAt(i).Key}, Value: { dict1.ElementAt(i).Value}");
    }
    // for循环输出下面三个
    // Key: xm, Value: 厦门
    // Key: bj, Value: 北京
    // Key: sh, Value: 上海
}

更新 Dictionary

通过索引器来更新值,如果这个key 不存在的话则会添加一个新的。 (注意 如果用Add来添加的话要先判断有没有存,不然会出错,通过这种方式来添加的话则不需要判断)

static void Main(string[] args)
{
    var dict1 = new Dictionary<string, string>()
    {
        ["xm"] = "厦门",
        ["bj"] = "北京",
        ["sh"] = "上海",
    };

    dict1["cn"] = "中国";
    dict1["xm"] = "厦门2021";

    Console.WriteLine(string.Join(",", dict1.Keys));
    // xm,bj,sh,cn
}

从 Dictionary 里删除元素

  • 使用 Remove() 方法清除某个元素
  • 使用 Remove() 方法清空整个字典
static void Main(string[] args)
{
    var dict1 = new Dictionary<string, string>()
    {
        ["xm"] = "厦门",
        ["bj"] = "北京",
        ["sh"] = "上海",
    };

    dict1.Remove("xm");
    dict1.Remove("xm2"); //.net5.0 没有这个key也不会出错

    Console.WriteLine(string.Join(",", dict1.Keys));
    // bj,sh

    dict1.Clear(); //清掉所有
    Console.WriteLine(string.Join(",", dict1.Keys));
    //没东西了
}

把条件判断转化成:查表

如下的代码。我们需要把code转成名字, 我们可以用if else 或者 switch来写这个代码。 但是更方便的则是我们创建一个Dictionary然后直接查表。

private static string GetNameByCode(string code)
{

    switch (code)
    {
        case "xm":
            return "厦门";

        case "bj":
            return "北京";

        case "sh":
            return "上海";
    }
    throw new ArgumentOutOfRangeException("找不到这个Key");
}

static void Main(string[] args)
{
    var name = GetNameByCode("xm");

    var dict1 = new Dictionary<string, string>()
    {
        ["xm"] = "厦门",
        ["bj"] = "北京",
        ["sh"] = "上海",
    };

    var name2 = dict1["xm"];

}

其它复杂的操作也是可以类似上面转成查表的。只是把value部分变成 Action<> 或者 Func<>

栈 Stack

栈(stack)又名堆栈,它是一种运算受限的线性表。限定仅在表尾进行插入和删除操作的线性表。这一端被称为栈顶,相对地,把另一端称为栈底。 向一个栈插入新元素又称作进栈、入栈或压栈(push),它是把新元素放到栈顶元素的上面,使之成为新的栈顶元素; 从一个栈删除元素又称作出栈或退栈(pop),它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。 它这种存储方式被称为 LIFO (Last in first out)(后进先出,选进后出) C# 包括泛型 Stack 和非泛型 Stack 集合类。 建议使用泛型 Stack 集合。

特征

  • Stack 是后进先出集合。
  • 它位于 System.Collection.Generic 命名空间下。
  • Stack 可以包含指定类型的元素。 它提供编译时类型检查并且不执行装箱拆箱,因为它是泛型。
  • 可以使用 Push() 方法添加元素。 不能使用集合初始值设定项语法。
  • 可以使用 Pop() 和 Peek() 方法检索元素。 它不支持索引器。

属性

属性类型描述
Countint获取 Stack<T> 中包含的元素数。

方法

方法返回类型描述
Clear()void从 Stack<T> 中移除所有对象。
Contains(T item)bool确定某个元素是否在 Stack<T> 中。
GetEnumerator()Enumerator<T>返回循环访问 Stack<T> 的枚举数。
Peek()T返回位于 Stack<T> 顶部的对象但不将其移除。
Pop()T移除并返回位于 Stack<T> 顶部的对象。
Push(T item)void将一个对象插入到 Stack<T> 的顶部。
ToArray()T[]将 Stack<T> 复制到一个新数组中。

实践

创建一个堆栈 Stack

您可以通过为 Stack 可以存储的元素类型指定类型参数来创建 Stack 的对象。 以下示例使用 Push() 方法在 Stack 中创建和添加元素。 堆栈允许null(对于引用类型)和重复值。

static void Main(string[] args)
{
    var myStack = new Stack<int>();
    myStack.Push(1);
    myStack.Push(2);
    myStack.Push(3);
    myStack.Push(4);

    foreach (var item in myStack)
        Console.Write(item + ","); //输出 4,3,2,1, 
}

也可以通过数组来创建一个堆栈

static void Main(string[] args)
{
    int[] arr = new int[] { 1, 2, 3, 4 };
    Stack<int> myStack = new Stack<int>(arr);

    foreach (var item in myStack)
        Console.Write(item + ","); //输出 4,3,2,1, 
}

contains pop peek 例子

static void Main(string[] args)
{
    var myStack = new Stack<int>();
    myStack.Push(1);
    myStack.Push(2);
    myStack.Push(3);
    myStack.Push(4);

    Console.WriteLine("包含3" + myStack.Contains(3));//包含3True
    var first = myStack.Pop(); //4
    var second = myStack.Pop();//3
    Console.WriteLine($"第一个:{first},第二个:{second}");
    Console.WriteLine("包含3" + myStack.Contains(3));//包含3False
    Console.WriteLine(myStack.Count());//2
    var third = myStack.Peek();
    Console.WriteLine($"第三个:{third}"); //第三个: 2
    Console.WriteLine(myStack.Count());//2  说明peek没有改变数量 
}

队列 Queue

Queue 是一种特殊类型的集合,它以 FIFO (先进先出)的方式来存储元素,与 堆栈 Stack 完全相反。 C# 包括通用 Queue 和非通用 Queue 集合。 建议使用通用 Queue 集合。

特征

  • Queue 是 FIFO(先进先出)集合。
  • 它位于 System.Collection.Generic 命名空间下。
  • Queue 可以包含指定类型的元素。 它提供编译时类型检查并且不执行装箱拆箱,因为它是泛型。
  • 可以使用 Enqueue() 方法添加元素。 不能使用集合初始值设定项语法。
  • 可以使用 Dequeue() 和 Peek() 方法检索元素。 它不支持索引器。

属性

属性描述
Count获取 Queue 中包含的元素个数。

方法

方法返回类型描述
Clear()void从 Queue<T> 中移除所有对象。
Contains(T item)bool确定某个元素是否在 Queue<T> 中。
CopyTo(T[] array, int arrayIndex)void将 Queue<T> 中的元素复制到现有一维数组中,从指定的数组索引开始。
Dequeue()T移除并返回 Queue<T> 开始处的对象。
Enqueue(T item)void将一个对象添加到 Queue<T> 的结尾。
GetEnumerator()Enumerator<T>返回循环访问 Queue<T> 的枚举数。
ToArray()T[]将 Queue<T> 复制到一个新数组中。
TryDequeue(out T result)bool移除并返回 Queue<T> 开始处的对象。如果操作成功,则返回 true,否则返回 false
TryPeek(out T result)bool返回位于 Queue<T> 开始处的对象但不将其移除。如果操作成功,则返回 true,否则返回 false

实践

创建队列

我们可以通过指定类型参数来创建 Queue 的对象。 以下示例使用 Enqueue() 方法在 Queue 中创建和添加元素。 Queue 集合允许 null(对于引用类型)和重复值。

static void Main(string[] args)
{
    var myQueue = new Queue<int>();
    myQueue.Enqueue(1);
    myQueue.Enqueue(2);
    myQueue.Enqueue(3);
    myQueue.Enqueue(4);

    foreach (var id in myQueue)
        Console.Write(id); //prints 1234
}

示例

static void Main(string[] args)
{
    var myQueue = new Queue<int>(2);
    myQueue.Enqueue(1);
    myQueue.Enqueue(2);
    myQueue.Enqueue(3);
    myQueue.Enqueue(4);

    var first = myQueue.Dequeue(); //1
    var has = myQueue.Contains(2); // true;
    
    foreach (var id in myQueue)
        Console.Write(id); //prints 234
    var second = myQueue.Peek(); //2 没有移除元素
}

元组 Tuple

Tuple 类是在 .NET Framework 4.0 中引入的。 元组是一种数据结构,其中包含一系列不同数据类型的元素。 如果我想用一个临时的结构来传递数据,又不想创建一个新类的话,就可以用它了。

(ValueTuple)的使用会比这个更加方便,在下一节会介绍到它。

Tuple<T1, T2, T3, T4, T5, T6, T7, TRest>

以下示例创建一个包含三个元素的元组:

var person = new Tuple<int, string, string>(1, "malema", ".net");

在上面的例子中,我们创建了一个保存个人记录的元组实例。 我们为每个元素指定了一个类型并将值传递给构造函数。 指定每个元素的类型参数是比较麻烦的。 Tuple里面有一个静态泛型方法Create(), 使用它的话,编译器会自动进行类型推断,如下。

var person = Tuple.Create(1, "malema", ".net");

一个元组 最多只能包含八个 元素。 当您尝试包含八个以上的元素时,它会给出编译器错误。

var person = Tuple.Create(1, 2, 3, 4, 5, 6, 7, 8, 9);
//有错 “Create”方法没有采用 9 个参数的重载

属性

访问元组元素

可以使用Item<数字> 属性访问元组元素,例如,Item1、Item2、Item3 等等,直到 Item7 属性。 Item1 属性返回第一个元素,Item2 返回第二个元素,依此类推。 最后一个元素(第 8 个元素)将使用 Rest 属性返回。

static void Main(string[] args)
{
    var numbers = Tuple.Create("一", 2, 3, "四", 5, "六", 7, 8);

    Console.WriteLine(numbers.Item1); // 输出 "一"
    Console.WriteLine(numbers.Item2); // 输出 2
    Console.WriteLine(numbers.Item3); // 输出 3
    Console.WriteLine(numbers.Item4); // 输出 "四"
    Console.WriteLine(numbers.Item5); // 输出 5
    Console.WriteLine(numbers.Item6); // 输出 "六"
    Console.WriteLine(numbers.Item7); // 输出 7
    Console.WriteLine(numbers.Rest); // 输出 (8)
    Console.WriteLine(numbers.Rest.Item1); // 输出 8 //注意还要加一个Item1
}

通常,第 8 个位置用于嵌套元组,您可以使用 Rest 属性访问它。

嵌套元组

如果要在一个元组中包含八个以上的元素,可以通过嵌套另一个元组对象作为第八个元素来实现。 可以使用 Rest 属性访问最后一个嵌套的元组。 要访问嵌套元组的元素,请使用 Rest.Item1.Item<数字> 属性。

static void Main(string[] args)
{
    var numbers = Tuple.Create(1, 2, 3, 4, 5, 6, 7, Tuple.Create(8, 9, 10, 11, 12, 13));
    Console.WriteLine(numbers.Item1); // 输出 1
    Console.WriteLine(numbers.Item7); // 输出 7
    Console.WriteLine(numbers.Rest.Item1); //输出 (8, 9, 10, 11, 12, 13)
    Console.WriteLine(numbers.Rest.Item1.Item1); //输出 8
    Console.WriteLine(numbers.Rest.Item1.Item2); //输出 9
}

虽然推荐是在最后一个放入嵌套元素,但是实际上我们可以在任意位置放入嵌套元组。如下

var numbers = Tuple.Create(1, 2, Tuple.Create(3, 4, 5, 6, 7,  8), 9, 10, 11, 12, 13 );

(当需要这么多元素的时候,我们最好还是定义一个类,这样的代码可读性才更好)

实践

元组做为参数

static void Main(string[] args)
{
    var person = Tuple.Create(1, "Malema", ".net");
    DisplayTuple(person);
}

static void DisplayTuple(Tuple<int, string, string> person)
{
    Console.WriteLine($"Id = { person.Item1}");
    Console.WriteLine($"First Name = { person.Item2}");
    Console.WriteLine($"Last Name = { person.Item3}");
}

元组做为返回值

static void Main(string[] args)
{
    var person = GetPerson();
}

static Tuple<int, string, string> GetPerson() 
{
    return Tuple.Create(1, "Bill", "Gates");
}

使用场景

  • 当您想从一个方法返回多个值而不使用 ref 或 out 参数时。
  • 当您想通过单个参数将多个值传递给方法时。
  • 当您想临时保存数据库记录或某些值而不创建单独的类时。

限制

  • 元组是引用类型而不是值类型。 它在堆上分配并可能导致 CPU 密集型操作。
  • 元组仅限于包含八个元素。 如果需要存储更多元素,则需要使用嵌套元组。 但是,这可能会导致歧义。
  • 可以使用名称模式为 Item 的属性访问元组元素,可读性不好。

元组 ValueTuple

优点

优点 1

可以使用括号 () 并指定其中的值来创建和初始化它。

var person = (1, "Malema", ".net");
//等价于下面这种.
var person = ValueTuple.Create(1, "Malema", ".net"); //注意不是 Tuple.Create

优点 2

跟 Tuple 不一样 ValueTuple 可以包含8个以上的元素

var numbers = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14);

优点 3

可以为 ValueTuple 属性指定名称,而不是使用默认的属性名称,如 Item1、Item2 等。

static void Main(string[] args)
{
    var (id, firstName, lastName) = (1, "Malema", ".net"); // 最简写的方式
    // (int id, string firstName, string lastName) = (1, "Malema", ".net"); // 可为每个成员规定类型
    // (var id, var firstName, var lastName) = (1, "Malema", ".net"); // 不确定类型就写 var
    Console.WriteLine(id); // 1
    Console.WriteLine(firstName); // Malema
    Console.WriteLine(lastName); // .net
}

我们还可以为右侧的成员名称分配值,如下所示。

static void Main(string[] args)
{
    var person = (Id: 1, FirstName: "Malema", LastName: ".net");
    Console.WriteLine(person.Id); // 1
    Console.WriteLine(person.FirstName); // Malema
    Console.WriteLine(person.LastName); // .net
}

实践

元组作为参数传递

static void Main(string[] args)
{
    DisplayTuple((1, "Bill", "Gates"));
}

static void DisplayTuple((int id, string firstName, string lastName) person)
{
    Console.WriteLine($"Id = {person.id}"); // Id = 1
    Console.WriteLine($"First Name = { person.firstName}"); // First Name = Bill
    Console.WriteLine($"Last Name = { person.lastName}"); // Last Name = Gates
}

元组作为返回值

static void Main(string[] args)
{
    var person = GetPerson();
}

//在这边定义上成员名字。这样调用的地方也可以很方便的使用,打点的话会有智能提示
static (int Id, string FirstName, string LastName) GetPerson()
{
    return (1, "Bill", "Gates");
}

解构取出元组中的成员

我们可以通过解构,直接取出元组中的成员。 上面的代码可以变成如下的形式

static void Main(string[] args)
{
    (int id, string firstName, string lastName) = GetPerson();
}

static (int Id, string FirstName, string LastName) GetPerson()
{
    return (1, "Bill", "Gates");
}

当我们不需要某个成员的时候我们可以使用 _ 称为弃元discards

class Program
{
    static void Main(string[] args)
    {
        (int id, string firstName, _) = GetPerson();
    }

    static (int Id, string FirstName, string LastName) GetPerson()
    {
        return (1, "Bill", "Gates");
    }
}

另外,类也是可以进行解构的。如下

class Program
{
    static void Main(string[] args)
    {

        (int id, string firstName, string lastName) = GetPerson();
    }

    static Person GetPerson()
    {
        return new Person();
    }


    class Person
    {
        public void Deconstruct(out int id, out string firstName, out string lastName)
        {
            id = 3;
            firstName = "malema";
            lastName = ".net";
        }
    }
}

委托

如果我们想 传递一个函数作为参数 怎么办? C# 如何处理回调函数或事件处理程序? 答案是——委托

委托的核心作用是:把函数作为参数传递给委托,可通过委托实例加和累计多个函数,按流程帮我处理

委托是定义方法签名的引用类型数据类型。 您可以定义委托的变量,就像其他数据类型一样,可以引用与委托具有相同签名的任何方法。

我们使用委托时会涉及到三个步骤:

  • 声明一个委托 Delegate
  • 设置目标方法
  • 调用委托 Delegate

声明委托

可以使用 delegate 关键字后跟函数签名来声明委托,如下所示。

[访问修饰符] delegate [返回类型] [委托名称]([参数...])

下面声明了一个名为 MyDelegate 的委托。

public delegate void MyDelegate(string msg);

上面,我们已经声明了一个具有 void 返回类型和字符串参数的委托 MyDelegate。 委托可以在类外类内声明。 推荐:声明在类之外

声明委托后,我们需要设置目标方法或 lambda 表达式。 我们可以通过使用 new 关键字创建委托对象并传递签名与委托签名匹配的方法来实现。

委托的使用

using System;

namespace ConsoleApp1
{
    // 1. 声明一个委托
    public delegate void MyDelegate(string msg);
    class Program
    {
        // 2. 设置跟 MyDelegate 有相同参数和返回值的方法
        static void MethodA(string message)
        {
            Console.WriteLine(message);
        }

        static void Main(string[] args)
        {
            // a. 把函数传给委托
            MyDelegate del1 = new MyDelegate(MethodA);
            // b. 直接赋值给委托
            MyDelegate del2 = MethodA;
            // c. 使用 lambda 表达式
            MyDelegate del3 = (string msg) => Console.WriteLine(msg);

            // 3. 调用委托
            del1.Invoke("1"); // a. 使用 Invoke() 方法调用委托
            del2("2"); // b. 使用 () 运算符调用委托
            del3("3");

            Console.ReadKey();
        }
    }
}

输出

1
2
3

多播委托

委托可以 引用多个方法。当调用多播委托时,它将依次调用所有关联的方法。要将多个方法分配给同一个委托实例,可以使用 += 运算符

using System;

delegate int NumberChanger(int n);
namespace DelegateAppl
{
   class TestDelegate
   {
      static int num = 10;
      public static int AddNum(int p)
      {
         num += p;
         return num;
      }

      public static int MultNum(int q)
      {
         num *= q;
         return num;
      }
      public static int getNum()
      {
         return num;
      }

      static void Main(string[] args)
      {
         // 创建委托实例
         NumberChanger nc;
         NumberChanger nc1 = new NumberChanger(AddNum);
         NumberChanger nc2 = new NumberChanger(MultNum);
         nc = nc1;
         nc += nc2;
         // 调用多播
         nc(5);
         Console.WriteLine("Value of Num: {0}", getNum());
         Console.ReadKey();
      }
   }
}

输出:

Value of Num: 75

将委托(函数)作为参数传递

在c#里面,有的时候一些代码的实现必须用到委托,比如:

  • 线程里面修改某个textBox的值,如果直接在线程里面写修改的代码,执行时候,编译器会报错,因为c#不允许这样写。
  • 还有在单独写的类里面,修改某个form里面某个控件的属性值等等,也是不被允许的。

这时候,就需要使用到委托(delegate)。委托其实是这样的,为某些实现写一个函数,并将其赋值给委托(相当于函数指针),在使用的时候直接通过 委托名 来调用。
如果我们要把方法当做参数来进行传递的话,就要用到委托。 简单来说,委托是一个类型,这个类型可以赋值一个方法的引用。这是委托的主要用途

委托的理解是:委托别人帮我做事,那别人就得提前想(写)好方法才能帮我干活

using System;

class Program
{
    // 1. 定义委托
    public delegate void GreetDelegate(string name);
    // 2. 设置跟 GreetDelegate 有相同参数和返回值的方法
    private static void EnglishGreeting(string name)
    {
        Console.WriteLine("Morning, " + name);
    }
    private static void ChineseGreeting(string name)
    {
        Console.WriteLine("早上好," + name);
    }
    // 3. 定义一个接收【name + GreetingDelegate】类型的方法
    private static void GreetPeople(string name, GreetDelegate MakeGreeting)
    {
        MakeGreeting(name);
    }
    static void Main(string[] args)
    {
        // 4. 将方法作为参数传递,并同时传递方法执行需要的参数
        GreetPeople("bro", EnglishGreeting);
        GreetPeople("兄弟", ChineseGreeting);
        Console.ReadKey();
    }
}

输出

Morning, bro
早上好,兄弟

事件

事件(Event)基本上说是某个【用户操作】,如按键、点击、鼠标移动等等,或者是一些【提示通知】,如系统生成的通知。应用程序需要在事件发生时响应事件

C# 中使用事件机制实现 线程间的通信

通过事件使用委托

事件在类中声明且生成,且通过使用同一个类或其他类中的委托与事件处理程序关联。包含事件的类用于发布事件。这被称为 发布器  类。其他接受该事件的类被称为 订阅器)  类。事件使用 发布-订阅  模型。

发布器  是一个包含事件和委托定义的对象。事件和委托之间的联系也定义在这个对象中。发布器(publisher)类的对象调用这个事件,并通知其他的对象。

订阅器  是一个接受事件并提供事件处理程序的对象。在发布器(publisher)类中的委托调用订阅器(subscriber)类中的方法(事件处理程序)。

声明事件

在类的内部声明事件,首先必须声明该事件的委托类型。例如:

public delegate void BoilerLogHandler(string status);

然后,声明事件本身,使用 event 关键字:

// 基于上面的委托定义事件
public event BoilerLogHandler BoilerEventLog;

上面的代码定义了一个名为 BoilerLogHandler 的委托和一个名为 BoilerEventLog 的事件,该事件在生成的时候会调用委托。

实例

using System;
namespace SimpleEvent
{
  using System;
  /***********发布器类***********/
  public class EventTest
  {
    private int value;
    public delegate void NumManipulationHandler();
    public event NumManipulationHandler ChangeNum;
    protected virtual void OnNumChanged()
    {
      if ( ChangeNum != null )
      {
        ChangeNum(); /* 事件被触发 */
      } else {
        Console.WriteLine( "event not fire" );
        Console.ReadKey(); /* 回车继续 */
      }
    }

    public EventTest()
    {
      int n = 5;
      SetValue( n );
    }

    public void SetValue( int n )
    {
      if ( value != n )
      {
        value = n;
        OnNumChanged();
      }
    }
  }

  /***********订阅器类***********/

  public class subscribEvent
  {
    public void printf()
    {
      Console.WriteLine( "event fire" );
      Console.ReadKey(); /* 回车继续 */
    }
  }

  /***********触发***********/
  public class MainClass
  {
    public static void Main()
    {
      EventTest e = new EventTest(); /* 实例化对象,第一次没有触发事件 */
      subscribEvent v = new subscribEvent(); /* 实例化对象 */
      e.ChangeNum += new EventTest.NumManipulationHandler( v.printf ); /* 注册 */
      e.SetValue( 7 );
      e.SetValue( 11 );
    }
  }
}

委托和事件的区别

委托和事件有一定的相似性,这里先说一下委托和事件的定义

委托:delegate 是一种可用于封装命名或匿名方法的引用类型。 委托类似于 C++ 中的 函数指针;但是,委托是类型安全和可靠的。

事件:事件是特殊类型的多路广播委托,仅可从声明它们的类或结构里面调用。 如果其他类或结构订阅了该事件,则当发行者类引发该事件时,会调用其事件处理程序方法。

委托和事件的区别如下:

区别委托事件
1是否是一个类型否,事件修饰的是一个对象
2是否可以在类外部进行调用
3是否可以使用=来赋值

区别一:是否是一个类型

class Class3
{
    static void Main(string[] args)
    {
        //委托正确使用
        Class1.NumberChanger n1 = Class2.ClassA_Test;
        //事件使用 编译器报错
        Class1.changer handle2 = Class2.ClassA_Test;
    }
}

错误:Class1.changer 是“字段”,但此处被当做“类型”来使用

区别二:委托可以在声明它的类外部进行调用,而事件只能在类的内部进行调用。

(1)在类外部调用委托

在这里插入图片描述

(2)在类外部调用事件

在这里插入图片描述

事件“ClassC.Say_EventHandler”只能出现在 += 或 -= 的左边(从类型“ClassC”中使用时除外)

区别三:委托可以在外部类使用 = 来赋值,事件只能在内部类用 = 赋值,外部类不可以

在这里插入图片描述

从编译器提示的错误,我们可以了解到,事件只能在声明它的类内部被调用。从事件本身来讲,事件一般用于类自身的属性变化时,用来通知外界自身的变化的。我们将对ClassC内部的一个属性赋值,然后调用事件,模拟对外通知。代码如下所示

在这里插入图片描述

总结:事件与委托最主要的区别应该是不能在外部调用,但可以通过+=或-=进行注册,但如果委托变量为私有,则外部不能注册;如果为公有,则外部有可以调用,破坏了封装,所以没办法,在这种情况就定义一个event就好了

反射

特性

特性(Attribute)用于添加元数据,如编译器指令和注释、描述、方法、类等其他信息。.Net 框架提供了两种类型的特性:预定义特性和自定义特性。在 C# 中,通过用方括号 ([]) 将特性名称括起来,并置于应用该特性的实体的声明上方以指定特性。如下代码所示:

public class User 
{
    [Required(ErrorMessage = "用户Id是必填的")]
    public int UserId { get; set; }
}

条件编译,如 Debug 模式

这个预定义特性标记了一个条件方法,其执行依赖于指定的预处理标识符。

它会引起方法调用的条件编译,取决于指定的值,比如 DebugTrace。例如,当调试代码时显示变量的值。

规定该特性的语法如下:

[Conditional(conditionalSymbol)]

示例

namespace Malema.net
{
    public class Myclass
    {
        [Conditional("DEBUG")]
        public static void Message(string msg)
        {
            Console.WriteLine(msg);
        }
    }
    class Program
    {
        static async Task Main(string[] args)
        {
            Myclass.Message("hello");
            Console.ReadLine();
        }
    }
}

上面的代码在Debug模式下,会输出 hello。在Release模式下不会输出hello

标记程序,如过期提示

这个预定义特性标记了不应被使用的程序实体。它可以让您通知编译器丢弃某个特定的目标元素。例如,当一个新方法被用在一个类中,但是您仍然想要保持类中的旧方法,您可以通过显示一个应该使用新方法,而不是旧方法的消息,来把它标记为 obsolete(过时的)。

规定该特性的语法如下:

[Obsolete(message)]
[Obsolete(message,iserror)]

其中:

  • 参数 message,是一个字符串,描述项目为什么过时以及该替代使用什么。
  • 参数 iserror,是一个布尔值。如果该值为 true,编译器应把该项目的使用当作一个错误。默认值是 false(编译器生成一个警告)。 下面的实例演示了该特性:
 [Obsolete("过期了,请使用YourClass.Abc")]
    public class Myclass
    {
        public static void Message(string msg)
        {
            Console.WriteLine(msg);
        }
    }
    class Program
    {
        static async Task Main(string[] args)
        {
            Myclass.Message("helo");
            Console.ReadLine();
        }
    }

我们会看到如下图这样的编译警告提示

反射

C# 中,查看和操作元数据就称之为反射!反射提高了程序的灵活性和扩展性。说到反射,不得不说到我们 C# 中的 System.Type 类。

获取任何类型的Type引用的常用方式有如下两种

Type type = typeof(int);

int number = 1;
Type type = number.GetType();

Type是许多反射功能的入口,它实现了很多方法和属性!属性主要是用来获取相关的各种名称的字符串,而方法主要是用来获取对应数据类型的构造函数、方法、事件等东西

static void Main(string[] args)
{
    Type type = typeof(int);

    Console.WriteLine(type.Name);   //  获取当前类型的名称
    Console.WriteLine(type.FullName);// 获取当前类型的名称包括了命名空间
    Console.WriteLine(type.IsAbstract); //判断是否是抽象的
    Console.WriteLine(type.IsValueType); // 判断是否是值类型
    Console.WriteLine(type.IsArray); // 该值指示类型是否为数组
    .....
}

多线程

线程基础

了解过计算机的人可能知道 程序最小执行单元是线程,最小资源分配单位是进程进程里必然至少有一个线程,而一个程序也必然至少有一个进程。这里不过多的介绍进程和线程的区别于关系,只需要记着线程是程序最小执行单元,我们在开发中最常用的也是线程。

在很多不太严谨的编程教程中,都会把多线程和并行化作等号。但是这里有一个很微妙的区别,对于单核CPU来说,多进程和多线程一样,都不会产生并行的效果;对于多核CPU而言,多进程必然是并行的,但是多线程则不一定并行。所以C#中,线程更多的用作异步处理上,而不是并行计算上。

在C#程序中,需要引用System.Threading。C#的入门级线程操作只需要知道Thread类、一个带参数的无返回值方法和一个不带参数的无返回值方法,这三个要点就可以了。

线程的状态

一般情况线程分为五个阶段,也就是五种状态:分别是【准备、就绪、运行、阻塞、死亡】。当然在不同的地方,状态可能会细分为更多的级别,这里只做初步的介绍。状态之间的切换如下:

image-20200423102030652

线程的状态之间切换顺序有着严格的限制,而且只能从就绪态由CPU切换到运行态,运行态无法从其他状态切换过去,而且这一步的切换开发者不能控制

线程在程序中的操作

1、创建线程

通过声明并实例化Thread就可以创建线程,它接收方法作为参数。使用Thread.Start()就可以开启子线程,让其去执行方法中的内容。

static void Main(string[] args)
{            
    // 新创建的线程中输出
    Thread oneThread = new Thread(PrintNumber);
    oneThread.Start();

    // 主线程中输出
    PrintNumber();
    Console.ReadKey();
}

static void PrintNumber() 
{
    Console.WriteLine("开始......");
    for (int i = 0; i < 10; i++)
    {
        Console.WriteLine(i);
    }
}

主线程和子线程同时输出

可以看到当我们在子线程和主线程中同时输出PrintNumber()中的内容时,它是乱的随机交叉输出的。

2、暂停线程

暂停线程故名思意就是让线程暂停,不让其占用CPU资源,在一直等待,啥时候取消暂停就恢复运行。在C#中暂停就是让这个线程进入睡眠状态,让其休眠,不让其占用系统资源就可以了。

Thread.Sleep(TimeSpan.FromSeconds(2));    //睡眠2s

3、线程等待

线程等待就是多个线程在处理某个任务时,某个线程必须等待前一个线程处理所有数据后才可以进行执行,在这个期间,这个线程是阻塞状态的。只有前一个线程完事了,他才可以再继续执行。

static void Main(string[] args)
{            
    // 新创建的线程中输出
    Thread oneThread = new Thread(PrintNumber);
    oneThread.Start();
    oneThread.Join();

    // 主线程中输出
    PrintNumber();
    Console.ReadKey();
}

也就是说上面的程序主线程必须得等oneThread线程执行完PrintNumber方法后,它才可以执行。

4、线程终止

就是线程在执行过程中,利用某些操作(Thread.Abort())可以使其线程立即退出,不进行工作了。

static void Main(string[] args)
{            
    // 新创建的线程中输出
    Thread oneThread = new Thread(PrintNumber);
    oneThread.Start();

    Thread.Sleep(TimeSpan.FromSeconds(6));
    oneThread.Abort();

    // 主线程中输出
    PrintNumber();
    Console.ReadKey();
}

上面的程序可以看到,当主程序再等待6s后,立即将oneThread线程终止掉。

其实Abort()方法是给线程注入了ThreadAbortException方法,导致线程被终结,这其实很危险,因为该线程可能正在处理某些重要的数据,比如接收传输数据等,这样子就传递摧毁了程序,数据也就丢失了。还有就是这个方法不能保证100%终止线程。有时候有些异常会被吃掉,我们可以利用某些关键变量在子线程中进行控制,从而取消线程的执行就可以。

任务

C#中的任务与线程的区别不是很大,因为C#的任务就是 基于线程 实现的,而任务比线程更友好,使用也更方便,当然使用也更加复杂。不过对于开发者而言,任务 取消了线程的状态切换,只保留了有限的一部分。而且,在C# 更推荐使用任务,任务也是对线程的 进一步抽象和改进

创建一个任务

如线程相同的一点是,任务的创建也是通过传递一个方法(严格上讲是一个委托)。不同的是,线程的委托没有返回值而且也不接受从线程返回的值,而任务则不同,调用方可以期待任务是有返回值的而且也可以正常使用。

我们先来看看任务是什么,任务的命名空间System.Threading.Tasks,任务的类有以下两种声明:

public class Task : IAsyncResult, IDisposable;
public class Task<TResult> : System.Threading.Tasks.Task;

第一个,没有泛型的Task类表示一个没有返回值的任务;

第二个,泛型Task类表示该任务有一个返回值,返回值的类型为传递进来的泛型参数。

两个任务类的初始化类似于Thread类,不过与之不同的是 泛型Task的参数是Func,都有一个带Object参数的委托。

与线程不同,任务的创建就有很多种方法:

1、通过构造函数创建

var task1 = new Task(() => { });
var task2 = new Task<int>(()=> 
{
    int i = 0;
    return i;
});

2、使用任务工厂:

var task1 = Task.Factory.StartNew(() => { });
var task2 = Task.Factory.StartNew(() =>
{
    int i = 0;
    return i;
});

3、通过Task.Run创建:

var task1 = Task.Run(() => { });
var task2 = Task.Run(() =>
{
    int i = 0;
    return i;
});

以上三种方式创建的任务是等效的。当然实际上任务的创建并非只有这么几种,但这几种是任务创建的基础,使用频率相当高。

执行任务

与线程不同的是,任务创建完成之后就会自动执行,不需要调用方法。

关于任务的运行有以下需要注意的地方:

  1. 任务的运行不会阻塞主线程;
  2. 主线程结束后,任务一定也会结束;

任务可以IsCompleted属性确定任务是否执行完成,所以可以通过访问任务对象的IsCompleted确认该任务是否执行完成,但有一个问题,这个属性只会表示当前任务是否完成。所以如果需要等待任务完成,则可以通过访问Wait()方法,强制主线程等待任务结束。

如果使用的任务是泛型Task也就是待返回值的任务,可以通过访问Result属性获取任务执行结果。有意思的地方就是,这个属性能获取到结果的时候,也是任务执行完成的时候,所以不需要调用Wait()IsCompleted来判断任务是否完成。

注:通过构造方法创建的任务需要调用 Start方法才能启动,而通过Task.Run和Task.Factory.StartNew创建的则不需要。

线程同步

线程池

参考文章