本文最后更新于: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) { 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) { Action1<Animal> act1 = ActOnAnimal; Action1<Dog> dog1 = act1;
dog1(new Dog()); } }
public class Animal { public int Legs = 4; }
public class Dog : Animal {
}
|
有关协变和逆变的理解这块我个人可能不是很透彻,建议参考参考其他文章。
强委托类型
其实就是.Net Core
框架里边的泛型委托类型,包括Action
、Func
和Predicate
。
其中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);
|
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);
|
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 要在调试停止时自动关闭控制台,请启用“工具”->“选项”->“调试”->“调试停止时自动关闭控制台”。 按任意键关闭此窗口. . .
|
然后大致说明一下事件的一个流程:
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 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
子句来对泛型接受的类型进行约束。
约束有非常多种,建议直接在官网看:点我
这里列举几个常用的约束:
where T : class
限定类型参数必须是引用类型
where T : <基类名>
限定类型参数必须是指定的基类或派生自指定的基类
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
是产生枚举器的迭代器。
对于一个类,有两种模式。
- 枚举器的迭代器模式
- 可枚举类型的迭代器模式
其中第一种模式,是在类里面利用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 查询操作都由以下三个不同的操作组成:
- 获取数据源,如:
int[] nums = { 0, 1, 2, 3, 4 };
- 创建查询,如:
IEnumerable<int> numQuery = from num in nums where (num % 2) == 0 select num;
- 执行查询,如:
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
子句开头,且必须以select
或group
子句结尾。
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
不执行最终的这一合并步骤)
希望本文章能够帮到您~