探索 C# 高级特性(一)
一、受关注的 C# 7
C# 7 于 2017 年 3 月发布,是 Visual Studio 2017 发布的一部分。如上所述。NET Blog 中,C# 7 专注于数据消费、简化代码和提高性能。C# 7 最大的特点是元组和模式匹配。
使用元组,开发人员可以从函数中返回多个值。传统上,C# 允许开发人员通过构建一个结构并返回该结构的一个实例,从一个函数返回多个值。
您还可以使用 out 参数,这些参数对函数返回的每个值使用 out 关键字。在 C# 7 中,元组提供了从函数中返回多个值的额外方法。
第二大特性是模式匹配,它可以测试一个值是否具有某种形状,然后对该数据做一些事情。在这一章中,我们将会看到这些概念以及更多。你可以从这一章中得到什么:
-
元组入门
-
模式匹配和解构
-
使用变量
-
使用本地函数
-
通用异步返回类型
-
抛出表达式
-
丢弃
C# 7 为开发人员提供了如此多的东西,绝对值得您花些时间来更好地了解这种新语言的特性。拿起一杯咖啡(如果你还没有的话),让我们开始探索 C# 7 的旅程。
请注意,本书中的代码和截图我都使用了 Visual Studio Enterprise 2019 预览版。你可以从 https://visualstudio.microsoft.com 下载一份。或者,您可以继续使用 Visual Studio 2017,但请注意,您将无法运行 C# 8.0 章节中的任何代码示例。
元组入门
到底是什么让元组如此伟大?如你所知,从一个函数返回多个值是你在 C# 中已经可以做到的。元组只是给了你另一种方法来做到这一点。
创建一个名为TupleExample的类。您的 Visual Studio 项目可能如图 1-1 所示。
图 1-1。
Visual Studio 解决方案
接下来,在名为GetGuitarType的类中添加一个元组返回函数。在其最简单的形式中,元组返回函数如下所示。
public (string, int) GetGuitarType()
{
return ("Les Paul Studio", 6);
}
Listing 1-1Tuple-returning function
这个函数所做的就是向调用代码返回一个 tuple,它的吉他类型是一个字符串,字符串的数量是一个整数。因为这段代码在一个类中,您可以简单地如下调用它。
TupleExample te = new TupleExample();
var guitarResult = te.GetGuitarType();
Debug.WriteLine(guitarResult.Item1);
Debug.WriteLine(guitarResult.Item2);
Listing 1-2Calling tuple-returning function
因为我使用 Windows 窗体项目来演示元组的使用,所以我只是通过使用Debug.WriteLine将元组的结果写出到 Visual Studio 中的输出窗口。你可以用你喜欢的任何方式做这件事。
如果查看输出窗口,您会注意到显示了从函数返回的值。
图 1-2。
返回元组的输出
返回元组最简单的方法是使用一个隐式变量,这个变量是用关键字var声明的。不过需要注意的是guitarResult变量中项目 1 和项目 2 的使用。您将看到,默认情况下,元组中返回的值被赋予了位置名称(Item1、Item2、Item3 等。)取决于您要返回多少个值。
您会注意到,当您在 guitarResult 变量上点号时,Intellisense 会带回元组值的位置名称。
图 1-3。
元组变量的位置名称
更改元组值的默认位置名称
您可能想知道是否有可能更改元组值的默认位置名称。幸运的是,答案是响亮的是。可以将新的默认成员名作为元组函数的返回类型声明的一部分。
首先修改前面创建的 tuple 函数,并包含成员的逻辑名称,如下所示。
public (string GuitarType, int StringCount) GetGuitarType()
{
return ("Les Paul Studio", 6);
}
Listing 1-3Adding member names to return type declaration
对于字符串返回类型,我指定它应该由成员名 GuitarType 来标识。对于整数返回类型,它将被标识为 StringCount。
这一次,如果您点击guitarResult变量,您将会注意到 Item1 和 Item2 位置名已经被我们在返回类型声明中定义的成员名所取代。
图 1-4。
成员名称取代位置名称
您仍然可以使用 Item1 、 Item2 等来引用元组值。这仍然有效,但是现在您可以显式地引用成员名,如下所示。
TupleExample te = new TupleExample();
var guitarResult = te.GetGuitarType();
Debug.WriteLine(guitarResult.GuitarType);
Debug.WriteLine(guitarResult.StringCount);
Listing 1-4Reference member names for tuple values
这使得引用 tuple 函数返回的值更加容易,并且消除了使用位置名可能导致的任何混淆(和可能的错误)。
在返回数据中创建本地元组变量
您可能会猜测,通过将元组成员名称作为默认成员名称,您也能够为它们定义本地相关的名称。这是百分之百正确的。让我澄清一下先前的说法。
您为元组值指定的成员名称只是建议的名称。也就是说,吉他类型和琴弦计数名称只是建议名称。当您处理返回值时,您可以指定本地相关的成员名称。这意味着如果我不想调用成员吉他类型和弦计数,那么我可以改变它。
通过将var guitarResult更改为(string BrandType, int GuitarStringCount) guitarResult,您可以覆盖元组返回类型声明中声明的建议默认成员名称。
当您点击guitarResult变量时,您会看到成员名称已经相应地改变了。
图 1-5。
元组值的本地成员名
这意味着我们的调用代码需要修改以引用本地相关的成员名,如下所示。
TupleExample te = new TupleExample();
(string BrandType, int GuitarStringCount) guitarResult = te.GetGuitarType();
Debug.WriteLine(guitarResult.BrandType);
Debug.WriteLine(guitarResult.GuitarStringCount);
Listing 1-5Local tuple variables
您不必绑定到 tuple 函数的返回类型声明中定义的默认成员名。创建自己的本地声明的名称在处理元组时会给你更多的灵活性。
作为离散变量的元组成员
C# 7 允许你使用元组成员作为离散变量。您将看到代码非常类似于创建本地元组变量。这里唯一的区别是省略了guitarResult变量。您会记得,我们的代码通过执行以下操作将函数返回的元组赋给了guitarResult变量。
(string BrandType, int GuitarStringCount) guitarResult = te.GetGuitarType();
Listing 1-6Returning local tuple variables
对于离散变量,我们可以简单地删除guitarResult变量来生成下面的代码。
(string BrandType, int GuitarStringCount) = te.GetGuitarType();
Listing 1-7Discrete tuple variables
将所有代码放在一起,您将会看到现在可以单独使用BrandType和GuitarStringCount。
TupleExample te = new TupleExample();
(string BrandType, int GuitarStringCount) = te.GetGuitarType();
Debug.WriteLine(BrandType);
Debug.WriteLine(GuitarStringCount);
Listing 1-8Using the discrete tuple variables
在 C# 中,我们称之为解构。您也不需要在括号中显式声明每个字段的类型。您可以使用var关键字为每个字段声明隐式类型变量。
TupleExample te = new TupleExample();
var (BrandType, GuitarStringCount) = te.GetGuitarType();
Debug.WriteLine(BrandType);
Debug.WriteLine(GuitarStringCount);
Listing 1-9Implicitly typed variables using var
在清单 1-9 中,var 关键字在括号之外。您还可以将 var 关键字与括号中声明的任何或所有变量混合使用。考虑下面的代码示例。
TupleExample te = new TupleExample();
(string BrandType, var GuitarStringCount) = te.GetGuitarType();
Debug.WriteLine(BrandType);
Debug.WriteLine(GuitarStringCount);
Listing 1-10Using var with some of the variables
如果你认为离散变量很有趣,你应该看看元组变量的实例。接下来看看怎么做。
元组变量的实例
C# 7 允许你使用元组作为实例变量。这意味着您可以将变量声明为元组。为了说明这一点,首先创建一个名为PlayInstrument的方法,它接受一个元组作为参数。所有这些只是输出一行文本。
private void PlayInstrument((string, int) instrumentToPlay)
{
Debug.WriteLine($"I am playing a {instrumentToPlay.Item1} with {instrumentToPlay.Item2} strings");
}
Listing 1-11The PlayInstrument method
您需要创建一个名为InstrumentType的enum,它有几个乐器。枚举只是简单的public enum InstrumentType { guitar, cello, violin },用在你的类文件的顶部。然后,您可以在下面的代码中使用enum以及元组变量的实例。
string instrumentType = nameof(InstrumentType.guitar);
int strings = 12;
(string TypeOfInstrument, int NumberOfStrings) instrument = (instrumentType, strings);
PlayInstrument(instrument);
Listing 1-12Using tuples as instance variables
您会注意到,我将名为instrument的元组变量的实例传递给了PlayInstrument方法。在PlayInstrument方法中,我通过使用元组值的位置名来引用元组值。我也可以编写如下的PlayInstrument方法。
private void PlayInstrument((string instrument, int strings) instrumentToPlay)
{
Debug.WriteLine($"I am playing a {instrumentToPlay.instrument} with {instrumentToPlay.strings} strings");
}
Listing 1-13PlayInstrument method using custom member names
这是引用元组值的更自然的方式。
比较元组
还可以比较元组成员。为了说明这一点,让我们停留在乐器上,比较一下吉他和小提琴的弦数。
首先,使用您之前创建的 enum 并创建以下 tuple 类型变量。
string instrumentType1 = nameof(InstrumentType.guitar);
int stringsCount1 = 6;
(string TypeOfInstrument, int NumberOfStrings) instrument1 = (instrumentType1, stringsCount1);
string instrumentType2 = nameof(InstrumentType.violin);
int stringsCount2 = 4;
(string TypeOfInstrument, int NumberOfStrings) instrument2 = (instrumentType2, stringsCount2);
Listing 1-14Creating tuple type variables
小提琴和吉他的弦数不同。吉他有六个,而小提琴只有四个。检查计数的相等性就像使用一个if语句一样简单。
if (instrument1.NumberOfStrings != instrument2.NumberOfStrings)
{
Debug.WriteLine($"A {instrument2.TypeOfInstrument} does not have the same number of strings as a {instrument1.TypeOfInstrument}");
}
Listing 1-15Comparing tuple members
还可以将整个元组变量相互比较。在 7.3 版本之前,检查元组相等性需要使用Equals方法。
if (!instrument1.Equals(instrument2))
{
Debug.WriteLine("We are dealing with different instruments here.");
}
Listing 1-16Comparing tuples before C# 7.3
如果您尝试对 tuple 类型使用==或!=,您会看到一个错误。
图 1-6。
C# 7.0 中的元组相等错误
要使用==或!=测试元组相等,您需要 C# 7.3 或更高版本。要使用此版本的 C#,您需要执行以下操作:
-
右键点击项目,点击属性。
-
在构建选项卡上,点击高级按钮。
-
在高级构建设置中,将语言版本设置为最新的次要版本。
这足以选择 C# 7.3(在我们的例子中)在项目中使用。
图 1-7。
选择 C# 语言版本
请注意,C# 8.0(测试版)在此列表中可用。这是因为我用的是 Visual Studio 2019 预览版。如果你用的是 Visual Studio 2017,就看不到 C# 8.0 了。
在你选择了你的 C# 语言版本之后,回到你的代码,看看我们之前看到错误的那一行。错误已经消失了。就我个人而言,我不太喜欢在 Equals 方法中使用!。这在某种程度上模糊了我的可读性。
对我来说,if (instrument1 != instrument2)比if (!instrument1.Equals(instrument2))读起来更自然。
推断元组元素名称
从 C# 7.1 开始,对 C# 语言做了一个小的改进,以推断元组元素名称。考虑下面的代码块。
string instrumentType = nameof(InstrumentType.guitar);
int stringsCount = 6;
var instrument = (instrumentType, stringsCount);
Listing 1-17Inferring tuple element names
当我点击instrument变量时,智能感知向我显示从用于初始化元组的变量中推断出的成员名称。
图 1-8。
推断的成员名称
从 7.1 版本开始,这是对 C# 7 的一个受欢迎的增强。
解构元组的方法
术语tuple destruction简单地说就是取出一个 tuple 中的所有条目,并在一次操作中将其拆分出来。事实上,本节中的代码清单已经做到了这一点。
您会经常听到这个术语,因为它指的是在处理元组时自然完成的事情。下图说明了元组解构发生的方式。
图 1-9。
解构元组
如您所见,本质上只有四种方法来执行元组解构。
其实只有三种方式,但是我统计了两种方式把推断作为一种单独的解构方式。
这些解构的方法是
-
显式声明每个字段的类型
-
用单个 var 关键字推断每个变量的类型
-
通过将 var 关键字与任何或所有变量声明混合来推断变量的类型
-
声明变量并将元组解构为先前声明的变量
对我来说,使用单个var关键字可能是解构元组最有效的方式。其他方法对我来说有点啰嗦。我想这完全取决于个人喜好。
无论您使用哪种方法来解构一个元组,我可以在单个解构操作中做到这一点的事实确实是一个受欢迎的特性。
关于元组的最后思考
元组在您的日常编码实践中肯定有一席之地。经常使用它们将有助于更好地理解它们。请注意,元组可以不只有我在代码示例中使用的两个成员。创建一个拥有如此多成员的元组可能并不是一个好主意,因为这样会使它变得难以管理和使用。
在 C# 中,Tuple.Create最多允许八项。实际上,这通常就足够了。但是,如果您发现自己在创建具有大量成员的元组,那么您可能需要考虑使用一个类或一个结构。令人难以置信的是,一些音乐家可以在几件弦乐器上取得成就。开发者用元组能达到的效果更是不可思议。
模式匹配
在 C# 7 中,我们现在有能力使用模式匹配。通过使用模式,我们可以测试一个值是否有某个形状,如果有,就使用匹配形状的信息。
事实上,当您使用if和switch语句测试值时,您已经在使用模式匹配算法了。如果语句匹配,则获取匹配的值并提取其信息。
在 C# 7 中,你可以使用新的语法元素来扩展你已经熟悉的is和switch语句。让我们首先创建一个名为PatternMatchingExample的新类,并将我们的代码添加到这个类中。
图 1-10。
PatternMatchingExample 示例类
我在 PatternMatchingExample 类中创建了以下枚举。
public enum UniversityCourses { Maths, Chemistry, Anatomy, LifeSkills }
public enum UniversityDegree { BA, BSc }
Listing 1-18
Class enums
我不打算详细讨论这个例子中使用的每个类。您可以下载本书的源代码,并根据需要使用示例。
现在,假设我们有以下对象:
-
人员类别
-
学生类(继承自 Person 类)
-
讲师类(继承自 Person 类)
-
校友类(继承自 Person 类)
-
ExchangeStudent 结构
这些类都是相似的,只是有一点点不同,我将在这里简要强调一下。我们还有一个用于ExchangeStudent的结构。
严格来说,Lecturer和Alumnus应该继承自Student而不是Person class,但我不想把事情复杂化。毕竟,这一章不是在讨论继承。
如前所述,Student类、Lecturer类和Alumnus类都继承自 Person 类。Person 类有以下代码。
public class Person
{
public string FirstName { get; set; }
public string LastName { get; set; }
public int Age { get; set; }
}
Listing 1-19
Person class code
Student类具有学生注册的课程的属性。它还从StudentDetails方法中返回唯一的值。学生类有以下定义。
public class Student : Person
{
public int StudentNumber { get; }
public UniversityCourses CourseEnrolledFor { get; }
public Student((string firstname, string lastname, int age) personDetails, int studentNumber, UniversityCourses courseEnrolled)
{
FirstName = personDetails.firstname;
LastName = personDetails.lastname;
Age = personDetails.age;
StudentNumber = studentNumber;
CourseEnrolledFor = courseEnrolled;
}
public (string fullName, int studentNum, string studentCourse) StudentDetails()
{
var studentDetails = ($"{FirstName} {LastName}", StudentNumber, CourseEnrolledFor.ToString());
return studentDetails;
}
}
Listing 1-20
Student class code
其他类从返回特定对象细节的方法返回不同的属性。例如,Lecturer类包含讲师教授的课程专门化的属性。然而,它的 details 方法计算并返回讲师被雇用的天数。这是Lecturer类的代码。
public class Lecturer : Person
{
public int EmployeeNumber { get; }
public string CourseSpecialization { get; }
public DateTime DateEmployed { get; }
public Lecturer((string firstname, string lastname, int age) personDetails, int employeeNumber, UniversityCourses courseSpecialization, DateTime dateEmployed)
{
FirstName = personDetails.firstname;
LastName = personDetails.lastname;
Age = personDetails.age;
EmployeeNumber = employeeNumber;
CourseSpecialization = courseSpecialization.ToString();
DateEmployed = dateEmployed;
}
public (string fullName, int employeeNum, string courseSpecial, int totalDayesEmployed) LecturerDetails()
{
double lengthOfServiceInDays = DateTime.Now.Subtract(DateEmployed).TotalDays;
var lecturerDetails = ($"{FirstName} {LastName}", EmployeeNumber, CourseSpecialization, Convert.ToInt32(lengthOfServiceInDays));
return lecturerDetails;
}
}
Listing 1-21
Lecturer class code
Alumnus已经完成了他们的学位,所以Alumnus类包含了他们获得的学位和他们完成学位的年份的属性。Alumnus类如下所示。
public class Alumnus : Person
{
public int YearCompleted { get; }
public UniversityDegree DegreeObtained { get; }
public Alumnus((string firstname, string lastname, int age) personDetails, int yearStudiesCompleted, UniversityDegree degreeObtained)
{
FirstName = personDetails.firstname;
LastName = personDetails.lastname;
Age = personDetails.age;
YearCompleted = yearStudiesCompleted;
DegreeObtained = degreeObtained;
}
public (string fullName, int yearCompleted, string degreeObtained) AlumnusDetails()
{
var alumnusDetails = ($"{FirstName} {LastName}", YearCompleted, DegreeObtained.ToString());
return alumnusDetails;
}
}
Listing 1-22
Alumnus class code
最后,ExchangeStudent是一个结构,包含他们参加的短期课程和学生签证剩余天数的属性。ExchangeStudent结构如下所示。
public struct ExchangeStudent
{
public string FirstName { get; }
public string LastName { get; }
public string ShortCourse { get; }
public DateTime VisaExpiryDate { get; }
public ExchangeStudent((string firstname, string lastname, int age) personDetails, UniversityCourses shortCourse, DateTime studentVisaExpiryDate)
{
FirstName = personDetails.firstname;
LastName = personDetails.lastname;
ShortCourse = shortCourse.ToString();
VisaExpiryDate = studentVisaExpiryDate;
}
public (string fullName, string shortCourse, int daysLeftOnVisa) ExchangeStudentDetails()
{
double lenOfVisa = VisaExpiryDate.Subtract(DateTime.Now).TotalDays;
var exchangeDetails = ($"{FirstName} {LastName}", ShortCourse, Convert.ToInt32(lenOfVisa));
return exchangeDetails;
}
}
Listing 1-23
ExchangeStudent struct code
如果我们有一个特定的对象,我们希望获得该对象的正确细节。您会注意到,我们从元组中的每个类返回信息。
我们的类的设计在这里并不重要。重要的是我们确定其形状的方式,然后在此基础上,提取数据进行处理。现在,我们将了解模式匹配如何作用于这些对象。
使用 Is 类型模式表达式
在 C# 7 之前,你必须使用一系列的if和is语句来测试对象的类型。这是一个典型的类型模式,你正在测试一个变量以确定它是什么类型。
根据变量的类型,您可以执行不同的操作。此类代码的示例可能如下所示。
// Before C# 7
if (someperson is Student)
{
var student = (Student)someperson;
return $"{student.StudentDetails().fullName} is enrolled for {student.StudentDetails().studentCourse} with student number {student.StudentDetails().studentNum}";
}
else if (someperson is Lecturer)
{
var lecturer = (Lecturer)someperson;
return $"{lecturer.LecturerDetails().fullName} teaches {lecturer.LecturerDetails().courseSpecial}";
}
else if (someperson is Alumnus)
{
var alumnus = (Alumnus)someperson;
return $"{alumnus.AlumnusDetails().fullName} has completed {alumnus.AlumnusDetails().degreeObtained} in {alumnus.AlumnusDetails().yearCompleted}";
}
Listing 1-24Pre-C# 7 type testing
快进到 C# 7,我们有一个更简单、更简洁的方法来做这件事。在下面的代码中,我们使用扩展的is表达式,如果测试成功,它将分配一个变量。代码如下所示。
// The is type pattern
if (someperson is Student student)
{
return $"{student.StudentDetails().fullName} is enrolled for {student.StudentDetails().studentCourse} with student number {student.StudentDetails().studentNum}";
}
else if (someperson is Lecturer lecturer)
{
return $"{lecturer.LecturerDetails().fullName} teaches {lecturer.LecturerDetails().courseSpecial}";
}
else if (someperson is Alumnus alumnus)
{
return $"{alumnus.AlumnusDetails().fullName} has completed {alumnus.AlumnusDetails().degreeObtained} in {alumnus.AlumnusDetails().yearCompleted}";
}
else if (someperson is ExchangeStudent exchStudent)
{
return $"{exchStudent.ExchangeStudentDetails().fullName} has {exchStudent.ExchangeStudentDetails().daysLeftOnVisa} days left on Student Visa";
}
Listing 1-25The is type pattern expression
我们现在有了一个使用is表达式的快捷方式。这是因为它做了两件事。它测试这个变量,并把它赋给一个新的变量。还要注意,我包含了一个结构类型ExchangeStudent。这意味着新的is表达式可以很好地处理值类型(结构)和引用类型(类)。
关于结构和类的补充说明:当创建一个结构时,分配给该结构的变量保存该结构的实际数据。当它被赋给一个新变量时,它被复制,这给了新变量一个单独的内存空间。原始变量和新变量现在包含相同数据的两个独立副本。这就是我们所说的值类型。
类是一种引用类型。引用类型包含指向保存数据的另一个内存位置的指针。
扩展的is表达式使得代码更短,可读性更好。需要注意的另一点是在每个is表达式后新创建的变量。这些只有在模式匹配表达式返回 true 结果时才在范围内分配。
使用开关模式匹配语句
在上一节中,我们看了一下is模式匹配表达式。它需要对您需要检查的每种类型使用if语句。这可能有点麻烦,因为它也只测试输入是否匹配单一类型。这就是switch表达派上用场的地方。
传统的switch语句只支持常量模式。它也只支持数字类型和string类型。在 C# 7 中,你现在可以使用类型模式。这意味着我们可以做以下事情。
// Using switch statements pattern matching
switch (someperson)
{
case Student student:
return $"{student.StudentDetails().fullName} is enrolled for {student.StudentDetails().studentCourse} with student number {student.StudentDetails().studentNum}";
case Lecturer lecturer:
return $"{lecturer.LecturerDetails().fullName} teaches {lecturer.LecturerDetails().courseSpecial}";
case Alumnus alumnus:
return $"{alumnus.AlumnusDetails().fullName} has completed {alumnus.AlumnusDetails().degreeObtained} in {alumnus.AlumnusDetails().yearCompleted}";
case ExchangeStudent exchangeStudent:
return $"{exchangeStudent.ExchangeStudentDetails().fullName} has {exchangeStudent.ExchangeStudentDetails().daysLeftOnVisa} days left on Student Visa";
}
Listing 1-26Switch pattern matching statements
每当 case 语句的计算结果为 true 时,就会运行它下面的代码。在 C# 7 中,变量类型的限制已经从switch表达式中删除,任何类型都可以使用。
在 Case 表达式中使用 When 子句
我们可以通过在case标签上使用when条款来满足特殊情况。让我们假设我们也想确定资深校友。这些人将在 1976 年之前完成他们的课程。
因此,我们可以在 case 标签上使用 when 子句来检查这种情况。然后考虑下面的代码清单。
// Using switch statements pattern matching
switch (someperson)
{
case Student student:
return $"{student.StudentDetails().fullName} is enrolled for {student.StudentDetails().studentCourse} with student number {student.StudentDetails().studentNum}";
case Lecturer lecturer:
return $"{lecturer.LecturerDetails().fullName} teaches {lecturer.LecturerDetails().courseSpecial}";
case Alumnus alumnus when alumnus.YearCompleted <= 1975: // Note the when keyword here
return $"{alumnus.AlumnusDetails().fullName} is a senior Alumnus";
case Alumnus alumnus:
return $"{alumnus.AlumnusDetails().fullName} has completed {alumnus.AlumnusDetails().degreeObtained} in {alumnus.AlumnusDetails().yearCompleted}";
case ExchangeStudent exchangeStudent:
return $"{exchangeStudent.ExchangeStudentDetails().fullName} has {exchangeStudent.ExchangeStudentDetails().daysLeftOnVisa} days left on Student Visa";
}
Listing 1-27Using a when clause
如果YearCompleted的值是<= 1975,,我们返回一个稍微不同的消息给调用代码。
如果代码有点难以理解,可以考虑下载本书的源代码,并在 Visual Studio 中学习。
另一个值得注意的有趣的事情是,多个case标签可以被组合在一个switch部分下。考虑下面的代码。
// Using multiple case labels in switch statements
switch (someperson)
{
case Student student when student.CourseEnrolledFor == UniversityCourses.Chemistry:
case Alumnus alumnus when alumnus.DegreeObtained == UniversityDegree.BSc:
return "Chemistry and BSc excluded";
case Student student:
return $"{student.StudentDetails().fullName} is enrolled for {student.StudentDetails().studentCourse} with student number {student.StudentDetails().studentNum}";
case Lecturer lecturer:
return $"{lecturer.LecturerDetails().fullName} teaches {lecturer.LecturerDetails().courseSpecial}";
case Alumnus alumnus when alumnus.YearCompleted <= 1975:
return $"{alumnus.AlumnusDetails().fullName} is a senior Alumnus";
case Alumnus alumnus:
return $"{alumnus.AlumnusDetails().fullName} has completed {alumnus.AlumnusDetails().degreeObtained} in {alumnus.AlumnusDetails().yearCompleted}";
case ExchangeStudent exchangeStudent:
return $"{exchangeStudent.ExchangeStudentDetails().fullName} has {exchangeStudent.ExchangeStudentDetails().daysLeftOnVisa} days left on Student Visa";
}
Listing 1-28
Multiple case labels
在这里,您可以看到我们想要排除化学系学生和理学学士校友。根据注册的课程或获得的学位排除这些对象类型的例子是相当愚蠢的(即,可能不是一个很好的现实世界的例子)。但是,它强调了 switch 语句的一个重要特性:
-
我可以将多个机箱标签应用到单个交换机部分。
-
每个部分的顺序很重要。
那么,当我说这些部分的顺序很重要时,我的意思是什么呢?我们将考虑在switch语句中添加代码case Student student作为第一个case的影响。这将导致学生的带有when子句的case永远不会被评估。
事实上,清单 1-28 中的代码已经将高级校友排除在外,因为根据获得的学位排除校友的第一个case标签将包括任何高级校友。因此,获得理学士学位的高年级校友将永远被排除在高年级校友评估之外switch。为了演示这一点,请考虑以下对象。
Alumnus alumnus = new Alumnus(("Gabby", "Salinger", 26), 2017, UniversityDegree.BSc);
Alumnus senalumnus = new Alumnus(("Frank", "Greer", 74), 1970, UniversityDegree.BSc);
Listing 1-29
Alumnus objects
运行代码并向其传递两个Alumnus类的实例将导致两个对象的Chemistry and BSc excluded输出。为了克服这个问题,我们可以添加条件逻辑 AND 运算符。
&&运算符也称为短路逻辑 AND 运算符。它计算bool操作数的逻辑与,如果&&的两端都计算为true,则运算结果为true。因此,如果第一个条件为假,表达式会立即短路。这意味着只有当第一个条件为真时,才会计算第二个条件。
为了说明这一点并允许高级校友仍然被评估,修改您的 switch 语句如下。
// Modified switch statement to cater for senior alumni
switch (someperson)
{
case Student student when student.CourseEnrolledFor == UniversityCourses.Chemistry:
case Alumnus alumnus when alumnus.DegreeObtained == UniversityDegree.BSc && alumnus.YearCompleted > 1975:
return "Chemistry and BSc excluded";
case Student student:
return $"{student.StudentDetails().fullName} is enrolled for {student.StudentDetails().studentCourse} with student number {student.StudentDetails().studentNum}";
case Lecturer lecturer:
return $"{lecturer.LecturerDetails().fullName} teaches {lecturer.LecturerDetails().courseSpecial}";
case Alumnus alumnus when alumnus.YearCompleted <= 1975:
return $"{alumnus.AlumnusDetails().fullName} is a senior Alumnus";
case Alumnus alumnus:
return $"{alumnus.AlumnusDetails().fullName} has completed {alumnus.AlumnusDetails().degreeObtained} in {alumnus.AlumnusDetails().yearCompleted}";
case ExchangeStudent exchangeStudent:
return $"{exchangeStudent.ExchangeStudentDetails().fullName} has {exchangeStudent.ExchangeStudentDetails().daysLeftOnVisa} days left on Student Visa";
}
Listing 1-30Modified switch statement to cater for senior alumni
我们所做的只是将&& alumnus.YearCompleted > 1975添加到校友case标签的when子句中。本质上,我是说,只有当校友获得理学士学位并且获得该学位的年份是在 1975 年之后,才必须排除Alumnus对象。
如果我在清单 1-29 中使用相同的Alumnus对象并运行我的代码,我会在输出窗口中看到不同的结果。
Chemistry and BSc excluded
Frank Greer is a senior Alumnus
Listing 1-31Output window results
当第一个Alumnus对象根据获得的学位被排除时,第二个对象通过case,因为不满足具有 1975 年之后获得的学位的条件。高年级校友因此仍被评估。
正如您将看到的,每个部分的顺序绝对很重要。一般的经验法则是将最具限制性的case标签放在switch语句的顶部,而将最通用的case标签放在最后。
检查 Switch 语句中的空值
我们可以通过添加一个null案例来检查null。这确保了传递给 switch 语句的参数不为空。考虑下面的代码。
// Cater for null
switch (someperson)
{
case Student student:
return $"{student.StudentDetails().fullName}";
case Lecturer lecturer:
return $"{lecturer.LecturerDetails().fullName}";
case Alumnus alumnus:
return $"{alumnus.AlumnusDetails().fullName}";
case ExchangeStudent exchangeStudent:
return $"{exchangeStudent.ExchangeStudentDetails().fullName}";
case null:
return $"{nameof(someperson)} cannot be null";
}
Listing 1-32
Null case
向这个switch语句传递一个null对象将导致对null案例进行评估,并返回消息someone 不能为 null 。
模式匹配是控制代码逻辑流程的一种非常好的方式。有些人认为这是句法上的甜言蜜语。无论你对模式匹配有什么想法,能够在 C# 7 中使用它肯定是很棒的。
使用变量
C# 中的out关键字已经有一段时间了。使用out通过引用传递参数。默认情况下,C# 中的所有参数都是通过值传递的,除非您显式地包含了一个out或ref修饰符。在过去,你必须声明一个变量作为out参数。
这在 C# 7 中已经改变了,你可以在你使用它的地方声明变量。假设我们想测试一个变量是否是一个有效的整数值。这是我们的代码在 C# 7 之前的样子。
string num = "123";
int numParsed;
if (int.TryParse(num, out numParsed))
{
Debug.WriteLine($"{num} is a valid integer");
}
else
{
Debug.WriteLine($"{num} is not a valid integer");
}
Listing 1-33Pre-C#7 code for out keyword
我们有一个叫做 numParsed 的整型变量,它就在附近。在 C# 7 中,我们现在可以做以下事情。
string num = "123";
if (int.TryParse(num, out int numParsed))
{
Debug.WriteLine($"{num} is a valid integer");
}
else
{
Debug.WriteLine($"{num} is not a valid integer");
}
Listing 1-34C# 7 code for out keyword
你看出区别了吗?眨眼,你可能会错过它。我们不再需要声明一个有趣的松散的固定变量,它在我们的TryParse检查之前一直存在。
对 C# 语言来说,这是一个微小但受欢迎的变化。另一点需要注意的是,编译器能够推断出numParsed变量的类型,这意味着我们也可以使用var关键字。
这只是意味着我们可以使用out var而不是使用out int,并获得相同的结果。考虑下面的代码清单。
string num = "123";
if (int.TryParse(num, out var numParsed))
{
Debug.WriteLine($"{num} is a valid integer");
}
else
{
Debug.WriteLine($"{num} is not a valid integer");
}
Listing 1-35Using var with out
然后,C# 7 中还有一个小的附加功能,可能会被一些开发人员忽略。那就是包含弃。现在讨论丢弃是有意义的,因为它在out参数的上下文中受到支持。
丢弃
在 C# 7 中,这种语言现在支持丢弃。请将这些视为临时的虚拟变量,不会在您的应用代码中使用。换句话说,你实际上并不关心赋值。使用丢弃与使用未赋值变量是一样的,因为变量本身不包含值。
这意味着丢弃变量甚至可能没有被分配存储空间,这反过来减少了内存分配。在 C# 7 的以下上下文中支持丢弃变量:
-
元组和对象解构
-
用
is和switch进行模式匹配 -
方法调用中使用的
out参数 -
范围内没有其他丢弃变量时的独立丢弃变量
为了表明一个变量是被丢弃的,你需要给它分配一个下划线字符作为它的变量名。以前面的 out 参数清单为例,我们可以做一点小小的改变,使用一个 discard 变量。考虑下面的代码清单。
string num = "123";
if (int.TryParse(num, out _))
{
Debug.WriteLine($"{num} is a valid integer");
}
else
{
Debug.WriteLine($"{num} is not a valid integer");
}
Listing 1-36Using discards with out parameters
我唯一改变的部分是我用int.TryParse(num, out _)代替了int.TryParse(num, out var numParsed)。这真的很好,完全取消了不必要的numParsed变量声明。
我将在本章的后面讨论弃牌,所以不要走开。接下来,我们将看看什么是局部函数,以及如何在 C# 7 中使用它们。
使用本地函数
局部函数是嵌套在另一个方法中的私有方法。局部函数的使用在函数式语言中相当普遍。这已经包含在 C# 7 中。
局部函数的使用实际上仅限于包含方法。这意味着只有包含方法可以调用本地函数。因此,局部函数的使用应该在包含成员的范围内有意义,并且实际上应该只在包含成员内有值。
出于这个原因,使用局部函数可以让读者更清楚地理解代码的意图。这是因为您将知道本地函数只能被包含成员调用,而不能被其他成员调用。可以从以下成员声明和调用局部函数:
-
方法、匿名方法和构造函数
-
属性访问器和事件访问器
-
λ表达式
-
终结器
-
其他本地功能
让我们来看一个局部函数的例子。在这个例子中,我将为不同的对象创建类。本地函数将被添加到我的类构造函数中,并将计算形状的体积。构造函数将负责确定对象描述。
首先,在项目中添加一个名为 LocalFunctionExample 的类。然后为这个类创建一个构造函数。我们将在这里添加所有的代码。
图 1-11。
LocalFunctionExample 类
继续为可以计算体积的对象创建类。我使用了以下对象:
-
立方
-
金字塔
-
范围
每个物体在形状上明显不同;因此,每个类都满足确定每个对象的体积所需的维度。下面是Cube类、Pyramid类和Sphere类的代码。
public class Cube
{
public double Edge { get; }
public Cube(double edgeLength)
{
Edge = edgeLength;
}
}
public class Pyramid
{
public double BaseLength { get; }
public double BaseWidth { get; }
public double Height { get; }
public Pyramid(double triangleBaseLength, double triangleBaseWidth, double triangleHeight)
{
BaseLength = triangleBaseLength;
BaseWidth = triangleBaseWidth;
Height = triangleHeight;
}
}
public class Sphere
{
public double Radius { get; }
public Sphere(double circleRadius)
{
Radius = circleRadius;
}
}
Listing 1-37The object classes’ code
接下来,您需要为 LocalFunctionExample 类创建一个构造函数,该构造函数将包含确定对象描述和计算体积的本地函数所需的逻辑。
要快速创建构造函数,键入 ctor 并按两次 Tab 键。Visual Studio 将自动为您插入构造函数。
考虑下面的LocalFunctionExample构造函数代码。
public class LocalFunctionExample
{
public double ObjectVolume { get; }
public string ObjectType { get; }
public LocalFunctionExample(object shapeObject)
{
double GetObjectVolume(object shape)
{
switch (shape)
{
case Cube square:
return Math.Pow(square.Edge, 3.00);
case Pyramid triangle:
return (triangle.BaseLength * triangle.BaseWidth * triangle.Height) / 3;
case Sphere sphere:
return 4 * Math.PI * Math.Pow(sphere.Radius, 3) / 3;
case null:
return 0.0;
}
return 0.0;
}
ObjectVolume = GetObjectVolume(shapeObject);
ObjectType = ObjectVolume == 0.0 ? "Invalid Object Shape" : shapeObject.GetType().Name;
}
}
Listing 1-38The LocalFunctionExample class
你会注意到,我添加了一个名为GetObjectVolume的本地函数,它获取传递给构造函数的对象,并使用模式匹配来确定我们正在处理的对象的类型。
如果任何未识别的形状被传递给局部函数,局部函数将返回一个体积0.0,这将导致三元条件表达式显示无效对象形状作为ObjectType值。
为了测试本地函数,添加下面的代码并将对象传递给你的LocalFunctionExample类。只需使用Debug.WriteLine来显示来自LocalFunctionExample类的输出。
Cube cube = new Cube(5);
Pyramid pyramid = new Pyramid(5, 5, 5);
Sphere sphere = new Sphere(5);
Student student = new Student(("john", "doe", 22), 12345, UniversityCourses.Anatomy);
Listing 1-39Testing the local function
这将导致在输出窗口中显示以下行。
This is a Cube with a volume of 125
This is a Pyramid with a volume of 41,6666666666667
This is a Sphere with a volume of 523,598775598299
This is a Invalid Object Shape with a volume of 0
Listing 1-40
Output
您可以看到,当我们将一个无法识别的对象传递给构造函数时,该类通过删除 switch 语句并将音量设置为 0.0 来处理它。
这里还有一些关于局部函数的注意事项:
-
在包含成员中定义的所有局部变量都可以从局部函数中访问。
-
所有方法参数都可以从本地函数中访问。
-
局部函数是私有的;因此它们不能包含访问修饰符。
-
对于局部函数,不能包含
static关键字。 -
不能将属性应用于局部函数或其参数。
当您想在整个方法中使用某些功能时,局部函数非常好,因为这些功能只适用于它的包含成员。您还会注意到,局部函数位于构造函数的顶部,引用它的代码(计算体积的代码)位于局部函数之后。
这个的位置不重要。您可以轻松地在本地函数代码之前调用ObjectVolume = GetObjectVolume(shapeObject);,并且仍然获得相同的输出。
通用异步返回类型
async/await 的功能被广泛用于避免性能瓶颈和提高应用的响应能力。尽管在某些情况下,从异步方法返回一个Task对象可能会引入性能问题,但还是有一个小问题。
当async方法返回缓存的结果或以同步方式完成时,这一点尤其明显。我们知道支持的返回类型是Task<T>、Task和void。在 C# 7 中,ValueTask类型已经被添加,以允许async方法返回除了我一分钟前提到的类型之外的其他类型。
这个特性最好用一个例子来说明。我将简单地使用一个控制台应用来说明ValueTask类型的用法。在我们开始编写代码之前,我们需要安装 NuGet 包System.Threading.Tasks.Extensions,这样我们就可以使用ValueTask<TResult>类型。
图 1-12。
NuGet 包管理器
一旦你安装了 NuGet 包,你就会看到这个系统。项目参考中列出了 Threading.Tasks.Extensions。
图 1-13。
控制台应用参考
现在我们可以开始写一些代码了。控制台应用是纳斯达克的一个虚拟股票报价器。对于股价,我显然将使用虚拟数据,但这应该说明使用ValueTask类型的性能收益。
该应用将循环 1 亿次,但只有在超过缓存期时才读取新的股票信息。首先创建一个保存股票信息的StockListing类。
public class StockListing
{
public string NASDAQTickerSymbol { get; }
public decimal Open { get; }
public decimal High { get; }
public decimal Low { get; }
public string MarketCap { get; }
public StockListing(string nasdaq, decimal open, decimal high, decimal low, string marketCap)
{
NASDAQTickerSymbol = nasdaq;
Open = open;
High = high;
Low = low;
MarketCap = marketCap;
}
}
Listing 1-41The StockListing class
下一个类将简单地使用Task<T>来返回股票查询的结果。该类包含一个名为GetShareDetails的本地函数,用于读取最新的共享信息。
然而,如果缓存时间没有过期,则返回缓存的股票列表。类代码如下所示。
public class ShareService
{
private readonly TimeSpan cacheTime = TimeSpan.FromSeconds(2);
private DateTime lastRun = DateTime.Now;
private IEnumerable<StockListing> cachedListings;
public async Task<IEnumerable<StockListing>> GetStockDetails()
{
async Task<IEnumerable<StockListing>> GetShareDetails()
{
cachedListings = await Task.Run(() => new List<StockListing>
{
new StockListing("AAPL", 157.50m, 158.52m, 154.55m, "741,37B")
,new StockListing("AMZN", 1473.35m, 1513.47m, 1449.00m, "722,71B")
,new StockListing("QCOM", 56.33m, 57.53m, 56.24m, "68,86B")
});
lastRun = DateTime.Now;
WriteLine($"Get share details - {lastRun}");
return cachedListings;
}
if (DateTime.Now - lastRun < cacheTime)
{
return cachedListings;
}
return await GetShareDetails();
}
}
Listing 1-42
ShareService class
在控制台应用中,我们以下列方式使用服务。
static void Main(string[] args)
{
var shareListing = new ShareService();
for (int i = 0; i < 100_000_000; i++)
{
var result = shareListing.GetStockDetails().Result;
}
WriteLine($"Garbage collection occurred {GC.CollectionCount(0)} times");
ReadLine();
}
Listing 1-43Calling the service from the console application
这只是返回结果,然后输出垃圾收集发生的次数。
请注意,我已经将using static System.Console添加到我的using语句中。这允许我在WriteLine和ReadLine方法之前删除Console。
现在运行应用会产生以下结果。
图 1-14。
任务诊断结果
从诊断工具中可以明显看出以下情况:
-
进程内存在 12MB 左右。
-
完成该过程所需的时间为 27,071 秒。
控制台应用屏幕的输出还报告垃圾收集在第 0 代中发生了 1833 次。让我们改进 ShareService 类中的代码,并利用ValueTask类型。
public class ShareService
{
private readonly TimeSpan cacheTime = TimeSpan.FromSeconds(2);
private DateTime lastRun = DateTime.Now;
private IEnumerable<StockListing> cachedListings;
public ValueTask<IEnumerable<StockListing>> GetStockDetails()
{
async Task<IEnumerable<StockListing>> GetShareDetails()
{
cachedListings = await Task.Run(() => new List<StockListing>
{
new StockListing("AAPL", 157.50m, 158.52m, 154.55m, "741,37B")
,new StockListing("AMZN", 1473.35m, 1513.47m, 1449.00m, "722,71B")
,new StockListing("QCOM", 56.33m, 57.53m, 56.24m, "68,86B")
});
lastRun = DateTime.Now;
WriteLine($"Get share details - {lastRun}");
return cachedListings;
}
if (DateTime.Now - lastRun < cacheTime)
{
return new ValueTask<IEnumerable<StockListing>>(cachedListings);
}
return new ValueTask<IEnumerable<StockListing>>(GetShareDetails());
}
}
Listing 1-44Improved ShareService class
你会注意到我已经用ValueTask<IEnumerable<StockListing>>替换了Task<IEnumerable<StockListing>>,并且我还删除了async关键字。去掉async关键字是有意义的,因为大多数时候结果会同步返回。使用改进的代码再次运行应用会产生以下改进的结果。
图 1-15。
值任务诊断结果
现在,从诊断工具中可以明显看出以下信息,并且肯定有所改进:
-
进程内存约为 9MB(低于 12MB)。
-
完成该过程所需的时间为 14,938 秒(低于上次运行的 27,071 秒)。
控制台应用屏幕的输出还报告垃圾回收在第 0 代中发生了 0 次。
ValueTask是值类型。这意味着通过返回缓存的股票列表,堆上不会发生分配。
那么,我为什么要使用任务?
异步方法的默认选择应该是返回一个Task或Task<T>。如果你想用ValueTask<T>来代替,你应该只考虑使用它,如果这样做可以提高性能的话。
抛出表达式
在 C# 7 之前,我们使用throw语句。不存在使用throw表达式的情况。这有点道理,因为使用throw作为表达式总是会导致异常。
不管不包含throw表达式的理由是什么,C# 的发展已经使得包含这个特性成为必要。在 C# 7 中,现在可以在有限的上下文中包含throw表达式。这些是
-
在一个表达式的主体中-主体成员
-
在 lambda 表达式的主体中
-
作为零合并的第二个操作数。?操作员
-
作为三元条件的第二个操作数?操作员
考虑下面的代码清单。
public class Square
{
public int Side { get; }
public string Description { get; }
public Square(int side, string description)
{
if (description == null)
{
throw new ArgumentNullException(nameof(description));
}
Side = side;
Description = description;
}
}
Listing 1-45Null check in constructor
Visual Studio 现在为我们提出了一个代码改进,因为我们可以在这里使用 throw 表达式来简化代码。
图 1-16。
Visual Studio 提出简化代码
单击灯泡将建议使用投掷表达式。因此,代码被重构为如下所示。
public class Square
{
public int Side { get; }
public string Description { get; }
public Square(int side, string description)
{
Side = side;
Description = description ?? throw new ArgumentNullException(nameof(description));
}
}
Listing 1-46
Null check extension method
随着扩展到 C# 7 中的构造函数的表达式主体成员的出现,当我们处理可以被改变为表达式主体定义的构造函数时,我们能够进一步简化代码。考虑这段代码。
public class Rectangle
{
public string Description { get; set; }
public Rectangle(string description)
{
if (description == null)
{
throw new ArgumentNullException(nameof(description));
}
Description = description;
}
}
Listing 1-47A simple constructor
因为我们可以将表达式主体成员应用于构造函数,并且因为 throw 表达式可用于表达式主体成员,所以我们可以将代码简化如下。
public class Rectangle
{
public string Description { get; set; }
public Rectangle(string description) => Description = description ?? throw new ArgumentNullException(nameof(description));
}
Listing 1-48
Expression-bodied constructor
我们的Rectangle类的构造函数已经减少到只有一行代码。Throw 表达式是 C# 的必要组成部分,因为它已经发展到我们今天所拥有的程度。使用 throw 表达式不仅会使你的代码更容易理解,还会减少你要写的代码量。
丢弃
正如我前面指出的,在讨论out参数时,C# 7 引入了丢弃。这是一个非常受欢迎的新语言。它允许你告诉编译器你不关心一个特定变量的值。因此,丢弃是在应用中根本不会用到的虚拟或临时变量。
因此,丢弃是未赋值的并且不包含值也是有意义的,这反过来减少了内存分配。为了表明一个变量被丢弃,你使用下划线_作为变量名。
请注意,int _ example 仍然是一个有效的变量名,因此不能在与 discard 相同的范围内使用。
在以下情况下支持丢弃:
-
元组
-
模式匹配
-
输出参数
-
当范围内没有其他
_时,独立为_
还要注意,当使用丢弃时,不能读取它的值,也不能在赋值中使用它。还记得我们前面提到过,丢弃变量根本没有赋值。让我们来看几个使用案例。
元组
在本章前面,我们已经了解了如何在 C# 7 中使用元组。我们了解到元组是从单个方法调用返回多个值的好方法。我们还看了一下本地函数。您会记得,有时代码的逻辑只与它的封闭方法相关。换句话说,将包含在局部函数中的代码放在独立的公共方法中是没有意义的。
现在让我们来看一个使用场景,在这个场景中,我们结合了 C# 7 的这两个特性,然后通过使用丢弃来增强它。该代码示例是一个局部函数,它检查给定值是否大于零且小于 20。然后它被标记为在范围内。考虑下面的代码。
private void UsingDiscards()
{
// Local function
(bool zeroCheck, bool maxCheck, bool inRangeCheck) DoSomething(int value)
{
bool blnAboveZero = false;
bool blnBelowTwenty = false;
bool blnInRange = false;
if (value > 0)
blnAboveZero = true;
if (value <= 20)
blnBelowTwenty = true;
if (blnAboveZero && blnBelowTwenty)
blnInRange = true;
return (blnAboveZero, blnBelowTwenty, blnInRange);
}
var (isZero, isNotmax, inRange) = DoSomething(15);
}
Listing 1-49Using tuples without discards
本地函数返回一个元组,该元组包含三个布尔变量,分别用于零以上检查、20 以下检查和标记值是否在范围内的标志。
严格来说,局部函数的inRangeCheck值足够好地告诉我们零检查和最大值检查都为真。因此,我可以将代码修改如下。
private void UsingDiscards()
{
// Local function
(bool zeroCheck, bool maxCheck, bool inRangeCheck) DoSomething(int value)
{
bool blnAboveZero = false;
bool blnBelowTwenty = false;
bool blnInRange = false;
if (value > 0)
blnAboveZero = true;
if (value <= 20)
blnBelowTwenty = true;
if (blnAboveZero && blnBelowTwenty)
blnInRange = true;
return (blnAboveZero, blnBelowTwenty, blnInRange);
}
var (_, _, blnValid) = DoSomething(15);
}
Listing 1-50Using discards in tuples
因此,我们可以通过在解构中使用_来丢弃零检查和最大检查值。这样做,我告诉编译器,我不关心元组返回的变量的前两个校验值是什么。
输出参数
C# 7 中对 out 参数的增强非常受欢迎。在本章的前面,我们已经了解了如何使用 out 参数。很明显,当使用 out 参数时,我们不再需要声明一个独立的变量。这一点在我们创建TryParse的时候就很明显了。
请注意,out 参数不仅作为 TryParse 的 out 参数有用。当在常规方法中使用它时,如果您希望返回一个额外的值,它也可以增加很多值,而使用元组有点大材小用。
特别是在TryParse中,out 参数在某些情况下可能有些无用。丢弃为这个问题提供了一个简洁的解决方案。考虑下面的代码清单。
// Out parameters
if (bool.TryParse("true", out _))
Debug.WriteLine("The string value is a valid boolean");
else
Debug.WriteLine("The string value is not a valid boolean");
Listing 1-51Using out parameters with discards
我根本不会使用 out 参数。我只想检查这个值是否是有效的布尔值。因此,我可以告诉编译器,我不关心 out 参数,它可以被丢弃。
独立丢弃
丢弃可以单独使用,表示您想要忽略该变量。您可能想知道这在什么时候有用。考虑下面对 ExecuteCommand 方法的调用。
请注意,SQL 查询和 SQL 连接字符串参数只是占位符。您需要在这里添加有效值,否则代码将抛出异常。
默认情况下,它返回受UPDATE、INSERT或DELETE语句影响的行数。
private void UsingDiscards()
{
// Standalone discard
_ = ExecuteCommand("[UPDATE table SQL]", "[sql connection string here]");
}
private int ExecuteCommand(string sql, string sqlConnectionString)
{
using (SqlConnection conn = new SqlConnection(sqlConnectionString))
{
SqlCommand cmd = new SqlCommand(sql, conn);
cmd.Connection.Open();
return cmd.ExecuteNonQuery();
}
}
Listing 1-52Standalone discard variable
在对 ExecuteCommand 方法的调用中,我使用了一个 discard 变量来忽略受影响的行数。我知道使用没有变量赋值的ExecuteCommand("[UPDATE table SQL]", "[sql connection string here]");不会返回任何东西(很明显),但是我想说明使用丢弃变量_本质上做了同样的事情。
另一个例子是在下面的控制台应用代码清单中选择忽略从 async DoSomethingAsync方法返回的任务对象。
public static async Task DoSomethingAsync(int valueA, int valueB)
{
WriteLine("Async started at: " + DateTime.Now);
_ = Task.Run(() => valueA + valueB);
await Task.Delay(5000);
WriteLine("Async completed at: " + DateTime.Now);
}
Listing 1-53Ignoring the Task object returned with discard
如果您想提高代码的可读性和应用的性能,丢弃是非常有益的。不可否认,使用单个丢弃变量减少的内存分配很可能很小。对于大型应用,忽略不必要的变量确实会产生很大的影响。
模式匹配
如果你回想一下关于模式匹配的部分,你会记得我们使用了一个is表达式来检查我们是否正在处理一个Student、Lecturer、Alumnus或ExchangeStudent对象。
丢弃也可以和is表达式一起使用。考虑下面的代码清单。
// Using discard with is expression
if (someperson is Student student)
{
return $"{student.StudentDetails().fullName} is enrolled for {student.StudentDetails().studentCourse} with student number {student.StudentDetails().studentNum}";
}
else if (someperson is Lecturer lecturer)
{
return $"{lecturer.LecturerDetails().fullName} teaches {lecturer.LecturerDetails().courseSpecial}";
}
else if (someperson is Alumnus alumnus)
{
return $"{alumnus.AlumnusDetails().fullName} has completed {alumnus.AlumnusDetails().degreeObtained} in {alumnus.AlumnusDetails().yearCompleted}";
}
else if (someperson is ExchangeStudent exchStudent)
{
return $"{exchStudent.ExchangeStudentDetails().fullName} has {exchStudent.ExchangeStudentDetails().daysLeftOnVisa} days left on Student Visa";
}
else if (someperson is var _)
{
return $"Invalid {nameof(someperson)} object passed.";
}
Listing 1-54Using discard with is expression
最后一句话基本上是说,如果我不能将类与任何东西相匹配,那么我真的不知道我在处理什么。在这里给一个变量赋值实际上没有意义,所以我只使用丢弃变量,并向调用代码返回一条消息。
我们可以用 switch 语句做完全相同的事情。
// Using discard with switch
switch (someperson)
{
case Student student:
return $"{student.StudentDetails().fullName}";
case Lecturer lecturer:
return $"{lecturer.LecturerDetails().fullName}";
case Alumnus alumnus:
return $"{alumnus.AlumnusDetails().fullName}";
case ExchangeStudent exchangeStudent:
return $"{exchangeStudent.ExchangeStudentDetails().fullName}";
case var _:
return $"Invalid {nameof(someperson)} object passed.";
}
Listing 1-55Using discard with a switch
同样的道理也适用于开关。如果我不知道我在处理什么,我可以使用一个 discard 变量向调用代码返回一条消息,指出传递的参数与任何预期的对象都不匹配。
包扎
我们已经学习了 C# 7 的很多特性。我们从查看元组以及如何更改元组值的默认位置名称开始。我们还看了比较元组以及元组如何推断元组元素名称。
然后我们看了一下模式匹配以及如何使用is类型模式和switch模式。我们还看到了如何在case表达式中使用when子句,以及如何检查null。
下一节简要介绍了 out 变量,在这里我引入了丢弃,并在 out 变量的上下文中对此进行了简要讨论。
接下来是局部函数,我向您展示了当您在局部函数中使用的代码仅适用于包含成员时,这是如何为您带来好处的。
对于通用异步返回类型,我们看到,如果使用正确,它肯定可以提高应用的性能。你会记得建议的做法是使用Task或Task<T>,只有在完成性能测试后,你才应该考虑使用ValueTask<T>。
然后讨论了抛出表达式,您了解到 C# 的发展需要在某些情况下使用抛出表达式。
最后,我们更详细地回顾了丢弃,因为它与元组、输出参数、模式匹配和独立丢弃有关。
二、探索 C#
本章将介绍一些开发人员可能会忽略的 C# 特性。这是我在讨论特定功能时经常听到的一句话:“我听说过它,但以前没用过。”
抽象类和接口等特性。你知道这两者之间的区别吗?你会如何使用其中一个?lambda 表达式怎么样?你以前在日常编码中使用过这个特性吗?
这一章是关于进一步探索 C# 的。我们不会讨论 C# 7 特定的代码,而是讨论 C# 语言的一般特性。将讨论以下主题:
-
使用和实现抽象类
-
使用和实现接口
-
使用 async 和 await 的异步编程
-
利用扩展方法
-
泛型
-
可空类型
-
动态类型
如果不简要回顾一下 C# 的历史,讨论 C # 的特性是不完整的。让我们看看这一切是如何开始的。
C# 的历史
1999 年 1 月,安德斯·海尔斯伯格和他的团队开始开发这种被称为“酷”的新语言。它代表类似于 C 的面向对象语言的 ??,但是在 2000 年 7 月举行的专业开发者大会上被重新命名为 C#。
有人表示,将名称从 Cool 改为 C# 的决定是出于某些商标限制。微软开始寻找另一个名字,但这个名字仍然与 c 有关。
众所周知,C# 中的++运算符用于将变量递增 1。鉴于已经有了一种叫做 C++的语言,微软的团队需要想出一些不同但相似的东西。称之为 C+++是行不通的,但是如果你观察四个+符号,#符号可以被看作是串在一起的四个+符号。
这意味着这种类 C 面向对象语言的下一个增量将被称为 C#。对音乐的引用也很有趣,尤其是当人们考虑到#是一种将音符提高半音的音乐符号时。表 2-1 列出了 C# 的版本以及这些版本中发布的特性。
表 2-1。
C# 这么多年来
|C# 版本
|
出厂日期
|
。NET 框架
|
可视化工作室
|
功能概述
| | --- | --- | --- | --- | --- | | C# 1.0 | 2002 年 1 月 | One | 与 2002 年相比 | 类、结构、接口、事件、属性、委托、表达式、语句、属性、文字 | | C# 1.2 | 2003 年 4 月 | One point one | 与 2003 年相比 | 小的增强,foreach 循环现在在 IEnumerator 上实现 IDisposable 时调用 Dispose | | C# 2.0 | 2005 年 11 月 | Two | vs2005 | 泛型、分部类型、匿名方法、可空类型、迭代器、协变和逆变。对现有功能的增强,例如 getter 和 setter 的独立可访问性、静态类、委托接口 | | C# 3.0 | 2007 年 11 月 | 3.0 和 3.5 | vs2008 | 自动实现的属性、匿名类型、查询表达式、lambda 表达式、表达式树、扩展方法、隐式类型化局部变量、分部方法、对象和集合初始值设定项 | | C# 4.0 | 2010 年 4 月 | four | 与 2010 年相比 | 动态绑定、名称/可选参数、通用协变和逆变、嵌入式互操作类型 | | C# 5.0 | 2012 年 8 月 | Four point five | 对比 2012 年和 2013 年 | 异步成员(异步和等待),调用者信息属性 | | C# 6.0 | 2015 年 7 月 | Four point six | 对比 2015 年 | 静态导入、异常过滤器、自动属性初始值设定项、表达式主体成员、空传播器、字符串插值、运算符名称、索引初始值设定项、catch/finally 中的 await、仅 getter 属性的默认值 | | C# 7.0 | 2017 年 3 月 | 4.6.2 | VS 2017 | 输出变量、元组、丢弃、模式匹配、局部函数、抛出表达式、通用异步和返回类型、文字语法改进、引用局部变量和返回、更多表达式体成员 | | C# 7.1 | 2017 年 8 月 | Four point seven | VS 2017 | 异步主方法、默认文字表达式、推断的元组元素名称 | | C# 7.2 | 2017 年 11 月 | 4.7.1 | VS 2017 | 条件引用表达式、私有受保护访问修饰符、数字文本中的前导下划线、非尾随命名参数、编写安全高效代码的技术 | | C# 7.3 | 2018 年 5 月 | 4.7.2 | VS 2017 | 重新分配 ref 局部变量,stackalloc 数组上的初始值设定项,对任何支持模式的类型使用 fixed 语句,使用== and 测试元组类型!=,在更多位置使用表达式变量 |
有关 C# 不同版本特性的更多信息,请参考位于 https://docs.microsoft.com 的微软文档。
既然我们已经看到了我们的进步,让我们来看看本章开始时概述的 C# 的一些具体特性。
使用和实现抽象类
在我们看抽象类之前,我们首先需要看一下abstract修饰符以及它的意思。abstract修饰符只是告诉你被修改的东西没有完整的实现。此修饰符可以与一起使用
-
班级
-
方法
-
性能
-
索引器
-
事件
当我们在类声明中使用abstract修饰符时,我们实际上是在说我们正在创建的类只是其他类的基本基类。
这意味着任何标记为抽象的成员或基类中包含的成员都必须由派生类(使用基类的类)实现。你还会听说抽象类也被称为蓝图。
抽象类特征
因此,抽象类具有以下重要特征:
-
您不能创建抽象类的实例。
-
抽象类可以包含抽象方法和访问器。
-
不能对抽象类使用
sealed修饰符。 -
如果一个非抽象类是从一个抽象类派生的,那么派生类必须包含抽象方法和访问器的实现。
sealed修饰符不能用于抽象类的原因是因为sealed修饰符阻止类继承,而抽象修饰符要求类必须被继承。
抽象方法
在方法或属性声明中使用abstract修饰符只是简单地声明
-
抽象方法隐含地是一个虚方法。
-
只能在抽象类中使用抽象方法。
-
抽象方法没有实现;因此它没有方法体。
-
不允许在抽象方法声明中使用
static或virtual修饰符。
当我们说一个抽象方法没有实现,因此没有方法体,这意味着什么?考虑下面的代码清单。
public abstract void MyAbstractMethod();
Listing 2-1Abstract method declaration
这基本上告诉我们,派生类需要实现这个方法,并为这个方法提供实现。
抽象属性
当考虑抽象方法时,你会注意到抽象属性的行为方式非常相似。真正的区别在于声明和调用语法:
-
不能在静态属性上使用
abstract修饰符。 -
通过声明使用
override修饰符的属性,可以在派生类中重写继承的抽象属性。
当查看一些代码示例时,所有这些将更有意义。接下来让我们来说明抽象类的用法。
使用抽象类
为了说明抽象类的使用,我将创建一个非常简单的抽象类。然后它将被继承并在派生类中使用。考虑下面的清单。
abstract class AbstractBaseClass
{
protected int _propA = 100;
protected int _propB = 200;
public abstract int PropA { get; }
public abstract int PropB { get; }
public abstract int PerformCalculationAB();
}
Listing 2-2Abstract class
现在我们有了抽象类,让我们去实例化它。如图 2-1 所示,我们有一个错误。为什么我们会有错误?
图 2-1。
抽象类实例化时出错
啊哈!记得我之前说过我们不能实例化一个抽象类。编译器显示一个错误,指出您无法创建抽象类的实例。然而,我们可以创建一个新的类,并从抽象类中派生出来。考虑下面的代码清单。
class DerivedClass : AbstractBaseClass
{
}
Listing 2-3Inheriting from an abstract class
我们继承了名为DerivedClass的派生类中的抽象类。然后编译器给我们另一个警告,如图 2-2 所示。
图 2-2。
派生类实现
编译器告诉你你需要实现抽象类的成员。当你点击灯泡,点击实现抽象类时,Visual Studio 会自动为你提供实现结构。这样做之后,您的代码将如清单 2-4 所示。
class DerivedClass : AbstractBaseClass
{
public override int PropA => throw new NotImplementedException();
public override int PropB => throw new NotImplementedException();
public override int PerformCalculationAB()
{
throw new NotImplementedException();
}
}
Listing 2-4Implementing the abstract class
您会注意到生成的代码将抛出一个NotImplementedException。这是有意义的,因为您实际上没有为代码提供任何实现,编译器无法猜测您想在派生类中做什么。让我们给我们的派生类添加一些代码,如清单 2-5 所示。
class DerivedClass : AbstractBaseClass
{
public override int PropA => _propA;
public override int PropB => _propB;
public override int PerformCalculationAB()
{
_propA += 50;
_propB += 100;
return _propA + _propB;
}
}
Listing 2-5Code implementation added
在调用代码中,我们现在可以实例化派生类并写出值。
为此,我简单地使用了一个控制台应用,将using static System.Console;添加到using语句中。
static void Main(string[] args)
{
DerivedClass d = new DerivedClass();
WriteLine($"PropA before calculation {d.PropA}");
WriteLine($"PropB before calculation {d.PropB}");
WriteLine($"Perform calculation {d.PerformCalculationAB()}");
WriteLine($"PropA after calculation {d.PropA}");
WriteLine($"PropB after calculation {d.PropB}");
ReadLine();
}
Listing 2-6Calling the derived class
检查我们编写的代码的输出,您将看到显示了两个属性的默认值。执行计算后,我们的属性值发生了变化。
PropA before calculation 100
PropB before calculation 200
Perform calculation 450
PropA after calculation 150
PropB after calculation 300
Listing 2-7Output from code in derived class
控制台应用的输出在这里并不重要。我想向您展示的是一个从您之前创建的抽象类继承而来的派生类的工作示例。
我什么时候使用抽象类?
上一节中的代码清单有点抽象(双关语)。为什么不把一个类定义为正常的呢?什么时候应该使用抽象类?
我认为这是许多开发人员可能会思考的问题,但是一旦理解了一个基本概念,使用抽象类的逻辑就非常简单了。
抽象类就像描述派生对象的普通名词。当我们考虑下面的描述时,这被清楚地说明。
轿车、SUV、皮卡、两厢都是车辆。尽管轿车与 SUV 或皮卡有很大不同,但它们都有作为车辆的共性。
因此,车辆必须有发动机、车辆识别号、前灯等等。这些(以及更多)将是车辆之间的共同特征。因此,我们可以声明一个名为Vehicle的抽象类,并赋予它这些派生类(轿车、SUV 等)的共同特征。)必须实现。
因此,由派生类将实现添加到抽象类,然后拥有仅特定于派生类的附加属性和方法。例如,皮卡将有一个装载区,而轿车将没有。轿车会有一个行李箱空间。
虽然这个例子相当简单,但它很好地说明了这个概念。一个更真实的例子是使用销售订单和采购订单的 ERP 系统。这两个都是订单,我们可以定义一个名为Order的抽象类,它定义了订单号、订单状态、订单行数等等。
派生类SalesOrder和PurchaseOrder必须都具有这些属性,但是只有销售订单可以包含客户信息,而采购订单将包含供应商信息。
因此,抽象类允许我们清楚地定义密切相关的派生对象之间的共性。
使用和实现接口
在上一节中,我们看了一下抽象类。你会记得我说过抽象类就像描述派生对象的普通名词。然而,当提到接口时,我们谈论的是接口包含了对相关功能进行分组的定义这一事实。这意味着实现一个接口的类或结构共享相同的功能。
回想一下我们抽象的车辆类例子。我们说轿车,SUV 等。都是交通工具。因此,抽象的Vehicle类告诉我们派生类必须实现什么共同的特征。然而,当提到接口时,我们是说一些或所有的派生类共享某种功能。因此,我们可以把接口看作描述动作的动词。
让我们假设所有的车辆都必须有一个 VIN。这是我们可以用来检查没有两辆车有相同的 VIN 的东西。
VIN 是汽车工业中用于识别机动车辆的唯一车辆识别号。
因此,可以肯定地说,我们可以创建一个名为IComparable的接口,它将增加比较车辆 vin 的能力。然后,我们知道不同的车辆有不同的特点。通常你在一辆车上花的越多,它的功能就越多。然而,某些功能只对某些车辆有意义。差速锁(或差速锁)只在某些车辆上才有意义,例如 SUV。
因此,我们可以有把握地说,创建一个名为IDiffLockable的接口将增加确定某些车辆是否可以自动差速锁的能力。
请注意,按照惯例,接口通常以 I 开头的名称创建。
接口具有以下属性:
-
这就像一个抽象类;因此,任何实现接口的类或结构都必须实现其成员。
-
您不能直接实例化接口。
-
接口成员由执行实现的类或结构来实现。
-
事件、索引器、属性和方法都可以包含在一个接口中。
-
接口不包含方法的实现。
-
允许在一个类或结构上实现多个接口。
-
您可以从基类继承,也可以实现多个接口。
让我们继续为我们的车辆类创建两个接口,并仔细看看我们将如何使用这些接口。
创建抽象和派生类
让我们继续创建一个名为Vehicle的抽象类,我们的派生类将继承它。
abstract class Vehicle
{
protected int _wheelCount = 4;
protected int _engineSize = 0;
protected string _vinNumber = "";
public abstract string VinNumber { get; }
public abstract int EngineSize { get; }
public abstract int WheelCount { get; }
}
Listing 2-8The Vehicle abstract class
这个抽象类本质上非常简单,但是它的目的是为我们将要创建的名为Car和SUV的派生类提供实现成员。
class Car : Vehicle
{
public override string VinNumber => _vinNumber;
public override int EngineSize => _engineSize;
public override int WheelCount => _wheelCount;
public Car(string vinNumber, int engineSize, int wheelCount)
{
_vinNumber = vinNumber;
_engineSize = engineSize;
_wheelCount = wheelCount;
}
}
Listing 2-9Car class
class SUV : Vehicle
{
public override string VinNumber => _vinNumber;
public override int EngineSize => _engineSize;
public override int WheelCount => _wheelCount;
public SUV(string vinNumber, int engineSize, int wheelCount)
{
_vinNumber = vinNumber;
_engineSize = engineSize;
_wheelCount = wheelCount;
}
}
Listing 2-10SUV class
现在我们已经创建了抽象的Vehicle类和派生的Car和SUV类,我们可以继续创建我们的接口了。
创建接口
如前所述,我们需要能够比较车辆的 vin,以确保它们确实是唯一的编号。为此,我们将使用interface关键字创建一个IComparable接口。
interface IComparable<T>
{
bool VinNumberEqual(T obj);
}
Listing 2-11
IComparable interface
因此,该接口将要求实现该接口的任何类或结构为名为VinNumberEqual的方法提供定义,该方法与该接口指定的签名相匹配。
您会注意到在IComparable接口中使用了 T 类型参数。我们在这里使用一个通用接口,客户端代码决定我们比较的对象的类型。本章稍后将讨论泛型。
换句话说,任何实现 IComparable 的类都必须包含一个名为VinNumberEqual的方法。我们还希望能够指定车辆是否具有自动差速锁功能。为此,我们将创建一个名为IDiffLockable的接口。
interface IDiffLockable
{
bool AutomaticDiff { get; }
}
Listing 2-12
IDiffLockable interface
因此,同样的逻辑也适用于这个接口。实现类必须提供一个名为AutomaticDiff的属性,该属性将启用或移除车辆的该特性。
实现接口
我们现在将在Car类上实现IComparable接口。Car类已经继承了Vehicle抽象类。为了实现IComparable,我们需要添加如下内容。
class Car : Vehicle, IComparable<Car>
Listing 2-13Implementing IComparable
Visual Studio 现在将提示您实现 IComparable 接口,如图 2-3 所示。
图 2-3。
Visual Studio 提示实现接口
当您单击灯泡并实现接口时,您的代码将如下所示。
class Car : Vehicle, IComparable<Car>
{
public override string VinNumber => _vinNumber;
public override int EngineSize => _engineSize;
public override int WheelCount => _wheelCount;
public Car(string vinNumber, int engineSize, int wheelCount)
{
_vinNumber = vinNumber;
_engineSize = engineSize;
_wheelCount = wheelCount;
}
public bool VinNumberEqual(Car car)
{
return VinNumber.Equals(car.VinNumber);
}
}
Listing 2-14IComparable interface implemented on Car class
接口成员VinNumberEqual被添加到您的类中,默认抛出一个NotImplementedException。要实现接口方法,添加一些代码,以便在Car对象相等时返回一个布尔值。这允许我们通过使用以下代码来检查两辆车的 VIN 是否相等。
Car car1 = new Car("VIN12345", 2, 4);
Car car2 = new Car("VIN12345", 2, 4);
WriteLine(car1.VinNumberEqual(car2) ? "ERROR: Vin numbers equal" : "Vin numbers unique");
Listing 2-15Checking the VIN of two Car classes
这个简单的例子向我们展示了如何使用接口向类添加功能,因为类和结构必须实现接口成员。
但是SUV类呢?它需要实现IComparable和IDiffLockable接口。我们按如下方式做这件事。
class SUV : Vehicle, IComparable<SUV>, IDiffLockable
{
}
Listing 2-16Implementing IComparable and IDiffLockable
Visual Studio 现在还提示您在SUV类上实现接口。当我们完成了这些并添加了您实现的代码后,您的类将如下所示。
class SUV : Vehicle, IComparable<SUV>, IDiffLockable
{
public override string VinNumber => _vinNumber;
public override int EngineSize => _engineSize;
public override int WheelCount => _wheelCount;
public bool AutomaticDiff { get; } = false;
public SUV(string vinNumber, int engineSize, int wheelCount, bool autoDiff)
{
_vinNumber = vinNumber;
_engineSize = engineSize;
_wheelCount = wheelCount;
AutomaticDiff = autoDiff;
}
public bool VinNumberEqual(SUV suv)
{
return VinNumber.Equals(suv.VinNumber);
}
}
Listing 2-17SUV class with implemented interfaces
我们正在实施 VIN 检查和自动 difflock 功能。
有时我们会遇到这样的情况,两个接口有相同的方法,但是有不同的实现。这很容易导致一个或两个接口的错误实现。正是因为这个原因,我们才能够显式地实现接口成员。
能够使用接口允许您从单个接口扩展几个类的功能。使用接口是因为它可以应用于一个或多个(但不是所有)类。很明显,事实上只有IDiffLockable在SUV类上实现,而IComparable在Car和SUV类上都实现了。
使用 Async 和 Await 的异步编程
异步编程将允许您编写能够执行长时间运行任务的代码,同时仍然保持应用的响应性。随着异步在。NET Framework 4.5,它简化了以前在应用中实现异步功能的复杂方法。
在这一节中,我们将看看如何使用 async 和 await,以及它们如何有利于您的开发工作。
如何编写异步方法?
要编写异步方法,使用async和await关键字是必要的。以下几点是异步方法的典型特征:
-
方法签名必须包括
async修饰符。 -
该方法必须返回
Task<T>、Task、void或ValueTask<T>。 -
方法语句必须包括至少一个
await表达式。 -
按照惯例,你的方法名应该以 Async 结尾。
为了说明异步代码的概念,您将创建一个 Windows 窗体项目,该项目读取一个大文件,并在处理文件中的每行文本时计算它读取的行数。
为此,我下载了一个包含《战争与和平》文本的大型文本文件。然后,我将该文本复制了几次,创建了一个非常大的文本文件。
我们的应用将处理该文件,并更新 UI 上的标签,以通知用户已经读取了多少行。在整个过程中,应用将保持完全响应。
基本表单设计(图 2-4 )包括一个标签,用于跟踪当前计算的行数,另一个标签将显示该过程完成后读取的总行数。它还有一个用于启动文件读取的按钮。
图 2-4。
响应式表单设计
在后面的代码中,您将添加一个名为ReadFileAsync的异步方法。在这里,我们将添加我们的异步文件读取逻辑。
private async Task<int> ReadFileAsync()
{
var FileLines = new List<string>();
int lineCount = 0;
using (var reader = File.OpenText(@"C:\temp\big_file.txt"))
{
string line = string.Empty;
while ((line = await reader.ReadLineAsync().ConfigureAwait(false)) != null)
{
FileLines.Add(line);
lineCount += 1;
if (lblLinesRead.InvokeRequired)
{
lblLinesRead.Invoke(new Action(() => lblLinesRead.Text = lineCount.ToString()));
}
else
{
lblLinesRead.Text = lineCount.ToString();
}
}
}
return lineCount;
}
Listing 2-18
ReadFileAsync async method
您会注意到,我在 label 控件上使用了InvokeRequired方法来更新 text 属性,因为我们与创建 label 控件的线程在不同的线程上。如果您试图在这里更新标签上的 text 属性而不使用InvokeRequired,您将收到一个跨线程冲突错误。
接下来,您需要将按钮点击事件更改为异步,并在ReadFileAsync方法上调用 await。代码将如下所示。
private async void btnReadBigFile_Click(object sender, EventArgs e)
{
int linesInFile = await ReadFileAsync();
lblCompletedLineCount.Text = linesInFile.ToString();
}
Listing 2-19
Button click event
运行您的应用,点击读取大文件按钮(图 2-5 )开始文件读取过程。请注意,在整个文件读取过程中,您可以移动 Windows 窗体并调整其大小。
图 2-5。
响应文件读取应用
只有在文件读取过程完成时,才会更新行计数标签。这很棒,我们有一个非常简单的异步方法。但是后台发生了什么呢?编译器是如何做到这一切的呢?
在后台
让我们继续,使用一个反编译器来查看我们的异步ReadFileAsync方法生成的代码。
我用的是 Redgate 的试用版。NET Reflector 来看看编译器生成的代码。
图 2-6。
原始异步 ReadFileAsync 方法
回头看看我们最初的异步ReadFileAsync方法,你会注意到它实际上是一个非常简单的代码(图 2-6 )。它符合前面详述的异步方法的特征。
[CompilerGenerated]
private sealed class <ReadFileAsync>d_ 3 : |AsyncStateMachine
{
// Fields
public int <>1_state;
public AsyncTaskMethodBuilder<int> <>t_builder;
public Form1 <>4_ this;
private Form1.<>c_DisplayClass3_0 <>8_1;
private List<string> <FileLines>5_2;
private StreamReader <reader>5_3;
private string <line>5_4;
private string <>s_5;
private ConfiguredTaskAwaitable< string>.ConfiguredTaskAwaiter <>u_1;
// Methods
public <ReadFileAsync>d_3();
private void MoveNext();
[DebuggerHidden]
private void SetStateMachine(|AsyncStateMachine stateMachine);
}
Listing 2-20Compiler generated code for the async ReadFileAsync method
然而,编译器生成的代码完全不同。作为开始,编译器实际上生成了一个类。在原始代码中,我们创建了一个方法。这里我们看到编译器创建了一个实现IAsyncStateMachine接口的密封类。
然后,ReadFileAsync方法中的所有变量现在都是密封类中的字段。这意味着我们在方法中创建的变量被捕获为用于管理本地状态的状态机中的字段。如果我们的ReadFileAsync方法被传递了一个参数,它也会被捕获为密封类中的一个字段。
再往下看,你会注意到一个叫做MoveNext的方法。状态机被编码到一个MoveNext中,每个步骤都会调用它。它用一个名为num的变量跟踪一个整数状态,并用它来执行代码。
因此,每次我们的代码调用await,就会有另一个状态和MoveNext来管理我们的异步方法的状态。
private void MoveNext()
{
int num = this.<>1__state;
try
{
if (num != 0)
{
this.<>8__1 = new Form1.<>c_DisplayClass3_0();
this.<>8__1.<>4_this = this.<>4_ this;
this.<FileLines>5__2 = new List<string>();
this.<>8__1.lineCount = 0;
this.<reader>5__3 = File.OpenText(@"C:\temp\big_file.txt");
}
try
{
ConfiguredTaskAwaitable<string>.ConfiguredTaskAwaiter awaiter;
if (num == 0)
{
awaiter = this.<>u__1;
this.<>u__1 = new ConfiguredTaskAwaitable<string>.ConfiguredTaskAwaiter();
this.<>1__state = num = -1;
}
else
{
this.<line>5__4 = string.Empty;
goto TR_0014;
}
TR_0010:
this.<>s__5 = awaiter.GetResult();
if ((this.<line>5__4 = this.<>s__5) != null)
{
this.<FileLines>5__2.Add(this.<line>5__4);
this.<>8__1.lineCount++;
if (!this.<>4__this.lblLinesRead.InvokeRequired)
{
this.<>4__this.lblLinesRead.Text = this.<>8__1.lineCount.ToString();
}
else
{
Action method = this.<>8__1.<>9__0;
if (this.<>8__1.<>9__0 == null)
{
Action local1 = this.<> 8__1.<>9__0;
method = this.<>8__1.<>9__0 = new Action(this.<>8__1.<ReadFileAsync>b__0);
}
this.<>4__this.lblLinesRead.Invoke(method);
}
goto TR_0014;
}
else
{
this.<>s__5 = null;
this.<line>5__4 = null;
}
goto TR_0003;
TR_0014:
while (true)
{
awaiter = this.<reader>5__3.ReadLineAsync().ConfigureAwait(false).GetAwaiter();
if (awaiter.lsCompleted)
{
goto TR_0010;
}
else
{
this.<>1__state = num = 0;
this.<>u__1 = awaiter;
Form1.<ReadFileAsync>d__3 stateMachine = this;
this.<>t__builder.AwaitUnsafeOnCompleted<ConfiguredTaskAwaitable<string>.Configured
}
break;
}
return;
}
finally
{
if ((num < 0) && (this.<reader>5__3 != null))
{
this.<reader>5__3.Dispose();
}
}
TR_0003:
this.<reader>5__3 = null;
int lineCount = this.<>8__1.lineCount;
this.<>1__state = -2;
this.<>t_builder.SetResult(lineCount);
}
catch (Exception exception)
{
this.<>1__state = -2;
this.<>t_builder.SetException(exception);
}
}
Listing 2-21MoveNext method for state machine
整个MoveNext方法被包装在一个try / catch块中。这意味着即使你的异步方法没有try / catch处理程序,任何异常仍然会被捕获。这就是await能够在调用代码中重新抛出异常的方式。
一些最后的提示
async 和 await 的话题很大,要学的东西很多。这种学习的大部分将通过编写代码和犯错误来完成。这里有一些可能有助于缓解学习曲线的提示。
避免使用 Wait()
通常认为在以下情况下避免使用Wait是最佳做法。请看下面的伪代码清单。
async Task PerformSomeLongRunningOperation()
{
DoSomeWork(false).Wait();
}
async Task DoSomeWork(bool blnToggleIsOn)
{
// Some work is done here
}
Listing 2-22Using Wait
在我们的异步PerformSomeLongRunningOperation方法中,我们有一个对DoSomeWork的调用,它传递一个布尔值作为参数并调用Wait。这样做对我们使用 async 和 await 没有任何好处,因为Wait正在阻塞代码。
因为DoSomeWork异步方法返回一个任务,我们应该使用 await。然后,我们的代码需要进行如下更改。
async Task PerformSomeLongRunningOperation()
{
await DoSomeWork(false);
}
Listing 2-23Using await
如果出于某种原因我们必须同步运行DoSomeWork异步方法,我们需要使用GetAwaiter和GetResult,如下面的代码清单所示。
async Task PerformSomeLongRunningOperation()
{
DoSomeWork(false).GetAwaiter().GetResult();
}
Listing 2-24Using GetAwaiter and GetResult
从本质上来说,GetAwaiter GetResult做的事情和 Wait(也就是 block)一样,但是唯一的区别是GetAwaiter GetResult将解开任何在DoSomeWork方法中抛出的异常。
必要时使用 configurewait(false)
当使用 Windows 窗体应用时,应用使用 UI 线程。这意味着该上下文是一个 UI 上下文。对于 web 应用来说也是如此。当响应 ASP.NET 请求时,上下文是 ASP.NET 请求上下文。如果既不使用 UI 也不使用请求上下文,则使用线程池。
如果你的代码没有接触 UI,那么使用ConfigureAwait(false)告诉异步方法不要在上下文中继续。然后,它将在线程池中的一个线程上继续。如果设置为 true,则代码会尝试将延续封送回原始上下文。
利用扩展方法
从 C# 3.0 开始,扩展方法已经对我使用代码的方式产生了巨大的影响。我能够在不创建新的派生类型的情况下向现有类型添加方法。《C# 编程指南》将扩展方法描述为一种特殊的静态方法。唯一的区别是,它们被调用时就好像它们是被扩展的类型上的实例方法一样(即,通过使用实例方法语法来调用)。
但是到底什么才是有用的扩展方法呢?让我们看一个扩展方法的例子。
检查字符串是否是有效的整数
我将使用的例子非常简单。您将检查一个字符串值是否是一个有效的整数。首先创建一个包含静态扩展方法的静态类。
请注意,括号中的第一个参数是对正在扩展的内容的引用。换句话说,this String指的是这个扩展方法作用的类型。它作用于琴弦。
这就是这个扩展方法的全部内容。它接受被扩展类型的值,并检查它是否可以被解析为整数。然后将 true 或 false 返回给调用代码。考虑下面的代码清单。
public static class ExtensionMethods
{
public static bool IsValidInt(this String value)
{
bool blnValidInt = false;
if (int.TryParse(value, out int result))
{
blnValidInt = true;
}
return blnValidInt;
}
}
Listing 2-25Extension method example
在字符串变量上调用扩展方法IsValidInt时,您会注意到智能感知将其标记为一个带有向下箭头的正方形(图 2-7 )。这表示智能感知窗口中的扩展方法。在智能感知窗口打开时按下 Alt+X ,将只显示扩展方法。令人惊讶的是有这么多扩展方法。
图 2-7。
扩展方法智能感知
另一件要注意的事情是,因为您指定了 extension 方法只扩展 string 类型,所以它显然不能用于 Boolean 等其他类型。您可以通过向扩展方法参数添加this String value来实现这一点。
如果您希望这个扩展方法扩展另一个类型,您需要在扩展方法的签名中指定这一点。
string strInt = "123";
if (strInt.IsValidInt())
{
WriteLine("Valid Integer");
}
else
{
WriteLine("Not an Integer");
}
Listing 2-26Calling IsValidInt
还可以向扩展方法传递附加参数。在下一个示例中,如果整数值是有效的整数,我们将返回它。这可以很容易地用 out 参数来完成,如下所示。
public static bool IsValidInt(this String value, out int integerValue)
{
bool blnValidInt = false;
integerValue = 0;
if (int.TryParse(value, out int result))
{
blnValidInt = true;
integerValue = result;
}
return blnValidInt;
}
Listing 2-27Passing argument to an extension method
这允许您非常灵活地使用扩展方法。
扩展方法的优先级低于实例方法
不过要注意的一点是,扩展方法的优先级低于类型本身中定义的实例方法。扩展方法将扩展一个类或接口,但不会覆盖它们。
当遇到方法调用时,编译器将总是在类型的实例方法中寻找匹配。此后,它将搜索为该类型定义的任何扩展方法。
有时,您可能会看到一个错误,指出某个类型不包含您调用的方法的定义,并且找不到接受该类型作为第一个参数的可访问扩展方法。这是编译器试图找到你调用的东西,但是找不到。最后提到扩展方法也很有意思。
最好用一个例子来说明这一点。继续创建下面的类。
public class WorkerClass
{
public void DoSomething()
{
Console.WriteLine("I am a method of the WorkerClass");
}
}
Listing 2-28Class with DoSomething method
接下来,创建一个名为DoSomething的扩展方法。
public static void DoSomething(this Car value)
{
Console.WriteLine("I am an extension method");
}
Listing 2-29Extension method DoSomething
创建该类的一个实例并运行代码将显示文本我是 WorkerClass 的一个方法。
WorkerClass worker = new WorkerClass();
worker.DoSomething();
Listing 2-30Calling the DoSomething method
这意味着永远不会调用扩展方法,因为类的DoSomething方法比扩展方法具有更高的优先级,并且两个方法的签名是相同的。
如果你必须改变DoSomething扩展方法的签名,扩展方法将被调用。考虑下面的代码清单。
public static void DoSomething(this WorkerClass value, int iValue)
{
Console.WriteLine($"I am an extension method with parameter {iValue}");
}
Listing 2-31DoSomething method with changed signature
如果你用worker.DoSomething(5);调用扩展方法,控制台应用将输出文本我是一个带参数 5 的扩展方法。这是因为类上的DoSomething方法和DoSomething扩展方法的签名是不同的。
泛型
从 C# 2 开始,泛型就伴随着我们。目标是允许开发人员在维护类型安全的同时重用代码。请将泛型视为一个蓝图,它允许您定义类型安全的数据结构,而无需实际定义类型。
例如,对于泛型,调用代码在实例化泛型类时决定类型。稍后您将看到,我们创建的泛型类将允许收集混合类型。
您可能不知道,但是您实际上一直在使用泛型。泛型用于 LINQ、列表(图 2-8 )、字典等等。这些结构中的代码专注于管理代码,而不必担心类型。
图 2-8。
T 列表
回想一下你创建List<>的时候。这使用泛型,并允许您在创建列表时指定类型。你可以创建一个整数列表,就像创建一个双精度列表或者你自己定制的类列表一样简单。
按照惯例,T 在泛型中用来表示使用泛型类型参数。
当创建一个泛型类时,我们可以给它一个泛型类型参数,如下所示。
public class VehicleCarrier<T>
Listing 2-32VehicleCarrier of T
T用在尖括号之间,您可以定义多个类型参数。T因此被用作你的类定义的一个参数。我们也可以说T参数化了你将在类中使用的类型。
你可以对数组做同样的事情。
private T[] _loadbay;
Listing 2-33Array of T
您没有定义整数数组,而是定义了一个T数组。如果在我的类内部使用,T将是在类型参数中传递给类的类型。
非通用运载工具类别
让我来说明使用泛型的好处。在下面的代码清单中,我有一个用于保存一组Car对象的类。
想想汽车工业中用来运输车辆的卡车。
在我的VehicleCarrier类中,我有一个_capacity,它只允许我将特定数量的Car对象添加到_loadbay数组中。我不能添加超过容量变量中定义的最大数量的车辆。
public class VehicleCarrier
{
private Car[] _loadbay;
private int _capacity;
public VehicleCarrier(int capacity)
{
_loadbay = new Car[capacity];
_capacity = capacity;
}
public void AddVehicle(Car vehicle)
{
var loaded = _loadbay.Where(x => x != null).Count();
if (loaded == _capacity)
{
Console.WriteLine($"Vehicle Carrier filled to capacity {_capacity}.");
}
else
{
_loadbay[loaded] = vehicle;
}
}
public void GetAllVehicles()
{
foreach (Car vehicle in _loadbay)
{
Console.WriteLine($"Vehicle with VIN number {vehicle.VinNumber} loaded");
}
}
}
Listing 2-34Non-generic VehicleCarrier class
这个VehicleCarrier类所做的就是包含汽车的集合,并将它传递到我代码中的其他地方。当我需要检查载体时,我可以输出所有包含在VehicleCarrier类中的汽车的 vin。为了使用这个类,我可以创建一些Car对象并将它们添加到一个列表中。
注意,如前所述,通过在代码中使用 T 列表,您已经在这里使用泛型了。在这种情况下,您正在创建一个汽车列表。
然后这个列表被添加到我的车辆承运人类别中。
//Without Generics
Car car1 = new Car("123", 2, 4);
Car car2 = new Car("456", 3, 4);
Car car3 = new Car("789", 2, 4);
List<Car> carList = new List<Car>(new Car[] { car1, car2, car3 });
VehicleCarrier carrier = new VehicleCarrier(3);
foreach (var vehicle in carList)
{
carrier.AddVehicle(vehicle);
}
carrier.GetAllVehicles();
Listing 2-35Using non-generic VehicleCarrier class
当我调用GetAllVehicles方法时,该类的输出只是包含在VehicleCarrier类中的每个Car对象的 vin。
Vehicle with VIN number 123 loaded
Vehicle with VIN number 456 loaded
Vehicle with VIN number 789 loaded
Listing 2-36Console window output from non-generic VehicleCarrier class
VehicleCarrier类(图 2-9 )是收集和移动Car对象的好方法,但不幸的是,我只能用它来处理Car对象。
图 2-9。
错误
我将无法使用我的VehicleCarrier类来传输SUV对象。这样做会导致编译器错误。因此,我们的VehicleCarrier类的功能非常有限。我们不能灵活使用它,因为它只接受Car对象。
将车辆承运人类别更改为通用类别
让我们对VehicleCarrier类做一些修改,使它更加灵活。我将从向我的类添加一个泛型类型参数开始。这里我告诉编译器我的类将使用一种类型的T。
我现在能够将我的_loadbay定义为一个T的数组。事实上,在我的VehicleCarrier类中,我可以用T替换Car类型。
下面的代码清单是修改过的VehicleCarrier类,也包含了一个使用模式匹配的活跃的GetAllVehicles方法。
public class VehicleCarrier<T>
{
private T[] _loadbay;
private int _capacity;
public VehicleCarrier(int capacity)
{
_loadbay = new T[capacity];
_capacity = capacity;
}
public void AddVehicle(T vehicle)
{
var loaded = _loadbay.Where(x => x != null).Count();
if (loaded == _capacity)
{
Console.WriteLine($"Vehicle Carrier filled to capacity {_capacity}.");
}
else
{
_loadbay[loaded] = vehicle;
}
}
public void GetAllVehicles()
{
foreach (T vehicle in _loadbay)
{
switch (vehicle)
{
case Car car:
Console.WriteLine($"{car.GetType().Name} with VIN number {car.VinNumber} loaded");
break;
case SUV suv:
Console.WriteLine($"{suv.GetType().Name} with VIN number {suv.VinNumber} loaded");
break;
default:
Console.WriteLine($"Vehicle not determined");
break;
}
}
}
}
Listing 2-37Generic VehicleCarrier class
这允许我创建一个SUV对象的列表,并将其传递给我的VehicleCarrier类。我不再局限于只在我的VehicleCarrier类中使用Car对象。
// With Generics
SUV suv1 = new SUV("123", 2, 4, false);
SUV suv2 = new SUV("456", 3, 4, false);
SUV suv3 = new SUV("789", 2, 4, false);
List<SUV> carList = new List<SUV>(new SUV[] { suv1, suv2, suv3 });
VehicleCarrier<SUV> carrier = new VehicleCarrier<SUV>(3);
foreach (var vehicle in carList)
{
carrier.AddVehicle(vehicle);
}
carrier.GetAllVehicles();
Listing 2-38Using generic VehicleCarrier class
调用方法GetAllVehicles返回包含在我的类中的SUV对象的 vin。
SUV with VIN number 123 loaded
SUV with VIN number 456 loaded
SUV with VIN number 789 loaded
Listing 2-39Console window output from generic VehicleCarrier class
这意味着我可以使用同一个VehicleCarrier类创建一个Car的VehicleCarrier和一个SUV的VehicleCarrier。看到好处了吗?
混合搭配
我还能够通过指定我的VehicleCarrier类与类型object一起使用来混合和匹配。这允许我创建一个由Car和SUV对象组成的List,并将其添加到我的VehicleCarrier类中。
SUV suv1 = new SUV("123", 2, 4, false);
Car car1 = new Car("456", 3, 4);
SUV suv3 = new SUV("789", 2, 4, false);
List<object> carList = new List<object>(new object[] { suv1, car1, suv3 });
VehicleCarrier<object> carrier = new VehicleCarrier<object>(3);
foreach (var vehicle in carList)
{
carrier.AddVehicle(vehicle);
}
carrier.GetAllVehicles();
Listing 2-40Loading SUV and Car classes
我现在可以调用GetAllVehicles方法,该方法使用 switch 语句和模式匹配来输出它正在处理的特定对象的 VIN。
SUV with VIN number 123 loaded
Car with VIN number 456 loaded
SUV with VIN number 789 loaded
Listing 2-41Generic VehicleCarrier class of object output
我的T的泛型VehicleCarrier现在完全是泛型和高性能的。它减少了代码重复,并允许我在应用中有更多的灵活性。
概述和更多关于泛型的内容
当我们用尖括号<>结束一个类时,我们称之为泛型类。然而,泛型并没有就此止步。我们也可以有泛型结构、泛型接口和泛型委托。如前所述,T代表类型参数。它定义了一个泛型类(例如)将处理什么类型的数据。
只是一个习惯用法,但是你可以使用任何你想用的名字。无论如何,坚持T的惯例可能是个好主意。
因此,t 就像一个占位符,可以在整个类中需要定义类型的地方使用。这可以是字段、局部变量、传递给方法的参数或方法的返回类型。
因此,使用泛型类的调用代码负责通过传递类型参数来定义将在整个类中使用的类型。在我们的例子中,这是代码的VehicleCarrier<Car>部分。
泛型和集合
C# 中的集合管理和组织数据。你肯定知道 List,如果你还记得前面,我们看到 List 是通用的。因此,我们可以考虑如下列表。
public class List<T>
{
public void Add(T listItem);
}
Listing 2-42List of T
我们需要知道何时使用哪个集合来管理我们的数据。如果我们想尽可能提高效率,这是有意义的。下面是泛型集合及其用途的摘要。
列表
List<T>保存数据类型的集合。当达到列表的容量时,它会将容量翻倍以容纳更多数据。因此List<T>可以根据需要增长。
队列
把Queue<T>想象成你站在银行里的一个队列。如果有人在你之后进入银行,但在你之前得到帮助,你可能会有点不安。这是因为你是第一个,而且等待的时间更长。Queue<T>完全一样。它提供了对条目进行排队的Enqueue方法和按照添加顺序删除条目的Dequeue方法。我们称之为先进先出或 FIFO 集合。
堆栈
当想到Stack<T>时,想象一罐品客薯片。打开盖子首先看到的酥脆是最后加入罐头的酥脆。堆栈< T >也是如此,因为它使用后进先出或 LIFO。为此,它公开了方法Push和Pop。你把一个项目推到堆栈上,然后从顶部弹出堆栈。
哈希集
如果您需要一个集合只包含唯一的项目,您可以使用一个HashSet<T>。它将只允许独特的项目。为了做到这一点,无论添加成功与否,Add方法都会返回一个true或一个false。一个HashSet<T>可以很好地处理值类型。然而,它对于对象和引用类型来说并不太好,除非您创建一个对象的实例并添加它。
链接列表
LinkedList<T>将会给你更多的控制来管理链表中的条目。它通过公开一个Next和一个Previous方法来做到这一点。还提供了AddFirst、AddLast、AddBefore、AddAfter等灵活的插入方式。
字典
这是另一个你可能习惯使用的集合。字典通过使用一个键来提供数据的快速查找。因此,字典有一个Key和一个Value,我们称之为键值对。
分类字典
如果你需要一个有序的数据集合,那么考虑一下SortedDictionary<TKey, TValue>。这个通用集合知道如何对它包含的现成数据进行排序。项目按关键字排序。如果你的键是一个字符串,那么它将按字母顺序排列你的数据。如果你经常查阅资料,你需要使用分类词典。它针对数据的添加和删除进行了优化。
排
如果您需要一个高效的通用集合,它还提供存储在其中的排序项,那么可以考虑使用一个SortedList<TKey, TValue>。排序列表被优化以使用尽可能少的内存。
排序集
如果您需要一个只允许唯一项目的排序集合,您将需要使用一个SortedSet<T>。像我们之前看到的HashSet<T>一样,它只允许唯一的条目,但是是按顺序排序的。
通用接口
泛型还允许你创建泛型接口。您会记得在关于接口的章节中,我们创建了一个IComparable通用接口。这一次,我们将创建一个接口来定义VehicleCarrier类的功能。如果我们需要创建在功能上略有不同的其他类型的载体,这是很有用的。
想象一下,我们需要一个可以动态添加车辆并且没有固定容量的车辆承运商。基于上一节关于泛型和集合的内容,您可能记得List<T>可以在这里帮助我们。我们的通用接口将如下所示。
public interface ICarrier<T>
{
void AddVehicle(T value);
void GetAllVehicles();
}
Listing 2-43Generic ICarrier interface
您会注意到泛型接口也接受泛型类型参数。这里我们说这个接口必须要求任何实现它的类有一个GetAllVehicles方法和一个接受值T的AddVehicle方法。现在我们能够修改现有的VehicleCarrier类来实现ICarrier<T>。
public class VehicleCarrier<T> : ICarrier<T>
{
}
Listing 2-44Modifying VehicleCarrier class
我们还可以创建一个新的 DynamicCarrier 类,随着更多车辆的加入,该类将调整其容量。考虑下面的代码。
public class DynamicCarrier<T> : ICarrier<T>
{
private List<T> _loadbay;
public DynamicCarrier()
{
_loadbay = new List<T>();
}
public void AddVehicle(T vehicle)
{
_loadbay.Add(vehicle);
}
public void GetAllVehicles()
{
foreach (T vehicle in _loadbay)
{
switch (vehicle)
{
case Car car:
Console.WriteLine($"{car.GetType().Name} with VIN number {car.VinNumber} loaded");
break;
case SUV suv:
Console.WriteLine($"{suv.GetType().Name} with VIN number {suv.VinNumber} loaded");
break;
default:
Console.WriteLine($"Vehicle not determined");
break;
}
}
}
}
Listing 2-45DynamicCarrier<T> class implements ICarrier<T>
因为DynamicCarrier<T>实现了ICarrier<T>,所以它必须有AddVehicle和GetAllVehicles方法。我现在可以自由地向所有实现ICarrier<T>的类添加逻辑,只需简单地添加到接口本身。虽然VehicleCarrier<T>和DynamicCarrier<T>都服务于相同的目的(运输车辆),但其中包含的逻辑却大不相同。
有关接口的概述,请参考本章开头的接口部分。
可空类型
在 C# 中,所有引用类型(如字符串和程序定义的对象)都是可空的。实际上,null是引用类型变量的默认值。这意味着虽然它们可以是null,但我们实际上需要将null关键字视为表示null引用的文字。换句话说,不引用。NET 框架。
随着 C# 2.0 的发布,我们引入了可空值类型。如果你看一下System.Nullable名称空间(图 2-10 ,你会注意到我们在这里处理的是一个泛型类型。
图 2-10。
系统。可空
这意味着我们现在可以创建一个Nullable<int>并将从MinValue到MaxValue的任意整数值赋给它,包括null。其余的值类型也是如此。
可空类型的一些特征
当我们谈论 C# 中的可空类型时,以下是正确的:
-
因为引用类型已经支持 null,所以可为 null 的类型只适用于值类型。
-
Nullable<T>也可以简称为T? -
因为值类型可以为空,所以可以使用
HasValuereadonly 属性来测试null,然后使用 readonlyValue属性来获取它的值。 -
您可以对可空类型使用
==和!=运算符。 -
C# 7.0 允许使用模式匹配来检查
null并获取值。 -
您可以使用 null-coalescing 操作符来检查
null,如果是null,则为底层类型赋值。
虽然我们已经定义了什么是可空类型,但是我们到底如何使用它们呢?更重要的是,我们为什么要使用它们?有时你可能会期望在某些情况下将null赋值给值类型。能够将值类型定义为可空允许您编写更好、更安全的代码。考虑下面的代码清单。
使用可空类型
在下图中(图 2-11 ,你会看到我可以给iValue整数赋值,也可以给可空的iValue2整数赋值。试图将null赋给iValue3整数给我一个编译器错误。
图 2-11。
iValue4 可空类型允许空值
当使用 int 的可空值类型时,请考虑以下逻辑。它检查iValue2变量是否有值,如果有,将值赋给变量iValue。
// Valid code
int iValue = 10;
int? iValue2 = null;
if (iValue2.HasValue)
{
iValue = iValue2.Value;
}
else
{
iValue = -1;
}
Listing 2-46Checking a nullable type with HasValue
在前面的代码清单中,控制台应用将返回一个-1,因为iValue2变量的值是null。使用零合并操作符,我们可以通过编写如下代码块来极大地简化代码。
int? iValue2 = null;
int iValue = iValue2 ?? -1;
Listing 2-47Using a null-coalescing operator
多时髦啊。我们的代码已经减少到两行代码,它做的事情与清单 2-46 中的完全一样。有了 C# 7.0,我们现在也能使用模式匹配了。因此,我们可以做到以下几点。
int iValue = 10;
int? iValue2 = null;
if (iValue2 is int value)
{
iValue = value;
}
else
{
iValue = -1;
}
Listing 2-48Use pattern matching
如果变量iValue2是null(在本例中是这样的),应用将返回-1。然而,如果该值不是null,变量iValue将被设置为iValue2的值。
窥视可空的内部
在前面的章节中,我们已经了解了Nullable<T>的一些特征以及如何使用Nullable<T>。但究竟是什么让它(因为找不到更好的词)运转呢?
在引擎盖下窥视,我们看到Nullable<T>是一个struct(图 2-12 )。我们还看到了之前讨论过的预期的HasValue和Value属性。
图 2-12。
可空的引擎盖下
此外,你会注意到我们在与 LINQ 合作时经常看到的GetValueOrDefault方法。从图 2-12 中的图像,你会注意到这是一个重载的方法。
您可以检索当前Nullable<T>对象的值,或者如果我的Nullable<T>对象确实是null,您可以提供一个默认值。但是如果Nullable<T>对象为空,但是您没有提供默认值,会发生什么呢?
在这种情况下,将返回基础类型的默认值。为了演示这一点,请考虑下面的代码。
int iValue = 10;
int? iValue2 = null;
iValue = iValue2.GetValueOrDefault(-1);
WriteLine($"The value of iValue = {iValue}");
Listing 2-49GetValueOrDefault
清单 2-49 中的代码将返回我们提供的默认值-1。如果Nullable<T>对象确实是null,我们将为其提供需要返回的默认值。现在删除默认值并再次运行代码。
int iValue = 10;
int? iValue2 = null;
iValue = iValue2.GetValueOrDefault();
WriteLine($"The value of iValue = {iValue}");
Listing 2-50Default value of the underlying type
清单 2-50 中的代码将返回底层类型的默认值。因为基础类型是整数,所以默认值为 0。表 2-2 显示了值类型的默认值。
表 2-2。
值类型的默认值
|默认
|
值类型
| | --- | --- | | Zero | int,byte,sbyte,short,uint,ulong,ushort | | 错误的 | 弯曲件 | | '\0' | 茶 | | 0M | 小数 | | 0.0D | 两倍 | | 0.0F | 漂浮物 | | 0L | 长的 |
通过将所有值类型字段设置为该特定类型的默认值并将所有引用类型字段设置为null来产生struct的默认值。
从 C# 7.1 开始,您可以使用default文字表达式用特定于其类型的默认值初始化变量。
bool? blnValue = default;
int? iVal = default;
double? dblValue = default;
decimal? decVal = default;
WriteLine($"The default values are " +
$"- blnValue = {blnValue.GetValueOrDefault()} " +
$"- iVal = {iVal.GetValueOrDefault()} " +
$"- dblValue = {dblValue.GetValueOrDefault()} " +
$"- decVal = {decVal.GetValueOrDefault()}");
ReadLine();
Listing 2-51Using the default literal
作为开发人员,在 C# 中使用可空类型无疑会给你带来一些好处。能够为基础类型提供默认值也使得避免意外变得非常容易。当处理来自数据库的数据时尤其如此。
动态类型
随着 C# 4.0 的发布,开发人员被引入了一种新的dynamic类型。它是静态类型,但是dynamic对象绕过了静态类型检查。想象它有一个类型object。最好用一些代码示例来解释。
dynamic dObject = "I am dynamic";
WriteLine($"dObject = {dObject}");
dObject = 1;
WriteLine($"dObject = {dObject}");
dObject = false;
WriteLine($"dObject = {dObject}");
dObject = 1.1;
WriteLine($"dObject = {dObject}");
Listing 2-52The dynamic type
编译器在编译时不知道变量是什么类型。在dynamic类型上没有可用的智能感知也是很符合逻辑的。因此,dynamic变量的类型将在运行时确定。清单 2-52 中的代码将产生以下输出。
dObject = I am dynamic
dObject = 1
dObject = False
dObject = 1,1
Listing 2-53Dynamic output
可以想象,模式匹配与dynamic变量配合得相当好。它可以是一个简单的if (dObject is int iValue) {}或更复杂的case陈述。
switch (dObject)
{
case int iObject:
WriteLine($"dObject is an Integer {iObject}");
break;
case bool blnObject:
WriteLine($"dObject is a bool {blnObject}");
break;
case string strObject:
WriteLine($"dObject is a string {strObject}");
break;
case double dblObject:
WriteLine($"dObject is a double {dblObject}");
break;
default:
WriteLine($"dObject type can't be determined");
break;
}
Listing 2-54Pattern matching with dynamic variable
有趣的是,dynamic类型只在编译时存在。在运行时,dynamic类型的变量被编译成object类型的变量。
您可以在中使用动态
-
菲尔茨
-
性能
-
因素
-
返回类型
-
局部变量
您也可以使用dynamic作为转换的目标类型。考虑下面的代码清单。
dynamic dObj;
bool blnFalse = false;
dObj = (dynamic)blnFalse;
WriteLine($"dObj = {dObj}");
Listing 2-55Conversion to dynamic
名为动态语言运行时(DLR) 的新 API 被添加到。NET 框架 4。这个 API 支持 C# 中的dynamic类型,也支持动态编程语言的实现,例如 IronRuby。
包扎
C# 是一种在过去几年中发展很快的语言。对于 C# 7,我们已经看到了更快的点版本,引入了新的特性和改进,您可以在日常开发中使用。
作为一名开发人员,保持与时俱进仍然是一个挑战。微软在 https://docs.microsoft.com 有在线文档形式的极好资源。
这一章永远不会完整,因为 C# 语言中有太多的内容需要介绍。试图在一章中做到这一点的局限性在页数上是显而易见的。我们看了一下抽象类和什么是接口。然后我们看了 async 和 await,以及它们如何帮助您创建响应性应用。我们还通过查看 async 和 wait 创建的状态机,了解了它们是如何神奇地工作的。
然后我举例说明了扩展方法的使用,以及这个特性可以为您的开发做些什么。我们也看到了泛型在 C# 中扮演了一个重要的角色,并且你很可能一直在使用泛型(想想List<T>)。
最后,我们稍微深入地研究了一下Nullable<T>以及它是如何组合在一起的,并对dynamic类型进行了简单的解释。在下一章,我们将看看 C# 8.0 的新特性。