C#笔记——委托、事件、泛型、迭代器和LINQ

本文最后更新于:2024年8月6日 中午

委托(Delegate)

委托是什么?

委托是一种引用类型,表示对具有特定参数列表和返回类型的方法的引用。——C#编程指南

用我的话来说,就是可以按顺序执行一系列方法的对象。
比如:现在有两个类,一个“肉铺”类,一个“蔬菜店”类,这两个类里边各自拥有名为“取得肉”和“取得蔬菜”的方法。现在你妈让你去“取得肉”和“取得蔬菜”,也就是调用这两个方法,我们可以怎么办呢?
最直接的办法就是直接调用,那要是你妈天天都有这个需求怎么办呢?我们或许会新创建一个方法,并把这两个方法放入这个方法的方法体中,然后调用这个新方法。当然,我们也能够创建一个委托,并把这两个方法交给委托,然后执行委托。

委托的食用方法

最基本的食用方法

首先我们需要使用delegate关键字来创建委托类型,然后实例化委托,最后调用委托。上面例子的完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
namespace Learning1
{
internal class Program
{
// 创建委托类型
public delegate void Del(string name);
static void Main(string[] args)
{
string myName = "Maple";
// 实例化一个委托
Del myDel = ButcherShop.GetMeet;
// 为委托多添加一个方法
myDel += Greengrocer.GetVegetables;
// 调用委托
myDel(myName);
}
}

// 肉铺类,内含一个“取得肉”的静态方法
public class ButcherShop
{
public static void GetMeet(String name)
{
Console.WriteLine($"Congratulations!{name} get meet!");
}
}

// 蔬菜商类,内涵一个“取得蔬菜”的静态方法
public class Greengrocer
{
public static void GetVegetables(String name)
{
Console.WriteLine($"Congratulations!{name} get vegetables!");
}
}
}

执行成功后,显示如下:

1
2
3
4
5
6
Congratulations!Maple get meet!
Congratulations!Maple get vegetables!

E:\Project\C#\CSharpLearning\Learning1\Learning1\bin\Debug\net6.0\Learning1.exe (进程 30208)已退出,代码为 0
要在调试停止时自动关闭控制台,请启用“工具”->“选项”->“调试”->“调试停止时自动关闭控制台”。
按任意键关闭此窗口. . .

委托作为实参或分配给一个属性

因为实例化的委托也是一个对象,所以可以将其作为实参传入方法中,又或者将其分配给一个属性。

这允许方法接受委托作为参数并在稍后调用委托。 这被称为异步回调,是在长进程完成时通知调用方的常用方法。——C#编程指南

依旧是上面的例子,不过这次要和朋友去买菜了,所以创建一个新方法BuyWithFriend,该方法接受三个参数,前两个是买菜人的名字,最后一个参数是一个委托。代码如下:

1
2
3
4
static void BuyWithFriend(string name1, string name2, Del del)
{
del(name1 + " and " + name2);
}

然后在主程序使用BuyWithFriend(myName, "Tom", myDel)来调用新方法,得到如下输出:

1
2
3
4
5
6
Congratulations!Maple and Tom get meet!
Congratulations!Maple and Tom get vegetables!

E:\Project\C#\CSharpLearning\Learning1\Learning1\bin\Debug\net6.0\Learning1.exe (进程 21864)已退出,代码为 0
要在调试停止时自动关闭控制台,请启用“工具”->“选项”->“调试”->“调试停止时自动关闭控制台”。
按任意键关闭此窗口. . .

委托也是派生类

委托类型派生自System.Delegate,而且是密封的(sealed)。我们可以在委托上调用Delegate类定义的方法和属性,比如查询委托调用列表中方法的数量:Console.WriteLine(myDel.GetInvocationList().GetLength(0));

调用列表中具有多个方法的委托派生自MulticastDelegate,该类属于System.Delegate的子类。

协变和逆变

其实这一块应该放在泛型里面说的(?)。
所谓协变,就是定义的委托类型的返回类型,接受其派生类。如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
internal class CovarianceAndContravariance
{
delegate T Factory<out T>();

static Dog MakeDog()
{
return new Dog();
}

static void Main(string[] args)
{
// Covariance
Factory<Dog> dogMaker = MakeDog;
Factory<Animal> animalMaker = dogMaker;

Console.WriteLine(animalMaker().Legs.ToString());
}
}

public class Animal
{
public int Legs = 4;
}

public class Dog : Animal
{

}

在这里,如果把out关键字去掉的话,编译是不通过的,这是因为Factory<Dog>并不是从Factory<Animal>派生的。
所以这里的out关键字标记委托声明中的类型参数,可以让编译器知道在这里类型参数只用作于输出值。这就是协变。
那逆变其实和协变差不多,就是在期望传入基类时允许传入派生对象的特性,就叫逆变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
internal class CovarianceAndContravariance
{
delegate void Action1<in T>(T a);

static void ActOnAnimal(Animal a)
{
Console.WriteLine(a.Legs);
}

static void Main(string[] args)
{
// Contravariance
Action1<Animal> act1 = ActOnAnimal;
Action1<Dog> dog1 = act1;

dog1(new Dog());
}
}

public class Animal
{
public int Legs = 4;
}

public class Dog : Animal
{

}

有关协变和逆变的理解这块我个人可能不是很透彻,建议参考参考其他文章。

强委托类型

其实就是.Net Core框架里边的泛型委托类型,包括ActionFuncPredicate
其中Action类型用于任何具有void返回类型的委托类型:

1
2
3
4
public delegate void Action();
public delegate void Action<in T>(T arg);
public delegate void Action<in T1, in T2>(T1 arg1, T2 arg2);
// Other variations removed for brevity.

Func类型用于具有返回值的委托类型:

1
2
3
4
public delegate TResult Func<out TResult>();
public delegate TResult Func<in T1, out TResult>(T1 arg);
public delegate TResult Func<in T1, in T2, out TResult>(T1 arg1, T2 arg2);
// Other variations removed for brevity

Predicate则用于返回单个值的测试结果:

1
public delegate bool Predicate<in T>(T obj);

当然你也许已经注意到了,Action就是逆变的应用,Func就是协变的应用。

委托的常见模式

LINQ查询表达式

LINQ查询表达式模式依赖于其所有功能的委托,比如Where方法的原型:

1
public static IEnumerable<TSource> Where<TSource> (this IEnumerable<TSource> source, Func<TSource, bool> predicate);

通过委托生成自己的组件

下面定义了一个可用于大型系统中日志消息的组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public enum Severity
{
Verbose,
Trace,
Information,
Warning,
Error,
Critical
}

public static class Logger
{
public static Action<string>? WriteMessage;

public static Severity LogLevel { get; set; } = Severity.Warning;

public static void LogMessage(Severity severity, string component, string message)
{
if (severity < LogLevel)
return;

var outputMsg = $"{DateTime.Now}\t{severity}\t{component}\t{message}";
WriteMessage?.Invoke(outputMsg);
}
}

public static class LoggingMethod
{
public static void LogToConsole(string message)
{
Console.Error.WriteLine(message);
}
}

public class FileLogger
{
private readonly string _logPath;
public FileLogger(string logPath)
{
_logPath = logPath;
Logger.WriteMessage += LogMessage;
}

public void DetachLog() => Logger.WriteMessage -= LogMessage;
private void LogMessage(string message)
{
try
{
using (var log = File.AppendText(_logPath))
{
log.WriteLine(message);
log.Flush();
}
}
catch (Exception)
{

}
}
}

Logger的使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void Main()
{
FileLogger logger = new FileLogger(@".\log.txt");
try
{
throw new Exception($"Logger Test: {new Random().Next(100)}");
}
catch (Exception ex)
{
Logger.LogMessage(Severity.Error, "LoggerTest.Main", ex.Message);
LoggingMethod.LogToConsole(ex.Message);
}
}

大致工作流程:首先,new一个FileLogger对象,指定要生成的日志的路径及名称,在FileLogger的构造方法中,会自动将自己的私有LogMessage方法挂载到Logger的静态委托WriteMessage上;
然后,在需要写入日志的地方(如捕获到异常)直接使用Logger.LogMessage,该方法中会执行委托的Invoke方法,开始处理委托。

事件(Event)

什么是事件?

类或对象可以通过事件向其他类或对象通知发生的相关事情。 发送(或引发)事件的类称为“发布者”,接收(或处理)事件的类称为“订阅者”。——C#编程指南

事件和委托类似,也是后期绑定机制(实际上,事件是建立在对委托的语言支持之上的)。当然要我说的话,无非就是用来实现发布订阅模式的东西,比如现在我们有一个事件,发布者会发布这个事件,广播这个事件发生了,而广播的对象就是那些订阅了这个事件的订阅者。其实就是图形系统的交互啦,点击一个按钮,然后会发生一些事情,其实就是事件的应用。

事件食用方法

定义事件:public event EventHandler<FileListArgs> Progress;

在这里,EventHandler是委托类型,而Progress是名称。

事件不是类型!和方法、属性一样,事件是类或结构的成员!

引发事件:Progress?.Invoke(this, new FileListArgs(file));

订阅事件:

1
2
3
4
EventHandler<FileListArgs> onProgress = (sender, eventArgs) =>
Console.WriteLine(eventArgs.FoundFile);

fileLister.Progress += onProgress;

取消订阅:fileLister.Progress -= onProgress;

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class SimpleEventArgs : EventArgs
{
public string Msg { get; }
public SimpleEventArgs(string msg) => Msg = msg;
}

class Publisher
{
public event EventHandler<SimpleEventArgs>? SimpleEvent;
public void RaiseTheEvent(string msg) =>
SimpleEvent?.Invoke(this, new SimpleEventArgs(msg));
}

class Subscriber
{
public EventHandler<SimpleEventArgs> onA = (sender, eventArgs) => { Console.WriteLine($"A: {eventArgs.Msg}"); };
public EventHandler<SimpleEventArgs> onB = (sender, eventArgs) => { Console.WriteLine($"B: {eventArgs.Msg}"); };
}

internal class Event
{
public static void Main(string[] args)
{
Publisher publisher = new Publisher();
Subscriber subscriber = new Subscriber();

publisher.SimpleEvent += subscriber.onA;
publisher.SimpleEvent += subscriber.onB;
publisher.RaiseTheEvent("test");

Console.WriteLine("\r\nRemove onB");
publisher.SimpleEvent -= subscriber.onB;
publisher.RaiseTheEvent("test 2");
}
}

运行后,控制台输出如下:

1
2
3
4
5
6
7
8
9
A: test
B: test

Remove onB
A: test 2

E:\Project\C#\CSharpLearning\Learning1\Learning1\bin\Debug\net6.0\Learning1.exe (进程 26916)已退出,代码为 0。
要在调试停止时自动关闭控制台,请启用“工具”->“选项”->“调试”->“调试停止时自动关闭控制台”。
按任意键关闭此窗口. . .

然后大致说明一下事件的一个流程:

1
2
3
4
5
6
7
8
9
10
Publisher publisher = new Publisher();
Subscriber subscriber = new Subscriber();

publisher.SimpleEvent += subscriber.onA;
publisher.SimpleEvent += subscriber.onB;
publisher.RaiseTheEvent("test");

Console.WriteLine("\r\nRemove onB");
publisher.SimpleEvent -= subscriber.onB;
publisher.RaiseTheEvent("test 2");

首先使用+=订阅事件,也就是给事件增加事件处理程序,这里决定之后发布事件的时候会调用哪些事件处理程序进行处理。然后调用RaiseTheEvent方法发布(触发)事件,该方法包含了SimpleEvent?.Invoke()语句。之后事件被触发,执行该事件保存的事件处理程序。

EventArgs不能传递任何数据。如果要传递数据,那就声明一个派生自EventArgs的类(比如上面的SimpleEventArgs类)。

我们还可以改变+=-=运算符的行为,只需要为事件定义事件访问器即可:

1
2
3
4
5
6
7
8
9
10
11
public event EventHandler<SimpleEventArgs>? SimpleEvent
{
add
{
...
}
remove
{
...
}
}

泛型(Generic)

泛型是什么

泛型(generic)允许我们声明类型参数化(type-parameterized)的代码,用不同的类型进行实例化。也就是,我们可以用“类型占位符”来写代码,然后在创建类的实例时指名真实的类型。

简单来说就是现在我们有一个返回两数中更大数的方法,返回类型可以不再限定是某一个具体的类型(比如int),而是可以是任一类型,这样我们的代码就能够进行复用。

泛型的食用方法

泛型方法

首先声明泛型方法:

1
2
3
4
public void OneGenericMethod<TFirst, TSecond>(TFirst first, TSecond second)
{
...
}

需要调用这个泛型方法的时候,用实际类型将类型参数列表的泛型类型替代即可:

1
OneGenericMethod<int, int>(0, 0);

当然编译器能够从方法参数中推断类型参数,所以我们可以省略尖括号:

1
OneGenericMethod(0, 0);

泛型类

要创建泛型类,一共分为三大步:

  1. 声明泛型类
  2. 构造实际类
  3. 创建实例

首先是声明泛型类:

1
2
3
4
5
class OneGenericClass<TFirst, TSecond>
{
public TFirst First;
public TSecond Second;
}

然后是构造实际类和使用new关键字创建实际类的实例:

1
public OneGenericClass<int, int> A = new OneGenericClass<int, int>();

类型参数的约束

我们可以提供额外信息来让编译器知道参数可以接受哪些类型,这些额外信息就是约束。只有符合约束的类型才能替代给定的类型参数来产生构造类型。

利用Where子句来对泛型接受的类型进行约束。

约束有非常多种,建议直接在官网看:点我

这里列举几个常用的约束:

  1. where T : class限定类型参数必须是引用类型
  2. where T : <基类名>限定类型参数必须是指定的基类或派生自指定的基类
  3. where T : <接口名称>限定类型参数必须是指定的接口或实现指定的接口

泛型结构

泛型结构和泛型类相似,这里不再赘述。

泛型委托

其实在上面已经见过了,C#本身提供了几个泛型委托。

泛型接口

泛型接口其实也跟泛型类差不多,毕竟泛型接口就是为了给泛型类实现的。

与其他泛型相似,用不同类型参数实例化的泛型接口的实例时不同接口,而且我们也可以在非泛型类型中实现泛型接口,比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface IExampleIfc<T>
{
T ReturnValue(T value);
}

class ExampleClass : IExampleIfc<int>, IExampleIfc<string>
{
public int ReturnValue(int value)
{
return value;
}

public string ReturnValue(string value)
{
return value;
}
}

不过需要注意的是下面这种情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface IExampleIfc<T>
{
T ReturnValue(T value);
}

class ExampleClass<S> : IExampleIfc<int>, IExampleIfc<S>
{
public int ReturnValue(int value)
{
return value;
}

public S ReturnValue(S value)
{
return value;
}
}

如果这里的S利用int来替代的话,这个类就有两个相同类型的接口了,而这是不允许的。

迭代器(Iterator)

如果我们要循环访问一个集合中的所有内容,我们就会使用foreach语句。那要在这个集合上使用foreach语句,前提就是这个集合会提供一个叫做枚举器(enumerator)的对象。(其实这里不就像python中的for index, item in enumerator(list)吗)

那么如何让集合提供一个枚举器呢?那就是通过IEnumerable<T>IEnumerator<T>这两个接口(当然C#也提供了这两个接口的非泛型版本)。

迭代器会为我们创建枚举器。

(其实这部分讲得不是很清楚,自己理解的也不是很清楚,建议还是看看官网吧:点我

枚举器的食用方法

对于一个方法,可以简单的将其返回类型设置为IEnumerable并在方法体内使用yield return语句来实现返回迭代器。

IEnumerable是产生可枚举类型的迭代器,而IEnumerator是产生枚举器的迭代器。

对于一个类,有两种模式。

  1. 枚举器的迭代器模式
  2. 可枚举类型的迭代器模式

其中第一种模式,是在类里面利用IEnumerator首先实现一个返回枚举器的迭代器,然后再通过实现GetEnumerator来让类可枚举。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class MyClass
{
public IEnumerator GetEnumerator()
{
return IteratorMethod();
}

public IEnumerator IteratorMethod()
{
yield return ...;
}
}

Main
{
MyClass mc = new MyClass();
foreach( string x in mc )
...
}

而第二种模式则是在类里用IEnumerable来实现返回可枚举类型的迭代器,这样做的话,我们可以不需要实现GetEnumerator,因为能够直接调用迭代器方法来获取可枚举类型。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class MyClass
{
public IEnumerator GetEnumerator()
{
return IteratorMethod().GetEnumerator();
}

public IEnumerable IteratorMethod()
{
yield return ...;
}
}

Main
{
MyClass mc = new MyClass();
foreach( string x in mc )
...
foreach( string x in mc.IteratorMethod() )
...
}

语言集成查询(LINQ)

什么事LINQ

LINQ,即语言集成查询(Language Integrated Query),是 .NET 框架的扩展,它允许我们以使用 SQL 查询数据库的类似方式来查询数据集合。使用 LINQ,我们可以从数据库、对象集合以及 XML 文档等中查询数据。

查询操作的三个部分

所有 LINQ 查询操作都由以下三个不同的操作组成:

  1. 获取数据源,如:int[] nums = { 0, 1, 2, 3, 4 };
  2. 创建查询,如:IEnumerable<int> numQuery = from num in nums where (num % 2) == 0 select num;
  3. 执行查询,如:foreach (int num in num Query) Console.WriteLine(num);

数据源

LINQ 数据源是支持泛型IEnumerable<T>接口或从中继承的接口的任意对象。

查询

查询是一组指令。查询指定要从数据源中检索的信息。查询还可以指定在返回这些信息之前如何对其进行排序、分组和结构化。
目前需要注意的是,在 LINQ 中,查询变量本身不执行任何操作并且不返回任何数据,它只是存储在以后某个时刻执行查询时为生成结果而必需的信息。

执行查询

查询变量本身只存储查询命令,以前面的代码为例,numQuery就是查询变量,但是它不包含查询的结果,而是包含能够执行这个查询的代码。但是如果查询返回的是标量(即单个值,如:numQuery.Count()),则查询会立即执行,并且把结果保存在查询变量中。

  • 如果查询表达式返回枚举,则查询一直到处理枚举时才会执行。
  • 如果枚举被处理多次,查询就会执行多次。
  • 如果在进行遍历之后、查询执行之前数据有改动,则查询会使用新的数据。
  • 如果查询表达式返回标量,查询立即执行,并且把结果保存在查询变量中。

方法语法和查询语法

我们在写 LINQ 查询时可以使用两种形式的语法:查询语法和方法语法。

  • 方法语法(method syntax):使用标准的方法调用。方法语法时命令式(imperative)的,它指明了查询方法调用的顺序。
  • 查询语法(query syntax):和 SQL 语句很相似,使用查询表达式形式书写。查询语法是声明式(declarative)的,也就是说,查询描述的是你想返回的东西,但并没有指明如何执行这个查询。
  • 方法语法 + 查询语法混用

    微软推荐使用查询语法。不过有一些运算符必须使用方法语法来书写。

查询表达式

查询表达式是以查询语法表示的查询。查询表达式必须以from子句开头,且必须以selectgroup子句结尾。

from 子句

from子句指定了要作为数据源使用的数据集合。from子句的用法如下:from Type Item in Items

  • Type是集合中元素的类型。这是可选的,因为编译器可以从集合中推断类型。
  • Item是迭代变量的名字。
  • Items是要查询的集合的名字。集合必须是可枚举的(IEnumerable)。

join 子句

join子句可基于每个元素中指定的键之间的相等比较,将一个数据源中的元素与另一个数据源中的元素进行关联和/或合并。例如:

1
2
3
var query = from s in students 
join c in stuInCourses on s.StId equals c.StId
select s.Name;

注意必须使用上下文关键字equals来比较字段,而不能使用==运算符。

where 子句

where子句可基于一个或多个谓词表达式,从数据源中筛选出元素。例如:

1
2
3
IEnumerable<Student> query = from s in students 
where s.Age = 18
select s;

let 子句

let子句可将表达式(如方法调用)的结果存储在新范围变量中。例如:

1
2
3
IEnumerable<string> query = from name in names 
let firstName = name.Split(' ')[0]
select firstName;

orderby 子句

orderby子句可按升序或降序对结果进行排序,还可以指定次要排序顺序。例如:

1
2
3
IEnumerable<Student> query = from s in students 
orderby s.StId, s.Age descending
select s;

select 子句

select子句可生成所有其他类型的序列。比如:

1
2
3
IEnumerable<Country> sortedQuery = from country in countries 
orderby country.Area
select country;

在这里select子句生成重新排序的Country对象的序列。

group 子句

group子句可生成按指定键组织的组的序列。不过要注意的是group子句返回对象集合的集合IEnumerable<IGrouping<TKey, TElement>>),而不是对象的集合。

into 子句

into子句可以接受查询的一部分的结果并赋予一个名字,从而可以在查询的另一个部分中使用。例如:

1
2
3
4
5
var query = from a in groupA
join b in groupB on a equals b
into groupAandB
from c in groupAandB
select c

标准查询运算符

标准查询运算符由一系列 API 方法组成,能让我们查询任何 .NET 数组和集合。例如以下两个查询是等效的:

1
2
3
4
5
6
IEnumerable<int> qurey1 = from num in numbers
where num % 2 == 0
orderby num
select num;

IEnumerable<int> query2 = numbers.Where(num => num % 2 == 0).OrderBy(n => n);

如果想要了解更多 API 请参考官方文档:标准查询运算符概述 (C#)

扩展:PLINQ

PLINQ 就是 LINQ 的并行实现,它会尝试充分利用系统上的所有处理器(来执行查询从而提升性能)。如果单独处理源集合中的每个元素,且各个代理之间不涉及共享状态,PLINQ 的性能最佳。

在许多情况下,并行执行意味着查询运行速度显著提高。…但是,并行可能会引入其自身的复杂性,因此并非所有的查询操作的运行速度在 PLINQ 中都更快。 事实上,并行实际上会降低某些查询的速度。 因此,应了解排序等问题将如何对并行查询产生影响。

System.Linq.ParallelEnumerable类及其常用方法

  • AsParallel: PLINQ 的入口点。指定如果可能,应并行化查询的其余部分。
  • AsSequential: 指定查询的其余部分应像非并行的 LINQ查询一样按顺序运行。
  • AsOrdered: 指定 PLINQ 应为查询的其余部分保留源序列的排序
  • WithCancellation: 指定 PLINQ 应定期监视请求取消时所提供的取消标记的状态以及取消执行。

执行模式

可以使用WithExecutionMode方法和System.Linq.ParallelExecutionMode枚举指示 PLINQ 选择并行算法。

并行度

默认情况下,PLINQ 使用主机计算机上的所有处理器。可以使用WithDegreeOfParallelism方法指示 PLINQ 使用不超过指定数量的处理器。

ForAll运算符

在 PLINQ 中,如果不在乎查询结果的最终排序,请使用ForAll方法执行 PLINQ 查询。(PLINQ 查询会将从每个工作线程中得到的结果合并回主线程上,但是ForAll不执行最终的这一合并步骤)


这里有一只爱丽丝

希望本文章能够帮到您~


C#笔记——委托、事件、泛型、迭代器和LINQ
https://map1e-g.github.io/2023/06/01/CSharp-learning-1/
作者
MaP1e-G
发布于
2023年6月1日
许可协议