C#10 快速语法参考(三)
二十八、事件
事件使一个对象能够在感兴趣的事情发生时通知其他对象。引发事件的对象称为发布者,处理事件的对象称为订阅者。
出版者
为了演示事件的使用,将首先创建一个发布者。这将是一个继承自ArrayList的类,但是这个版本将在列表中添加一个项目时引发一个事件。在创建事件之前,需要一个委托来保存订阅者。这可以是任何类型的委托,但是标准的设计模式是使用接受两个参数的void委托。第一个参数指定事件的源对象,第二个参数是一个类型,它是或者继承自System.EventArgs类。这个参数通常包含事件的细节,但是在这个例子中不需要传递任何事件数据,所以基类EventArgs将被用作参数的类型。
public delegate void
EventHandlerDelegate(object sender,
System.EventArgs e);
class Publisher : System.Collections.ArrayList
{
// ...
}
事件关键字
定义了委托后,可以在Publisher类中使用event关键字后跟委托和事件名称来创建事件。event关键字创建了一种特殊的委托,只能从声明它的类中调用。它的访问级别是公共的,因此允许其他类订阅该事件。事件关键字后面的委托称为事件委托。事件的名称通常是动词。在这种情况下,将在添加项目后引发事件,因此使用动词“Add”的过去式,即“added”。如果创建了一个前置事件,它在实际事件之前引发,那么将使用动词的动名词(–ing)形式,在本例中为“添加”。
public event EventHandlerDelegate Added;
或者,可以使用预定义的System.EventHandler委托来代替这个定制事件委托。这个委托与前面定义的委托相同,它用在。NET 类库来创建没有事件数据的事件。
事件调用方
要调用事件,可以创建一个事件调用方。该方法的命名约定是在事件名称前加上单词On,在本例中变成了OnAdded。该方法具有受保护的访问级别,以防止它被不相关的类调用,并且它被标记为虚拟的,以允许派生类重写它。它将事件参数作为它的一个参数,在本例中是EventArgs类型。只有当事件不为 null 时,方法才会引发事件,也就是说只有当事件有任何已注册的订户时。为了引发该事件,this实例引用作为发送方被传递,而EventArgs对象是被传递给该方法的对象。
protected virtual void OnAdded(System.EventArgs e)
{
if (Added != null) Added(this, e);
}
引发事件
现在这个类有了一个事件和一个调用它的方法,最后一步是覆盖ArrayList的Add方法,让它引发事件。在这个方法的重写版本中,首先调用基类的Add方法,并存储结果。然后用OnAdded方法引发该事件,向其传递System.EventArgs类中的Empty字段,该字段表示没有数据的事件。最后,将结果返回给调用者。
public override int Add(object value)
{
int i = base.Add(value);
OnAdded(System.EventArgs.Empty);
return i;
}
完整的Publisher类现在如下所示:
class Publisher : System.Collections.ArrayList
{
public delegate void
EventHandlerDelegate(object sender,
System.EventArgs e);
public event EventHandlerDelegate Added;
protected virtual void OnAdded(System.EventArgs e)
{
if (Added != null) Added(this, e);
}
public override int Add(object value)
{
int i = base.Add(value);
OnAdded(System.EventArgs.Empty);
return i;
}
}
订户
要使用Publisher类,将创建另一个订阅该事件的类。
class Subscriber
{
//...
}
此类包含一个事件处理程序,它是一个与事件委托具有相同签名的方法,用于处理事件。处理程序的名称通常与事件名称相同,后面跟有EventHandler后缀。
class Subscriber
{
public void AddedEventHandler(object sender,
System.EventArgs e)
{
System.Console.WriteLine("AddEvent occurred");
}
}
订阅事件
Publisher和Subscriber类现在已经完成。为了演示它们的用法,添加了一个Main方法,其中创建了Publisher和Subscriber类的对象。为了将Subscriber对象中的处理程序注册到Publisher对象中的事件,事件处理程序被添加到事件中,就好像它是一个委托。但是,与委托不同的是,事件不能从其包含类的外部直接调用。相反,该事件只能由Publisher类引发,在这种情况下,当一个项目被添加到该对象时就会发生。
class MyApp
{
static void Main()
{
Subscriber s = new Subscriber();
Publisher p = new Publisher();
p.Added += s.AddedEventHandler;
p.Add(10); // "AddEvent occurred"
}
}
二十九、泛型
泛型指的是类型参数的使用,它提供了一种设计代码模板的方法,这些模板可以操作不同的数据类型。具体来说,可以创建泛型方法、类、接口、委托和事件。
通用方法
在下面的示例中,有一个交换两个整数参数的方法:
static void Swap(ref int a, ref int b)
{
int temp = a;
a = b;
b = temp;
}
要使它成为一个可以处理任何数据类型的泛型方法,首先需要在方法名后面添加一个类型参数,用尖括号括起来。类型参数的命名约定是,它们应该以大写字母T开头,然后每个描述参数的单词都要大写。然而,在这种情况下,描述性的名称不会增加太多的价值,通常只是用大写字母T来命名类型参数。
static void Swap<T>(ref int a, ref int b)
{
int temp = a;
a = b;
b = temp;
}
类型参数现在可以用作方法中的任何其他类型,因此完成泛型方法需要做的第二件事是用类型参数替换将要成为泛型的数据类型。
static void Swap<T>(ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}
调用泛型方法
泛型方法现在完成了。要调用它,需要在方法参数之前的尖括号中指定所需的类型参数。
int a = 0, b = 1;
Swap<int>(ref a, ref b);
在这种情况下,也可以像调用常规方法一样调用泛型方法,而无需指定类型参数。这是因为编译器可以自动确定类型,因为泛型方法的参数使用类型参数。但是,如果不是这种情况,或者要使用除编译器选择的类型参数之外的另一个类型参数,则需要显式指定类型参数。
Swap(ref a, ref b);
每当在运行时第一次调用泛型时,都会实例化该泛型的一个专用版本,该版本用指定的类型实参替换了每次出现的类型形参。将调用的是这个生成的方法,而不是泛型方法本身。用相同的类型参数再次调用泛型方法将重用这个实例化的方法。
Swap<int>(ref a, ref b); // create & call Swap<int>
Swap<int>(ref a, ref b); // call Swap<int>
当使用新类型调用泛型方法时,将实例化另一个专用方法。
long c = 0, d = 1;
Swap<long>(ref c, ref d); // create & call Swap<long>
泛型类型参数
通过在尖括号之间添加更多的类型参数,可以将泛型定义为接受多个类型参数。根据泛型方法定义的类型参数的数量,泛型方法也可以被重载。
static void Dictionary<K, V>() {}
static void Dictionary<K>() {}
缺省值
当使用泛型时,可能出现的一个问题是如何将默认值赋给类型参数,因为该值取决于类型。解决方案是使用default关键字,后跟括号中的类型参数。无论使用哪个类型参数,该表达式都将返回默认值。
static void Reset<T>(ref T a)
{
a = default(T);
}
C# 7.1 中增强了默认表达式。从这个版本开始,当编译器可以根据上下文推断类型时,可以省略提供给 default 的类型。
static void Reset<T>(ref T a)
{
a = default; // same as default(T)
}
通用类
泛型类允许类成员使用类型参数。它们的定义方式与泛型方法相同,即在类名后添加一个类型参数。
class Point<T>
{
public T x, y;
}
为了从泛型类实例化一个对象,使用了标准的符号,但是在两个类名后面都指定了类型参数。请注意,与泛型方法不同,泛型类必须总是用显式指定的类型参数进行实例化。
Point<short> p = new Point<short>();
泛型类继承
泛型类的继承方式略有不同。泛型类可以从非泛型类(也称为具体类)继承。其次,它可以从另一个指定了类型参数的泛型类继承,即所谓的封闭构造基类。最后,它可以从一个开放构造的基类继承,该基类是一个泛型类,其类型参数未指定。
class BaseConcrete {}
class BaseGeneric<T>{}
class Gen1<T> : BaseConcrete {} // concrete
class Gen2<T> : BaseGeneric<int>{} // closed constructed
class Gen3<T> : BaseGeneric<T> {} // open constructed
从开放构造基类继承的泛型类必须定义基类的所有类型参数,即使派生的泛型类不需要它们。这是因为当子类被实例化时,只能发送子类的类型参数。
class BaseMultiple<T, U, V> {}
class Gen4<T, U> : BaseMultiple<T, U, int> {}
这也意味着非泛型类只能从封闭的构造基类继承,而不能从开放的基类继承,因为非泛型类在实例化时不能指定任何类型参数。
class Con1 : BaseGeneric<int> {} // ok
class Con2 : BaseGeneric<T> {} // error
通用接口
用类型参数声明的接口成为泛型接口。通用接口和常规接口有两个相同的目的。创建它们或者是为了公开将被其他类使用的类的成员,或者是为了强制一个类实现特定的功能。实现泛型接口时,必须指定类型参数。泛型接口可以由泛型和非泛型类实现。
// Generic functionality interface
interface IGenericCollection<T>
{
void store(T t);
}
// Non-generic class implementing generic interface
class Box : IGenericCollection<int>
{
public int myBox;
public void store(int i) { myBox = i; }
}
// Generic class implementing generic interface
class GenericBox<T> : IGenericCollection<T>
{
public T myBox;
public void store(T t) { myBox = t; }
}
通用委托
可以用类型参数定义委托。例如,下面的泛型委托使用其类型参数来指定可引用方法的参数。从这个委托类型中,可以创建一个委托对象,该对象可以引用任何采用单个参数的void方法,而不管其类型如何。
class MyApp
{
public delegate void PrintDelegate<T>(T arg);
public static void Print(string s)
{
System.Console.Write(s);
}
static void Main()
{
PrintDelegate<string> d = Print;
}
}
一般事件
泛型委托可用于定义泛型事件。例如,不使用典型的设计模式,即事件的发送者是Object类型,类型参数可以允许指定发送者的实际类型。这将使参数成为强类型,从而允许编译器强制对该参数使用正确的类型。
delegate void EventDelegate<T, U>(T sender, U eventArgs);
event EventDelegate<MyApp, System.EventArgs> myEvent;
泛型和对象
一般来说,应该避免使用Object类型作为通用容器。像ArrayList这样的Object容器之所以存在于。NET 类库是因为泛型直到 C# 2.0 才引入。与Object类型相比,泛型不仅确保了编译时的类型安全,还消除了与将值类型装箱和拆箱到Object容器中相关的性能开销。
// Object container class
class Box { public object o; }
// Generic container class (preferred)
class Box<T> { public T o; }
class MyApp
{
static void Main()
{
// .NET object container
System.Collections.ArrayList a;
// .NET generic container (preferred)
System.Collections.Generic.List<int> b;
}
}
限制
当定义泛型类或方法时,可以对类或方法实例化时可能使用的类型参数的种类应用编译时强制限制。这些限制被称为约束,并使用where关键字指定。总而言之,有六种约束。
首先,可以使用struct关键字将类型参数限制为值类型。
class C<T> where T : struct {} // value type
其次,可以通过使用class关键字将参数约束为引用类型。
class D<T> where T : class {} // reference type
第三,约束可以是类名。这将把类型限制为该类或它的一个派生类。
class B {}
class E<T> where T : B {} // be/derive from base class
第四,该类型可以被约束为另一个类型参数或从另一个类型参数派生。
class F<T, U> where T : U {} // be/derive from U
第五个约束是指定一个接口。这将把类型参数限制为仅实现指定接口的类型,或者是接口类型本身的类型。
interface I {}
class G<T> where T : I {} // be/implement interface
最后,类型参数可以被约束为只有那些具有公共无参数构造函数的类型。
class H<T> where T : new() {} // parameterless constructor
多重约束
通过在逗号分隔的列表中指定多个约束,可以将这些约束应用于类型参数。此外,为了约束多个类型参数,可以添加额外的where子句。注意,如果使用了类或struct约束,它必须出现在列表的第一位。此外,如果使用无参数构造函数约束,它必须是列表中的最后一个。
class J<T, U>
where T : class, I
where U : I, new() {}
为什么使用约束
除了将泛型方法或类的使用仅限于某些参数类型之外,应用约束的另一个原因是增加约束类型支持的允许操作和方法调用的数量。不受约束的类型只能使用System.Object方法。但是,通过应用基类约束,该基类的可访问成员也变得可用。
class Person
{
public string name;
}
class PersonNameBox<T> where T : Person
{
public string box;
public void StorePersonName(T a)
{
box = a.name;
}
}
下面的示例使用无参数构造函数约束。此约束允许实例化类型参数的新对象。
class Base<T> where T : new() {}
请注意,如果一个类对其类型参数有约束,并且该类的一个子类有一个受基类约束的类型参数,则该约束也必须应用于子类的类型参数。
class Derived<T> : Base<T>
where T : Base<T>, new() {}
三十、常量
通过在数据类型前添加const关键字,C# 中的变量可以变成编译时常量。这个修饰符意味着变量不能被改变,因此必须在声明变量的同时给它赋值。任何向常量赋值的尝试都会导致编译时错误。
局部常量
局部常量必须在声明的同时初始化。
static void Main()
{
const int a = 10; // compile-time constant
}
const修饰符创建了一个编译时常量,因此编译器会用它的值替换常量的所有用法。因此,赋值必须在编译时已知。因此,const修饰符只能与简单类型一起使用,也可以与枚举和字符串类型一起使用。
常量字段
可以对字段应用const修饰符,使字段不可更改。
class Box
{
const int b = 5; // compile-time constant field
}
常量字段不能有static修饰符。它们是隐式静态的,访问方式与静态字段相同。
int a = Box.b;
只读
另一个类似于const的变量修饰符是readonly,它创建一个运行时常量。这个修改器可以应用于字段,并且,像const一样,它使字段不可更改。
class Box
{
readonly int c = 3; // run-time constant field
}
因为readonly字段是在运行时分配的,所以它可以被分配一个直到运行时才知道的动态值。
readonly int d = System.DateTime.Now.Hour;
与const不同,readonly可以应用于任何数据类型。
readonly int[] e = { 1, 2, 3 }; // readonly array
此外,readonly字段不能仅在声明时初始化。也可以在构造函数中给它赋值。
class Box
{
readonly string s;
public Box() { s = "Hello World"; }
}
从 C# 7.2 开始,readonly修饰符不仅可以应用于字段,还可以应用于struct s。将一个struct声明为readonly将对struct的成员强制不变性,要求所有字段和属性都成为readonly。
readonly struct Container
{
public readonly int value;
public int Property { get; }
public Container(int v, int p)
{
value = v;
Property = p;
}
}
C# 7.2 中的另一个新增功能是,当通过引用使用ref修饰符返回值类型时,可以将方法的返回值标记为readonly。这将不允许调用者修改返回值,前提是返回值也被指定为一个readonly引用,而不仅仅是一个副本。
class MyApp
{
readonly static int i;
static ref readonly int GetValue() { return ref i; }
static void Main()
{
ref readonly int a = ref GetValue();
a = 5; // error: readonly variable
}
}
在参数中
类似于ref参数修饰符,C# 7.2 增加了in修饰符,它提供了传递参数作为readonly引用的能力。方法中任何试图修改in参数(或者在struct的情况下修改其成员)的代码都会在编译时失败,因此参数必须在方法调用之前初始化。
class MyApp
{
static void Test(in int num)
{
num = 5; // error: readonly parameter
}
static void Main()
{
int i = 10;
Test(i); // passed by readonly reference
Test(2); // allowed, temporary variable created
}
}
像ref修饰符一样,in修饰符防止对值类型进行不必要的复制。出于性能原因,这很有用,特别是当将一个大的struct对象传递给一个被多次调用的方法时。
不变的准则
一般来说,如果不需要重新分配变量,最好总是将变量声明为const或readonly。这确保了变量不会在程序的任何地方被错误地改变,这反过来有助于防止错误。当一个变量不打算被修改时,它也清楚地传达给其他开发人员。
三十一、异步方法
一个异步方法是一个可以在完成执行之前返回的方法。任何执行潜在的长时间运行任务的方法,比如访问 web 资源或读取文件,都可以变成异步的,以提高程序的响应能力。这在图形应用中尤其重要,因为任何在用户界面线程上花费很长时间执行的方法都会导致程序在等待该方法完成时没有响应。
Async 和 Await 关键字
在 C# 5 中引入的关键字async和await允许用类似于同步(常规)方法的简单结构编写异步方法。async修饰符指定该方法是异步的,因此它可以包含一个或多个 await 表达式。await 表达式由关键字await和一个 await 方法调用组成。
class MyApp
{
async void AsyncWriter()
{
System.Console.Write("A");
await System.Threading.Tasks.Task.Delay(2000);
System.Console.Write("C");
}
}
该方法将同步运行,直到到达 await 表达式,此时该方法被挂起,执行返回到调用方。等待的任务被安排在同一线程的后台运行。在这种情况下,任务是一个定时延迟,将在 2000 毫秒后完成。一旦任务完成,异步方法的剩余部分将执行。
从 Main 调用 async 方法将输出“A ”,然后是“B ”,延迟后是“C”。请注意,这里使用 ReadKey 方法是为了防止控制台程序在异步方法完成之前退出。
static void Main()
{
new MyApp().AsyncWriter();
System.Console.Write("B");
System.Console.ReadKey();
}
异步返回类型
在 C# 5 中,一个异步方法可以有三种内置的返回类型:Task 、Task 和 void。指定 Task 或 void 表示该方法不返回值,而 Task 表示它将返回 t 类型的值。与 void 相反,Task 和 Task 类型是可调用的,因此调用方可以使用await关键字挂起自己,直到任务完成。void 类型主要用于定义异步事件处理程序,因为事件处理程序需要 void 返回类型。
自定义异步方法
为了异步调用一个方法,必须将它包装在另一个返回已启动任务的方法中。举例来说,下面的方法定义、启动并返回一个任务,该任务在返回字母“Y”之前需要执行 2000 毫秒。为了简明起见,这里通过使用λ表达式来定义任务。
using System.Threading.Tasks;
using System.Threading;
class MyApp
{
Task<string> MyTask()
{
return Task.Run<string>( () => {
Thread.Sleep(2000);
return "Y";
});
}
// ...
}
可以从异步方法中异步调用此任务方法。这些方法的命名约定是在方法名后面加上“Async”。本例中的异步方法等待任务的结果,然后打印出来。
async void TaskAsync()
{
string result = await MyTask();
System.Console.Write(result);
}
异步方法的调用方式与常规方法相同,如下面的 Main 方法所示。程序的输出将是“XY”。
static void Main()
{
new MyApp().TaskAsync();
System.Console.Write("X");
System.Console.ReadKey();
}
扩展退货类型
C# 7.0 减少了对异步方法返回类型的限制。当异步方法返回常量结果或可能同步完成时,这可能很有用,在这种情况下,Task 对象的额外分配可能会成为不希望的性能成本。条件是返回的类型必须实现 GetAwaiter 方法,该方法返回一个 Awaiter 对象。来利用这个新功能。NET 提供了 ValueTask 类型,这是一个包含此方法的轻量值类型。
举个例子,下面的 PowTwo 异步方法给出了参数的二次幂的结果(a 2 )。如果参数小于正负 10,它将同步执行,因此返回 ValueTask < double >类型,以便在这种情况下不必分配任务对象。注意,这里的 Main 方法有 async 修饰符。从 C# 7.1 开始,这是允许的,并且当 Main 方法直接调用 async 方法时,这种情况也可以使用。
using System.Threading.Tasks;
public class MyApp
{
static async Task Main()
{
double d = await PowTwo(10);
System.Console.WriteLine(d); // "100"
}
private static async ValueTask<double> PowTwo(double a)
{
if (a < 10 && a > -10) {
return System.Math.Pow(a, 2);
}
return await Task.Run(() => System.Math.Pow(a, 2));
}
}
若要在 Visual Studio 2022 之前的版本中使用 ValueTask 类型,需要向项目中添加一个 NuGet 包。NuGet 是一个软件包管理器,它为 Visual Studio 提供免费的开源扩展。通过在解决方案资源管理器中右键单击引用(依赖项)并选择“管理 NuGet 包”来添加包。切换到“浏览”选项卡,搜索“任务”以找到系统。线程.任务.扩展包。选择此软件包,然后单击安装。
异步流
C# 8 中增加了异步流,允许异步方法返回多个结果。这拓宽了它们的可用性,使异步方法能够在数据可用时返回数据。异步流(生产者方法)使用 yield return 语句。这会将结果返回给调用方,然后继续执行该方法,允许该方法在产生每个结果之间进行异步调用。
using System.Collections.Generic;
using System.Threading.Tasks;
class MyApp
{
static async IAsyncEnumerable<int> Streamer(int count)
{
int sum = 0;
for (int i = 0; i <= count; i++)
{
sum = sum + i;
yield return sum; // return a result
// Simulate waiting for more data
await Task.Delay(1000);
}
// end stream
}
}
为了异步流的目的,C# 8 增加了通用枚举器接口的异步版本。IAsyncEnumerable 接口在这里用于返回一个可以使用 await foreach 循环使用的流。这种 foreach 循环的变体也在 C# 8 中引入。
static async Task Main()
{
await foreach (int data in Streamer(3))
{
System.Console.Write(data + " "); // "0 1 3 6"
}
}