C# 基础、核心概念和模式交互式指南(三)
九、C# 中 OOP 原则的快速回顾
老师开始讨论:欢迎来到 C# 面向对象编程的最后一部分。让我们回顾一下本书中已经介绍过的核心原则。
- 类别和对象
- 多态
- 抽象
- 包装
- 遗产
我们可以再加两个。
- 信息传递
- 动态绑定
恶作剧
您还记得 C# 的基本构件是如何涵盖这些主题的吗?
答案
- 类和对象:在整本书中,几乎在每个例子中,我们都使用了不同类型的类和对象。在静态类的例子中,我们没有创建对象。我们可以通过类名访问静态字段。
- 多态:涵盖了两种类型的多态。编译时多态通过方法重载(和操作符重载)来覆盖,运行时多态通过使用虚拟和重写关键字的方法重写技术来覆盖。
- 抽象:这个特性通过抽象类和接口进行了测试。
- 封装:除了访问修饰符,我们还使用了属性和索引器的概念。
- 继承:我们在两章中探讨了不同类型的继承。
- 消息传递:这个特性在多线程环境中很常见。但是我们可以在这一类中考虑运行时多态。
- 动态绑定:通过方法覆盖实例的运行时多态可以属于这一类。#
学生问:
先生,您能总结一下抽象和封装的区别吗?
老师说:将数据和代码包装成一个实体的过程称为封装。使用这种技术,我们可以防止任意和不安全的访问。我们使用了不同种类的访问修饰符以及带有 get 和 set 访问器的属性示例来实现封装的概念。
在抽象中,我们展示了基本的特性,但是对用户隐藏了详细的实现;例如,当我们用遥控器打开电视时,我们并不关心该设备的内部电路。只要按下按钮后图像从电视中出来,我们对这个设备绝对没问题。
您可以重新阅读第一章了解这些定义。
学生问:
一般来说,编译时多态和运行时多态哪个更快?
老师说:我表示,如果电话能够尽早解决,通常会更快。这就是为什么我们可以得出编译时绑定比运行时绑定(或多态)更快的结论——因为您预先知道要调用哪个方法。
学生问:
先生,你早些时候告诉我们,继承并不总是提供最好的解决办法。你能详细说明一下吗?
老师说:在某些情况下,作文可以提供更好的解决方案。但是要理解构成,你需要知道这些概念:
- 联合
- 聚合
关联可以是单向的也可以是双向的。当你看到这种 UML 图时,这意味着 ClassA 知道 ClassB,但反过来却不是这样。
下图显示了一个双向关联,因为这两个类彼此都认识。
考虑一个例子。在大学里,一个学生可以向多个老师学习,一个老师可以教多个学生。在这种关系中没有专门的所有权。所以,当我们在编程中用类和对象来表示它们时,我们可以说这两种对象都可以独立地创建和删除。
聚合是一种更强的关联类型。广泛代表如下。
考虑此类别中的一个示例。假设 X 教授提交辞职信,因为他决定加入一个新的机构。虽然 X 教授和他以前的机构没有对方也能生存,但最终 X 教授需要与机构中的一个部门建立联系。在编程世界中类似的情况下,我们会说系是这种关系的所有者,并且系里有教授。
同样,我们可以说汽车有座位,自行车有轮胎,等等。
注意
一个系有一个教授。这就是为什么关联关系也被称为“具有”关系。(这里一定要记住和继承的关键区别。继承与“是”的关系相关联。
组合是一种更强的聚合形式,这次我们有一个填充的菱形。
学院中的一个系不能离开学院而存在。学院只创建或关闭它的系。(你可以争辩说,如果根本没有系,学院就不可能存在,但是我们没有必要考虑这种类型的极端情况而使事情复杂化。换句话说,一个系的寿命完全取决于它的学院。这也被称为死亡关系,因为如果我们摧毁了学院,它的所有部门都会被自动摧毁。
为了展示构图的威力,让我们重温一下我们在第三章讨论过的钻石问题,然后分析下面的程序。
我们现有的代码
using System;
namespace CompositionEx1
{
class Parent
{
public virtual void Show()
{
Console.WriteLine("I am in Parent");
}
}
class Child1 : Parent
{
public override void Show()
{
Console.WriteLine("I am in Child-1");
}
}
class Child2 : Parent
{
public override void Show()
{
Console.WriteLine("I am in Child-2");
}
}
假设孙辈派生自 Child1 和 Child2,但它没有覆盖Show()方法。
因此,我们预期的 UML 图可能如下所示:
我们现在有了歧义。孙儿将从哪个类调用Show()方法——child 1 还是 Child2?为了消除这种类型的歧义,C# 不支持通过类的多重继承。这就是所谓的钻石问题。
所以,如果你这样编码:
class GrandChild : Child1, Child2//Error: Diamond Effect
{
public void Show()
{
Console.WriteLine("I am in Child-2");
}
}
C# 编译器会报错:
现在让我们看看如何用构图来处理这种情况。考虑下面的代码。
演示 1:处理前面问题的组合
using System;
namespace CompositionEx1
{
class Parent
{
public virtual void Show()
{
Console.WriteLine("I am in Parent");
}
}
class Child1 : Parent
{
public override void Show()
{
Console.WriteLine("I am in Child-1");
}
}
class Child2 : Parent
{
public override void Show()
{
Console.WriteLine("I am in Child-2");
}
}
//class GrandChild : Child1, Child2//Error: Diamond Effect
//{
//}
class Grandchild
{
Child1 ch1 = new Child1();
Child2 ch2 = new Child2();
public void ShowFromChild1()
{
ch1.Show();
}
public void ShowFromChild2()
{
ch2.Show();
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Composition
to handle the Diamond Problem***\n");
Grandchild gChild = new Grandchild();
gChild.ShowFromChild1();
gChild.ShowFromChild2();
Console.ReadKey();
}
}
}
输出
分析
您可以看到 Class1 和 Class2 都覆盖了它们的父方法Show()。而孙儿类没有自己的Show()方法。不过,我们可以通过孙子的对象调用那些特定于类的方法。
孙子女正在其体内创建来自 Class1 和 Class2 的对象。因此,如果我们的应用中不存在孙对象(例如,如果这些对象被垃圾收集),我们可以说系统中没有 Class1 或 Class2 对象。您还可以对用户设置一些限制,使他们不能直接在应用中创建 Class1 和 Class2 的对象;但是为了简单起见,我们忽略了这一部分。
演示 2:聚合示例
假设在前面的例子中,你想变得自由一点。您希望避免孙类和子类之间的死亡关系。您可以使用聚合来实现一个程序,其中其他类可以有效地使用对 Class1 和 Class2 的引用。
using System;
namespace AggregationEx1
{
class Parent
{
public virtual void Show()
{
Console.WriteLine("I am in Parent");
}
}
class Child1 : Parent
{
public override void Show()
{
Console.WriteLine("I am in Child-1");
}
}
class Child2 : Parent
{
public override void Show()
{
Console.WriteLine("I am in Child-2");
}
}
//class GrandChild : Child1, Child2//Error: Diamond Effect
//{
//}
class Grandchild
{
Child1 ch1;
Child2 ch2;
public Grandchild(Child1 ch1, Child2 ch2)
{
this.ch1 = ch1;
this.ch2 = ch2;
}
public void ShowFromChild1()
{
ch1.Show();
}
public void ShowFromChild2()
{
ch2.Show();
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Aggregation
to handle the Diamond Problem***\n");
Child1 child1 = new Child1();
Child2 child2 = new Child2();
Grandchild gChild = new Grandchild(child1,child2);
gChild.ShowFromChild1();
gChild.ShowFromChild2();
Console.ReadKey();
}
}
}
输出
分析
在这种情况下,Child1 和 Child2 对象可以在没有孙对象的情况下继续存在。这就是为什么我们说组合是一种更强的聚合形式。
注意
你意识到一般化、特殊化和实现。我们在应用中使用了这些概念。当我们的类扩展另一个类(即继承)时,我们使用泛化和特化的概念;例如,足球运动员是一种特殊的运动员。或者我们可以说足球运动员和篮球运动员都是运动员(泛化)。当我们的类实现一个接口时,我们使用了实现的概念。
学生问:
OOP 的挑战和缺点是什么?
老师说:许多专家认为,一般来说,面向对象程序的规模较大。由于更大的尺寸,我们可能需要更多的存储空间(但是现在,这些问题已经不重要了。)
一些开发人员发现面向对象编程风格的困难。他们可能仍然喜欢其他方法,比如结构化编程。因此,如果他们被迫在这样的环境中工作,生活对他们来说就变得艰难了。
此外,我们不能以面向对象的方式为每个现实世界的问题建模。然而,总的来说,我个人喜欢面向对象的编程风格,因为我相信它的优点大于缺点。
摘要
本章包括以下内容:
- 快速回顾本书中的核心 OOP 原则
- 如何区分抽象和封装
- 如何在我们的 C# 应用中实现组合和聚合的概念
- 与 OOP 相关的挑战和缺点
十、委托和事件
委托介绍
老师开始讨论:委托是 C# 编程中最重要的话题之一,它们使 C# 变得非常强大。委托是从 System.Delegate 派生的引用类型。它们类似于对象引用,但主要区别在于它们指向方法。我们可以通过使用委托来实现类型安全。因此,有时我们称它们为类型安全函数指针。
Points to Remember
- 一个对象引用指向一个特定类型的对象(例如,当我们写
A ob=new A();时,我们的意思是ob是对一个A类型对象的引用);而委托指向特定类型的方法。 - 委托是一个知道如何调用与其关联的方法的对象。有了委托类型,您就知道它的实例调用哪种方法。
- 我们可以用委托编写插件方法。
假设我们有一个名为 Sum 的方法,带有两个整型参数,如下所示:
public static int Sum(int a, int b)
{
return a+b;
}
我们可以声明一个委托来指向Sum方法,如下所示:
Mydel del = new Mydel(Sum);
但在此之前,我们需要定义Mydel委托,它必须具有相同的签名,如下所示:
public delegate int Mydel(int x, int y);
对于Sum方法和Mydel委托,返回类型、参数及其对应的顺序是相同的。(记住方法名不是签名的一部分。)
注意,Mydel与任何具有integer返回类型(int)并接受两个整数参数的方法兼容,比如Sum (int a,int b)方法。
正式的定义
委托是从 System 派生的引用类型。委托,它的实例用于调用具有匹配签名的方法。委托的一般定义是“委托”因此,我们可以说我们的委托必须用匹配的签名来表示方法。
下面的示例阐释了委托的用法。
案例 1 是一个不使用委托的方法调用。
案例 2 是一个调用委托的方法。
演示 1
using System;
namespace DelegateEx1
{
public delegate int Mydel(int x, int y);
class Program
{
public static int Sum(int a, int b) { return a + b; }
static void Main(string[] args)
{
Console.WriteLine("***Delegate Example -1: A simple delegate demo***");
int a = 25, b = 37;
//Case-1
Console.WriteLine("\n Calling Sum(..) method without using a delegate:");
Console.WriteLine("Sum of a and b is : {0}", Sum(a,b));
Mydel del = new Mydel(Sum);
Console.WriteLine("\n Using delegate now:");
//Case-2
Console.WriteLine("Calling Sum(..) method with the use of a delegate:");
//del(a,b) is shorthand for del.Invoke(a,b)
Console.WriteLine("Sum of a and b is: {0}", del(a, b));
//Console.WriteLine("Sum of a and b is: {0}", del.Invoke(a, b));
Console.ReadKey();
}
}
}
输出
缩短你的代码长度
我们可以缩短前面例子中的代码长度。
替换此行:
Mydel del = new Mydel(Sum);
使用这一行:
Mydel del = Sum;
请注意注释行。del(a,b)是的简写
del.Invoke(a,b)
学生问:
假设在我们的程序中,Sum()方法是重载的。那么如果我们写 Mydel del=Sum,编译器可能会很困惑;。这是正确的吗?
老师说:一点也不。编译器可以绑定正确的重载方法。让我们用一个简单的例子来测试一下。(在前面的例子中,我们用委托测试了静态方法,所以这次我们有意使用非静态方法来涵盖这两种情况。)
演示 2
using System;
namespace Quiz1OnDelegate
{
public delegate int Mydel1(int x, int y);
public delegate int Mydel2(int x, int y,int z);
class A
{
//Overloaded non static Methods
public int Sum(int a, int b) { return a + b; }
public int Sum(int a, int b,int c) { return a + b+ c; }
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Quiz on Delegate***");
int a = 25, b = 37, c=100;
A obA1 = new A();
A obA2 = new A();
Mydel1 del1 = obA1.Sum;
Console.WriteLine("del1 is pointing Sum(int a,int b):");
//Pointing Sum(int a, int b)
Console.WriteLine("Sum of a and b is: {0}", del1(a, b));
Mydel2 del2 = obA1.Sum;//pointing Sum(int a, int b, int c)
Console.WriteLine("del2 is pointing Sum(int a,int b,int c):");
//Pointing Sum(int a, int b, int c)
Console.WriteLine("Sum of a, b and c is: {0}", del2(a, b,c));
//same as
//Console.WriteLine("Sum of a, b and c is: {0}", del2.Invoke(a, b, c));
Console.ReadKey();
}
}
}
输出
分析
编译器正在选择正确的重载方法。如果您错误地编写了这样的代码,您总是会收到一个编译时错误:
del1(a,b,c)
或者,如果你这样编码:
del2(a,b)
学生问:
为什么委托经常被称为类型安全函数指针?
老师说:当我们想把任何方法传递给委托时,委托签名和方法签名需要匹配。因此,它们通常被称为类型安全函数指针。
恶作剧
代码会编译吗?
using System;
namespace Test1_Delegate
{
public delegate int MultiDel(int a, int b);
class A : System.Delegate//Error
{ ..}
}
回答
不。我们不能从委托类派生。
多播代理/链接代理
老师继续说:当一个委托被用来封装一个匹配签名的多个方法时,我们称之为多播委托。这些委托是 System 的子类型。MulticastDelegate,它是 System.Delegate 的子类。
演示 3
using System;
namespace MulticastDelegateEx1
{
public delegate void MultiDel();
class Program
{
public static void show1() { Console.WriteLine("Program.Show1()"); }
public static void show2() { Console.WriteLine("Program.Show2()"); }
public static void show3() { Console.WriteLine("Program.Show3()"); }
static void Main(string[] args)
{
Console.WriteLine("***Example of a Multicast Delegate***");
MultiDel md = new MultiDel(show1);
md += show2;
md += show3;
md();
Console.ReadKey();
}
}
}
输出
学生问:
在前面的例子中,我们的多播委托的返回类型是 void。这背后的意图是什么?
老师说:一般来说,对于多播委托,我们在调用列表中有多个方法。但是,单个方法或委托调用只能返回单个值,因此多播委托类型应该具有 void 返回类型。如果您仍然想尝试一个非 void 返回类型,您将只从最后一个方法接收返回值。将调用前面的方法,但返回值将被丢弃。为了清楚地理解,请完成下面的测验。
恶作剧
假设我们已经编写了下面的程序,其中多播委托和与之相关的方法都有返回类型。程序会编译吗?
using System;
namespace MulticastDelegateEx2
{
public delegate int MultiDel(int a, int b);
class Program
{
public static int Sum(int a, int b)
{
Console.Write("Program.Sum->\t");
Console.WriteLine("Sum={0}", a+b);
return a + b;
}
public static int Difference(int a, int b)
{
Console.Write("Program.Difference->\t");
Console.WriteLine("Difference={0}", a - b);
return a - b;
}
public static int Multiply(int a, int b)
{
Console.Write("Program.Multiply->\t");
Console.WriteLine("Multiplication={0}", a * b);
return a * b;
}
static void Main(string[] args)
{
Console.WriteLine("***Testing a Multicast Delegate***");
MultiDel md = new MultiDel(Sum);
md += Difference;
md += Multiply;
int c = md(10, 5);
Console.WriteLine("Analyzing the value of c");
Console.WriteLine("c={0}", c);
Console.ReadKey();
}
}
}
输出
是的,程序将会编译,输出如下:
分析
注意c的值。为了编译和运行,多播委托不需要 void 返回类型。但是,如果我们有这些方法的返回类型,并且我们编写了这样的代码,那么我们将从调用/调用链中最后一个被调用的方法中获取值。在这两个值之间的所有其他值都将被丢弃,但不会对此发出警报。因此,建议您试验 void 返回类型的多播委托。
学生问:
因此,即使我们对多播委托使用 nonvoid 返回类型,我们也不会看到任何编译错误。这种理解正确吗?
老师说:是的。在这种情况下,您将只接收最后一个方法的返回值。所以,只要想想这对你是否有意义。
学生问:
我们可以使用委托来定义回调方法吗?
是的。这是使用委托的主要目的之一。
学生问:
多播委托的调用列表是什么?
老师说:多播代理维护一个代理的链表。这个列表称为调用列表,由一个或多个元素组成。当我们调用多播委托时,调用列表中的委托按照它们出现的顺序被同步调用。如果在执行过程中出现任何错误,它将抛出一个异常。
委托中的协变和逆变
当我们实例化一个委托时,我们可以给它分配一个比“最初指定的返回类型”具有“更多派生的返回类型”的方法从 C# 2.0 开始,这种支持就可用了。另一方面,逆变允许方法的参数类型比委托类型派生得少。协方差的概念从 C#1.0 开始就支持数组,所以我们可以这样写:
Console.WriteLine("***Covariance in arrays(C#1.0 onwards)***");
//ok, but not type safe
object[] myObjArray = new string[5];
但是这不是类型安全的,因为这种行
myObjArray[0] = 10;//runtime error
会遇到运行时错误。
委托/方法组方差中的协方差
从 C# 2.0 开始,委托就支持协变和逆变。对泛型类型参数、泛型接口和泛型委托的支持始于 C#4.0。到目前为止,我还没有讨论过泛型类型。所以,这一节处理非泛型委托,从协方差开始。
演示 4
using System;
namespace CovarianceWithDelegatesEx1
{
class Vehicle
{
public Vehicle ShowVehicle()
{
Vehicle myVehicle = new Vehicle();
Console.WriteLine(" A Vehicle created");
return myVehicle;
}
}
class Bus:Vehicle
{
public Bus ShowBus()
{
Bus myBus = new Bus();
Console.WriteLine(" A Bus created");
return myBus;
}
}
class Program
{
public delegate Vehicle ShowVehicleTypeDelegate();
static void Main(string[] args)
{
Vehicle vehicle1 = new Vehicle();
Bus bus1 = new Bus();
Console.WriteLine("***Covariance in delegates(C# 2.0 onwards)***");
ShowVehicleTypeDelegate del1 = vehicle1.ShowVehicle;
del1();
//Note that it is expecting a Vehicle(i.e. a basetype) but received a Bus(subtype)
//Still this is allowed through Covariance
ShowVehicleTypeDelegate del2 = bus1.ShowBus;
del2();
Console.ReadKey();
}
}
}
输出
分析
在前面的程序中,我们可以看到编译器没有抱怨这一行:
ShowVehicleTypeDelegate del2 = bus1.ShowBus;
尽管我们的委托返回类型是 Vehicle,但是它的 del2 对象接收了一个派生类型“Bus”对象。
委托的矛盾
逆变与参数有关。假设委托可以指向接受派生类型参数的方法。在 contravariance 的帮助下,我们可以使用同一个委托指向一个接受基类型参数的方法。
演示 5
using System;
namespace ContravariancewithDelegatesEx1
{
class Vehicle
{
public void ShowVehicle(Vehicle myV)
{
Console.WriteLine(" Vehicle.ShowVehicle");
}
}
class Bus : Vehicle
{
public void ShowBus(Bus myB)
{
Console.WriteLine("Bus.ShowBus");
}
}
class Program
{
public delegate void TakingDerivedTypeParameterDelegate(Bus v);
static void Main(string[] args)
{
Vehicle vehicle1 = new Vehicle();//ok
Bus bus1 = new Bus();//ok
Console.WriteLine("***Exploring Contravariance
with C# delegates***");
//General case
TakingDerivedTypeParameterDelegate del1 = bus1.ShowBus;
del1(bus1);
//Special case:
//Contravariance
:
/*Note that the delegate expected a method that accepts a bus(derived) object parameter but still it can point to the method that accepts vehicle(base) object parameter*/
TakingDerivedTypeParameterDelegate del2 = vehicle1.ShowVehicle;
del2(bus1);
//Additional note:you cannot pass vehicle object here
//del2(vehicle1);//error
Console.ReadKey();
}
}
}
输出
分析
浏览程序和支持的注释行,以便更好地理解代码。从前面的例子中我们可以看到,我们的委托TakingDerivedTypeParameterDelegate期望一个接受总线(派生的)对象参数的方法,然而它可以指向一个接受车辆作为(基本)对象参数的方法。
事件
老师说:事件是用来通知或表示一个物体的状态发生了变化。该信息对于该对象的客户端非常有用(例如,GUI 应用中的鼠标点击或按键是非常常见的事件示例)。
在现实世界中,考虑一个社交媒体平台,比如脸书。每当我们更新任何关于脸书的信息,我们的朋友都会立即得到通知。(这是一个非常常见的观察者设计模式的例子)。因此,您可以假设当您对脸书页面进行一些更改时,内部会触发一些事件,以便您的朋友可以获得这些更新。只有那些已经在我们的好友列表中的人(即,我们已经接受他们为我们的好友)才会收到这些更新。在编程术语中,我们说这些人注册在我们的好友列表中。如果有人不想获得更新,他/她可以简单地从好友列表中注销。因此,术语“注册和取消注册”与事件相关联。
在我们前进之前,我们必须记住以下几点:
- 事件与委托相关联。要理解事件,首先要学习委托。当一个事件发生时,它的客户给它的委托被调用。
- 英寸 NET 中,事件被实现为多播委托。
- 这里遵循发布者-订阅者模型。发布者(或广播者)发布通知(或信息),订户接收该通知。但是用户可以自由决定何时开始监听,何时停止监听(用编程术语来说,就是何时注册,何时注销)。
- Publisher 是包含委托的类型。订阅者通过在发布者的委托上使用+=来注册自己,并通过在该委托上使用-=来注销自己。因此,当我们将+=或-=应用于一个事件时,它们具有特殊的含义(换句话说,在这些情况下,它们不是赋值的快捷方式)。
- 用户之间不说话。实际上,这些是支持事件架构的关键目标:
- 订户不能相互通信。
- 我们可以构建一个松散耦合的系统。
- 如果我们使用 Visual Studio IDE,当我们处理事件时,它使我们的生活变得极其简单。但是我相信这些概念是 C# 的核心,所以最好从基础开始学习。
- 那个。NET framework 提供了一个支持标准事件设计模式的泛型委托,如下所示:
public delegate void EventHandler<TEventArgs>(object sendersource, TEventArgs e) where TEventArgs : EventArgs;
直到现在,你还没有学习 C# 泛型。为了支持向后兼容性,中的大多数事件。NET framework 遵循我们在这里使用的非泛型自定义委托模式。
在 C# 中实现简单事件的步骤
(这里我们将尝试遵循最广泛接受的命名约定。)
步骤 1:创建发布者类
- #1.1.创建代理人。(首先,为您的活动选择一个名称,比如说 JobDone。然后创建一个名为 JobDoneEventHandler 的委托。
- #1.2.基于委托创建事件(使用 event 关键字)。
- #1.3.引发事件。(标准模式要求该方法应该用 protected virtual 标记。此外,该名称必须与事件名称相匹配,并以 On 为前缀)。
步骤 2:创建订户类。
- #2.1 .编写事件处理程序方法。按照约定,事件处理程序方法的名称以 On 开头。
让我们看一下这个程序。
演示 6
using System;
namespace EventEx1
{
//Step1-Create a publisher
class Publisher
{
//Step1.1-Create a delegate.Delegate name should be //"yourEventName"+EventHandler
public delegate void JobDoneEventHandler(object sender, EventArgs args);
//Step1.2-Create the event based on the delgate
public event JobDoneEventHandler JobDone;
public void ProcessOneJob()
{
Console.WriteLine("Publisher:One Job is processed");
//Step1.3-Raise the event
OnJobDone();
}
/*The standard pattern requires that the method should be tagged with protected
virtual. Also the name must match name of the event and it will be prefixed with "On".*/
protected
virtual void OnJobDone()
{
if (JobDone != null)
JobDone(this, EventArgs.Empty);
}
}
//Step2-Create a subscriber
class Subscriber
{
//Handling the event
public void OnJobDoneEventHandler(object sender, EventArgs args)
{
Console.WriteLine("Subscriber is notified");
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***A simple event demo***");
Publisher sender = new Publisher();
Subscriber receiver = new Subscriber();
sender.JobDone += receiver.OnJobDoneEventHandler;
sender.ProcessOneJob();
Console.ReadKey();
}
}
}
输出
学生问:
先生,我们可以为一个事件订阅多个事件处理程序吗?
老师说:是的。在 C# 中,事件被实现为多播委托,因此我们可以将多个事件处理程序关联到一个事件。假设我们有两个订阅者:Subscriber1 和 Subscriber2,他们都希望从发布者那里获得通知。以下代码将正常工作:
Points to Remember
在现实世界的编码中,您必须小心这些订阅(例如,在您的应用中,您只通过事件进行注册,然后在一段时间后,您会观察到内存泄漏这一副作用。因此,您的应用会很慢(可能会崩溃)。如果您没有将取消订阅操作放在适当的位置,垃圾收集器将无法回忆起这些记忆。
传递带有事件参数的数据
如果你再看一下前面的程序,你会发现我们没有用事件参数传递任何特定的东西。
在现实编程中,我们需要传递比 EventArgs 更多的信息。空(或 null)。在这些情况下,我们需要遵循以下步骤:
- 创建 System.EventArgs 的子类。
- 用事件封装预期数据。在下面的例子中,我们使用了一个属性。
- 创建此类的一个实例,并将其与事件一起传递。
为了更好地演示,我稍微修改了前面的程序。
演示 7
using System;
namespace EventEx2
{
//Step-a. Create a subclass of System.EventArgs
public class JobNoEventArgs : EventArgs
{
//Step-b.Encapsulate your intended data with the event. In the below example, we have used a property.
private int jobNo;
public int JobNo
{
get
{
return jobNo;
}
set
{
JobNo = value;
}
}
public JobNoEventArgs(int jobNo)
{
this.jobNo = jobNo;
}
}
//Step1-Create a publisher
class Publisher
{
//Step1.1-Create a delegate.Delegate name should be "yourEventName"+EventHandler
//public delegate void JobDoneEventHandler(object sender, EventArgs args);
public delegate void JobDoneEventHandler(object sender, JobNoEventArgs args);
//Step1.2-Create the event based on the delgate
public event JobDoneEventHandler JobDone;
public void ProcessOneJob()
{
Console.WriteLine("Publisher:One Job is processed");
//Step1.3-Raise the event
OnJobDone();
}
/*The standard pattern requires that the method should be tagged with protected
virtual.
Also the name must match name of the event and it will be prefixed with "On".*/
protected
virtual void OnJobDone()
{
if (JobDone != null)
//Step-c. Lastly create an instance of the event generator class and pass it with the event.
JobDone(this,new JobNoEventArgs(1));
}
}
//Step2-Create a subscriber
class Subscriber
{
//Handling the event
public void OnJobDoneEventHandler(object sender, JobNoEventArgs args)
{
Console.WriteLine("Subscriber is notified.Number of job processed is :{0}",args.JobNo);
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("*** Event example 2:Passing data with events***");
Publisher sender = new Publisher();
Subscriber receiver = new Subscriber();
sender.JobDone += receiver.OnJobDoneEventHandler;
sender.ProcessOneJob();
Console.ReadKey();
}
}
}
输出
分析
现在我们可以看到,通过遵循前面的机制,我们可以在引发事件时获得额外的信息(处理的作业数)。
事件访问器
回到我们关于事件的第一个程序(EventEx1),在这里我们将事件声明为
public event JobDoneEventHandler JobDone;
编译器使用私有委托字段对此进行转换,并提供两个事件访问器:add 和 remove。
以下代码将产生等效的行为:
//public event JobDoneEventHandler JobDone;
#region custom event accessors
private JobDoneEventHandler _JobDone;
public event JobDoneEventHandler JobDone
{
add
{
_JobDone += value;
}
remove
{
_JobDone -= value;
}
}
#endregion
如果你在这个程序中使用这些代码,你需要修改我们的OnJobDone()方法,就像这样:
如果你想证实我们的说法,你可以简单地参考 IL 代码。
从 IL 代码中,我们可以看到 add 和 remove 部分被编译成add_<EventName>和remove_<EventName>。
如果编译器为我们做了所有的事情,那么我们为什么需要为这些细节而烦恼呢?简单的答案是
- 我们自己定义这些访问器来进行额外的控制(例如,我们可能想要进行一些特殊类型的验证,或者我们可能想要记录更多的信息,等等)。)
- 有时我们需要显式地实现一个接口,而这个接口可能包含一个或多个事件。
现在稍微修改一下我们的 EventEx2 程序。在这种情况下,我们使用自定义访问器并记录一些附加信息。让我们看看下面的程序和输出。
演示 8
using System;
namespace EventAccessorsEx1
{
//Step-a. Create a subclass of System.EventArgs
public class JobNoEventArgs : EventArgs
{
//Step-b.Encapsulate your intended data with the event. In the below example, we have used a property.
private int jobNo;
public int JobNo
{
get
{
return jobNo;
}
set
{
JobNo = value;
}
}
public JobNoEventArgs(int jobNo)
{
this.jobNo = jobNo;
}
}
//Step1-Create a publisher
class Publisher
{
//Step1.1-Create a delegate.Delegate name should be "yourEventName"+EventHandler
//public delegate void JobDoneEventHandler(object sender, EventArgs args);
public delegate void JobDoneEventHandler(object sender, JobNoEventArgs args);
//Step1.2-Create the event based on the delgate
//public event JobDoneEventHandler JobDone;
#region custom event accessors
private JobDoneEventHandler _JobDone;
public event JobDoneEventHandler JobDone
{
add
{
Console.WriteLine("Inside add accessor-Entry");
_JobDone += value;
}
remove
{
_JobDone -= value;
Console.WriteLine("Unregister completed-Exit from remove accessor");
}
}
#endregion
public void ProcessOneJob()
{
Console.WriteLine("Publisher:One Job is processed");
//Step1.3-Raise the event
OnJobDone();
}
/*The standard pattern requires that the method should be tagged with protected
virtual.
* Also the name must match name of the event and it will be prefixed with "On".*/
protected
virtual void OnJobDone()
{
if (_JobDone != null)
//Step-c. Lastly create an instance of the event generator class and pass it with the event.
_JobDone(this, new JobNoEventArgs(1));
}
}
//Step2-Create a subscriber
class Subscriber
{
//Handling the event
public void OnJobDoneEventHandler(object sender, JobNoEventArgs args)
{
Console.WriteLine(" Subscriber is notified.Number of job processed is :{0}", args.JobNo);
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("*** Testing custom event accessors***");
Publisher sender = new Publisher();
Subscriber receiver = new Subscriber();
//Subscribe/Register
sender.JobDone += receiver.OnJobDoneEventHandler;
sender.ProcessOneJob();
//Unsubscribe/Unregister
sender.JobDone -= receiver.OnJobDoneEventHandler;
Console.ReadKey();
}
}
}
输出
当您应用自定义事件访问器时,建议您也实现锁定机制;也就是说,我们可以这样写:
一般来说,锁定操作开销很大。为了使我们的例子简单,我在这里忽略了这个建议。
学生问:
先生,什么类型的修饰语被允许用于事件?
老师说:既然我们已经使用了虚拟关键字,你可以猜测覆盖事件是允许的。事件也可以是抽象的、密封的或静态的。
摘要
本章涵盖了
- 委托及其重要性
- 如何在我们的程序中使用委托
- 为什么委托是类型安全的
- 多播代理
- 如何使用委托实现协变和逆变
- 事件以及如何使用它们
- 如何传递带有事件参数的数据
- 事件访问器以及如何使用它们
十一、匿名函数的灵活性
匿名方法和 Lamda 表达式
教师开始讨论:让我们回到我们的代表计划(DelegateEx1)。我在该程序中添加了几行代码来生成相同的输出。为了帮助你理解调用之间的差异,我保留了旧的东西。
注意额外的东西。这些额外的代码块可以帮助你更好地理解匿名方法和 lambda 表达式。C# 2.0 引入了匿名方法,C# 3.0 引入了 lambda 表达式。
顾名思义,没有名字的方法就是 C# 中的匿名方法。匿名方法的主要目标是快速完成一个动作,即使我们写的代码更少。它是一个可以用作委托参数的代码块。
类似地,lambda 表达式是一个没有名字的方法。它用于代替委托实例。编译器可以将这些表达式转换为委托实例或表达式树。(表达式树的讨论超出了本书的范围。)
在下面的演示中,添加了两个额外的代码块:一个用于匿名方法,一个用于 lambda 表达式。每一个都产生相同的输出。
演示 1
using System;
namespace LambdaExpressionEx1
{
public delegate int Mydel(int x, int y);
class Program
{
public static int Sum(int a, int b) { return a + b; }
static void Main(string[] args)
{
Console.WriteLine("*** Exploring Lambda Expression***");
//Without using delgates or lambda expression
int a = 25, b = 37;
Console.WriteLine("\n Calling Sum method without using a delegate:");
Console.WriteLine("Sum of a and b is : {0}", Sum(a, b));
//Using Delegate( Initialization with a named method)
Mydel del = new Mydel(Sum);
Console.WriteLine("\n Using delegate now:");
Console.WriteLine("Calling Sum method with the use of a delegate:");
Console.WriteLine("Sum of a and b is: {0}", del(a, b));
//Using Anonymous method(C# 2.0 onwards)
Mydel del2 = delegate (int x, int y) { return x + y; };
Console.WriteLine("\n Using Anonymous method now:");
Console.WriteLine("Calling Sum method with the use of an anonymous method
:");
Console.WriteLine("Sum of a and b is: {0}", del2(a, b));
//Using Lambda expression(C# 3.0 onwards)
Console.WriteLine("\n Using Lambda Expresson now:");
Mydel sumOfTwoIntegers = (x1, y1) => x1 + y1;
Console.WriteLine("Sum of a and b is: {0}", sumOfTwoIntegers(a, b));
Console.ReadKey();
}
}
}
输出
分析
以下是 lambda 表达式的主要特征:
- 它是一个匿名方法(或未命名的方法),而不是委托实例。
- 它可以包含创建委托或表达式树的表达式或语句(LINQ 查询和表达式树超出了本书的范围)。
请注意,我们有以下委托:
public delegate int Mydel(int x, int y);
我们已经指定并调用了一个 lambda 表达式
(x1, y1) => x1 + y1
因此,您可以看到 lambda 表达式的每个参数对应于委托参数(本例中为 x1 到 x,y1 到 y ),表达式的类型(本例中 x+y 为 int)对应于返回委托的类型。
- Lambda 运算符
=>(读作 goes to)用于 lambda 表达式中。它具有正确的结合性,其优先级与赋值(=)运算符相同。 - 输入参数在 lambda 运算符的左侧指定,表达式或语句在 lambda 运算符的右侧指定。
- 如果我们的 lambda 表达式只有一个参数,我们可以省略括号;例如,我们可以这样写来计算一个数的平方:
x=>x*x
Points to Remember
- C# 2.0 引入了匿名方法,C# 3.0 引入了 lambda 表达式,它们很相似,但是 lambda 表达式更简洁,专家建议如果你的应用是面向。NET Framework 版或更高版本,一般来说你应该更喜欢 lambda 表达式而不是匿名方法。这两个特性在 C# 中被称为匿名函数。
- 您应该避免在匿名方法体中使用不安全的代码和跳转语句,如 break、goto 和 continue。
学生问:
那么我们应该总是尝试使用匿名方法,因为它更快,代码更小。这是正确的吗?
老师说:不。看看与匿名方法相关的限制。此外,如果您需要多次编写类似的功能,您必须避免匿名方法。
函数、动作和谓词委托
作者注:现在我们将快速介绍三个重要的泛型委托。我把这个主题放在这里是因为它们很容易与 lambda 表达式和匿名方法联系起来。我们将很快讨论泛型。所以,如果你已经对泛型编程有了一个基本的概念,你可以继续;否则,一旦你对他们有了更多的了解,请回来。
Func 代表
Func 委托有多种形式。它们可以接受 0 到 16 个输入参数,但总是有一个返回类型。考虑以下方法:
private static string ShowStudent(string name, int rollNo)
{
return string.Format("Student Name is :{0} and Roll Number is :{1}", name, rollNo);
}
要使用委托调用此方法,我们需要遵循以下步骤:
第一步。像这样定义委托:
public delegate string Mydel(string n, int r);
第二步。像这样用委托附加方法:
Mydel myDelOb = new Mydel (ShowStudent);
或者简而言之,
Mydel myDelOb = ShowStudent;
第三步。现在,您可以像这样调用该方法:
myDelOb.Invoke ("Jon", 5);
或者仅仅是
myDelOb ("Jon", 5);
但是在这种情况下,我们可以使用现成的/内置的委托函数使代码更简单、更短,如下所示:
Func<string, int, string> student = new Func<string, int, string>(ShowStudent);
Console.WriteLine(ShowStudent("Amit", 1));
因此,您可以预测这个 Func 委托很好地考虑了两种输入类型——string 和 int——以及返回类型 string。在 Visual Studio 中,如果您将光标移动到此处,可以看到最后一个参数被视为函数的返回类型,其他参数被视为输入类型。
学生问:
先生,我们有不同种类的方法,可以接受不同数量的输入参数。与前面的方法不同,我们如何在考虑多于或少于两个输入参数的函数中使用 Func?
老师说:Func 代表可以考虑 0 到 16 个输入参数。所以,我们可以使用这些形式中的任何一种:
Func<T, TResult>
Func<T1, T2, TResult>
Func<T1, T2, T3, TResult>
.....
Func<T1, T2, T3..., T15, T16, TResult>
动作代表
动作委托可以接受 1 到 16 个输入参数,但没有返回类型。因此,假设我们有一个SumOfThreeNumbers方法,它有三个输入参数,其返回类型是 void,如下所示:
private static void SumOfThreeNumbers(int i1, int i2, int i3)
{
int sum = i1 + i2 + i3;
Console.WriteLine("Sum of {0},{1} and {2} is: {3}", i1, i2, i3, sum);
}
我们可以使用动作委托来获得三个整数的和,如下所示:
Action<int, int, int> sum = new Action<int, int, int>(SumOfThreeNumbers);
sum(10, 3, 7);
谓词委托
谓词委托用于评估某些东西。例如,一个方法定义了一些标准,我们需要检查一个对象是否满足这些标准。考虑以下方法:
private static bool GreaterThan100(int myInt)
{
return myInt > 100 ? true : false;
}
我们可以看到这个方法评估一个输入是否大于 100。我们可以使用谓词委托来执行相同的测试,如下所示:
Predicate<int> isGreater = new Predicate<int>(GreaterThan100);
Console.WriteLine("125 is greater than 100? {0}", isGreater(125));
Console.WriteLine("60 is greater than 100? {0}", isGreater(60));
下面的程序用一个简单的程序演示了所有这些概念。
演示 2
using System;
namespace Test1_FuncVsActionVsPredicate
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Testing Func vs Action vs Predicate***");
//Func
Console.WriteLine("<---Using Func--->");
Func<string, int, string> student = new Func<string, int, string>(ShowStudent);
Console.WriteLine(ShowStudent("Amit", 1));
Console.WriteLine(ShowStudent("Sumit", 2));
//Action
Console.WriteLine("<---Using Action--->");
Action<int, int, int> sum = new Action<int, int, int>(SumOfThreeNumbers);
sum(10, 3, 7);
sum(5, 10, 15);
//Predicate
Console.WriteLine("<---Using Predicate--->");
Predicate<int> isGreater = new Predicate<int>(GreaterThan100);
Console.WriteLine("125 is greater than 100? {0}", isGreater(125));
Console.WriteLine("60 is greater than 100? {0}", isGreater(60));
Console.ReadKey();
}
private static string ShowStudent(string name, int rollNo)
{
return string.Format("Student Name is :{0} and Roll Number is :{1}", name, rollNo);
}
private static void SumOfThreeNumbers(int i1, int i2, int i3)
{
int sum = i1 + i2 + i3;
Console.WriteLine("Sum of {0},{1} and {2} is: {3}", i1, i2, i3, sum);
}
private static bool GreaterThan100(int myInt)
{
return myInt > 100 ? true : false;
}
}
}
输出
摘要
本章讨论了以下内容:
- 匿名方法
- λ表达式
- 函数、动作和谓词委托
- 如何在 C# 应用中有效地使用这些概念
十二、泛型
泛型程序和非泛型程序的比较
教师开始讨论:泛型是 C# 的关键概念之一。它们出现在 C# 2.0 中,从那以后,它们扩展了新的特性。
为了理解泛型的强大,我们将从一个非泛型程序开始,然后编写一个泛型程序。稍后,我们将进行比较分析,然后我们将尝试发现泛型编程的优势。考虑下面的程序和输出。
演示 1:非泛型程序
using System;
namespace NonGenericEx
{
class NonGenericEx
{
public int ShowInteger(int i)
{
return i;
}
public string ShowString(string s1)
{
return s1;
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***A non-generic program example***");
NonGenericEx nonGenericOb = new NonGenericEx();
Console.WriteLine("ShowInteger returns :{0}", nonGenericOb.ShowInteger(25));
Console.WriteLine("ShowString returns :{0}", nonGenericOb.ShowString("Non Generic method called"));
Console.ReadKey();
}
}
}
输出
现在让我们试着介绍一个泛型程序。在我们开始之前,这些是关键点:
- 尖括号
<>用于创建通用类。 - 我们可以定义一个类,用占位符来表示它的方法、字段、参数等的类型。以及在泛型程序中;这些占位符将被特定的类型替换。
- 微软声明:“泛型类和方法结合了可重用性、类型安全性和效率,这是非泛型类和方法所不能做到的。泛型最常用于集合和对集合进行操作的方法。的 2.0 版。NET Framework 类库提供了一个新的命名空间 System。包含几个新的基于泛型的集合类。建议所有面向。NET Framework 2.0 和更高版本使用新的泛型集合类,而不是旧的非泛型集合类,如 ArrayList。(见
https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/generics/introduction-to-generics)。)
让我们从下面的程序开始。
演示 2:泛型程序
using System;
namespace GenericProgrammingEx1
{
class MyGenericClass<T>
{
public T Show(T value)
{
return value;
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Introduction to Generics***");
MyGenericClass<int> myGenericClassIntOb = new MyGenericClass<int>();
Console.WriteLine("Show returns :{0}", myGenericClassIntOb.Show(100));
MyGenericClass<string> myGenericClassStringOb = new MyGenericClass<string>();
Console.WriteLine("Show returns :{0}", myGenericClassStringOb.Show("Generic method called"));
MyGenericClass<double> myGenericClassDoubleOb = new MyGenericClass<double>();
Console.WriteLine("Show returns :{0}", myGenericClassDoubleOb.Show(100.5));
Console.ReadKey();
}
}
}
输出
分析
我们现在可以做一个演示 1 和演示 2 的对比分析。我们看到了以下特征:
-
对于非泛型方法,我们需要指定像
ShowInteger()和ShowString()这样的方法来处理特定的数据类型。另一方面,对于通用版本,Show()就足够了。一般来说,通用版本的代码行更少(即代码更小)。 -
在演示 1 的
Main()中,我们在第二行遇到了一个编译时错误,如下所示:Console.WriteLine("ShowDouble returns :{0}", nonGenericOb.ShowDouble(25.5));//error
原因是:在这个例子中,我们没有定义一个'ShowDouble(double d)'方法。因此,为了避免这个错误,我们需要在类中包含一个额外的方法 NonGenericEx,如下所示:
进一步分析
我们的NonGenericEx类的代码大小随着这一增加而增加。我们需要增加代码大小,因为我们现在试图处理不同的数据类型“double”
现在来看演示 2,我们在不修改 MyGenericClass 的情况下获得了 double 数据类型。因此,我们可以得出结论,通用版本更加灵活。
注意
一般来说,泛型编程比非泛型编程更灵活,并且需要更少的代码行。
考虑下面的程序。
演示 3
using System;
using System.Collections;
namespace GenericEx2
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Use Generics to avoid runtime error***");
ArrayList myList = new ArrayList();
myList.Add(10);
myList.Add(20);
myList.Add("Invalid");//No compile time error but will cause
//runtime error
foreach (int myInt in myList)
{
Console.WriteLine((int)myInt); //downcasting
}
Console.ReadKey();
}
}
}
输出
该程序不会引发任何编译时错误。
但是在运行时,我们会遇到这个错误:
这是因为第三个元素(即我们的 ArrayList 中的 myList [2])不是整数(它是一个字符串)。在编译时,我们没有遇到任何问题,因为它是作为对象存储的。请注意取自 visual studio 的快照:
分析
在这种类型的编程中,由于装箱和向下转换,我们可能会面临性能开销。
现在考虑下面的程序。
演示 4
using System;
using System.Collections.Generic;
namespace GenericEx3
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Use Generics to avoid runtime error***");
List<int> myGenericList = new List<int>();
myGenericList.Add(10);
myGenericList.Add(20);
myGenericList.Add("Invalid");// compile time error
foreach (int myInt in myGenericList)
{
Console.WriteLine((int)myInt);//downcasting
}
Console.ReadKey();
}
}
}
输出
在这种情况下,我们不能在 myGenericList 中添加字符串,因为它只用于保存整数。该错误在编译时被捕获;我们不需要等到运行时才得到这个错误。
分析
通过比较演示 3 和演示 4,我们可以说
- 为了避免运行时错误,我们应该更喜欢泛型版本的代码,而不是非泛型版本。
- 如果我们使用泛型编程,我们可以避免装箱/拆箱带来的损失。
- 我们可以使用
List<string> myGenericList2 = new List<string>();来创建一个包含字符串的列表。List 版本比非泛型版本 ArrayList 更加灵活和可用。
演示 5:自引用泛型类型练习
让我们假设在您的雇员类中有雇员 id 和部门名称。写一个简单的程序来判断两个雇员是否相同。但是对您的约束是,您的类应该从定义该比较方法规范的通用接口派生。
下面的演示可以被视为需求的一个示例实现。
using System;
namespace GenericEx4
{
interface ISameEmployee<T>
{
string CheckForIdenticalEmployee(T obj);
}
class Employee : ISameEmployee<Employee>
{
string deptName;
int employeeID;
public Employee(string deptName, int employeeId)
{
this.deptName = deptName;
this.employeeID = employeeId;
}
public string CheckForIdenticalEmployee(Employee obj)
{
if (obj == null)
{
return "Cannot Compare with a Null Object";
}
else
{
if (this.deptName == obj.deptName && this.employeeID == obj.employeeID)
{
return "Same Employee";
}
else
{
return "Different Employee";
}
}
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("**Suppose, we have an Employee class that contains deptName and employeeID***");
Console.WriteLine("***We need to check whether 2 employee objects are same or not.***");
Console.WriteLine();
Employee emp1 = new Employee("Maths", 1);
Employee emp2 = new Employee("Maths", 2);
Employee emp3 = new Employee("Comp. Sc.", 1);
Employee emp4 = new Employee("Maths", 2);
Employee emp5=null;
Console.WriteLine("Comparing Emp1 and Emp3 :{0}", emp1.CheckForIdenticalEmployee(emp3));
Console.WriteLine("Comparing Emp2 and Emp4 :{0}", emp2.CheckForIdenticalEmployee(emp4));
Console.WriteLine("Comparing Emp3 and Emp5 :{0}", emp3.CheckForIdenticalEmployee(emp5));
Console.ReadKey();
}
}
}
输出
分析
这是一个类型将自己命名为具体类型的示例(换句话说,这是一个自引用泛型类型的示例)。
一个特殊的关键字默认值
我们熟悉 switch 语句中 default 关键字的用法,其中 default 用于指代默认情况。在泛型的上下文中,它有特殊的含义。这里我们使用 default 用它们的默认值初始化泛型类型(例如,引用类型的默认值是 null,值类型的默认值是按位零)。
考虑下面的例子。
演示 6
using System;
namespace CaseStudyWithDefault
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Case study- default keyword***");
Console.WriteLine("default(int) is {0}", default(int));//0
bool b1 = (default(int) == null);//False
Console.WriteLine("default(int) is null ?Answer: {0}", b1);
Console.WriteLine("default(string) is {0}", default(string));//null
bool b2 = (default(string) == null);//True
Console.WriteLine("default(string) is null ? Answer:{0}", b2);
Console.ReadKey();
}
}
}
输出
分析
我们必须记住,int是值类型,string是引用类型。因此,您现在可以使用前面的程序和输出来检查它们的默认值。
演示 7:分配
让我们假设你有一个仓库,你可以存储多达三个对象。要存储这些对象,可以使用数组。编写一个泛型程序,通过它你可以在库中存储/检索不同的类型。使用 default 关键字的概念用数组各自的类型初始化数组。
下面的演示可以被视为需求的一个示例实现。
using System;
namespace Assignment
{
public class MyStoreHouse<T>
{
T[] myStore = new T[3];
int position = 0;
public MyStoreHouse()
{
for (int i = 0; i < myStore.Length; i++)
{
myStore[i] = default(T);
}
}
public void AddToStore(T value)
{
if (position < myStore.Length)
{
myStore[position] = value;
position++;
}
else
{
Console.WriteLine("Store is full already");
}
}
public void RetrieveFromStore()
{
foreach (T t in myStore)
{
Console.WriteLine(t);
}
//Or Use this block
//for (int i = 0; i < myStore.Length; i++)
//{
// Console.WriteLine(myStore[i]);
//}
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Use case-default keyword
in generic programming:***");
Console.WriteLine("***\nCreating an Integer store:***");
MyStoreHouse<int> intStore = new MyStoreHouse<int>();
intStore.AddToStore(45);
intStore.AddToStore(75);
Console.WriteLine("***Integer store at this moment:***");
intStore.RetrieveFromStore();
Console.WriteLine("***\nCreating an String store:***");
MyStoreHouse<string> strStore = new MyStoreHouse<string>();
strStore.AddToStore("abc");
strStore.AddToStore("def");
strStore.AddToStore("ghi");
strStore.AddToStore("jkl");//Store is full already
Console.WriteLine("***String store at this moment:***");
strStore.RetrieveFromStore();
Console.ReadKey();
}
}
}
输出
通用约束
考虑下面的程序和输出,然后进行分析。
演示 8
using System;
using System.Collections.Generic;
namespace GenericConstraintEx
{
interface IEmployee
{
string Position();
}
class Employee : IEmployee
{
public string Name;
public int yearOfExp;
public Employee(string name, int years)
{
this.Name = name;
this.yearOfExp = years;
}
public string Position()
{
if (yearOfExp < 5)
{
return " A Junior Employee";
}
else
{
return " A Senior Employee";
}
}
}
class EmployeeStoreHouse<Employee> where Employee : IEmployee
//class EmployeeStoreHouse<Employee>//error
{
private List<Employee> MyStore = new List<Employee>();
public void AddToStore(Employee element)
{
MyStore.Add(element);
}
public void DisplaySore()
{
Console.WriteLine("The store contains:");
foreach (Employee e in MyStore)
{
Console.WriteLine(e.Position());
}
}
}
namespace Generic.Constraint_1
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Example of Generic Constraints***");
//Employees
Employee e1 = new Employee("Amit", 2);
Employee e2 = new Employee("Bob", 5);
Employee e3 = new Employee("Jon", 7);
//Employee StoreHouse
EmployeeStoreHouse<Employee> myEmployeeStore = new EmployeeStoreHouse<Employee>();
myEmployeeStore.AddToStore(e1);
myEmployeeStore.AddToStore(e2);
myEmployeeStore.AddToStore(e3);
//Display the Employee Positions in Store
myEmployeeStore.DisplaySore();
Console.ReadKey();
}
}
}
}
输出
注意
在这个例子中,我们检查了如何在应用中设置约束。如果不使用“where Employee:IEmployee”语句,我们会遇到以下问题:
上下文关键字'where'帮助我们在应用中设置约束。一般来说,我们可以有以下约束:
where T:struct表示类型 T 必须是值类型。(请记住,struct 是一种值类型。)where T: class表示类型 T 必须是引用类型。(记住类是一个引用类型。)where T: IMyInter表示类型 T 必须实现 IMyInter 接口。where T: new()意味着类型 T 必须有一个默认的(无参数的)构造函数。(如果与其他约束一起使用,将其放在最后一个位置。)where T: S意味着类型 T 必须从另一个泛型类型 s 派生。它有时被称为裸类型约束。
恶作剧
学生问:
我们能否编写一个更通用的 EmployeeStoreHouse 形式?
回答
老师说:是的,我们能。考虑下面的代码。
class EmployeeStoreHouse<T> where T : IEmployee
{
private List<T> MyStore = new List<T>();
public void AddToStore(T element)
{
MyStore.Add(element);
}
public void DisplaySore()
{
foreach (T e in MyStore)
{
Console.WriteLine(e.Position());
}
}
}
协方差和逆变
在第十章关于委托的讨论中,你了解到委托中的协变和逆变支持是从 C# 2.0 开始的。从 C# 4.0 开始,这些概念可以应用于泛型类型参数、泛型接口和泛型委托。第十章也探讨了非泛型委托的概念。
在本章中,我们将通过更多的案例继续探讨这些概念。
在继续之前,请记住以下几点:
- 协方差和逆变处理带有参数和返回类型的类型转换。
- 协方差和逆变已用于我们对不同类型的对象/数组等的编码。
- 。NET 4 支持泛型委托和泛型接口。(在早期版本中,我们会遇到泛型委托或泛型接口的编译错误)。
- 逆变通常被定义为调整或修改。当我们试图在编码世界中实现这些概念时,我们也试图接受以下真理(或类似的真理):
- 所有的足球运动员都是运动员,但反过来就不一样了(因为有很多运动员打高尔夫、篮球、曲棍球等。)同样,我们可以说所有的公交车都是交通工具,但反过来就不成立。
- 在编程术语中,所有的派生类都是基类,但反之则不然。例如,假设我们有一个名为 Rectangle 的类,它是从一个名为 Shape 的类派生而来的。那么我们可以说所有的矩形都是形状,但反过来就不成立了。
- 按照 MSDN 的说法,协方差和逆变的概念处理数组、委托和泛型类型的隐式引用转换。协方差保持赋值兼容性,逆变则相反。
学生问:
“任务兼容性”是什么意思?
老师说:这意味着你可以把一个更具体的类型分配给一个兼容的不太具体的类型。例如,整数变量的值可以存储在对象变量中,如下所示:
int i = 25;
object o = i;//ok: Assignment Compatible
让我们试着从数学的角度来理解协变、逆变和不变性的含义。
假设我们只考虑整数的定义域。
情况 1:我们定义我们的函数,f (x) = x + 2 对于所有的 x。
如果 x ≤ y,那么我们也可以说 f (x) ≤ f (y)对于所有的 x,投影(函数 f)是保持大小方向的。
情况 2:我们定义我们的 f(x)=–x(对于所有的 x 都属于整数)。
现在我们可以看到 10 ≤ 20 但是 f (10) ≥ f (20)(因为 f(10)=–10,f(20)=–20 和–10 >–20)。所以,我们的大小方向颠倒了。
情况 3:我们定义我们的函数,f (x) = x*x。
现在,我们可以看到–1≤0 且 f(–1)> f(0),但 1 < 2 且 f (1) < f (2)。所以投影(函数 f)既不总是保持大小的方向,也不反转大小的方向。
在情况 1 中,函数 f 是协变的;在情况 2 中,函数 f 是逆变的;在情况 3 中,函数 f 是不变的。
Points to Remember
你可以随时参考微软在 https://docs.microsoft.com/en-us/dotnet/standard/generics/covariance -and-contravariance的简单定义。
- 协方差我们可以使用比最初指定的更派生的类型。
- 我们可以使用一个比最初指定的更通用(更少派生)的类型。
- 不变性我们只允许使用最初指定的类型。
协方差和逆变统称为方差。
从。NET Framework 4 中,在 C# 中有关键字将接口和委托的泛型类型参数标记为协变或逆变。协变接口和委托用 out 关键字标记(表示值出来)。逆变接口和委托与 in 关键字相关联(指示值进入)。
让我们看一下我们的 C# 例子。记住 IEnumerable 在 T 上是协变的,Action 在 T 上是逆变的,让我们检查一下在 Visual Studio 中 IEnumerable 接口的定义。
作者注:注意,我们可以看到“out”这个词与 IEnumerable 的定义相关联。所以,我们可以将 IEnumerable 赋给 IEnumerable 。这就是为什么我们可以将 IEnumerable 赋给 IEnumerable 。
现在检查 Visual Studio 中 Action 委托的定义。我们将看到:
或者,检查 Visual Studio 中的定义 IComparer 接口。我们将看到:
Note
注意,我们可以看到 in 中的单词与 Acion 的定义相关联。因此,我们可以将动作分配给动作。
底线是:由于 IEnumerable 在 T 上是协变的,我们可以从 IEnumerable 转换成 IEnumerable 。从这些情况中得出值(协方差)。
另一方面,由于动作在 T 上是逆变的,我们也可以将动作转换成动作。值进入这些对象(逆变)。
为了测试这两种风格,我们将讨论泛型接口的协变性和泛型委托的逆变性。我建议您尝试实现剩下的两种情况:用泛型委托实现协方差,用泛型接口实现逆变。
演示 9:通用接口的协变性
using System;
using System.Collections.Generic;
namespace CovarianceWithGenericInterfaceEx
{
class Parent
{
public virtual void ShowMe()
{
Console.WriteLine(" I am from Parent, my hash code is :" + GetHashCode());
}
}
class Child : Parent
{
public override void ShowMe()
{
Console.WriteLine(" I am from Child, my hash code is:" + GetHashCode());
}
}
class Program
{
static void Main(string[] args)
{
//Covariance Example
Console.WriteLine("***Covariance with Generic Interface Example***\n");
Console.WriteLine("***IEnumerable<T> is covariant");
//Some Parent objects
Parent pob1 = new Parent();
Parent pob2 = new Parent();
//Some Child objects
Child cob1 = new Child();
Child cob2 = new Child();
//Creating a child List
List<Child> childList = new List<Child>();
childList.Add(cob1);
childList.Add(cob2);
IEnumerable<Child> childEnumerable = childList;
/* An object which was instantiated with a more derived type argument (Child) is assigned to an object instantiated with a less derived type argument(Parent). Assignment compatibility is preserved here. */
IEnumerable<Parent> parentEnumerable = childEnumerable;
foreach (Parent p in parentEnumerable)
{
p.ShowMe();
}
Console.ReadKey();
}
}
}
输出
分析
仔细阅读程序中包含的注释,以便更好地理解。
演示 10:与泛型委托的对比
using System;
namespace ContravarianceWithGenericDelegatesEx
{
//A generic delegate
delegate void aDelegateMethod<in T>(T t);
class Vehicle
{
public virtual void ShowMe()
{
Console.WriteLine(" Vehicle.ShowMe()");
}
}
class Bus: Vehicle
{
public override void ShowMe()
{
Console.WriteLine(" Bus.ShowMe()");
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Contra-variance with Generic Delegates
example ***");
Vehicle obVehicle = new Vehicle();
Bus obBus = new Bus();
aDelegateMethod<Vehicle> delVehicle = ShowVehicleType;
delVehicle(obVehicle);
//Contravariance
with Delegate
//Using less derived type to more derived type
aDelegateMethod<Bus> delChild = ShowVehicleType;
delChild(obBus);
Console.ReadKey();
}
private static void ShowVehicleType(Vehicle p)
{
p.ShowMe();
}
}
}
输出
分析
像前一个例子一样,浏览这个程序中的注释以获得更好的理解。
学生问:
在前面的程序中,您使用了带有泛型委托的静态方法(ShowVehicleType (…))。你能在非静态方法中使用同样的概念吗?
老师说:显然,你可以。
摘要
本章讨论了以下内容:
- C# 中的泛型
- 为什么泛型很重要
- 泛型编程相对于非泛型编程的优势
- 泛型上下文中的关键字 default
- 如何在泛型编程中施加约束
- 通用接口的协变
- 与泛型委托相反