高级-C--进阶手册-全-
高级 C# 进阶手册(全)
一、委托
在包括 C# 在内的许多编程语言中,委托的概念是一个非常强大的功能。我相信,讨论 C# 高级编程离不开委托。在这一章中,你将学习委托以及为什么委托是必不可少的。
让我们回忆一下类和对象的基本原理。为了创建一个对象——比方说,obA
从一个类 A 中,你可以写一些像下面这样的东西。
A obA=new A();
在这里,对象引用obA
指向 a 的一个对象,与此类似,委托也是引用类型,但关键区别在于它们指向方法。简单地说,委托是一个知道如何调用方法的对象。委托是从System.Delegate
类派生的。
让我们从另一个角度来看。你知道变量是什么,它是如何表现的。你已经看到可以放不同的布尔值(true/false
)、字符串(或、字)、数字(整数、双精度等。)在各自类型的变量中。但是当你使用委托的时候,你可以把一个方法赋给一个变量并传递它。
简而言之,通过使用委托,您可以像对待对象一样对待您的方法。因此,您可以将委托存储在变量中,将其作为方法参数传递,并从方法中返回。
委托的使用有助于促进类型安全。(从广义上讲,术语类型安全只是告诉你,如果一种类型与另一种类型不兼容,你就不能将它们赋值给另一种类型。类型安全检查可以在编译时和运行时出现)。这就是为什么委托经常被称为类型安全函数指针的原因。
在演示 1 中,一个名为 Sum 的方法接受两个 integer (int)参数并返回一个整数,如下所示。
public static int Sum(int a, int b)
{
return a + b;
}
在这种情况下,您可以声明一个委托来指向 Sum 方法,如下所示。
DelegateWithTwoIntParameterReturnInt delOb = new DelegateWithTwoIntParameterReturnInt (Sum);
但在此之前,您需要定义DelegateWithTwoIntParameterReturnInt
委托,它必须具有相同的签名,如下所示。
delegate int DelegateWithTwoIntParameterReturnInt(int x, int y);
Sum 方法和DelegateWithTwoIntParameterReturnInt
委托的返回类型、参数以及它们对应的顺序是相同的。为了可读性更好,我为我的委托选择了一个长名字。您可以随时选择自己的委托姓名。
首先要明白的重要一点是,一旦有了DelegateWithTwoIntParameterReturnInt
,就可以用它来跟踪任何一个以两个整数为输入参数,返回一个整数的方法;例如,计算两个整数的和、两个整数的差、两个整数的乘法、两个整数的除法等等。
Points to Remember
-
委托实例包含方法的细节,而不是数据。
-
对于匹配委托签名的方法,可以使用委托。例如,顾名思义,
DelegateWithTwoIntParameterReturnInt
兼容任何接受两个 int 参数并返回一个 int 的方法。 -
当您使用委托来调用方法时,在较高的层次上,整个过程可以分为两个部分。在第一部分,您(调用方)调用委托,在第二部分,委托调用您的目标方法。这种机制将调用方从目标方法中分离出来。
定义
委托是从System.Delegate
派生的引用类型,它的实例用于调用具有匹配签名和返回类型的方法。在这一章的后面,你将了解到差异,你将发现在这个上下文中,单词兼容比单词匹配更合适。我在努力让事情尽可能简单。
delegate 一词的字典含义是“委托或代理人”C# 编程中的委托表示具有匹配签名的方法。这是委托声明的一般形式。
<modifier> delegate <return type> (parameter list);
以下是委托声明的示例。
delegate void DelegateWithNoParameter();
public delegate int MyDelegateWithOneIntParameter(int i);
public delegate double MakeTotal(double firstNo, double secondNo);
delegate int DelegateWithTwoIntParameterReturnInt(int x, int y);
您可能会注意到,这些方法类似于没有主体的方法。但是,当编译器看到关键字delegate
时,它知道您使用的是从System.Delegate
派生的类型。
从委托开始,在下面的例子中,我向您展示了两种情况。第一种情况对你来说很熟悉。您只需调用一个方法,而无需使用委托。在第二种情况下,您使用委托来调用方法。
演示 1
在本演示中,请注意以下代码段。
// Creating a delegate instance
// DelegateWithTwoIntParameterReturnInt delOb = new DelegateWithTwoIntParameterReturnInt(Sum);
// Or, simply write as follows:
DelegateWithTwoIntParameterReturnInt delOb = Sum;
我保留了注释,说明我在创建委托实例时使用了简写形式。你可以使用任何一个。
当您使用delOb(25,75)
而不是delOb.Invoke(25,75)
时,您也可以使代码长度更短。这也是为什么我还保留了下面的评论。
// delOb(25,75) is shorthand for delOb.Invoke(25,75)
当您使用缩写形式时(即,您将方法名分配给委托实例,而不使用 new 运算符或显式调用委托的构造函数),您使用的是一种称为方法组转换的功能。从 2.0 版开始就允许这种形式。
现在让我们来看看完整的示例以及相应的输出和分析。
using System;
namespace DelegateExample1
{
delegate int DelegateWithTwoIntParameterReturnInt(int x, int y);
class Program
{
public static int Sum(int a, int b)
{
return a + b;
}
static void Main(string[] args)
{
Console.WriteLine("***A simple delegate demo.***");
Console.WriteLine("\n Calling Sum(..) method without using a delegate:");
Console.WriteLine("Sum of 10 and 20 is : {0}", Sum(10, 20));
//Creating a delegate instance
//DelegateWithTwoIntParameterReturnInt delOb = new DelegateWithTwoIntParameterReturnInt(Sum);
//Or,simply write as follows:
DelegateWithTwoIntParameterReturnInt delOb = Sum;
Console.WriteLine("\nCalling Sum(..) method using a delegate.");
int total = delOb(10, 20);
Console.WriteLine("Sum of 10 and 20 is: {0}", total);
/* Alternative way to calculate the aggregate of the numbers.*/
//delOb(25,75) is shorthand for delOb.Invoke(25,75)
Console.WriteLine("\nUsing Invoke() method on delegate instance, calculating sum of 25 and 75.");
total = delOb.Invoke(25,75);
Console.WriteLine("Sum of 25 and 75 is: {0}", total);
Console.ReadKey();
}
}
}
输出
以下是运行该程序的输出。
***A simple delegate demo.***
Calling Sum(..) method without using a delegate:
Sum of 10 and 20 is : 30
Calling Sum(..) method using a delegate.
Sum of 10 and 20 is: 30
Using Invoke() method on delegate instance, calculating sum of 25 and 75.
Sum of 25 and 75 is: 100
分析
让我们仔细看看代码。为了更容易理解,图 1-1 展示了 IL 代码的部分截图。 1
图 1-1
委托示例 1 的 IL 代码的部分屏幕截图
注意,当你创建一个委托时,C# 编译器把它变成一个从MulticastDelegate
扩展的类。让我们再深入一层。如果你看到了MulticastDelegate
的实现,你会发现它是从System.Delegate
类派生出来的。供大家参考,图 1-2 呈现了来自 Visual Studio 2019 的部分截图。
图 1-2
Visual Studio IDE 2019 中 MulticastDelegate 类的部分屏幕截图
图 1-3 显示了演示 1 中Main
方法的 IL 代码。
图 1-3
先前演示中的Main
方法的 IL 代码的部分屏幕截图
在图 1-3 中,箭头指向的那一行表示delOb(10,20)
是delOb.Invoke(10,20).
的语法快捷方式
Points to Remember
-
那个。NET 框架定义了委托和组播委托类。当你创建一个委托时,C# 编译器生成一个从 MulticastDelegate 派生的类,后者从 Delegate 类派生。
-
Only the C# compiler can create a class that derives from the Delegate class or the
MulticastDelegate
class, but you cannot do the same. In other words, these delegate types are implicitly sealed. You will get a compile-time error if you write something like the following.class MyClass : Delegate { }
或者,
class MyClass : MulticastDelegate { }
-
在演示中,您看到了
delOb(10,20)
是delOb.Invoke(10,20)
的语法快捷方式。因此,在实际编程中,最好在调用操作之前进行空检查。 -
委托方法也被称为可调用实体。
问答环节
1.1 在演示 1 中,您在 Program 类之外定义了委托。这是强制性的吗?
不。因为它是一个类类型,你可以在类内部,类外部,或者在名字空间的开始定义它。
1.2 你说只有 C# 编译器可以创建从委托类或者 MulticastDelegate
类 派生的类,但是你不能这么做。你的意思是这些委托类型是隐式密封的吗?
是的。
1.3 委托的使用仅限于静态方法吗?
您可以使用委托引用静态和非静态方法。委托不关心调用该方法的对象类型。所以,这位委托
delegate int MyDelegate(int aNumber);
它可以引用实例方法。
public int Factorial(int i)
{
// method body
}
也可以参考下面的静态方法。
public static int MyStaticMethod(int a)
{
// method body
}
但是当您在委托的上下文中使用静态方法或非静态方法时,有一些重要的区别。您将很快看到关于这一点的案例研究。
比较静态方法和实例方法
我已经说过,您可以将静态方法和实例方法分配给委托对象。为了演示这一点,我修改了演示 1。我添加了一个新类OutsideProgram
,并在其中放置了一个名为CalculateSum
的实例方法。我已经将静态方法Sum
和实例方法CalculateSum
分配给委托实例delOb
,并分析了每种情况。
在每种情况下,您都会看到以下代码行。
Console.WriteLine("delOb.Target={0}", delOb.Target);
Console.WriteLine("delOb.Target==null? {0}", delOb.Target == null);
Console.WriteLine("delOb.Method={0}",delOb.Method);
这些代码行的输出表明,当您将非静态方法分配给委托对象时,该对象不仅维护对该方法的引用,还维护对该方法所属的实例的引用。
委托类中的目标属性可用于验证这一点。这就是为什么在这个上下文中比较静态方法和实例方法时,您可能会注意到前两行的输出不同。为了便于参考,我向您展示了 Visual Studio 中对目标属性的描述,如下所示。
// Summary:
// Gets the class instance on which the current delegate invokes //the instance method.
//
// Returns:
//The object on which the current delegate invokes the instance //method, if the delegate represents an instance method; null //if the delegate represents a static method.
[NullableAttribute(2)]
public object? Target { get; }
这个来自 Visual Studio 的描述还说,如果你给委托对象delOb
分配一个静态方法,那么delOb.Target
将包含null
。
演示 2
using System;
namespace DelegateExample2
{
delegate int DelegateWithTwoIntParameterReturnInt(int x, int y);
class Program
{
public static int Sum(int a, int b)
{
return a + b;
}
static void Main(string[] args)
{
Console.WriteLine("***Comparing the behavior of a static method and instance method when assign them to a delegate instance.***");
Console.WriteLine("Assigning a static method to a delegate object.");
//Assigning a static method to a delegate object.
DelegateWithTwoIntParameterReturnInt delOb = Sum;
Console.WriteLine("Calling Sum(..) method of Program Class using a delegate.");
int total = delOb(10, 20);
Console.WriteLine("Sum of 10 and 20 is: {0}", total);
Console.WriteLine("delOb.Target={0}", delOb.Target);
Console.WriteLine("delOb.Target==null? {0}", delOb.Target == null);//True
Console.WriteLine("delOb.Method={0}", delOb.Method);
OutSideProgram outsideOb = new OutSideProgram();
Console.WriteLine("\nAssigning an instance method to a delegate object.");
//Assigning an instance method to a delegate object.
delOb = outsideOb.CalculateSum;
Console.WriteLine("Calling CalculateSum(..) method of OutsideProgram class using a delegate.");
total = delOb(50, 70);
Console.WriteLine("Sum of 50 and 70 is: {0}", total);
Console.WriteLine("delOb.Target={0}", delOb.Target);//delOb.Target=DelegateEx1.OutSideProgramClass
Console.WriteLine("delOb.Target==null? {0}", delOb.Target == null);//False
Console.WriteLine("delOb.Method={0}", delOb.Method);
Console.ReadKey();
}
}
class OutSideProgram
{
public int CalculateSum(int x, int y)
{
return x + y;
}
}
}
输出
这是输出。我加粗了几行以引起你的注意。
***Comparing the behavior of a static method and instance method when assign them to a delegate instance.***
Assigning a static method to a delegate object.
Calling Sum(..) method of Program Class using a delegate.
Sum of 10 and 20 is: 30
delOb.Target=
delOb.Target==null? True
delOb.Method=Int32 Sum(Int32, Int32)
Assigning an instance method to a delegate object.
Calling CalculateSum(..) method of OutsideProgram class using a delegate.
Sum of 50 and 70 is: 120
delOb.Target=DelegateExample2.OutSideProgram
delOb.Target==null? False
delOb.Method=Int32 CalculateSum(Int32, Int32)
使用多播代理
通过使用委托实例,可以引用多个目标方法。您可以通过使用 += 操作符来实现这一点。当一个委托用于封装一个匹配签名的多个方法时,它就是一个组播委托。这些委托是System.MulticastDelegate
的子类型,?? 是System.Delegate
的子类。
在下面的示例中,您以三个方法为目标。为了演示一般情况,我将静态和实例方法结合到委托对象中。使用了以下带有支持性注释的代码段。
// Target a static method
MultiDelegate multiDel = MethodOne;
// Target another static method
multiDel += MethodTwo;
// Target an instance method
multiDel += new OutsideProgram().MethodThree;
在这种情况下,按照您在调用链中添加委托的顺序调用委托。当您调用multiDel()
时,这三个方法都会被调用。
Points to Remember
-
下面两行代码在功能上是等效的。
multiDel += MethodTwo; //Same as the following line multiDel = multiDel+MethodTwo;
-
当您使用多播委托时,委托按照您在调用链中添加它们的顺序被调用。
您可以通过使用 += 操作符来增加方法链。类似地,您可以通过使用 -= 操作符来减少链。为了演示这一点,在下面的例子中我第二次调用multiDel()
之前,我使用下面的代码行从链中删除了MethodTwo
。
multiDel -= MethodTwo;
现在来看下面的例子,它展示了使用多播委托的完整演示。
演示 3
using System;
namespace MulticastDelegateExample1
{
delegate void MultiDelegate();
class Program
{
public static void MethodOne()
{
Console.WriteLine("A static method of Program class- MethodOne() executed.");
}
public static void MethodTwo()
{
Console.WriteLine("A static method of Program class- MethodTwo() executed.");
}
static void Main(string[] args)
{
Console.WriteLine("***Example of a Multicast Delegate.***");
// Target a static method
MultiDelegate multiDel = MethodOne;
// Target another static method
multiDel += MethodTwo;
//Target an instance method
multiDel += new OutsideProgram().MethodThree;
multiDel();
//Reducing the delegate chain
Console.WriteLine("\nReducing the length of delegate chain by discarding MethodTwo now.");
multiDel -= MethodTwo;
//The following invocation will call MethodOne and MethodThree now.
multiDel();
Console.ReadKey();
}
}
class OutsideProgram
{
public void MethodThree()
{
Console.WriteLine("An instance method of OutsideProgram class is executed.");
}
}
}
输出
以下是运行该程序的输出。
***Example of a Multicast Delegate.***
A static method of Program class- MethodOne() executed.
A static method of Program class- MethodTwo() executed.
An instance method of OutsideProgram class is executed.
Reducing the length of delegate chain by discarding MethodTwo now.
A static method of Program class- MethodOne() executed.
An instance method of OutsideProgram class is executed.
分析
在演示 3 中,您看到了目标方法具有 void 返回类型。这是因为多播委托通常用于具有 void 返回类型的方法。
问答环节
1.4 你说过多播委托经常用于具有 void 返回类型 的 方法。这是什么原因呢?
多播委托以调用列表中的多个方法为目标。但是,单个方法或委托调用只能返回单个值。如果在多播委托调用中使用多个具有非 void 返回类型的方法,您将从调用列表中的最后一个方法获得返回值。尽管也调用了其他方法,但这些值都被丢弃了。下面的例子让你对此有一个更清晰的了解。
演示 4
using System;
namespace MulticastDelegateExample2
{
delegate int MultiDelegate();
class Program
{
public static int MethodOne()
{
Console.WriteLine("A static method of Program class- MethodOne() executed.");
return 1;
}
public static int MethodTwo()
{
Console.WriteLine("A static method of Program class- MethodTwo() executed.");
return 2;
}
public static int MethodThree()
{
Console.WriteLine("A static method of Program class- MethodThree() executed.");
return 3;
}
static void Main(string[] args)
{
Console.WriteLine("***A case study with a multicast delegate when we target non-void methods.***");
// Target MethodOne
MultiDelegate multiDel = MethodOne;
// Target MethodTwo
multiDel += MethodTwo;
// Target MethodThree
multiDel += MethodThree;
int finalValue=multiDel();
Console.WriteLine("The final value is {0}", finalValue);
Console.ReadKey();
}
}
}
输出
以下是运行该程序的输出。
***A case study with a multicast delegate when we target non-void methods.***
A static method of Program class- MethodOne() executed.
A static method of Program class- MethodTwo() executed.
A static method of Program class- MethodThree() executed.
The final value is 3
分析
调用列表中的三个方法(MethodOne()
、MethodTwo()
和MethodThree()
)被调用,但最终返回值是 3,它来自MethodThree
。
问答环节
我知道多播委托对于具有非 void 返回类型的方法没有用,因为中间返回值被丢弃了。但是我相信没有什么能阻止我储存这些价值观并以不同的方式使用它们。这是正确的吗?
绝对的。你总是可以收集那些价值,并随心所欲地使用它们;但很少有人做到。此外,在撰写本文时,C# 语言规范中还没有这方面的语法捷径。因此,如果对具有非 void 返回类型的方法使用多播委托,中间返回值将会丢失,这通常被认为是功能损失。
此外,您需要特别注意异常处理。如果调用列表中的方法抛出异常,其他方法将没有机会处理它。
你能提供一个例子来说明当我使用多播委托时,为什么异常处理是一个问题吗?
让我们将演示 3 中的MethodOne()
修改如下。
public static void MethodOne()
{
Console.WriteLine("A static method of Program class- MethodOne() executed.");
// For Q&A 1.6
// Let's say, some code causes an exception
// like the following
int a = 10, b = 0,c;
c = a / b;
Console.WriteLine("c={0}",c);
}
现在再次执行程序。这一次,您将得到下面的异常,结果,调用列表中的下一个方法将不会执行。这就是MethodTwo()
不会运行的原因;它没有机会处理异常。图 1-4 是来自 Visual Studio 的运行时截图。
图 1-4
Visual Studio IDE 中的运行时错误屏幕截图
1.7 在演示 1 中,您使用了以下代码行:
DelegateWithTwoIntParameterReturnInt delOb = Sum;
现在我很担心。如果我重载 Sum 方法会发生什么?
没关系。委托的作用类似于类型安全的函数指针,因为它们可以准确地跟踪完整的方法签名(例如,参数的数量、参数的类型、方法的返回类型)。
当您使用委托并具有重载方法时,编译器可以为您绑定正确的方法。为了研究这个问题,考虑下面的例子,其中的Sum
方法是重载的(我使用了静态方法,但是您也可以使用实例方法)。Sum 方法有两个重载版本。一种情况下,Sum
方法接受两个 int 参数,另一种情况下,接受三个 int 参数;但是DelegateWithTwoIntParameterReturnInt
可以适当地绑定预定的方法。
演示 5
using System;
namespace CaseStudyWithOverloadedMethods
{
delegate int DelegateWithTwoIntParameterReturnInt(int x, int y);
class Program
{
public static int Sum(int a, int b)
{
return a + b;
}
public static int Sum(int a, int b, int c)
{
return a + b + c;
}
static void Main(string[] args)
{
Console.WriteLine("***A case study with overloaded methods.***");
DelegateWithTwoIntParameterReturnInt delOb = Sum;
Console.WriteLine("\nCalling Sum(..) method using a delegate.");
int total = delOb(10, 20);
Console.WriteLine("Sum of 10 and 20 is: {0}", total);
Console.ReadKey();
}
}
}
输出
运行这个程序时,您会得到以下输出。
***A case study with overloaded methods.***
Calling Sum(..) method using a delegate.
Sum of 10 and 20 is: 30
分析
需要注意的是,如果没有正确的重载版本,就会出现编译时错误。例如,如果您注释掉预期的方法,如下所示,
//public static int Sum(int a, int b)
//{
// return a + b;
//}
您将得到以下编译错误:
No Overload for 'Sum' matches delegate 'DelegateWithTwoIntParameterReturnInt'
图 1-5 是来自 Visual Studio IDE 的部分截图。
图 1-5
Visual Studio IDE 中的编译时错误屏幕截图
问答环节
1.8 如何常用委托?
您会看到在事件处理和回调方法中使用委托(尤其是在异步编程中)。我将在本书后面的章节中讨论这一点。
1.9 我可以使用委托来指向构造函数吗?
不会。但是通过编程,您可以实现类似的效果。例如,考虑演示 2。让我们为OutsideProgram
类提供一个公共构造函数。经过这样的修改,看起来是这样的。
class OutSideProgram
{
//For Q&A 1.9
public OutSideProgram()
{
Console.WriteLine("\nOutSideProgram constructor is called.");
}
public int CalculateSum(int x, int y)
{
return x + y;
}
}
让我们定义一个委托,如下所示。
delegate OutSideProgram ConsGenerator();
现在,在 Main 中,你可以写下面几行(我在这里用了一个 lambda 表达式。你将在第三章中学习 lambda 表达式。
// For Q&A 1.9
ConsGenerator consGenerator =() =>
{
return new OutSideProgram();
};
consGenerator();
如果您现在执行程序,您将在输出中看到消息“OutSideProgram 构造函数被调用”。简而言之,你可以使用一个方法来模仿构造函数的行为。我在那里使用了 lambda 表达式,因为我还没有引入任何可以做同样事情的新方法。
1.10 我了解到在方法重载中,方法的返回类型并不重要,但是在委托的上下文中,它看起来很重要。这是正确的吗?
是的。这是需要记住的重要一点。
委托的差异
当实例化委托时,可以为它分配一个方法,该方法具有比最初指定的返回类型“更派生”的返回类型。这种支持在 C # 2.0 版及更高版本中可用。另一方面,逆变允许方法的参数类型比委托类型派生得少。总的来说,协方差和逆变称为方法组方差。
为了更好地理解,让我们从数学开始,从数学的角度来探讨重要的术语。让我们假设你有一个整数域。
对于情况 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)=xx(对于所有, x 属于整数)。在这种情况下,可以看到–1≤0 和f(–1)>f(0)。另一方面,1 < 2 和f(1)<f*(2)。投影(功能 f )既不保持尺寸方向,也不反转尺寸方向。
在情况 1 中,函数 f 是协变的;在情况 2 中,函数 f 是逆变的;而在情况 3 中,函数 f 是不变的。
在 C# 编程中,可以用匹配的签名将方法分配给委托。但是可能有这样的情况,当你的方法的返回类型与委托的返回类型不完全匹配,但是你发现这个方法的返回类型是委托的返回类型的派生类型。在这种情况下,协方差允许您将方法与委托相匹配。因此,简单地说,协方差允许您匹配具有比委托中定义的“原始返回类型”更“派生”的返回类型的方法。
逆变处理参数。它允许一个方法拥有一个比委托类型派生程度低的参数类型。
Points to Remember
让我们记住以下几点。
-
协方差允许你在需要父类型的地方传递一个派生类型;对于委托,您可以将这个概念应用于返回类型。
-
Contravariance 允许你使用比最初指定的更通用(更少派生)的类型。使用委托,可以将带有基类参数的方法分配给期望获取派生类参数的委托。
-
不变性允许你只使用最初指定的类型。它既不是协变的也不是逆变的。
协方差和逆变统称为方差。
协方差的概念从 C#1.0 开始就支持数组。你可以这样写:
Console.WriteLine("***Covariance in arrays(C#1.0 onwards)***");
// It is not type safe
object[] myObjArray = new string[5];
// Following line will cause run-time error
myObjArray[0] = 10;
但是这段代码将导致运行时错误,输出如下内容。
System.ArrayTypeMismatchException: 'Attempted to access an element as a type incompatible with the array.'
委托中的协方差
从 2.0 版开始,委托就支持协变和逆变。对泛型类型参数、泛型接口和泛型委托的支持始于 C# 4.0。我还没有讨论泛型类型。本节讨论非泛型委托,从协方差开始。在接下来的例子中,Bus
类派生自Vehicle
类。所以,你很容易理解我用Vehicle
作为基础类型,用Bus
作为派生类型。
演示 6
using System;
namespace CovarianceWithNonGenericDelegate
{
class Vehicle
{
public Vehicle CreateVehicle()
{
Vehicle myVehicle = new Vehicle();
Console.WriteLine(" Inside Vehicle.CreateVehicle, a vehicle object is created.");
return myVehicle;
}
}
class Bus : Vehicle
{
public Bus CreateBus()
{
Bus myBus = new Bus();
Console.WriteLine(" Inside Bus.CreateBus, a bus object is created.");
return myBus;
}
}
class Program
{
public delegate Vehicle VehicleDelegate();
static void Main(string[] args)
{
Vehicle vehicleOb = new Vehicle();
Bus busOb = new Bus();
Console.WriteLine("***Testing covariance with delegates. It is allowed C# 2.0 onwards.***\n");
// Normal case:
/* VehicleDelegate is expecting a method with return type Vehicle.*/
VehicleDelegate vehicleDelegate1 = vehicleOb.CreateVehicle;
vehicleDelegate1();
/* VehicleDelegate is expecting a method with return type Vehicle(i.e. a basetype) but you're assigning a method with return type Bus( a derived type) Covariance allows this kind of assignment.*/
VehicleDelegate vehicleDelegate2 = busOb.CreateBus;
vehicleDelegate2();
Console.ReadKey();
}
}
}
输出
以下是运行该程序的输出。
***Testing covariance with delegates. It is allowed C# 2.0 onwards.***
Inside Vehicle.CreateVehicle, a vehicle object is created.
Inside Bus.CreateBus, a bus object is created.
分析
请注意这一行代码以及前面程序中的支持注释。
/* VehicleDelegate is expecting a method with return type
Vehicle(i.e. a basetype)but you're assigning a method with
return type Bus( a derived type)
Covariance allows this kind of assignment.*/
VehicleDelegate vehicleDelegate2 = busOb.CreateBus;
编译器没有抱怨这一行,因为协方差提供了这种灵活性。
委托的矛盾
逆变与参数有关。假设委托可以指向接受派生类型参数的方法。使用 contravariance,可以使用同一个委托指向接受基类型参数的方法。
演示 7
using System;
namespace ContravarianceWithNonGenegicDelegate
{
class Vehicle
{
public void ShowVehicle(Vehicle myVehicle)
{
Console.WriteLine("Vehicle.ShowVehicle is called.");
Console.WriteLine("myVehicle.GetHashCode() is: {0}", myVehicle.GetHashCode());
}
}
class Bus : Vehicle
{
public void ShowBus(Bus myBus)
{
Console.WriteLine("Bus.ShowBus is called.");
Console.WriteLine("myBus.GetHashCode() is: {0}", myBus.GetHashCode());
}
}
class Program
{
public delegate void BusDelegate(Bus bus);
static void Main(string[] args)
{
Console.WriteLine("***Demonstration-7.Exploring Contravariance with non-generic delegates***");
Vehicle myVehicle = new Vehicle();
Bus myBus = new Bus();
//Normal case
BusDelegate busDelegate = myBus.ShowBus;
busDelegate(myBus);
// Special case
// Contravariance:
/*
* Note that the following 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
*/
BusDelegate anotherBusDelegate = myVehicle.ShowVehicle;
anotherBusDelegate(myBus);
// Additional note:you cannot pass vehicle object here
// anotherBusDelegate(myVehicle);//error
Console.ReadKey();
}
}
}
输出
以下是运行该程序的输出。
***Demonstration-7.Exploring Contravariance with non-generic delegates***
Bus.ShowBus is called.
myBus.GetHashCode() is: 58225482
Vehicle.ShowVehicle is called.
myVehicle.GetHashCode() is: 58225482
分析
您可以看到在前面的例子中,BusDelegate
接受一个Bus
类型参数。仍然使用 contravariance,当实例化一个BusDelegate
对象时,可以指向一个接受Vehicle
类型参数的方法。因此,逆变允许以下类型的赋值。
BusDelegate anotherBusDelegate = myVehicle.ShowVehicle;
在这两种情况下,我将同一个对象传递给了两个委托对象。因此,您会在输出中看到相同的哈希代码。本例中保留了支持性注释,以帮助您理解。
问答环节
1.11 您使用了术语 方法组方差 。为什么叫方法组?
MSDN 强调以下几点。
-
方法组,它是成员查找产生的一组重载方法。
-
方法组允许出现在 invocation _ expression(In location expressions)、delegate _ creation _ expression(delegate creation expressions)以及 is 运算符的左侧 - ,并且可以隐式转换为兼容的委托类型( m 方法组转换)。在任何其他上下文中,被分类为方法组的表达式会导致编译时错误。
使用重载方法的演示 5 案例研究包括以下代码行。
DelegateWithTwoIntParameterReturnInt delOb = Sum;
这里,Sum
指的是一个方法组。当您使用这种语句时(即,方法参数没有括号),组中的所有方法在相同的上下文中都可用,但是方法组转换可以创建调用预期方法的委托。但是在参数中包含括号的情况下,方法调用可以容易且明确地识别出来。
最后的话
您总是可以创建和使用自己的委托,但是在实际编程中,使用现成的构造可能有助于节省时间和精力。在这种情况下,Func
、Action
和Predicate
委托非常有用。但是当你在本书后面学习高级主题时,你可以有效地使用它们;比如 lambda 表达式和泛型编程。让我们暂时跳过这个话题,跳到下一个话题:事件。
摘要
本章涵盖了以下关键问题。
-
什么是委托?
-
什么是多播代理?
-
什么时候应该使用多播委托?
-
当您用委托定位静态方法和实例方法时,如何区分这些方法?
-
如何使用委托实现协变和逆变?
-
委托通常是如何使用的?
二、事件
对事件的支持被认为是 C# 中最激动人心的特性之一。
以下是事件的一些基本特征。我建议您在使用事件编码之前,反复阅读这些要点。
-
事件的支柱是委托,所以在使用事件之前了解委托是很重要的。
-
使用事件时,一段代码可以向另一段代码发送通知。
-
事件通常在 GUI 应用中使用。例如,当您单击一个按钮或选择一个单选按钮时,您可能会注意到 UI 布局中一些有趣的变化。
-
在发布者-订阅者模型中,一个对象引发一个通知(事件),一个或多个对象侦听这些事件。引发事件的对象称为发送者(或发布者或广播者),接收事件的对象称为接收者(或订阅者)。发送者不关心接收者如何解释事件。它可能不关心谁在注册以接收或取消注册以停止接收事件或通知。你可以把这和脸书或者推特联系起来。如果您关注某人,您可以在此人更新个人资料时收到通知。如果您不想收到通知,您可以随时取消订阅。简而言之,订户可以决定何时开始收听事件或何时停止收听事件。(用编程术语来说,就是什么时候注册事件,什么时候注销事件)。
-
英寸 NET 中,事件被实现为多播委托。
-
发布者包含委托。订阅者在发布者的委托上使用+=进行注册,在该委托上使用-=进行注销。所以,当我们将+=或-=应用于一个事件时,有一个特殊的含义(换句话说,它们不是赋值的快捷键)。
-
订户彼此不通信。因此,您可以构建一个松散耦合的系统。这通常是事件驱动架构的关键目标。
-
在 GUI 应用中,Visual Studio IDE 可以让您在处理事件时更加轻松。(我相信,既然这些概念是 C# 的核心,不如从基础开始学。)
-
那个。NET framework 提供了一个支持标准事件设计模式的泛型委托,如下所示:
public delegate void EventHandler<TEventArgs>(object sendersource, TEventArgs e), where TEventArgs : EventArgs;.
我还没有讨论泛型,所以你现在可以跳过这一点。但是有趣的是,为了支持向后兼容性,在。NET framework 遵循非泛型自定义委托模式。
-
下面是一个事件声明的示例:
public event EventHandler MyIntChanged;
这只是表明
MyIntChanged
是事件的名称,而EventHandler
是相应的代表。 -
修饰符不需要是公共的。你可以为你的事件选择非公开的修饰语,比如
private
、protected
、internal
等等。在这种情况下,你也可以使用关键字static
、virtual
、override
、abstract
、sealed
和new
。
演示 1
现在您已经准备好编码了。在声明事件之前,您需要一个委托。在示例中,您会看到下面的代码行。
public event EventHandler MyIntChanged;
但是您看不到委托声明,因为我使用了预定义的EventHandler
委托。
现在让我们关注我们的实现。有两类:Sender
和Receiver
。Sender
扮演广播员的角色;当您更改myInt
实例值时,它会引发MyIntChanged
事件。Receiver
类扮演消费者的角色。它有一个方法叫做GetNotificationFromSender
。要从发件人处获得通知,请注意下面的代码行。
// Receiver is registering for a notification from sender
sender.MyIntChanged += receiver.GetNotificationFromSender;
这里的sender
是一个Sender
类对象,receiver
是一个Receiver
类对象。最终,接收者不再对从发送者那里获得进一步的通知感兴趣,并使用下面的代码取消订阅事件。
// Unregistering now
sender.MyIntChanged -= receiver.GetNotificationFromSender;
值得注意的是,发送者可以向自己发送通知。为了演示这一点,在最后几行of Main
中,您会看到下面的代码。
// Sender will receive its own notification now onwards
sender.MyIntChanged += sender.GetNotificationItself;
using System;
namespace EventEx1
{
class Sender
{
private int myInt;
public int MyInt
{
get
{
return myInt;
}
set
{
myInt = value;
//Whenever we set a new value, the event will fire.
OnMyIntChanged();
}
}
//EventHandler is a predefined delegate which is used to //handle simple events.
//It has the following signature:
//delegate void System.EventHandler(object sender,System.EventArgs e)
//where the sender tells who is sending the event and
//EventArgs is used to store information about the event.
public event EventHandler MyIntChanged;
public void OnMyIntChanged()
{
if(MyIntChanged!=null )
{
MyIntChanged(this, EventArgs.Empty);
}
}
public void GetNotificationItself(Object sender, System.EventArgs e)
{
Console.WriteLine("Sender himself send a notification: I have changed myInt value to {0} ", myInt);
}
}
class Receiver
{
public void GetNotificationFromSender(Object sender, System.EventArgs e)
{
Console.WriteLine("Receiver receives a notification: Sender recently has changed the myInt value . ");
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Exploring events.***");
Sender sender = new Sender();
Receiver receiver = new Receiver();
//Receiver is registering for a notification from sender
sender.MyIntChanged += receiver.GetNotificationFromSender;
sender.MyInt = 1;
sender.MyInt = 2;
//Unregistering now
sender.MyIntChanged -= receiver.GetNotificationFromSender;
//No notification sent for the receiver now.
sender.MyInt = 3;
//Sender will receive its own notification now onwards.
sender.MyIntChanged += sender.GetNotificationItself;
sender.MyInt = 4;
Console.ReadKey();
}
}
}
输出
以下是运行该程序的输出。
***Exploring events.***
Receiver receives a notification: Sender recently has changed the myInt value.
Receiver receives a notification: Sender recently has changed the myInt value.
Sender himself send a notification: I have changed myInt value to 4
分析
最初,我使用MyInt
属性更改了发送者的myInt
值。当我将该值更改为 1 或 2 时,Receiver 对象(receiver
)收到了通知,因为它订阅了该事件。然后receiver
退订了。因此,当我将值改为 3 时,receiver
没有任何通知。然后sender
订阅事件通知。结果,当我将值改为 4 时,sender
收到了通知。
Note
在现实应用中,一旦你订阅了一个事件,你也应该在离开前退订该事件;否则,您可能会看到内存泄漏的影响。
问答环节
2.1 我可以在特定事件上使用任何方法吗?
不。它应该与委托签名相匹配。例如,让我们假设Receiver
类有另一个名为UnRelatedMethod
的方法,如下所示。
public void UnRelatedMethod()
{
Console.WriteLine(" An unrelated method. ");
}
在演示 1 中,如果您通过使用语句用MyIntChanged
附加了这个方法
sender.MyIntChanged += receiver.UnRelatedMethod;//Error
您将得到以下编译时错误:
CS0123 No overload for 'UnRelatedMethod' matches delegate 'EventHandler'
创建自定义事件
在演示 1 中,您看到了一个内置的委托,但是在许多情况下,您可能需要自己的事件来处理特定的场景。让我们来练习一个关于自定义事件的程序。为了使这个例子简单明了,我们假设发送者不需要给自己发送任何通知。所以,现在Sender
类中没有类似GetNotificationItself
的方法。
为了使更改与前面的示例保持一致,让我们按照以下步骤操作。
-
创建代理人。按照惯例,选择带
EventHandler
后缀的代理名称;大概如下:delegate void MyIntChangedEventHandler(Object sender, EventArgs eventArgs);
-
定义您的活动。按照惯例,您可以去掉代理名称的后缀
EventHandler
并设置您的事件名称。public event MyIntChangedEventHandler MyIntChanged;
-
引发事件。让我们在 Sender 类中使用下面的方法。一般情况下,不做方法
public
,建议你做方法protected virtual
。protected virtual void OnMyIntChanged() { if (MyIntChanged != null) { MyIntChanged(this, EventArgs.Empty); } }
-
处理事件。让我们使用一个
Receiver
类,它有下面的方法来处理被引发的事件。让我们保持与演示 1 中的相同。class Receiver { public void GetNotificationFromSender(Object sender, System.EventArgs e) { Console.WriteLine("Receiver receives a notification: Sender recently has changed the myInt value . "); } }
演示 2
现在进行完整的演示。
using System;
namespace EventsEx2
{
//Step 1-Create a delegate.
//You can pick an name (this name will be your event name)
//which has the suffix EventHandler.For example, in the following case
//'MyIntChanged' is the event name which has the suffix 'EventHandler'
delegate void MyIntChangedEventHandler(Object sender, EventArgs eventArgs);
//Create a Sender or Publisher for the event.
class Sender
{
//Step-2: Create the event based on your delegate.
public event MyIntChangedEventHandler MyIntChanged;
private int myInt;
public int MyInt
{
get
{
return myInt;
}
set
{
myInt = value;
//Raise the event.
//Whenever we set a new value, the event will fire.
OnMyIntChanged();
}
}
/*
Step-3.
In the standard practise, the method name is the event name with a prefix 'On'.For example, MyIntChanged(event name) is prefixed with 'On' here.
Also, in normal practises, instead of making the method 'public',
you make the method 'protected virtual'.
*/
protected virtual void OnMyIntChanged()
{
if (MyIntChanged != null)
{
MyIntChanged(this, EventArgs.Empty);
}
}
}
//Step-4: Create a Receiver or Subscriber for the event.
class Receiver
{
public void GetNotificationFromSender(Object sender, System.EventArgs e)
{
Console.WriteLine("Receiver receives a notification: Sender recently has changed the myInt value . ");
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Exploring a custom event.***");
Sender sender = new Sender();
Receiver receiver = new Receiver();
//Receiver is registering for a notification from sender
sender.MyIntChanged += receiver.GetNotificationFromSender;
sender.MyInt = 1;
sender.MyInt = 2;
//Unregistering now
sender.MyIntChanged -= receiver.GetNotificationFromSender;
//No notification sent for the receiver now.
sender.MyInt = 3;
Console.ReadKey();
}
}
}
输出
以下是运行该程序的输出。
***Exploring a custom event.***
Receiver receives a notification: Sender recently has changed the myInt value .
Receiver receives a notification: Sender recently has changed the myInt value .
分析
您可以看到,通过使用MyInt
属性,我正在更改myInt
的值。当该值设置为 1 或 2 时,接收方会收到通知,但是当myInt
值更改为 3 时,接收方没有收到通知,因为事件通知被取消订阅。
将数据传递给事件参数
让我们再来看看OnMyIntChanged
方法。在前两个演示中,我在方法中使用了下面一行代码。
MyIntChanged(this, EventArgs.Empty);
我没有在事件参数中传递任何东西。但是在现实编程中,你可能需要传递一些有意义的东西。让我们在演示 3 中分析这样一个案例。
演示 3
在这个演示中,我遵循了这些步骤。
-
创建
EventArgs
的子类。这个类有一个JobNo property
来设置jobNo
实例变量的值。 -
修改
OnMyIntChanged
方法,用事件封装预期数据(在本例中是job number
)。现在这个方法看起来如下:protected virtual void OnMyIntChanged() { if (MyIntChanged != null) { // Combine your data with the event argument JobNoEventArgs jobNoEventArgs = new JobNoEventArgs(); jobNoEventArgs.JobNo = myInt; MyIntChanged(this, jobNoEventArgs); }}
-
在这次演示中,我保持了相同的步骤。
这是完整的演示。
using System;
namespace EventsEx3
{
// Create a subclass of System.EventArgs
class JobNoEventArgs : EventArgs
{
int jobNo = 0;
public int JobNo
{
get { return jobNo; }
set { jobNo = value; }
}
}
// Create a delegate.
delegate void MyIntChangedEventHandler(Object sender, JobNoEventArgs eventArgs);
// Create a Sender or Publisher for the event.
class Sender
{
// Create the event based on your delegate.
public event MyIntChangedEventHandler MyIntChanged;
private int myInt;
public int MyInt
{
get
{
return myInt;
}
set
{
myInt = value;
// Raise the event.
// Whenever you set a new value, the event will fire.
OnMyIntChanged();
}
}
/*
In the standard practise, the method name is the event name with a prefix 'On'.For example, MyIntChanged(event name) is prefixed with 'On' here.Also, in normal practises, instead of making the method 'public',you make the method 'protected virtual'.
*/
protected virtual void OnMyIntChanged()
{
if (MyIntChanged != null)
{ // Combine your data with the event argument
JobNoEventArgs jobNoEventArgs = new JobNoEventArgs();
jobNoEventArgs.JobNo = myInt;
MyIntChanged(this, jobNoEventArgs);
}
}
}
// Create a Receiver or Subscriber for the event.
class Receiver
{
public void GetNotificationFromSender(Object sender, JobNoEventArgs e)
{
Console.WriteLine("Receiver receives a notification: Sender recently has changed the myInt value to {0}.",e.JobNo);
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Passing data in the event argument.***");
Sender sender = new Sender();
Receiver receiver = new Receiver();
// Receiver is registering for a notification from sender
sender.MyIntChanged += receiver.GetNotificationFromSender;
sender.MyInt = 1;
sender.MyInt = 2;
// Unregistering now
sender.MyIntChanged -= receiver.GetNotificationFromSender;
// No notification sent for the receiver now.
sender.MyInt = 3;
Console.ReadKey();
}
}
}
输出
以下是运行该程序的输出。
***Passing data in the event argument.***
Receiver receives a notification: Sender recently has changed the myInt value to 1.
Receiver receives a notification: Sender recently has changed the myInt value to 2.
使用事件访问器
让我们对演示 3 做一些有趣的修改。而不是使用
public event MyIntChangedEventHandler MyIntChanged;
使用下面的代码段。
private MyIntChangedEventHandler myIntChanged;
public event MyIntChangedEventHandler MyIntChanged
{
add
{
myIntChanged += value;
}
remove
{
myIntChanged -= value;
}
}
为了适应这种变化,让我们如下更新OnMyIntChanged
方法。
protected virtual void OnMyIntChanged()
{
if (myIntChanged != null)
{
// Combine your data with the event argument
JobNoEventArgs jobNoEventArgs = new JobNoEventArgs();
jobNoEventArgs.JobNo = myInt;
myIntChanged(this, jobNoEventArgs);
}
}
现在如果你执行这个程序,你会得到同样的输出。这怎么可能?编译器的工作方式类似于您声明事件时的方式。让我们回到事件的基本原理。
事件是一种特殊的多播委托,您只能从包含该事件的类中调用它。接收者可以订阅该事件,并使用其中的方法处理该事件。因此,接收者在订阅事件时传递方法引用。因此,此方法通过事件访问器添加到委托的订阅列表中。这些事件访问器类似于属性访问器,只是它们被命名为add
和remove
。
通常,您不需要提供自定义事件访问器。但是当您定义它们时,您是在指示 C# 编译器不要为您生成默认的字段和访问器。
在撰写本文时,基于。NET 框架目标 c# 7.3;鉴于。NET 核心应用面向 C# 8.0。如果您在。NET Framework(我们将其重命名为EventEx3DotNetFramework
)并研究 IL 代码,您会注意到 IL 代码中出现了add_<EventName>
和remove_<EventName>
。图 2-1 是 IL 代码的部分截图。
图 2-1
IL 代码的部分截图
演示 4
我们来做一个完整的演示,如下。
using System;
namespace EventsEx4
{
//Create a subclass of System.EventArgs
class JobNoEventArgs : EventArgs
{
int jobNo = 0;
public int JobNo
{
get { return jobNo; }
set { jobNo = value; }
}
}
// Create a delegate.
delegate void MyIntChangedEventHandler(Object sender, JobNoEventArgs eventArgs);
// Create a Sender or Publisher for the event.
class Sender
{
// Create the event based on your delegate.
#region equivalent code
// public event MyIntChangedEventHandler MyIntChanged;
private MyIntChangedEventHandler myIntChanged;
public event MyIntChangedEventHandler MyIntChanged
{
add
{
Console.WriteLine("***Inside add accessor.Entry point.***");
myIntChanged += value;
}
remove
{
myIntChanged -= value;
Console.WriteLine("***Inside remove accessor.Exit point.***");
}
}
#endregion
private int myInt;
public int MyInt
{
get
{
return myInt;
}
set
{
myInt = value;
// Raise the event.
// Whenever we set a new value, the event will fire.
OnMyIntChanged();
}
}
protected virtual void OnMyIntChanged()
{
// if (MyIntChanged != null)
if (myIntChanged != null)
{
// Combine your data with the event argument
JobNoEventArgs jobNoEventArgs = new JobNoEventArgs();
jobNoEventArgs.JobNo = myInt;
// MyIntChanged(this, jobNoEventArgs);
myIntChanged(this, jobNoEventArgs);
}
}
}
// Create a Receiver or Subscriber for the event.
class Receiver
{
public void GetNotificationFromSender(Object sender, JobNoEventArgs e)
{
Console.WriteLine("Receiver receives a notification: Sender recently has changed the myInt value to {0}.", e.JobNo);
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Using event accessors.***");
Sender sender = new Sender();
Receiver receiver = new Receiver();
// Receiver is registering for a notification from sender
sender.MyIntChanged += receiver.GetNotificationFromSender;
sender.MyInt = 1;
sender.MyInt = 2;
// Unregistering now
sender.MyIntChanged -= receiver.GetNotificationFromSender;
// No notification sent for the receiver now.
sender.MyInt = 3;
Console.ReadKey();
}
}
}
输出
以下是运行该程序的输出。
***Using event accessors.***
***Inside add accessor.Entry point.***
Receiver receives a notification: Sender recently has changed the myInt value to 1.
Receiver receives a notification: Sender recently has changed the myInt value to 2.
***Inside remove accessor.Exit point.***
分析
当您使用事件访问器时,请记住一个重要的建议:实现锁定机制。例如,当您编写以下代码段时,可以改进演示 4。
public object lockObject = new object();
private MyIntChangedEventHandler myIntChanged;
public event MyIntChangedEventHandler MyIntChanged
{
add
{
lock (lockObject)
{
Console.WriteLine("***Inside add accessor.Entry point.***");
myIntChanged += value;
}
}
remove
{
lock (lockObject)
{
myIntChanged -= value;
Console.WriteLine("***Inside remove accessor.Exit point.***");
}
}
}
问答环节
2.2 使用用户定义的事件访问器的主要好处是什么?
让我们仔细看看下面这段代码。
private MyIntChangedEventHandler myIntChanged;
public event MyIntChangedEventHandler MyIntChanged
{
add
{
myIntChanged += value;
}
remove
{
myIntChanged -= value;
}
}
注意,这些事件访问器类似于属性访问器,除了它们被命名为add
和remove
。这里你在你的委托周围使用了一个类似属性的包装。因此,只有包含类可以直接调用委托;外人不能这么做。这促进了更好的安全性和对代码的控制。
处理界面事件
接口可以包含事件。当您实现接口方法或属性时,您需要遵循相同的规则。以下示例显示了这样的实现。
演示 5
在这个例子中,IMyInterface
有一个MyIntChanged
事件。我使用了Sender
和Receiver
,它们与前面的例子相同。唯一不同的是,这一次,Sender
类实现了IMyInterface
接口。
using System;
namespace EventEx5
{
interface IMyInterface
{
// An interface event
event EventHandler MyIntChanged;
}
class Sender : IMyInterface
{
// Declare the event here and raise from your intended location
public event EventHandler MyIntChanged;
private int myInt;
public int MyInt
{
get
{
return myInt;
}
set
{
// Setting a new value prior to raise the event.
myInt = value;
OnMyIntChanged();
}
}
protected virtual void OnMyIntChanged()
{
if (MyIntChanged != null)
{
MyIntChanged(this, EventArgs.Empty);
}
}
}
class Receiver
{
public void GetNotificationFromSender(Object sender, System.EventArgs e)
{
Console.WriteLine("Receiver receives a notification: Sender recently has changed the myInt value . ");
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Exploring an event with an interface.***");
Sender sender = new Sender();
Receiver receiver = new Receiver();
// Receiver is registering for a notification from sender
sender.MyIntChanged += receiver.GetNotificationFromSender;
sender.MyInt = 1;
sender.MyInt = 2;
// Unregistering now
sender.MyIntChanged -= receiver.GetNotificationFromSender;
// No notification sent for the receiver now.
sender.MyInt = 3;
Console.ReadKey();
}
}
}
输出
以下是运行该程序的输出。
***Exploring an event with an interface.***
Receiver receives a notification: Sender recently has changed the myInt value .
Receiver receives a notification: Sender recently has changed the myInt value .
问答环节
2.3 当接口事件同名时,我的类如何实现多个接口?
是的,这种情况很有意思。当您的类实现多个具有公共名称事件的接口时,您需要遵循显式接口实现技术。但是有一个重要的限制,即在这种情况下,您需要提供添加和移除事件访问器。通常,编译器可以提供这些访问器,但在这种情况下,它不能。下一节提供了完整的演示。
处理显式接口事件
为了简单起见,这个例子与前面的例子一致。我们假设现在你有两个接口:IBeforeInterface
和IAfterInterface
。进一步假设每个包含一个名为MyIntChanged.
的事件
Sender
类实现了这些接口。现在你有两个接收者:ReceiverBefore
和ReceiverAfter
。当myInt
改变时,这些接收者类想要得到通知。在这个例子中,ReceiverBefore
对象在myInt
改变之前得到通知,而ReceiverAfter
对象在myInt
改变之后得到通知。
在演示 4 中,您看到了如何实现事件访问器。这里遵循相同的机制。这一次,我遵循了微软的建议,所以您可以看到锁在事件访问器中的使用。
演示 6
完成下面的完整演示。
using System;
namespace EventEx6
{
interface IBeforeInterface
{
public event EventHandler MyIntChanged;
}
interface IAfterInterface
{
public event EventHandler MyIntChanged;
}
class Sender : IBeforeInterface, IAfterInterface
{
// Creating two separate events for two interface events
public event EventHandler BeforeMyIntChanged;
public event EventHandler AfterMyIntChanged;
// Microsoft recommends this, i.e. to use a lock inside accessors
object objectLock = new Object();
private int myInt;
public int MyInt
{
get
{
return myInt;
}
set
{
// Fire an event before we make a change to myInt.
OnMyIntChangedBefore();
Console.WriteLine("Making a change to myInt from {0} to {1}.",myInt,value);
myInt = value;
// Fire an event after we make a change to myInt.
OnMyIntChangedAfter();
}
}
// Explicit interface implementation required.
// Associate IBeforeInterface's event with
// BeforeMyIntChanged
event EventHandler IBeforeInterface.MyIntChanged
{
add
{
lock (objectLock)
{
BeforeMyIntChanged += value;
}
}
remove
{
lock (objectLock)
{
BeforeMyIntChanged -= value;
}
}
}
// Explicit interface implementation required.
// Associate IAfterInterface's event with
// AfterMyIntChanged
event EventHandler IAfterInterface.MyIntChanged
{
add
{
lock (objectLock)
{
AfterMyIntChanged += value;
}
}
remove
{
lock (objectLock)
{
AfterMyIntChanged -= value;
}
}
}
// This method uses BeforeMyIntChanged event
protected virtual void OnMyIntChangedBefore()
{
if (BeforeMyIntChanged != null)
{
BeforeMyIntChanged(this, EventArgs.Empty);
}
}
// This method uses AfterMyIntChanged event
protected virtual void OnMyIntChangedAfter()
{
if (AfterMyIntChanged != null)
{
AfterMyIntChanged(this, EventArgs.Empty);
}
}
}
// First receiver: ReceiverBefore class
class ReceiverBefore
{
public void GetNotificationFromSender(Object sender, System.EventArgs e)
{
Console.WriteLine("ReceiverBefore receives : Sender is about to change the myInt value . ");
}
}
// Second receiver: ReceiverAfter class
class ReceiverAfter
{
public void GetNotificationFromSender(Object sender, System.EventArgs e)
{
Console.WriteLine("ReceiverAfter receives : Sender recently has changed the myInt value . ");
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Handling explicit interface events.***");
Sender sender = new Sender();
ReceiverBefore receiverBefore = new ReceiverBefore();
ReceiverAfter receiverAfter = new ReceiverAfter();
// Receiver's are registering for getting //notifications from Sender
sender.BeforeMyIntChanged += receiverBefore.GetNotificationFromSender;
sender.AfterMyIntChanged += receiverAfter.GetNotificationFromSender;
sender.MyInt = 1;
Console.WriteLine("");
sender.MyInt = 2;
// Unregistering now
sender.BeforeMyIntChanged -= receiverBefore.GetNotificationFromSender;
sender.AfterMyIntChanged -= receiverAfter.GetNotificationFromSender;
Console.WriteLine("");
// No notification sent for the receivers now.
sender.MyInt = 3;
Console.ReadKey();
}
}
}
输出
以下是运行该程序的输出。
***Handling explicit interface events.***
ReceiverBefore receives : Sender is about to change the myInt value .
Making a change to myInt from 0 to 1.
ReceiverAfter receives : Sender recently has changed the myInt value .
ReceiverBefore receives : Sender is about to change the myInt value .
Making a change to myInt from 1 to 2.
ReceiverAfter receives : Sender recently has changed the myInt value .
Making a change to myInt from 2 to 3.
问答环节
2.4 代理是事件的支柱,一般来说,当我们为事件编写代码以及注册和注销这些事件时,我们遵循观察者设计模式。这是正确的吗?
是的。
在这一章的开始,你说当我写一个关于某个事件的程序时,我也可以使用“new”关键字。能举个例子吗?
我基本上用的是简写形式。例如,在演示 1 中,当我注册事件时,您会看到下面一行代码。
sender.MyIntChanged += receiver.GetNotificationFromSender;
现在,如果您回忆一下在第一章的委托上下文中使用的缩写形式,您可以编写等价的代码,如下所示。
sender.MyIntChanged += new EventHandler(receiver.GetNotificationFromSender);
除此之外,考虑另一种情况,发送者类包含一个密封的事件。如果您有 Sender 的派生类,则它不能使用事件。相反,派生类可以使用“new”关键字来指示它没有重写基类事件。
你能举一个抽象事件的例子吗?
见演示 7。
演示 7
微软表示,对于一个抽象事件,你不会得到编译器生成的add
和remove
事件访问器块。所以,你的派生类需要提供自己的实现。让我们简化一下,稍微修改一下演示 1。像演示 2 一样,让我们假设在这个例子中,发送者不需要向自己发送通知。在这个演示中,Sender
类中没有GetNotificationItself
方法。
现在我们来关注关键部分。Sender 类包含一个抽象事件,如下所示。
public abstract event EventHandler MyIntChanged;
由于该类包含一个抽象事件,因此该类本身也变得抽象。
我现在将介绍另一个名为ConcreteSender
的类,它派生自 Sender。它重写事件并完成事件调用过程。
下面是ConcreteSender
的实现。
class ConcreteSender : Sender
{
public override event EventHandler MyIntChanged;
protected override void OnMyIntChanged()
{
if (MyIntChanged != null)
{
MyIntChanged(this, EventArgs.Empty);
}
}
}
现在我们来看一下完整的程序和输出。
using System;
namespace EventsEx7
{
abstract class Sender
{
private int myInt;
public int MyInt
{
get
{
return myInt;
}
set
{
myInt = value;
// Whenever we set a new value, the event will fire.
OnMyIntChanged();
}
}
// Abstract event.The containing class becomes abstract for this.
public abstract event EventHandler MyIntChanged;
protected virtual void OnMyIntChanged()
{
Console.WriteLine("Sender.OnMyIntChanged");
}
}
class ConcreteSender : Sender
{
public override event EventHandler MyIntChanged;
protected override void OnMyIntChanged()
{
if (MyIntChanged != null)
{
MyIntChanged(this, EventArgs.Empty);
}
}
}
class Receiver
{
public void GetNotificationFromSender(Object sender, System.EventArgs e)
{
Console.WriteLine("Receiver receives a notification: Sender recently has changed the myInt value . ");
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Exploring an abstract event.***");
Sender sender = new ConcreteSender();
Receiver receiver = new Receiver();
// Receiver is registering for a notification from sender
sender.MyIntChanged += receiver.GetNotificationFromSender;
sender.MyInt = 1;
sender.MyInt = 2;
// Unregistering now
sender.MyIntChanged -= receiver.GetNotificationFromSender;
// No notification sent for the receiver now.
sender.MyInt = 3;
Console.ReadKey();
}
}}
输出
以下是运行该程序的输出。
***Exploring an abstract event.***
Receiver receives a notification: Sender recently has changed the myInt value .
Receiver receives a notification: Sender recently has changed the myInt value .
问答环节
2.7 我知道 EventHandler
是一个预定义的代表。但是在很多地方,我看到人们在广义上使用术语 事件处理程序 。有什么特别的含义与之相关吗?
简单地说,事件处理程序是一个过程,当一个特定的事件发生时,你决定做什么。例如,当用户点击 GUI 应用中的按钮时。请注意,您的事件可以有多个处理程序,同时,处理事件的方法也可以动态变化。在本章中,你看到了事件是如何工作的,特别是 Receiver 类是如何处理事件的。但是如果您使用像 Visual Studio 中的 Windows 窗体设计器这样的现成构造,就可以非常容易地编写事件代码。
有一个如何在 GUI 应用中添加事件处理程序的例子会很有帮助。
让我们看看演示 8。
演示 8
在这个演示中,我创建了一个简单的 UI 应用来演示一个简单的事件处理机制。做这件事的步骤如下。
-
创建 Windows 窗体应用。
-
From the Toolbox, drag a button onto the form. Let’s name it Test. Figure 2-2 shows what it may look like.
图 2-2
放置在 Form1 上的测试按钮
-
Select the button. Open the Properties window and click the Events button. Name the Click event
TestBtnClickHandler
(see Figure 2-3).图 2-3
将点击事件名称设置为
TestBtnClickHandler
-
双击测试按钮。这将打开 Form1.cs 文件,您可以在其中为事件处理程序编写以下代码。
private void TestBtnClickHandler(object sender, EventArgs e) { MessageBox.Show("Hello Reader."); }
输出
运行您的应用并单击 Test 按钮。您会看到如图 2-4 所示的输出。(为了更好的截图,我在 Form1 上拖动了消息框窗口。)
图 2-4
单击“测试”按钮时,从 Visual Studio 输出屏幕截图
Note
演示 8 于年执行。但在. NET Framework 中没有。NET 核心。在撰写本文时,可视化设计器被标记为的“预览功能”。NET 核心应用,它受到了很多问题的困扰(更多信息,请访问 https://github.com/dotnet/winforms/blob/master/Documentation/designer-releases/0.1/knownissues.md
)。在解决方案资源管理器中单击 Form1.cs 文件时,在. NET 核心应用中看不到 Form1.cs[Design]。
最后的话
在演示 2 中,您看到了下面的代码段。
if (MyIntChanged != null)
{
MyIntChanged(this, EventArgs.Empty);
}
实际上,在所有示例中,在引发事件之前都会看到这种空检查。这很重要,因为如果事件没有监听器(或接收器),您可能会遇到一个名为NullReferenceException
的异常。在这种情况下,Visual Studio 会向您显示如图 2-5 所示的屏幕。
图 2-5
由于缺少事件侦听器和正确的空检查,发生了 NullReferenceException
在引发事件之前,空检查非常重要。但是你可以假设在一个真实的应用中,如果你需要做一些空值检查,这会让你的代码变得笨拙。在这种情况下,您可以使用从 C# 6.0 开始就有的功能。您可以使用空条件操作符来避免突然的意外。
我使用这个操作符提供了另一个代码段。(我保留了带注释的死代码,以便您可以同时比较两个代码段)。
//if (MyIntChanged != null)
//{
// MyIntChanged(this, EventArgs.Empty);
//}
//Alternate code
MyIntChanged?.Invoke(this, EventArgs.Empty);
这都是关于事件的。现在让我们继续第三章,在这里你将学习使用 C# 中另一个强大的特性:lambda 表达式。
摘要
本章讨论了以下关键问题。
-
什么是事件?如何使用内置的事件支持?
-
如何编写自定义事件?
-
如何将数据传递给事件参数?
-
如何使用事件访问器?它们为什么有用?
-
如何使用不同的界面事件?
-
如何将不同的修饰语和关键词应用到一个事件中?
-
如何在简单的 UI 应用中实现事件处理机制?
三、Lambda 表达式
Lambda 表达式和匿名方法是高级编程中的两个重要概念。总的来说,它们通常被称为匿名函数。C# 2.0 引入了匿名方法的概念,C# 3.0 引入了 lambda 表达式。随着时间的推移,lambda 表达式变得比匿名方法更受欢迎。如果你瞄准了。NET Framework 3.5 或更高版本,建议您使用 lambda 表达式。本章向你展示了使用 lambda 表达式的不同方法,以及如何在高级编程中有效地使用它们。
Lambda 表达式的有用性
lambda 表达式是一种以易于阅读的形式编写的匿名方法。什么是匿名方法,为什么它有用?顾名思义,匿名方法就是没有名字的方法。在某些情况下,它们非常有用。例如,当您使用委托指向一个方法,但该方法出现在源文件中的不同位置时(或者,在极端情况下,它出现在不同的源文件中)。这种分离的代码很难理解、调试和维护。在这种情况下,匿名方法很有帮助,因为您可以定义一个没有名称的“内联”方法来满足您的目的。
lambda 这个词来自 lambda calculus,它模拟了一个图灵机。它用希腊字母 lambda (λ)拼写,而你的键盘上没有。要表示一个 lambda 运算符,可以使用= >符号。运算符的左侧指定输入参数(如果有),运算符的右侧指定表达式或语句块。= >是右关联的,其优先级与=相同。当读取包含 lambda 运算符的代码时,将 lambda 运算符替换为转到 , 转到 , 箭头 ,或成为。比如你读x=> x+5
;因为x
去了x+5
。同样,你把(x,y)=>x+y;
读成x
,y
到x+y
。
C# 编译器可以将 lambda 表达式转换为委托实例或表达式树(这在 LINQ 中经常使用)。这本书没有详细讨论 LINQ,但是你已经了解了代表,并且在第一章看到了几个关于他们的例子。让我们在这里关注委托实例。
Note
当 lambda 表达式转换为委托类型时,结果取决于输入参数和返回类型。如果一个 lambda 表达式没有返回类型,它可以被转换成一个Action
委托类型;如果它有一个返回类型,它可以被转换成一个Func
委托类型。Func
和Action
是通用的代表,你会在第四章中了解到。
演示 1
我从一个简单的程序开始,这个程序使用各种方法计算两个整数(21 和 79)的和。第一种方法使用普通的方法(这是您所熟悉的)。您可以使用这个方法来计算int
的总和。接下来,我将向您展示如何使用一个委托实例来做同样的事情。最后两段代码分别展示了匿名方法和 lambda 表达式的用法。每个程序段生成相同的输出。这个程序让你选择方法。为了可读性,请浏览支持性注释。
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 the use of a lambda expression and comparing it with other techniques. ***");
// Without using delgates or lambda expression
Console.WriteLine(" Using a normal method.");
int a = 21, b = 79;
Console.WriteLine(" Invoking the Sum() method in a common way without using a delegate.");
Console.WriteLine("Sum of {0} and {1} is : {2}", a,b, Sum(a, b));
/* Using Delegate(Initialization with a named method)*/
Mydel del1 = new Mydel(Sum);
Console.WriteLine("\n Using delegate now.");
Console.WriteLine("Invoking the Sum() method with the use of a delegate.");
Console.WriteLine("Sum of {0} and {1} is : {2}", a, b, del1(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("Invoking the Sum() method using an anonymous method.");
Console.WriteLine("Sum of {0} and {1} is : {2}", a, b, del2(a, b));
// Using Lambda expression(C# 3.0 onwards)
Console.WriteLine("\n Using lambda expression now.");
Mydel sumOfTwoIntegers = (x, y) => x + y;
Console.WriteLine("Sum of {0} and {1} is : {2}", a, b, sumOfTwoIntegers(a, b));
Console.ReadKey();
}
}
}
输出
以下是运行该程序的输出。
***Exploring the use of a lambda expression and comparing it with other techniques.***
Using a normal method.
Invoking the Sum() method in a common way without using a delegate.
Sum of 21 and 79 is : 100
Using delegate now.
Invoking the Sum() method with the use of a delegate.
Sum of 21 and 79 is : 100
Using anonymous method now.
Invoking the Sum() method using an anonymous method.
Sum of 21 and 79 is : 100
Using lambda expression now.
Sum of 21 and 79 is : 100
分析
让我们回顾一下用于匿名方法和 lambda 表达式的语句。对于匿名方法,我使用了
delegate (int x, int y) { return x + y; };
对于 lambda 表达式,我使用
(x, y) => x + y;
如果您熟悉匿名方法,但不熟悉 lambda 表达式,您可以使用以下步骤从匿名方法中获取 lambda 表达式。
对于第 1 步,从匿名方法表达式中删除 delegate 关键字,这将产生如图 3-1 所示的结果。
图 3-1
从匿名方法表达式中移除 delegate 关键字
也就是你得到(int x, int y) {return x + y; };
。
在步骤 2 中,添加一个 lambda 操作符,结果如图 3-2 所示。(它还会产生有效的 lambda 表达式。)
图 3-2
在步骤 1 表达式中添加 lambda 运算符
请注意,在本例中,我处理的是一个 return 语句。在这种情况下,作为第 3 步,您可以删除花括号、分号和回车,结果如图 3-3 所示(这是一个有效的 lambda 语句)。
图 3-3
从步骤 2 的表达式中删除花括号、分号和“return”
也就是说,你得到:(int x, int y) => x + y;
在大多数情况下,编译器在处理 lambda 表达式时可以识别输入参数和返回类型。用编程术语来说,这叫做类型推理。尽管如此,在某些特殊情况下,您可能需要保留这种类型信息,以便让编译器正确地计算表达式。但这是一个非常简单的情况,编译器可以正确理解它(在这种情况下,请注意委托声明),即使您没有提到输入参数的类型。因此,对于步骤 4,您可以从输入参数中移除类型信息,并使表达式更短,如图 3-4 所示。
图 3-4
从步骤 3 的表达式中删除类型信息以获得最短的表达式
也就是你得到(x, y) => x + y;
。
有(和没有)参数的 Lambda 表达式
lambda 表达式可以接受一个或多个参数。也可以使用不接受任何参数的 lambda 表达式。
在演示 1 中,您看到当一个 lambda 表达式使用多个参数时,您将它们列在用逗号分隔的括号中,就像(x, y)=> x+y;
。
如果 lambda 表达式只接受一个参数,可以省略括号。例如,您可以使用(x)=> x*x;
或x=>x*x;
。两者都有相同的目的。
最后,() => Console.WriteLine("No parameter.");
是一个没有任何参数的 lambda 表达式的例子。演示 2 展示了所有案例。
演示 2
本演示涵盖了带有不同参数的 lambda 表达式的用法。
using System;
namespace LambdaExpTest2
{
class Program
{
public delegate void DelegateWithNoParameter();
public delegate int DelegateWithOneIntParameter(int x);
public delegate void DelegateWithTwoIntParameters(int x, int y);
static void Main(string[] args)
{
Console.WriteLine("***Experimenting lambda expressions with different parameters.***\n");
// Without lambda exp.
Method1(5, 10);
// Using Lambda expression
DelegateWithNoParameter delWithNoParam = () => Console.WriteLine("Using lambda expression with no parameter, printing Hello");
delWithNoParam();
DelegateWithOneIntParameter delWithOneIntParam = (x) => x * x;
Console.WriteLine("\nUsing a lambda expression with one parameter, square of 5 is {0}", delWithOneIntParam(5));
DelegateWithTwoIntParameters delWithTwoIntParam = (int x, int y) =>
{
Console.WriteLine("\nUsing lambda expression with two parameters.");
Console.WriteLine("It is called a statement lambda because it has a block of statements in it's body.");
Console.WriteLine("This lambda accepts two parameters.");
int sum = x + y;
Console.WriteLine("Sum of {0} and {1} is {2}", x, y, sum);
};
delWithTwoIntParam(10,20);
Console.ReadKey();
}
private static void Method1(int a, int b)
{
Console.WriteLine("\nThis is Method1() without lambda expression.");
int sum = a + b;
Console.WriteLine("Sum of {0} and {1} is {2}", a, b, sum);
}
}
}
输出
以下是运行该程序的输出。
***Experimenting lambda expressions with different parameters.***
This is Method1() without lambda expression.
Sum of 5 and 10 is 15
Using lambda expression with no parameter, printing Hello
Using a lambda expression with one parameter, square of 5 is 25
Using lambda expression with two parameters.
It is called a statement lambda because it has a block of statements in it's body.
This lambda accepts two parameters.
Sum of 10 and 20 is 30
Lambda 表达式的类型
理想情况下,lambda 表达式用于单行方法。但是在演示 2 中,您看到了 lambda 表达式可以不止一行。
在编程术语中,您将 lambda 表达式分为表达式 lambdas 和语句 lambdas。表达式 lambda 只有一个表达式,而语句 lambda 包含一组语句。语句 lambdas 可以使用花括号、分号和 return 语句。一个语句 lambda 可以包含任意数量的语句,但一般来说,它们包含两个或三个语句。如果在一个 lambda 表达式中使用三行以上,可能会使理解变得复杂;在这些情况下,你可能更喜欢普通的方法而不是 lambda 表达式。
表情丰富的成员
Lambda 表达式最早出现在 C# 3.0 中,但从 C# 6.0 开始,它们提供了额外的灵活性:如果你在一个类中有一个非 lambda 方法,你可以使用相同的表达式语法来定义相同的方法。例如,在下面的演示中,有一个名为 Test 的类。
class Test
{
public int CalculateSum1(int a, int b)
{
int sum = a + b;
return sum;
}
// Expression-bodied method is not available in C#5
public int CalculateSum2(int a, int b) => a + b; // ok
}
注意非 lambda 方法CalculateSum1
。这是一个简单的方法,接受两个整数,计算它们的和,并返回结果(也是一个整数)。
从 C# 6.0 开始,您可以编写 lambda 表达式来定义 CalculateSum1 的等效版本。下面是这样的表达。
public int calculatesum 2(int a,int b)=>a+b;
如果您在 C# 6.0 之前的 C# 版本中使用它(比如在 C# 5.0 中),您会得到下面的编译时错误。
CS8026: Feature 'expression-bodied method' is not available in C# 5\. Please use language version 6 or greater.
图 3-5 是一个 Visual Studio IDE 截图,供大家参考。
图 3-5
C# 5 中没有“表达式主体方法”功能
我保留了这些评论来帮助你理解。但是需要注意的是,当您的方法可以用单个表达式表示时(即,在方法实现中只有一行代码),您可以使用这个概念。换句话说,它适用于表达式λ语法,但不能用于语句λ语法。在演示 3 中,如果取消对以下代码段的注释,就会出现编译时错误。
//int CalculateSum3(int a, int b) =>{
// int sum = a + b;
// return sum;
//}
演示 3
这个完整的演示展示了表达式体方法的使用。
using System;
namespace ExpressionBodiedMethodDemo
{
class Test
{
public int CalculateSum1(int a, int b)
{
int sum = a + b;
return sum;
}
/*
Expression-bodied method is not available in C#5.
C#6.0 onwards,you can use same expression syntax to define a non-lambda method within a class
It is ok for single expression, i.e. for
expression lambda syntax,but not for statement lambda.
*/
public int CalculateSum2(int a, int b) => a + b;//ok
// Following causes compile-time error
// For expression-bodied methods, you cannot use
// statement lambda
//int CalculateSum3(int a, int b) =>{
// int sum = a + b;
// return sum;
//}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Experimenting lambda expression with expression-bodied method.***\n");
// Using Normal method
Test test = new Test();
int result1 = test.CalculateSum1(5, 7);
Console.WriteLine("\nUsing a normal method, CalculateSum1(5, 7) results: {0}", result1);
// Using expression syntax
int result2 = test.CalculateSum2(5, 7);
Console.WriteLine("\nUsing expression syntax for CalculateSum2(5,7),result is: {0}", result2);
Console.ReadKey();
}
}
}
输出
以下是运行该程序的输出。
***Experimenting lambda expression with expression-bodied method.***
Using a normal method, CalculateSum1(5, 7) results: 12
Using expression syntax for CalculateSum2(5,7),result is: 12
Points to Remember
定义非 lambda 方法的表达式语法不适用于语句 lambdas 。你只能对表情 lambdas 使用它。
演示 4
演示 3 向您展示了表达式体方法的使用,但它也适用于属性、构造函数和终结器。在演示 4 中,您将看到它是如何与构造函数、只读属性和读写属性一起使用的。所以,让我们把重点放在重要的代码段上,并将它们与通常的实现进行比较。
假设您有一个Employee
类,其中有雇员 ID、公司名称和雇员姓名。在代码中,我将它们分别表示为empId
、company
和name
。当你初始化一个Employee
对象时,你提供了empId
。Company
是只读属性,Name
是读写属性。
下面是公共构造函数的通常实现,它只有一个参数。
public Employee(int id)
{
empId = id;
}
下面显示了表达式主体构造函数。
public Employee(int id) => empId = id;
下面是只读属性Company
的通常实现。
public string Company
{
get
{
return company;
}
}
下面显示了只读属性的表达式体定义。
public string Company => company;
下面是读写Name
属性的通常实现。
public string Name
{
get
{
return name;
}
set
{
name = value;
}
}
下面显示了读写属性的表达式体定义。
public string Name
{
get => name;
set => name = value;
}
我们来看一下完整的演示和输出,如下所示。
using System;
namespace Expression_BodiedPropertiesDemo
{
class Employee
{
private int empId;
private string company = "XYZ Ltd.";
private string name = String.Empty;
//Usual implementation of a constructor.
//public Employee(int id)
//{
// empId = id;
//}
//Following shows an expression-bodied constructor
public Employee(int id) => empId = id;//ok
//Usual implementation of a read-only property
//public string Company
//{
// get
// {
// return company;
// }
//}
//Read-only property.C#6.0 onwards.
public string Company => company;
//Usual implementation
//public string Name
//{
// get
// {
// return name;
// }
// set
// {
// name = value;
// }
//}
//C#7.0 onwards , we can use expression-body definition for the get //and set accessors.
public string Name
{
get => name;
set => name = value;
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Experimenting lambda expressions with expression-bodied properties.***");
Employee empOb = new Employee(1);
//Error.Company is read-only
//empOb.Company = "ABC Co.";
empOb.Name = "Rohan Roy ";//ok
Console.WriteLine("{0} works in {1} as an employee.", empOb.Name,empOb.Company);
Console.ReadKey();
}
}
}
输出
以下是运行该程序的输出。
***Experimenting lambda expressions with expression-bodied properties.***
Rohan Roy works in XYZ Ltd. as an employee.
Points to Remember
在 C# 6.0 中,我们得到了对表达式体方法和只读属性的支持。在 C# 7.0 中,这种支持扩展到了属性、索引器、构造函数和终结器。
Lambda 表达式中的局部变量
你可能已经注意到局部变量在 lambda 表达式中的使用。在这种情况下,变量必须在范围内。演示 5 展示了 lambda 表达式中局部变量的简单用法。
演示 5
该演示将您的注意力吸引到以下几点。
-
您可以在程序中使用查询语法或方法调用语法。我已经展示了两者的用法。(如果你熟悉 LINQ 编程,你知道查询语法;否则,您可以跳过这段代码,直到您了解它。)考虑下面的代码,尤其是粗体部分:
IEnumerable<int> numbersAboveMidPoint = intList.Where(x => x > midPoint);
-
midPoint
是一个局部变量。lambda 表达式可以访问此变量,因为它在此位置的范围内。 -
本例中使用了
List<int>
和IEnumerable<int>
。它们是泛型编程中最简单的构造。如果你是泛型的新手,你可以暂时跳过这个例子,在第四章讲述泛型编程后再回来。
让我们来看一下演示。
using System;
using System.Collections.Generic;
using System.Linq;
namespace TestingLocalVariableScopeUsingLambdaExpression
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Testing local variable scope with a lambda expression.***\n");
#region Using query syntax
/* Inside lambda Expression,you can access the variable that are in scope (at that location).*/
int midPoint = 5;
List<int> intList = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
var myQueryAboveMidPoint = from i in intList
where i > midPoint
select i;
Console.WriteLine("Numbers above mid point(5) in intList are as follows:");
foreach (int number in myQueryAboveMidPoint)
{
Console.WriteLine(number);
}
#endregion
#region Using method call syntax
// Alternative way( using method call syntax)
Console.WriteLine("Using a lambda expression, numbers above mid point(5) in intList are as follows:");
IEnumerable<int> numbersAboveMidPoint = intList.Where(x => x > midPoint);
foreach (int number in numbersAboveMidPoint)
{
Console.WriteLine(number);
}
#endregion
Console.ReadKey();
}
}
}
输出
以下是运行该程序的输出。
***Testing local variable scope with a lambda expression.***
Numbers above mid point(5) in intList are as follows:
6
7
8
9
10
Using a lambda expression, numbers above mid point(5) in intList are as follows:
6
7
8
9
10
在 Lambda 表达式中使用元组
从 C# 7.0 开始,就有了对元组的内置支持。在许多应用中,元组有内置的委托(例如,Func
、Action
等)。)和 lambda 表达式。你将在第四章中了解内置代理。现在,让我们在用户定义的委托的上下文中使用元组。
在下面的例子中,我将一个元组传递给一个方法。为了简单起见,我们假设元组只有两个组件。你想把这个元组传递给一个方法参数,反过来,你想得到一个元组,在这个元组中你得到每个组件的双精度值。下面的方法代表了这样一个示例。
static Tuple<int, double> MakeDoubleMethod(Tuple<int, double> input)
{
return Tuple.Create(input.Item1 * 2, input.Item2 * 2);
}
可以看到在 tuple 内部,第一个组件是一个int
,第二个是一个double
。我只是将输入参数乘以 2,以获得每个组件的 double 值,并返回包含另一个元组的结果。
在 Main 方法中,我如下调用了这个方法。
var resultantTuple = MakeDoubleMethod(inputTuple);
因为我是从静态上下文中调用方法,所以我将MakeDoubleMethod
设为静态。
现在你知道如何在一个方法中使用元组了。让我们使用 lambda 表达式来实现这个概念。
首先,声明一个委托,如下所示。
delegate Tuple<int, double> MakeDoubleDelegate(Tuple<int, double> input);
现在您有了委托,所以您可以使用 lambda 表达式,如下所示。
MakeDoubleDelegate delegateObject =
(Tuple<int, double> input) => Tuple.Create(input.Item1 * 2, input.Item2 * 2);
如果不使用命名组件,默认情况下,元组的字段被命名为Item1
、Item2
、Item3
等等。要获得预期的结果,可以使用下面几行代码。
var resultantTupleUsingLambda= delegateObject(inputTuple);
Console.WriteLine("Using lambda expression, the content of resultant tuple is as follows:");
Console.WriteLine("First Element: " + resultantTupleUsingLambda.Item1);
Console.WriteLine("Second Element: " + resultantTupleUsingLambda.Item2);
像本书中的许多其他例子一样,我保留了两种方法来获得预期的结果。它有助于比较 lambda 表达式的使用和类似上下文中的普通方法。接下来是完整的演示。
演示 6
using System;
namespace UsingTuplesInLambdaExp
{
delegate Tuple<int, double> MakeDoubleDelegate(Tuple<int, double> input);
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Using Tuples in Lambda Expression.***");
var inputTuple = Tuple.Create(1, 2.3);
Console.WriteLine("Content of input tuple is as follows:");
Console.WriteLine("First Element: " + inputTuple.Item1);
Console.WriteLine("Second Element: " + inputTuple.Item2);
var resultantTuple = MakeDoubleMethod(inputTuple);
Console.WriteLine("\nPassing tuple as an input argument in a normal method which again returns a tuple.");
Console.WriteLine("Content of resultant tuple is as follows:");
Console.WriteLine("First Element: " + resultantTuple.Item1);
Console.WriteLine("Second Element: " + resultantTuple.Item2);
Console.WriteLine("\nUsing delegate and lambda expression with tuple now.");
MakeDoubleDelegate delegateObject =
(Tuple<int, double> input) => Tuple.Create(input.Item1 * 2, input.Item2 * 2);
var resultantTupleUsingLambda= delegateObject(inputTuple);
Console.WriteLine("Using lambda expression, the content of resultant tuple is as follows:");
Console.WriteLine("First Element: " + resultantTupleUsingLambda.Item1);
Console.WriteLine("Second Element: " + resultantTupleUsingLambda.Item2);
Console.ReadKey();
}
static Tuple<int, double> MakeDoubleMethod(Tuple<int, double> input)
{
return Tuple.Create(input.Item1 * 2, input.Item2 * 2);
}
}
}
输出
以下是运行该程序的输出。
***Using Tuples in Lambda Expression.***
Content of input tuple is as follows:
First Element: 1
Second Element: 2.3
Passing tuple as an input argument in a normal method which again returns a tuple.
Content of resultant tuple is as follows:
First Element: 2
Second Element: 4.6
Using delegate and lambda expression with tuple now.
Using lambda expression, the content of resultant tuple is as follows:
First Element: 2
Second Element: 4.6
带有 Lambda 表达式的事件订阅
可以对事件使用 lambda 表达式。
演示 7
为了演示一个案例,我们来看一下第二章的第一个程序,并对其进行修改。由于我们关注的是 lambda 表达式,这一次,您不需要创建一个Receiver
类,它有一个名为GetNotificationFromSender
,
的方法,用于在myInt
在Sender
类对象中发生变化时处理事件通知。在那个例子中,Sender
类也有一个GetNotificationItself
方法来处理它自己的事件。它向您展示了一个Sender
类也可以处理自己的事件。
这是完整的演示。
using System;
namespace UsingEventsAndLambdaExp
{
class Sender
{
private int myInt;
public int MyInt
{
get
{
return myInt;
}
set
{
myInt = value;
//Whenever we set a new value, the event will fire.
OnMyIntChanged();
}
}
// EventHandler is a predefined delegate which is used to handle //simple events.
// It has the following signature:
//delegate void System.EventHandler(object sender,System.EventArgs e)
//where the sender tells who is sending the event and
//EventArgs is used to store information about the event.
public event EventHandler MyIntChanged;
public void OnMyIntChanged()
{
if (MyIntChanged != null)
{
MyIntChanged(this, EventArgs.Empty);
}
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Demonstration-.Exploring events with lambda expression.***");
Sender sender = new Sender();
//Using lambda expression as an event handler
//Bad practise
//sender.MyIntChanged += (Object sender, System.EventArgs e) =>
// Console.WriteLine("Using lambda expression, inside Main method, received a notification: Sender recently has changed the myInt value . ");
//Better practise
EventHandler myEvent =
(object sender, EventArgs e) =>
Console.WriteLine("Using lambda expression, inside Main method, received a notification: Sender recently has changed the myInt value . ");
sender.MyIntChanged += myEvent;
sender.MyInt = 1;
sender.MyInt = 2;
//Unregistering now
//sender.MyIntChanged -= receiver.GetNotificationFromSender;
//No notification sent for the receiver now.
//but there is no guarantee if you follow the bad practise
//sender.MyIntChanged -= (Object sender, System.EventArgs e) =>
// Console.WriteLine("Unregistered event notification.");
//But now it can remove the event properly.
sender.MyIntChanged -= myEvent;
sender.MyInt = 3;
Console.ReadKey();
}
}
}
输出
以下是运行该程序的输出。
***Demonstration-.Exploring events with lambda expression.***
Using lambda expression, inside Main method, received a notification: Sender recently has changed the myInt value .
Using lambda expression, inside Main method, received a notification: Sender recently has changed the myInt value .
问答环节
3.1 你为什么要编写额外的代码?我发现你可以简单地写下下面的内容来订阅这个活动。
sender.MyIntChanged += (Object sender, System.EventArgs e) =>
Console.WriteLine("Using lambda expression, inside Main method, received a notification: Sender recently has changed the myInt value . ");
你可以用它来替换下面几行。
EventHandler myEvent = (object sender, EventArgs e) =>
Console.WriteLine("Using lambda expression, inside Main method, received a notification: Sender recently has changed the myInt value . ");
sender.MyIntChanged += myEvent;
这是正确的吗?
接得好,但这是必要的。假设您使用以下代码行来订阅事件。
sender.MyIntChanged += (Object sender, System.EventArgs e) =>
Console.WriteLine("Using lambda expression, inside Main method, received a notification: Sender recently has changed the myInt value . ");
然后用下面一行取消订阅。
sender.MyIntChanged -= (Object sender, System.EventArgs e) =>
Console.WriteLine("Unregistered event notification.");
不能保证编译器会取消订阅正确的事件。例如,在这种情况下,当我执行程序时,我注意到输出中的第三种情况是不需要的(因为在我将myInt
的值设置为 3 之前,我想取消订阅事件通知)。以下是输出。
***Demonstration-.Exploring events with lambda expression.***
Using lambda expression, inside Main method, received a notification: Sender recently has changed the myInt value .
Using lambda expression, inside Main method, received a notification: Sender recently has changed the myInt value .
Using lambda expression, inside Main method, received a notification: Sender recently has changed the myInt value .
所以,专家建议,在这种情况下,应该将匿名方法/lambda 表达式存储在一个委托变量 、 中,然后将这个委托添加到事件中。这样一来,你就可以随时关注它 , 并且如果你愿意,你可以适当地取消订阅**。当您希望以后取消订阅某个事件时,通常建议不要使用匿名函数来订阅该事件。这是因为,为了避免现实应用中的内存泄漏,一旦您订阅了一个事件,您就应该在预期的工作完成后取消订阅。
*3.2 什么是表达式?
根据微软的说法,表达式可以是运算符和操作数的组合。它可以计算为单个值、方法、对象或命名空间。表达式可以包括方法调用、带有操作数的运算符、文本值(文本是没有名称的常量值),或者只是变量、类型成员、方法参数、命名空间或类型的名称。
下面是一个简单的表达式语句示例。
int i=1;
这里,i
是一个简单的名字,1 是字面值。文字和简单名称是两种最简单的表达式。
3.3 什么是图灵机?
图灵机是一种抽象机器,它可以通过遵循规则来操纵磁带的符号。它是许多编程语言的数学基础。
3.4 语句 lambda 和表达式 lambda 有什么区别?
表达式 lambda 只有一个表达式,但语句 lambda 在 lambda 运算符的右侧有一个语句块。使用语句 lambda,可以在花括号中包含任意数量的语句。在撰写本文时,您不能将语句 lambda 用于表达式体方法,但是您可以在这些上下文中使用表达式 lambda。
在表达式树中,只能使用表达式 lambdas 但是你不能在这些上下文中使用 lambda 语句。我排除了对表达式树的讨论,因为它是与 LINQ 相关的特性,超出了本书的范围。
3.5 如果不提供参数,编译器仍然可以确定类型。但是当您提供它们时,它们必须匹配委托类型。这是正确的吗?
是的。但有时编译器无法推断出来。在这种情况下,您需要提供参数。在这种情况下,您需要记住输入参数必须是隐式的或显式的。
3.6 你说输入参数必须是隐式的或者显式的。这是什么意思?
让我们假设您有以下委托。
delegate string MyDelegate(int a, int b);
如果你写了下面这段代码,你会得到一个编译时错误,如图 3-6 所示。
图 3-6
由于 lambda 参数用法不一致而导致的编译时错误
MyDelegate resultOfLambdaExp =(int x, y)=> (x > y) ? "Yes." : "No.";
补救方法如下。
MyDelegate resultOfLambdaExp =(int x, int y)=> (x > y) ? "Yes." : "No.";
或者,您可以删除这两个 int,如下所示。
MyDelegate resultOfLambdaExp =( x, y)=> (x > y) ? "Yes." : "No.";
3.7 lambda 表达式有什么限制?
Lambda 表达式是匿名方法的超集。适用于匿名方法的所有限制也适用于 lambda 表达式(例如,在匿名方法的上下文中,不能使用定义方法的 ref 或 out 参数)。作为参考,请记住以下几点。
-
在“is”或“as”运算符的左侧不允许出现 Lambdas。在这种情况下,您可能还记得 C# 6.0 规范中的语句,它说,“匿名函数本身没有值或类型,但可以转换为兼容的委托或表达式树类型。”
-
不能使用
break
、goto
或continue
跳出 lambda 表达式范围。 -
不能在 lambda 表达式中使用
unsafe
代码。例如,假设您有以下委托:
delegate void DelegateWithNoParameter();
如果您编写以下代码段,您会在所有操作指针的地方(我用注释标记了这些地方)得到一个编译时错误。
DelegateWithNoParameter delOb = () =>
{
int a = 10;
//CS 0214:Pointers and fixed sized buffers may //only be used only in an unsafe context
int* p = &a;//Error
//Console.WriteLine("a={0}", a);
//Printing using string interpolation
Console.WriteLine($"a={a}");
Console.WriteLine($"*p={*p}");//Error CS0214
};
3.8 你说过在匿名方法的上下文中,不能使用定义方法的 ref 或 out 参数。你能详细说明一下吗?
让我们考虑一下演示 1 中的 Sum 方法,并将其修改如下。
public static int Sum(ref int a, ref int b)
{
//return a + b;
//Using Anonymous method(C# 2.0 onwards)
Mydel del2 = delegate (int x, int y)
{
//Following segment will NOT work
x = a;//CS1628
y = b;//CS1628
return x + y;
//Following segment will work
//return x + y;
};
return del2(a, b);
}
Where the Mydel delegate is unchanged and as follows:
public delegate int Mydel(int x, int y);
对于这段代码,您将得到一个编译时错误(标记为 CS1628)。CS1628 指出,在匿名方法或 lambda 表达式中,不能在参数中使用 ref、out、或。图 3-7 是 Visual Studio 2019 错误截图,供大家参考。
图 3-7
编译时错误。不能在匿名方法或 lambda 表达式中使用 ref、out 或 in 参数
您可以参考前面代码段中使用注释行显示的潜在解决方案。
最后的话
目前关于兰姆达斯就这些了。在你离开这一章之前,我想提醒你,尽管很酷的特性很容易使用,但是代码的可读性和可理解性应该是你最优先考虑的。
接下来,我们转到本书的第二部分(从第四章开始),在这里你可以看到目前为止你所学概念的用法。虽然第二部分是这本书的核心,但第一部分的内容(第 1 、 2 和 3 章)是它们的组成部分。
摘要
本章讨论了以下关键问题。
-
为什么匿名方法和 lambda 表达式有用?
-
如何将匿名方法转换成 lambda 表达式?
-
如何使用 lambda 表达式来接受不同数量的参数?
-
如何在 lambda 表达式中使用局部变量?
-
什么是表达式λ?
-
什么是语句 lambda?
-
如何使用表达式语法来定义非 lambda 方法?以及它的使用限制是什么?
-
适用于 lambda 表达式的关键限制是什么?*
四、泛型编程
在这一章中,你将学习泛型编程,并了解 C# 最酷的特性之一泛型。它是高级编程不可或缺的一部分。泛型编程仅仅意味着泛型的有效使用。它最早出现在 C# 2.0 中。随着时间的推移,这个强大的特性增加了额外的灵活性,现在,您会发现现实生活中很少有应用的核心不使用泛型。
泛型背后的动机
当您在应用中使用泛型类型时,您不必为实例提交特定的类型。例如,当您实例化一个泛型类时,您可以说您希望您的对象处理 int 类型,但在另一个时候,您可以说您希望您的对象处理 double 类型、string 类型、object 类型等等。简而言之,这种编程允许您创建一个类型安全的类,而不必提交任何特定的类型。
这不是什么新概念,也绝对不局限于 C#。在其他语言中也可以看到类似的编程,例如 Java 和 C++(使用模板)。以下是使用通用应用的一些优点。
-
你的程序是可重用的。
-
更好的类型安全丰富了你的程序。
-
您的程序可以避免典型的运行时错误,这些错误可能是由于不正确的类型转换引起的。
为了解决这些问题,我将从一个简单的非泛型程序开始,并分析其潜在的缺点。之后,我会向您展示一个相应的泛型程序,并进行对比分析,以发现泛型编程的优势。我们开始吧。
演示 1
演示 1 有一个名为NonGenericEx
的类。这个类有两个实例方法:DisplayMyInteger
和DisplayMyString
。
public int DisplayMyInteger(int myInt)
{
return myInt;
}
public string DisplayMyString(string myStr)
{
return myStr;
}
你有没有注意到这两个方法基本上在做同样的操作,但是一个方法在处理一个int
而另一个方法在处理一个string
?这种方法不仅难看,而且还有另一个潜在的缺点,您将在分析部分看到这一点。但是在我们分析它之前,让我们执行程序。
using System;
namespace NonGenericProgramDemo1
{
class NonGenericEx
{
public int DisplayMyInteger(int myInt)
{
return myInt;
}
public string DisplayMyString(string myStr)
{
return myStr;
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***A non-generic program demonstration.***");
NonGenericEx nonGenericOb = new NonGenericEx();
Console.WriteLine("DisplayMyInteger returns :{0}", nonGenericOb.DisplayMyInteger(123));
Console.WriteLine("DisplayMyString returns :{0}", nonGenericOb.DisplayMyString("DisplayMyString method inside NonGenericEx is called."));
Console.ReadKey();
}
}
}
输出
这是输出。
***A non-generic program demonstration.***
DisplayMyInteger returns :123
DisplayMyString returns :DisplayMyString method inside NonGenericEx is called.
分析
让我们假设现在您需要处理另一个数据类型—一个double
。使用当前代码,在Main
中添加下面一行。
Console.WriteLine("ShowDouble returns :{0}", nonGenericOb.DisplayMyDouble(25.5));//error
您会得到下面的编译时错误。
Error CS1061 'NonGenericEx' does not contain a definition for 'DisplayMyDouble' and no accessible extension method 'DisplayMyDouble' accepting a first argument of type 'NonGenericEx' could be found (are you missing a using directive or an assembly reference?)
这是因为您还没有一个DisplayMyDouble
方法。同时,您不能使用任何现有的方法来处理double
数据类型。一个显而易见的方法是引入如下所示的方法。
public double DisplayMyDouble(double myDouble)
{
return myDouble;
}
但是你能忍受多久呢?如果您的代码大小对于所有其他数据类型都以同样的方式增长,那么您的代码将不能被不同的数据类型重用。与此同时,随着代码的增长,它看起来会很难看,整体的维护会变得非常繁忙。幸运的是,当你喜欢泛型编程胜过非泛型编程时,你有一个简单的解决方案。
首先,以下是你应该记住的要点。
-
泛型类和方法提高了可重用性、类型安全性和效率。它们的非通用对应物不具备这些品质。您经常会看到泛型与集合以及处理它们的方法一起使用。
-
那个。NET Framework 类库包含一个
System.Collections.Generic
名称空间,其中有几个基于泛型的集合类。此命名空间是在 2.0 版中添加的。这就是为什么微软建议任何以。NET Framework 2.0(或更高版本)应该使用泛型集合类,而不是它们的非泛型对应物,比如ArrayList
。 -
尖括号<>用于通用程序。泛型放在尖括号中;比如在你的类定义中的
。当您只处理单个泛型类型时,t 是表示泛型类型的最常见的单个字母。 -
在泛型程序中,可以用占位符定义一个类的方法、字段、参数等类型。稍后,这些占位符将被替换为您想要使用的特定类型。
-
Here is the simple generic class used in demonstration 2:
class GenericClassDemo<T> { public T Display(T value) { return value; } }
T
称为泛型类型参数。The following is an example of instantiation from a generic class:
GenericClassDemo<int> myGenericClassIntOb = new GenericClassDemo<int>();
注意,在这种情况下,类型参数被替换为
int
。 -
您可能会注意到在一个特定的声明中有多个泛型类型参数。例如,下面的类有多个泛型类型:
public class MyDictionary<K,V>{//Some code}
-
泛型方法可能使用其类型参数作为其返回类型。它还可以将类型参数用作形参的类型。在
GenericClassDemo<T>
类中,Display
方法使用 T 作为返回类型。该方法还使用 T 作为其形参的类型。 -
您可以对泛型类型施加约束。这将在本章后面探讨。
现在进行演示 2。
演示 2
演示 2 是一个简单的通用程序。在实例化泛型类之前,需要指定用类型参数替换的实际类型。在这个演示中,下面几行代码在Main
中。
GenericClassDemo<int> myGenericClassIntOb = new GenericClassDemo<int>();
GenericClassDemo<string> myGenericClassStringOb = new GenericClassDemo<string>();
GenericClassDemo<double> myGenericClassDoubleOb = new GenericClassDemo<double>();
这三行代码告诉你,第一行用一个int
替代类型参数;第二行用一个string;
代替类型参数,第三行用一个double
代替类型参数。
当您进行这种编码时,该类型会在它出现的任何地方替换类型参数。因此,您会得到一个基于您选择的类型构造的类型安全类。当您选择一个int
类型并使用下面的代码行时,
GenericClassDemo<int> myGenericClassIntOb = new GenericClassDemo<int>();
您可以使用下面的代码行从Display
方法中获取一个int
。
Console.WriteLine("Display method returns :{0}", myGenericClassIntOb.Display(1));
这是完整的演示。
using System;
namespace GenericProgramDemo1
{
class GenericClassDemo<T>
{
public T Display(T value)
{
return value;
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Introduction to Generic Programming.***");
GenericClassDemo<int> myGenericClassIntOb = new GenericClassDemo<int>();
Console.WriteLine("Display method returns :{0}", myGenericClassIntOb.Display(1));
GenericClassDemo<string> myGenericClassStringOb = new GenericClassDemo<string>();
Console.WriteLine("Display method returns :{0}", myGenericClassStringOb.Display("A generic method is called."));
GenericClassDemo<double> myGenericClassDoubleOb = new GenericClassDemo<double>();
Console.WriteLine("Display method returns :{0}", myGenericClassDoubleOb.Display(12.345));
Console.ReadKey();
}
}
}
输出
这是输出。
***Introduction to Generic Programming.***
Display method returns :1
Display method returns :A generic method is called.
Display method returns :12.345
分析
让我们对演示 1(非泛型程序)和演示 2(泛型程序)进行比较分析。这两个程序执行相同的操作,但是它们之间有一些关键的区别,如下所示。
-
在演示 1 中,您需要指定诸如
DisplayInteger
、DisplayString
、DisplayDouble
等方法来处理数据类型。但是在演示 2 中,只有一个通用的Display
方法足以处理不同的数据类型,并且您可以用更少的代码行完成这项任务。 -
在演示 1 中,当
Main
中没有DisplayDouble
方法时,当我们想要处理double
数据类型时,我们遇到了一个编译时错误。但是在演示 2 中,不需要定义任何额外的方法来处理 double 数据类型(或任何其他数据类型)。所以,你可以看到这个通用版本比非通用版本更灵活。
现在考虑演示 3。
演示 3
这个演示展示了一个使用ArrayList
类的非泛型程序。一个ArrayList
的大小可以动态增长。它有一个叫做Add
的方法,可以帮助你在ArrayList
的末尾添加一个对象。在接下来的演示中,我使用了以下代码行。
myList.Add(1);
myList.Add(2);
// No compile time error
myList.Add("InvalidElement");
因为该方法需要对象作为参数,所以这些行被成功编译。但是如果您使用下面的代码段获取数据,您将会面临这个问题。
foreach (int myInt in myList)
{
Console.WriteLine((int)myInt); //downcasting
}
第三个元素不是 int(它是一个字符串),因此您会遇到一个运行时错误。运行时错误比编译时错误更糟糕,因为在这个阶段,您几乎不能做任何有成效的事情。
这是完整的演示。
using System;
using System.Collections;
namespace NonGenericProgramDemo2
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Use Generics to avoid runtime error***");
ArrayList myList = new ArrayList();
myList.Add(1);
myList.Add(2);
//No compile time error
myList.Add("InvalidElement");
foreach (int myInt in myList)
{
/*Will encounter run-time exception for the final element which is not an int */
Console.WriteLine((int)myInt); //downcasting
}
Console.ReadKey();
}
}
}
输出
该程序不会引发任何编译时错误,但是在运行时,您会看到如图 4-1 所示的异常。
图 4-1
出现运行时错误 InvalidCastException
现在你明白你遇到这个运行时错误是因为第三个元素(即ArrayList
中的myList [2]
)应该是一个 int,但是我存储了一个 string。在编译时,我没有遇到任何问题,因为它是作为对象存储的。
分析
由于装箱和向下转换,前面的演示也存在性能开销。
快速浏览列表类
在你进一步深入之前,让我们快速看一下内置的List
类。这个类很常见,应用也很广泛。它是为泛型设计的,所以当你实例化一个List
类时,你可以在你的列表中提到你想要的类型。例如,在下面
List<int> myList = new List<int>(); contains a list of ints.
List<double> myList = new List<double>(); contains a list of doubles.
List<string> myList = new List<string>(); contains a list of strings
List
类有许多内置方法。我建议你浏览一下。这些现成的构造使您的编程生活更加容易。现在,让我们使用Add
方法。使用这种方法,您可以将项目添加到列表的末尾。
这是来自 Visual IDE 的方法说明。
//
// Summary:
// Adds an object to the end of the System.Collections.Generic.List`1.
//
// Parameters:
// item:
// The object to be added to the end of the // System.Collections.Generic.List`1\. The value can be null // for reference types.
public void Add(T item);
下面的代码段创建了一个int
列表,然后向其中添加了两个条目。
List<int> myList = new List<int>();
myList.Add(10);
myList.Add(20);
现在来看重要的部分。如果您错误地将一个string
添加到这个列表中,就会得到一个编译时错误。
这是错误的代码段。
//Compile time error: Cannot convert from 'string' to 'int'
//myList.Add("InvalidElement");//error
演示 4
为了与演示 3 进行比较,在下面的例子中,让我们使用List<int>
而不是ArrayList
,然后回顾我们到目前为止讨论过的概念。
这是完整的程序。
using System;
using System.Collections.Generic;
namespace GenericProgramDemo2
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Using Generics to avoid run-time error.***");
List<int> myList = new List<int>();
myList.Add(10);
myList.Add(20);
//Cannot convert from 'string' to 'int'
myList.Add("InvalidElement");//Compile-time error
foreach (int myInt in myList)
{
Console.WriteLine((int)myInt);//downcasting
}
Console.ReadKey();
}
}
}
输出
在这个程序中,您会得到以下编译时错误
CS1503 Argument 1: cannot convert from 'string' to 'int'
下面的代码行。
myList.Add("InvalidElement");
您不能在myList
中添加一个string
,因为它只用于保存整数(注意我使用的是List<int>
)。因为错误是在编译时捕获的,所以您不需要等到运行时才捕获这个缺陷。
一旦注释掉错误的行,就可以编译这个程序并生成以下输出。
***Using Generics to avoid run-time error.***
1
2
分析
当您比较演示 3 和演示 4 时,您会发现
-
为了避免运行时错误,您应该更喜欢通用版本,而不是它的对应物—非通用版本。
-
泛型编程有助于避免装箱/拆箱带来的损失。
-
为了存储字符串,您可以使用类似于
List<string> myList2 = new List<string>();
的东西来创建一个只保存字符串类型的列表。类似地,List可以用于其他数据类型。这说明 List<T>
版本比非通用版本ArrayList
更灵活、更实用。
通用委托
在第一章中,你学习了用户定义的代理及其重要性。现在,让我们讨论泛型委托。在这一节中,我将介绍三个重要的内置泛型委托——称为Func
、Action
和Predicate
,它们在泛型编程中非常常见。我们开始吧。
功能委托
Func
委托有 17 个重载版本。它们可以接受 0 到 16 个输入参数,但总是有一个返回类型。举个例子,
Func<out TResult>
Func<in T, out TResult>
Func<in T1, in T2,out TResult>
Func<in T1, in T2, in T3, out TResult>
......
Func<in T1, in T2, in T3,in T4, in T5, in T6,in T7,in T8,in T9,in T10,in T11,in T12,in T13,in T14,in T15,in T16, out TResult>
为了理解用法,让我们考虑下面的方法。
private static string DisplayEmployeeDetails(string name, int empId, double salary)
{
return string.Format("Employee Name:{0},id:{1}, salary:{2}$", name, empId,salary);
}
若要使用自定义委托调用此方法,可以按照下列步骤操作。
-
定义一个代表(比如说,
Mydel
);大概是这样的:public delegate string Mydel(string n, int r, double d);
-
创建一个委托对象并使用代码指向该方法;类似于以下内容:
Mydel myDelOb = new Mydel(DisplayEmployeeDetails); Or in short, Mydel myDelOb = DisplayEmployeeDetails;
-
像这样调用方法:
myDelOb.Invoke("Amit", 1, 1025.75);
Or, simply with this:
myDelOb("Amit", 1, 1025.75);
如果您使用内置的Func
委托,您可以使您的代码更简单、更短。在这种情况下,您可以如下使用它。
Func<string, int, double, string> empOb = new Func<string, int, double,string>(DisplayEmployeeDetails);
Console.WriteLine(empOb("Amit", 1,1025.75));
Func
委托完美地考虑了所有三个输入参数(分别是一个string
、一个int
和一个double
)并返回一个string
。您可能会感到困惑,想知道哪个参数表示返回类型。如果在 Visual Studio 中将光标移到它上面,可以看到最后一个参数(TResult
)被认为是函数的返回类型,其他的被认为是输入类型(见图 4-2 )。
图 4-2
Func
Note
输入和输出参数的魔力将很快向您展示。
问答环节
4.1 在前面的代码段中, DisplayEmployeeDetails
有三个参数,其返回类型为 string
。通常,我有不同的方法可以接受不同数量的输入参数。我如何在那些上下文中使用 Func
?
Func 委托可以考虑 0 到 16 个输入参数。可以使用适合自己需求的重载版本。例如,如果你有一个方法,它接受一个 string 和一个 int 作为输入参数,并且它的返回类型是一个 string,那么这个方法就像下面这样。
private static string DisplayEmployeeDetailsShortForm(string name, int empId)
{
return string.Format("Employee Name:{0},id:{1}", name, empId);
}
您可以使用以下重载版本的 Func。
Func<string, int, string> empOb2 = new Func<string, int, string> (DisplayEmployeeDetailsShortForm);
Console.WriteLine(empOb2("Amit", 1));
动作代表
Visual studio 描述了有关操作委托的以下内容:
封装没有参数且不返回值的方法。
public delegate void Action();
但是通常你会注意到这个委托的通用版本,它可以接受 1 到 16 个输入参数,但是没有返回类型。重载版本如下。
Action<in T>
Action<in T1,in T2>
Action<in T1,in T2, in T3>
....
Action<in T1, in T2, in T3,in T4, in T5, in T6,in T7,in T8,in T9,in T10,in T11,in T12,in T13,in T14,in T15,in T16>
假设您有一个名为CalculateSumOfThreeInts
的方法,它将三个 int 作为输入参数,其返回类型为void
,如下所示。
private static void CalculateSumOfThreeInts(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>(CalculateSumOfThreeInts);
sum(10,3,7);
谓词委托
谓词委托计算一些东西。例如,假设您有一个定义了一些标准的方法,您需要检查一个对象是否满足标准。让我们考虑下面的方法。
private static bool GreaterThan100(int myInt)
{
return myInt > 100 ? true : false;
}
你可以看到这个方法计算一个 int 是否大于 100。因此,您可以使用谓词委托来执行相同的测试,如下所示。
Predicate<int> isGreater = new Predicate<int>(IsGreaterThan100);
Console.WriteLine("101 is greater than 100? {0}", isGreater(101));
Console.WriteLine("99 is greater than 100? {0}", isGreater(99));
演示 5
这是一个完整的程序,演示了到目前为止讨论的所有概念。
using System;
namespace GenericDelegatesDemo
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Using Generic Delegates.***");
// Func
Console.WriteLine("Using Func delegate now.");
Func<string, int, double,string> empOb = new Func<string, int, double,string>(DisplayEmployeeDetails);
Console.WriteLine(empOb("Amit", 1,1025.75));
Console.WriteLine(empOb("Sumit", 2,3024.55));
// Action
Console.WriteLine("Using Action delegate now.");
Action<int, int, int> sum = new Action<int, int, int>(CalculateSumOfThreeInts);
sum(10, 3, 7);
sum(5, 10, 15);
/*
Error:Keyword 'void' cannot be used in this context
//Func<int, int, int, void> sum2 = new Func<int, int, int, void>(CalculateSumOfThreeInts);
*/
// Predicate
Console.WriteLine("Using Predicate delegate now.");
Predicate<int> isGreater = new Predicate<int>(IsGreaterThan100);
Console.WriteLine("101 is greater than 100? {0}", isGreater(101));
Console.WriteLine("99 is greater than 100? {0}", isGreater(99));
Console.ReadKey();
}
private static string DisplayEmployeeDetails(string name, int empId, double salary)
{
return string.Format("Employee Name:{0},id:{1}, salary:{2}$", name, empId,salary);
}
private static void CalculateSumOfThreeInts(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 IsGreaterThan100(int input)
{
return input > 100 ? true : false;
}
}
}
输出
***Using Generic Delegates.***
Using Func delegate now.
Employee Name:Amit,id:1, salary:1025.75$
Employee Name:Sumit,id:2, salary:3024.55$
Using Action delegate now.
Sum of 10,3 and 7 is: 20
Sum of 5,10 and 15 is: 30
Using Predicate delegate now.
101 is greater than 100? True
99 is greater than 100? False
问答环节
我见过内置泛型委托的使用。如何使用我自己的泛型委托?
我使用了内置的泛型委托,因为它们让您的生活更轻松。没有人限制你使用自己的泛型委托。不过,我建议您在使用自己的委托之前,先遵循这些泛型委托的构造。例如,在前面的演示中,我使用了如下的动作委托。
Action<int, int, int> sum = new Action<int, int, int>(CalculateSumOfThreeInts);
sum(10, 3, 7);
现在,不使用内置委托,您可以定义自己的泛型委托(比如 CustomAction ),如下所示。
// Custom delegate
public delegate void CustomAction<in T1, in T2, in T3>(T1 arg1, T2 arg2, T3 arg3);
然后你可以像这样使用它。
CustomAction<int, int, int> sum2 = new CustomAction<int, int, int>(CalculateSumOfThreeInts);
sum2(10, 3, 7);
我发现当你创建代理实例时,你没有使用简写形式。有什么原因吗?
好发现。你总是可以使用简写形式。例如,不使用
Action<int, int, int> sum = new Action<int, int, int>(CalculateSumOfThreeInts);
我可以简单地使用
Action<int, int, int> sum = CalculateSumOfThreeInts;
但是由于您刚刚开始学习委托,这些长形式通常可以帮助您更好地理解代码。
4.4 我可以用 Func
委托指向一个返回 void 的方法吗?
当您的方法具有 void 返回类型时,建议您使用操作委托。如果您在前面的演示中错误地使用了下面的代码行,您会得到一个编译时错误,因为目标方法的返回类型是 void。
//Error:Keyword 'void' cannot be used in this context
Func<int, int, int, void> sum2 = new Func<int, int, int, void>(CalculateSumOfThreeInts);//error
4.5 我可以拥有泛型方法吗?
在演示 2 中,您看到了一个泛型方法,如下所示。
public T Display(T value)
{
return value;
}
它表明,当您有一组除了类型之外完全相同的方法时,您可以选择泛型方法。
例如,在演示 2 中,您已经看到我在调用时使用了相同的命名方法:Display(1)
、Display("A generic method is called.")
和Display(12.345)
。
泛型中的默认关键字
说明你已经看到了 default 关键字在switch
语句中的使用,其中 default 指的是一个默认情况。在泛型编程中,它有特殊的含义。您可以使用default
用默认值初始化泛型类型。在这种情况下,您可能会注意到以下几点。
-
参考类型的默认值是
null
。 -
值类型(struct 和 bool 类型除外)的默认值为 0 。
-
对于 bool 类型,默认值是
false
。 -
对于结构(是值类型)类型,默认值是该结构的对象,其中所有字段都设置有它们的默认值(即,结构的默认值是通过将所有值类型字段设置为它们的默认值并将所有引用类型字段设置为 null 而产生的值。)
演示 6
考虑以下输出示例。
using System;
namespace UsingdefaultKeywordinGenerics
{
class MyClass
{
// Some other stuff as per need
}
struct MyStruct
{
// Some other stuff as per need
}
class Program
{
static void PrintDefault<T>()
{
T defaultValue = default(T);
string printMe = String.Empty;
printMe = (defaultValue == null) ? "null" : defaultValue.ToString();
Console.WriteLine("Default value of {0} is {1}", typeof(T), printMe);
// C# 6.0 onwards,you can use interpolated string
//Console.WriteLine($"Default value of {typeof(T)} is {printMe}.");
}
static void Main(string[] args)
{
Console.WriteLine("***Using default keyword in Generic Programming.***");
PrintDefault<int>();//0
PrintDefault<double>();//0
PrintDefault<bool>();//False
PrintDefault<string>();//null
PrintDefault<int?>();//null
PrintDefault<System.Numerics.Complex>(); //(0,0)
PrintDefault<System.Collections.Generic.List<int>>(); // null
PrintDefault<System.Collections.Generic.List<string>>(); // null
PrintDefault<MyClass>(); //null
PrintDefault<MyStruct>();
Console.ReadKey();
}
}
}
输出
这是输出。
***Using default keyword in Generic Programming.***
Default value of System.Int32 is 0
Default value of System.Double is 0
Default value of System.Boolean is False
Default value of System.String is null
Default value of System.Nullable`1[System.Int32] is null
Default value of System.Numerics.Complex is (0, 0)
Default value of System.Collections.Generic.List`1[System.Int32] is null
Default value of System.Collections.Generic.List`1[System.String] is null
Default value of UsingdefaultKeywordinGenerics.MyClass is null
Default value of UsingdefaultKeywordinGenerics.MyStruct is UsingdefaultKeywordinGenerics.MyStruct
Note
输出的最后一行是打印structure>;
的<namespace>.<Name
,基本上你不能为一个结构设置默认值。更具体地说,结构的默认值是该结构的默认构造函数返回的值。如前所述,结构的默认值是通过将所有值类型字段设置为默认值并将所有引用类型字段设置为 null 而产生的值。每个结构中的隐式无参数构造函数设置这些默认值。您不能定义显式的无参数构造函数供自己使用。了解 C# 中的简单类型(如 int、double、bool 等)也很有用。通常被称为结构类型。
问答环节
4.6 泛型编程中如何使用 default 关键字?
您已经看到 default 关键字可以帮助您找到类型的默认值。在泛型编程中,有时您可能希望为泛型类型提供默认值。在前面的例子中,您看到了默认值根据值类型或引用类型的不同而不同。在这个例子中,请注意PrintDefault<T>()
方法。
不使用下面的代码行
T defaultValue = default(T);
如果你使用类似
T defaultValue = null;//will not work for value types
你会得到一个编译时错误,
Error CS0403 Cannot convert null to type parameter 'T' because it could be a non-nullable value type. Consider using 'default(T)' instead.
或者,如果您使用下面的代码行
T defaultValue = 0;//will not work for reference types
您会得到一个编译时错误,
Error CS0029 Cannot implicitly convert type 'int' to 'T'
实现通用接口
就像泛型类一样,你也可以拥有泛型接口。泛型接口可以包含泛型方法和非泛型方法。如果要实现泛型接口方法,可以遵循通常实现非泛型接口方法时使用的相同方法。下面的程序演示如何实现泛型接口的方法。
演示 7
为了涵盖这两种场景,在这个例子中,通用接口GenericInterface<T>
有一个称为GenericMethod(T param)
的通用方法和一个称为NonGenericMethod()
的非通用方法。第一个方法有一个通用的返回类型T
,第二个方法有一个void
返回类型。
剩下的部分比较好理解,我保留了评论,供大家参考。
using System;
namespace ImplementingGenericInterface
{
interface GenericInterface<T>
{
//A generic method
T GenericMethod(T param);
//A non-generic method
public void NonGenericMethod();
}
//Implementing the interface
class ConcreteClass<T>:GenericInterface<T>
{
//Implementing interface method
public T GenericMethod(T param)
{
return param;
}
public void NonGenericMethod()
{
Console.WriteLine("Implementing NonGenericMethod of GenericInterface<T>");
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Implementing generic interfaces.***\n");
//Using 'int' type
GenericInterface<int> concreteInt = new ConcreteClass<int>();
int myInt = concreteInt.GenericMethod(5);
Console.WriteLine($"The value stored in myInt is : {myInt}");
concreteInt.NonGenericMethod();
//Using 'string' type now
GenericInterface<string> concreteString = new ConcreteClass<string>();
string myStr = concreteString.GenericMethod("Hello Reader");
Console.WriteLine($"The value stored in myStr is : {myInt}");
concreteString.NonGenericMethod();
Console.ReadKey();
}
}
}
输出
这是输出。
***Implementing generic interfaces.***
The value stored in myInt is : 5
Implementing NonGenericMethod of GenericInterface<T>
The value stored in myStr is : 5
Implementing NonGenericMethod of GenericInterface<T>
分析
在前一个例子中有一些有趣的地方需要注意。让我们检查他们。
-
If you have another concrete class that wants to implement
GenericInterface<T>,
and you write following code block, you get compile-time errors.class ConcreteClass2 : GenericInterface<T>//Error { public T GenericMethod(T param) { throw new NotImplementedException(); } public void NonGenericMethod() { throw new NotImplementedException(); } }
这是因为我没有将类型参数 T 传递给 ConcreteClass2。您有三个相同的编译时错误“错误 CS0246 找不到类型或命名空间名称“T ”(您是否缺少 using 指令或程序集引用?)."消息。
-
如果您编写以下代码段,您会得到同样的错误:
class ConcreteClass2<U> : GenericInterface<T>//Error
原因很明显:找不到 T。
当您实现泛型接口时,实现类需要处理相同的 T 类型参数。这就是下面这段代码有效的原因。
class ConcreteClass<T> : GenericInterface<T>
{//remaining code}
问答环节
在前面的例子中,我的实现类可以处理多个类型参数吗?
是的。以下两个代码段也是有效的。
class ConcreteClass2<U,T> : GenericInterface<T>//valid
{//remaining code}
class ConcreteClass2<T, U> : GenericInterface<T>//also valid
{remaining code}
要记住的关键是,你的实现类需要提供接口所需的参数(例如,在这种情况下,实现类必须包含 T 参数,它出现在GenericInterface<T>
接口中。
假设你有以下两个界面。
interface IFirstInterface1<T> { }
interface ISecondInterface2<T, U> { }
您能预测下面的代码段能否编译吗?
Segment 1:
class MyClass1<T> : IFirstInterface<T> { }
Segment 2:
class MyClass2<T> : ISecondInterface<T, U> { }
Segment 3:
class MyClass3<T> : ISecondInterface<T, string> { }
Segment 4:
class MyClass4<T> : ISecondInterface<string, U> { }
Segment 5:
class MyClass5<T> : ISecondInterface<string, int> { }
Segment 6:
class MyClass6 : ISecondInterface<string, int> { }
只有段 2 和段 4 不会编译。在段 2 中,MyClass2 不包括 U 参数。在段 4 中,MyClass4 不包含 T 参数。
在段 1 和段 3 中,MyClass1 和 MyClass3 分别具有所需的参数。
第 5 段和第 6 段没有任何问题,因为在这些情况下,各自的类在构造封闭的接口上工作。
通用约束
您可以对泛型类型参数进行限制。例如,您可以选择泛型类型必须是引用类型或值类型,或者它应该从任何其他基类型派生,等等。但是为什么要在代码中允许约束呢?简单的答案是,通过使用约束,你可以对你的代码有很多控制,并且你允许 C# 编译器预先知道你将要使用的类型。因此,C# 编译器可以帮助您在编译时检测错误。
要指定一个约束,可以使用where
关键字和一个冒号(:
)操作符,如下所示。
class EmployeeStoreHouse<T> where T : IEmployee
或者,
class EmployeeStoreHouse<T> where T : IEmployee,new()
IEmployee
是一个接口。
一般来说,使用以下约束。
-
where T
:struct
表示类型 T 必须是值类型。(请记住,结构是一种值类型。) -
where T: class
表示类型 T 必须是引用类型。(记住,类是一个引用类型。) -
where T: IMyInter
表示 T 类型必须实现IMyInter
接口。 -
where T: new()
意味着类型 T 必须有一个默认的(无参数的)构造函数。(如果与其他约束一起使用,则将其放在最后一个位置。) -
where T: S
意味着类型 T 必须从另一个泛型类型 s 派生。它有时被称为裸类型约束。
现在让我们进行一次演示。
演示 8
在演示 8 中,IEmployee
接口包含一个抽象的Position
方法。在将雇员的详细信息存储在雇员存储中之前,我使用这个方法来设置雇员的名称(可以把它看作一个简单的雇员数据库)。Employee
类继承自IEmployee
,所以它需要实现这个接口方法。Employee
类有一个公共构造函数,它可以接受两个参数:第一个参数设置雇员姓名,第二个参数表示工作年限。我正在根据员工的经验设定一个名称。(是的,为了简单起见,我只考虑多年的经验来定位。)
在本演示中,您会看到下面一行。
class EmployeeStoreHouse<T> where T : IEmployee
泛型参数的约束只是告诉你泛型类型T
必须实现IEmployee
接口。
最后,我使用了基于范围的 switch 语句,从 C# 7.0 开始就支持这种语句。如果您使用的是遗留版本,可以用传统的 switch 语句替换代码段。
这是完整的演示。
using System;
using System.Collections.Generic;
namespace UsingConstratintsinGenerics
{
interface IEmployee
{
string Position();
}
class Employee : IEmployee
{
public string Name;
public int YearOfExp;
//public Employee() { }
public Employee(string name, int yearOfExp)
{
this.Name = name;
this.YearOfExp = yearOfExp;
}
public string Position()
{
string designation;
//C#7.0 onwards range based switch statements are allowed.
switch (YearOfExp)
{
case int n when (n <= 1):
designation = "Fresher";
break;
case int n when (n >= 2 && n <= 5):
designation = "Intermediate";
break;
default:
designation = "Expert";
break;
}
return designation;
}
}
class EmployeeStoreHouse<T> where T : IEmployee
{
private List<Employee> EmpStore = new List<Employee>();
public void AddToStore(Employee element)
{
EmpStore.Add(element);
}
public void DisplayStore()
{
Console.WriteLine("The store contains:");
foreach (Employee e in EmpStore)
{
Console.WriteLine(e.Position());
}
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Using constraints in generic programming.***\n");
//Employees
Employee e1 = new Employee("Suresh", 1);
Employee e2 = new Employee("Jack", 5);
Employee e3 = new Employee("Jon", 7);
Employee e4 = new Employee("Michael", 2);
Employee e5 = new Employee("Amit", 3);
//Employee StoreHouse
EmployeeStoreHouse<Employee> myEmployeeStore = new EmployeeStoreHouse<Employee>();
myEmployeeStore.AddToStore(e1);
myEmployeeStore.AddToStore(e2);
myEmployeeStore.AddToStore(e3);
myEmployeeStore.AddToStore(e4);
myEmployeeStore.AddToStore(e5);
//Display the Employee Positions in Store
myEmployeeStore.DisplayStore();
Console.ReadKey();
}
}
}
输出
这是输出。
***Using constraints in generic programming.***
The store contains:
Fresher
Intermediate
Expert
Intermediate
Intermediate
问答环节
4.9 为什么我在下面一行中得到多个编译时错误?
class EmployeeStoreHouse<T> where T : new(),IEmployee
目前有两个问题。首先,您没有将新的()约束作为最后一个约束。其次,Employee 类没有公共的无参数构造函数。Visual Studio 为您提供了关于这两种错误的线索;错误截图如图 4-3 所示。
图 4-3
由于不正确使用 new()约束而导致的编译时错误
简单的补救方法是
-
在最后一个位置放置一个
new()
约束 -
在 Employee 类中定义一个公共的无参数构造函数,例如
public Employee() { }
4.10 我可以对构造函数应用约束吗?
当您为泛型类型使用new()
约束时,您实际上是将约束放在了构造函数上。例如,在下面的代码中,该类型必须有一个无参数的构造函数。
public class MyClass<T> where T:new()
在这种情况下,一定要注意不能使用“参数化”的构造函数约束。例如,如果在下面的代码中使用 new(int)这样的代码,就会出现几个编译时错误。
class EmployeeStoreHouse<T> where T : IEmployee,new(int) //Error
一个错误说,
Error CS0701 'int' is not a valid constraint. A type used as a constraint must be an interface, a nonsealed class or a type parameter.
4.11 我可以在单一类型上应用多个接口作为约束吗?
是的。例如,如果您使用现成的 List 类,您将看到以下内容。
public class List<[NullableAttribute(2)]T>
: ICollection<T>, IEnumerable<T>, IEnumerable, IList<T>, IReadOnlyCollection<T>, IReadOnlyList<T>, ICollection, IList
{//some other stuff}
你可以看到ICollection<T>
、IEnumerable<T>
、IList<T>
应用在List<T>
上。
使用协方差和逆变
在第一章关于委托的讨论中,你了解到协变和逆变支持委托最早出现在 C# 2.0 中。从 C# 4.0 开始,这些概念可以应用于泛型类型参数、泛型接口和泛型委托。第一章也与非通用代表探讨了这些概念。在本章中,我们将通过更多的案例继续探讨这些概念。
在继续之前,请记住以下几点。
-
协方差和逆变处理带有参数和返回类型的类型转换。
-
英寸 NET 4 以后,您可以在泛型委托和泛型接口中使用这些概念。(在早期版本中,会出现编译时错误。)
-
逆变通常被定义为调整或修改。当你试图在编码世界中实现这些概念时,你就明白了下面的道理(或者类似的道理)。
-
所有的足球运动员都是运动员,但反过来却不是这样(因为有许多运动员打高尔夫、篮球、曲棍球等。)同样,你可以说所有的公共汽车或火车都是交通工具,但反过来就不一定了。
-
在编程术语中,所有派生类都是基于类型的类,但反之则不然。例如,假设您有一个名为
Rectangle
的类,它是从名为Shape
的类派生而来的。那么你可以说所有的矩形都是形状,但反过来就不成立了。 -
根据 MSDN 的说法,协方差和逆变处理数组、委托和泛型类型的隐式引用转换。协方差保持赋值兼容性,逆变则相反。
-
从。NET Framework 4,在 C# 中,有关键字将接口和委托的泛型类型参数标记为协变或逆变。对于协变接口和委托,您会看到使用了out
关键字(表示值出来了)。逆变接口和委托与关键字in
相关联(表示值正在进入)。
考虑一个内置的 C# 构造。我们来查看一下IEnumerable<T>
在 Visual Studio 中的定义,如图 4-4 。
图 4-4
来自 Visual Studio 2019 的 IEnumerable
你可以看到out
与IEnumerable
相关联。这仅仅意味着你可以将IEnumerable<DerivedType>
分配给IEnumerable<BaseType>
。这就是为什么你可以将IEnumerable<string>
分配给IEnumerable<object>
。所以,你可以说IEnumerable<T>
是T
上的协变。
现在在 Visual Studio 中检查Action<T>
委托的定义,如图 4-5 所示。
图 4-5
来自 Visual Studio 2019 的动作
或者,你可以在 Visual Studio 中查看IComparer<T>
接口的定义,如图 4-6 所示。
图 4-6
Visual Studio 2019 的 IComparer
您可以看到in
与Action
委托和IComparer
接口相关联。这仅仅意味着你可以将Action<BaseType>
分配给Action<DerivedType>
。所以,你可以说动作
类似地,因为 type 参数在the IComparer
接口中是逆变的,所以您可以使用您指定的实际类型或者任何更通用(或者更少派生)的类型。
问答环节
4.12 在一个 Func 委托中,我看到了同时存在的 in
和 out
参数。比如在 T1 中的 Func < in T,out TResult > 或 Func <,在 T2,out TResult >,这些定义我该怎么解读?
它只是告诉你Func
委托有协变的返回类型和逆变的参数类型。
4.13“任务兼容性”是什么意思?
下面是一个示例,您可以将更具体的类型(或派生类型)分配给兼容的不太具体的类型。例如,整数变量的值可以存储在对象变量中,如下所示:
int i = 25;
object o = i;//Assignment Compatible
具有泛型委托的协方差
让我们用一个泛型委托来检查协方差。在下面的演示中,我声明了一个具有协变返回类型的泛型委托,如下所示。
delegate TResult CovDelegate<out TResult>();
在这个例子中,Vehicle
是父类,Bus
是派生类,所以您看到了层次结构。(我没有在这些类中添加任何额外的方法/代码,因为本演示不需要它们。)
class Vehicle
{
//Some code if needed
}
class Bus : Vehicle
{
//Some code if needed
}
此外,您会看到以下两个静态方法的存在:GetOneVehicle()
和GetOneBus()
。第一个返回一个Vehicle
对象,第二个返回一个Bus
对象。
private static Vehicle GetOneVehicle()
{
Console.WriteLine("Creating one vehicle and returning it.");
return new Vehicle();
}
private static Bus GetOneBus()
{
Console.WriteLine("Creating one bus and returning the bus.");
下面的代码段简单易懂,因为它们与委托签名相匹配。
CovDelegate<Vehicle> covVehicle = GetOneVehicle;
covVehicle();
CovDelegate<Bus> covBus = GetOneBus;
covBus();
现在有趣的部分来了。注意下面的赋值。
covVehicle = covBus;
这种赋值不会引发任何编译错误,因为我使用了具有协变返回类型的委托。但是需要注意的是,如果没有使用 out 参数使委托的返回类型协变,这种赋值会导致下面的编译时错误。
Error CS0029 Cannot implicitly convert type 'CovarianceWithGenericDelegates.CovDelegate<CovarianceWithGenericDelegates.Bus>' to 'CovarianceWithGenericDelegates.CovDelegate<CovarianceWithGenericDelegates.Vehicle>'
演示 9
进行完整的演示。请参考支持性注释来帮助您理解。
using System;
namespace CovarianceWithGenericDelegates
{
//A generic delegate with covariant return type
//(Notice the use of 'out' keyword)
delegate TResult CovDelegate<out TResult>();
//Here 'out' is not used(i.e. it is non-covariant)
//delegate TResult CovDelegate<TResult>();
class Vehicle
{
//Some code if needed
}
class Bus : Vehicle
{
//Some code if needed
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Testing covariance with a Generic Delegate.***");
Console.WriteLine("Normal usage:");
CovDelegate<Vehicle> covVehicle = GetOneVehicle;
covVehicle();
CovDelegate<Bus> covBus = GetOneBus;
covBus();
//Testing Covariance
//covBus to covVehicle (i.e. more specific-> more general) is //allowed through covariance
Console.WriteLine("Using covariance now.");
//Following assignment is Ok, if you use 'out' in delegate //definition
Otherwise, you'll receive compile-time error
covVehicle = covBus;//Still ok
covVehicle();
Console.WriteLine("End covariance testing.\n");
Console.ReadKey();
}
private static Vehicle GetOneVehicle()
{
Console.WriteLine("Creating one vehicle and returning it.");
return new Vehicle();
}
private static Bus GetOneBus()
{
Console.WriteLine("Creating one bus and returning the bus.");
return new Bus();
}
}
}
输出
这是输出。
***Testing covariance with a Generic Delegate.***
Normal usage:
Creating one vehicle and returning it.
Creating one bus and returning the bus.
Using covariance now.
Creating one bus and returning the bus.
End covariance testing.
通用接口的协变
让我们用一个通用接口来检查协方差。在这个例子中,我使用了 C# 中另一个名为IEnumerable<T>
的内置结构。这是一个为 C# 中最重要的功能提供基础的接口。如果您想对集合中的每一项做一些有意义的事情,并逐个处理它们,那么可以在一个foreach
循环中使用IEnumerable<T>
。包含多个元素的. NET Framework 实现了此接口。例如,常用的List
类实现了这个接口。
演示 10
和前面的演示一样,在这个例子中,Vehicle
是父类,Bus
是派生类,但是这次,我在它们中分别放置了一个名为ShowMe()
的实例方法。你已经在IEnumerable<T>
中看到,T 是协变的,所以这一次,我可以应用下面的赋值。
IEnumerable<Vehicle> vehicleEnumerable= busEnumerable;
busEnumerable
是一个IEnumerable<Bus>
对象,可能如下所示。
IEnumerable<Bus> busEnumerable=new List<Bus>();
在许多现实生活中的应用中,使用返回IEnumerable<T>.
的方法是一种常见的做法,当您不想向他人公开实际的具体类型并且能够循环遍历项目时,这很有用。
现在浏览完整的演示,如果需要的话可以参考支持性的注释。
using System;
using System.Collections.Generic;
namespace CovarianceWithGenericInterface
{
class Vehicle
{
public virtual void ShowMe()
{
Console.WriteLine("Vehicle.ShowMe().The hash code is : " + GetHashCode());
}
}
class Bus : Vehicle
{
public override void ShowMe()
{
Console.WriteLine("Bus.ShowMe().Here the hash code is : " + GetHashCode());
}
}
class Program
{
static void Main(string[] args)
{
//Covariance Example
Console.WriteLine("***Using Covariance with Generic Interface.***\n");
Console.WriteLine("**Remember that T in IEnumerable<T> is covariant");
//Some Parent objects
//Vehicle vehicle1 = new Vehicle();
//Vehicle vehicle2 = new Vehicle();
//Some Bus objects
Bus bus1 = new Bus();
Bus bus2 = new Bus();
//Creating a child List
//List<T> implements IEnumerable<T>
List<Bus> busList = new List<Bus>();
busList.Add(bus1);
busList.Add(bus2);
IEnumerable<Bus> busEnumerable = busList;
/*
An object which was instantiated with a more derived type argument (Bus) is assigned to an object instantiated with a less derived type argument(Vehicle).Assignment compatibility is preserved here.
*/
IEnumerable<Vehicle> vehicleEnumerable = busEnumerable;
foreach (Vehicle vehicle in vehicleEnumerable)
{
vehicle.ShowMe();
}
Console.ReadKey();
}
}
}
输出
这是输出。
***Using Covariance with Generic Interface.***
**Remember that T in IEnumerable<T> is covariant
Bus.ShowMe().Here the hash code is : 58225482
Bus.ShowMe().Here the hash code is : 54267293
与泛型委托相反
让我们用一个泛型委托来检查 contravariance。在这个演示中,我声明了一个泛型逆变委托,如下所示。
delegate void ContraDelegate<in T>(T t);
同样,Vehicle
是父类,Bus
是派生类,它们都包含一个名为ShowMe()
的方法。您会看到下面的代码段。
class Vehicle
{
public virtual void ShowMe()
{
Console.WriteLine(" Vehicle.ShowMe()");
}
}
class Bus : Vehicle
{
public override void ShowMe()
{
Console.WriteLine(" Bus.ShowMe()");
}
}
除了这些类,您还会看到下面两个静态方法的存在:ShowVehicleType()
和ShowBusType()
。(第一个从Vehicle
对象调用ShowMe()
,第二个从Bus
对象调用ShowMe()
。)
private static void ShowVehicleType(Vehicle vehicle)
{
vehicle.ShowMe();
}
private static void ShowBusType(Bus bus)
{
bus.ShowMe();
}
下面的代码段简单易懂,因为它们与委托签名相匹配。(输出也显示在注释中。)
ContraDelegate<Vehicle> contraVehicle = ShowVehicleType;
contraVehicle(obVehicle); // Vehicle.ShowMe()
ContraDelegate<Bus> contraBus = ShowBusType;
contraBus(obBus); // Bus.ShowMe()
现在到了有趣的部分,它与协方差相反。注意下面的赋值。
contraBus = contraVehicle;
这个赋值不会引发任何编译错误,因为我使用的是逆变委托。 但是需要注意的是,如果没有使用 in
参数使委托逆变,这种赋值会导致下面的编译时错误。
Error CS0029 Cannot implicitly convert type 'ContravarianceWithGenericDelegates.ContraDelegate<ContravarianceWithGenericDelegates.Vehicle>' to 'ContravarianceWithGenericDelegates.ContraDelegate<ContravarianceWithGenericDelegates.Bus>'
演示 11
现在浏览完整的演示,并参考支持注释来帮助您理解。
using System;
namespace ContravarianceWithGenericDelegates
{
// A generic contravariant delegate
delegate void ContraDelegate<in T>(T t);
// A generic non-contravariant delegate
//delegate void ContraDelegate<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("*** Testing Contra-variance with Generic Delegates.***");
Vehicle obVehicle = new Vehicle();
Bus obBus = new Bus();
Console.WriteLine("Normal usage:");
ContraDelegate<Vehicle> contraVehicle = ShowVehicleType;
contraVehicle(obVehicle);
ContraDelegate<Bus> contraBus = ShowBusType;
contraBus(obBus);
Console.WriteLine("Using contravariance now.");
/*
Using general type to derived type.
Following assignment is Ok, if you use 'in' in delegate definition.
Otherwise, you'll receive compile-time error.
*/
contraBus = contraVehicle;//ok
contraBus(obBus);
Console.ReadKey();
}
private static void ShowVehicleType(Vehicle vehicle)
{
vehicle.ShowMe();
}
private static void ShowBusType(Bus bus)
{
bus.ShowMe();
}
}
}
输出
这是输出。
*** Testing Contra-variance with Generic Delegates.***
Normal usage:
Vehicle.ShowMe()
Bus.ShowMe()
Using contravariance now.
Bus.ShowMe()
与通用接口相反
现在你明白了协变和逆变。您已经看到了在泛型委托中使用协变和逆变,以及使用泛型接口实现协变。我将剩下的情况作为家庭作业,您需要编写一个完整的程序,并使用通用接口实现逆变的概念。
我提供了可以帮助您实现它的部分代码段。如果您愿意,可以使用以下代码段作为参考来验证您的实现。为了更好地理解,您也可以参考相关的注释。
部分实施
这是一个通用的逆变接口。
// Contravariant interface
interface IContraInterface<in T>{ }
// Following interface is neither covariant nor contravariant
//interface IContraInterface< T> { }
class Implementor<T>: IContraInterface<T> { }
这是一个继承层次结构。
class Vehicle
{
// Some code if needed
}
class Bus : Vehicle
{
// Some code if needed
}
这是关键任务。
IContraInterface<Vehicle> vehicleOb = new Implementor<Vehicle>();
IContraInterface<Bus> busOb = new Implementor<Bus>();
// Contravarince allows the following
// but you'll receive a compile-time error
// if you do not make the interface contravariant using 'in'
busOb = vehicleOb;
问答环节
当我使用协方差时,看起来好像是在使用一种简单的多态性技术。例如,在前面的演示中,您使用了以下代码行。
IEnumerable<Vehicle> vehicleEnumerable = busEnumerable;
这是正确的吗?
是的。
4.15 我可以覆盖一个泛型方法吗?
是的。您需要遵循应用于非泛型方法的相同规则。让我们看看演示 12。
演示 12
在本演示中,BaseClass<T>
是父类。它有一个名为MyMethod
的方法,接受T
作为参数,它的返回类型也是T
。DerivedClass<T>
从此父类派生并覆盖此方法。
using System;
namespace MethodOverridingDemo
{
class BaseClass<T>
{
public virtual T MyMethod(T param)
{
Console.WriteLine("Inside BaseClass.BaseMethod()");
return param;
}
}
class DerivedClass<T>: BaseClass<T>
{
public override T MyMethod(T param)
{
Console.WriteLine("Here I'm inside of DerivedClass.DerivedMethod()");
return param;
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Overriding a virtual method.***\n");
BaseClass<int> intBase = new BaseClass<int>();
// Invoking Parent class method
Console.WriteLine($"Parent class method returns {intBase.MyMethod(25)}");//25
// Now pointing to the child class method and invoking it.
intBase = new DerivedClass<int>();
Console.WriteLine($"Derived class method returns {intBase.MyMethod(25)}");//25
// The following will cause compile-time error
//intBase = new DerivedClass<double>(); // error
Console.ReadKey();
}
}
}
输出
这是输出。
***Overriding a virtual method.***
Inside BaseClass.BaseMethod()
Parent class method returns 25
Here I'm inside of DerivedClass.DerivedMethod()
Derived class method returns 25
分析
您可以看到,通过遵循一个简单的多态性,我使用父类引用(intBase
)指向子类对象。这种编码没有问题,因为两种情况都只处理int
类型。但是下面几行带注释的代码很容易理解,因为使用intBase
,你不能指向一个处理不同类型的对象(在这个例子中是double
)。
// The following will cause compile-time error
//intBase = new DerivedClass<double>(); // error
为了打印输出消息,我使用了字符串插值技术。我用它只是为了一个改变,但在这种情况下,你需要使用 C# 6.0 或更高版本;否则,您可以使用传统的方法。
问答环节
4.16 我可以重载一个泛型方法吗?
是的。在这种情况下,您也需要遵循应用于非泛型方法的相同规则,但是您必须小心使用接受类型参数的方法。在这种情况下,上的类型差异不被认为是泛型类型; 代替 , 这要看你把 替换成什么类型参数了。
4.17 你说泛型类型上不考虑类型差异;相反,它取决于您替换为类型参数的类型参数。你能详细说明一下吗?
我的意思是,有时看起来你已经完美地遵循了重载的规则,但是当你重载一个接受类型参数的泛型方法时,还需要考虑更多的东西。
你知道对于重载,数量和/或类型参数是不同的。所以,如果你的类中有以下两个方法,你可以说这是一个重载的例子。
public void MyMethod2(int a, double b) { // some code };
public void MyMethod2(double b, int a) { // some code };
现在考虑下面的代码段,它涉及泛型类型参数。
class MyClass<T,U>
{
public void MyMethod(T param1, U param2)
{
Console.WriteLine("Inside MyMethod(T param1, U param2)");
}
public void MyMethod(U param1, T param2)
{
Console.WriteLine("Inside MyMethod(U param1, T param2)");
}
}
似乎有两个 MyMethod 的重载版本,因为泛型类型参数的顺序不同。但是有潜在的歧义,当你练习下面的代码段时,你就会明白了。
MyClass<int, double> object1 = new MyClass<int, double>();
object1.MyMethod(1, 2.3); // ok
MyClass<int, int> object2 = new MyClass<int, int>();
// Ambiguous call
object2.MyMethod(1, 2); // error
对于这段代码,您会得到下面的编译时错误(对于标有// error 的行)。
CS0121 The call is ambiguous between the following methods or properties: 'MyClass<T, U>.MyMethod(T, U)' and 'MyClass<T, U>.MyMethod(U, T)'
演示 13
这是完整的演示。
using System;
namespace MethodOverloadingDemo
{
class MyClass<T,U>
{
public void MyMethod(T param1, U param2)
{
Console.WriteLine("Inside MyMethod(T param1, U param2)");
}
public void MyMethod(U param1, T param2)
{
Console.WriteLine("Inside MyMethod(U param1, T param2)");
}
public void MyMethod2(int a, double b)
{
Console.WriteLine("Inside MyMethod2(int a, double b).");
}
public void MyMethod2(double b, int a)
{
Console.WriteLine("MyMethod2(double b, int a) is called here.");
} }
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Method overloading demo.***\n");
MyClass<int, double> object1 = new MyClass<int, double>();
object1.MyMethod(1, 2.3);//ok
object1.MyMethod2(1, 2.3);//ok
object1.MyMethod2(2.3, 1);//ok
MyClass<int, int> object2 = new MyClass<int, int>();
// Ambiguous call
object2.MyMethod(1, 2); // error
Console.ReadKey();
}
}
}
输出
同样,您会得到下面的编译时错误。
CS0121 The call is ambiguous between the following methods or properties: 'MyClass<T, U>.MyMethod(T, U)' and 'MyClass<T, U>.MyMethod(U, T)'
您可以注释掉这个不明确的调用,如下所示,然后编译并运行程序。
//object2.MyMethod(1, 2);//error
这一次,您将获得以下输出。
***Method overloading demo.***
Inside MyMethod(T param1, U param2)
Inside MyMethod2(int a, double b).
MyMethod2(double b, int a) is called here.
自引用泛型类型
有时你可能需要比较一个类的两个实例。在这种情况下,你有两个选择。
-
使用内置结构。
-
自己写比较法。
当您对使用内置构造感兴趣时,您有多种选择。例如,您可以使用IComparable<T>
的CompareTo
方法或IEquitable<T>
的Equals
方法。你可能会注意到在 C# 中也有一个非泛型的IComparable
。
以下是来自 Visual Studio 的关于CompareTo
的信息。
//
// Summary:
// Compares the current instance with another object of the same // type and returns an integer that indicates whether the current instance // precedes, follows, or occurs in the same position in the sort // order as the other object.
//
// Parameters:
// other:
// An object to compare with this instance.
//
// Returns:
// A value that indicates the relative order of the objects being // compared. The return value has these meanings: Value Meaning Less // than zero This instance precedes other in the sort order. Zero // This instance occurs in the same position in the sort order as other. // Greater than zero This instance follows other in the sort order.
int CompareTo([AllowNull] T other);
以下是来自 Visual Studio 的关于Equals
的信息。
//
// Summary:
// Indicates whether the current object is equal to another object of// the same type.
//
// Parameters:
// other:
// An object to compare with this object.
//
// Returns:
// true if the current object is equal to the other parameter; // otherwise, false.
bool Equals([AllowNull] T other);
如果您的类实现了这些接口中的任何一个,您可以使用这些方法并根据需要重写它们。这些接口在System
名称空间中可用,它们由内置类型实现,如int
、double
、and string
。
然而,在许多情况下,您可能想要编写自己的比较方法。我在演示 14 中这样做了。
当类型关闭类型参数时,它可以将自己命名为具体类型。
演示 14
在这个演示中,Employee
类实现了IIdenticalEmployee<T>
,它有一个名为CheckEqualityWith
的抽象方法。让我们假设在您的Employee
类中,您有员工 id 和部门名称。一旦我实例化了来自Employee
类的对象,我的任务就是比较这些对象。
为了便于比较,我简单地检查了两个雇员的deptName
和employeeID
是否相同。如果匹配,则员工是相同的。(使用单词同,我指的只是这些对象的内容,而不是对堆的引用。)
这就是比较法。
public string CheckEqualityWith(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 Employees.";
}
}
}
现在查看完整的实现和输出。
using System;
namespace SelfReferencingGenericTypeDemo
{
interface IIdenticalEmployee<T>
{
string CheckEqualityWith(T obj);
}
class Employee : IIdenticalEmployee<Employee>
{
string deptName;
int employeeID;
public Employee(string deptName, int employeeId)
{
this.deptName = deptName;
this.employeeID = employeeId;
}
public string CheckEqualityWith(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 Employees.";
}
}
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("**Self-referencing generic type demo.***\n");
Console.WriteLine("***We are checking whether two employee objects are same or different.***");
Console.WriteLine();
Employee emp1 = new Employee("Chemistry", 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.CheckEqualityWith(emp3));
Console.WriteLine("Comparing emp2 and emp4 :{0}", emp2.CheckEqualityWith(emp4));
Console.WriteLine("Comparing emp2 and emp5 :{0}", emp2.CheckEqualityWith(emp5));
Console.ReadKey();
}
}
}
输出
这是输出。
**Self-referencing generic type demo.***
***We are checking whether two employee objects are same or different.***
Comparing emp1 and emp3 :Different Employees.
Comparing emp2 and emp4 :Same Employee.
Comparing emp2 and emp5 :Cannot Compare with a null Object
分析
此示例向您展示了当类型关闭类型参数时,它可以将自己命名为具体类型。它演示了如何使用一个 自引用泛型 类型。 同样,在这个例子中,我使用了这个词,我指的只是对象的内容,而不是对堆的引用。
问答环节
4.18 你能总结一下泛型的关键用法吗?
您可以促进类型安全,而不必创建大量非常相似的类型,尤其是仅在它们使用的类型上有所不同的类型。因此,您可以避免运行时错误,并降低装箱和拆箱的成本。
4.19 静态变量如何在泛型编程环境中工作?
静态数据对于每个封闭类型都是唯一的。考虑下面的程序和输出供您参考。
示范 15
在本演示中,让我们关注 count 变量,看看当用不同类型实例化MyGenericClass<T>
泛型类时,它是如何递增的。
using System;
namespace TestingStaticData
{
class MyGenericClass<T>
{
public static int count;
public void IncrementMe()
{
Console.WriteLine($"Incremented value is : {++count}");
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Testing static in the context of generic programming.***");
MyGenericClass<int> intOb = new MyGenericClass<int>();
Console.WriteLine("\nUsing intOb now.");
intOb.IncrementMe();//1
intOb.IncrementMe();//2
intOb.IncrementMe();//3
Console.WriteLine("\nUsing strOb now.");
MyGenericClass<string> strOb = new MyGenericClass<string>();
strOb.IncrementMe();//1
strOb.IncrementMe();//2
Console.WriteLine("\nUsing doubleOb now.");
MyGenericClass<double> doubleOb = new MyGenericClass<double>();
doubleOb.IncrementMe();//1
doubleOb.IncrementMe();//2
MyGenericClass<int> intOb2 = new MyGenericClass<int>();
Console.WriteLine("\nUsing intOb2 now.");
intOb2.IncrementMe();//4
intOb2.IncrementMe();//5
Console.ReadKey();
}
}
}
输出
这是输出。
***Testing static in the context of generic programming.***
Using intOb now.
Incremented value is : 1
Incremented value is : 2
Incremented value is : 3
Using strOb now.
Incremented value is : 1
Incremented value is : 2
Using doubleOb now.
Incremented value is : 1
Incremented value is : 2
Using intOb2 now.
Incremented value is : 4
Incremented value is : 5
问答环节
4.20 使用泛型有什么重要的限制?
以下是一些需要注意的重要限制。
-
静态数据对于每个封闭类型是唯一的,但是对于不同的构造类型来说不是。
-
不能在泛型方法中使用外部修饰符。因此,下面的代码段
using System; using System.Runtime.InteropServices; class GenericClassDemo2<T> { [DllImport("avifil32.dll")] // error in generic method private static extern void AVIFileInit(); } raises the following compile-time error: Error CS7042 The DllImport attribute cannot be applied to a method that is generic or contained in a generic type.
-
不能将指针类型用作类型参数。因此,下面代码段
class GenericClassDemo2<T> { static unsafe void ShowMe() { int a = 10; // ok int* p; // ok p = &a; // ok T* myVar; // error } }
中的最后一行引发了下面的编译时错误
:
Error CS0208 Cannot take the address of, get the size of, or declare a pointer to a managed type ('T')
-
在问答环节问题 4.9 中,你看到了如果你有多个约束,
new()
约束必须放在最后。
最后的话
我希望这一章能够揭开泛型编程的关键特性。起初,泛型语法可能看起来有点令人不知所措,但是实践和重复使用这些概念将帮助您掌握它们,并且您将能够使用 C# 开发出高质量的软件。
现在让我们跳到下一章,在那里你将学习线程编程。
摘要
本章讨论了以下关键问题。
-
什么是泛型程序?为什么它很重要?
-
泛型编程比非泛型编程有什么优势?
-
为什么 default 关键字在泛型的上下文中有用?如何在我的程序中使用它?
-
如何在程序中使用内置委托——函数、动作和谓词?
-
在泛型编程中如何施加约束?
-
如何对泛型委托和接口使用协变和逆变?
-
如何重载泛型方法?你为什么要小心呢?
-
如何重写泛型方法?
-
如何使用自引用泛型类型?
-
静态变量在泛型程序中是如何表现的?
-
泛型的一些关键限制是什么?
五、多线程编程
当今世界,大家对多任务都很熟悉,简单的说就是你可以并行做多件事。考虑一个常见的场景。例如,当我在笔记本电脑上用 Microsoft Word 写这一章时,我正在 Windows Media Player 上听一段非常平静的音乐。同样,您可以在 C# 应用中同时执行不同的方法。要实现这个概念,您需要熟悉多线程。
在早期,计算机只有一个处理器,但是现在,情况已经发生了很大的变化。当今世界的大多数计算机都有多个处理器。例如,在撰写本文时,我正在使用一个带有四个逻辑处理器的双核系统;然而,在当今世界,这并不被认为是一台超高速计算机,因为有大量处理器(显然是昂贵的)和更强大的计算能力的机器。不过,如果另一台超高速电脑通过网络连接到我的电脑,我可以在上面执行一些工作。所以,使用其他机器的计算能力是可能的。但事实是,除非您将代码构建为在多个处理器上运行,否则您没有充分利用机器的计算潜力。在本章中,你将熟悉多线程,并学习如何有效地使用它。我们开始吧。
线程编程基础
到目前为止,您所看到的大多数程序都有一个单一的顺序控制流(即,一旦程序开始执行,它就按顺序遍历所有语句,直到结束)。因此,在任何特定时刻,只有一条语句正在执行。线程类似于程序。它只有一个控制流。它在起点和终点之间也有一个主体,它按顺序执行命令。每个程序至少有一个线程。
在 C# 中,一个程序中可以有多个控制流。在这些情况下,每个控制流被称为一个线程,这些线程可以并行运行。在多线程环境中,每个线程都有一个独特的执行流。这是一种编程范式,其中一个程序被分成多个可以并行实现的子程序(或部分)。但是,如果计算机只有一个处理器,它如何并行执行多项任务呢?处理器在这些子程序(或代码段)之间切换得非常快,因此在人眼看来,它们似乎都在同时执行。
简单来说,当操作系统在不同的应用之间划分处理器执行时间时,该场景是多任务处理,当操作系统在单个应用内的不同线程之间划分执行时间时,该场景被称为多线程。这就是为什么多线程被认为是一种特殊的多任务处理。
在这种情况下,在任何操作系统理论书籍中回顾进程和线程之间的区别是很重要的。供您参考,表 5-1 强调了一些关键区别。
表 5-1
进程和线程之间的比较
|过程
|
线
|
| --- | --- |
| 分配单位。 | 执行单位。 |
| 建筑构造。 | 编码结构不影响架构。 |
| 每个进程都有一个或多个线程。 | 每个线程属于一个进程。 |
| 由于上下文切换,进程间通信(通常称为 IPC)的开销很大。 | 线程间通信成本较低,可以使用进程内存,并且可能不需要上下文切换。 |
| 安全:一个进程不能破坏另一个进程。 | 不安全:一个线程可以写入另一个线程使用的内存。 |
管理多线程环境可能很有挑战性,但您可以更快地完成任务,并显著减少总体空闲时间。通常,在自动化环境中,计算机的输入比用户的键盘输入快得多。或者,当您通过网络传输数据时,网络传输速率可能比接收计算机的消耗速率慢。如果您需要等待每个任务完成后才能开始下一个任务,则总的空闲时间会更长。在这种情况下,多线程环境总是更好的选择。C# 可以帮助你有效地建模多线程环境。
图 5-1 展示了多线程程序中的一个常见场景,其中main thread
创建了另外两个线程——threadOne
和threadTwo
——并且所有线程都在并发运行。
图 5-1
在多线程程序中,主线程创建两个以上的线程,并且它们都是并发运行的
POINTS TO REMEMBER
多线程的核心目标是你可以在单独的线程中执行独立的代码段,这样你就可以更快地完成任务。
在。NET Framework 中,您可以同时拥有前台和后台线程。创建线程时,默认情况下它是前台线程。但是您可以将前台线程转换为后台线程。关键区别在于,当前台线程终止时,关联的后台线程也会停止。
问答环节
5.1 在图 5-1 中,我看到了术语“上下文切换”在这个上下文中是什么意思?
通常,许多线程可以在您的计算机上并行运行。计算机允许一个线程在一个处理器中运行一段时间,然后它可以突然切换到另一个处理器。这个决定是由不同的因素做出的。正常情况下,所有线程都有相同的优先级,它们之间的切换执行得很好。线程之间的切换称为上下文切换。它还使您能够存储当前线程(或进程)的状态,以便以后可以从这一点继续执行。
5.2 与单线程环境相比,多线程环境的主要优势是什么?
在单线程环境中,如果线程被阻塞,整个程序就会暂停,而在多线程环境中则不是这样。此外,您可以通过有效利用 CPU 来减少总的空闲时间。例如,当程序的一部分通过网络发送大量数据时,程序的另一部分可以接受用户输入,而程序的另一部分可以验证该输入并准备发送下一个数据块。
5.3 我有一个多核系统,但是多线程还能帮我吗?
曾几何时,大多数计算机只有一个内核;并发线程共享 CPU 周期,但是它们不能并行运行。使用多线程的概念,您可以通过有效使用 CPU 来减少总的空闲时间。但是如果你有多个处理器,你可以同时运行多个线程。因此,您可以进一步提高程序的速度。
5.4 多线程程序可以有多个并发运行的部分。这些部分中的每一个都是线程,每个线程可以有一个单独的执行流。这是正确的吗?
是的。
用 C# 编写多线程程序
在用 C# 编写多线程程序之前,首先要记住的是从
using System.Threading;
这个名称空间包含有不同方法的Thread
类。您将在接下来的演示中看到其中的一些方法。现在到了下一步。要运行一个方法,比如说Method1()
,在一个单独的线程中,您需要编写如下代码。
Thread threadOne = new Thread(Method1);
threadOne.Start();
注意前面的两行。如果将鼠标悬停在 Visual Studio 中的 Thread 类型上,您会看到 Thread 类有四个不同的构造函数,如下所示。
public Thread(ThreadStart start)
public Thread(ParameterizedThreadStart start)
public Thread(ThreadStart start, int maxStackSize)
public Thread(ParameterizedThreadStart start, int maxStackSize)
ThreadStart
和ParameterizedThreadStart
是代表。现在让我们详细研究一下这些代表。从 Visual Studio IDE 中,您可以获得对ThreadStart
委托的以下描述。
//
// Summary:
// Represents the method that executes on a //System.Threading.Thread.
[ComVisible(true)]
public delegate void ThreadStart();
类似地,Visual Studio IDE 显示了对ParameterizedThreadStart
委托的以下描述。
//
// Summary:
// Represents the method that executes on a //System.Threading.Thread.
//
// Parameters:
// obj:
// An object that contains data for the thread procedure.
[ComVisible(false)]
public delegate void ParameterizedThreadStart(object obj);
这些描述显示了以下几点。
-
两个委托都有
void
返回类型。 -
The ThreadStart
delegate 没有参数,而ParameterizedThreadStart
可以接受对象参数。
您将很快对这两个代理进行实验。但到目前为止,您已经学会了在不同的线程中运行一个方法;这些方法应该匹配任一委托签名。
最后一点:在演示 1 和演示 2 中,我使用了最简单的Start()
方法,它没有任何参数。稍后,您还会注意到该方法的另一个重载版本的使用,它可以接受一个对象参数。因此,根据您的需要,您可以使用以下任何一种方法:
public void Start();
public void Start(object? parameter);
使用ThreadStart
委托
让我们从ThreadStart
代表开始。假设你有一个叫做Method1
的方法,如下。
static void Method1()
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine("-ThreadOne from Method1() prints {0}", i);
}}
因为Method1
不接受任何参数,并且它有一个void
返回类型,它与ThreadStart
委托签名匹配。在第一章中,你学到了如果你写下以下内容,
ThreadStart delegateObject = new ThreadStart(Method1);
它相当于写作
ThreadStart delegateObject = Method1;
因此,当您在线程构造函数中传递一个ThreadStart
委托对象时,您可以编写如下代码。
Thread threadOne = new Thread(new ThreadStart(Method1));
这相当于写作
Thread threadOne = new Thread(Method1);
最后,值得注意以下几点。
-
在接下来的例子中,
Method1()
是一个静态方法。在这种情况下,您可以引用该方法,而无需实例化任何对象。 -
一旦调用了
Start()
方法,线程就被创建并开始执行。 -
如果你在一个已经运行的线程上调用
Start()
方法,你会遇到一个运行时错误,说,System.Threading.ThreadStateException
: 线程正在运行或者终止;它无法重启。
我们再来看最后一点。通过编程,一个线程可以有几种状态。Start
方法可以将当前实例的状态更改为ThreadState.Running
。在 Visual Studio2019 IDE 中,如果将鼠标悬停在ThreadState
定义处,会看到图 5-2 所示的枚举,描述了不同的线程状态。
图 5-2
C# 中线程的不同状态
这些都是不言自明的,但是你可能会对一个叫做WaitSleepJoin
的感兴趣。由于调用了Sleep()
或Join()
,或者请求了一个锁,线程可以进入这种阻塞状态;例如,当你调用Wait()
、Monitor.Enter()
等带有适当参数的时候。您很快就会了解到这一点。
演示 1
在下面的演示中,有两个静态方法:Method1
和Method2
。这些方法与ThreadStart
代表的签名相匹配。正如在“线程编程的基础”一节中所讨论的,我在单独的线程中运行它们。
POINTS TO REMEMBER
在本章中,对于一些初始演示,您会看到方法体中的硬编码行,例如
Console.WriteLine("-ThreadOne from Method1() prints {0}", i);
或者,
Console.WriteLine("--ThreadTwo from Method2() prints 2.0{0}", i);
理想情况下,您不应该像这样硬编码线程细节,因为在多线程环境中,Method1()可以在不同的线程中执行。但是如果你设置了一个线程名,那么你可以写类似下面的代码。
Console.WriteLine("-{0} from Method1() prints {1}", Thread.CurrentThread.Name, i);
或者,如果您喜欢使用字符串插值,您可以编写如下代码。
Console.WriteLine($"{Thread.CurrentThread.Name} from MyMethod() prints {i}");
在这里,我一步一步来。我还没有从 Thread 类中引入 Name 属性。为了简单起见,我使用 threadOne 对象执行 Method1(),使用 threadTwo 对象执行 Method2(),以此类推。
这是完整的演示。
using System;
using System.Threading;
namespace ThreadProgrammingEx1
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Thread Demonstration-1****");
Console.WriteLine("Main thread has started.");
Thread threadOne = new Thread(Method1);
// Same as
/* Thread threadOne = new Thread(new ThreadStart(Method1));*/
Thread threadTwo = new Thread(Method2);
// Same as
/* Thread threadTwo = new Thread(new ThreadStart(Method2));*/
Console.WriteLine("Starting threadOne shortly.");
// threadOne starts
threadOne.Start();
Console.WriteLine("Starting threadTwo shortly.");
// threadTwo starts
threadTwo.Start();
Console.WriteLine("Control comes at the end of Main() method.");
Console.ReadKey();
}
static void Method1()
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine("-ThreadOne from Method1() prints {0}", i);
}
}
static void Method2()
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine("--ThreadTwo from Method2() prints 2.0{0}", i);
}
}
}
}
输出
这是一种可能的输出。
***Thread Demonstration-1****
Main thread has started.
Starting threadOne shortly.
Starting threadTwo shortly.
-ThreadOne from Method1() prints 0
-ThreadOne from Method1() prints 1
-ThreadOne from Method1() prints 2
-ThreadOne from Method1() prints 3
-ThreadOne from Method1() prints 4
-ThreadOne from Method1() prints 5
-ThreadOne from Method1() prints 6
-ThreadOne from Method1() prints 7
-ThreadOne from Method1() prints 8
Control comes at the end of Main() method.
--ThreadTwo from Method2() prints 2.00
--ThreadTwo from Method2() prints 2.01
-ThreadOne from Method1() prints 9
--ThreadTwo from Method2() prints 2.02
--ThreadTwo from Method2() prints 2.03
--ThreadTwo from Method2() prints 2.04
--ThreadTwo from Method2() prints 2.05
--ThreadTwo from Method2() prints 2.06
--ThreadTwo from Method2() prints 2.07
--ThreadTwo from Method2() prints 2.08
--ThreadTwo from Method2() prints 2.09
这是另一个可能的输出。
***Thread Demonstration-1****
Main thread has started.
Starting threadOne shortly.
Starting threadTwo shortly.
-ThreadOne from Method1() prints 0
-ThreadOne from Method1() prints 1
-ThreadOne from Method1() prints 2
Control comes at the end of Main() method.
-ThreadOne from Method1() prints 3
-ThreadOne from Method1() prints 4
-ThreadOne from Method1() prints 5
-ThreadOne from Method1() prints 6
-ThreadOne from Method1() prints 7
-ThreadOne from Method1() prints 8
-ThreadOne from Method1() prints 9
--ThreadTwo from Method2() prints 2.00
--ThreadTwo from Method2() prints 2.01
--ThreadTwo from Method2() prints 2.02
--ThreadTwo from Method2() prints 2.03
--ThreadTwo from Method2() prints 2.04
--ThreadTwo from Method2() prints 2.05
--ThreadTwo from Method2() prints 2.06
--ThreadTwo from Method2() prints 2.07
--ThreadTwo from Method2() prints 2.08
--ThreadTwo from Method2() prints 2.09
分析
我提出了两种可能的输出结果:它可能因你的情况而异。这在线程编程中很常见,因为您的操作系统根据设计采用了上下文切换。稍后,您将看到可以使用一种特殊的机制来控制执行顺序。
演示 2
在演示 1 中,原始线程(对于Main()
方法)在衍生线程(对于Method1
和Method2
)之前结束。但是在真实的应用中,您可能不希望父线程在子线程之前完成(尽管程序会继续运行,直到其前台线程处于活动状态)。
在简单的场景中,您可以使用Sleep(int millisecondsTimeout)
方法。这是一种常用的static
方法。它会导致当前执行的线程暂停一段指定的时间。int
参数提示您需要将毫秒作为参数传递。如果您希望当前线程暂停 1 秒钟,您可以将 1000 作为参数传递给Sleep
方法。但是Sleep
方法不如Join()
有效,?? 也在Thread
类中定义。这是因为Join()
方法可以帮助你阻塞一个线程,直到另一个线程完成它的执行。在下面的演示中,我使用了这种方法,您会看到下面几行带有支持注释的代码。
// Waiting for threadOne to finish
threadOne.Join();
// Waiting for threadtwo to finish
threadTwo.Join();
这些语句是在Main()
方法内部编写的。一旦原始线程通过这些语句,它就等待threadOne
和threadTwo
完成它们的任务,并有效地加入子线程的执行。
现在浏览完整的演示并查看输出,然后是一个简短的分析。
using System;
using System.Threading;
namespace ThreadProgrammingEx2
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Thread Demonstration-2****");
Console.WriteLine("***Exploring Join() method.It helps to make a thread wait for another running thread to finish it's job.***");
Console.WriteLine("Main thread has started.");
Thread threadOne = new Thread(Method1);
// Same as
//Thread threadOne = new Thread(new ThreadStart(Method1));
Thread threadTwo = new Thread(Method2);
// Same as
//Thread threadTwo = new Thread(new ThreadStart(Method2));
Console.WriteLine("Starting threadOne shortly.");
// threadOne starts
threadOne.Start();
Console.WriteLine("Starting threadTwo shortly.");
// threadTwo starts
threadTwo.Start();
// Waiting for threadOne to finish
threadOne.Join();
// Waiting for threadtwo to finish
threadTwo.Join();
Console.WriteLine("Control comes at the end of Main() method.");
Console.ReadKey();
}
static void Method1()
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine("-ThreadOne from Method1() prints {0}", i);
}
}
static void Method2()
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine("--ThreadTwo from Method2() prints 2.0{0}", i);
}
}
}
}
输出
这是一种可能的输出。
***Thread Demonstration-2****
***Exploring Join() method.It helps to make a thread wait for another running thread to finish it's job.***
Main thread has started.
Starting threadOne shortly.
Starting threadTwo shortly.
-ThreadOne from Method1() prints 0
-ThreadOne from Method1() prints 1
-ThreadOne from Method1() prints 2
-ThreadOne from Method1() prints 3
-ThreadOne from Method1() prints 4
--ThreadTwo from Method2() prints 2.00
--ThreadTwo from Method2() prints 2.01
--ThreadTwo from Method2() prints 2.02
--ThreadTwo from Method2() prints 2.03
--ThreadTwo from Method2() prints 2.04
--ThreadTwo from Method2() prints 2.05
--ThreadTwo from Method2() prints 2.06
--ThreadTwo from Method2() prints 2.07
--ThreadTwo from Method2() prints 2.08
--ThreadTwo from Method2() prints 2.09
-ThreadOne from Method1() prints 5
-ThreadOne from Method1() prints 6
-ThreadOne from Method1() prints 7
-ThreadOne from Method1() prints 8
-ThreadOne from Method1() prints 9
Control comes at the end of Main() method.
分析
在这个演示中,您看到了在Main()
方法中使用了Join()
方法。原始线程保持活动状态,直到其他线程完成执行。所以,"Control comes at the end of Main() method."
语句总是出现在输出的末尾。
值得注意的是
-
Start
和Join
方法都有不同的重载版本。 -
你遇到一个运行时错误,说,
System.Threading.ThreadStateException:
‘线程没有启动。’如果你在一个没有启动的线程上调用Join()
。
问答环节
5.5 如何穿线。Sleep()不同于 Thread。Join()?
Sleep()
方法有两种变体。
public static void Sleep(int millisecondsTimeout)
and
public static void Sleep(TimeSpan timeout)
使用 Sleep()方法,可以将当前线程挂起一段特定的时间。
Join() has three variations.
public void Join();
public bool Join(int millisecondsTimeout);
public bool Join(TimeSpan timeout);
基本思想是,通过使用 Join(),可以阻塞调用线程,直到该实例所代表的线程终止。(虽然可以在重载版本的 Join()内部指定超时限制。)
使用sleep()
,如果你指定的时间不必要的长,线程将处于暂停状态,即使其他线程已经完成了它们的执行。但是通过使用 Join(),您可以等待其他线程完成,然后立即继续。
另一个有趣的区别是Sleep()
是一个静态方法,你在当前线程上调用这个方法。但是Join()
是一个实例方法,当你写类似下面这样的东西时,从调用者的角度来看,你传递了某个其他线程(而不是调用线程)的实例,并等待那个线程先完成。
// Waiting for threadOne to finish
threadOne.Join();
使用ParameterizedThreadStart
委托
您已经看到了ThreadStart
委托的用法。您无法处理可以接受参数的方法,但是带参数的方法在编程中非常常见。接下来,您将看到ParameterizedThreadStart
委托的使用。您已经知道它可以接受一个object
参数,并且它的返回类型是 void。因为参数是一个对象,所以您可以将它用于任何类型,只要您可以将强制转换正确地应用于正确的类型。
演示 3
在本演示中,您有以下方法。
static void Method3(Object number)
{
int upperLimit = (int)number;
for (int i = 0; i < upperLimit; i++)
{
Console.WriteLine("---ThreadThree from Method3() prints 3.0{0}", i);
}
}
您可以看到,尽管该方法有一个object
参数,但我将它转换为一个int
,然后使用它将所需的数据打印到控制台窗口。在这个演示中,有三种方法:Method1
、Method2
和Method3
。Method1
和Method2
在之前的演示中。新增Method3
是为了在下面的例子中演示ThreadStart
委托和ParameterizedThreadStart
委托的用法。
using System;
using System.Threading;
namespace UsingParameterizedThreadStart_delegate
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***ParameterizedThreadStart delegate is used in this demonstration****");
Console.WriteLine("Main thread has started.");
Thread threadOne = new Thread(Method1);
// Same as
//Thread threadOne = new Thread(new ThreadStart(Method1));
Thread threadTwo = new Thread(Method2);
// Same as
//Thread threadTwo = new Thread(new ThreadStart(Method2));
Thread threadThree = new Thread(Method3);
// Same as
//Thread threadThree = new Thread(new ParameterizedThreadStart(Method3));
Console.WriteLine("Starting threadOne shortly.");
// threadOne starts
threadOne.Start();
Console.WriteLine("Starting threadTwo shortly.");
// threadTwo starts
threadTwo.Start();
Console.WriteLine("Starting threadThree shortly.Here we use ParameterizedThreadStart delegate.");
// threadThree starts
threadThree.Start(15);
// Waiting for threadOne to finish
threadOne.Join();
// Waiting for threadtwo to finish
threadTwo.Join();
// Waiting for threadthree to finish
threadThree.Join();
Console.WriteLine("Main() method ends now.");
Console.ReadKey();
}
static void Method1()
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine("-ThreadOne from Method1() prints {0}", i);
}
}
static void Method2()
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine("--ThreadTwo from Method2() prints 2.0{0}", i);
}
}
/*
The following method has an object parameter
This method matches the ParameterizedThreadStart delegate signature;because it has a single parameter of type Object and this method doesn't return a value.
*/
static void Method3(Object number)
{
int upperLimit = (int)number;
for (int i = 0; i < upperLimit; i++)
{
Console.WriteLine("---ThreadThree from Method3() prints 3.0{0}", i);
}
}
}
}
输出
这是一种可能的输出。
***ParameterizedThreadStart delegate is used in this demonstration****
Main thread has started.
Starting threadOne shortly.
Starting threadTwo shortly.
-ThreadOne from Method1() prints 0
-ThreadOne from Method1() prints 1
-ThreadOne from Method1() prints 2
-ThreadOne from Method1() prints 3
-ThreadOne from Method1() prints 4
Starting threadThree shortly.Here we use ParameterizedThreadStart delegate.
--ThreadTwo from Method2() prints 2.00
-ThreadOne from Method1() prints 5
--ThreadTwo from Method2() prints 2.01
-ThreadOne from Method1() prints 6
--ThreadTwo from Method2() prints 2.02
-ThreadOne from Method1() prints 7
--ThreadTwo from Method2() prints 2.03
-ThreadOne from Method1() prints 8
---ThreadThree from Method3() prints 3.00
--ThreadTwo from Method2() prints 2.04
-ThreadOne from Method1() prints 9
---ThreadThree from Method3() prints 3.01
--ThreadTwo from Method2() prints 2.05
---ThreadThree from Method3() prints 3.02
--ThreadTwo from Method2() prints 2.06
---ThreadThree from Method3() prints 3.03
---ThreadThree from Method3() prints 3.04
---ThreadThree from Method3() prints 3.05
---ThreadThree from Method3() prints 3.06
--ThreadTwo from Method2() prints 2.07
--ThreadTwo from Method2() prints 2.08
---ThreadThree from Method3() prints 3.07
--ThreadTwo from Method2() prints 2.09
---ThreadThree from Method3() prints 3.08
---ThreadThree from Method3() prints 3.09
---ThreadThree from Method3() prints 3.010
---ThreadThree from Method3() prints 3.011
---ThreadThree from Method3() prints 3.012
---ThreadThree from Method3() prints 3.013
---ThreadThree from Method3() prints 3.014
Main() method ends now.
分析
与演示 2 一样,本例中使用了Join()
方法。因此,行"Main() method ends now."
位于输出的末尾。还要注意,这次我使用了下面一行:threadThree.Start(15);
这里我使用了重载版本的Start()
方法,它可以接受一个对象参数。
问答环节
5.6 我知道通过使用 ParameterizedThreadStart
委托,我可以使用可以接受 object
参数的方法。但是如何使用除 object 之外接受参数的其他方法呢?
因为参数是一个对象,所以几乎可以在任何情况下使用它,并且可能需要正确应用强制转换。例如,在演示 3 中,我将一个int
传递给了Method3
的参数,该参数被隐式地转换为一个object
,后来我对对象参数应用了强制转换,以取回所需的int
。
5.7 使用 ParameterizedThreadStart
委托,我可以处理接受多个参数的方法吗?
是的,你可以。演示 4 向您展示了这样一种用法。
演示 4
在这个例子中,您会看到下面这个名为Boundaries
的类,它有一个带有两个int
参数的公共构造函数。
class Boundaries
{
public int lowerLimit;
public int upperLimit;
public Boundaries( int lower, int upper)
{
lowerLimit = lower;
upperLimit = upper;
}
}
并且有一个名为Method4
的静态方法匹配ParameterizedThreadStart
委托的签名。该方法定义如下。
static void Method4(Object limits)
{
Boundaries boundaries = (Boundaries)limits;
int lowerLimit = boundaries.lowerLimit;
int upperLimit = boundaries.upperLimit;
for (int i = lowerLimit; i < upperLimit; i++)
{
Console.WriteLine("---ThreadFour from Method4() prints 4.0{0}", i);
}
}
在Main
里面是下面几行代码。
Thread threadFour = new Thread(Method4);
threadFour.Start(new Boundaries(0, 10));
您可以看到我正在创建一个 Boundaries 类对象,并将 0 和 10 作为参数传递。以类似的方式,您可以传递任意多的参数来构造一个对象,然后将它传递给与ParameterizedThreadStart
委托匹配的方法。
using System;
using System.Threading;
namespace ThreadProgrammingEx4
{
class Boundaries
{
public int lowerLimit;
public int upperLimit;
public Boundaries( int lower, int upper)
{
lowerLimit = lower;
upperLimit = upper;
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Thread Demonstration-4****");
Console.WriteLine("Main thread has started.");
Thread threadOne = new Thread(Method1);
// Same as
//Thread threadOne = new Thread(new ThreadStart(Method1));
Thread threadTwo = new Thread(Method2);
// Same as
//Thread threadTwo = new Thread(new ThreadStart(Method2));
Thread threadThree = new Thread(Method3);
// Same as
//Thread threadThree = new Thread(new ParameterizedThreadStart(Method3));
Thread threadFour = new Thread(Method4);
// Same as
//Thread threadThree = new Thread(new ParameterizedThreadStart(Method4));
Console.WriteLine("Starting threadOne shortly.");
// threadOne starts
threadOne.Start();
Console.WriteLine("Starting threadTwo shortly.");
// threadTwo starts
threadTwo.Start();
Console.WriteLine("Starting threadThree shortly.Here we use ParameterizedThreadStart delegate.");
// threadThree starts
threadThree.Start(15);
Console.WriteLine("Starting threadFour shortly.Here we use ParameterizedThreadStart delegate.");
// threadFour starts
threadFour.Start(new Boundaries(0,10));
// Waiting for threadOne to finish
threadOne.Join();
// Waiting for threadtwo to finish
threadTwo.Join();
// Waiting for threadthree to finish
threadThree.Join();
Console.WriteLine("Main() method ends now.");
Console.ReadKey();
}
static void Method1()
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine("-ThreadOne from Method1() prints {0}", i);
}
}
static void Method2()
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine("--ThreadTwo from Method2() prints 2.0{0}", i);
}
}
/*
The following method has an object parameter
This method matches the ParameterizedThreadStart delegate signature;because it has a single parameter of type Object and this method doesn't return a value.
*/
static void Method3(Object number)
{
int upperLimit = (int)number;
for (int i = 0; i < upperLimit; i++)
{
Console.WriteLine("---ThreadThree from Method3() prints 3.0{0}", i);
}
}
/*
The following method also has one parameter.This method matches the ParameterizedThreadStart delegate signature; because it has a single parameter of type Object and this method doesn't return a value.
*/
static void Method4(Object limits)
{
Boundaries boundaries = (Boundaries)limits;
int lowerLimit = boundaries.lowerLimit;
int upperLimit = boundaries.upperLimit;
for (int i = lowerLimit; i < upperLimit; i++)
{
Console.WriteLine("---ThreadFour from Method4() prints 4.0{0}", i);
}
}
}
}
输出
这是一种可能的输出。
***Thread Demonstration-4****
Main thread has started.
Starting threadOne shortly.
Starting threadTwo shortly.
-ThreadOne from Method1() prints 0
-ThreadOne from Method1() prints 1
Starting threadThree shortly.Here we use ParameterizedThreadStart delegate.
-ThreadOne from Method1() prints 2
-ThreadOne from Method1() prints 3
-ThreadOne from Method1() prints 4
-ThreadOne from Method1() prints 5
-ThreadOne from Method1() prints 6
-ThreadOne from Method1() prints 7
-ThreadOne from Method1() prints 8
-ThreadOne from Method1() prints 9
---ThreadThree from Method3() prints 3.00
---ThreadThree from Method3() prints 3.01
---ThreadThree from Method3() prints 3.02
---ThreadThree from Method3() prints 3.03
---ThreadThree from Method3() prints 3.04
---ThreadThree from Method3() prints 3.05
---ThreadThree from Method3() prints 3.06
--ThreadTwo from Method2() prints 2.00
--ThreadTwo from Method2() prints 2.01
--ThreadTwo from Method2() prints 2.02
--ThreadTwo from Method2() prints 2.03
--ThreadTwo from Method2() prints 2.04
--ThreadTwo from Method2() prints 2.05
--ThreadTwo from Method2() prints 2.06
--ThreadTwo from Method2() prints 2.07
--ThreadTwo from Method2() prints 2.08
--ThreadTwo from Method2() prints 2.09
---ThreadThree from Method3() prints 3.07
Starting threadFour shortly.Here we use ParameterizedThreadStart delegate.
---ThreadThree from Method3() prints 3.08
---ThreadThree from Method3() prints 3.09
---ThreadThree from Method3() prints 3.010
---ThreadThree from Method3() prints 3.011
---ThreadThree from Method3() prints 3.012
---ThreadThree from Method3() prints 3.013
---ThreadThree from Method3() prints 3.014
---ThreadFour from Method4() prints 4.00
---ThreadFour from Method4() prints 4.01
---ThreadFour from Method4() prints 4.02
---ThreadFour from Method4() prints 4.03
---ThreadFour from Method4() prints 4.04
---ThreadFour from Method4() prints 4.05
---ThreadFour from Method4() prints 4.06
---ThreadFour from Method4() prints 4.07
---ThreadFour from Method4() prints 4.08
Main() method ends now.
---ThreadFour from Method4() prints 4.09
分析
我没有用Join()
代替threadFour
,所以主线程有可能在threadFour
完成它的任务之前就结束了。
问答环节
5.8 parameterized threadstart 委托不处理具有非 void 返回类型的方法。但是,如果我需要获得退货信息,我应该如何进行?
你可以用不同的方式处理它。例如,在第六章中,你将学习不同的技术来实现异步编程。在那一章中,您将看到一个基于任务的异步模式演示,其中有一个返回string
结果的方法。如果您想处理一个返回不同数据类型的方法,比如说一个int
,您可以使用类似的方法。
现在,您可以使用 lambda 表达式来获得您想要的结果。演示 5 展示了这样一个例子。(作为变体,我在这个例子中使用了字符串插值来打印控制台消息。)
演示 5
这个演示是一个例子,在这个例子中,您可以使用 lambda 表达式来执行在不同线程中运行的两个不同的方法(使用返回类型)。
using System;
using System.Threading;
namespace ThreadProgrammingEx5
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Dealing methods with return types.These methods run in different threads.***");
int myInt = 0;//Initial value
Console.WriteLine($"Inside Main(),ManagedThreadId:{Thread.CurrentThread.ManagedThreadId}");
Thread threadOne = new Thread(
() => {
Console.WriteLine($"Method1() is executing in ManagedThreadId:{Thread.CurrentThread.ManagedThreadId}");
// Do some activity/task
myInt = 5;//An arbitrary value
});
string myStr = "Failure"; // Initial value
Thread threadTwo = new Thread(
() => {
Console.WriteLine($"Method2() is executing in ManagedThreadId:{Thread.CurrentThread.ManagedThreadId}");
// Do some activity/task
myStr = "Success.";
});
Console.WriteLine("Starting threadOne shortly.");
// threadOne starts
threadOne.Start();
Console.WriteLine("Starting threadTwo shortly.");
// threadTwo starts
threadTwo.Start();
// Waiting for threadOne to finish
threadOne.Join();
// Waiting for threadtwo to finish
threadTwo.Join();
Console.WriteLine($"Method1() returns {myInt}");
Console.WriteLine($"Method2() returns {myStr} ");
Console.WriteLine("Control comes at the end of Main() method.");
Console.ReadKey();
}
}
}
输出
这是一种可能的输出。
***Dealing methods with return types.These methods run in different threads.***
Inside Main(),ManagedThreadId:1
Starting threadOne shortly.
Starting threadTwo shortly.
Method1() is executing in ManagedThreadId:3
Method2() is executing in ManagedThreadId:4
Method1() returns 5
Method2() returns Success.
Control comes at the end of Main() method.
Note
ManagedThreadId 只为特定的托管线程获取唯一标识符。当您在机器上运行应用时,您可能会注意到一个不同的值。不要觉得既然你已经创建了 n 个线程,你应该只看到 1 和 n 之间的线程 id。可能还有其他线程也在后台运行。
*### 问答环节
5.9 在本章中,你使用的术语是。这是什么意思?
**当您执行程序时,一个线程会自动启动。这是主线。这些演示中的Main()
方法正在创建主线程,它在Main()
方法结束时终止。当我使用Thread
类创建其他线程时,我将它们称为子线程。在这种情况下,需要注意的是Thread.CurrentThread
属性可以帮助您获得关于线程的信息;例如,您可以使用下面几行代码来获取线程的名称(可以在前面设置)、ID 和优先级。
Console.WriteLine("Inside Main,Thread Name is:{0}", Thread.CurrentThread.Name);
Console.WriteLine("Inside Main,ManagedThreadId is:{0}", Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("Inside Main,Thread Priority is: {0}", Thread.CurrentThread.Priority);
在编写时,C# 中的一个线程可以有以下优先级: Lowest
、BelowNormal
、Normal
、AboveNormal
、and Highest
。图 5-3 显示了 Visual Studio 的部分屏幕截图,其中显示了关于ThreadPriority
枚举的信息。
图 5-3
C# 中不同的线程优先级
演示 6
这个演示展示了我们刚刚讨论过的Thread
类中的Name
、Priority
和ManagedThreadId
属性的用法。
using System;
using System.Threading;
namespace UsingMainThread
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Working on the main thread and a child Thread only.****");
Thread.CurrentThread.Name = "Main Thread";
Thread threadOne = new Thread(Method1);
threadOne.Name = "Child Thread-1";
threadOne.Priority = ThreadPriority.AboveNormal;
Console.WriteLine("Starting threadOne shortly.");
// threadOne starts
threadOne.Start();
Console.WriteLine("Inside Main,Thread Name is:{0}", Thread.CurrentThread.Name);
Console.WriteLine("Inside Main,ManagedThreadId is:{0}", Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("Inside Main,Thread Priority is: {0}", Thread.CurrentThread.Priority);
Console.WriteLine("Control comes at the end of Main() method.");
Console.ReadKey();
}
static void Method1()
{
Console.WriteLine("Inside Method1(),Thread Name is:{0}", Thread.CurrentThread.Name);
Console.WriteLine("Inside Method1(),ManagedThreadId is:{0}", Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("Inside Method1(),Thread Priority is:{0}", Thread.CurrentThread.Priority);
for (int i = 0; i < 5; i++)
{
Console.WriteLine("Using Method1(), printing the value {0}", i);
}
}
}
}
输出
这是一个可能的输出。
***Working on the main thread and a child Thread only.****
Starting threadOne shortly.
Inside Main,Thread Name is:Main Thread
Inside Main,ManagedThreadId is:1
Inside Method1(),Thread Name is:Child Thread-1
Inside Method1(),ManagedThreadId is:5
Inside Method1(),Thread Priority is:AboveNormal
Using Method1(), printing the value 0
Using Method1(), printing the value 1
Using Method1(), printing the value 2
Using Method1(), printing the value 3
Using Method1(), printing the value 4
Inside Main,Thread Priority is: Normal
Control comes at the end of Main() method.
分析
尽管子线程的优先级高于主线程,但这并不保证子线程会在主线程之前完成。有几个其他因素可能会决定这个输出。
问答环节
5.10“它不能保证子线程会在主线程之前完成。还有其他几个因素可能决定这一产出”。你能详细说明一下吗?
从概念上讲,优先级决定了一个线程获得 CPU 时间的频率。理论上,高优先级线程比低优先级线程获得更多的 CPU 时间,在抢占式调度中,它们可以抢占低优先级线程。但是,你需要考虑许多其他因素。例如,可能会发生高优先级线程正在等待获取共享资源,因此被阻塞的情况;在这种情况下,低优先级线程可以有机会完成它的任务。
考虑另一种情况,低优先级线程正在执行一个非常短的任务,而高优先级线程正在执行一个非常长的任务。如果低优先级线程有机会执行,它会在高优先级线程之前完成。
最后,任务调度在操作系统中的实现方式也很重要,因为 CPU 分配也取决于此。这就是为什么你不应该完全依赖优先级来预测产出。
如何终止一个线程?
通过使用在Thread
类中定义的Abort()
方法,您可以终止一个线程。
下面是一些示例代码。
threadOne.Abort();
Abort()
方法有两个不同的重载版本,如下。
public void Abort();
public void Abort(object stateInfo);
前台线程与后台线程
Thread 类有一个名为IsBackground
的属性,描述如下。
//
// Summary:
// Gets or sets a value indicating whether or not a thread is a // background thread.
//
// Returns:
// true if this thread is or is to become a background thread; // otherwise, false.
//
// Exceptions:
// T:System.Threading.ThreadStateException:
// The thread is dead.
public bool IsBackground { get; set; }
默认情况下,线程是前台线程。当您将IsBackground
属性设置为true
时,您可以将前台线程转换为后台线程。以下代码片段可以帮助您更好地理解这一点。(我做了两个Thread
类对象:threadFour
和threadFive
。稍后我会做一个threadFive
后台线程。我用注释标记了这个部分的预期输出)。
Thread threadFour = new Thread(Method1);
Console.WriteLine("Is threadFour is a background thread?:{0} ", threadFour.IsBackground); // False
Thread threadFive = new Thread(Method1);
threadFive.IsBackground = true;
Console.WriteLine("Is threadFive is a background thread?:{0} ", threadFive.IsBackground); // True
如果您想要一个完整的演示,请考虑下面的例子。
演示 7
在Main()
中,我只创建了一个线程。我将其命名为Child Thread-1
,并将IsBackground
属性设置为 true。现在运行这个程序,并遵循输出和相应的讨论。
using System;
using System.Threading;
namespace TestingBackgroundThreads
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Comparing a foreground threads with a background thread****");
Thread.CurrentThread.Name = "Main Thread";
Console.WriteLine($"{Thread.CurrentThread.Name} has started.");
Thread childThread = new Thread(MyMethod);
childThread.Name = "Child Thread-1";
Console.WriteLine("Starting Child Thread-1 shortly.");
// threadOne starts
childThread.Start();
childThread.IsBackground = true;
Console.WriteLine("Control comes at the end of Main() method.");
//Console.ReadKey();
}
static void MyMethod()
{
Console.WriteLine($"{Thread.CurrentThread.Name} enters into MyMethod()");
for (int i = 0; i < 10; i++)
{
Console.WriteLine($"{Thread.CurrentThread.Name} from MyMethod() prints {i}");
//Taking a small sleep
Thread.Sleep(100);
}
Console.WriteLine($"{Thread.CurrentThread.Name} exits from MyMethod()");
}
}
}
输出
这是一个可能的输出。
***Comparing a forground threads with a background thread****
Main Thread has started.
Starting Child Thread-1 shortly.
Control comes at the end of Main() method.
Child Thread-1 enters into MyMethod()
Child Thread-1 from MyMethod() prints 0
但是如果您注释掉前面示例中的下面一行,如下所示,
//childThread.IsBackground = true;
您可能会得到以下输出。
***Comparing a forground threads with a background thread****
Main Thread has started.
Starting Child Thread-1 shortly.
Control comes at the end of Main() method.
Child Thread-1 enters into MyMethod()
Child Thread-1 from MyMethod() prints 0
Child Thread-1 from MyMethod() prints 1
Child Thread-1 from MyMethod() prints 2
Child Thread-1 from MyMethod() prints 3
Child Thread-1 from MyMethod() prints 4
Child Thread-1 from MyMethod() prints 5
Child Thread-1 from MyMethod() prints 6
Child Thread-1 from MyMethod() prints 7
Child Thread-1 from MyMethod() prints 8
Child Thread-1 from MyMethod() prints 9
Child Thread-1 exits from MyMethod()
这告诉你子线程(又名工作线程)能够完成它的任务;当您不使它成为后台线程时,它可以在主线程完成其执行后继续其任务。
附加说明
我还在将IsBackground
属性设置为true
时注释了下面一行。
//Console.ReadKey();
这是因为我不想等待用户输入。我希望一旦主线程死亡,子线程立即终止。
Note
在许多上下文中(特别是在 UI 应用中),您会看到术语工作线程。它描述了不同于当前线程的另一个线程。从技术上来说,它是一个在后台运行的线程,尽管没有人声称这是真正的定义。微软写道,“工作线程通常用于处理后台任务,用户不必等待就可以继续使用你的应用。诸如重新计算和后台打印之类的任务就是工作线程的典型例子。”(见 https://docs.microsoft.com/en-us/cpp/parallel/multithreading-creating-worker-threads?view=vs-2019
)。
微软说,在 C# 的环境中,“默认情况下,一个. NET 程序是由一个单独的线程启动的,通常被称为主线程。但是,它可以创建额外的线程来与主线程并行或并发地执行代码。这些线程通常被称为工作线程。(参见 https://docs.microsoft.com/en-us/dotnet/standard/threading/threads-and-threading
)。
线程安全
有时多个线程需要访问共享资源。控制这些情况是棘手的;例如,考虑当一个线程试图从文件中读取数据,而另一个线程仍在同一文件中写入或更新数据。如果你不能管理正确的顺序,你可能会得到令人惊讶的结果。在这些情况下,同步的概念很有用。
非同步版本
为了理解同步方法的必要性,让我们从一个没有实现这个概念的程序开始。在下面的演示中,一个名为SharedResource
的类包含一个名为SharedMethod()
的公共方法。让我们假设在这个方法中,有可以在多个线程之间共享的资源。为了简单起见,我用一些简单的语句来表示线程的入口和出口。为了精确地观察效果,我在方法体中放了一个简单的Sleep
语句。它增加了将执行切换到另一个线程的可能性。
我在Main
方法中创建了两个子线程:Child Thread-1
和Child Thread-2
。请注意演示中的以下代码行。
SharedResource sharedObject = new SharedResource();
Thread threadOne = new Thread(sharedObject.SharedMethod);
threadOne.Name = "Child Thread-1";
Thread threadTwo = new Thread(sharedObject.SharedMethod);
threadTwo.Name = "Child Thread-2";
一旦您运行这个程序的非同步版本,您可能会在可能的输出中注意到下面几行。
子线程-1 已经进入共享位置。
子线程-2 已经进入共享位置。
子线程-1 退出。
子线程-2 退出。
从这个输出片段中,您可以看到Child Thread-1
首先进入了共享位置。但是在它完成执行之前,Child Thread-2
也已经进入了共享位置。
当您处理共享资源(或共享位置)时,您需要非常小心,因此如果有任何线程正在那里工作,您可能希望限制任何其他线程进入该位置。
演示 8
这个完整的例子描述了这种情况。
using System;
using System.Threading;
namespace ExploringTheNeedofSynchronizationInDotNetCore
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Exploring Thread Synchronization.****");
Console.WriteLine("***We are beginning with a non-synchronized version.****");
Thread.CurrentThread.Name = "Main Thread";
Console.WriteLine("Main thread has started already.");
SharedResource sharedObject = new SharedResource();
Thread threadOne = new Thread(sharedObject.SharedMethod);
threadOne.Name = "Child Thread-1";
Thread threadTwo = new Thread(sharedObject.SharedMethod);
threadTwo.Name = "Child Thread-2";
// Child Thread-1 starts.
threadOne.Start();
// Child Thread-2 starts.
threadTwo.Start();
// Waiting for Child Thread-1 to finish.
threadOne.Join();
// Waiting for Child Thread-2 to finish.
threadTwo.Join();
Console.WriteLine("The {0} exits now.", Thread.CurrentThread.Name);
Console.ReadKey();
}
}
class SharedResource
{
public void SharedMethod()
{
Console.Write(Thread.CurrentThread.Name + " has entered in the shared location. \n");
Thread.Sleep(3000);
Console.Write(Thread.CurrentThread.Name + " exits.\n");
}
}
}
输出
这是一个完全可能的输出。
***Exploring Thread Synchronization.****
***We are beginning with a non-synchronized version.****
Main thread has started already.
Child Thread-1 has entered in the shared location.
Child Thread-2 has entered in the shared location.
Child Thread-1 exits.
Child Thread-2 exits.
The Main Thread exits now.
Note
每次在系统中运行时,此输出可能会有所不同。为了获得相同的输出,您可能需要多次执行该应用。
同步版本
我相信你现在明白同步版本的必要性了。所以,让我们实现同步的概念,更新前面的演示。
演示 9
在本演示中,您将看到锁的使用。这种锁定机制通常防止由于同时访问共享位置中的多个线程而导致的共享资源的意外修改;当你成功地实现了这一点,你就可以说你的应用是线程安全的。
首先,我来解释一些常用术语。这些术语经常在类似的上下文中使用。您想要防止多个线程同时访问的代码段被称为临界段。在任何给定的时刻,您只允许一个线程在临界区工作。这个原理被称为互斥。实施这一原则的机制通常被称为互斥。
当线程获得锁时,它可以进入临界区。一旦它的工作完成,它就从这个位置退出并释放锁。现在,另一个线程可以获得锁并继续。但是如果一个线程想进入临界区,看到锁当前被另一个线程持有,它就不能进入。线程需要暂停活动,直到锁被释放。
你如何创建一个锁?这非常简单,通常用同一个对象/类中的私有实例变量来完成,如下所示。
private object myLock = new object(); // You can use any object.
如评论所说,你可以随心所欲的做锁。例如,如果您处理一个静态方法,您甚至可以编写如下代码。
private static StringBuilder strLock = new StringBuilder();
为了在前面的演示中实现线程安全,您可以如下修改SharedResource
类。(注意以粗体显示的新引入的行。)我还在Main
方法内部做了一些修改,以表明它是一个同步版本。因此,我将替换上一个演示中的下面一行
Console.WriteLine("***We are beginning with a non-synchronized version.****");
在接下来的演示中。
Console.WriteLine("***Here we have a synchronized version.We are using the concept of lock.****");
class SharedResource
{
private object myLock = new object();
public void SharedMethod()
{
lock (myLock)
{
Console.Write(Thread.CurrentThread.Name + " has entered in the shared location. \n");
Thread.Sleep(3000);
Console.Write(Thread.CurrentThread.Name + " exits.\n");
}
}
}
输出
这一次,您会得到以下输出。
***Exploring Thread Synchronization.****
***Here we have a synchronized version.We are using the concept of lock.****
Main thread has started already.
Child Thread-1 has entered in the shared location.
Child Thread-1 exits.
Child Thread-2 has entered in the shared location.
Child Thread-2 exits.
The Main Thread exits now.
你需要记住,当你如下使用 lock 语句时,myLock
是一个引用类型的表达式。
lock(myLock){ // Some code},
例如,在这个例子中,myLock
是一个Object
实例,它只是一个引用类型。但是,如果在如下上下文中使用值类型,而不是使用引用类型,
private int myLock = new int();//not correct
您将得到以下错误。
Error CS0185 'int' is not a reference type as required by the lock statement
使用 Monitor 类的另一种方法
在Monitor
类,
中,成员实现同步。既然你已经看到了locks
的使用,值得注意的是它内部包装了Monitor
的Entry
和Exit
方法。因此,您可以替换下面的代码段
lock (myLock)
{
Console.Write(Thread.CurrentThread.Name + " has entered in the shared location. \n");
Thread.Sleep(3000);
Console.Write(Thread.CurrentThread.Name + " exits.\n");
}
使用Monitor
的Entry
和Exit
方法的等价代码段,如下所示。
// lock internally wraps Monitor's Entry and Exit method in a try...// finally block.
try
{
Monitor.Enter(myLock);
Console.Write(Thread.CurrentThread.Name + " has entered in the shared location. \n");
Thread.Sleep(3000);
Console.Write(Thread.CurrentThread.Name + " exits.\n");
}
finally
{
Monitor.Exit(myLock);
}
除了这些方法之外,Monitor
类还有其他方法可以发送通知。例如,在这个类中,您可以看到具有不同重载版本的Wait
、Pulse
和PulseAll
方法。以下是对这些方法的简单描述。
-
Wait()
:
使用这种方法,一个线程可以等待其他线程的通知。 -
Pulse()
:
使用这种方法,一个线程可以向另一个线程发送通知。 -
PulseAll()
:
使用这种方法,一个线程可以通知一个进程内的所有其他线程。
除了这些方法,还有另一个有趣的重载版本的方法叫做TryEnter
。
这是这种方法的最简单形式,带有来自 Visual Studio 的说明。
//
// Summary:
// Attempts to acquire an exclusive lock on the specified object.
//
// Parameters:
// obj:
// The object on which to acquire the lock.
//
// Returns:
// true if the current thread acquires the lock; otherwise, false.
//
// Exceptions:
// T:System.ArgumentNullException:
// The obj parameter is null.
public static bool TryEnter(object obj);
如果调用线程可以获得所需对象的锁,TryEnter
方法返回布尔值true
;否则,它将返回false
。使用此方法的不同重载版本,您可以指定一个时间限制,在该时间限制内,您可以尝试获取所需对象的独占锁。
僵局
死锁是一种情况或条件,其中至少有两个进程或线程在等待对方完成或释放控制,以便每个进程或线程都可以完成其工作。这可能导致它们都无法启动(并且它们进入挂起状态。)你可能经常听说这些现实生活中的例子。
没有经验,你找不到工作;没有工作就无法获得经验。
或者,
两个好朋友吵架后,他们都希望对方能重新开始友谊。
POINTS TO REMEMBER
如果没有同步,您可能会看到令人惊讶的输出(例如,一些损坏的数据),但是如果同步使用不当,您可能会遇到死锁。
死锁的类型
理论上,有不同类型的死锁。
-
资源死锁。假设两个进程(P1 和 P2)拥有两个资源(分别是 R1 和 R2)。P1 请求资源 R2 和 P2 请求资源 R1 来完成他们的工作。操作系统通常处理这种类型的死锁。
-
同步死锁。假设流程 P1 仅在 P2 完成特定操作(a2)后等待执行操作(a1),P2 仅在 P1 完成 a1 后等待完成操作 a2。
-
通信死锁。与前面的场景类似,您可以用消息替换动作/资源的概念(即,两个流程等待从对方接收消息以进一步处理)。
在这一章中,我们关注的是多线程环境,所以我们只讨论在 C# 应用中由多线程引起的死锁。
演示 10
下面的程序会导致死锁。这个程序中使用了两个锁。这些锁分别被称为myFirstLock
和mySecondLock
。出于演示目的,本例中显示了错误的设计;你看Child Thread-1
试图获得myFirstLock
,然后mySecondLock
和Child Thread-2
试图以相反的顺序获得锁。因此,当两个线程同时锁定它们的第一个锁时,它们会遇到死锁的情况。
同样,下面是一个不正确的实现,仅用于演示目的。
using System;
using System.Threading;
namespace DeadlockDemoInDotNetCore
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Exploring Deadlock with an incorrect design of application.****");
Thread.CurrentThread.Name = "Main Thread";
Console.WriteLine("Main thread has started already.");
SharedResource sharedObject = new SharedResource();
Thread threadOne = new Thread(sharedObject.SharedMethodOne);
threadOne.Name = "Child Thread-1";
Thread threadTwo = new Thread(sharedObject.SharedMethodTwo);
threadTwo.Name = "Child Thread-2";
// Child Thread-1 starts.
threadOne.Start();
// Child Thread-2 starts.
threadTwo.Start();
// Waiting for Child Thread-1 to finish.
threadOne.Join();
// Waiting for Child Thread-2 to finish.
threadTwo.Join();
Console.WriteLine("The {0} exits now.", Thread.CurrentThread.Name);
Console.ReadKey();
}
}
class SharedResource
{
private object myFirstLock = new object();
private object mySecondLock = new object();
public void SharedMethodOne()
{
lock (myFirstLock)
{
Console.Write(Thread.CurrentThread.Name + " has entered into first part of SharedMethodOne. \n");
Thread.Sleep(1000);
Console.Write(Thread.CurrentThread.Name + " exits SharedMethodOne--first part.\n");
lock (mySecondLock)
{
Console.Write(Thread.CurrentThread.Name + " has entered into last part of SharedMethodOne. \n");
Thread.Sleep(1000);
Console.Write(Thread.CurrentThread.Name + " exits SharedMethodOne--last part.\n");
}
}
}
public void SharedMethodTwo()
{
lock (mySecondLock)
{
Console.Write(Thread.CurrentThread.Name + " has entered into first part of SharedMethodTwo. \n");
Thread.Sleep(1000);
Console.Write(Thread.CurrentThread.Name + " exits SharedMethodTwo--first part.\n");
lock (myFirstLock)
{
Console.Write(Thread.CurrentThread.Name + " has entered into last part of SharedMethodTwo. \n");
Thread.Sleep(1000);
Console.Write(Thread.CurrentThread.Name + " exits SharedMethodTwo--last part.\n");
}
}
}
}
}
输出
当您的程序挂起时,您只能在输出中看到以下几行。
***Exploring Deadlock with an incorrect design of application.****
Main thread has started already.
Child Thread-1 has entered into first part of SharedMethodOne.
Child Thread-2 has entered into first part of SharedMethodTwo.
Child Thread-1 exits SharedMethodOne--first part.
Child Thread-2 exits SharedMethodTwo--first part.
Note
第一次运行可能不会遇到死锁,但会一直执行程序;最终会出现你看到死锁的情况。
调查 Visual Studio 中的死锁状态
在这种挂起状态下,进入调试 ➤ 断开所有。然后进入调试 ➤ 窗口 ➤ 线程。
你会看到一个类似图 5-4 的屏幕。请注意,您可以看到主线程和子线程的状态。让我们打开所有的窗户(见图 5-4 、 5-5 和 5-6 )。
图 5-4 为主线程窗口。
图 5-4
主线程窗口处于死锁状态
图 5-5 为子线程 1 窗口。
图 5-5
处于死锁状态的子线程 1 窗口
图 5-6 为子线程 2 窗口。
图 5-6
子线程 2 窗口处于死锁状态
如果将窗口分割成垂直和水平两部分,可以一次看到全部,如图 5-7 所示。
图 5-7
死锁状态下主线程、子线程 1、子线程 2 窗口一览
Note
要垂直拆分一个窗口,你可以去窗口 ➤ 新窗口创建一个标签的克隆。请注意名为 Program.cs:1 和 Program2.cs:2 的选项卡。右击其中任何一个,然后选择新建垂直选项卡组。类似地,要水平分割窗口,选择新建水平选项卡组。在图 5-7 中,我垂直划分了窗口,然后水平划分了其中一个。
您应该能够清楚地看到线程被子线程中的锁语句卡住了。作为副作用,Main
螺纹也卡在了threadOne.Join().
上
在多线程程序的执行过程中,如果 Visual Studio 向您显示挂起状态,您可以用类似的方式调查原因。还需要注意的是,死锁可能发生在许多不同的情况下,但是本节的重点是锁。
这是线程编程的基础。像任何高级主题一样,深入的讨论需要更多的页面。不过,你现在应该对基本面有一个公平的想法。
最后的话
现在你可能明白为什么从下面这行代码开始了。
using System.Threading;
是因为这个命名空间有Thread
类,是线程编程的基础。Thread
是一个sealed
类,它有很多成员,包括属性、方法、构造函数和析构函数。到目前为止,您已经看到了以下内容。
-
使用以下两个构造函数:
public Thread(ThreadStart start); public Thread(ParameterizedThreadStart start);
-
使用以下属性:
public bool IsBackground { get; set; } public static Thread CurrentThread { get; } public string Name { get; set; } public int ManagedThreadId { get; }
-
使用以下方法:
public void Start(); public void Join(); public void Abort();
还有许多其他成员也是有用的。我建议你看看它们。如果您使用的是 Visual Studio IDE,只需右键单击Thread
类,然后选择转到定义(F2) 来查看它们的定义。
同样值得注意的是,Thread
类的一些成员被弃用,(例如,Suspend
和Resume)
)。Microsoft 建议您为应用使用其他方法,而不是这些方法。例如,在Suspend
方法定义中,您会看到如下内容。
[Obsolete("Thread.Suspend has been deprecated. Please use other classes in System.Threading, such as Monitor, Mutex, Event, and Semaphore, to synchronize Threads or protect resources. http://go.microsoft.com/fwlink/?linkid=14202", false)]
[SecuritySafeCritical]
public void Suspend();
这也告诉您,Monitor
、Mutex
、Event
和Semaphore
是您在程序中实现同步时的重要类。对这些类更详细的描述超出了本书的范围。
最后,当您希望并发执行而不是顺序执行时,您可以使用多线程,并且您可能认为创建线程可以提高应用的性能。但这可能并不总是对的!您应该限制应用中的线程数量,以避免线程间过多的上下文切换。上下文切换带来的开销会降低应用的整体性能。
这些是 C# 中线程编程的基础。多线程是一个复杂的话题,它有几个方面。一整本书都可以用来讨论这个话题。不过,我相信这一章会让你对基本原理有一个清晰的概念。在下一章中,我们将讨论异步编程,你将在类似的环境中学习一些有趣的概念。
摘要
本章讨论了以下关键问题。
-
什么是线程,它与进程有什么不同?
-
如何创建线程?
-
如何使用不同的线程类构造函数?
-
使用
ParameterizedThreadStart
委托,如何使用接受多个参数的方法? -
如何使用重要的
Thread
类成员? -
如何区分前台线程和后台线程?
-
什么是同步,为什么需要同步?
-
在 C# 中如何使用 lock 语句实现线程安全?
-
如何使用 Monitor 的
Entry
和Exit
方法实现另一种锁定语句的方法? -
什么是死锁,如何检测系统中的死锁?***
六、异步编程
异步编程是艰难和具有挑战性的,但也是有趣的。也被称为异步。整体概念不是一天进化出来的;这需要时间。async 和 await 关键字第一次出现在 C# 5.0 中是为了使它更容易。在此之前,不同的程序员使用不同的技术实现了这个概念。每种技术都有其优缺点。本章的目标是向你介绍异步编程,并通过一些常见的实现方法。
概观
我们先来讨论一下什么是异步编程。简单地说,你在你的应用中取一个代码段,并在一个单独的线程上运行它。关键优势是什么?简单的答案是,您可以释放原始线程,让它继续完成剩余的任务,并在一个单独的线程中执行不同的任务。这种机制帮助您开发现代应用;例如,当您实现一个高度响应的用户界面时,这些概念非常有用。
POINTS TO REMEMBER
概括地说,您会注意到异步编程中有三种不同的模式。
-
IAsyncResult 模式:这也被称为异步编程模型(APM) 。在这个模式中,您可以看到支持异步行为的
IAsyncResult
接口。在同步模型中,如果您有一个名为 XXX()的同步方法,在异步版本中,您会看到相应的同步方法使用了BeginXXX()
和EndXXX()
方法。比如在同步版本中,如果Read()
方法支持读操作,在异步编程中,你看到BeginRead()
和EndRead()
方法异步支持相应的读操作。使用这个概念,您可以在演示 5、6 和 7 中看到BeginInvoke
和EndInvoke
方法。然而,新的开发不推荐这种模式。 -
基于事件的异步模式 : 这种模式伴随而来。NET 框架 2.0。它基于一种事件机制。这里您可以看到带有
Async
后缀的方法名、一个或多个事件以及EventArg
派生类型。新开发不推荐使用这种模式。 -
基于任务的异步模式 : 这最早出现在。NET 框架 4。这是目前异步编程的推荐做法。在 C# 中,你经常会看到这种模式中的
async
和await
关键字。
为了更好地理解异步编程,让我们从它的对应物开始讨论:同步编程。同步方法很简单,代码路径也很容易理解;但是你需要等待来自特定代码段的结果,在那之前,你只是无所事事。考虑一些典型案例;例如,当您知道一段代码试图打开一个可能需要时间加载的网页时,或者当一段代码正在运行一个长时间运行的算法时,等等。如果您遵循同步方法,当您执行长时间运行的操作时,您必须闲置,因为您不能做任何有用的事情。
这就是为什么为了支持现代需求和构建高响应性的应用,对异步编程的需求越来越大。
使用同步方法
演示 1 执行一个简单的程序。让我们从同步方法开始。有两个简单的方法:Method1()
和Method2()
。在Main()
方法内部,这些方法被同步调用(即先调用Method1()
,再调用Method2()
)。)我使用了简单的 sleep 语句,因此这些方法执行的任务需要花费大量的时间来完成。一旦您运行应用并注意到输出,您会看到只有在Method1()
完成执行后,Method2()
才开始执行。在这些方法完成它们的执行之前,Main()
方法不能完成。
Note
在本章中,你会看到这些方法略有不同。我试图维护相似的方法(或操作),以便您可以比较异步编程的不同技术。出于演示的目的,Method1()
需要更多的时间来完成,因为它执行了一个冗长的操作(我在其中强制了一个相对较长的睡眠)。Method2()
执行一个小任务,所以我在里面放了一个短睡眠。此外,为了简单起见,我使用了简称。
演示 1
这是完整的演示。
using System;
using System.Threading;
namespace SynchronousProgrammingExample
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Demonstration-1.A Synchronous Program Demo.***");
Method1();
Method2();
Console.WriteLine("End Main().");
Console.ReadKey();
}
// Method1
private static void Method1()
{
Console.WriteLine("Method1() has started.");
// Some big task
Thread.Sleep(1000);
Console.WriteLine("Method1() has finished.");
}
// Method2
private static void Method2()
{
Console.WriteLine("Method2() has started.");
// Some small task
Thread.Sleep(100);
Console.WriteLine("Method2() has finished.");
}
}
}
输出
这是输出。
***Demonstration-1.A Synchronous Program Demo.***
Method1() has started.
Method1() has finished.
Method2() has started.
Method2() has finished.
End Main().
使用线程类
如果您仔细观察演示 1 中的方法,您会发现它们并不相互依赖。如果您可以并行执行它们,您的应用的响应时间将会得到改善,并且您可以减少总的执行时间。让我们找到一些更好的方法。
你在第五章中学到了线程,所以你可以实现多线程的概念。演示 2 向您展示了一个使用线程的显而易见的解决方案。我保留了注释代码供您参考。这个演示的重点是在一个新线程中替换Method1()
。
演示 2
using System;
using System.Threading;
namespace UsingThreadClass
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Asynchronous Programming Demonstration-1.***");
//Method1();
// Old approach.Creating a separate thread for the following // task(i.e Method1.)
Thread newThread = new Thread(()=>
{
Console.WriteLine("Method1() has started on a separate thread.");
// Some big task
Thread.Sleep(1000);
Console.WriteLine("Method1() has finished.");
}
);
newThread.Start();
Thread.Sleep(10);
Method2();
Console.WriteLine("End Main().");
Console.ReadKey();
}
// Method1
//private static void Method1()
//{
// Console.WriteLine("Method1() has started.");
// // Some big task
// Thread.Sleep(1000);
// Console.WriteLine("Method1() has finished.");
//}
private static void Method2()
{
Console.WriteLine("Method2() has started.");
// Some small task
Thread.Sleep(100);
Console.WriteLine("Method2() has finished.");
}
}
}
输出
这是一种可能的输出。
***Asynchronous Programming Demonstration-1.***
Method1() has started on a separate thread.
Method2() has started.
Method2() has finished.
End Main().
Method1() has finished.
分析
注意,虽然Method1()
被提前调用,但是Method2
不需要等待Method1()
完成执行。此外,由于Method2()
做得很少(睡眠时间为 100 毫秒),它能够在Method1()
完成执行之前完成。还要注意,因为主线程没有被阻塞,所以它能够继续执行。
问答环节
6.1 为什么在 Main 中执行 Method2()之前使用 sleep 语句?
接得好。这不是必须的,但是在某些情况下,您可能会注意到,即使您试图在当前线程中的Method2()
之前启动Method1()
在一个单独的线程上执行,这也不会发生,因此,您可能会注意到以下输出。
***Asynchronous Programming Demonstration-1.***
Method2() has started.
Method1() has started in a separate thread.
Method2() has finished.
End Main().
Method1() has finished.
在这个例子中,这个简单的 sleep 语句可以帮助你增加在Method2()
之前开始Method1()
的概率。
使用线程池类
通常不鼓励在现实世界的应用中直接创建线程。以下是这背后的一些主要原因。
-
维护太多的线程会导致困难和高成本的操作。
-
大量时间浪费在上下文切换上,而不是做真正的工作。
为了避免直接创建线程,C# 为您提供了使用内置ThreadPool
类的便利。有了这个类,您可以使用现有的线程,这些线程可以重用以满足您的需要。ThreadPool
类在维护应用中的最佳线程数量方面非常有效。如果需要,您可以使用这个工具异步执行一些任务。
ThreadPool
是一个包含static
方法的静态类,其中一些方法还有一个重载版本。图 6-1 是 Visual Studio IDE 的部分截图,展示了ThreadPool
类中的方法。
图 6-1
Visual Studio 2019 IDE 中 ThreadPool 类的截图
在本节中,我们的重点是QueueUserWorkItem
方法。在图 6-1 中,注意这个方法有两个重载版本。要了解关于此方法的更多信息,让我们展开 Visual Studio 中的方法描述。例如,一旦展开此方法的第一个重载版本,就会看到以下内容。
//
// Summary:
// Queues a method for execution. The method executes when a thread // pool thread becomes available.
//
// Parameters:
// callBack:
// A System.Threading.WaitCallback that represents the method to be // executed.
//
// Returns:
// true if the method is successfully queued; System.// NotSupportedException is thrown
// if the work item could not be queued.
//
// Exceptions:
// T:System.ArgumentNullException:
// callBack is null.
//
// T:System.NotSupportedException:
// The common language runtime (CLR) is hosted, and the host does not// support this action.
[SecuritySafeCritical]
public static bool QueueUserWorkItem(WaitCallback callBack);
如果您进一步研究方法参数,您会发现WaitCallBack
是一个具有以下描述的委托。
//
// Summary:
// Represents a callback method to be executed by a thread pool thread.
//
// Parameters:
// state:
// An object containing information to be used by the callback method.
[ComVisible(true)]
public delegate void WaitCallback(object state);
第二个重载版本的QueueUserWorkItem
可以接受一个名为state
的object
参数。内容如下。
public static bool QueueUserWorkItem(WaitCallback callBack, object state);
如果查看细节,您会发现可以通过这个参数将有价值的数据传递给方法。在演示 3 中,我使用了两个重载版本,并且我引入了Method3
,其中我传递了一个对象参数。
演示 3
为了有效地使用QueueUserWorkItem
方法,您需要一个匹配WaitCallBack
委托签名的方法。在下面的演示中,我在ThreadPool
中对两个方法进行排队。在演示 2 中,Method2 不接受任何参数。如果将它传递给QueueUserWorkItem
,会得到如下编译错误。
No overload for 'Method2' matches delegate 'WaitCallback'
让我们用一个虚拟对象参数来修改Method2
,如下(我保留了注释供你参考)。
/* The following method's signature should match the delegate WaitCallback.*/
private static void Method2(Object state)
{
Console.WriteLine("--Method2() has started.");
// Some small task
Thread.Sleep(100);
Console.WriteLine("--Method2() has finished.");
}
接下来,我们来介绍一下使用了Object
参数的Method3,
。Method3
描述如下。
static void Method3(Object number)
{
Console.WriteLine("---Method3() has started.");
int upperLimit = (int)number;
for (int i = 0; i < upperLimit; i++)
{
Console.WriteLine("---Method3() prints 3.0{0}", i);
}
Thread.Sleep(100);
Console.WriteLine("---Method3() has finished.");
}
现在来看下面的演示和相应的输出。
using System;
using System.Threading;
namespace UsingThreadPool
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Asynchronous Programming Demonstration.***");
Console.WriteLine("***Using ThreadPool.***");
// Using Threadpool
// Not passing any parameter for Method2
ThreadPool.QueueUserWorkItem(new WaitCallback(Method2));
// Passing 10 as the parameter for Method3
ThreadPool.QueueUserWorkItem(new WaitCallback(Method3), 10);
Method1();
Console.WriteLine("End Main().");
Console.ReadKey();
}
private static void Method1()
{
Console.WriteLine("-Method1() has started.");
// Some big task
Thread.Sleep(1000);
Console.WriteLine("-Method1() has finished.");
}
/* The following method's signature should match the delegate WaitCallback.
It is as follows:
public delegate void WaitCallback(object state)
*/
//private static void Method2()//Compilation error
private static void Method2(Object state)
{
Console.WriteLine("--Method2() has started.");
// Some small task
Thread.Sleep(100);
Console.WriteLine("--Method2() has finished.");
}
/*
The following method has a parameter.This method's signature matches the WaitCallBack delegate signature.Notice that this method also matches the ParameterizedThreadStart delegate signature; because it has a single parameter of type Object and this method doesn't return a value.
*/
static void Method3(Object number)
{
Console.WriteLine("---Method3() has started.");
int upperLimit = (int)number;
for (int i = 0; i < upperLimit; i++)
{
Console.WriteLine("---Method3() prints 3.0{0}", i);
}
Thread.Sleep(100);
Console.WriteLine("---Method3() has finished.");
}
}
}
输出
这是一个可能的输出。
***Asynchronous Programming Demonstration.***
***Using ThreadPool.***
-Method1() has started.
---Method3() has started.
---Method3() prints 3.00
---Method3() prints 3.01
---Method3() prints 3.02
---Method3() prints 3.03
--Method2() has started.
---Method3() prints 3.04
---Method3() prints 3.05
---Method3() prints 3.06
---Method3() prints 3.07
---Method3() prints 3.08
---Method3() prints 3.09
--Method2() has finished.
---Method3() has finished.
-Method1() has finished.
End Main().
问答环节
6.2 按照简单的委托实例化技术,如果我使用下面的代码行:
ThreadPool.QueueUserWorkItem(Method2);
instead of this line:
ThreadPool.QueueUserWorkItem(new WaitCallback(Method2));
will the application compile and run?
Yes, but since you are learning to use the WaitCallback delegate now, I kept it for your reference.
对线程池使用 Lambda 表达式
如果您喜欢 lambda 表达式,您可以在类似的上下文中使用它。例如,在演示 3 中,您可以使用 lambda 表达式替换Method3
,如下所示。
// Using lambda Expression
// Here the method needs a parameter(input).
// Passing 10 as the parameter for Method3
ThreadPool.QueueUserWorkItem((number) =>
{
Console.WriteLine("---Method3() has started.");
int upperLimit = (int)number;
for (int i = 0; i < upperLimit; i++)
{
Console.WriteLine("---Method3() prints 3.0{0}", i);
}
Thread.Sleep(100);
Console.WriteLine("---Method3() has finished.");
}, 10
);
在演示 3 中,您可以注释掉下面的代码行,并用前面显示的 lambda 表达式替换Method3
。
ThreadPool.QueueUserWorkItem(new WaitCallback(Method3), 10);
如果您再次执行该程序,您会得到类似的输出。演示 4 是完整的实现,供您参考。
演示 4
using System;
using System.Threading;
namespace UsingThreadPoolWithLambdaExpression
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Asynchronous Programming Demonstration.***");
Console.WriteLine("***Using ThreadPool with Lambda Expression.***");
// Using Threadpool
// Not passing any parameter for Method2
ThreadPool.QueueUserWorkItem(Method2);
// Using lambda Expression
// Here the method needs a parameter(input).
// Passing 10 as the parameter for Method3
ThreadPool.QueueUserWorkItem( (number) =>
{
Console.WriteLine("--Method3() has started.");
int upperLimit = (int)number;
for (int i = 0; i < upperLimit; i++)
{
Console.WriteLine("---Method3() prints 3.0{0}", i);
}
Thread.Sleep(100);
Console.WriteLine("--Method3() has finished.");
}, 10
);
Method1();
Console.WriteLine("End Main().");
Console.ReadKey();
}
private static void Method1()
{
Console.WriteLine("-Method1() has started.");
// Some task
Thread.Sleep(500);
Console.WriteLine("-Method1() has finished.");
}
/* The following method's signature should match the delegate WaitCallback.
It is as follows:
public delegate void WaitCallback(object state)
*/
//private static void Method2()//Compilation error
private static void Method2(Object state)
{
Console.WriteLine("--Method2() has started.");
// Some task
Thread.Sleep(100);
Console.WriteLine("--Method2() has finished.");
}
}
}
输出
这是一个可能的输出。
***Asynchronous Programming Demonstration.***
***Using ThreadPool with Lambda Expression.***
-Method1() has started.
--Method3() has started.
---Method3() prints 3.00
--Method2() has started.
---Method3() prints 3.01
---Method3() prints 3.02
---Method3() prints 3.03
---Method3() prints 3.04
---Method3() prints 3.05
---Method3() prints 3.06
---Method3() prints 3.07
---Method3() prints 3.08
---Method3() prints 3.09
--Method2() has finished.
--Method3() has finished.
-Method1() has finished.
End Main().
Note
这一次,您看到了 lambda 表达式在ThreadPool
类中的使用。在演示 2 中,您看到了 lambda 表达式在Thread
类中的使用。
使用 IAsyncResult 模式
IAsyncResult
接口帮助您实现异步行为。让我们回忆一下我早些时候告诉你的话。在同步模型中,如果有一个名为 XXX 的同步方法,在异步版本中,BeginXXX
和EndXXX
方法就是对应的同步方法。让我们仔细看看。
使用异步委托进行轮询
到目前为止,您已经看到了委托的许多不同用法。在本节中,您将了解另一种用法,即通过使用委托,您可以异步调用方法。轮询是一种重复检查条件的机制。在演示 5 中,让我们检查一个委托实例是否完成了它的任务。
演示 5
有两种方法叫做Method1
和Method2
。让我们再次假设Method1
比Method2
花费更多的时间来完成它的任务。简单来说,Sleep()
语句在这些方法内部传递。在这个例子中,Method1
接收一个休眠 3000 毫秒的参数,Method2
休眠 100 毫秒。
现在看看代码的重要部分。首先,创建一个委托实例来匹配Method1
签名。Method1
如下。
// Method1
private static void Method1(int sleepTimeInMilliSec)
{
Console.WriteLine("Method1() has started.");
Console.WriteLine("Inside Method1(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
// Some big task
Thread.Sleep(sleepTimeInMilliSec);
Console.WriteLine("\nMethod1() has finished.");
}
为了匹配签名,如下声明Method1Delegate
。
public delegate void Method1Delegate(int sleepTimeinMilliSec);
稍后,如下实例化它。
Method1Delegate method1Del = Method1;
到目前为止,一切都很简单。现在我们来看代码中最重要的一行,如下所示。
IAsyncResult asyncResult = method1Del.BeginInvoke(3000, null, null);
你还记得在委托的上下文中,你可以使用Invoke()
方法吗?但是上一次,代码遵循同步路径。现在我们正在探索异步编程,您会看到BeginInvoke
和EndInvoke
方法的使用。当 C# 编译器看到 delegate 关键字时,它会为动态生成的类提供这些方法。
BeginInvoke
方法的返回类型是IAsyncResult
。如果您将鼠标悬停在BeginInvoke
上或者注意它的结构,您会看到虽然Method1
只接受一个参数,但是BeginInvoke
方法总是接受两个额外的参数——一个类型为AsyncCallback
,一个类型为object
。我很快会讨论它们。
在这个例子中,我只使用第一个参数,并将 3000 毫秒作为Method1
的参数。但是对于BeginInvoke
的最后两个参数,我通过了null
。
BeginInvoke
的结果很重要。我将结果保存在一个IAsyncResult
对象中。IAsyncResult
具有以下只读属性。
public interface IAsyncResult
{
bool IsCompleted { get; }
WaitHandle AsyncWaitHandle { get; }
object AsyncState { get; }
bool CompletedSynchronously { get; }
}
目前,我关注的是isCompleted
属性。如果您进一步扩展这些定义,您会看到isCompleted
的定义如下。
//
// Summary:
// Gets a value that indicates whether the asynchronous operation has// completed.
//
// Returns:
// true if the operation is complete; otherwise, false.
bool IsCompleted { get; }
很明显,您可以使用这个属性来验证委托是否已经完成了它的工作。
在下面的例子中,我检查另一个线程中的委托是否已经完成了它的工作。如果工作没有完成,我会在控制台窗口中打印星号(*),并强制主线程短暂休眠。这就是您在本演示中看到下面这段代码的原因。
while (!asyncResult.IsCompleted)
{
// Keep working in main thread
Console.Write("*");
Thread.Sleep(5);
}
最后,EndInvoke
方法接受一个类型为IAsyncResult
的参数。我通过asyncResult
作为这个方法中的一个参数。
现在进行完整的演示。
using System;
using System.Threading;
namespace PollingDemo
{
class Program
{
public delegate void Method1Delegate(int sleepTimeinMilliSec);
static void Main(string[] args)
{
Console.WriteLine("***Polling Demo.***");
Console.WriteLine("Inside Main(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
// Synchronous call
//Method1(3000);
Method1Delegate method1Del = Method1;
IAsyncResult asyncResult = method1Del.BeginInvoke(3000, null, null);
Method2();
while (!asyncResult.IsCompleted)
{
// Keep working in main thread
Console.Write("*");
Thread.Sleep(5);
}
method1Del.EndInvoke(asyncResult);
Console.ReadKey();
}
// Method1
private static void Method1(int sleepTimeInMilliSec)
{
Console.WriteLine("Method1() has started.");
Console.WriteLine("Inside Method1(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
// Some big task
Thread.Sleep(sleepTimeInMilliSec);
Console.WriteLine("\nMethod1() has finished.");
}
// Method2
private static void Method2()
{
Console.WriteLine("Method2() has started.");
Console.WriteLine("Inside Method2(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
// Some small task
Thread.Sleep(100);
Console.WriteLine("Method2() has finished.");
}
}
}
输出
这是一种可能的输出。
***Polling Demo.***
Inside Main(),Thread id 1 .
Method2() has started.
Inside Method2(),Thread id 1 .
Method1() has started.
Inside Method1(),Thread id 3 .
Method2() has finished.
***********************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
Method1() has finished.
问答环节
6.3 上一个案例中,Method1 带一个参数, BeginInvoke
带三个参数。如果 Method1
接受 n
数量的参数,那么 BeginInvoke
就会有 n+2
参数。
是的,初始的参数集是基于您的方法的,但是对于最后两个参数,一个是类型AsyncCallback
的,最后一个是类型object
的。
POINTS TO REMEMBER
-
这种类型的例子在。NET 框架 4.7.2。如果你在。NET Core 3.0,你得到一个异常说“系统。PlatformNotSupportedException:此平台不支持操作。其中一个主要原因是异步委托实现依赖于中不存在的远程处理功能。NET 核心。关于这一点的讨论是在
https://github.com/dotnet/runtime/issues/16312
。 -
如果您不想在控制台窗口中检查和打印星号(*),您可以在主线程完成执行后简单地调用委托类型的
EndInvoke()
方法。EndInvoke()
方法一直等到代理完成它的工作。 -
If you don’t explicitly examine whether the delegate finishes its execution or not, or you simply forget to call
EndInvoke()
, the thread of the delegate is stopped after the main thread dies. For example, if you comment out the following segment of code from the prior example,//while (!asyncResult.IsCompleted) //{ // Keep working in main thread // Console.Write("*"); // Thread.Sleep(5); //} //method1Del.EndInvoke(asyncResult); //Console.ReadKey();
并再次运行应用,您可能看不到
"Method1() has finished."
语句。 -
BeginInvoke
通过使用EndInvoke
帮助调用线程稍后获得异步方法调用的结果。
使用 IAsyncResult 的 AsyncWaitHandle 属性
现在我将向您展示一种使用另一个属性AsyncWaitHandle
的替代方法,这个属性在IAsyncResult
中也是可用的。如果看到IAsyncResult
的内容,发现AsyncWaitHandle
返回WaitHandle
,有如下描述。
//
// Summary:
// Gets a System.Threading.WaitHandle that is used to wait for an // asynchronous operation to complete.
//
// Returns:
// A System.Threading.WaitHandle that is used to wait for an // asynchronous operation to complete.
WaitHandle AsyncWaitHandle { get; }
Visual Studio IDE 确认WaitHandle
是一个等待对共享资源进行独占访问的抽象类。在WaitHandle
中,你可以看到有五个不同重载版本的WaitOne()
方法。
public virtual bool WaitOne(int millisecondsTimeout);
public virtual bool WaitOne(int millisecondsTimeout, bool exitContext);
public virtual bool WaitOne(TimeSpan timeout);
public virtual bool WaitOne(TimeSpan timeout, bool exitContext);
public virtual bool WaitOne();
通过使用WaitHandle,
,你可以等待一个委托线程完成它的工作。在演示 6 中,使用了第一个重载版本,并提供了一个可选的超时值,单位为毫秒。如果等待成功,控制从while
循环中退出;但是如果超时发生,WaitOne()
返回 false,并且while
循环继续并在控制台中打印星号(*)。
演示 6
using System;
using System.Threading;
namespace UsingWaitHandle
{
class Program
{
public delegate void Method1Delegate(int sleepTimeinMilliSec);
static void Main(string[] args)
{
Console.WriteLine("***Polling and WaitHandle Demo.***");
Console.WriteLine("Inside Main(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
// Synchronous call
//Method1(3000);
// Asynchrous call using a delegate
Method1Delegate method1Del = Method1;
IAsyncResult asyncResult = method1Del.BeginInvoke(3000, null, null);
Method2();
// while (!asyncResult.IsCompleted)
while (true)
{
// Keep working in main thread
Console.Write("*");
/* There are 5 different overload method for WaitOne().Following method blocks the current thread until the current System.Threading.WaitHandle receives a signal, using a 32-bit signed integer to specify the time interval in milliseconds.
*/
if (asyncResult.AsyncWaitHandle.WaitOne(10))
{
Console.Write("\nResult is available now.");
break;
}
}
method1Del.EndInvoke(asyncResult);
Console.WriteLine("\nExiting Main().");
Console.ReadKey();
}
// Method1
private static void Method1(int sleepTimeInMilliSec)
{
Console.WriteLine("Method1() has started.");
// It will have a different thread id
Console.WriteLine("Inside Method1(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
// Some big task
Thread.Sleep(sleepTimeInMilliSec);
Console.WriteLine("\nMethod1() has finished.");
}
// Method2
private static void Method2()
{
Console.WriteLine("Method2() has started.");
// Main thread id and this thread id will be same
Console.WriteLine("Inside Method2(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
// Some small task
Thread.Sleep(100);
Console.WriteLine("Method2() has finished.");
}
}
}
输出
这是一种可能的输出。
***Polling and WaitHandle Demo.***
Inside Main(),Thread id 1 .
Method2() has started.
Inside Method2(),Thread id 1 .
Method1() has started.
Inside Method1(),Thread id 3 .
Method2() has finished.
*************************************************************************************************************************************************************
Method1() has finished.
*
Result is available now.
Exiting Main().
分析
如果将这个演示与上一个进行比较,您会注意到这里您等待异步操作以不同的方式完成。这次你没有使用IsCompleted
属性,而是使用了IAsyncResult
的AsyncWaitHandle
属性。
使用异步回调
让我们回顾一下前两次演示中的BeginInvoke
方法。
// Asynchrous call using a delegate
Method1Delegate method1Del = Method1;
IAsyncResult asyncResult = method1Del.BeginInvoke(3000, null, null);
这意味着为最后两个方法参数传递了两个null
值。如果您将鼠标悬停在这些先前演示的行上,您会注意到在本例中,BeginInvoke
期望一个IAsyncCallback
委托作为第二个参数,一个object
作为第三个参数。
让我们调查一下IAsyncCallback
代表。Visual Studio IDE 说此委托是在系统命名空间中定义的;它有以下描述。
//
// Summary:
// References a method to be called when a corresponding asynchronous // operation completes.
//
// Parameters:
// ar:
// The result of the asynchronous operation.
[ComVisible(true)]
public delegate void AsyncCallback(IAsyncResult ar);
你可以使用一个callback
方法来执行一些有用的东西(例如,家务工作)。AsyncCallback
委托有一个void
返回类型,它接受一个IAsyncResult
参数。让我们定义一个可以匹配这个委托签名的方法,并在Method1Del
实例完成执行后调用这个方法。
下面是一个示例方法,将在接下来的演示中使用。
// Method3: It's a callback method.
// This method will be invoked when Method1Delegate completes its work.
private static void Method3(IAsyncResult asyncResult)
{
if (asyncResult != null) // if null you can throw some exception
{
Console.WriteLine("\nMethod3() has started.");
Console.WriteLine("Inside Method3(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
// Do some housekeeping work/ clean-up operation
Thread.Sleep(100);
Console.WriteLine("Method3() has finished.");
}
}
演示 7
现在查看完整的实现。
using System;
using System.Threading;
namespace UsingAsynchronousCallback
{
class Program
{
public delegate void Method1Delegate(int sleepTimeinMilliSec);
static void Main(string[] args)
{
Console.WriteLine("***Using Asynchronous Callback.***");
Console.WriteLine("Inside Main(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
// Synchronous call
//Method1(3000);
// Asynchrous call using a delegate
Method1Delegate method1Del = Method1;
IAsyncResult asyncResult = method1Del.BeginInvoke(3000,Method3, null);
Method2();
while (!asyncResult.IsCompleted)
{
// Keep working in main thread
Console.Write("*");
Thread.Sleep(5);
}
method1Del.EndInvoke(asyncResult);
Console.WriteLine("Exit Main().");
Console.ReadKey();
}
// Method1
private static void Method1(int sleepTimeInMilliSec)
{
Console.WriteLine("Method1() has started.");
Console.WriteLine("Inside Method1(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
// Some big task
Thread.Sleep(sleepTimeInMilliSec);
Console.WriteLine("\nMethod1() has finished.");
}
// Method2
private static void Method2()
{
Console.WriteLine("Method2() has started.");
Console.WriteLine("Inside Method2(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
//Some small task
Thread.Sleep(100);
Console.WriteLine("Method2() has finished.");
}
/* Method3: It's a callback method.This method will be invoked when Method1Delegate completes its work.*/
private static void Method3(IAsyncResult asyncResult)
{
if (asyncResult != null)//if null you can throw some exception
{
Console.WriteLine("\nMethod3() has started.");
Console.WriteLine("Inside Method3(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
// Do some housekeeping work/ clean-up operation
Thread.Sleep(100);
Console.WriteLine("Method3() has finished.");
}
}
}
}
输出
这是一个可能的输出。
***Using Asynchronous Callback.***
Inside Main(),Thread id 1 .
Method2() has started.
Inside Method2(),Thread id 1 .
Method1() has started.
Inside Method1(),Thread id 3 .
Method2() has finished.
**********************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
Method1() has finished.
Method3() has started.
Inside Method3(),Thread id 3 .
Exit Main().
Method3() has finished.
分析
请注意,Method3 仅在 Method1()完成执行后才开始工作。还要注意的是Method1()
和Method3()
的线程 ID 是相同的。这是因为Method3()
是从运行 Method1()的线程中调用的。
问答环节
6.4 什么是回调方法?
通常,它是一个仅在特定操作完成后调用的方法。您经常会在异步编程中看到这种方法的使用,在异步编程中,您不知道某个操作的确切完成时间,但希望在前一个任务结束后开始某个特定的任务。例如,在前面的示例中,如果 Method1()在执行期间分配了资源,Method3 可以执行一些清理工作。
我发现 Method3()没有从主线程调用。这是意料之中的吗?
是的。这里您使用了回调方法。在这个例子中,Method3()是回调方法,它只能在 Method1()完成工作后开始执行。因此,从运行 Method1()的同一个线程中调用 Method3()是有意义的。
我可以在这个例子中使用 lambda 表达式吗?
接得好。为了获得类似的输出,在前面的演示中,没有创建一个新方法Method3()
,而是使用下面的代码行,
IAsyncResult asyncResult = method1Del.BeginInvoke(3000, Method3, null);
您可以使用 lambda 表达式替换它,如下所示。
IAsyncResult asyncResult = method1Del.BeginInvoke(3000,
(result) =>
{
if (result != null)//if null you can throw some exception
{
Console.WriteLine("\nMethod3() has started.");
Console.WriteLine("Inside Method3(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
// Do some housekeeping work/ clean-up operation
Thread.Sleep(100);
Console.WriteLine("Method3() has finished.");
}
},
null);
6.7 当您在 BeginInvoke
方法中使用回调方法 Method3 时,您传递的不是一个对象作为最终参数,而是一个空值。这有什么具体原因吗?
不,我没有在这些演示中使用该参数。因为它是一个对象参数,你可以传递任何对你有意义的东西。使用回调方法时,可以传递委托实例。它可以帮助您的回调方法分析异步方法的结果。
但是为了简单起见,让我们修改前面的演示并传递一个字符串消息作为BeginInvoke
中的最后一个参数。假设您正在修改现有的代码行,
IAsyncResult asyncResult = method1Del.BeginInvoke(3000,Method3, null);
有了下面这个。
IAsyncResult asyncResult = method1Del.BeginInvoke(3000, Method3, "Method1Delegate, thank you for using me." );
To accommodate this change, let’s modify the Method3() method too.The newly added lines are shown in bold.
private static void Method3(IAsyncResult asyncResult)
{
if (asyncResult != null) // if null you can throw some exception
{
Console.WriteLine("\nMethod3() has started.");
Console.WriteLine("Inside Method3(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
// Do some housekeeping work/ clean-up operation
Thread.Sleep(100);
// For Q&A
string msg = (string)asyncResult.AsyncState;
Console.WriteLine("Method3() says : '{0}'",msg);
Console.WriteLine("Method3() has finished.");
}
}
如果您再次运行该程序,这一次您可能会看到以下输出。
***Using Asynchronous Callback.***
Inside Main(),Thread id 1 .
Method2() has started.
Inside Method2(),Thread id 1 .
Method1() has started.
Inside Method1(),Thread id 3 .
Method2() has finished.
***************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************************
Method1() has finished.
Method3() has started.
Inside Method3(),Thread id 3 .
Exit Main().
Method3() says : 'Method1Delegate, thank you for using me.'
Method3() has finished.
POINTS TO REMEMBER
您已经看到了使用委托实现轮询、等待句柄和异步回调。这个编程模型也存在于。NET 框架;比如HttpWebRequest
级的BeginGetResponse
、BeginGetRequestStream
,或者SqlCommand
级的BeginExecuteNonQuery()
、BeginExecuteReader()
、BeginExecuteXmlReader()
。这些方法也有重载。
使用基于事件的异步模式(EAP)
在本节中,您将看到 EAP 的用法。起初,基于事件的模式似乎很难理解。根据应用的复杂性,这种模式可以有多种形式。
以下是这种模式的一些关键特征。
-
一般来说,异步方法是其同步版本的精确副本,但是当您调用它时,它在一个单独的线程上启动,然后立即返回。这种机制允许在后台运行预期操作的同时继续调用线程。这些操作的例子可以是长时间运行的过程,例如加载大图像、下载大文件、连接和建立到数据库的连接,等等。EAP 在这些情况下是有帮助的。例如,一旦长时间运行的下载操作完成,就可以引发一个事件来通知信息。事件的订阅者可以根据该通知立即采取行动。
-
您可以同时执行多个操作,并在每个操作完成时收到通知。
-
使用这种模式,您可以利用多线程,但同时也隐藏了整体的复杂性。
-
在最简单的情况下,你的方法名会有一个 Async 后缀来告诉其他人你正在使用一个异步版本的方法。同时,您有一个带有 完成 后缀的相应事件。在理想情况下,您应该有一个相应的 cancel 方法,并且它应该支持显示进度条/报告。支持取消操作的方法也可以命名为method nameasync cancel(或者简称为 CancelAsync )。
-
像
SoundPlayer
、PictureBox
、WebClient
和BackgroundWorker
这样的组件是这种模式的常见代表。
演示 8 是WebClient
的一个简单应用。我们开始吧。
演示 8
在程序的开始,您会看到我需要包含一些特定的名称空间。我用注释来说明为什么这些对于这个演示是必要的。
在这个案例研究中,我想将一个文件下载到我的本地系统中。但是我没有使用来自互联网的真实的URL
,而是将源文件存储在我的本地系统中。这可以给你带来两大好处。
-
运行此应用不需要互联网连接。
-
由于您没有使用互联网连接,下载操作会相对较快。
现在看看下面的代码块,您将在完整的示例中看到它。
WebClient webClient = new WebClient();
// File location
Uri myLocation = new Uri(@"C:\TestData\testfile_original.txt");
// Target location for download
string targetLocation = @"C:\TestData\downloaded_file.txt";
webClient.DownloadFileAsync(myLocation, targetLocation);
webClient.DownloadFileCompleted += new AsyncCompletedEventHandler(Completed);
到目前为止,事情简单明了。但是我想让你注意下面几行代码。
webClient.DownloadFileAsync(myLocation, targetLocation);
webClient.DownloadFileCompleted += new AsyncCompletedEventHandler(Completed);
你可以看到在第一行中,我使用了一个在WebClient
中定义的叫做DownloadFileAsync
的方法。在 Visual Studio 中,方法描述告诉我们以下内容。
// Summary:
// Downloads, to a local file, the resource with the specified URI. // This method does not block the calling thread.
//
// Parameters:
// address:
// The URI of the resource to download.
//
// fileName:
// The name of the file to be placed on the local computer.
//
// Exceptions:
// T:System.ArgumentNullException:
// The address parameter is null. -or- The fileName parameter is null.
//
// T:System.Net.WebException:
// The URI formed by combining System.Net.WebClient.BaseAddress and // address is invalid.
// -or- An error occurred while downloading the resource.
//
// T:System.InvalidOperationException:
// The local file specified by fileName is in use by another thread.
public void DownloadFileAsync(Uri address, string fileName);
从方法总结中,您了解到当您使用此方法时,调用线程不会被阻塞。(实际上,DownloadFileAsync
是DownloadFile
方法的异步版本,在WebClient.
中也有定义)
现在来看下一行代码。
webClient.DownloadFileCompleted += new AsyncCompletedEventHandler(Completed);
Visual Studio 对DownloadFileCompleted
事件的描述如下。
/ Summary:
// Occurs when an asynchronous file download operation completes.
public event AsyncCompletedEventHandler DownloadFileCompleted;
它对AsyncCompletedEventHandler
的描述如下。
// Summary:
// Represents the method that will handle the MethodNameCompleted event // of an asynchronous operation.
//
// Parameters:
// sender:
// The source of the event.
//
// e:
// An System.ComponentModel.AsyncCompletedEventArgs that contains the // event data.
public delegate void AsyncCompletedEventHandler(object sender, AsyncCompletedEventArgs e);
您可以订阅DownloadFileCompleted
事件来显示下载操作完成的通知。为此,使用以下方法。
private static void DownloadCompleted(object sender, AsyncCompletedEventArgs e)
{
Console.WriteLine("Successfully downloaded the file now.");
}
Note
DownloadCompleted
方法匹配AsyncCompletedEventHandler
委托的签名。
既然您已经掌握了委托和事件的概念,您知道您可以替换这一行代码
webClient.DownloadFileCompleted += new AsyncCompletedEventHandler(DownloadCompleted);
使用下面的代码行。
webClient.DownloadFileCompleted += DownloadCompleted;
为了更好的可读性,我保留了长版本。
现在查看完整的示例和输出。
using System;
// For AsyncCompletedEventHandler delegate
using System.ComponentModel;
using System.Net; // For WebClient
using System.Threading; // For Thread.Sleep() method
namespace UsingWebClient
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Demonstration-.Event Based Asynchronous Program Demo.***");
// Method1();
#region The lenghty operation(download)
Console.WriteLine("Starting a download operation.");
WebClient webClient = new WebClient();
// File location
Uri myLocation = new Uri(@"C:\TestData\OriginalFile.txt");
// Target location for download
string targetLocation = @"C:\TestData\DownloadedFile.txt";
webClient.DownloadFileAsync(myLocation, targetLocation);
webClient.DownloadFileCompleted += new AsyncCompletedEventHandler(Completed);
#endregion
Method2();
Console.WriteLine("End Main()...");
Console.ReadKey();
}
// Method2
private static void Method2()
{
Console.WriteLine("Method2() has started.");
// Some small task
// Thread.Sleep(10);
Console.WriteLine("Method2() has finished.");
}
private static void Completed(object sender, AsyncCompletedEventArgs e)
{
Console.WriteLine("Successfully downloaded the file now.");
}
}
}
输出
这是一个可能的输出。
***Demonstration-.Event Based Asynchronous Program Demo.***
Starting a download operation.
Method2() has started.
Method2() has finished.
End Main()...
Successfully downloaded the file now.
分析
您可以看到下载操作是在Method2()
开始执行之前开始的。然而,Method2()
在下载操作完成之前完成了它的任务。如果你有兴趣看Original.txt
的内容,如下。
Dear Reader,
This is my test file.It is originally stored at C:\TestData in my system.
您可以测试一个类似的文件及其内容,以便在您的终端上进行快速验证。
附加说明
当你引入一个进度条时,你可以使这个例子更好。您可以使用 Windows 窗体应用来获得对进度条的内置支持。我们先忽略Method2
,把重点放在异步下载操作上。你可以做一个基本的表单,如图 6-2 所示,包含三个简单的按钮和一个进度条。(您需要首先将这些控件拖放到您的表单上。我假设你知道这些活动)。
图 6-2
一个简单的 UI 应用,演示基于事件的异步
下面这段代码是不言自明的。
using System;
using System.ComponentModel;
using System.Net;
using System.Threading;
using System.Windows.Forms;
namespace UsingWebClientWithWinForm
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private void StartDownload_Click(object sender, EventArgs e)
{
WebClient webClient = new WebClient();
Uri myLocation = new Uri(@"C:\TestData\testfile_original.txt");
string targetLocation = @"C:\TestData\downloaded_file.txt";
webClient.DownloadFileAsync(myLocation, targetLocation);
webClient.DownloadFileCompleted += new AsyncCompletedEventHandler(DownloadCompleted);
webClient.DownloadProgressChanged += new DownloadProgressChangedEventHandler(ProgressChanged);
Thread.Sleep(3000);
MessageBox.Show("Method1() has finished.");
}
private void DownloadCompleted(object sender, AsyncCompletedEventArgs e)
{
MessageBox.Show("Successfully downloaded the file now.");
}
private void ProgressChanged(object sender, DownloadProgressChangedEventArgs e)
{
progressBar.Value = e.ProgressPercentage;
}
private void ResetButton_Click(object sender, EventArgs e)
{
progressBar.Value = 0;
}
private void ExitButton_Click(object sender, EventArgs e)
{
this.Close();
}
}
}
输出
一旦点击StartDownload button
,就会得到如图 6-3 所示的输出。
图 6-3
UI 应用运行时的运行时屏幕截图
问答环节
基于事件的异步程序有哪些优点和缺点?
以下是与这种方法相关的一些常见的优点和缺点。
赞成的意见
- 您可以调用一个长时间运行的方法并立即获得一个返回。当方法完成时,您会收到一个通知。
骗局
-
因为您已经分离了代码,所以理解、调试和维护通常很困难。
-
当您订阅了一个事件,但后来忘记取消订阅时,可能会出现一个大问题。这个错误会导致应用中的内存泄漏,影响可能非常严重;例如,您的系统可能会挂起或没有响应,您可能需要经常重新启动系统。
了解任务
要理解基于任务的异步模式,首先要知道的是,任务只是您想要执行的一个工作单元。您可以在同一个线程或不同的线程中完成这项工作。通过使用任务,您可以更好地控制线程;例如,您可以在任务完成后执行后续工作。父任务可以创建子任务,因此您可以组织层次结构。当你级联你的消息时,这种层次结构是重要的;例如,在您的应用中,您可能决定一旦父任务被取消,子任务也应该被取消。
您可以用不同的方式创建任务。在下面的演示中,我用三种不同的方式创建了三个任务。请注意下面这段带有支持性注释的代码。
#region Different ways to create and execute task
// Using constructor
Task taskOne = new Task(MyMethod);
taskOne.Start();
// Using task factory
TaskFactory taskFactory = new TaskFactory();
// StartNew Method creates and starts a task.
// It has different overloaded version.
Task taskTwo = taskFactory.StartNew(MyMethod);
// Using task factory via a task
Task taskThree = Task.Factory.StartNew(MyMethod);
#endregion
您可以看到所有三个任务都在执行相同的操作。它们中的每一个都在执行MyMethod()
,描述如下。
private static void MyMethod()
{
Console.WriteLine("Task.id={0} with Thread id {1} has started.", Task.CurrentId, Thread.CurrentThread.ManagedThreadId);
// Some task
Thread.Sleep(100);
Console.WriteLine("MyMethod for Task.id={0} and Thread id {1} is completed.", Task.CurrentId, Thread.CurrentThread.ManagedThreadId);
}
你可以看到在MyMethod()
内部,为了区分任务和线程,它们对应的 id 被打印在控制台中。
最后一件事。您可以看到方法名作为参数被传递到了StartNew()
方法中。这个方法在编写时有 16 个重载版本,我使用的是如下定义的那个。
//
// Summary:
// Creates and starts a task.
//
// Parameters:
// action:
// The action delegate to execute asynchronously.
//
// Returns:
// The started task.
//
// Exceptions:
// T:System.ArgumentNullException:
// The action argument is null.
public Task StartNew(Action action);
因为在这种情况下MyMethod()
匹配Action
委托的签名,所以对StartNew
使用这种方法没有问题。
演示 9
现在进行完整的演示和输出。
using System;
using System.Threading;
using System.Threading.Tasks;
namespace CreatingTasks
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Using different ways to create tasks.****");
Console.WriteLine("Inside Main().Thread ID:{0}", Thread.CurrentThread.ManagedThreadId);
#region Different ways to create and execute task
// Using constructor
Task taskOne = new Task(MyMethod);
taskOne.Start();
// Using task factory
TaskFactory taskFactory = new TaskFactory();
// StartNew Method creates and starts a task.
// It has different overloaded version.
Task taskTwo = taskFactory.StartNew(MyMethod);
// Using task factory via a task
Task taskThree = Task.Factory.StartNew(MyMethod);
#endregion
Console.ReadKey();
}
private static void MyMethod()
{
Console.WriteLine("Task.id={0} with Thread id {1} has started.", Task.CurrentId, Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(100);
Console.WriteLine("MyMethod for Task.id={0} and Thread id {1} is completed.", Task.CurrentId, Thread.CurrentThread.ManagedThreadId);
}
}
}
输出
这是一个可能的输出。
***Using different ways to create tasks.****
Inside Main().Thread ID:1
Task.id=2 with Thread id 6 has started.
Task.id=1 with Thread id 5 has started.
Task.id=3 with Thread id 4 has started.
MyMethod for Task.id=1 and Thread id 5 is completed.
MyMethod for Task.id=3 and Thread id 4 is completed.
MyMethod for Task.id=2 and Thread id 6 is completed.
问答环节
6.9 StartNew()
只能用于匹配动作委托签名的方法。这是正确的吗?
不。我在一个接受参数的StartNew
重载中使用了它,参数是匹配动作委托签名的方法的名称。但是,还有其他过载版本的StartNew
;例如,考虑以下情况。
public Task<TResult> StartNew<[NullableAttribute(2)]TResult>
(Func<TResult> function, TaskCreationOptions creationOptions);
Or,
public Task<TResult> StartNew<[NullableAttribute(2)]TResult>
(Func<TResult> function, CancellationToken cancellationToken);
6.10 在之前的一个 Q & A 中,我看到了 TaskCreationOptions
的用法。这是什么意思?
这是一个enum
。您可以使用它来设置任务的行为。下面描述了这个enum
,并包括您拥有的不同选项。
public enum TaskCreationOptions
{
None = 0,
PreferFairness = 1,
LongRunning = 2,
AttachedToParent = 4,
DenyChildAttach = 8,
HideScheduler = 16,
RunContinuationsAsynchronously = 64,
}
在接下来的演示中,您将看到一个叫做TaskContinuationOptions
的重要enum
的使用,它也有助于设置任务行为。
使用基于任务的异步模式(TAP)
TAP 最早出现在 C# 4.0 中。是 C# 5.0 中出现的async/await
的基础。TAP 引入了Task
类及其通用变体,当异步代码块的返回值不是问题时,使用Task<TResult>. Task
,但是当您希望返回值进一步处理时,您应该使用Task<TResult>
通用版本.
。让我们用这个概念来实现使用Method1()
和Method2()
的 TAP。
演示 10
这是一个完整的演示。
using System;
using System.Threading;
using System.Threading.Tasks;
namespace UsingTAP
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Using Task-based Asynchronous Pattern.****");
Console.WriteLine("Inside Main().Thread ID:{0}", Thread.CurrentThread.ManagedThreadId);
Task taskForMethod1 = new Task(Method1);
taskForMethod1.Start();
Method2();
Console.ReadKey();
}
private static void Method1()
{
Console.WriteLine("Method1() has started.");
Console.WriteLine("Inside Method1(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
// Some big task
Thread.Sleep(3000);
Console.WriteLine("Method1() has completed its job now.");
}
private static void Method2()
{
Console.WriteLine("Method2() has started.");
Console.WriteLine("Inside Method2(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(100);
Console.WriteLine("Method2() is completed.");
}
}
}
输出
这是一个可能的输出。
***Using Task-based Asynchronous Pattern.****
Inside Main().Thread ID:1
Method2() has started.
Inside Method2(),Thread id 1 .
Method1() has started.
Inside Method1(),Thread id 4 .
Method2() is completed.
Method1() has completed its job now.
您刚刚看到了一个基于任务的异步模式的示例演示。我不关心Method1
的返回值。但是假设你想看Method1
执行成功与否。为了简单起见,我使用一条string
消息来表示成功完成。这次你会看到任务的一个普通变体Task<string>
。对于 lambda 表达式爱好者,我在这个例子中用 lambda 表达式修改了Method1
。为了满足关键需求,我调整了返回类型。这次我添加了另一个叫做Method3()
的方法。出于比较的目的,最初这个方法将被注释掉,程序将被执行,输出将被分析。稍后我将取消对它的注释,并使用该方法创建一个任务层次结构。一旦完成,程序将被再次执行,你会注意到当Method1()
完成它的工作时Method3()
执行。为了更好的理解,我保留了评论。
现在来看一下接下来的演示。
演示 11
这是一个完整的演示。
using System;
using System.Threading;
using System.Threading.Tasks;
namespace UsingTAPDemo2
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Using Task-based Asynchronous Pattern.Using lambda expression into it.****");
Console.WriteLine("Inside Main().Thread ID:{0}", Thread.CurrentThread.ManagedThreadId);
// Task taskForMethod1 = new Task(Method1);
// taskForMethod1.Start();
Task<string> taskForMethod1 = Method1();
// Wait for task to complete.It’ll be no more //asynchonous now.
// taskForMethod1.Wait();
// Continue the task
// The taskForMethod3 will continue once taskForMethod1 is // finished
// Task taskForMethod3 = taskForMethod1.ContinueWith(Method3, TaskContinuationOptions.OnlyOnRanToCompletion);
Method2();
Console.WriteLine("Task for Method1 was a : {0}", taskForMethod1.Result);
Console.ReadKey();
}
// Using lambda expression
private static Task<string> Method1()
{
return Task.Run(() =>
{
string result = "Failure";
try
{
Console.WriteLine("Inside Method1(),Task.id={0}", Task.CurrentId);
Console.WriteLine("Method1() has started.");
Console.WriteLine("Inside Method1(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
//Some big task
Thread.Sleep(3000);
Console.WriteLine("Method1() has completed its job now.");
result = "Success";
}
catch (Exception ex)
{
Console.WriteLine("Exception caught:{0}", ex.Message);
}
return result;
}
);
}
private static void Method2()
{
Console.WriteLine("Method2() has started.");
Console.WriteLine("Inside Method2(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(100);
Console.WriteLine("Method2() is completed.");
}
private static void Method3(Task task)
{
Console.WriteLine("Method3 starts now.");
Console.WriteLine("Task.id is:{0} with Thread id is :{1} ", Task.CurrentId, Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(20);
Console.WriteLine("Method3 for Task.id {0} and Thread id {1} is completed.", Task.CurrentId, Thread.CurrentThread.ManagedThreadId);
}
}
}
输出
***Using Task-based Asynchronous Pattern.Using lambda expression into it.****
Inside Main().Thread ID:1
Method2() has started.
Inside Method2(),Thread id 1 .
Inside Method1(),Task.id=1
Method1() has started.
Inside Method1(),Thread id 4 .
Method2() is completed.
Method1() has completed its job now.
Task for Method1 was a : Success
分析
你注意到我没有对taskForMethod1
使用Start()
方法吗?相反,我使用了Task
类中的Run()
方法来执行Method1()
。我为什么这么做?嗯,在Task
类里面,Run
是一个静态方法。Visual Studio 中的方法总结告诉我们关于这个Run
方法的如下内容:"Queues the specified work to run on the thread pool and returns a System.Threading.Tasks.Task
1 object that represents that work."`在编写的时候,这个方法有八个重载版本,如下。
public static Task Run(Action action);
public static Task Run(Action action, CancellationToken cancellationToken);
public static Task<TResult> Run<TResult>(Func<TResult> function);
public static Task<TResult> Run<TResult>(Func<TResult> function, CancellationToken cancellationToken);
public static Task Run(Func<Task> function);
public static Task Run(Func<Task> function, CancellationToken cancellationToken);
public static Task<TResult> Run<TResult>(Func<Task<TResult>> function);
public static Task<TResult> Run<TResult>(Func<Task<TResult>> function, CancellationToken cancellationToken);
现在检查这个例子中的另一个要点。如果取消对下面一行的注释
// Task taskForMethod3 = taskForMethod1.ContinueWith(Method3, TaskContinuationOptions.OnlyOnRanToCompletion);
并再次运行该应用,您可以得到类似下面的输出。
***Using Task-based Asynchronous Pattern.Using lambda expression into it.****
Inside Main().Thread ID:1
Method2() has started.
Inside Method1(),Task.id=1
Method1() has started.
Inside Method1(),Thread id 4 .
Inside Method2(),Thread id 1 .
Method2() is completed.
Method1() has completed its job now.
Task for Method1 was a : Success
Method3 starts now.
Task.id is:2 with Thread id is :5
Method3 for Task.id 2 and Thread id 5 is completed.
ContinueWith()
方法有助于继续任务。你可能还会注意到下面的部分。
TaskContinuationOptions.OnlyOnRanToCompletion
它只是声明当taskForMethod1
完成它的工作时,任务将继续。同样,您可以通过使用TaskContinuationOptions
enum
来选择其他选项,其描述如下。
public enum TaskContinuationOptions
{
None = 0,
PreferFairness = 1,
LongRunning = 2,
AttachedToParent = 4,
DenyChildAttach = 8,
HideScheduler = 16,
LazyCancellation = 32,
RunContinuationsAsynchronously = 64,
NotOnRanToCompletion = 65536,
NotOnFaulted = 131072,
OnlyOnCanceled = 196608,
NotOnCanceled = 262144,
OnlyOnFaulted = 327680,
OnlyOnRanToCompletion = 393216,
ExecuteSynchronously = 524288
}
问答环节
6.11 我可以一次分配多项任务吗?
是的,你可以。在前面修改过的例子中,假设您有一个名为Method4
的方法,描述如下。
private static void Method4(Task task)
{
Console.WriteLine("Method4 starts now.");
Console.WriteLine("Task.id is:{0} with Thread id is :{1} ", Task.CurrentId, Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(10);
Console.WriteLine("Method4 for Task.id {0} and Thread id {1} is completed.", Task.CurrentId, Thread.CurrentThread.ManagedThreadId);
}
你可以写下面几行。
Task<string> taskForMethod1 = Method1();
Task taskForMethod3 = taskForMethod1.ContinueWith(Method3, TaskContinuationOptions.OnlyOnRanToCompletion);
taskForMethod3 = taskForMethod1.ContinueWith(Method4, TaskContinuationOptions.OnlyOnRanToCompletion);
这意味着一旦 taskForMethod1 完成了任务,您就会看到 taskForMethod3 的后续工作,它执行 Method3 和 Method4。
还需要注意的是,延续工作可以有延续工作。例如,让我们假设您想要以下内容。
-
一旦 taskForMethod1 完成,然后继续 taskForMethod3。
-
一旦 taskForMethod3 完成,就只能继续 taskForMethod4
你可以写类似下面的东西。
// Method1 starts
Task<string> taskForMethod1 = Method1();
// Task taskForMethod3 starts after Task taskForMethod1
Task taskForMethod3 = taskForMethod1.ContinueWith(Method3,
TaskContinuationOptions.OnlyOnRanToCompletion);
// Task taskForMethod4 starts after Task taskForMethod3
Task taskForMethod4 = taskForMethod3.ContinueWith(Method4, TaskContinuationOptions.OnlyOnRanToCompletion);
使用 async 和 await 关键字
使用async
和await
关键字使得点击模式非常灵活。本章使用了两种方法,其中第一种方法是长时间运行的方法,比第二种方法需要更多的时间来完成。我继续用同样的Method1()
和Method2()
方法.
进行案例研究
在接下来的演示中,我使用async
和 await 关键字。我从一个非 lambda 版本开始,但是在分析部分,我给出了 lambda 表达式代码的变体。首先我们来看Method1() again
。
private static void Method1()
{
Console.WriteLine("Method1() has started.");
Console.WriteLine("Inside Method1(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
// Some big task
Thread.Sleep(3000);
Console.WriteLine("Method1() has completed its job now.");
}
当您使用 lambda 表达式和一个async/await
对时,您的代码可能如下所示。
// Using lambda expression
private static async Task ExecuteMethod1()
{
await Task.Run(() =>
{
Console.WriteLine("Method1() has started.");
Console.WriteLine("Inside Method1(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
// Some big task
Thread.Sleep(3000);
Console.WriteLine("Method1() has completed its job now.");
}
);
}
你有没有注意到同步版本和异步版本非常相似?但是许多早期实现异步编程的解决方案并不是这样的。(我也相信它们是复杂的。)
等待是做什么的?当你分析代码时,你会发现一旦你得到一个await
,调用线程就会跳出这个方法,继续做别的事情。在接下来的演示中,Task.Run is used;
它导致异步调用在一个单独的线程上继续。然而,需要注意的是,这个并不意味着延续工作应该在一个新的线程上完成,因为你可能不总是关心不同的线程;例如,当您的呼叫等待通过网络建立连接以下载某些内容时。
在非 lambda 版本中,我使用下面的代码块。
private static async Task ExecuteTaskOne()
{
await Task.Run(Method1);
}
在Main()
内部,ExecuteTaskOne()
不调用Method1()
,而是异步执行Method1()
。我通过了Run
方法里面的Method1
。我在这里使用了最短的重载版本的Run
方法。由于Method1
匹配一个Action
委托的签名(记住这个委托封装了任何没有参数和void
返回类型的方法),您可以在Task
类的Run
方法中将它作为参数传递。
演示 12
这是完整的演示。
using System;
using System.Threading;
using System.Threading.Tasks;
namespace UsingAsyncAwaitDemo
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Exploring task-based asynchronous pattern(TAP) using async and await.****");
Console.WriteLine("Inside Main().Thread ID:{0}", Thread.CurrentThread.ManagedThreadId);
/*
* This call is not awaited.So,the current method
* continues before the call is completed.
*/
ExecuteTaskOne();//Async call,this call is not awaited
Method2();
Console.ReadKey();
}
private static async Task ExecuteTaskOne()
{
await Task.Run(Method1);
}
private static void Method1()
{
Console.WriteLine("Method1() has started.");
Console.WriteLine("Inside Method1(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
// Some big task
Thread.Sleep(3000);
Console.WriteLine("Method1() has completed its job now.");
}
private static void Method2()
{
Console.WriteLine("Method2() has started.");
Console.WriteLine("Inside Method2(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(100);
Console.WriteLine("Method2() is completed.");
}
}
}
输出
这是一个可能的输出。
***Exploring task-based asynchronous pattern(TAP) using async and await.****
Inside Main().Thread ID:1
Method1() has started.
Inside Method1(),Thread id 4 .
Method2() has started.
Inside Method2(),Thread id 1 .
Method2() is completed.
Method1() has completed its job now.
分析
您可以看到Method1()
开始得更早,但是Method2()
的执行并没有因此而被阻塞。还要注意,Method2()
在一个主线程中运行,而Method1()
在一个不同的线程中执行。
和前面的例子一样,如果您喜欢 lambda 表达式,您可以替换下面的代码段:
private static async Task ExecuteTaskOne()
{
await Task.Run(Method1);
}
private static void Method1()
{
Console.WriteLine("Method1() has started.");
Console.WriteLine("Inside Method1(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
// Some big task
Thread.Sleep(3000);
Console.WriteLine("Method1() has completed its job now.");
}
有了这个:
// Using lambda expression
private static async Task ExecuteMethod1()
{
await Task.Run(() =>
{
Console.WriteLine("Method1() has started.");
Console.WriteLine("Inside Method1(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
// Some big task
Thread.Sleep(3000);
Console.WriteLine("Method1() has completed its job now.");
}
);
}
在演示 12 中,您可以直接调用ExecuteMethod1()
方法来获得类似的输出,而不是调用ExecuteTaskOne()
。
在前面的示例中,您会看到下面一行的警告消息:ExecuteMethod1();
,它陈述了以下内容。
Warning CS4014 Because this call is not awaited, execution of the current method continues before the call is completed. Consider applying the 'await' operator to the result of the call.
如果你将鼠标悬停在这里,你会得到两个建议。第一个建议您应用丢弃,如下所示:
_ = ExecuteMethod1(); // applying discard
Note
从 C #7.0 开始支持丢弃。它们是应用中临时的、虚拟的和未使用的变量。因为这些变量可能不在分配的存储中,所以它们可以减少内存分配。这些变量可以增强可读性和可维护性。使用下划线(_)表示应用中被丢弃的变量。
下面使用第二个建议,并在该行之前插入await
。
await ExecuteMethod1();
在这种情况下,编译器会引发另一个错误。
Error CS4033 The 'await' operator can only be used within an async method. Consider marking this method with the 'async' modifier and changing its return type to 'Task'.
要消除这个错误,您需要使包含方法async
(即,从如下行开始:
static async Task Main(string[] args)
在应用了async/await
对之后,Main()
方法可能如下所示。
class Program
{
// static void Main(string[] args)
static async Task Main(string[] args)
{
Console.WriteLine("***Exploring task-based asynchronous pattern(TAP) using async and await.****");
Console.WriteLine("Inside Main().Thread ID:{0}", Thread.CurrentThread.ManagedThreadId);
await ExecuteMethod1();
// remaining code
这种全面的讨论提醒您一起应用 async/await,并正确放置它们。
我用另一个演示来结束这一章,在这个演示中,我稍微修改了应用的调用序列。我用的Method3(),
和Method2()
差不多。该方法从ExecuteTaskOne()
调用,其结构如下。
private static async Task ExecuteTaskOne()
{
Console.WriteLine("Inside ExecuteTaskOne(), prior to await() call.");
int value=await Task.Run(Method1);
Console.WriteLine("Inside ExecuteTaskOne(), after await() call.");
// Method3 will be called if Method1 executes successfully
if (value != -1)
{
Method3();
}
}
这段代码简单地说,我想从Method1()
获取返回值,并基于该值决定是否调用Method3()
。这次,Method1()
的返回类型不是void
;相反,它返回一个int
(0 表示成功完成;否则为–1)。这个方法用如下所示的try-catch
块进行了重构。
private static int Method1()
{
int flag = 0;
try
{
Console.WriteLine("Method1() has started.");
Console.WriteLine("Inside Method1(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
// Some big task
Thread.Sleep(3000);
Console.WriteLine("Method1() has completed its job now.");
}
catch (Exception e)
{
Console.WriteLine("Caught Exception {0}", e);
flag = -1;
}
return flag;
}
现在来看看下面的例子。
演示 13
这是完整的演示。
using System;
using System.Threading;
using System.Threading.Tasks;
namespace UsingAsyncAwaitDemo3
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("***Exploring task-based asynchronous pattern(TAP) using async and await.****");
Console.WriteLine("***This is a modified example with three methods.***");
Console.WriteLine("Inside Main().Thread ID:{0}", Thread.CurrentThread.ManagedThreadId);
/*
* This call is not awaited.So,the current method
* continues before the call is completed.
*/
_=ExecuteTaskOne();//Async call,this call is not awaited
Method2();
Console.ReadKey();
}
private static async Task ExecuteTaskOne()
{
Console.WriteLine("Inside ExecuteTaskOne(), prior to await() call.");
int value=await Task.Run(Method1);
Console.WriteLine("Inside ExecuteTaskOne(), after await() call.");
// Method3 will be called if Method1 executes successfully
if (value != -1)
{
Method3();
}
}
private static int Method1()
{
int flag = 0;
try
{
Console.WriteLine("Method1() has started.");
Console.WriteLine("Inside Method1(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
//Some big task
Thread.Sleep(3000);
Console.WriteLine("Method1() has completed its job now.");
}
catch (Exception e)
{
Console.WriteLine("Caught Exception {0}", e);
flag = -1;
}
return flag;
}
private static void Method2()
{
Console.WriteLine("Method2() has started.");
Console.WriteLine("Inside Method2(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(100);
Console.WriteLine("Method2() is completed.");
}
private static void Method3()
{
Console.WriteLine("Method3() has started.");
Console.WriteLine("Inside Method3(),Thread id {0} .", Thread.CurrentThread.ManagedThreadId);
Thread.Sleep(100);
Console.WriteLine("Method3() is completed.");
}
}
}
输出
***Exploring task-based asynchronous pattern(TAP) using async and await.****
***This is a modified example with three methods.***
Inside Main().Thread ID:1
Inside ExecuteTaskOne(), prior to await() call.
Method1() has started.
Inside Method1(),Thread id 4 .
Method2() has started.
Inside Method2(),Thread id 1 .
Method2() is completed.
Method1() has completed its job now.
Inside ExecuteTaskOne(), after await() call.
Method3() has started.
Inside Method3(),Thread id 4 .
Method3() is completed.
分析
密切注意输出。你可以看到Method3()
需要等待Method1()
的完成,但是Method2()
可以在Method1()
结束执行之前完成它的执行。这里,如果Method1()
的返回值不等于–1,则Method3()
可以继续。这个场景类似于您在演示 11 中看到的ContinueWith()
方法。
最重要的是,再次注意下面的代码行。
int value=await Task.Run(Method1);
它只是将代码段分为两部分:对 await
的前调用和对 await
的后调用。这个语法类似于任何同步调用,但是通过使用await
(在一个async
方法中),您应用了一个暂停点并使用了异步编程的力量。
我用微软的一些有趣的笔记来结束这一章。当您进一步探索 async/await 关键字时,它们非常方便。记住以下几点。
-
await 运算符不能出现在 lock 语句的正文中。
-
您可能会在一个
async
方法的主体中看到多个await
。在一个async
方法中没有await
不会引发任何编译时错误。相反,您会得到一个警告,并且该方法以同步方式执行。注意下面类似的警告:Warning CS1998 This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread
.
最后的话
又一个大篇章!希望我能够揭开异步编程不同方法的神秘面纱。尽管在未来的开发中不再推荐使用IAsyncResult
模式和event-based asynchrony
,但我在本章中讨论了它们,以帮助您理解遗留代码,并向您展示异步程序的发展。毫无疑问,你将来会发现它很有用。
现在你已经准备好跳入异步编程的汪洋大海,探索剩下的边角案例了,没有自我实践是无法掌握的。所以,继续努力。
到目前为止,您已经看到了许多基于委托、事件和 lambda 表达式的应用!现在让我们进入最后一章,关于数据库编程。它有点不同,但非常有用和有趣。
摘要
本章讨论了以下关键问题。
-
什么是异步程序?它与同步程序有什么不同?
-
如何使用
Thread
类编写异步程序? -
什么是线程池?如何使用
ThreadPool
类编写异步程序? -
如何在异步程序中使用 lambda 表达式?
-
如何按照基于事件的异步模式编写异步程序?
-
什么是任务?如何在你的程序中使用
Task
类? -
如何按照基于任务的异步模式编写异步程序?
-
如何使用
async/await
关键字编写一个异步程序? -
你如何在你的应用中使用丢弃?
-
当你在程序中使用
async/await
关键字时,有哪些重要的限制?
七、数据库编程
C# 客户端应用可以使用 ADO.NET 与数据库对话。总的来说,它是一组类(通常称为框架),可以帮助您连接数据源,比如 XML 文件或数据库。使用这些类(和相应的方法),您可以操作所需的数据。这是另一个大话题,但我将讨论限制在一个简单的 C# 应用如何使用 SQL 查询与 RDBMS(关系数据库管理系统)对话。
当我谈到 RDBMS 时,有多种选择。例如,Oracle、Microsoft SQL Server 和 MySQL 就是其中的一些主要参与者。事实上,在许多书籍中,ADO.NET 是与微软 SQL Server 一起讨论的。在本章中,MySQL 是首选的关系数据库。但好消息是,当您选择不同的选项(例如,Microsoft SQL Server)时,底层方法不会有太大的变化。
与数据库对话还有其他方法。例如,开发人员可以选择基于对象关系映射(ORM)的实体框架(EF),而不是使用 ADO.NET。因此,他们不是直接编写 SQL 查询,而是处理类(或对象)并使用 LINQ 查询。虽然详细的讨论超出了本章的范围,但是知道以下内容可能是有用的,因为 EF 是建立在 ADO.NET 之上的,它并不比使用 ADO.NET 更快(但是它可以使你的代码开发更快并且组织得更整齐)。此外,当您解决数据访问问题时,理解 ADO.NET 的核心职责可以使编程更容易。
要体验数据库编程,您需要熟悉以下概念。
-
什么是数据库以及它如何帮助您存储或组织数据
-
数据库是如何连接的
-
C# 应用如何与数据库对话(例如,如何建立与数据库的连接,以及如何在数据库中插入、更新或删除记录)
图 7-1 展示了一个客户端应用(一个 C# 程序)和一个使用 ADO.NET 连接的数据库的整个过程的简化视图。
Note
你可以用各种方式存储数据;例如,您使用数据库,您可以将数据存储在文本文件中,等等。在这一章中,我交替使用术语数据存储器和数据库。
图 7-1
通过 ADO.NET 将 C# 应用连接到数据库
如果您是数据库编程新手,您可能需要学习一些关键术语,本章将简要介绍这些术语。我建议您反复阅读这些定义,以便更好地理解它们。渐渐地,这些术语你会越来越清楚。
数据库和 DBMS
数据库是一个有组织的数据集合。例如,根据其类型,数据库可以是与文件(或表)相关的集合。一个表可以是相关记录的集合,其中每个记录可以是相关字段的集合。字段是文件(或表格)中最小的有意义的信息。
一个数据库管理系统 (DBMS)有效地创建和管理数据库。Oracle 数据库、SQL Server、MySQL 和 MS Access 是流行的 DBMS 软件包。
通常,数据库、DBMS 和相应的应用的集合可以形成一个数据库系统。
DBMS 的类型
有不同类型的 DBMS,包括以下几种。
-
分级数据库管理系统
-
网络数据库管理系统
-
关系数据库管理系统
-
面向对象数据库
-
分布式数据库管理系统
各有利弊。选择数据库取决于您的需求。与其选择 SQL 数据结构(适合 RDBMS),不如选择 NoSQL(适合 DDBMS 的非关系结构)。在本章中,您只看到了 RDBMS 和简单 SQL 语句的用法。
关系型数据库管理系统
在 RDBMS 中,数据存储在行和列中,这类似于表。你可能会看到常见的术语,比如关系、元组、属性。当您使用 SQL 时,正式的关系模型术语——关系、元组和属性——分别用于表、行和列。
表中的每一行都包含一条记录。每列都包含字段。图 7-2 所示的表格标记了所有的记录和属性。
图 7-2
示例表标有记录和属性
您可以基于一个称为关系代数的数学公式来处理一个关系的不同记录。因为整个数据库都可以用它来处理,所以关系代数是关系数据库和 SQL 的理论基础。
Oracle 数据库、MySQL、Microsoft SQL Server 和 IBM DB2 是 RDBMS 的常见例子;在这一章中,我使用 MySQL 来演示例子。
Note
附录 A 包括在 Win10 机器上安装 MySQL 的步骤。
结构化查询语言
SQL 代表结构化查询语言。它是一种非常流行和广泛使用的 RDBMS 语言。它是一种类似英语的语言,被认为是第四代语言。创建数据、更新数据、读取数据和删除数据是 SQL 最常见的操作。
POINTS TO REMEMBER
-
C#、Java、C++和 C 都是通用语言的例子。每一种都被归类为第三代语言(3GL),而 SQL 被称为 4GL。在 3GL 中,重点是“我如何解决一个问题?”但在 4GL,焦点是“我想要什么结果?”但是代替提供方法,你有让你的计算机/机器决定如何获得它的自由。虽然一个高级 3GL 可以结合 4GL 的一些重要方面。
-
值得注意的是,SQL 不区分大写和小写字符集,但通常需要使用大写关键字。
-
本章的各种程序中都使用了简单的 SQL 语句。如果您是 SQL 新手,我建议您在继续下一步之前,先在您喜欢的数据库中做一些简单的 SQL 语句练习,以便获得更好的想法。
我假设您已经在本地计算机上安装了 MySQL。如果尚未安装,请前往 https://dev.mysql.com/downloads/installer/
下载安装程序并了解相关信息。也可以参考附录 A,里面有在 Win10 机器上安装 MySQL 的步骤。
当我为另一本书写关于不同技术的类似章节时,MySQL-installer-community-8 . 0 . 16 . 0 是最新版本。但随着我的前进,更新不断出现,我也在不断更新。最后定在了 8.0.19。
安装数据库是第一步。那么您需要一个特定于供应商的连接器。我用的是 MySQL。NET 框架。我寻找一个适合我的连接器。我去了 https://dev.mysql.com/downloads/connector/net/
,如图 7-3 。
图 7-3
下载连接器
下载完压缩文件后,解压缩它以获得连接器。强烈建议您登陆官网( https://dev.mysql.com/doc/connector-net/en/connector-net-versions.html
)了解更多。网络连接器。忍不住要提以下几个有趣的点。
-
MySQL Connector/NET 有多个版本。
-
官网上说 MySQL Connector/NET 8.0 是 Connector/NET 7.0 的延续,目前命名为将版本号的第一位数字与其支持的(最高)MySQL 服务器版本同步。
-
MySQL Connector/NET 8.0 is highly recommended for use with MySQL Server 8.0, 5.7, and 5.6. Based on your system configuration, you may need to upgrade and use the proper version of the connector. It’s worth looking at the table on the website, as shown in Figure 7-4, before you proceed.
图 7-4
相关产品的连接器/网络要求(来源:
https://dev.mysql.com/doc/connector-net/en/connector-net-versions.html
) -
我电脑上安装的 Visual Studio 版本是 16.3.9。MySQL 版本是 8.0.18。我用过。NET Core 3.0 中的许多例子。因此,使用连接器版本 8.0 和更高版本是有意义的。
-
Once installed, you need to add a MySql.Data.dll reference to your project. On my machine, I used C:\Program Files (x86)\MySQL\MySQL Connector Net 8.0.19\Assemblies\v4.5.2. Once you do this, to verify the information, open the Properties window. Note the arrow in Figure 7-5. It is an ADO.NET driver for MySQL for .NET Framework and .NET Core.
图 7-5
MySql。数据是 MySQL 的 ADO.NET。NET 框架和。净核心
-
如果你使用。NET 核心,你可能需要安装一个 MySql。数据包,然后再继续。您可以使用 Visual Studio 添加该包。或者,您可以转到工具➤ NuGet 软件包管理器➤软件包管理器控制台并键入以下命令(版本可能因您的情况而异):
PM> Install-Package MySql.Data -Version 8.0.19
正确操作后,您会看到类似于图 7-6 的屏幕。
图 7-6
MySql。数据已成功添加到。网络核心应用
ADO.NET 简论
因为我将在接下来的演示中使用 about,所以让我们快速讨论一下。首先,它是一个面向对象的框架,非常灵活。在传统方法中,您必须打开一个连接来连接数据库,然后执行简单的 SQL 查询。您的应用总是连接到数据库。因此,即使不使用数据存储,也会使用昂贵的数据库资源,这会降低应用的整体性能和效率。
为了克服这一点,ADO.NET 还支持断开数据架构,它说当你需要运行 SQL 查询时,你连接到数据库,获得结果,然后立即断开连接。为了保存这些结果,需要使用一个称为数据集的本地缓冲区。但是请记住,ADO.NET 也可以支持传统的面向连接的服务,并且在那里使用 DataReader。在这一章中,我将向您展示传统的面向连接的实现及其对应的非连接数据架构实现。
Note
ADO 代表 ActiveX 数据对象。但是微软基于 COM 的数据访问模型(ADO)不同于 ADO.NET。是的,有一些相似之处(例如,Command
和Connection
对象),但它们有很大的不同。许多 ADO.NET 类型在 ADO 中没有直接的对等项(例如,DataAdapter
)。ADO 有少量支持 COM 标准的数据类型;而 ADO.NET 是为。NET 应用连接数据库,它支持大量的数据类型。
理解代码
在 C# 中,数据库编程涉及到大量的类、接口、方法和属性。在这个层次上,对它们的详细描述并不重要。(老实说,如果你从所有这些术语的详细描述开始,可能会让你感到厌烦。他们通过练习被记住。)所以,让我们把重点放在接下来的演示中需要理解的部分。
在前两个演示中,我使用了面向连接的架构。在这种方法中,您的代码库显式地连接到数据存储,一旦数据处理完成,它就与数据存储断开连接。你经常看到使用Connection
对象、Command
对象和DataReader
对象。
在第三个演示中,您将看到非连接数据架构的使用。在这种方法中,您首先会看到一个DataSet
对象,它可以存储表、关系和约束(应用于表上)。一旦获得了这个对象,就可以使用它来遍历或操作数据,并将其用作客户端副本(即本地副本)。用户可以对本地副本进行更改,并在以后将更改应用到实际的数据库。这种方法加快了数据处理速度。它减少了网络流量,提高了应用的整体性能。
在 ADO.NET,你看不到连接不同数据库管理系统的单个对象集;取而代之的是各种数据提供者。这些提供程序针对连接特定的 DBMS(如 Oracle、MySQL 和 Microsoft SQL Server)进行了优化。例如,Microsoft 为 SQL Server 数据库提供了专门的优化类。这些类以 Sql 开始,包含在System.Data.SqlClient
中。
同样,在我们的例子中,类名以 MySql 开头;比如MySqlConnection
、MySqlCommandBuilder
、MySqlDataReader
。要在我的程序中使用这些类,需要以下名称空间。
using MySql.Data.MySqlClient;
您可以正确地假设每个提供者在一个命名空间中为您提供了一组类型。要在程序中使用这些类型,您需要包含相应的名称空间并安装正确的 NuGet 包(您已经学习了安装 MySql 的步骤。程序中的数据包)。在这个阶段,选择哪个数据库管理系统并不重要。通常,为了支持数据库编程的核心功能,每个提供者都提供相似的类和接口。当你看到演示时,你会对此有更好的理解。
首先,让我向您介绍一些在数据库编程中经常使用的常见对象类型,总结如下。
-
对象:连接和断开数据存储。
-
对象:表示 SQL 查询和存储过程。(存储过程将在后面讨论。)
-
DataReader
object:从连接架构中的数据库读取数据。 -
DataAdapter
对象:连接到一个数据库,从中获取记录,并填充一个本地缓冲区(DataSet
)。在一个不相连的架构中,它的作用是至关重要的。 -
对象:表示参数化查询中的参数。
尽管每个提供程序的核心类的具体名称不同,但由于这些类继承自相同的基类并实现相同的接口,因此您可以假设如何使用特定于供应商的数据库。例如,因为我使用的是 MySQL 数据库,所以你会看到在我的程序中使用了MySqlConnection
、MySqlDataReader
和MySqlCommand
。
类似地,其他供应商也提供遵循通用命名约定的名称。每个提供程序都在相关 DBMS 的名称前加上它们的构造。因此,如果您连接到一个 SQL 服务器,您可能会在类似的上下文中看到SqlConnection
、SqlDataReader
、SqlCommand
等等的用法。
有趣的是,没有一个名为Connection
的类。这同样适用于其他对象,例如Command
对象、DataAdapter
对象等等。因此,在这些上下文中,您只能看到特定于供应商的名称(例如,MySqlConnection, MySqlCommandBuilder, MySqlDataReader
等)。).
开始编码吧。在接下来的演示中,您会看到下面几行代码。
static MySqlConnection mySqlConnection = null;
static MySqlCommand mySqlCommand = null;
static MySqlDataReader mySqlDataReader = null;
让我们来关注这三个元素:MySqlConnection
、MySqlCommand
和MySqlDataReader
。我使用了“静态”变量,但这不是必需的。为了共享公共副本并避免不同方法中的重复初始化,我在这些程序中将它们设为静态。
Note
有来自 Visual Studio IDE 的部分/全部截图供您立即参考。我解释了重要的特征。
mysql 连接
MySqlConnection
是继承了DbConnection
的密封类。图 7-7 是来自 Visual Studio IDE 的MySqlConnection
类的部分截图。
图 7-7
Visual Studio 2019 中 MySqlConnection 类的部分截图
深入一点,你会发现DbConnection
是从IDbConnection
派生出来的。图 7-8 是来自 Visual Studio IDE 的DbConnection
类的部分截图。
图 7-8
Visual Studio 2019 中 DbConnection 类的部分截图
IDbConnection
包含在System.Data
名称空间中,其方法如图 7-9 所示。
图 7-9
来自 Visual Studio 2019 的 IDbConnection 界面的部分截图
这些接口成员配置到特定数据存储的连接。您可以随时展开方法描述来查看有关该方法的信息。但是在高层次上,您可以看到数据提供者类型需要覆盖抽象类方法或实现接口方法。MySqlConnection
正在做这个。
MySqlCommand
MySqlCommand
是继承自DbCommand
的密封类。图 7-10 是来自 Visual Studio IDE 的MySqlCommand
类的部分截图。
图 7-10
Visual Studio 2019 中 MySqlCommand 类的部分截图
DbCommand
是一个抽象类,它为表示各种命令的特定于数据库的类提供了一个基类。它包含一个名为ExecuteReader()
的方法,描述如下。
//
// Summary:
// Executes the System.Data.Common.DbCommand.CommandText against the // System.Data.Common.DbCommand.Connection,
// and returns an System.Data.Common.DbDataReader.
//
// Returns:
// A System.Data.Common.DbDataReader object.
public DbDataReader ExecuteReader();
MySqlCommand
类覆盖了这个方法。在演示 1 中,您将在下面的代码段中看到该类的用法。
mySqlCommand = new MySqlCommand(sqlQuery,mySqlConnection);
mySqlDataReader = mySqlCommand.ExecuteReader();
从这段代码中,你可以看到我正在使用下面的代码制作一个MySqlCommand
对象。
mySqlCommand = new MySqlCommand(sqlQuery,mySqlConnection);
在MySqlCommand
类的部分截图中,有四个重载的构造函数版本可用。在这段代码中,我使用了下面的版本。
// Summary:
// Initializes a new instance of the MySql.Data.MySqlClient.MySqlCommand class with the text of the query and a MySql.Data.MySqlClient.MySqlConnection.
//(Other details omitted)
public MySqlCommand(string cmdText, MySqlConnection connection);
然后我用ExecuteReader()
方法构建了一个MySqlDataReader
对象。
MySqlDataReader
MySqlDataReader
是一个密封类,扩展了DbDataReader
、IDataReader
、IDataRecord
和IDisposable
。图 7-11 是来自 VS2019 的部分截图;显示关于MySqlDataReader
的信息。
图 7-11
Visual Studio 2019 中 MySqlDataReader 类的部分截图
类成员帮助您从MySQL
数据库中读取只进的行流。换句话说,一旦获得了一个对象MySqlDataReader
,就可以以只读和只进的方式迭代结果。单词只进意味着一旦指向记录 2,就不能返回到记录 1,以此类推。
图 7-12 显示了IDataReader
的概要。
图 7-12
Visual Studio 2019 中 IDataReader 界面的部分截图
可以看到IDataReader
扩展了IDataRecord
接口,其中包含了很多方法。接口定义告诉我们,这些方法可以访问DataReader
的每一行中的列值。还可以从流中提取强类型值。
在演示 1 中,您会看到Close()
和Read()
方法。如果可能的话,Close()
方法关闭DataReader
对象,Read()
方法帮助DataReader
对象前进到下一条记录。
在接下来的演示中不会用到方法,但是我会包括一个 Visual Studio 截图,它可以在将来的实现中帮助你。图 7-13 显示了IDataRecord
界面的概要。
图 7-13
Visual Studio 2019 中 IDataRecord 界面的部分截图
实现面向连接的体系结构
现在您已经准备好实现一个面向连接的模型了。要使用 C# 应用连接到 MySQL 数据库,您需要有一个MySqlConnection
对象和一个连接字符串。一个连接字符串可能包含几个键值对,用分号分隔。在每个键/值对中,选项名及其对应的值由等号连接。在下一个演示中,您会看到下面一行代码。
connectMe = "server=localhost;database=test;username=root;password=admin";
mySqlConnection = new MySqlConnection(connectMe);
mySqlConnection
是MySqlConnection
的实例。这一行简单地说明我将使用一个 MySqlConnection 对象,它被配置为连接到位于localhost (the server)
的 MySQL 服务器。数据库名为test
,用户名为root
,密码为admin
。
在演示 1 中,在Main()
内部,您可以看到以下三种方法的存在。
// Open the database connection i.e. connect to a MySQL database.
ConnectToMySqlDatabase();
// Display details of Employee table.
DisplayRecordsFromEmployeeTable();
// Close the database connection.
CloseDatabaseConnection();
根据这些方法的名称和支持注释,很容易假设我正在使用一个C#
应用打开一个到MySQL
数据库的连接,然后从一个名为 Employee 的表中检索信息,最后关闭连接。在这个例子中,所有的方法都被try-catch
块包围。这是推荐的做法。一旦遇到异常,这个结构可以帮助你更好地分析情况。
Open()
和Close()
方法用于打开和关闭该程序中的连接。MySqlConnection
类中的Open()
方法的细节如下所示。
public override void Open();
这只是说供应商已经覆盖了DbConnection
的Open()
方法,其中Open()
被声明为抽象方法,如下所示。
//
// Summary:
// When overridden in a derived class, opens a database connection with
// the settings specified by the System.Data.Common.DbConnection.// ConnectionString.public abstract void Open();
Close()
方法也在MySqlConnection
类中被覆盖,但是它关闭了一个数据库连接。
在DisplayRecordsFromEmployeeTable()
方法中,你可以看到MySqlCommand
对象和ExecuteReader()
方法。
mySqlDataReader.Close();
当读取完Employee
表中的所有记录后,关闭DataReader
对象。
在许多情况下,您会看到类似程序的 Windows 窗体应用。当您使用 Windows 窗体(ASP.NET WPF、UWP 等)时。),你得到了更好的布局,你可以通过使用各种控件来美化作品。但是本章对那些美化不感兴趣。对于本书中的几乎所有程序,我都使用了控制台应用,我在这里也保持不变。
演示 1
这是完整的演示。
using System;
using MySql.Data.MySqlClient;
namespace ConnectingDatabase
{
class Program
{
static string connectMe = String.Empty;
static MySqlConnection mySqlConnection = null;
static MySqlCommand mySqlCommand = null;
static MySqlDataReader mySqlDataReader = null;
static void Main(string[] args)
{
Console.WriteLine("∗∗∗Demonstration-1.Connecting and retrieving details from a MySQL database table.∗∗∗");
try
{
/∗ Open the database connection i.e. connect to a MySQL database.∗/
ConnectToMySqlDatabase();
// Display details of Employee table.
DisplayRecordsFromEmployeeTable();
// Close the database connection.
CloseDatabaseConnection();
}
catch (Exception ex)
{
Console.WriteLine("Caught exception.Here is the problem details.");
Console.WriteLine(ex.Message);
}
Console.ReadKey();
}
private static void DisplayRecordsFromEmployeeTable()
{
try
{
string sqlQuery = "select ∗ from Employee ;";
mySqlCommand = new MySqlCommand(sqlQuery,mySqlConnection);
mySqlDataReader = mySqlCommand.ExecuteReader();
Console.WriteLine("EmployeeId\t" + "EmployeeName\t" + "Age\t" + "Salary");
Console.WriteLine("_____________________________________");
while (mySqlDataReader.Read())
{
Console.WriteLine(mySqlDataReader["EmpId"] + "\t\t" + mySqlDataReader["Name"] + "\t\t" + mySqlDataReader["Age"] + "\t" + mySqlDataReader["Salary"]);
}
mySqlDataReader.Close();
}
catch (MySqlException ex)
{
Console.WriteLine("Cannot show the records.Here is the problem details.");
Console.WriteLine(ex.Message);
}
}
private static void ConnectToMySqlDatabase()
{
try
{
connectMe = "server=localhost;database=test;username=root;password=admin";
mySqlConnection = new MySqlConnection(connectMe);
mySqlConnection.Open();
Console.WriteLine("Connection to MySQL successful.");
}
catch (MySqlException ex)
{
Console.WriteLine("Could not connect to the database.Here is the problem details.");
Console.WriteLine(ex.Message);
}
}
private static void CloseDatabaseConnection()
{
try
{
mySqlConnection.Close();
}
catch (MySqlException ex)
{
Console.WriteLine("Could not close the connection.Here is the problem details.");
Console.WriteLine(ex.Message);
}
}
}
}
输出
这是输出。
∗∗∗Demonstration-1.Connecting and retrieving details from a MySQL database table.∗∗∗
Connection to MySQL successful.
EmployeeId EmployeeName Age Salary
___________________________________________
1 Amit 25 1200.5
2 Sam 23 1000.25
3 Bob 30 1500
分析
首先,在 C# 应用和 MySQL 数据库之间建立连接;然后可以从 Employee 表中检索信息。因为它是面向连接的体系结构,所以从数据库中获取记录后,就不能关闭连接。为了测试这一点,让我们修改下面的代码块,并假设您调用了CloseDatabaseConnection()
方法,如下所示。
while (mySqlDataReader.Read())
{
Console.WriteLine(mySqlDataReader["EmpId"] + "\t\t" + mySqlDataReader["Name"] + "\t\t" + mySqlDataReader["Age"] + "\t" + mySqlDataReader["Salary"]);
// Closing the connection
CloseDatabaseConnection();
}
如果您执行该程序,您会得到以下输出。
∗∗∗Demonstration-1.Connecting and retrieving details from a MySQL database table.∗∗∗
Connection to MySQL successful.
EmployeeId EmployeeName Age Salary
___________________________________________
1 Amit 25 1200.5
Cannot show the records. Here is the problem details.
Invalid attempt to Read when reader is closed.
在演示 3 中,您将学习如何在连接关闭后继续工作。
演示 2
演示 1 向您展示了如何建立连接、关闭连接以及从数据库的表中检索信息。这个演示介绍了另外三种方法:一种在表中插入记录,一种从表中删除记录,一种更新表。它们分别被命名为InsertNewRecordIntoEmployeeTable()
、DeleteRecordFromEmployeeTable()
和UpdateExistingRecordIntoEmployeeTable()
。
这个计划很简单,但我想强调以下几点。
-
一旦工作完成,每个方法都会调用
DisplayRecordsFromEmployeeTable()
方法来显示表的当前状态。 -
在删除记录时,您看到了一种从键盘接受用户提供的输入的方法。您可以在类似的上下文中参考以下代码行:
mySqlCmd = new MySqlCommand("Delete from employee where name=@NameToBeDeleted", mySqlConnection); mySqlCmd.Parameters.AddWithValue("@NameToBeDeleted", empNameToDelete); mySqlCmd.Prepare(); mySqlCmd.ExecuteNonQuery(); mySqlCmd is an object of MySqlCmd.
-
Inside the
DeleteRecordFromEmployeeTable()
method in the following segment of code, you see a comment prior to the line of code./∗ If deletion performs successfully, print this message.∗/ Console.WriteLine("One record is deleted from employee table.");
我用这个来表示我没有验证用户输入。为了简单起见,我没有包括验证,但是您可能需要对所有方法进行类似的验证。我把这个简单的练习留给你。(然而,在演示 4 中,在执行删除操作之后,使用了一个简单的验证。)
-
在
UpdateExistingRecordIntoEmployeeTable()
方法中,我更新了 Bob 的工资两次。最初,我将其更改为 3000.75,后来我将其重置为旧值:1500.00。我这样做是为了保持表的原始值。在实际编程中,最好对原始表进行备份。如果需要,可以使用备份表。您可以通过各种方式进行备份。但是这里我们处理的是一个名为Employee
的表。因此,在这种情况下,您可以使用下面的查询从现有的 employee 表中创建另一个表(比如,employee_backup
)并随意使用它。 -
重要的是要注意一个情况。prior 命令复制包含数据的表,但不复制其他数据库对象,如主键、外键、索引等。(如果您不知道这些键,请参考问答 7.9)。要从现有的表和所有依赖对象中复制数据,您可以使用下面两个命令(这里我假设您通过复制
employee_backup
来创建Employee
表)。我还在创建表格之前检查它是否已经存在):create table if not exists employee like employee_backup; insert employee select ∗ from employee_backup;
create table employee_backup as (select ∗ from employee);
现在进行完整的演示。
using System;
using MySql.Data.MySqlClient;
namespace ExercisingSqlCommands
{
class Program
{
static string connectMe = String.Empty;
static MySqlConnection mySqlConnection = null;
static MySqlDataReader mySqlDataReader = null;
static MySqlCommand mySqlCommand = null;
static void Main(string[] args)
{
Console.WriteLine("∗∗∗Demonstration-2.Connecting and retrieving details from a MySQL database table.∗∗∗");
try
{
/∗ Open the database connection i.e. connect to a MySQL database.∗/
ConnectToMySqlDatabase();
// Display details of Employee table.
DisplayRecordsFromEmployeeTable();
#region insert and delete a record
// Insert a new record in Employee table.
InsertNewRecordIntoEmployeeTable();
// Delete a record from the Employee table.
DeleteRecordFromEmployeeTable();
#endregion
#region Update and reset a record
/∗
First updating a record and then resetting the value. So, basically there are two updates.∗/
UpdateExistingRecordIntoEmployeeTable();
#endregion
//Close the database connection.
CloseDatabaseConnection();
}
catch (Exception ex)
{
Console.WriteLine("Caught exception.Here is the problem details.");
Console.WriteLine(ex.Message);
}
Console.ReadKey();
}
private static void UpdateExistingRecordIntoEmployeeTable()
{
try
{
Console.WriteLine("Updating Bob's salary to 3000.75");
mySqlCommand = new MySqlCommand("update Employee set Salary=3000.75 where name="Bob";", mySqlConnection);
mySqlCommand.ExecuteNonQuery();
// If update performs successfully , print this message.
Console.WriteLine("One record is updated in employee table.");
Console.WriteLine("Here is the current table:");
DisplayRecordsFromEmployeeTable();
Console.WriteLine("Now resetting Bob's salary to 1500.00");
mySqlCommand = new MySqlCommand("update Employee set Salary=1500.00 where name="Bob";", mySqlConnection);
mySqlCommand.ExecuteNonQuery();
// If update performs successfully , print this message.
Console.WriteLine("One record is updated in employee table.");
Console.WriteLine("Here is the current table:");
DisplayRecordsFromEmployeeTable();
}
catch (MySqlException ex)
{
Console.WriteLine("Cannot update the record.Here is the problem details.");
Console.WriteLine(ex.Message);
}
}
private static void DeleteRecordFromEmployeeTable()
{
try
{
Console.WriteLine("Enter the employee name to be deleted from Employee table.");
string empNameToDelete = Console.ReadLine();
/∗ Additional validation required to confirm the employee name exists in the table.
Or, whether its a valid entry or not.
∗/
mySqlCmd = new MySqlCommand("Delete from employee where name=@NameToBeDeleted", mySqlConnection); mySqlCmd.Parameters.AddWithValue("@NameToBeDeleted", empNameToDelete);
mySqlCmd.Prepare();
mySqlCmd.ExecuteNonQuery();
/* If deletion performs successfully , print this message.*/
Console.WriteLine("One record is deleted from employee table.");
Console.WriteLine("Here is the current table:");
DisplayRecordsFromEmployeeTable();
}
catch (MySqlException ex)
{
Console.WriteLine("Cannot delete the record.Here is the problem details.");
Console.WriteLine(ex.Message);
}
}
private static void InsertNewRecordIntoEmployeeTable()
{
try
{
mySqlCommand = new MySqlCommand("insert into Employee values(4,'John',27,975);", mySqlConnection);
mySqlCommand.ExecuteNonQuery();
Console.WriteLine("New record insertion successful.");
Console.WriteLine("Here is the current table:");
DisplayRecordsFromEmployeeTable();
}
catch (MySqlException ex)
{
Console.WriteLine("Cannot insert the new record.Here is the problem details.");
Console.WriteLine(ex.Message);
}
}
private static void DisplayRecordsFromEmployeeTable()
{
try
{
string sqlQuery = "select ∗ from Employee ;";
mySqlCommand = new MySqlCommand(sqlQuery, mySqlConnection);
mySqlDataReader = mySqlCommand.ExecuteReader();
Console.WriteLine("EmployeeId\t" + "EmployeeName\t" + "Age\t" + "Salary");
Console.WriteLine("_____________________________________");
while (mySqlDataReader.Read())
{
Console.WriteLine(mySqlDataReader["EmpId"] + "\t\t" + mySqlDataReader["Name"] + "\t\t" + mySqlDataReader["Age"] + "\t" + mySqlDataReader["Salary"]);
}
mySqlDataReader.Close();
}
catch (MySqlException ex)
{
Console.WriteLine("Cannot show the records.Here is the problem details.");
Console.WriteLine(ex.Message);
}
}
private static void ConnectToMySqlDatabase()
{
try
{
connectMe = "server=localhost;database=test;username=root;password=admin";
mySqlConnection = new MySqlConnection(connectMe);
mySqlConnection.Open();
Console.WriteLine("Connection to MySQL successful.");
}
catch (MySqlException ex)
{
Console.WriteLine("Could not connect to the database.Here is the problem details.");
Console.WriteLine(ex.Message);
}
}
private static void CloseDatabaseConnection()
{
try
{
mySqlConnection.Close();
}
catch (MySqlException ex)
{
Console.WriteLine("Could not close the connection.Here is the problem details.");
Console.WriteLine(ex.Message);
}
}
}
}
输出
这是输出。
∗∗∗Demonstration-2.Connecting and retrieving details from a MySQL database table.∗∗∗
Connection to MySQL successful.
EmployeeId EmployeeName Age Salary
___________________________________________
1 Amit 25 1200.5
2 Sam 23 1000.25
3 Bob 30 1500
New record insertion successful.
Here is the current table:
EmployeeId EmployeeName Age Salary
___________________________________________
1 Amit 25 1200.5
2 Sam 23 1000.25
3 Bob 30 1500
4 John 27 975
Enter the employee name to be deleted from Employee table.
John
One record is deleted from employee table.
Here is the current table:
EmployeeId EmployeeName Age Salary
___________________________________________
1 Amit 25 1200.5
2 Sam 23 1000.25
3 Bob 30 1500
Updating Bob's salary to 3000.75
One record is updated in employee table.
Here is the current table:
EmployeeId EmployeeName Age Salary
___________________________________________
1 Amit 25 1200.5
2 Sam 23 1000.25
3 Bob 30 3000.75
Now resetting Bob's salary to 1500.00
One record is updated in employee table.
Here is the current table:
EmployeeId EmployeeName Age Salary
___________________________________________
1 Amit 25 1200.5
2 Sam 23 1000.25
3 Bob 30 1500
实现断开连接的数据架构
现在是时候向您展示一个非连接数据架构的演示了(也称为非连接层)。演示 3 就是为此而做的。在这里,您可以看到DataTable
、DataRow
和DataSet
类。要获得这些类,您需要包含以下名称空间。
using System.Data;
是 ADO.NET 议会的核心。这个名称空间还包含其他重要的类,如DataColumn
、DataRelation
和Constraint
(它是一个抽象类)。以下是对它们每一个的简要描述。
-
DataSet
:它是你的本地缓冲区(内存缓存),是表或者记录集的集合。 -
DataTable
:使用行和列以表格形式包含数据。 -
DataRow
:表示DataTable
中的单个行(即记录)。 -
DataColumn
:表示DataTable
中的一列。 -
DataRelation
:表示两个DataTable
对象之间的父子关系。 -
Constraint
:
表示对一个或多个DataColumn
对象的限制。
在本演示中,您会看到下面几行代码。
static MySqlDataAdapter mySqlDataAdapter = null;
static MySqlCommandBuilder mySqlCommandBuilder = null;
所以,让我们来看看他们。
MySqlDataAdapter
MySqlDataAdapter
是一个密封类,因此不能创建另一个从它继承的类。class summary 声明它代表一组数据命令和一个数据库连接,用于填充数据集和更新 MySQL 数据库。看看 VS2019 怎么说。图 7-14 是MySqlDataAdapter
级的部分截图。
图 7-14
Visual Studio 2019 中 MySqlDataAdapter 类的部分截图
图 7-14 告诉你MySqlDataAdapter
继承了DbAdpater
、IDbDataAdapter
和IDataAdapter
。命名约定表明IDbDataAdapter
和IDataAdapter
是两个接口。我们来看看这些接口的总结。图 7-15 是IDbDataAdapter
的截图。
图 7-15
来自 Visual Studio 2019 的 IDbDataAdapter 界面的部分截图
您可以看到IDbDataAdapter
有四个属性,用于从数据库中选择、插入、更新或删除记录。图 7-15 显示IDataAdapter
是IDbDataAdapter
的父级。
图 7-16 是来自 VS2019 的截图;它显示了IDataAdapter
的摘要。
图 7-16
Visual Studio 2019 中 IDataAdapter 接口的部分截图
在这个接口中,您可以看到属性和方法。当您实现一个断开的数据架构时,Fill
、FillSchema
和Update
方法是非常常见的。在演示 3 中,您会看到这些方法。演示 3 的下面一段代码展示了Fill
和FillSchema
方法。
// Retrieve details from 'Employee' table
mySqlDataAdapter.FillSchema(localDataSet, SchemaType.Source, "Employee");
mySqlDataAdapter.Fill(localDataSet, "Employee");
在使用Fill()
方法之前,您可能需要调用FillSchema()
,这允许您匹配源表的模式。当您在DataTable
中离线插入新记录时,这一点很重要。
在许多应用中,您可能只会看到Fill()
方法。在这些应用中,Fill()
方法是最重要的方法,因为它是DataAdapter
对象连接到物理数据库并获取查询结果的步骤(在本例中,我们在Fill()
之前调用了FillSchema()
)。
在这个界面中,您可以看到TableMappings
属性,它将数据库列名(来自源表)映射到更加用户友好的显示名称(到数据集表)。
最后,DbDataAdapter
是继承了前面两个接口的抽象类。方法总结指出这个类帮助实现了IDbDataAdapter
接口。图 7-17 是来自 Visual Studio IDE 的DbDataAdapter
类的部分截图。
图 7-17
Visual Studio 2019 中 DbDataAdapter 类的部分截图
MySqlCommandBuilder
MySqlCommandBuilder
是继承自DbCommandBuilder
的密封类。class summary 声明它会自动生成单表命令,以便将对DataSet
的更改与相关的 MySQL 数据库进行协调。
图 7-18 是来自 Visual Studio IDE 的MySqlCommandBuilder
的部分截图。
图 7-18
Visual Studio 2019 中 MySqlCommandBuilder 类的部分截图
在演示 3 中,当您想要将您的本地更改反映到实际数据库时,您会看到下面几行。
mySqlCommandBuilder = new MySqlCommandBuilder(mySqlDataAdapter);
Console.WriteLine("Syncing with remote database table");
mySqlDataAdapter.Update(localDataSet, "Employee");
这里,mySqlDataAdapter
是MySqlDataAdapter
的一个宾语。
演示 3
此演示显示 Employee 表中的记录,插入一条新记录,然后删除一条记录。为了便于理解这些操作,这些方法分别被命名为DisplayRecordsFromEmployeeTable
、InsertRecordIntoEmployeeTable
和 DeleteRecordIntoEmployeeTable。如果您在本演示之前浏览了类描述和讨论,您应该不难理解该代码——除了下面的代码段。
// Creates a new record with the same schema as the table
DataRow currentRow = localDataTable.NewRow();
currentRow["EmpId"] = 4;
currentRow["Name"] = "Jack";
currentRow["Age"] = 40;
currentRow["Salary"] = 2500.75;
// Add this record to local table
localDataTable.Rows.Add(currentRow);
是的,你猜对了!支持性的注释告诉您,这是向表中添加记录的一种方式。然而,有一种替代方法可以做到这一点。例如,下面的代码段也适用于这种环境。
// Also works
currentRow[0] = 4;
currentRow[1] = "Jack";
currentRow[2] = 40;
currentRow[3] = 2500.75;
你可以选择你喜欢的方法。如果您想减少输入,请选择第二种方法。如果你想要更好的可读性,选择第一个。
现在来看完整的演示和相应的输出。
using System;
using System.Data;
using MySql.Data.MySqlClient;
namespace ConnectingDatabase
{
class Program
{
static string connectMe = String.Empty;
static string sqlCommand = String.Empty;
static MySqlDataAdapter mySqlDataAdapter = null;
static MySqlCommandBuilder mySqlCommandBuilder = null;
static DataSet localDataSet = null;
static void Main(string[] args)
{
Console.WriteLine("∗∗∗Connecting and retrieving details from a MySQL database table.∗∗∗");
Console.WriteLine("∗∗∗Testing the disconnected architecture now.∗∗∗");
try
{
// Get a local copy of Employee table
DataTable localDataTable = CreateLocalTable();
//Display from the client-side(local)table.
DisplayRecordsFromEmployeeTable(localDataTable);
/∗ Insert a new record into local table and sync it with the database∗/
InsertRecordIntoEmployeeTable(localDataTable);
Console.WriteLine("∗∗After Inserting a record into the Employee table...∗∗");
DisplayRecordsFromEmployeeTable(localDataTable);
/∗ Delete an existing record from local table and sync it with the database. ∗/
DeleteRecordIntoEmployeeTable(localDataTable);
Console.WriteLine("∗∗After deleting a record into the Employee table...∗∗");
DisplayRecordsFromEmployeeTable(localDataTable);
}
catch (Exception ex)
{
Console.WriteLine("Caught exception.Here is the problem details.");
Console.WriteLine(ex.Message);
}
Console.ReadKey();
}
private static void DeleteRecordIntoEmployeeTable(DataTable localDataTable)
{
try
{
Console.WriteLine("Now deleting the record for EmpId4.");
DataTable dataTable = localDataSet.Tables["Employee"];
// Deleting a record
DataRow deleteRow = dataTable.Rows.Find(4);
deleteRow.Delete();
//If deletion performs successfully, print this message.
Console.WriteLine("Successfully deleted the record from local buffer where EmpId was 4.");
// Apply the change to MySQL
mySqlCommandBuilder = new MySqlCommandBuilder(mySqlDataAdapter);
Console.WriteLine("Syncing with remote database table");
mySqlDataAdapter.Update(localDataSet, "Employee");
Console.WriteLine("Successfullly updated the remote table.\n");
}
catch (MySqlException ex)
{
Console.WriteLine("Could not delete the record.Here is the problem details.");
Console.WriteLine(ex.Message);
}
}
private static void InsertRecordIntoEmployeeTable(DataTable localDataTable)
{
try
{
/∗ Creates a new record with the same schema as the table.∗/
DataRow currentRow = localDataTable.NewRow();
currentRow["EmpId"] = 4;
currentRow["Name"] = "Jack";
currentRow["Age"] = 40;
currentRow["Salary"] = 2500.75;
// Add this record to local table
localDataTable.Rows.Add(currentRow);
Console.WriteLine("Successfully added a record into local buffer.");
int noOfRecords = localDataTable.Rows.Count;
Console.WriteLine("Local table currently has {0} number of records.", noOfRecords);
// Apply the change to MySQL
mySqlCommandBuilder = new MySqlCommandBuilder(mySqlDataAdapter);
Console.WriteLine("Syncing with remote database table");
mySqlDataAdapter.Update(localDataSet, "Employee");
Console.WriteLine("Successfullly updated the remote table");
}
catch (MySqlException ex)
{
Console.WriteLine("Could not insert the record.Here is the problem details.");
Console.WriteLine(ex.Message);
}
}
private static void DisplayRecordsFromEmployeeTable(DataTable localDataTable)
{
try
{
int noOfRecords = localDataTable.Rows.Count;
Console.WriteLine("Here is the table for you:");
Console.WriteLine("EmployeeId\t" + "EmployeeName\t" + "Age\t" + "Salary");
Console.WriteLine("_____________________________________");
for (int currentRow = 0; currentRow < noOfRecords; currentRow++)
{
Console.WriteLine(
localDataTable.Rows[currentRow]["EmpId"] + "\t\t" +
localDataTable.Rows[currentRow]["Name"] + "\t\t" +
localDataTable.Rows[currentRow]["Age"] + "\t" +
localDataTable.Rows[currentRow]["Salary"]
);
}
}
catch (MySqlException ex)
{
Console.WriteLine("Cannot show the records.Here is the problem details.");
Console.WriteLine(ex.Message);
}
}
private static DataTable CreateLocalTable()
{
connectMe = "datasource=localhost;port=3306;database=test;username=root;password=admin";
sqlCommand = "select ∗ from Employee";
mySqlDataAdapter = new MySqlDataAdapter(sqlCommand, connectMe);
// Also works
//mySqlConnection = new MySqlConnection(connectMe);
//mySqlDataAdapter = new MySqlDataAdapter(sqlCommand, mySqlConnection);
// Create a DataSet instance
/∗ I recommend you to use the following overloaded constructor of DataSet to use.∗/
localDataSet = new DataSet("LocalDataSet");
// Retrieve details from 'Employee' table
mySqlDataAdapter.FillSchema(localDataSet, SchemaType.Source, "Employee");
mySqlDataAdapter.Fill(localDataSet, "Employee");
// Create new instance of DataTable
DataTable dataTable = localDataSet.Tables["Employee"];
int noOfRecords = dataTable.Rows.Count;
Console.WriteLine("Created a local DataTable.Total number of records in this table is:{0}", noOfRecords);
return dataTable;
}
}
}
输出
这是输出。
∗∗∗Connecting and retrieving details from a MySQL database table.∗∗∗
∗∗∗Testing the disconnected architecture now.∗∗∗
Created a local DataTable.Total number of records in this table is:3
Here is the table for you:
EmployeeId EmployeeName Age Salary
___________________________________________
1 Amit 25 1200.5
2 Sam 23 1000.25
3 Bob 30 1500
Successfully added a record into local buffer.
Local table currently has 4 number of records.
Syncing with remote database table
Successfullly updated the remote table
∗∗After Inserting a record into the Employee table...∗∗
Here is the table for you:
EmployeeId EmployeeName Age Salary
___________________________________________
1 Amit 25 1200.5
2 Sam 23 1000.25
3 Bob 30 1500
4 Jack 40 2500.75
Now deleting the record for EmpId4.
Successfully deleted the record from local buffer where EmpId was 4.
Syncing with remote database table
Successfullly updated the remote table.
∗∗After deleting a record into the Employee table...∗∗
Here is the table for you:
EmployeeId EmployeeName Age Salary
___________________________________________
1 Amit 25 1200.5
2 Sam 23 1000.25
3 Bob 30 1500
使用存储过程编程
您已经看到了 SQL 语句在前面所有程序中的使用。当您开始学习数据库编程时,这很好。但是这种方法有一个潜在的缺点。使用这些简单的 SQL 语句,您可以在代码中公开数据库模式(设计),这是可以更改的。这就是为什么在现实世界的应用中,您通常会看到存储过程的使用,而不是普通的 SQL 语句。
存储过程具有以下特征。
-
它们是预编译的可执行对象。
-
您可以在存储过程中使用一个或多个 SQL 语句。
-
任何复杂的 SQL 语句都可以用存储过程代替。
-
它们可以接受输入并返回输出。
已经说过,我在本章中使用简单的 SQL 语句来演示数据库编程的思想,如果您是 SQL 新手,我建议您在首选数据库中做 SQL 语句练习,以熟悉 SQL 语句。我建议存储过程也是如此。
现在我将向您展示如何用简单的存储过程替换 SQL 语句。
出于演示目的,让我们回到演示 1 或演示 2。在这里,您看到了 select、insert、update 和 delete SQL 语句的使用。在下一个演示中,我用存储过程替换 select、insert 和 delete 语句。剩下的情况,更新,非常简单。我把这个练习留给你。
现在,我们开始吧。
Note
附录 A 包括在 MySQL 数据库中创建这些存储过程所需的完整命令。如果需要,可以参考他们。
存储过程来选择记录
在我的数据库中,我创建了以下名为GetAllEmployees
的存储过程,来替换演示 1 或演示 2 中的 select 查询。
DELIMITER $
CREATE PROCEDURE GetAllEmployees()
BEGIN
SELECT ∗ FROM EMPLOYEE;
END $
DELIMITER ;
存储过程插入一条记录
在我的数据库中,我创建了以下名为InsertOneNewRecord
的存储过程,来替换演示 2 中的插入查询。此存储过程插入具有预定义值的记录。
DELIMITER $
CREATE PROCEDURE InsertOneNewRecord()
BEGIN
insert into Employee values(4,'John',27,975);
END $
DELIMITER ;
存储过程删除一条记录
在我的数据库中,我创建了以下名为DeleteOneRecord
的存储过程,来替换演示 2 中的删除查询。该存储过程根据用户的输入删除一条记录。
DELIMITER //
CREATE PROCEDURE DeleteOneRecord(
IN NameToBeDeleted varchar(10)
)
BEGIN
Delete from employee where Name=NameToBeDeleted;
END //
DELIMITER;
Note
您可以选择分隔符。注意,在前两个存储过程中,我使用了$
作为分隔符,但是在最后一个存储过程中,我使用了//
。
一个简单的验证
为了简单和减少代码量,我告诉你在不同的操作之后实现你自己的验证方法。在这个演示中,我将使用下面的代码段(在DeleteRecordFromEmployeeTable()
方法中)向您展示一个简单的验证技术。
if (mySqlCommand.ExecuteNonQuery() == 1)
{
// If deletion performs successfully , print this message.
Console.WriteLine("One record is deleted from employee table.");
}
else
{
Console.WriteLine("Couldn't delete the record from employee table.");
}
如果您在 Visual Studio IDE 中看到ExecuteNonQuery()
方法描述,您将获得以下信息。
//
// Summary:
// Executes a SQL statement against the connection and returns the // number of rows affected.
//
// Returns:
// Number of rows affected
//
// Remarks:
// You can use ExecuteNonQuery to perform any type of database // operation, however any resultsets returned will not be available. // Any output parameters used in calling a stored procedure will be // populated with data and can be retrieved after execution is // complete. For UPDATE, INSERT, and DELETE statements, the return // value is the number of rows affected by the command. For all other // types of statements,the return value is -1.
public override int ExecuteNonQuery();
这个描述是不言自明的。您可以很容易地将这种方法用于自己的验证目的。
演示 4
这是完整的演示。方法名和操作与demonstration 2
相似,但是这次使用了简单的存储过程。我建议你参考相关评论,以便更好地了解。
using System;
using System.Data;
using MySql.Data.MySqlClient;
namespace UsingStoredProcedures
{
class Program
{
static string connectMe = String.Empty;
static MySqlConnection mySqlConnection = null;
static MySqlCommand mySqlCommand = null;
static MySqlDataReader mySqlDataReader = null;
static void Main(string[] args)
{
Console.WriteLine("∗∗∗Demonstration-4.Using stored procedure now.∗∗∗");
try
{
/∗ Open the database connection i.e. connect to a MySQL database∗/
ConnectToMySqlDatabase();
// Display details of Employee table.
DisplayRecordsFromEmployeeTable();
// Insert a new record in Employee table.
InsertNewRecordIntoEmployeeTable();
// Delete a record from the Employee table.
DeleteRecordFromEmployeeTable();
// Close the database connection.
CloseDatabaseConnection();
}
catch (Exception ex)
{
Console.WriteLine("Caught exception.Here is the problem details.");
Console.WriteLine(ex.Message);
}
Console.ReadKey();
}
private static void DisplayRecordsFromEmployeeTable()
{
try
{
#region old code( which you saw in previous demonstrations)
//string sqlQuery = "select ∗ from Employee ;";
//mySqlCommand = new MySqlCommand(sqlQuery, mySqlConnection);
#endregion
#region new code
//The following lines are moved to a common place
//mySqlCommand = new MySqlCommand();
//mySqlCommand.Connection = mySqlConnection;
mySqlCommand.CommandText = "SelectAllEmployees";//Using Stored Procedure
mySqlCommand.CommandType = CommandType.StoredProcedure;
#endregion
mySqlDataReader = mySqlCommand.ExecuteReader();
Console.WriteLine("EmployeeId\t" + "EmployeeName\t" + "Age\t" + "Salary");
Console.WriteLine("_____________________________________");
while (mySqlDataReader.Read())
{
Console.WriteLine(mySqlDataReader["EmpId"] + "\t\t" + mySqlDataReader["Name"] + "\t\t" + mySqlDataReader["Age"] + "\t" + mySqlDataReader["Salary"]);
}
mySqlDataReader.Close();
}
catch (MySqlException ex)
{
Console.WriteLine("Cannot show the records.Here is the problem details.");
Console.WriteLine(ex.Message);
}
}
private static void InsertNewRecordIntoEmployeeTable()
{
try
{
// Old code (you saw in demonstration 2)
//mySqlCommand = new MySqlCommand("insert into Employee values(4,'John',27,975);", mySqlConnection);
#region new code
//The following lines are moved to a common place
//mySqlCommand = new MySqlCommand();
//mySqlCommand.Connection = mySqlConnection;
mySqlCommand.CommandText = "InsertOneNewrecord";// Using Stored Procedure
mySqlCommand.CommandType = CommandType.StoredProcedure;
#endregion
mySqlCommand.ExecuteNonQuery();
Console.WriteLine("New record insertion successful.");
Console.WriteLine("Here is the current table:");
DisplayRecordsFromEmployeeTable();
}
catch (MySqlException ex)
{
Console.WriteLine("Cannot insert the new record.Here is the problem details.");
Console.WriteLine(ex.Message);
}
}
private static void DeleteRecordFromEmployeeTable()
{
try
{
Console.WriteLine("Enter the employee name to be deleted from Employee table.");
string empNameToDelete = Console.ReadLine();
#region new code
MySqlParameter deleteParameter = new MySqlParameter("NameToBeDeleted", MySqlDbType.VarChar);
mySqlCommand.CommandType = CommandType.StoredProcedure;
mySqlCommand.CommandText = "DeleteOneRecord";
// Using Stored Procedure
/∗ The following code segment will also work but in that case, you have to add the value to the parameter first.∗/
//deleteParameter.Value = empNameToDelete;
//mySqlCommand.Parameters.Add(deleteParameter);
mySqlCommand.Parameters.AddWithValue("NameToBeDeleted", empNameToDelete);
#endregion
if (mySqlCommand.ExecuteNonQuery()==1)
{
// If deletion performs successfully, print this message.
Console.WriteLine("One record is deleted from employee table.");
}
else
{
Console.WriteLine("Couldn't delete the record from employee table.");
}
Console.WriteLine("Here is the current table:");
DisplayRecordsFromEmployeeTable();
}
catch (MySqlException ex)
{
Console.WriteLine("Cannot delete the record.Here is the problem details.");
Console.WriteLine(ex.Message);
}
}
private static void ConnectToMySqlDatabase()
{
try
{
// The following will also work
//connectMe = "datasource=localhost;port=3306;database=test;username=root;password=admin";
connectMe = "server=localhost;database=test;username=root;password=admin";
mySqlConnection = new MySqlConnection(connectMe);
mySqlConnection.Open();
Console.WriteLine("Connection to MySQL successful.");
// Initializing Command here to remove duplicate codes.
mySqlCommand = new MySqlCommand();
mySqlCommand.Connection = mySqlConnection;
}
catch (MySqlException ex)
{
Console.WriteLine("Could not connect to the database.Here is the problem details.");
Console.WriteLine(ex.Message);
}
}
private static void CloseDatabaseConnection()
{
try
{
mySqlConnection.Close();
}
catch (MySqlException ex)
{
Console.WriteLine("Could not close the connection.Here is the problem details.");
Console.WriteLine(ex.Message);
}
}
}
}
输出
这是输出。
∗∗∗Demonstration-4.Using stored procedure now.∗∗∗
Connection to MySQL successful.
EmployeeId EmployeeName Age Salary
___________________________________________
1 Amit 25 1200.5
2 Sam 23 1000.25
3 Bob 30 1500
New record insertion successful.
Here is the current table:
EmployeeId EmployeeName Age Salary
___________________________________________
1 Amit 25 1200.5
2 Sam 23 1000.25
3 Bob 30 1500
4 John 27 975
Enter the employee name to be deleted from Employee table.
John
One record is deleted from employee table.
Here is the current table:
EmployeeId EmployeeName Age Salary
___________________________________________
1 Amit 25 1200.5
2 Sam 23 1000.25
3 Bob 30 1500
问答环节
7.1 为什么 ADO.NET 支持不同的提供者连接到不同的数据库,而不是给出一组对象来与不同的数据库通信?
以下是使用这种方法可以获得的一些重要好处。
-
特定的提供程序可以帮助您直接连接到特定的数据库。因此,您不需要支持调用者和数据存储之间的任何中间层。
-
对于特定的数据库,提供程序具有特殊和独特的功能。您可以从这种专业支持中获益。
7.2 在 ADO.NET****使用非连接数据架构有什么好处?
在大多数现实世界的应用中,所需的数据保存在远程计算机上,您通过网络连接到该计算机。在断开连接的数据架构中,一旦获得了DataSet
对象,所需的数据就保留在本地机器中,因此可以快速访问这些数据。同时,由于您不需要互联网连接来访问本地数据,因此可以减少网络流量。因此,应用的整体性能得到了增强。
最后,您可以对本地数据进行任何更改,并进行自己的实验。您可以决定是否要在实际的数据库中反映这些变化(通过调用适配器的Update()
方法)。在实际的数据库上做实验显然不是一个好主意。
7.3 在演示 3 中,您在下面的代码段中的 Fill()
方法 之前调用了 FillSchema()
。这有必要吗?
// Retrieve details from 'Employee' table
mySqlDataAdapter.FillSchema(localDataSet, SchemaType.Source, "Employee");
mySqlDataAdapter.Fill(localDataSet, "Employee");
这是一种更好的做法。为了包含现有表中的约束,我包含了这行代码。在这种情况下,您有两种选择:使用DataAdapter
的FillSchema
方法,或者在调用Fill
方法之前将DataAdapter
的MissingSchemaAction
属性设置为AddWithKey
。在我们的例子中,如果没有使用FillSchema()
,该行应该被注释如下。
//mySqlDataAdapter.FillSchema(localDataSet, SchemaType.Source, "Employee");
如果您再次运行该应用,您会在删除操作之前得到一个异常,该异常表明表没有主键。**
*如果你有兴趣了解这些方法,请访问 https://docs.microsoft.com/en-us/dotnet/framework/data/adonet/adding-existing-constraints-to-a-dataset
。
7.4 什么是存储过程?
如果要重复一系列任务,请创建一个存储过程。它是存储在数据库中的一个子程序。在我们的例子中,一个存储过程可以有一个名称、参数和 SQL 语句;它非常类似于 C# 中的一个方法。创建存储过程的步骤因数据库而异。在本章中,演示 4 向您展示了三个存储过程来满足我们的需求。
7.5 存储过程类似于函数。这是正确的吗?
存储过程和函数有一些显著的不同。例如,在 MySQL 中,一个存储过程可以返回一个或多个值,或者根本不返回值;而函数总是返回单个值。
7.6 如何创建一个函数?一个简单的演示可以帮助我。
这是一个 SQL 查询,用于创建一个名为 AddNumbers 的函数:
mysql> create function AddNumbers(firstNumber double,secondNumber double) returns double deterministic return firstNumber+secondNumber;
查询正常,0 行受影响(0.45 秒)
以下查询确认了函数的详细信息:
mysql> Select Routine_name as "Function Name", routine_Definition as "Definition", Routine_Schema "Schema", Data_Type as "Types", Created From Information_Schema.Routines Where Routine_Name="AddNumbers" and Routine_Type= 'FUNCTION';
Here is an output:
+-----------------+-------------------------+---------+--------+--------------------+
| Function Name | Definition | Schema | Types | CREATED |
+-----------------+---------------------------+---------+--------+---------------------+
| AddNumbers | return firstNumber+ secondNumber | test | double | 2020-03-17 10:13:20 |
+-----------------+--------------------------+---------+--------+--------------------+
1 row in set (0.00 sec)
图 7-19 为紧凑视图。
图 7-19
一旦在 MySQL 中创建了名为 AddNumbers 的函数,就可以从 MySQL 命令提示符中截取屏幕截图。
或者,您可以使用以下查询查看当前数据库中的所有函数。
mysql> show function status where db="test";
现在让我们执行函数。函数可以通过多种方式调用。下面是一个带有输出的示例查询。
mysql> select AddNumbers(25,45) as "Total";
+-------+
| Total |
+-------+
| 70 |
+-------+
1 row in set (0.00 sec)
7.7 我在一些地方的 SQL 上下文中看到了术语 DDL。这是什么意思?
SQL 命令通常分为以下几类。
-
DDL(数据定义语言)语句创建或修改数据库对象的结构。在这个上下文中使用 create、alter、drop 和 truncate 语句。
-
DML(数据操作语言)语句检索、插入、更新或删除数据库中的记录。例如,在这个上下文中使用 insert、update、delete 和 select 语句。一些工程师喜欢将 select 语句放在一个单独的类别中,称为 DQL(数据查询语言)。
-
DCL(数据控制语言)语句创建各种角色和权限来控制对数据库的访问。例如,您可以在此上下文中使用 grant 和 revoke 语句。
-
TCL(事务控制语言)语句管理数据库中发生的不同事务。例如,您可以在这个上下文中使用
commit
和rollback
语句。
7.8 ADO.NET 与 ADO 有何不同?
请参考《浅谈 ADO》中的注释。NET”部分。
7.8 什么是分隔符?为什么用?
你需要在 MySQL 中使用一个分隔符来处理一组语句(函数、存储过程等。)作为一个完整的语句。默认情况下,使用分隔符;
来分隔两个语句。但是当你需要把多个语句作为一个整体来处理的时候,你临时设置自己的分隔符,然后重新设置为默认的;
。当您创建自己的存储过程时,您可以选择不同的分隔符,如//
或$
。
7.9 您使用了术语 主键 和 外键 。你什么意思?
主键(更准确地说,是候选键)是用于正确识别记录的单个字段或字段组合。例如,在一个组织中,两个雇员可以有相同的名字,但是他们的雇员 id 不同。因此,您应该选择 id 作为主键,而不是选择名称。需要注意的是,主键不仅定义唯一性,而且不能为空。例如,雇员必须有一个标识号(ID)。
理论上,在一个表中可以有几个唯一标识一条记录的键。这些被称为候选键。在这些候选键中,你只选择一个作为主键,剩下的键叫做备用键。例如,假设您有一个名为StudentTable
的表来维护不同的学生记录。在这个表中,我们假设有像StudentName, StudentAddress, Department
这样的列,以此类推。现在您可能会发现,RollNumber
和StudentName, StudentAddress
的组合都可以唯一地标识一条记录。所以,如果你选择RollNumber
作为主键,另一个(StudentName, StudentAddress)
,就是备用键。当你有多个列作为一个键时,这些键也被称为组合键。例如,(StudentName,StudentAddress)就是组合键的一个例子。
外键用于定义两个表之间的关系。外键是作为另一个表的主键的列。包含外键的表通常被称为子表,包含候选键的表通常被称为引用表或父表。在这种情况下,MySQL 社区说:“外键关系涉及一个保存初始列值的父表,以及一个列值引用父列值的子表。在子表上定义了外键约束。(参见 https://dev.mysql.com/doc/refman/5.6/en/create-table-foreign-keys.html
)。
连接池
让我们以一个关于连接池的简短讨论来结束这一章。在本章中,数据库存储在本地。您可能看不到连接数据库所用时间的影响,但是许多实际应用都要花大量时间来连接数据库(无论是否使用。NET,Java 等。).
如果有许多连接快速发生(例如,一个 web 应用),初始化(或打开)连接然后关闭连接会降低应用的性能。为了克服这个问题,你可以使用连接池。连接池特定于给定的连接。为了启用它们,在许多应用中,您会在连接字符串中看到“Pooling=true”。(对于MySqlConnection
,默认情况下启用池)。
当连接池被启用时,提供者提供一个“连接的和开放的”连接池,无论谁请求一个连接,它都会立即被提供给谁。在这种情况下,在调用了Close()
方法之后,连接似乎是打开的,这是一种预期的行为。MySQL 社区在 https://dev.mysql.com/doc/connector-net/en/connector-net-connections-pooling.html
说了下面的话。
MySQL Connector/NET 支持连接池,以提高数据库密集型应用的性能和可伸缩性。默认情况下,这是启用的。您可以使用连接字符串选项池、连接重置、连接生存期、缓存服务器属性、最大池大小和最小池大小来关闭它或调整其性能特征。
连接池的工作原理是在客户端释放 MySqlConnection 时保持与服务器的本地连接。随后,如果打开一个新的 MySqlConnection 对象,它将从连接池中创建,而不是创建一个新的本机连接。这提高了性能。
最后,Max Pool Size
和Min Pool Size
的出现暗示了(连接的)池大小可能因应用而异。
最后的话
这些是数据库编程的基础。
这不仅是本章的结尾,也是本书的结尾。恭喜你,你已经完成了这本书!我相信你将来会发现这本书很有用。现在,您已经为 C# 的高级编程做好了准备,希望您可以进一步了解 C# 中即将出现的新概念。
直到我们再次见面,享受和快乐编码!祝你一切顺利。
摘要
本章讨论了以下关键问题。
-
什么是数据库?
-
什么是数据库管理系统?什么是不同类型的数据库管理系统?
-
RDBMS 中有哪些常用术语?
-
什么是 SQL?
-
什么是 ADO.NET?它与经典的 ADO 有何不同?
-
你如何连接 MySQL?
-
C# 应用如何与数据库对话?
-
如何使用 C# 应用实现连接层?
-
如何使用 C# 应用实现断开连接的层?
-
如何在程序中使用
MySqlConnection
、MySqlCommand
、MySqlDataReader
、MySqlDataAdapter
、MySqlCommandBuilder
? -
什么是存储过程?它与存储函数有什么不同?
-
你如何在你的程序中使用存储过程?*
第一部分:熟悉积木
Getting Familiar with Building Blocks
-
第一章:代表们
-
第二章:事件
-
第三章:Lambda 表达式
第二部分:探索高级编程
Exploring Advanced Programming
-
第四章:泛型编程
-
第五章:线程编程
-
第六章:异步编程
-
第七章:数据库编程