探索-C--高级特性-全-
探索 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?
-
因为值类型可以为空,所以可以使用
HasValue
readonly 属性来测试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 的新特性。
三、C# 8.0 的新特性
C# 的设计过程是开源的。你可以前往位于 https://github.com/dotnet/csharplang
的知识库,看看围绕语言设计的一些讨论。事实上,会议文件非常吸引人。
一旦你进入了 GitHub repo,就可以在 dotnet/csharplang/meetings 上查看按年份组织的文档集合。
第一件让我印象深刻的事情是,围绕 C# 语言的思维是非常结构化和深思熟虑的。在整个存储库中,您会看到最后提交日期总是很近。因此,这证明了您正在查看的存储库是一个动态文档,您可以跟随它并保持更新。
C# 8.0 呢?事实是,即使 C# 团队发布了 C# 7 的增量点版本(C# 7.1 到 C# 7.3),他们也在开发 C# 8.0。
本章将介绍 C# 8.0 的以下新特性:
-
可为空的引用类型
-
递归模式
-
范围和指数
-
切换表达式
-
目标类型的新表达式
-
异步流
-
使用声明
为了按照我将在本章中演示的代码清单编写代码,您需要一份 Visual Studio 2019。在撰写本章时,Visual Studio 2019 预览版(版本 16.0.0 预览版 2.0)已经可供下载。
确保如果使用的是 Visual Studio 2019 的预览版,已经从高级构建设置中选择了 C# 8.0 (beta)(图 3-1 )。为此,右击项目并选择属性。然后选择构建选项卡,然后点击高级按钮。
图 3-1。
高级构建设置
请注意,以下文本中说明的一些功能在 C# 8.0 的预览版和最终版本之间可能会有细微的变化。在写这本书的时候,本章的代码在语法上是正确的。
首先,让我们看看什么是可空引用类型。
可为空的引用类型
如果你回想一下第二章,我们讨论过可空类型。我们说过所有引用类型(比如字符串)都是可空的,引用类型的默认值是null
。随着 C# 2.0 的发布,微软引入了可空值类型。
我不打算重复引用类型和值类型之间的区别。如果你不确定,我会让你自己去阅读。引用类型现在可以为空这一事实(在我看来)是开发人员长期以来所需要的。使引用类型可空背后的思想是帮助开发人员避免NullReferenceException
异常。
你应该还记得上一章的内容,为了将一个变量标记为可空,你需要在声明一个变量时使用类型和?
。例如,int?
代表一个可空的int
。现在你可以对引用类型做同样的事情,比如string?
来声明一个可空的string
。
这一增加的好处是,你现在可以更清楚地表达你的设计意图。我可以说,一些变量可能有值,而另一些必须有值。
启用可为空的引用类型
在 C# 8.0 中,默认情况下不启用此功能。即使您正在创建 C# 8.0 应用,也必须选择可空引用类型特性。打开可空引用类型特性后,所有引用变量声明都将变成不可空的引用类型。因此,在启用可空引用类型时,您需要注意这一点。
即使启用了可空引用类型,Visual Studio 也只会在遇到设置为null
的不可空引用类型时显示警告。
这意味着如果您创建一个引用类型(例如一个string
变量声明)而没有启用可空引用类型,您将不会看到任何警告。请考虑以下情况。
图 3-2。
没有可为空的引用类型警告
图 3-2 中显示的警告是已分配的变量,但从未使用过警告。要在应用中启用可空引用类型特性,需要在源文件中的任意位置添加一个新的 pragma #nullable enable
。这将打开可空引用类型特性。
图 3-3。
可空引用类型已打开
该警告显示在错误列表中(图 3-3 )。如果在现有项目上启用此功能,您可能会遇到一些这样的警告。
pragma #nullable enable
还支持disable
关闭可空引用类型特性。
如果您需要为整个项目启用可空引用类型,请打开您的.csproj
文件并查找LangVersion
元素。
图 3-4。
为项目启用可为空的引用类型
然后你需要在LangVersion
元素后面添加<NullableReferenceTypes>true</NullableReferenceTypes>
,如图 3-4 所示。
概述
概括地说,在 C# 8.0 中,我们现在有可空的引用类型和不可空的引用类型。这些使您能够告诉编译器您使用引用类型变量的确切意图。
为了在 C# 8.0 中启用可空引用类型变量,您需要使用一个新的 pragma #nullable
。编译器将以两种方式之一解释您的意图。这些如下。
引用类型变量不能为空
如果引用类型变量不应该是null
,编译器将强制执行该规则,以确保在不检查变量是否为null
的情况下使用变量是安全的。这意味着变量必须初始化为非空值。因此,变量永远不会被赋予一个null
值。
引用类型可能为空
当我们声明一个可空的引用类型变量时,我们是在告诉编译器变量值有可能是null
。编译器现在将强制执行不同的规则,以确保您已经检查了空引用。因此,您可以用默认的null
来初始化这些变量。
递归模式
递归模式是 C# 的一个受欢迎的补充。你会记得在 C# 7 中,我们看到了模式匹配的引入。C# 8.0 更进一步,允许模式包含其他模式。考虑下面的类。
public class Person
{
public int Age { get; }
public string Name { get; }
public bool RegisteredToVote { get; set; }
public Person(string name, int age, bool registered)
{
Name = name;
Age = age;
RegisteredToVote = registered;
}
}
Listing 3-1Person class
该类包含一个布尔值,表明该人是否已注册投票。递归模式将允许我们通过以下操作提取那些没有注册投票的人。
foreach (var person in personList)
{
if (person is Person { RegisteredToVote: false })
{
WriteLine($"{person.Name} has not registered.");
}
}
Listing 3-2Recursive pattern
我们在这里说的是,如果列表中的一个对象属于类型Person
,并且这个人的属性RegisteredToVote
被设置为false
,那么显示这个人的名字。
图 3-5。
智能感知可用
您还会注意到,如果您需要向模式添加另一个条件,Intellisense 是可用的(图 3-5 )。将以下资格属性添加到您的类中。
public class Person
{
public int Age { get; }
public string Name { get; }
public bool RegisteredToVote { get; set; }
public bool EligibleToVote { get => Age > 18; }
public Person(string name, int age, bool registered)
{
Name = name;
Age = age;
RegisteredToVote = registered;
}
}
Listing 3-3Person class with eligibility property
我们现在可以检查一个人是否没有注册投票,但是只返回那些有资格投票的人。
foreach (var person in personList)
{
if (person is Person { RegisteredToVote: false, EligibleToVote: true })
{
WriteLine($"{person.Name} has not registered.");
}
}
Listing 3-4Returning only eligible people not registered
递归模式允许您更加灵活,并允许更具表现力的代码。
范围和指数
范围和指数是在 2018 年的前几个月设计的。C# 8.0 允许我们对索引数据结构做的是抓取数组、字符串或跨度的一部分。
string[] names = { "Dirk", "Jane", "James", "Albert", "Sally" };
foreach (var name in names)
{
// do something
}
Listing 3-5An array of names
考虑一个标准的名称数组,我们可以像前面的代码清单一样在一个foreach
中迭代这个数组。然而,在 C# 8.0 中,我们现在可以轻松地只取出数组的一部分,如下所示。
string[] names = { "Dirk", "Jane", "James", "Albert", "Sally" };
foreach (var name in names[1..4])
{
// do something
}
Listing 3-6Pulling out a part of the array
这允许我们迭代数组中的一部分名字。1..4 实际上是一个范围表达式。
请注意,前面示例中的端点 4 是排他的,这意味着元素 4 不包含在[1..4].
C# 对数组采用了 C 风格的方法,所以端点的唯一性与这种方法是一致的。这意味着在[1..4],我们想要的切片长度是 4-1 = 3。
需要注意的另一点是,范围表达式不必构成索引操作的一部分。可以用自己的类型Range
拉出来放到自己的变量里。这将允许以下代码有效。
string[] names = { "Dirk", "Jane", "James", "Albert", "Sally" };
Range range = 1..4;
foreach (var name in names[range])
{
// do something
}
Listing 3-7Using the Range type
在前面的代码示例中,范围表达式是一个integer
1..4.事实上,他们不必如此。实际上,它们属于一种叫做Index
的类型。非负整数值转换为Index
。
因为范围表达式的类型是Index
,所以您可以通过使用新的^
操作符来创建一个Index
。
有时新的^
操作员也被称为帽子操作员。当提到^
操作符时,时间会告诉你什么会被粘住。
新的^
运算符表示从末端开始的,因此1..¹
表示从末端开始的 1。因此,您可以拥有以下内容。
string[] names = { "Dirk", "Jane", "James", "Albert", "Sally" };
foreach (var name in names[1..¹])
{
// do something
}
Listing 3-8Using the “from-end” operator
¹
实际上是删除数组末尾的一个元素,返回一个包含中间元素的数组。
-
简
-
詹姆斯
-
艾伯特
有一些开发者认为,用^
来表示从始至终的是令人困惑的,尤其是因为在 regex 中^
从一开始就表示。但是正如 Mads Torgersen(c# 的设计负责人)所评论的,他们决定在使用从开始和从结束算法时遵循 Python。**
**范围表达式可以用几种方式编写。这些解释如下:
-
表达式
..¹
与0..¹
相同 -
表达式
1..
与1..⁰
相同 -
表达式
..
与0..⁰
相同
表达式0..⁰
从头到尾返回数组中的所有内容(例如)。您可以将⁰
视为最右边的元素。
切换表达式
在 C# 7.0 中,我们看到 switch 语句中包含了模式。你应该还记得,我们在第一章中看到了模式匹配。考虑下面的类示例。
public class Human : Species
{
public string Name { get; }
public bool RegisteredToVote { get; set; }
public bool EligibleToVote { get => Age > 18; }
public Human(string name, bool registered)
{
Name = name;
RegisteredToVote = registered;
}
}
public class Mammal : Species
{
public string Name { get; }
public Mammal(string name)
{
Name = name;
}
}
public class Reptile : Species
{
public string Name { get; }
public bool LaysEggs { get; }
public Reptile(string name, bool laysEggs)
{
Name = name;
LaysEggs = laysEggs;
}
}
public class Species
{
public int Age { get; set; }
}
Listing 3-9Class examples
这些类是非常基本的,如果我们想在 switch 语句中使用模式匹配,我们通常会做以下事情。
Species species = new Reptile("Snake", true);
species.Age = 2;
switch (species)
{
case Human h:
WriteLine($"{h.Name} is a {nameof(Human)}");
break;
case Mammal m:
WriteLine($"{m.Name} is a {nameof(Mammal)}");
break;
case Reptile r:
WriteLine($"{r.Name} is a {nameof(Reptile)}");
break;
default:
WriteLine("Species could not be determined");
break;
}
Listing 3-10C# 7.0 switch statement
这是一段有效的代码,但是写起来有些麻烦。在 C# 8.0 中,您将能够重写前面清单中的代码,如下所示。
var result = species switch
{
Human h => $"{h.Name} is a {nameof(Human)}",
Mammal m => $"{m.Name} is a {nameof(Mammal)}",
Reptile r => $"{r.Name} is a {nameof(Reptile)}",
_ => "Species could not be determined"
};
WriteLine(result);
Listing 3-11Switch expression
C# 8.0 引入了开关表达式,其中事例是表达式。可以把它看作 switch 语句的轻量级版本。
您会注意到,default
案例使用了一个丢弃_
变量。如果你需要回顾一下,弃牌在这本书的第一章已经讨论过了。
你会注意到case
关键字和:
已经被λ=>
箭头所取代。另一件要注意的事情是,主体现在是一个表达式,并且选定的主体成为开关表达式的结果。
我应该使用开关表达式吗?
就我个人而言,我发现开关表达式更好读和写,尤其是如图 3-6 所示的格式。更集中和简洁的代码的结果是显而易见的,我们将 15 行 case 语句减少到只有 7 行代码。
图 3-6。
更可读的代码
如果您希望使用更少的代码编写开关,并且更具表达性,请考虑使用开关表达式。
属性模式
让我们扩展我们的开关表达式,来区分产卵的爬行动物和产下幼仔的爬行动物。
是的,你会看到胎生的蛇,它们会生出活的幼蛇,例如绿色水蟒和大蟒蛇。
在switch
语句中包含一个案例,该案例将检查爬行动物的属性LayEggs
何时等于true
,并基于此输出不同的结果。
图 3-7。
检查胎生爬行动物
C# 8.0 现在允许模式更深入地挖掘模式匹配的值。这意味着作为开发人员,您可以通过添加花括号将其应用于值的属性或字段,从而使其成为属性模式。因此,您可以将图 3-7 中的开关表达式重写如下。
图 3-8。
使用属性模式切换表达式
C# 8.0 也允许更多可选的类型模式元素。如果我们在和一只产卵的Reptile
打交道,那么我们想要它的年龄。这里我们可以将var
模式应用于Age
属性。
图 3-9。
省略爬行动物 r
记住var
总是会成功,并声明一个新变量来保存该值(图 3-9 )。因此,变量age
开始包含r.Age
的值,我们可以删除r
,因为它从未被使用过(图 3-10 )。
图 3-10。
r 可以被丢弃,因为它没有被使用
包括属性模式在内的所有模式都要求该值非空。用{}
和null
替换回退情况将处理非空模式和空值(图 3-11 )。
图 3-11。
迎合非空对象和空
空属性模式由{}
处理,而null
将捕捉所有的空值。
目标类型的新表达式
微软已经走了很长一段路,从他们所在的地方开始拥抱开发者社区。C# 8.0 中引入的以下特性完美地展示了开发人员的思维过程以及他们对开发人员社区的意义。
这个特性的实现实际上是由社区成员 Alireza Habibi 贡献的。
例如,在过去,当创建一个数组Point
时,您需要添加类型。
Point[] ps = { new Point(1, 4), new Point(3, 2), new Point(9, 5) };
Listing 3-12Point array before C# 8.0
在 C# 8.0 中,您现在可以简单地修改前面清单中的代码,如下所示。
Point[] ps = { new (1, 4), new (3, 2), new (9, 5) };
Listing 3-13Point array in C# 8.0
该类型已从上下文中给定。因此,在这些情况下,C# 将允许您省略类型。
异步流
让我们回想一下第二章中讨论的异步。异步编程将允许您编写能够执行长时间运行任务的代码,同时仍然保持应用的响应性。
基本的想法是我们把这些东西叫做任务。NET,它代表着对未来结果的承诺。我们可能有一个如下的异步方法。
static async Task Main(string[] args)
{
var result = await GetSomethingAsync();
WriteLine(result);
ReadLine();
}
static async Task<int> GetSomethingAsync()
{
await Task.Delay(1000);
return 0;
}
Listing 3-14Async method
你会注意到我用 async 修饰符创建了Main
方法。
Async Main 是在 C# 7.1 中引入的,现在它允许你用async
修饰符为你的应用创建入口点。如果你的程序返回一个退出代码,你可以声明一个返回一个Task<int>
的Main
方法。
这里需要注意的是await
操作符。这允许您在代码的执行中插入一个暂停点,直到等待的任务完成它正在处理的任务。因此,这个任务代表了一些正在进行的工作,并且只有在用async
关键字修改方法时才能使用await
。
我们称这样一个包含一个或几个await
表达式的方法(使用async
修饰符)为异步方法。前面清单中的代码对于单个结果很好,但是对于连续的结果流呢?
设想一个数据库,它被查询的数据不能一次全部返回。所以,它需要对它进行流式处理,数据会以一定的间隔到达调用代码。但是,您的代码希望在自己的时间内处理这些数据。正是因为这个原因,C# 8.0 引入了IAsyncEnumerable<T>
,它是IEnumerable<T>
的异步版本。这样,您就可以编写下面的代码了。
static IAsyncEnumerable<int> GetLotsAsync()
{
await foreach(var item in GetSomethingAsync())
{
if (item > 8)
yield return item;
}
}
Listing 3-15IAsyncEnumerable<T>
代码执行普通的await
,但是您可以使用普通的语言结构(例如,foreach
)来消费数据。
当到达迭代器方法内部的yield return
时,expression
被返回并保留代码中的当前位置。如果再次运行这段代码,那么下次调用迭代器时,代码将从该位置重新开始执行。要结束迭代,请调用yield break
。
可以把它想象成一个async
迭代器,它结合了async
方法和迭代器方法,允许你在其中使用await
和yield return
。
可观测量与异步流
在与 Mads Torgersen 的一次访谈中,有人提到异步流感觉类似于可观察的或反应式的扩展。Mads Torgersen 解释说,异步流基本上是一种拉模型,在这种模型中,作为开发人员,你需要一些东西,然后得到它。另一方面,当观察对象有数据时,它们使用推模型。
有了 observables,生产者决定了数据传递给消费者的时间。在异步流中,消费者决定何时准备好接收数据。
使用声明
C# 8.0 的另一个好的补充是简化使用语句的特性。传统上,using 语句引入了一定程度的嵌套。就我个人而言,我喜欢它,因为它总是让人觉得 using 语句清楚地显示了资源何时被清理。当代码执行越过右花括号时,就会发生这种情况。
然而,对于简单的情况,我们现在在 C# 8.0 中有了using
声明。考虑下面的代码清单,它在使用 SQL 连接时有一个using
语句。
string tsql = "[SQL QRY]";
string sqlConnStr = "[SQL Connection String]";
using (var con = new SqlConnection(sqlConnStr))
{
SqlCommand cmd = new SqlCommand(tsql, con);
//..
}
Listing 3-16using statement pre-C# 8.0
using
语句将清理连接等。一旦代码执行移出 using 块。然而,使用 C# 8.0,我们可以做到以下几点。
string tsql = "[SQL QRY]";
string sqlConnStr = "[SQL Connection String]";
using var con = new SqlConnection(sqlConnStr);
SqlCommand cmd = new SqlCommand(tsql, con);
Listing 3-17Using declaration in C# 8.0
using
声明只是局部变量声明。唯一不同的是,它现在前面有一个using
关键字。因此,内容在当前语句块的末尾被释放。
包扎
C# 8.0 引入的语言特性确实令人兴奋。我确信随着时间的推移,C# 团队将会完善这些并添加更多的内容。另一个激动人心的发展是 C# 中点释放的速度。这在我们在 C# 7 中看到的点发布中是显而易见的。这是一个好主意,以保持与这些版本的更新,以防他们决定偷偷在一些非常酷的东西。
我们看了一下现在可用的可空引用类型,例如,它允许您使用string?
来指示字符串的可空性。然后我们看了一下允许模式包含其他模式的递归模式。接下来讨论了范围和索引,它们允许您抓取数组、字符串或跨度的一部分。然后,我向您展示了 switch 表达式是如何工作的,这可以看作是轻量级的 switch 语句。目标类型的新表达式允许您在创建一个Point
数组时省略类型,因为类型是从上下文中给定的。然后讨论了异步流,它允许你使用一个叫做IAsyncEnumerable
的IEnumerable
的异步版本。最后,我们看了一下通过不引入嵌套层来简化语句使用的声明。
在下一章,我们将看看如何使用 ASP.NET MVC、Bootstrap、jQuery 和 SCSS 创建响应式 web 应用。**
四、使用 ASP.NET MVC 的响应式 Web 应用
响应式 web 应用在现代应用开发中至关重要。用户需要能够在任何设备上查看您的 web 应用的内容。这意味着 web 应用需要根据查看它的设备来调整自己的大小。
在这一章中,你将创建一个简单的任务管理系统,它使用引导代码框架来保持响应。我们将了解以下内容:
-
创建您的 ASP.NET MVC 应用
-
引用 jQuery 和引导
-
设置和使用 SCSS
-
创建模型、控制器、视图和使用 Razor
-
添加插件
-
使用 Chrome 测试你的响应式布局
-
使用 Chrome 开发工具调试 jQuery
我将使用编写本章时可用的最新版本的 Visual Studio 2019。
创建您的 ASP.NET MVC 应用
Visual Studio 2019 中的新开始窗口看起来确实有点不同。你会注意到它现在有五个主要部分。这些是
-
打开最近的
-
克隆或签出代码
-
打开项目或解决方案
-
打开本地文件夹
-
创建新项目
将您最近的项目放在左侧,锁定或取消锁定是非常方便的,这将告诉您一些关于新开始窗口的信息。
我将在后面的章节中更深入地介绍 Visual Studio 2019 的这一功能和其他新功能,请密切关注。
Visual Studio 团队使用新的“开始”窗口的目的是让您能够快速访问访问代码的最常用方式。对大多数人来说,这可能是从一个存储库克隆或者打开一个现有的项目,如图 4-1 所示。
图 4-1。
Visual Studio 2019 新项目屏幕
现在,您将创建一个新的项目,因此单击该选项即可开始。
图 4-2。
选择项目模板
创建新项目可能是您非常熟悉的事情。新项目对话框(如图 4-2 所示)已经被清理了一点,不再包括节点和子节点的目录样式。
它现在包括一个最近的项目模板部分,类似于开始窗口中的打开最近的。对于这个项目,我们将使用选择一个 ASP.NET Web 应用。NET 框架。
图 4-3。
配置您的项目
选择项目模板后,您可以配置您的新项目(图 4-3 )。我们将创建一个简单的任务管理应用,它将管理任务,并根据我们稍后将定义的一些状态对它们进行颜色编码。另请注意,您可以选择。最后一个组合菜单中的. NET Framework 版本。
图 4-4。
选择一个 MVC 项目
接下来您将看到熟悉的项目配置屏幕,在这里您可以选择想要创建的 web 应用的类型(图 4-4 )。在这里选择 MVC,不用担心启用 Docker 支持或添加单元测试。在这个项目中,我们也不需要任何身份验证。
图 4-5。
解决方案资源管理器中创建的项目
Visual Studio 现在继续用所有默认的样板代码创建您的 ASP.NET MVC 应用。完成后,您应该会看到解决方案资源管理器,其中包含如图 4-5 所示的项目。如果您看到这个,那么您就准备好开始创建您的应用了。
构建您的项目并按下 F5 运行您的项目。
图 4-6。
运行您的 ASP.NET MVC 应用
如果一切设置正确,您将看到默认的 web 应用在浏览器中启动。
引用 jQuery 和引导
在你的解决方案浏览器中,如果你展开 App_Start 文件夹,你会看到一个名为BundleConfig
的类。在这里,您将看到对 CSS 和 JavaScript 文件的引用。
捆绑和缩小缩短了请求加载时间。他们通过减少对服务器的请求数量来做到这一点,并通过这样做来减少所请求的资产的大小。
您会注意到,RegisterBundles
方法包含对存储在 Scripts 文件夹中的 jQuery 和引导文件的引用。它还包括内容文件夹中包含的样式表。
public static void RegisterBundles(BundleCollection bundles)
{
bundles.Add(new ScriptBundle("~/bundles/jquery")
.Include("~/Scripts/jquery-{version}.js"));
bundles.Add(new ScriptBundle("~/bundles/jqueryval")
.Include("~/Scripts/jquery.validate*"));
// Use the development version of Modernizr to develop
// with and learn from. Then, when you're ready for
// production, use the build tool at
// https://modernizr.com to pick only the tests you need.
bundles.Add(new ScriptBundle("~/bundles/modernizr")
.Include("~/Scripts/modernizr-*"));
bundles.Add(new ScriptBundle("~/bundles/bootstrap")
.Include("~/Scripts/bootstrap.js"));
bundles.Add(new StyleBundle("~/Content/css")
.Include("~/Content/bootstrap.css",
"~/Content/site.css"));
}
Listing 4-1The BundleConfig class
您会注意到我们有一个用于 js 文件的ScriptBundle
和一个用于 css 文件的StyleBundle
。在这里的ScriptBundle
中,我们将添加另一个对 jquery-ui.min.js 文件的引用。
jQuery UI 是构建在 jQuery JavaScript 库之上的 UI 控件、资产、小部件和主题的集合。如果您需要包含某种形式的用户交互,请使用此选项。
在你的浏览器中,进入 http://jqueryui.com/download/
,在核心、交互、小部件和效果类别中进行选择。
图 4-7。
下载的 jQuery UI 文件
我希望允许用户在网页上拖动元素(特别是任务项)。因此,我只需要包含可拖动的交互,但是我将继续包含所有内容,以防以后需要使用其他交互。
我感兴趣的两个文件是 jquery-ui.js 和 jquery-ui.min.js 。将这两个文件添加到项目的脚本文件夹中。
图 4-8。
添加 jQuery UI 文件
添加完文件后,您需要通过添加一个带有缩小文件路径的Include
来更新BundleConfig
类中的RegisterBundles
方法。
bundles.Add(new ScriptBundle("~/bundles/jquery")
.Include("~/Scripts/jquery-{version}.js")
.Include("~/Scripts/jquery-ui.min.js"));
Listing 4-2Modified RegisterBundles method
这将创建一个名为~/bundles/jquery
的包,它将包含您指定的所有适当的文件以及匹配通配符{version}
字符串的文件。
创建捆绑包
我们可以通过在Include
方法中指定一个字符串数组来创建包。每个字符串都是资源的虚拟路径。下面是一个 StyleBundle 的例子,它指定了几个 CSS 文件的虚拟路径。
bundles.Add(new StyleBundle("~/Content/css").Include(
"~/Content/themes/base/jquery.ui.code.css",
"~/Content/themes/base/jquery.ui.button.css",
"~/Content/themes/base/jquery.ui.slider.css",
"~/Content/themes/base/jquery.ui.tabs.css",
"~/Content/themes/base/jquery.ui.datepicker.css",
"~/Content/themes/base/jquery.ui.theme.css"));
Listing 4-3A StyleBundle
注意所有这些 CSS 文件都在同一个目录中吗?Bundle
类还提供了一个名为IncludeDirectory
的方法。这允许你修改你的StyleBundle
,使其更加简洁。
bundles.Add(new StyleBundle("~/Content/css").IncludeDirectory(
"~/Content/themes/base/"
, "*.css"
,false));
Listing 4-4A StyleBundle using IncludeDirectory
我已经指定了一个虚拟目录路径,还指定了一个只匹配 CSS 文件的搜索模式。设置为false
的最后一个参数指定从搜索中排除子目录。
在视图中引用束
我们将在本章的下一节更仔细地研究视图。然而,我需要在这里提到,在视图中使用Render
方法引用包。对于 CSS 我们使用Styles.Render
,对于 JavaScript 我们使用Scripts.Render
。在 shared _ Layout.cshtml 视图中查看样式表和脚本是如何呈现的。 _Layout.cshtml 视图在所有其他视图之间共享(在旧的 ASP。网)。因此,此处引用的这些脚本和样式表包含在网站的所有页面中。
设置和使用 SCSS
现在我已经引用了 jQuery UI 文件,我想为我的应用创建一个定制样式表。为此,我将创建一个. scss 样式表。在您的项目中创建一个名为 scss 的文件夹,并将一个名为 customstyles.scss 的新 scss 文件添加到该文件夹中。
图 4-9。
添加新的 SCSS 样式表
将文件夹和文件添加到项目后,您的解决方案应该如下图所示。
图 4-10。
添加了 scss 文件夹和自定义样式文件
你会注意到内容文件夹包含了我们的 CSS 文件。从逻辑上讲,这就是我们想要放置 customstyles.css 文件的地方。这个 CSS 文件将从我们在 s CSS 文件夹下创建的 scss 文件中生成。为此,我们需要安装一个由 Mads Kristensen 创建的名为 Web 编译器的工具。前往 Visual Studio 2019 中的扩展菜单,点击扩展和更新。
图 4-11。
扩展和更新
下载工具后,Visual Studio 2019 将安排安装 Web 编译器。
在开始安装 Web 编译器之前,您需要关闭 Visual Studio。
图 4-12。
Web 编译器安装
安装 Web 编译器后,启动 Visual Studio 2019。看看我们之前创建的 customstyles.scss 文件。它只包含以下代码。
body {
}
Listing 4-5Contents of customstyles.scss file
我们稍后将向该文件添加一些样式代码,但首先右键单击该文件,然后单击 Web 编译器➤编译文件或按住 Shift+Alt+Q 将该文件编译成 CSS。
我们之前安装的 Web 编译器开始工作,为我们创建名为 customstyles.css 和 customstyles.min.css 的 CSS 文件。只有一个问题,生成的 CSS 文件不在正确的文件夹中。我们希望生成的 CSS 文件放在项目的 Content 文件夹中。
图 4-13。
生成的 CSS 文件
这很容易解决。当 Web 编译器生成 CSS 文件时,它还会在项目根目录中为您创建一个名为 compilerconfig.json 的文件。继续打开 compilerconfig.json 文件。
[
{
"outputFile": "scss/customstyles.css",
"inputFile": "scss/customstyles.scss"
}
]
Listing 4-6Compiler configuration for the scss file
您会注意到,该文件包含一个为生成的 CSS 文件设置的输出路径。该路径与输入文件路径相同。修改您的outputFile
路径,如下面的代码清单所示。
[
{
"outputFile": "Content/customstyles.css",
"inputFile": "scss/customstyles.scss"
}
]
Listing 4-7Modified Compiler configuration for the scss file
当您保存 compilerconfig.json 文件时,另一个编译会自动完成。
图 4-14。
生成的 CSS 文件
这将在正确的内容文件夹中创建 CSS 文件。你可以删除 scss 文件夹下的 CSS 文件。当我们修改 scss 文件时,这些将永远不会更新。
SCSS 到底是什么?
SCSS 是 SASS(语法上很棒的样式表)的一个实现。事实上,SASS 支持两种类型的语法,即 SCSS 和 SASS。SCSS 和萨斯的主要区别是 SCSS 使用的大括号和分号。习惯了 C#,用 SCSS 更有意义。
SCSS 完全符合 CSS,所以你现有的所有代码仍然可以工作。SCSS 的好处是
-
能够使用变量
-
允许嵌套语法
-
允许使用混合
-
允许使用分部来模块化代码
-
能够使用@extend 来继承和扩展类
-
允许使用函数
这允许您拆分代码来设计应用的样式,并分离应用中关于特定样式的关注点。继续添加另一个名为 _variables.scss 的 scss 文件到你的 scss 文件夹中。请注意,您必须在文件名前包含下划线,以将其标记为部分 scss 文件。
图 4-15。
_variables.scss 文件
将以下代码添加到 _variables.scss 文件中。
/* Header Colors */
$h2-color: #9DB941;
Listing 4-8The color variable for H2 tags
这只是一个为标记中的H2
元素设置值的变量(由一个$
符号表示)。接下来,如下修改您的 customstyles.scss 文件。
@import "_variables.scss";
h2{
color: $h2-color;
}
Listing 4-9Custom styling for H2 elements
这里我们导入了 _variables.scss 部分文件,然后将H2
元素颜色设置为$h2-color
变量的值。保存你的 scss 文件,看看 Content 文件夹里的 customstyles.css 文件。
/* Header Colors */
h2 {
color: #9DB941; }
Listing 4-10The customstyles.css file
编译后的 CSS 包含H2
元素的$h2-color
变量值。这就是 SCSS 为您的 Visual Studio web 应用项目带来的强大功能。
你会注意到 Web 编译器没有创建一个变量. css 文件。这是因为它被标记为部分文件,文件名前带有下划线字符。我们用customstyles.scss
文件中的@import
关键字将它包含在编译后的 CSS 文件中。
将我们的自定义 CSS 文件添加到 BundleConfig
我们需要在BundleConfig
类中包含自定义的 CSS 文件。继续编辑RegisterBundles
方法并包含 customstyles.css 文件。我们的方法目前引用了 site.css 文件。
bundles.Add(new StyleBundle("~/Content/css")
.Include("~/Content/bootstrap.css",
"~/Content/site.css"));
Listing 4-11StyleBundle referencing site.css
通过移除 site.css 引用并添加我们的 customstyles.css 引用,将其改为引用我们的自定义 CSS 文件。
bundles.Add(new StyleBundle("~/Content/css")
.Include("~/Content/bootstrap.css",
"~/Content/customstyles.css"));
Listing 4-12StyleBundle referencing customstyles.css
现在,我们已经成功地引用了样式表,我们将在整个应用中根据需要使用该样式表来设置元素的样式。
创建模型、控制器、视图和使用 Razor
在我们创建视图之前,我们首先需要为我们的任务应用创建一个模型和一个控制器。MVC 的整个前提是根据应用每个部分的角色来分离关注点。你可能知道,MVC 代表 M 模型、 V 视图、 C 控制器。让我们回顾一下 MVC 每个部分的职责。
什么是控制器?
当用户向浏览器发出请求时,控制器决定向用户返回什么响应。它负责控制 ASP.NET MVC 应用中的逻辑流。您会注意到我们的应用默认包含一个 HomeController 。它仅仅是一个 C# 类,最初包含一些名为Index
、About
和Contact
的方法。如果您必须输入 URL Home/Index ,那么控制器将调用Index
方法。在这里,您可以添加额外的方法(或操作)来匹配您的视图。
什么是视图?
如果你看一下 HomeController ,你会注意到每个方法都返回一个视图。在您的解决方案浏览器中展开视图文件夹,您会注意到它包含一个 Home 文件夹,其中有三个视图匹配 HomeController 类中的方法。因此,当 URL Home/Index 请求Index
方法时, HomeController 将寻找名为 Index 的视图。因此,在正确的位置创建视图非常重要。调用 Home/Index 将查找位于Views \ Home \ Index . cs html的 Index 视图。这些视图包含网页的标记。
什么是模型?
模型也只是一个 C# 类,包含应用的所有业务逻辑、任何需要的验证以及所有数据库逻辑。例如,使用实体框架作为数据库,它的逻辑将包含在 Models 文件夹中。这意味着您的视图必须只包含在网页中显示数据所需的代码。您的控制器必须只包含最少量的代码,以便选择正确的视图并将用户重定向到其他操作。模型应该包含代码逻辑的其余部分。一个通用的经验法则是,如果您的控制器变得太复杂或者包含大量代码,那么您需要考虑将该逻辑转移到一个模型中。在大多数情况下,你应该争取瘦控制器和脂肪模型。
什么是路由?
你们当中来自 ASP.NET 的人会记得,创建一个 ASP.NET 网页意味着你需要在用户输入的 URL 和被请求的页面之间有一对一的匹配。我的意思是,如果用户请求一个名为 DisplayTasks.aspx 的页面,该页面必须存在。
在 ASP.NET MVC,这不是真的。用户键入的 URL 与应用中的文件不对应。使用 MVC,用户输入的 URL 与控制器中的动作(前面提到的方法之一)相匹配。在我们应用的 HomeController 中,我们有动作索引、关于,以及联系人。
图 4-16。
MVC 设计模式
这种浏览器请求到控制器动作的映射在 ASP.NET MVC 中被称为路由。传入的请求被路由到控制器动作。这意味着如果用户请求 Home/Contact ,那么 HomeController 上的 Contact 动作将会运行。这也不意味着返回了联系人视图。还记得我们说过控制器的工作是决定应用中的逻辑流程吗?您可以有不同的联系人视图,控制器将根据某种逻辑(例如,原籍国)决定返回哪个视图。如果原籍国的母语不是英语,则控制器可以用不同的联系方式和不同的语言返回不同的视图。
路由的工作原理
ASP.NET 通过应用首次启动时创建的路由表处理传入的请求。您可以在项目根目录下的 Global.asax.cs 文件中看到这一点。
图 4-17。
路由表创建
它是在一个名为Application_Start
的方法中创建的,您还会注意到这也是包注册的地方。
您应该还记得我们在上一节中说过,捆绑和缩小可以改善请求加载时间。
如果你看一下 App_Start 文件夹中的 RouteConfig.cs 文件,你会看到我们的路由表只包含一条默认路由。
图 4-18。
RegisterRoutes 方法
所有传入的请求都被分成三个部分。您会注意到这些片段是正斜杠之间的部分。
图 4-19。
路线段
默认路线还为您的应用提供了三个路段的默认值。这意味着,默认情况下,当您的应用启动时,它将转到默认的 Home/Index 路径。第三部分id
被标记为可选。
如果您必须在您的任务/显示浏览器中输入一个 URL,那么基于您的默认路线的组成,您将需要一个名为TaskController
的控制器,它包含一个名为Display
的动作(方法)。简而言之,这就是路由的工作方式。
创建您的模型
让我们开始添加任务应用的内容。我们将从添加模型开始,然后创建控制器,最后设计视图。这将给我们一个可行的应用,我们可以扩展它来满足设计规范的需要。
如果您的解决方案中没有名为 Models 的文件夹,那么创建一个,并在该文件夹中创建一个名为 Task 的类。
图 4-20。
创建任务模型
当您创建了您的任务模型后,将下面的代码添加到您的模型中。
public class Task
{
public int TaskID { get; set; }
public string TaskTitle { get; set; }
public string TaskBody { get; set; }
public DateTime DueDate { get; set; }
}
Listing 4-13The Task model code
我们将通过在一个名为GetTasks
的方法中插入数据来模拟数据库查询,该方法返回一个List<Task>
对象。将以下代码添加到您的任务模型中。
public List<Task> GetTasks()
{
return new List<Task>()
{
new Task ()
{
TaskID = 1
, TaskTitle = "Review MVC tutorials"
, TaskBody = "Make some time to view MVA videos"
, DueDate = DateTime.Now
},
new Task ()
{
TaskID = 2
, TaskTitle = "Create Test Project"
, TaskBody = "Create a test project for demo at work"
, DueDate = DateTime.Now.AddDays(1)
},
new Task ()
{
TaskID = 3
, TaskTitle = "Lunch with Mary"
, TaskBody = "Remember to make lunch reservations"
, DueDate = DateTime.Now.AddDays(2)
},
new Task ()
{
TaskID = 4
, TaskTitle = "Car Service"
, TaskBody = "Have the car serviced before trip to HQ"
, DueDate = DateTime.Now.AddDays(3)
}
};
}
Listing 4-14GetTasks method
现在,我们将依靠这个方法返回我们的Task
对象,就好像它们是从数据库中读取的一样。我们现在需要添加负责我们任务的控制器。让我们接下来做那件事。
创建控制器
如果您展开控制器文件夹,您将会看到默认的HomeController
,这是在我们创建应用时为我们添加的。我们所有的控制器都将存放在这个控制器文件夹中。右键单击文件夹并为任务添加新的控制器。现在,只需选择添加一个空控制器。按照控制器的 MVC 惯例调用类TaskController
,并单击添加按钮。
namespace Tasker.Controllers
{
public class TaskController : Controller
{
// GET: Task
public ActionResult Index()
{
return View();
}
}
}
Listing 4-15The default TaskController code
控制器是用Index
动作的默认代码创建的。在这一点上,它实际上并没有做太多,但是它是一个很好的脚手架,您可以从它开始工作。
正是在这个搭建过程中,我们看到了一些有趣的事情发生。如果您展开您的视图文件夹,您会注意到一个名为任务的新文件夹。
图 4-21。
创建的模型、视图和控制器
这是 Visual Studio 告诉你,你为你的TaskController
创建的视图应该存在于视图下的任务文件夹中。你还会注意到 MVC 的构造方式,在我看来这是一个非常符合逻辑的方式。TaskController
需要更多一点的代码,我们稍后会谈到。现在,我们的应用需要一个视图来显示来自控制器的数据。让我们现在创建一个。
创建您的视图
为了显示我们从控制器收到的数据,我们将添加一个视图。正是为了这个视图,我们将为我们的应用添加标记。右键点击视图文件夹下的任务文件夹,点击添加,然后点击视图。
图 4-22。
添加视图
我们将把这个视图称为Index
,这样TaskController
就可以正确地映射到这个视图。如果控制器中的动作被称为其他名称,那么这个视图名称必须与控制器中的动作名称相匹配。
我们也不会选择模板,但是如果您愿意,您可以选择。如果您选择了一个模板,那么您需要选择我们之前创建的Task
模型,并将其输入到模型类字段中。
使用以下标记创建了一个非常基本的视图。
@{
ViewBag.Title = "Index";
}
<h2>Index</h2>
Listing 4-16Basic view markup
我们需要稍微扩展一下这段代码,这样我们就可以以一种逻辑的方式显示我们的任务。我们需要对模型中的任务进行计数。如下修改您的代码。
@model IEnumerable<Tasker.Models.Task>
@{
ViewBag.Title = "Index";
var iTaskCount = 0;
iTaskCount = Model.Count();
}
Listing 4-17Modified multi-statement block
您会注意到第一行表明我们的Index
视图被强类型化为我们的Task
类。Razor 视图引擎现在能够理解Index
视图已经被传递了一个Task
对象。这样做的好处是,我们现在可以访问模型的所有属性。更重要的是,我们可以在网页的标记中使用智能感知来做到这一点。
接下来我们要做的是为我们的视图编写 HTML 代码。在这里,你可以比我更有创造力。我有一个 Trello 类型的页面的想法,在那里你可以自由地在列之间移动任务项。
我们将创建三列,分别称为管道、进行中和已完成。我们的第一列将包含任务,如下所示。
图 4-23。
第一个任务列
如前所述,您可以按照自己喜欢的任何方式设置标记的样式。我选择使用三列布局,所以这就是我所做的。考虑下面的图像。
图 4-24。
任务的索引视图
我使用col-md-4
创建了三列,并且只在第一列添加了 Razor 逻辑。
我假设对 Bootstrap 有一些最起码的了解。如果您想了解更多关于引导网格布局的信息,请在 https://getbootstrap.com
上搜索引导网格示例。
我还为其中一个名为inprogress
的元素添加了一个惟一的 ID。当我们在网页上添加一些脚本时,我们将需要它。最后,我添加了 Razor 语法。让我们详细介绍一下它是什么以及它是如何工作的。
剃刀是什么?
我想在这里暂停一下,解释一下剃刀是什么。它基于 C# 语言,但也支持 Visual Basic。它是一种编程语法,允许您在网页中嵌入基于服务器的代码。从前面的图像中,您可以看到我们留下了一个包含两种内容的页面。这些是客户端内容和服务器代码。客户端内容是您习惯在网页中看到的所有标记。这是所有的 HTML 元素、JavaScript、CSS 和纯文本。
我们的 CSS 将被提取到 SCSS 文件中,该文件将编译成我们的 customstyles CSS 样式表。
使用 Razor 将服务器代码添加到客户机内容之间。服务器代码(顾名思义)在页面发送到浏览器之前运行。这非常强大,因为这意味着您可以根据服务器代码中的条件动态创建客户端内容。考虑以下逻辑。
图 4-25。
使用 Razor 动态创建客户端内容
在这里,您可以看到我们正在根据我们的任务数量动态创建页面标题。
如果你的标题没有显示,考虑暂时删除位于项目的共享文件夹中的_Layout.cshtml
文件中定义的navbar
元素。
我们将任务计数存储在一个名为iTaskCount
的变量中。这个变量很容易在我们的页面中使用。我们可以将它混合在 HTML 语法和其他 Razor 语法之间。当变量单独使用时,必须在变量前加上@
符号。
剃刀怎么写
以下是在网页中使用 Razor 语法的真实情况。当你想在页面上添加 Razor 代码时,你需要使用@
字符。@
字符可用于开始一个内联表达式、多语句块或单个语句块。
@{ var iTotal = 3; }
Listing 4-18Single statement block
这个语句块可以用在网页标记中的任何地方。接下来,如果您需要定义一个内联表达式,您需要执行以下操作。
<h2>You have @iTaskCount Tasks</h2>
Listing 4-19Inline expression
如果您需要在网页中显示变量值,这非常有用。在代码示例中,它用于显示任务的数量。最后,您可以使用多语句块。
@{
ViewBag.Title = "Index";
var iTaskCount = 0;
iTaskCount = Model.Count();
}
Listing 4-20Multi-statement block
这是我们在清单 4-17 中修改的代码。如果您需要在页面中包含几个代码语句,多语句块是一个不错的选择。
请记住,在@{ }
块中,代码语句仍然必须以分号结束。唯一不需要包含分号的时候是添加内联表达式的时候。
Razor 的强大之处在于,它允许您直接在 web 页面上使用变量,并在其他 HTML 标记之间混合使用变量。
将一切联系在一起
在运行我们的任务应用之前,我们需要将我们编写的片段链接在一起。我们已经创建了一个模型、一个控制器和一个视图。
您创建的完整的Index
视图需要包含以下代码。
@model IEnumerable<Tasker.Models.Task>
@{
ViewBag.Title = "Index";
var iTaskCount = 0;
iTaskCount = Model.Count();
}
@if (iTaskCount > 1)
{
<h2>You have @iTaskCount Tasks</h2>
}
else if (iTaskCount > 0)
{
<h2>You only have @iTaskCount Task</h2>
}
else
{
<h2>You have no Tasks</h2>
}
<div class="container">
<div class="row">
<div class="col-md-4 task-pipeline">
<div><h2>Pipeline</h2></div>
@foreach (var item in Model)
{
<div class="task">
<div class="task-id">
@item.TaskID
</div>
<div class="task-title">
@item.TaskTitle
</div>
<div class="task-body">
@item.TaskBody
</div>
<div class="task-date">
@item.DueDate.ToString("MMMM dd, yyyy")
</div>
</div>
}
</div>
<div class="col-md-4 task-in-progress" id="inprogress">
<div><h2>In progress</h2></div>
</div>
<div class="col-md-4 task-completed">
<div><h2>Completed</h2></div>
</div>
</div>
</div>
Listing 4-21The Index view code
让我们回到我们的TaskController
类,修改那里的代码,将Task
模型传递给我们的视图。
public class TaskController : Controller
{
// GET: Task
public ActionResult Index()
{
Task task = new Task();
List<Task> tasks = task.GetTasks();
return View(tasks);
}
}
Listing 4-22Modified TaskController class
接下来,我想告诉我的应用,当我的应用启动时,需要运行我的TaskController
的Index
动作。这将显示我们刚刚完成的Index
视图。为此,我们需要更改默认路由。在你的解决方案中展开 App_Start 文件夹。
图 4-26。
RouteConfig 类
在这里,您将看到以下代码。
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new
{
controller = "Home"
, action = "Index"
, id = UrlParameter.Optional
});
Listing 4-23Default routing
这里我们声明默认控制器是HomeController
,而HomeController
中的默认动作需要是Index
。我们想改变默认值,所以修改您的代码,使用TaskController
作为默认值。
routes.MapRoute(
name: "Default",
url: "{controller}/{action}/{id}",
defaults: new
{
controller = "Task"
, action = "Index"
, id = UrlParameter.Optional
});
Listing 4-24Modified default routing
完成这些之后,就可以运行应用了。按 F5 将启动您的应用,并显示Index
视图。它目前是未样式化的,看起来有点难看,所以我们接下来需要修复它。
添加样式
为了给我们的应用添加一些样式,我们将修改之前添加的 customstyles.scss 文件。您还记得,这个文件被编译成应用中使用的 CSS 文件。编辑 customstyles.scss 文件,并向其中添加以下代码。
.task {
border: 1px solid blue;
border-radius: 5px;
padding: 5px;
margin: 5px;
.task-id {
display: none;
}
.task-title {
font-weight: bold;
}
}
.task-pipeline, .task-in-progress, .task-completed {
min-height: 500px;
}
.task-pipeline {
background-color: powderblue;
}
.task-in-progress {
background-color: thistle;
z-index: -1;
}
.task-completed {
background-color: plum;
z-index: -1;
}
Listing 4-25Custom styling
您会注意到这些是我们在任务条目的Index
视图中添加的类名。保存该文件以确保它可以编译并再次运行您的应用。这一次,您将看到应用了样式,并且页面看起来更好了。
因此,使用这种方法来设计应用是合乎逻辑的。如前一节所述,SCSS 提供了一些您可以使用的非常强大的特性。
添加一些 jQuery
在上一节中,我们向应用添加了 jQuery UI 脚本文件。我这样做是因为我希望允许用户在网页上拖动任务项。要添加代码,打开任务的Index
视图,并向代码添加一个脚本部分。
@section scripts {
<script type="text/javascript">
$(function () {
$(".task").draggable();
});
</script>
}
Listing 4-26Scripts section in Index view
构建您的项目并再次运行它。现在,您可以单击任务项并在网页上拖动它们。
图 4-27。
四处拖动任务项目
这为您的 web 应用打开了一个全新的局面。能够向您的网页添加脚本部分使您能够向您的应用添加现成可用的附加功能。
但是让我们仔细看看我们添加到索引页面的这个@section scripts
块。回到 _Layout.cshtml 页面,向下滚动到页面底部。您将看到下面的代码。
@RenderSection("scripts", required: false)
Listing 4-27Rendering sections
方法RenderSection
、RenderBody
和RenderPage
告诉 ASP.NET 在哪里添加特定的页面元素。您将看到我们已经设置了一个参数来告诉 ASP.NET 脚本部分是可选的。
图 4-28。
部分名称必须匹配
最后,您必须记住在 _Layout.cshtml 共享视图中指定的部分名称必须与在 Index.cshtml 视图中包含您的脚本的部分名称相匹配。如果名称不匹配,您将在运行应用时收到一个错误。
包扎
本节采取了一种迂回的方式来解释视图、模型、控制器和 Razor,但是我觉得这样做是必要的,以便给你一个我们正在讨论的更完整的视图(注意双关语)。
添加插件
有时,您可能希望向 web 应用添加额外的功能。当然,你可以自己开发,但是如果功能存在于插件中,为什么要重新发明轮子呢?让我们假设我们想要过滤我们的事件,以便只显示关键任务。关键任务是在 1 天内到期的任务。为了提供这个功能,我们将看看一个名为同位素的插件,它可以从 https://isotope.metafizzy.co/
获得。
安装同位素
该插件允许您提供过滤和排序,以及为您的项目指定布局模式。它尤其适用于项目组。假设您有一个显示博客文章的页面。你可能想按日期或类型(文章、播客、视频等)过滤这些内容。).也许你需要为你的项目指定一个特殊的布局。这就是同位素真正能够提供你所需要的功能的地方。
在添加同位素之前,我想分离出我的div
列中的Task
项。我还需要提供一些东西来触发过滤器。为此,我将只添加两个按钮。这意味着我需要修改我的页面标记以及 customstyles.scss 文件。
我们需要做到以下几点:
-
添加两个按钮来筛选关键任务和原始任务。
-
将列标题移到单独的行中。
-
为标题指定新的 CSS 类。
-
修改 customstyles.scss 文件以设置标题样式。
说明这些对索引视图的改变的最简单的方法是在一个图形中总结它们。您将看到添加的按钮是标准的引导按钮。每个按钮都有一个 ID,因此我们可以在 jQuery 中将一个 click 事件附加到这些按钮上,以过滤我们的任务。
我已经将标题移到了它们自己的行中,因为我希望任务项在单独的div
中。最后,我在标题中添加了三个新的 CSS 类。这些是
-
任务管道标题
-
进行中的任务标题
-
任务完成标题
这些允许我专门针对标题,并对它们应用样式。修改你的索引视图,看起来如图 4-29 所示。
图 4-29。
修改的 HTML
接下来我们要做的是更改 customstyles.scss 文件,以适应标题的新类。我会让它非常简单,只是让颜色相同。
我们只需将新的类名添加到实现背景色的现有类中就可以做到这一点。在 scss 中,我们可以“链接”将相同样式应用于页面元素的类。您会看到类名由逗号分隔(参见task-pipeline
和task-pipeline-heading
)。
.task {
border: 1px solid blue;
border-radius: 5px;
padding: 5px;
margin: 5px;
.task-id {
display: none;
}
.task-title {
font-weight: bold;
}
}
.task-pipeline, .task-in-progress, .task-completed {
min-height: 500px;
}
.task-pipeline, .task-pipeline-heading {
background-color: powderblue;
}
.task-in-progress, .task-in-progress-heading {
background-color: thistle;
z-index: -1;
}
.task-completed, .task-completed-heading {
background-color: plum;
z-index: -1;
}
Listing 4-28Modified customstyles.scss
一旦我们做到了这一点,我们需要一种识别关键任务的方法。如前所述,关键任务应在 1 天内完成。这里我们可以使用 Razor 执行一些条件逻辑来创建动态客户端代码。我们将在这里动态生成的客户端代码是 CSS 类。
关键任务将添加一个类别critical
。这将允许同位素插件识别我们想要过滤的项目。
请注意,您过滤的类别可以是您想要的任何类别。它可以是日期、名称、颜色、类型或您需要的任何其他分类。您将告诉 Isotope jQuery 中的过滤器是什么。
在索引页面的foreach
循环中,我们将添加一个条件,如果任务项在一天之内到期,则需要将其归类为critical
。如果没有,它只是不添加类。按如下方式修改 foreach 循环。
@foreach (var item in Model)
{
<div class="task @(item.DueDate <= DateTime.Now.AddDays(1) ? "critical" : "")">
<div class="task-id">
@item.TaskID
</div>
<div class="task-title">
@item.TaskTitle
</div>
<div class="task-body">
@item.TaskBody
</div>
<div class="task-date">
@item.DueDate.ToString("MMMM dd, yyyy")
</div>
</div>
}
Listing 4-29Modified foreach loop
这个逻辑的关键在于给某些任务项增加了critical
分类。既然我们已经添加了正确设置任务类型和分类所需的代码,我们需要添加同位素插件。转到同位素网站,下载同位素. pkgd.min.js 文件。将这个文件添加到你的脚本文件夹中。
接下来,修改BundleConfig
类的RegisterBundles
方法,为同位素增加一个ScriptBundle
。
bundles.Add(new ScriptBundle("~/bundles/isotope")
.Include("~/Scripts/isotope.pkgd.min.js"));
Listing 4-30Adding Isotope ScriptBundle
最后,要在应用运行时添加这个包,需要修改 _Layout.cshtml 文件。就在 Bootstrap 的@Scripts.Render
下面,修改您的代码以包含同位素包。
图 4-30。
添加同位素包
我们现在准备向任务的索引视图添加一点 jQuery。
让同位素发挥作用
我们将在文档就绪部分添加一些 jQuery。在 jQuery 中,我们可以通过简单地输入$(function() { //code });
来使用传统$(document).ready(function(){ //code });
的简写代码。因此,我们的script
部分将如下所示。
<script type="text/javascript">
var $grid;
$(function () {
$(".task").draggable();
$grid = $('.task-pipeline').isotope({
// options
itemSelector: '.task'
});
$("#btn-order-default").click(function () {
$grid = $grid.isotope({ filter: '*' });
});
$("#btn-order-name").click(function () {
$grid = $grid.isotope({ filter: '.critical' });
});
});
</script>
Listing 4-31Modified script section
代码一开始可能看起来有点混乱,但是一旦我们把它分解成功能部分,就很容易理解了。
图 4-31。
同位素逻辑
我们需要为同位素网格指定一个容器。这是包含我们的Task
项的div
的类。包含我们的Task
项目的div
类是.task-pipeline
类。
接下来,我们需要告诉同位素它将包含的每个项目类是什么。在我们的标记中,我们的.task-pipeline
div 包含多个.task
类 div。告诉同位素我们的包含类和项目类本质上是什么,实例化同位素网格。
我称之为网格,因为从逻辑上讲,这对我有意义。
然后我需要为我的两个按钮添加点击事件。第一个按钮#btn-order-default
将告诉同位素网格根据它包含的所有项目进行过滤。
您会注意到一些元素是通过类名来引用的(例如,.task
和.task-pipeline
),而另一些元素是通过它们的 id 来引用的(例如,#btn-order-default
)。在 jQuery 中,如果引用元素的 ID,可以使用#[ID]符号。如果您引用该类,请使用句点[classname]。
第二个按钮#btn-order-name
将只显示也有critical
类的.task
项。查看生成的 HTML,我们可以看到只有两个任务被标记为关键任务。
图 4-32。
生成的 HTML
运行您的应用,您将看到显示了四个任务项。如果您单击关键任务按钮,您将看到项目过滤器,仅显示关键任务。
图 4-33。
按关键任务筛选
当您点击原始按钮时,任务列表被重置并显示所有任务。
图 4-34。
显示的原始任务
Isotope 插件为您的 web 应用提供了丰富的附加功能。在这里,我们只看了过滤,但它在排序项目方面同样出色,甚至有特定的方式为您的项目提供流畅的布局。
一般来说,这就是插件的力量。您可以通过使用支持良好、设计良好的插件来为您的 web 应用添加功能,这些插件可以省去您自己编写功能代码的麻烦。
使用 Chrome 测试你的响应式布局
谷歌 Chrome 浏览器无疑已经成为当今世界上最受欢迎的浏览器之一。能够用扩展向浏览器添加功能的能力允许用户将它变成他们自己的。对于开发者来说,它还以 Chrome 的开发者工具的形式提供了许多功能。在这一节中,我们将看到它的一部分,称为设备工具栏。这有助于开发人员跨多种设备测试 web 应用布局的响应性。
从开发人员工具开始
要开始使用开发者工具,按住 Ctrl+Shift+I 或右键单击您的网页并从上下文菜单中选择 Inspect 。
图 4-35。
Chrome DevTools(铬 DevTools)
在左上角,您将看到设备工具栏切换图标。单击此按钮将显示您的网页,就像在移动设备上查看一样。
图 4-36。
设备工具栏
设备工具栏允许我们选择特定的移动设备来查看页面。它的另一个很棒的特性是能够旋转你的网页,就像在移动设备上以旋转的方式被浏览一样。
这可能是您能够在多个设备上呈现您的网页,而不使用物理设备来呈现您的页面的最接近的方式。
使用 SCSS 的断点和媒体查询
既然我们已经看到了如何在多种设备上呈现我们的 web 页面,那么是时候看看我们的 web 应用如何在移动设备上呈现了。对于这个例子,我只是选择使用 iPhone X。
当我们将设备工具栏中的设备更改为 iPhone X 时,我们会发现有问题。多个移动设备(不包括平板电脑)可能都存在同样的问题。
图 4-37。
不正确的移动布局
我之前创建的标题 div 堆叠不正确。事实上,堆叠是 100%正确的,因为这就是引导系统的工作方式。然而,这并不是我们想要的 web 应用。
我们可以解决这个问题的方法是使用断点和媒体查询。这些允许我们在生成的 CSS 中为特定的移动设备指定特定的样式。
我不打算解决这个问题,我只是打算在移动设备上查看时隐藏标题。这将说明断点和媒体查询的使用以及它们是如何工作的。首先在你的 scss 文件夹中创建一个名为 _mixins.scss 的新文件。
图 4-38。
Add _mixins.scss 文件
在 your _variables.scss 文件中,添加一个名为$screen-mobile-max 的新变量,并将其设置如下。
$screen-mobile-max: 414px;
Listing 4-32New variable
现在编辑新的 _mixins.scss 文件,并向其中添加以下代码。
@import "_variables.scss";
@mixin mobile {
@media (max-width: #{$screen-mobile-max}) {
@content;
}
}
Listing 4-33Add mixin
因为我们在 mixin 中使用我们的新变量,我们需要导入 _variables.scss 文件。现在保存项目以编译代码。然后编辑 customstyles.scss 文件,并将以下媒体查询添加到该文件中,以针对移动设备。
@include mobile {
.task-pipeline-heading, .task-in-progress-heading, .task-completed-heading {
display: none;
}
}
Listing 4-34Target mobile devices
这是针对移动设备,最大宽度为414px
,然后将display: none
样式应用于列标题。这将隐藏移动设备上的列标题。如果屏幕宽度超过414px
,则列标题将再次显示。
这使您可以随意处理媒体查询,并针对特定的移动设备应用特定的样式。
使用 Chrome 开发工具调试 jQuery
能够调试 jQuery 和 JavaScript 让您可以完全控制自己编写的代码。不用再猜测和试错调试错误了(看看你的 SYSPRO VbScript 开发人员)。你可以完全放心地编写你的 jQuery 并使用 Chrome 开发工具来调试你的代码。
我想做的是允许用户检查某些任务,并将其标记为已完成。为此,我需要在我的任务上有一个复选框,上面有文本标记已完成。当用户选中此复选框时,文本必须更改为已完成,并且页面上的任务元素应更改为绿色。这一切都可以通过 jQuery 实现。我想做的第一件事是将所有的.task
元素设置为transparent
背景色。我希望能够取消选中该任务,并在取消选中时,将颜色设置为transparent
。修改 customstyles.scss 文件,给.task
类添加一个background-color: transparent
属性。
.task {
border: 1px solid blue;
border-radius: 5px;
padding: 5px;
margin: 5px;
background-color: transparent;
.task-id {
display: none;
}
.task-title {
font-weight: bold;
}
}
Listing 4-35Customstyles for Task element
接下来我想做的是给我的任务添加一个复选框,并给它一个惟一的 ID。
@{ var iCount = 0; }
@foreach (var item in Model)
{
iCount += 1;
<div class="task @(item.DueDate <= DateTime.Now.AddDays(1) ? "critical" : "")">
<div class="task-id">
@item.TaskID
</div>
<div class="task-title">
@item.TaskTitle
</div>
<div class="task-body">
@item.TaskBody
</div>
<div class="task-date">
@item.DueDate.ToString("MMMM dd, yyyy")
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="chkCompleted@(iCount)">
<label class="form-check-label" for="chkCompleted@(iCount)" id="chkLabel@(iCount)">Mark completed</label>
</div>
</div>
}
Listing 4-36Modified task item
我所做的是声明一个名为iCount
的计数器,它在每次迭代中递增。然后,我将这个值连接到复选框 ID,以确保复选框元素的 ID 是惟一的。我对label
元素做了同样的事情。
图 4-39。
设置唯一的 id
保存您的更改,运行您的应用并查看任务项。它们都有添加了文本标记已完成的复选框。
图 4-40。
带有复选框的任务项目
如果我们查看为我们的任务项生成的代码,我们会发现 id 确实是惟一的。
图 4-41。
为任务生成的客户端代码
我们现在准备编写一些 jQuery。这是我们面临的第一个挑战。我们不知道我们将有多少任务。因此,我们不知道复选框的 id 是什么。当用户选中一个复选框时,我们需要添加一个事件,但是为了做到这一点,我们需要知道该复选框的 ID。我们使用iCount
变量动态添加的那些 id。
在这里,我们将使用一些 jQuery 选择器和奇特的技巧来获得我们想要的元素。
请注意,我将要添加的代码包含一个 bug。本节的目的是演示如何使用 DevTools 进行调试。
继续将下面的 jQuery 添加到索引视图的脚本部分。
$('[id^="chkCompleted"]').click(function () {
var $div = $(this).closest('div');
if (this.checked) {
$("label[for='" + this.id + "']")["0"].innerText = "Completed";
$div.css("background-color","#89ea31");
}
else {
$("label[for='" + this.id + "']")["0"].innerText = "Mark completed";
$div.css("background-color","transparent");
}
});
Listing 4-37jQuery code to mark completed items
让我们分开来解释一下。图 4-42 中图像的代码与清单 4-37 中的代码完全相同。
图 4-42。
jQuery 逻辑
该逻辑对应于上图中的步骤,如下所示:
-
对于 ID 以文本 chkCompleted 开始的所有元素,添加一个
click
事件。 -
找到对最近的
div
元素的引用。 -
如果复选框已被选中…
-
获取
for
属性等于复选框 ID 的label
元素,并将其文本设置为已完成。 -
使用我们之前找到的
div
引用,设置背景颜色为绿色。 -
如果复选框未被选中,则将具有与复选框 ID 相等的
for
属性的label
元素重置回原始文本。 -
使用我们之前找到的
div
引用,设置背景色为transparent
。
我们似乎已经开始工作了,所以让我们运行我们的应用并测试我们的 jQuery。
图 4-43。
选中任务项目复选框
不幸的是,我们遇到了障碍。当 check 事件起作用并且 checkbox 标签的文本被正确更改时,整个任务项的背景色没有被设置为绿色。要查看发生了什么,按住 Ctrl+Shift+I 或右键单击您的网页并从上下文菜单中选择 Inspect 。选择源选项卡,并滚动到您的索引页面上的 jQuery 代码。
图 4-44。
在 jQuery 代码上添加断点
添加断点后,选中和取消选中您的任务项复选框。您将看到,在每个选中和取消选中操作中,断点被命中,并且您的网页进入暂停状态。
当代码暂停时,找到手表窗口,通过单击 + 图标并将表达式粘贴到提供的文本框中,将表达式$(this).closest('div')
添加到新手表中。
图 4-45。
chrome devtools watch(chrome devtools 观察)
我马上就能看出问题出在哪里。我们的 jQuery 指向了错误的div
元素。
图 4-46。
正确和不正确 div 的位置
我们的目标是最近的div
元素,而不是保存任务项的div
。如下修改您的 jQuery。
$('[id^="chkCompleted"]').click(function () {
var $div = $(this).closest('div[class^="task"]');
if (this.checked) {
$("label[for='" + this.id + "']")["0"]
.innerText = "Completed";
$div.css("background-color","#89ea31");
}
else {
$("label[for='" + this.id + "']")["0"]
.innerText = "Mark completed";
$div.css("background-color","transparent");
}
});
Listing 4-38Correct jQuery code
变化在于我们找到任务项div
的方式。我们不是只查找最近的div
元素,而是告诉 jQuery 查找最近的div
元素,该元素也有一个以文本“task
开头的类。保存四个更改并刷新您的网页。这一次,如果您检查您的任务项,它将完成并且任务项变为绿色。
Chrome Developer Tools 提供了一系列调试工具,这一章甚至没有提到。你绝对可以写一整本书来介绍使用 Chrome DevTools 的好处。然而,我们已经用完了空间,我鼓励你仔细看看 Google Chrome 为开发者提供的功能。
包扎
唷,这是一个很长的章节。我们对创建 ASP.NET MVC 应用以及 MVC 如何工作有了一个高层次的看法(相信我,MVC 有比本章所讨论的更多的东西)。
然而,这一章的重点并不是通常的 MVC 主题。我想带您更进一步,探索围绕开发响应式 web 应用和轻松设计这些应用的鲜为人知的特性。
在了解如何设置和使用 scss 来设计网页风格之前,我们先了解了如何引用 jQuery 和 Bootstrap。我们看到 scss 向下编译为 css,并且 scss 在语法上类似于 C#。
然后我们简单看了一下什么是模型、控制器和视图,以及如何在视图中使用 Razor。Razor 的强大之处显而易见,例如,我们可以基于来自数据库的逻辑动态创建客户端代码。
我们看了看如何通过添加一个名为 Isotope 的插件来扩展 web 应用的功能。它为我们提供了开箱即用的过滤,让我们不必自己动手。
最后,我们看了一下在各种移动设备上测试 web 应用的响应布局。更重要的是,我们是在谷歌 Chrome 开发者工具中这样做的。我们还看到了如何使用 DevTools 控制台中的监视窗口调试 jQuery 代码。
在下一章,我们将会看到。NET Core 并弄清楚所有这些大惊小怪的到底是什么,所以请继续关注。
五、开始使用 .NET Core 3.0
如今,很难在没有听说过这个词的情况下使用微软技术栈进行编码 .NET Core。这可能会让一些人想知道它到底是什么。嗯,。NET Core 是一个开源开发平台,由微软和。网络社区。它允许开发人员编写支持 Windows、Linux 和 macOS 的应用。事实上.NET Core 可以概括为以下特征:
-
它是跨平台的,可以在 macOS、Windows 和 Linux 上运行。
-
它是开源的,使用 MIT 和 Apache 2 许可证,也是一个. NET 基础项目(
https://dotnetfoundation.org/About
)。 -
它在包括 x86、x64 和 ARM 在内的多种架构上执行代码完全相同。
-
它允许使用命令行工具进行本地开发。
-
它可以与 Docker 容器一起使用,并排安装或包含在您的应用中,使 .NET Core 部署非常灵活。
-
.NET Core 兼容性扩展到 Mono、Xamarin 和。NET Framework 通过。净标准。
-
它由微软公司提供支持。净核心支持(
https://dotnet.microsoft.com/platform/support/policy/dotnet-core
)。
我们还需要看一看 .NET Core。它由以下部分组成:
-
那个。NET Core 运行时(可在 GitHub 上
https://github.com/dotnet/coreclr
)下载。它包括垃圾收集、JIT 编译器、原语类和低级类。 -
ASP.NET 运行时(可在 GitHub 上的
https://github.com/aspnet/AspNetCore
获得)。这允许您在 Windows、Mac 或 Linux 上构建基于云的 web 应用。 -
那个。NET Core CLI 工具(可在 GitHub
https://github.com/dotnet/cli
获得)。 -
启动的点网工具 .NET Core 应用和 CLI 工具。
在这一章中,我们将看一看创建和运行 .NET Core 应用。NET Core 3.0 预览版 2 和 Visual Studio 2019 预览版。我们将讨论
-
创造。Visual Studio 2019 中的. NET 核心应用
-
中的新内容.NET Core 3.0
-
正在安装。带有 Snap 的 Linux 上的 NET Core 3.0 预览版
-
在 Linux 上创建和运行 ASP.NET MVC 应用
-
使用 Visual Studio 代码在 Linux 上编辑 ASP.NET 核心 MVC 应用
-
用 Visual Studio 代码调试 ASP.NET 核心 MVC 项目
在我们开始之前,您需要确保您已经下载并安装了。NET Core 3.0 安装在您的系统上。点击这个网址,下载你的平台的安装程序: https://dotnet.microsoft.com/download/dotnet-core/3.0
创造。Visual Studio 2019 中的. NET 核心应用
一旦你安装了。NET Core 3.0,我们就可以开始创建应用了。我刚刚创建了一个简单的 .NET Core 控制台应用。创建项目时,确保您的目标是。NET Core 3.0 framework 从项目属性页(图 5-1 )。
图 5-1。
目标.NET Core 3.0
我不打算详细介绍如何创建. NET 核心控制台应用。这里的重点是告诉你如何瞄准。网芯 3.0。
创建 ASP.NET 核心应用时,确保从下拉列表中选择 ASP.NET 核心 3.0(图 5-2 )。
图 5-2。
创建 ASP.NET 核心 3.0 应用
我在 Visual Studio 中的解决方案现在包含两个项目,如图 5-3 所示。这是一个. NET 核心控制台应用和一个 ASP.NET 核心 MVC 应用。
图 5-3。
解决方案浏览器
创建了两个应用模板之后,让我们来看看。网芯 3.0 可以为开发者提供。
中的新内容.NET Core 3.0
中有许多新功能。网芯 3.0,有些我就不讨论了。然而,我将强调一些更有趣的特性。
Windows 桌面
随着的发布。NET Core 3.0,您现在可以使用 Windows 窗体和 WPF 创建 Windows 桌面应用,如图 5-4 所示。如果向解决方案中添加一个新项目,并按。NET Core,你会注意到你有两个新的模板可以选择。
图 5-4。
新的 .NET Core 项目模板
的前两个版本迭代。NET Core 支持的 web 应用和 API、物联网和控制台应用。
请注意即使。NET Core 3.0 增加了对使用 WinForms 和 WPF 构建 Windows 桌面应用的支持,但你仍然只能在 Windows 上运行这些应用。
因为实体框架被很多桌面 app 使用,。NET Core 3.0 也支持实体框架 6。Visual Studio 2019 让你能够创建 WinForm 和 WPF 应用,但你可以在命令行中使用dotnet new
做同样的事情。创建一个新的 .NET Core 应用的 WPF 和 WinForms,您可以从命令行运行以下命令。
dotnet new wpf
dotnet new winforms
Listing 5-1Using dotnet new in the command line
看到多简单了吗?事实上,看一下命令行就会看到图 5-5 中的截图。
图 5-5。
带 dotnet 的新 WinForms 应用新
如果我们现在将鼠标悬停在我们创建的文件夹上,我们可以看到由dotnet new
创建的解决方案文件(图 5-6 )。
图 5-6。
的文件。NET Core WinForm app
现在,您可以通过在命令行中键入dotnet run
来运行新的 WinForms 应用。编译和显示您的应用可能需要几秒钟的时间,但是很快您就会看到。NET Core WinForm app 如图 5-7 所示。
图 5-7。
运行。NET Core WinForms 应用
如果您查看用于创建应用的文件夹,您会注意到现在添加了一个 bin 文件夹。创作的时候 .NET Core 控制台应用,该项目的目标是Microsoft.NET.Sdk
SDK。如果您查看的 netcoredemo.csproj 文件,您会看到这一点 .NET Core 控制台应用。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.0</TargetFramework>
</PropertyGroup>
</Project>
Listing 5-2.NET Core Console csproj file
那个。NET Core WinForms 应用使用不同的 SDK(WPF 应用也顺便使用),但也声明它使用哪个 UI 框架。
那个。NET Core WPF 应用将在 csproj 文件中声明一个<UseWPF>true</UseWPF>
属性,而。NET Core WinForms app 会在 csproj 文件中声明一个<UseWindowsForms>true</UseWindowsForms>
属性。
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>netcoreapp3.0</TargetFramework>
<UseWindowsForms>true</UseWindowsForms>
</PropertyGroup>
</Project>
Listing 5-3.NET Core WinForms csproj file
我敢肯定,随着微软宣布推出 WinForms 和 WPF 桌面应用,你们中的一些人可能会期待它们能在 Linux 或 macOS 上运行。网芯 3.0。开发者社区对此似乎有些失望。然而使用的好处是。NET Core for Windows 应用意味着我们拥有
-
提高性能
-
开源带来的好处
-
能够安装多个 .NET Core 版本并行
-
发布独立应用的能力
-
获得。仅网络核心功能(如
Span<T>
)
是的,他们让我在改进性能。如你所知。NET Core 是开源的,但是 WPF 、 Windows Forms 、 WinUI 也都是开源的。在 GitHub 上找到它们:
-
Windows 窗体:
https://github.com/dotnet/winforms
-
Windows UI:
https://github.com/Microsoft/microsoft-ui-xaml
作为。NET Core 的发展,我们肯定会看到对 Windows 桌面应用中常用的 API 的更多支持。
支持 C# 8.0
回想一下我们讨论 C# 8.0 的第三章。开发人员可以使用的功能现在可以在中使用。网芯 3.0。随着每个新预览版的发布,更多的 C# 8.0 特性被引入。
这样做是有意义的,因为 C# 8.0 在别处可用,而在。NET Core 会有些令人沮丧。通读第三章(如果还没有通读的话),看看 C# 8.0 在语言改进方面为你提供了什么。
默认可执行文件
对于使用全局安装版本的应用 .NET Core,它们是用默认的 exe 文件构建的。在此之前,您只有一个自带应用的 exe。这意味着您可以双击 exe 或从命令行启动它,而无需使用 dotnet 工具。
在窗口上
在 Windows 上,您可以执行以下操作在 c:\temp 文件夹中创建一个新目录,创建一个新的 .NET Core 控制台应用并运行它。
cd c:\temp
md coreconsoletest
cd c:\temp\coreconsoletest
c:\temp\coreconsoletest>dotnet new console
dotnet build
cd c:\temp\coreconsoletest\bin\debug\netcoreapp3.0
coreconsoletest.exe
Listing 5-4Creating a .NET Core Console app on Windows
如果你运行你的 exe,你会看到文本 Hello World!在控制台窗口输出,如图 5-8 所示。通过点网运行 dll 会产生相同的结果。
图 5-8。
运行默认的 exe 和 dll
在 macOS 上
我们可以在 macOS 上做同样的事情。一定要下载。NET Core 3.0 预览版,安装在 macOS 上。接下来,打开终端。
在终端中,我在桌面上创建了一个名为 netcoremac 的文件夹,然后切换到该目录。然后我创建一个新的 .NET Core 控制台应用中的目录并构建它。然后我把目录换到可执行文件所在的位置,也就是 netcoreapp3.0 目录。
mkdir ~/Desktop/netcoremac
cd ~/Desktop/netcoremac
dotnet new console
dotnet build
cd ~/Desktop/netcoremac/bin/Debug/netcoreapp3.0
Listing 5-5Creating a .NET Core Console app on macOS
然后我就可以用./netcoremac
运行 netcoremac 可执行文件,也可以用dotnet
命令运行 netcoremac.dll,如图 5-9 所示。
图 5-9。
在 macOS 上运行默认的可执行文件和 dll
在 Linux 上
在 Linux 上,对于新的 .NET Core 控制台应用。打开终端,在桌面上创建一个名为 netcorelinux 的目录。在那个目录中,我创建了一个新的 .NET Core 控制台应用。
cd ~/Desktop
mkdir netcorelinux
cd netcorelinux
dotnet new console
dotnet build
cd ~/Desktop/netcorelinux/bin/Debug/netcoreapp3.0
Listing 5-6Creating a .NET Core Console app on Linux
我可以使用在 macOS 上使用的相同命令来运行默认的可执行文件。运行命令./netcorelinux
运行默认的可执行文件,然后运行命令dotnet netcorelinux.dll
运行 dll。输出如图 5-10 所示。
图 5-10。
在 Linux 上运行默认的可执行文件和 dll
快速内置 JSON 支持
JSON 已经成为现代社会的一部分。网络开发。首选图书馆是 Json.Net。从……开始。NET Core 3.0 中,System.Text.Json
名称空间中添加了三种主要的 JSON 相关类型,以提供内置的 JSON 支持。这些是
-
系统。Text.Json.Utf8JsonReader
-
系统。Text.Json.Utf8JsonWriter
-
系统。text . JSON . JSON 文档
这意味着新的内置 JSON 支持提供了高性能和低分配,并且基于Span<byte>
。你可以在这里阅读更多关于Span<T>
的内容: https://docs.microsoft.com/en-us/dotnet/api/system.span-1
密码系统
System.Security.Cryptography.AesGcm
和System.Security.Cryptography.AesCcm
名称空间增加了对 AES-GCM 和 AES-CCM 密码的支持。这些是添加到中的第一批经过身份验证的加密算法 .NET Core。让我们看看我们之前创建的 netcoredemo 控制台应用。我们将添加基本的加密和解密方法。确保您已经添加了System.Security.Cryptography
名称空间。
public static byte[] Encrypt(out byte[] key, out byte[] nonce, out byte[] tag, byte[] dataToEncrypt)
{
key = new byte[16];
nonce = new byte[12];
RandomNumberGenerator.Fill(key);
RandomNumberGenerator.Fill(nonce);
tag = new byte[16];
byte[] ciphertext = new byte[dataToEncrypt.Length];
using (AesGcm aes = new AesGcm(key))
aes.Encrypt(nonce, dataToEncrypt, ciphertext, tag);
return ciphertext;
}
Listing 5-7
AES-GCM encryption method
我们使用 out 参数将key
、nonce
和tag
值传递回调用代码。
使用加密技术时,我们会创建一个随机数,它是一个随机值,用于防止重放攻击。这是如果有人拦截了第一条消息并试图第二次发送该消息。每条消息的随机数必须是唯一的。如果接收应用收到重复的 nonce,它知道需要丢弃该消息。
加密的数据被返回给调用代码,并传递给 Decrypt 方法。
public static void Decrypt(byte[] key, byte[] nonce, byte[] tag, byte[] ciphertext)
{
byte[] decryptedData = new byte[ciphertext.Length];
using (AesGcm aes = new AesGcm(key))
aes.Decrypt(nonce, ciphertext, tag, decryptedData);
string decryptedText = Encoding.UTF8.GetString(decryptedData);
Console.WriteLine(decryptedText);
}
Listing 5-8
AES-GCM decryption method
调用代码将调用Encrypt
和Decrypt
方法,如下所示。
byte[] dataToEncrypt = Encoding.UTF8.GetBytes("String to encrypt");
var encrData = Encrypt(out byte[] key, out byte[] nonce, out byte[] tag, dataToEncrypt);
Decrypt(key, nonce, tag, encrData);
Console.ReadLine();
Listing 5-9Calling Encrypt and Decrypt
运行应用,您将看到解密的文本显示在decryptedText
变量中。见图 5-11 我检查过的变量。
图 5-11。
解密文本
如果您想实现 AES-CCM 密码,您基本上可以做同样的事情,只是使用不同的类名(AesCcm
)。
正在安装。带有 Snap 的 Linux 上的 NET Core 3.0 预览版
推荐的安装方式。Linux 上的 NET Core 3.0 预览版是通过 Snap 实现的。在写本章的时候,下列 Linux 发行版支持 Snap:
-
Arch Linux
-
一种自由操作系统
-
深度
-
基本操作系统
-
一种男式软呢帽
-
加利亚姆斯
-
KDE 霓虹灯
-
库班图
-
Linux 作为
-
卢班图
-
manjaro
-
大蜥蜴
-
Parrot 安全操作系统
-
Raspbian
-
Solus
-
人的本质
-
徐邦图
-
佐伦·OS
出于我的目的,我使用了 Linux Mint。在 Linux 系统上配置 Snap 之后,运行以下命令来安装。网芯 3.0 预览版。
sudo snap install dotnet-sdk --beta --classic
Listing 5-10Install .NET Core 3.0 Preview with Snap
这是现在的默认设置。通过 Snap 安装时的 NET Core 命令dotnet-sdk.dotnet
。这是一个命名空间命令,不会与全局安装的冲突。您可能在 Linux 系统上安装了. NET Core 版本。我更喜欢使用默认的dotnet
命令,因为这只是我使用的 Linux 的一个测试安装。
为此,您可以在终端中运行以下命令,为您的dotnet-sdk.dotnet
命令创建一个别名。
sudo snap alias dotnet-sdk.dotnet dotnet
Listing 5-11Creating the dotnet alias
有关设置的更多信息。Linux 上的 NET Core,参考以下链接: https://github.com/dotnet/core/blob/master/Documentation/linux-setup.md
。
在 Linux 上创建并运行 ASP.NET MVC 应用
在 Linux 桌面上创建一个新文件夹。出于我的目的,我使用 Linux Mint。打开“终端”并导航到您创建的新文件夹。若要查看哪些模板可供您使用,请在“终端”中键入以下命令。
dotnet new -l
Listing 5-12Listing the dotnet new templates
现在这里列出了所有可用的项目模板,您可以使用dotnet new
命令创建这些模板。
请注意,在安装之后,我通过键入sudo snap alias dotnet-sdk.dotnet dotnet
将我的dotnet-sdk.dotnet
命令别名为dotnet
。NET Core 3.0 预览版。
列出的模板之一是 ASP.NET 核心 MVC 应用。要创建此项目类型,请在之前在桌面上创建的目录中,在终端中运行以下命令。
dotnet new mvc
Listing 5-13Create a .NET Core MVC app on Linux
这将在我们之前创建的文件夹中创建您的 ASP.NET 核心 MVC 应用。
图 5-12。
ASP.NET 核心 MVC 项目
打开文件夹,您会注意到我们有所有我们通常在 Visual Studio 中看到的熟悉文件(图 5-12 )。
如果在上遇到访问路径错误。创建 MVC 应用时,nuget/packages 文件夹或 csproj 文件上的权限被拒绝错误,运行sudo dotnet restore
并在提示时键入您的密码。
要运行新的 ASP.NET 核心 MVC 应用,请在终端中键入以下命令。
dotnet run
Listing 5-14Running your ASP.NET Core MVC app
您将看到终端显示一些信息消息。其中一条消息应该是正在监听: https://localhost:[port]
,其中【端口】是一个有效的端口号。在你的浏览器中输入那个 URL(我用的是 Firefox),你会看到你的 ASP.NET 核心 MVC 应用运行在 Linux 上,如图 5-13 所示。
图 5-13。
Linux 上 Firefox 中的 ASP.NET 核心 MVC 应用
整个过程甚至不需要我们花 2 分钟来创建一个项目并在 Linux 上运行它。
使用 Visual Studio 代码在 Linux 上编辑您的 ASP.NET 核心 MVC 应用
微软做了大量的工作将 Visual Studio 带到所有平台上。Visual Studio 代码为开发人员在 Linux 和 macOS 上创建应用提供了一个极好的 IDE。如果我需要快速处理一个文件,我甚至每天都在我的 Windows 机器上使用它。
Visual Studio 代码可以从以下网址下载: https://code.visualstudio.com/
。使用 Visual Studio 代码的好处是
-
这是一个免费的 IDE,对于想要尝试新事物的开发人员来说是完美的。
-
它也是开源的。你可以在 GitHub 上查看这里的资源库:
https://github.com/Microsoft/vscode
。 -
它可以在 Windows、macOS 和 Linux 上运行。
这意味着我可以在 Linux 的 Visual Studio 代码中编辑我的 ASP.NET 核心 MVC 项目。
编辑您的项目
我已经下载了 Visual Studio 代码并安装在我的 Linux Mint 安装上。让我们用它来打开我们的 ASP.NET 核心 MVC 应用,并为 HomeController 修改 Index.cshtml 文件。首先打开 Visual Studio 代码并点击 Explorer 或者按下 Ctrl+Shift+E 。图 5-14 显示了在 Visual Studio 代码中可以找到资源管理器的地方。
图 5-14。
开启 Visual Studio 程式码总管
单击“打开文件夹”,然后打开项目的顶层文件夹。您将看到如图 5-15 所示的项目文件。
图 5-15。
在 Visual Studio 代码中打开项目
在视图文件夹中,选择主页文件夹,点击 Index.cshtml 。这将在代码编辑器中打开文件。如下修改您的代码。
@{
ViewData["Title"] = "Home Page";
var longAgoDate = DateTime.Today.AddYears(-100);
var longDayOfWeek = longAgoDate.DayOfWeek;
}
<div class="text-center">
<h1 class="display-4">Welcome</h1>
<h2 class="display-4">100 Years ago was @longDayOfWeek, @longAgoDate.ToString("MMMM dd, yyyy")</h2>
</div>
Listing 5-15The Index.cshtml view
将更改保存到文件中,然后在终端中键入dotnet build
命令,然后键入dotnet run
。在 Firefox 中运行你的应用(图 5-16 )。
图 5-16。
修改的 ASP.NET 核心 MVC 项目
用 Visual Studio 代码调试您的 ASP.NET 核心 MVC 项目
Visual Studio 代码允许您调试代码。你需要做一点繁重的工作来完成所有的设置,但是一旦你完成了这些,你就一切顺利了。
我在 Linux 上设置了这个例子,所以如果您不在 Linux 上工作,这些步骤可能会与您的系统不同。我用的是 Linux Mint。
为此,打开 Visual Studio 代码,查看一下扩展窗格,或者按住 Ctrl+Shift+X 。搜索由 OmniSharp 驱动的 C# 扩展(图 5-17 )。
图 5-17。
Visual Studio 代码扩展的 C#
在 Visual Studio 代码中,打开您之前在其中创建项目的 aspnetmvc 文件夹。当你这样做的时候,你应该看到下面的消息显示:‘aspnet MVC’中缺少构建和调试所需的资产。加他们?
当您单击“是”时,Visual Studio 代码将添加一个。vscode (图 5-18 )文件夹到你的项目中。
图 5-18。
确保. vscode 文件夹存在
在这个里面。vscode 文件夹,你会看到应该有两个 Visual Studio 代码创建的文件。这些文件是
-
launch.json
-
tasks.json
您将需要这些文件(图 5-19 )来调试您的 ASP.NET 核心 MVC 应用。如果这些文件不存在,删除。vscode 文件夹,重启 Visual Studio 代码,再次打开 aspnetmvc 项目文件夹。
图 5-19。
确保启动和任务文件存在
打开 launch.json 文件,检查其中的内容。注意,需要为 bin 文件夹中的【aspnetcore.dll】文件设置正确的路径。launch.json 文件配置并保存所有调试设置细节。调试项目时会用到这些调试配置信息。
"version": "0.2.0",
"configurations": [
{
"name": ".NET Core Launch (console)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceFolder}/bin/Debug/netcoreapp3.0/aspnetmvc.dll",
"args": [],
"cwd": "${workspaceFolder}",
"stopAtEntry": false,
"console": "internalConsole"
},
{
"name": ".NET Core Launch (web)",
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/bin/Debug/netcoreapp3.0/aspnetmvc.dll",
"args": [],
"cwd": "${workspaceFolder}",
"stopAtEntry": false,
"internalConsoleOptions": "openOnSessionStart",
"launchBrowser": {
"enabled": true,
"args": "${auto-detect-url}",
"windows": {
"command": "cmd.exe",
"args": "/C start ${auto-detect-url}"
},
"osx": {
"command": "open"
},
"linux": {
"command": "xdg-open"
}
},
"env": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"sourceFileMap": {
"/Views": "${workspaceFolder}/Views"
}
},
{
"name": ".NET Core Attach",
"type": "coreclr",
"request": "attach",
"processId": "${command:pickProcess}"
}
,]
}
Listing 5-16The launch.json file contents
我们看到的下一个文件是 tasks.json 文件。这将在您的 ASP.NET 核心 MVC 项目上运行构建任务。它可以包含其他几个任务,但目前这是我们所需要的。
{
"version": "2.0.0",
"tasks": [
{
"label": "build",
"command": "dotnet build",
"type": "shell",
"group": "build",
"presentation": {
"reveal": "silent"
},
"problemMatcher": "$msCompile"
}
]
}
Listing 5-17The tasks.json file contents
接下来,展开视图 ➤ 主页文件夹,打开 Index.cshtml 文件。点击空白处,在图 5-20 第 4 行放置一个断点。
图 5-20。
放置断点
我们现在需要按住 Ctrl+Shift+D 来调出调试窗格,如图 5-21 所示。首先,您会注意到您可以访问熟悉的变量、观察和调用堆栈。您还会看到断点组,当前设置的断点显示在 Index.cshtml 文件上。
图 5-21。
调试窗格
从调试组合框中,选择。NET Core Launch (web) 点击绿色播放按钮(图 5-22 )。
图 5-22。
调试开始
项目将通过一个生成过程,如果成功,调试栏将显示在 Visual Studio 代码 IDE 窗口的顶部。现在你要打开调试控制台。你可以通过视图 ➤ 调试控制台菜单或者按住 Ctrl+Shift+Y 来实现。您将看到我们之前看到的熟悉的输出,并且您会注意到它表明Microsoft.Hosting.Lifetime
web 主机正在本地主机上侦听(图 5-23 )。
图 5-23。
调试控制台
回到浏览器(我用的是 Firefox ),输入调试控制台中指定的 URL。
图 5-24。
断点命中
当你的网页加载时,你的断点将被点击,如图 5-24 所示。现在,您可以像平常一样逐句通过代码、查看变量、使用"监视"窗口以及执行所有正常的调试任务。您还可以在单步执行代码行时,通过将鼠标悬停在编辑器中的变量上来检查变量。
包扎
我们以微软文档( https://docs.microsoft.com
)的形式提供了丰富的在线信息,这些信息正是您所需要的。我希望这一章至少激起了你对。网芯 3.0。随着框架的发展,我们将会在每个版本中看到更多的特性。
这一章看了一下创造 .NET Core 应用。我们看到现在可以在上创建一个 Windows 桌面应用 .NET Core,但这些仅在 Windows 上受支持。
然后我们看了一些更有趣的新特性。网芯 3.0。我们看到我们支持 C# 8、快速内置 JSON 支持和加密技术。
我们看到我们可以安装。NET Core 3.0 在 Linux 上使用 Snap。继续使用 Linux,我们创建了一个 ASP.NET 核心 MVC 应用,并在 Linux 上运行。使用 Visual Studio 代码,我们看到可以编辑我们的项目文件,最后,我们看到可以使用 Visual Studio 代码在 Linux 上调试我们的应用。
如果您喜欢 Visual Studio 代码的灵活性,请继续关注。在下一章中,我们将浏览 Visual Studio 2019,并了解这款世界级 IDE 即将发布的新功能。你兴奋吗?我知道我是。
六、在 Visual Studio 中提高工作效率
自 20 多年前作为 Visual Studio 97 首次发布以来,Visual Studio 已经走过了漫长的道路。就我个人而言,我从 2003 年开始使用 IDE,并且很高兴看到它发展成今天的样子。然而,问题是开发人员陷入了编写代码的日常事务中,以至于他们往往会错过新版本的新功能和生产率的提高。
在某种程度上,我认为这是因为开发人员专注于完成工作。我们确实生活和工作在一个快节奏、以截止日期为导向的行业。因此,更好地了解新版本是很容易的,因为你需要完成工作。我经常听到开发人员说“我不知道你可以这样做!”。
事实上,许多开发人员非常专注于学习 C# 的新特性(举例来说),这是理所当然的。让我们在我们的老朋友,可信赖的 IDE 的脚下停顿片刻(或一章),它使这一切成为可能。
本章将看看新版本(目前在预览版中)在生产力改进和特性方面为我们提供了什么。
在撰写本章时,Visual Studio 2019 处于预览版 3 中。在最终发布之前,它可能会稍有变化,但是本章讨论的大部分内容应该保持不变。
我还将介绍一些现有的特性和好处,它们可以让您的日常开发工作变得更加轻松。我们将会看到
-
Visual Studio 2019 中的新功能
-
Visual Studio 实时共享
-
重构和代码修复
-
在 ASP.NET 项目中启用 JavaScript 调试
-
导出编辑器设置
-
使用 AI 的 Visual Studio IntelliCode
-
常规 Visual Studio 提示
这将变得更加令人兴奋,所以让我们开始使用 Visual Studio 2019。
Visual Studio 2019 中的新功能
Visual Studio 2019 通过提供 UI 的全新视角,以更简洁和专注的方式为开发人员带来了最重要的内容。
UI 改进
关于 Visual Studio 2019,你可能会注意到的第一件事是新的开始屏幕,如图 6-1 所示。
图 6-1。
Visual Studio 2019 开始菜单
微软将开发人员最常用的任务放在了开始菜单的最前面。在 Visual Studio 中,开始做最重要的事情真的很容易,那就是编写代码。
说到编写代码,new project 对话框还提供了对选择项目模板方式的改进。除了允许您从最近的项目类型列表中进行选择之外,您还可以通过从下拉菜单中进行选择来过滤项目模板,该下拉菜单可以根据语言、平台或项目类型进行过滤,如图 6-2 所示。
图 6-2。
“新建项目”对话框
这允许您快速搜索和选择项目模板,并开始编写代码。转到 IDE 本身,Visual Studio 2019 比 Visual Studio 2017 更能最大限度地减少混乱,只需最小化 chrome 和压缩菜单。这给了你更多的空间来写代码。对比 Visual Studio 2017 和 Visual Studio 2019 中的 IDE chrome 和菜单栏。
图 6-3 是我们目前在 Visual Studio 2017 中习惯看到的。
图 6-3。
Visual Studio 2017
图 6-4 展示了 Visual Studio 2019 的变化。你会注意到图标变了,Visual Studio 2019 IDE 看起来更紧凑了。
图 6-4。
Visual Studio 2019
事实上,请注意 Visual Studio 2017 的图像不包括开始按钮,而 Visual Studio 2019 的图像包括。这部分是由于缩短了解决方案配置和解决方案平台下拉菜单。
搜索改进
在 Visual Studio 2019 中,你可以点击菜单中的搜索栏(图 6-5 )或者按住 Ctrl+Q 将光标从代码编辑器跳转到搜索文本框。
图 6-5。
Visual Studio 2019 搜索栏
这允许您立即开始输入并搜索您需要的内容,如图 6-6 所示。
图 6-6。
Visual Studio 2019 中的搜索结果
搜索结果显示得很快,这给 IDE 一种更快的感觉(特别是当您专注于代码时)。你会注意到我打错了搜索词类,但是 Visual Studio 2019 仍然使用模糊搜索为我返回了相关结果。
搜索结果包括您正在寻找的菜单路径。但是,使用 Visual Studio search 为您提供了进入这些菜单的快捷方式,因此您可以将手放在键盘上。另一个需要注意的要点是,现在可以直接从 Visual Studio 2019 搜索结果中创建新项目。在前面的例子中,您可以看到我可以从我的搜索结果中添加一个新的类。
最后,如果你没有看到你要找的结果,你可以点击搜索结果底部的链接在网上搜索。
这些对搜索的改进使 Visual Studio 2019 更容易导航,并在一个真正功能丰富的 IDE 中找到自己的路。快,找到 C# 互动!走吧。
清理您的代码
Visual Studio 2019 为您提供了对代码以及如何格式化代码的大量控制。一个很好的方法是通过代码清理。
Visual Studio 允许您配置您的代码清理,所以让我们这样做。使用搜索栏查找单词清理,如图 6-7 所示。
图 6-7。
搜索代码清理配置屏幕
您将看到搜索结果包括配置代码清理。也可以按住 Ctrl+K,Ctrl+Q 打开配置画面。
图 6-8。
配置代码清理
图 6-8 中的配置屏幕允许您启用您想要在代码中应用的修复程序。请注意,我已经将应用内联‘输出’变量首选项添加到我的启用修复程序列表中。您也可以将这些偏好设置应用到另一个配置文件。
如果我们可以添加/重命名配置文件,那不是很好吗?提示提示 Visual Studio 团队。
回到我的代码编辑器,你会看到我可以点击一个小画笔图标(图 6-9 )来执行代码清理。
图 6-9。
代码清理前的代码
我还可以单击向下箭头来查看更多选项,例如运行与特定概要文件相关的代码清理,如图 6-10 所示。我也可以从这里访问代码清理配置。
图 6-10。
运行代码清理
现在,我只想应用内联‘out’变量首选项。点击画笔图标或按住 Ctrl+K,Ctrl+E ,你的代码将根据你的代码清理偏好被清理。你可以在图 6-11 中看到结果。
图 6-11。
代码已清理
除了配置代码清理配置文件之外,为了提高代码质量,我什么都不用做。这是新的 Visual Studio 2019 令人难以置信的强大功能和增值。
调试改进
当您调试代码时,您会注意到单步执行更快了。您还会注意到,现在您可以通过内置的搜索栏搜索您的汽车、本地和手表窗口(图 6-12 )。
图 6-12
监视窗口包括搜索功能
这真的很方便,尤其是当你的橱窗里有很多东西需要浏览的时候。您还会看到默认的搜索深度设置为 3。您可以更改这一点,但这意味着在搜索停止之前,您的结果只会深入到树的三个层次。
每台显示器感知渲染
如果您使用配置了不同显示比例因子的多个监视器,Visual Studio 可能会稍微模糊或缩放不正确。Visual Studio 2019 为 Visual Studio 成为完全基于显示器的应用奠定了基础。
为了尝试新的 PMA 功能,您需要安装 Windows 10 版本 1803(图 6-13 )或更高版本,以及。已安装. NET Framework 4.8 或更高版本。
图 6-13
Windows 10 要求
您可以通过转至工具➤选项并单击环境➤预览功能来启用预览功能,如图 6-14 所示。
图 6-14
预览功能
您会注意到,用于选择优化呈现的选项对我来说是灰色的,因为我没有。已安装. NET Framework 4.8。
免费/付费/试用扩展
在 Visual Studio 2017 之前,没有明确的方法来查看一个扩展是标记为免费、付费还是试用。有了 Visual Studio 2019,扩展和更新对话框会明确标记扩展是试用还是付费扩展。这种变化在图 6-15 中清晰可见。
图 6-15
免费/付费/试用扩展
免费扩展没有任何标签,而付费和试用扩展则明确标有蓝色标签。
Visual Studio 实时共享
Visual Studio Live Share 是一项非常棒的服务,它允许您“给朋友打电话”。您可以与同事共享您的代码库及其上下文,并直接在 Visual Studio 中与他们协作。
您的同事可以阅读您的代码、浏览代码、编辑和调试您通过 Visual Studio Live Share 与他们共享的任何项目。最棒的是,Visual Studio 2019 中默认包含了 Visual Studio Live Share。
要了解有关 Visual Studio Live Share 的更多信息,或者下载 Visual Studio 代码或 Visual Studio 2017,请访问 https://visualstudio.microsoft.com/services/live-share/
是的,你没听错。它可用于 Visual Studio 代码。该服务在两个开发人员之间工作得非常好,不管项目类型、编程语言或您正在使用的操作系统如何。
Visual Studio Live Share 的巨大优势在于,它不需要开发人员专门为了帮助同事而停止回购或设置他们的环境。
在过去,不得不建立一个项目来协助另一个开发人员的痛苦,由于协助的人可能没有安装项目所需的依赖关系而变得更加复杂。
它为参与代码评审打开了方便之门。想象一下,给学生上编程课的讲师意味着什么。就我个人而言,我认为讲师可能会得到更少的锻炼,因为他们不必在计算机实验室里走来走去,帮助学生。
因此,您可能想知道 Visual Studio Live Share 到底有多棒?让我给你量化一下。这是与一位同事分享一个运行在 Linux 上的 Visual Studio 代码项目,使用运行在 Windows 10 上的 Visual Studio 2019 有点酷。
共享您的代码
我的朋友贾森·威廉姆斯住在纽约。他刚刚开始学习编程的诀窍,并想开始编写 ASP.NET 核心 MVC 应用。他在使用剃须刀时遇到了一点麻烦,需要我的帮助来告诉他如何在 HTML 中添加一个 C# 变量。
他使用 Visual Studio 代码作为他的 IDE,已经建立了他的项目并添加了一些代码。让我们看看如何使用 Visual Studio Live Share 来解决他的问题。
在 Visual Studio 代码中,Jason 已经安装了 VS Live Share 扩展并启用了它。
在本例中,Jason 已经通过他的 GitHub 帐户登录到 Live Share。有时,Live Share 无法识别 VS 代码中的浏览器登录。在这之后的部分,我会告诉你如果遇到问题,如何通过用户代码登录。
Jason 单击实时共享图标(步骤 1)打开实时共享面板。然后点击会话详情部分下的开始协作会话(图 6-16 )。
图 6-16
Linux 上 Visual Studio 代码的实时共享
从图 6-17 中,您可以看到会话详细信息现在变为显示
-
参与者
-
共享服务器
-
共享终端
我还没有加入会话,因为 Jason 需要向我发送邀请链接。
图 6-17
实时共享会话已开始
在 Visual Studio 代码中,弹出一个通知(图 6-18 )告诉 Jason 邀请链接已经被复制到剪贴板。
图 6-18
带邀请链接的通知
这是他通过即时消息或电子邮件发给我的链接。不管什么效果最好。
图 6-19
粘贴到 Windows 浏览器中的邀请链接
在我的办公室里,我刚坐下来开始我的一天,Skype 上就出现了一条带有 Jason 邀请链接的消息(是的,Jason 在纽约很晚才睡)。我将链接粘贴到我的浏览器中(图 6-19 ),并获得在 Visual Studio 2019 中打开会话的选项。
图 6-20
加入 Visual Studio 2019 中的实时共享会话
Visual Studio 2019 的一个新实例启动,显示加入状态,如图 6-20 所示,带有下载云图标。
图 6-21
已加入实时共享会话
一旦我成功连接到实时共享会话,我的状态就会变为已加入,如图 6-21 所示,我可以看到 Jason 的图标显示出来。
图 6-22
Visual Studio 代码中的实时共享状态
回到 Jason 的 Linux 机器上(图 6-22 ,他可以看到我正在加入会话,并且正在查看第 11 行的 Index.cshtml 文件。
图 6-23
在 Visual Studio 2019 中识别 Jason 的光标
回到我的 Windows PC 上(图 6-23 ,我可以看到 Jason 当前将光标放在代码的第 4 行末尾,因为一个带有他名字的标签会立即弹出。
图 6-24
在 Visual Studio 代码中标识 Dirk 的光标
在 Jason 的 Visual Studio 代码中,他可以看到我选择了文本 Welcome ,这是通过一个带有我名字的标签瞬间弹出的(图 6-24 )。在整个会话中,当我们在代码中导航时,带有我们名字的标签会暂时显示在彼此的代码编辑器中。但是总会有一个光标,标识我们的光标相对于彼此的位置。
图 6-25
向参与者发送焦点请求
如果我需要将 Jason 的注意力集中到某一行代码上,我可以向他发送一个焦点通知(图 6-25 )。
图 6-26
在 Visual Studio 2019 中编辑 Jason 的代码
Jason 遇到的问题是在他的页面的 HTML 中插入一个变量。在我的会话中,在 Visual Studio 2019 上,我修改了他的代码,如图 6-26 所示,并将today
变量添加到H1
标签中。
请注意,Index.cshtml 文件在 Visual Studio 2019 中被标记为未保存。
图 6-27
Jason 的代码在 Visual Studio 代码中编辑
Jason 现在立即看到了我所做的代码更改(图 6-27 ,并且明白了将变量添加到他的 HTML 中的方法是在变量前面加上@
符号。杰森现在落后我 7 个小时。他在挑灯夜战,我能够快速有效地帮助他。
-
例如,我不必求助于 Skype 中笨重低效的屏幕共享。
-
我不需要从 GitHub 下载他的项目,也不需要在我的机器上以任何方式设置它。
-
我不需要安装 Linux 虚拟机,也不需要安装 Visual Studio 代码。
Jason 为了与我共享他的代码,没有改变他的环境,我也没有为了帮助他而改变我的环境。Visual Studio Live Share 非常好用。这几乎就像魔术一样。
当您无法登录时
有许多关于 Visual Studio Live Share 的文档。只需直接点击 https://docs.microsoft.com/en-us/visualstudio/liveshare/
就能看到正在讨论的话题。我遇到的一个问题是登录。我在 Linux 上运行 Visual Studio 代码,启动 Live Share 时浏览器登录表单没有弹出。
这是这个问题的解决方案。进入以下网址(图 6-28 )并登录: https://insiders.liveshare.vsengsaas.visualstudio.com/auth/login
图 6-28
Visual Studio 实时共享登录
我用我的 GitHub (Jason 的 GitHub)账户登录。您也可以使用 Microsoft 帐户登录。
图 6-29
选择用户代码方向
一旦看到图 6-29 中的准备协作画面,点击有问题的?链接。
图 6-30
复制生成的用户代码
复制屏幕上显示的用户代码(图 6-30 )并返回到 Visual Studio 代码(如果您在那里登录有问题,也可以返回到 Visual Studio)。在 Visual Studio 代码中按 F1 显示命令面板并输入文本“用户代码”。选择“实时共享:使用用户代码登录”选项,并输入您之前复制的用户代码。您现在应该能够成功登录到实时共享。
共享终端
Jason 的另一个问题是,他如何用运行在 Linux 上的 Visual Studio 代码构建他的项目。
图 6-31
Visual Studio 代码中的共享终端
事实证明,Jason 可以在实时共享会话期间轻松地与我共享他的终端(图 6-31 )。在共享终端部分,他只需点击共享终端选项。
图 6-32
共享终端访问级别
然后他需要选择他想给我的访问级别,如图 6-32 所示。他决定需要给我读/写权限。
图 6-33
Visual Studio 2019 中的新终端通知
回到我的会话(图 6-33 ),Visual Studio 2019 向我显示一个通知,告知我一个新的终端正在协作会话中共享。它为我提供了安装集成终端或始终使用外部终端的选项。
图 6-34
在 Windows 中打开的外部终端窗口
外部终端窗口在我的电脑上打开(图 6-34 ,我可以看到我通常在 Linux 上看到的熟悉的提示符。
我提醒 Jason,以根用户身份运行 Visual Studio 代码通常不是一个好主意。我还需要告诉他不要再用我的名字命名他的虚拟机。
然后,我在机器的终端窗口中输入命令dotnet build
。
图 6-35
终端在 Linux 上的 Visual Studio 代码中打开
回到纽约,Jason 可以看到我在 Visual Studio 代码中的终端上输入的命令(图 6-35 )。
图 6-36
Windows 中终端的成功构建结果
然后我按下 Enter 键,构建结果显示在我电脑的终端窗口中(图 6-36 )。
图 6-37
Visual Studio 代码终端中的成功生成结果
成功的编译结果反映在 Visual Studio 代码终端窗口中(图 6-37), Jason 现在知道如何使用终端窗口来执行编译。
关于实时共享的一些注意事项
需要注意的是,您的代码决不会存储在 Microsoft 服务器上。共享代码只存在于共享代码的机器上。它也不会以任何方式上传到云中。实时共享在您和您共享代码的人之间创建了一个安全的端到端加密连接。
使用 Live Share 的唯一真正要求是稳定的互联网连接。Azure 中继促进了在实时共享会话期间建立的安全通信。
在撰写本书时,除了发起实时共享会话的开发人员之外,实时共享还支持五个并发来宾。这意味着在任何给定时间,一个实时共享会话可以有六个开发人员参与。若要使用实时共享,您需要安装 Visual Studio 2017 (15.6+)、Visual Studio 2019 或 Visual Studio 代码。
Live Share 只与协作者分享需要的内容。例如,当您编辑一个文件时,只有该文件的内容被共享。调试时,调试操作(如单步执行)和状态(如调用、堆栈和局部变量)是共享的。
对于在分布式环境中工作的开发人员来说,Visual Studio Live Share 是一个不可或缺的工具。越来越多的公司意识到远程开发者的好处。微软现在给了我们做我们所做的事情的工具,不管同事之间的距离有多远。尝试一下 Live Share。我知道你会和我一样喜欢它。
重构和代码修复
在本书的这一部分,我们将学习一些通用的 Visual Studio 技巧。您可以在 Visual Studio 中使用这些来改进您的代码,并在您的日常编码中变得更加高效。作为开发人员,我们不得不从事遗留代码的工作,这是一项不值得羡慕的任务。从来都不好玩。这几乎就像用别人的球杆打高尔夫球。有时候感觉不太对。让我们看看如何在 Visual Studio 中完善你的挥杆。
将 foreach 转换为 LINQ(仅限 VS2019)
你知道在 Visual Studio 2019 中可以重构一个foreach
到 LINQ 吗?肯德拉·海文斯是。NET 团队,不久前在推特上发布了这个提示。
顺便提一下,我建议关注 Twitter 上的相关用户,如 @gotheap 、 @MadsTorgersen 、 @terrajobst 等。随着 C# 和 Visual Studio 的不断发展,你真的可以从他们那里学到一些很棒的技巧。
让我们来看一个非常简单的foreach
到 LINQ 的例子。你会在图 6-38 中看到我们想要重构foreach
部分。
图 6-38
简单 foreach 循环
通过将光标放在foreach
前面并单击出现的灯泡,可以将 foreach 循环重构为 LINQ。也可以按住 Ctrl+。或 Alt+Enter ,将显示重构菜单。
图 6-39
皈依 LINQ 教
点击转换为 LINQ 将使用查询表达式重构您的代码,如图 6-40 所示。
图 6-40
重构的 foreach 循环
如果你喜欢流畅的语法,你也可以选择重构它,点击 LINQ(调用自)重构前面的代码,如图 6-41 所示。
图 6-41
LINQ(电话来自)
无论您喜欢哪一种,能够将foreach
转换为 LINQ 是对 Visual Studio 中代码重构选项的一个很好的补充。
注意,这种重构只在 Visual Studio 2019 中可用。
那么,用哪个比较好呢?这里有没有 LINQ fluent vs. query 语法的争论?让我们暂停一下。
LINQ 流利与查询语法
基本上有两种方法可以使用代码创建 LINQ 查询。您可以使用流畅的语法,它对查询操作符中的参数使用 lambda 表达式。也感觉更现代。另一种方法是使用查询表达式,感觉类似于 SQL 查询。
一个不比另一个好。这真的取决于你的偏好和你将如何查询。如果使用let
关键字、进行连接或者有多个from
子句,查询语法可能是最佳选择。
let
子句允许您存储子表达式的结果,然后可以在后续子句中使用。
下面是一个使用查询语法和let
关键字的 LINQ 的例子。
var lstStockCode =
new List<string>()
{ "A100-341", "B101-754", "A100-197", "C201-341", "B102-774", "C101-111", "A100-774", "C105-191" };
var classAStockCodes =
from aclass in lstStockCode
let a = (aclass.StartsWith("A100") ? (aclass.Replace("A100-", "")) : "0")
where Convert.ToInt32(a) > 200
&& Convert.ToInt32(a) > 0
select aclass;
foreach(var cl in classAStockCodes)
{
WriteLine($"{cl} is a class A stock code in the 200 plus range");
}
Listing 6-1Query syntax using let
代码列表说明了如何查找破折号后的数字等于或大于 200 的所有 A 级股票代码。这里使用查询语法是有意义的,因为如果三元条件语句评估为 true,我们必须使用let
关键字来存储股票代码的数字部分。如果为假,我们就返回零。然后我们可以提取出符合我们的where
条件的股票代码。
转换为插值字符串
以下提示在 Visual Studio 2017 中可用,但我觉得值得一提,尤其是当您使用遗留代码时,它可以大大简化代码。考虑下面的代码。
string FirstName = "Dirk";
string LastName = "Strauss";
string FullName = string.Format("My name is {0} {1}", FirstName, LastName);
Listing 6-2String.Format string
使用string.Format
是很多开发人员都会遇到(甚至编写)的代码。现在有一个重构代码的选项。单击灯泡(您可以按 Ctrl+。或者 Alt+Enter)将显示代码重构菜单。
图 6-42
转换为插值字符串
这允许你将代码转换成插值字符串(图 6-42 )。结果代码如下。
string FirstName = "Dirk";
string LastName = "Strauss";
string FullName = $"My name is {FirstName} {LastName}";
Listing 6-3Formatted to interpolated string
这种编写包含变量的字符串的方式可读性更好,尤其是如果您很好地命名了变量的话。
将匿名类型转换为类
在 C# 中,匿名类型用于将只读属性封装到单个对象中,而不必先定义类型。编译器推断每个属性的类型。有了 Visual Studio 2019,现在可以将匿名类型(图 6-43 )转换为类。
图 6-43
记录器匿名类型
将光标放在new
关键字前,点击灯泡,按住 Ctrl+。或 Alt+Enter ,选择转换为类(图 6-44 )。
图 6-44
选择转换为类别
重命名窗口弹出并高亮显示它自动为你插入的NewClass
名(图 6-45 )。
图 6-45
重命名新的类名
为您想要创建的类提供一个更合理的名称,然后点击 Enter 按钮。
图 6-46
默认类名重命名为 LoggerClass
我调用了我的类LoggerClass
(图 6-46 ,如果我向下滚动到我的代码文件的底部,我会看到 Visual Studio 2019 已经为我插入了新的类。
internal class LoggerClass
{
public string Flag { get; }
public int Priority { get; }
public string LogLevel { get; }
public LoggerClass(string flag,
int priority,
string logLevel)
{
Flag = flag;
Priority = priority;
LogLevel = logLevel;
}
public override bool Equals(object obj)
{
return obj is LoggerClass other &&
Flag == other.Flag &&
Priority == other.Priority &&
LogLevel == other.LogLevel;
}
public override int GetHashCode()
{
var hashCode = -1332235279;
hashCode = hashCode * -1521134295 + System.Collections.Generic.EqualityComparer<string>.Default.GetHashCode(Flag);
hashCode = hashCode * -1521134295 + Priority.GetHashCode();
hashCode = hashCode * -1521134295 + System.Collections.Generic.EqualityComparer<string>.Default.GetHashCode(LogLevel);
return hashCode;
}
}
Listing 6-4The generated LoggerClass
这不是很时髦吗?
将局部函数转换为方法
让我们继续使用我们从匿名类型创建的LoggerClass
。我将在这个类中添加一个名为AddLogEntry
的方法。该方法将包含一个名为DetermineLogLevelPriority
的本地函数,该函数只接受LogLevel
属性值并为其返回一个整数值。
图 6-47
DetermineLogLevelPriority 局部函数
局部函数使用一个开关返回传递给类的LogLevel
值的整数值(图 6-47 )。
如果switch
的陈述对你来说看起来有点滑稽,看看本书第三章中的 switch 表达式。开关表达式是 C# 8 的一个新的语言特性。
我个人真的很喜欢局部函数。让我们假设DetermineLogLevelPriority
方法现在不再作为本地函数使用。这可能是因为需要本地函数提供的逻辑,在类的其他地方。在 Visual Studio 2019 中,我们可以通过将光标放在局部函数名前面,按住 Ctrl+来将局部函数转换为方法。或 Alt+Enter 选择转换为方法选项(图 6-48 )。
图 6-48
转换为方法
Visual Studio 将本地函数重构为一个方法,您现在可以从类中的任何位置调用该方法。
图 6-49
转换为方法的局部函数
像这样的代码重构节省了你大量的重写代码和复制粘贴代码的时间。
在 ASP.NET 项目中启用 JavaScript 调试
如果您为 ASP.NET 项目在 Browse With 菜单中创建新的浏览器配置,Visual Studio 2019 将在您启动调试会话时为您的项目启用 JavaScript 调试。继续创建一个新的 ASP.NET MVC 应用。
图 6-50
新 ASP.NET Web 应用对话框
你会从图 6-50 中看到,这个对话框也有了很好的修改。我们将坚持这里的默认设置,只需点击创建按钮。当您的项目被创建时,右键单击视图下 Home 文件夹中的 Index.cshtml 文件,并选择 Browse With。
图 6-51
添加新的浏览设置
选择 Google Chrome 的路径并传递--incognito
参数,如图 6-51 所示。然后给它一个友好的名称,并单击确定。将 Chrome Incognito 设置为默认设置,然后退出浏览界面。
双击 Index.cshtml 页面并查看代码。添加一个变量来保存当前的日期和时间值(图 6-52 )。
图 6-52
为今天的日期添加变量
在页面的底部,添加一个脚本部分,只将该值记录到控制台窗口中。然后在包含您的dateTime
变量的这行代码上放置一个断点。断点如图 6-53 所示。
图 6-53
在 JavaScript 代码中添加断点
您现在可以开始调试了。
确保您的 Index.cshtml 页面上的部分的名称“脚本”与您的 _Layout.cshtml 文件中的@RenderSection
代码的名称相匹配。
点击IIS Express(Chrome incognity)开始按钮,启动你的调试会话(图 6-54 )。
图 6-54
用 Chrome 匿名调试
Visual Studio 现在检测到我在一些 JavaScript 代码中添加了断点,并显示如下 JavaScript 调试警告消息,如图 6-55 所示。
图 6-55
JavaScript 调试警告
如果您在这里选择了启用 JavaScript 调试,Visual Studio 会在工具、选项、调试、常规,启用 ASP.NET(Chrome、Edge 和 IE)的 JavaScript 调试中为您设置此选项,如图 6-56 所示。
图 6-56
选项中启用的 JavaScript 调试
转到这个选项,您会看到它现在已被选中。
导出编辑器设置
如果你在一个团队中工作,你可以使用一个名为 EditorConfig 的文件来为你的项目实施某些编码风格。一个 EditorConfig 文件的好处在于,你可以将它签入到源代码控制中,并让它随着每次新的回购而移动。
在 Visual Studio 2019 中,您现在可以将您的代码样式设置导出到一个 EditorConfig 文件中,如图 6-57 所示。你可以在工具 ➤ 选项 ➤ 文本编辑器 ➤ C# ➤ 代码样式 ➤ 通用中找到这个选项。
图 6-57
生成。来自设置的编辑器配置文件
您会注意到,我指定了我对‘var’的偏好,并将严重性配置为显示为警告。点击生成。editorconfig 文件从设置按钮将所有这些代码风格偏好导出到。editorconfig 文件如图 6-58 所示。
图 6-58
已生成。编辑器配置文件
该文件现在决定代码的样式,因为该文件优先于全局 Visual Studio 文本编辑器设置。您仍然可以在 Visual Studio 选项对话框中设置自己的样式首选项,但是这些样式首选项将只应用于不包含的项目中。editorconfig 文件或中的样式。editorconfig 文件不会取代您设置的样式首选项。
图 6-59
记录健康指标
同。editorconfig 文件应用到我的项目时,我立即根据我设置的首选项看到一些警告(图 6-59 )。从事同一项目的其他开发人员也会看到这些警告。
如果我查看我的代码,我会看到在显式类型下出现一些弯弯曲曲的线条(图 6-60 )。
图 6-60
出现弯曲的线条
图 6-61 还显示了一些在中根据我的喜好显示的警告。编辑器配置文件。
图 6-61
为显式类型显示的警告
正在打开。editorconfig 文件,我可以看到我设置并导出的样式首选项(图 6-62 )。
图 6-62
那个。编辑器配置文件
另一件需要注意的事情是,Visual Studio 会通过状态栏清楚地通知您它正在使用一个。用户偏好的 editorconfig 文件(图 6-63 )。
图 6-63
状态显示。编辑器配置正在使用中
能够导出您的代码样式首选项允许您和您的团队轻松地共享代码样式首选项,并在您的几个或所有项目中保持一致的编码样式。
使用 AI 的 Visual Studio IntelliCode
在 2018 年的 Build 期间,微软宣布了人工智能驱动的 Visual Studio IntelliCode。它旨在通过提供关于上下文代码完成、推断样式规则和代码格式的建议来提高开发人员的生产力。它可用于 Visual Studio 2017、Visual Studio 2019 和 Visual Studio 代码,作为可选扩展,如图 6-64 所示。
图 6-64
Visual Studio IntelliCode 扩展
IntelliCode 的建议是基于成千上万的开源回购的学习模式。在 Visual Studio 中安装了该扩展后,您将看到 IntelliCode 的基本模型为 IntelliSense 提供了一些建议。这不同于通常显示的字母顺序。
图 6-65
IntelliCode 带星号的建议
有趣的是,建议可能是您最有可能对字符串执行的操作。IntelliCode 知道这一点,因为它已经在其基本模型中收集了这些信息。
然而,如果我想从我的Human
类中得到同样的功能,很遗憾我没有看到任何带星号的推荐(图 6-66 )。这个类不是一个开源类,它只存在于我的项目中。
图 6-66
没有关于人类阶级的建议
这是因为 IntelliCode 没有构建任何自定义模型来提供建议。为了构建这些自定义模型,您需要打开 IntelliCode 窗口,并在您的代码基础上对其进行训练。它目前在视图 ➤ 其他窗口 ➤ IntelliCode 下,但我预计这将在 Visual Studio 2019 即将发布的版本中发生变化。找到 IntelliCode 窗口最简单的方法可能是使用 Visual Studio 2019 中优秀的搜索功能。
点击 Ctrl+Q 开始打字(图 6-67 )。然后单击第一个结果打开 IntelliCode 窗口。
图 6-67
搜索并打开 IntelliCode 窗口
一旦 IntelliCode 窗口打开(图 6-68 ),您将会注意到没有为当前解决方案训练任何模型。训练所做的是分析你的代码,上传你的元数据到云端,学习你的代码模式。
代码分析发生在您的机器上,并提取有关您的代码的信息,这些信息被发送到 IntelliCode 的模型服务。然后,它被上传到云中,在那里生成一个模型,该模型被发送回您机器上的 IntelliCode。
需要注意的是,您的任何代码都不会上传到 IntelliCode 云服务。只有元数据被发送到云端,所以你所有的源代码都在你的机器上。
随着自定义模型的生成,IntelliCode 现在可以为您的自定义类和类型提供带星号的建议。
图 6-68
智能代码窗口
根据代码库的大小,培训过程可能需要几分钟才能完成。一旦培训完成(图 6-69 ,您将在 IntelliCode 窗口中看到以下信息。
图 6-69
代码培训已完成
训练自定义模型时会显示日期。如果需要,您可以共享或删除模型。您也可以随时重新训练您的 IntelliCode。另一个有趣的细节是,IntelliCode 窗口为您提供了模型细节部分中培训内容的要点。
图 6-70
使用自定义模型的 IntelliCode 建议
回到你的代码,如果你看一下带星号的建议,你会看到FullName
方法是带星号的(图 6-70 )。它现在知道了这一点,因为它已经分析了我是如何编写代码的,以及我的类和类型是什么样子的。
微软已经启用了智能代码
-
Visual Studio 中的 XAML
-
Visual Studio 中的 C++
-
Visual Studio 代码中的 JavaScript/TypeScript
-
Visual Studio 代码中的 Java
IntelliCode 是一个优秀的生产力工具,它基于您自己的代码来提高您的生产力。人工智能在 Visual Studio 中的力量。
常规 Visual Studio 提示
Visual Studio 为您提供了极大的灵活性。在我看来,这是 IDEs 的黄金标准。正如本章前面提到的,一些更好的技巧和特性可能会被忽略。在我们工作的快节奏行业中尤其如此。以下提示并不特定于 Visual Studio 2019(尽管围绕我详述的功能的一些细节可能是),并为开发人员提供了很多价值。
使用实时单元测试
单元测试在你的代码中是非常重要的。使用单元测试可以确保您编写的代码在您更改和改进代码时继续工作。之所以称之为单元测试,是因为您将代码的较小部分分解并作为单独的单元进行测试。
因此,单元测试的好处可以定义如下:
-
防止回归(当您的代码改变时)
-
查看方法结果(可执行文件)
-
单元测试迫使你分离你的代码
Visual Studio 包含测试资源管理器,通过该窗口可以查看单元测试的结果并再次运行失败的测试。
当我们谈论解耦代码时,我们的意思是说如果你的测试很复杂或者很难写,那么就简化被测试的代码。
为代码增值的单元测试的特征如下:
-
即使在大型项目中,测试也会运行得很快。
-
您的测试应该能够独立运行,没有任何外部依赖性,比如文件或数据库。
-
您可以多次运行相同的测试,如果您不更改任何代码,则返回相同的结果。
当然,单元测试还有许多其他方面,但这本身就可以写满一本书。让我们看看使用 Visual Studio 在一些简单的代码中如何使用单元测试。考虑下面的代码清单。
public static void PrintDate(string date)
{
WriteLine($"The date is {date}");
}
Listing 6-5Method to test
如您所见,该方法所做的只是将传递给该方法的日期打印到控制台窗口。
考虑这样一种可能性:在将日期打印到控制台窗口之前,您需要确保日期采用特定的格式。
图 6-71
创建单元测试
为此,我们可以通过右击方法并从上下文菜单中选择创建单元测试来创建单元测试(图 6-71 )。
Visual Studio 随后会显示一个对话框窗口(图 6-72 ,您可以在其中配置正在创建的单元测试。在下面的例子中,你会看到我使用 MSTestv2 作为测试框架。
图 6-72
单元测试选项
您可以安装并选择其他单元测试框架来与您的单元测试一起使用。安装完其他框架后,只需重启 Visual Studio,就可以从下拉菜单中选择它们了。
还要注意,如果您有依赖于外部依赖项的方法,您可以创建存根来模拟外部依赖项的功能。
Visual Studio Enterprise 允许使用 Microsoft Fakes 为外部依赖项创建替代类。
配置完单元测试属性后,单击“确定”按钮。这将在您的解决方案浏览器中创建一个新的测试项目,如图 6-73 所示。
图 6-73
测试项目已创建
您会注意到现在已经添加了一些样板代码,其中包含了您想要测试的方法。
[TestClass()]
public class ProgramTests
{
[TestMethod()]
public void PrintDate ()
{
Assert.Fail();
}
}
Listing 6-6Created Test Class
当您迭代您的代码库时,您可以扩展您创建的测试方法并更改被测试的代码。
用PrintDate
方法测试的代码一点也不复杂,你肯定会在测试方法上做更多的扩展,而不是简单地把Assert.Fail
留在那里。
正是在这里,您通常能够确定被测试的代码是否过于复杂或者过于紧密耦合。然后,您就能够简化被测试的方法,并且您会发现为您的代码创建单元测试变得更加容易。
图 6-74
开始实时单元测试
Visual Studio 包含了实时单元测试特性,允许您在后台运行测试。这意味着当你修改和添加代码时,测试结果会实时呈现给你(图 6-74 )。
如果您添加了额外的代码(例如,一个新方法),Visual Studio 将通知您该方法是否被单元测试覆盖。这是一个很好的提醒,提醒你在进行过程中要编写单元测试。
您可以通过进入工具 ➤ 选项 ➤ 现场单元测试 ➤ 常规来配置现场单元测试的常规设置。在这里,您可以限制用于实时单元测试的内存使用,定义测试进程的最大数量,输入测试用例的超时值,等等。
图 6-75
从 VS2019 中的解决方案资源管理器运行测试
从 Visual Studio 2019 开始,您现在可以直接从解决方案资源管理器中运行单元测试,如图 6-75 所示。这确实是对 Visual Studio 的一个坚实的补充,因为它在运行测试时为您提供了更多的灵活性。
从 XML 和 JSON 生成类
Visual Studio 的另一个经常被忽略的特性是从 XML 或 JSON 代码创建类。这意味着您可以复制以下 XML(例如)并将其粘贴为 Visual Studio 创建的类。
<restaurant>
<food>
<name>Hamburger</name>
<price>$5.95</price>
<description>160g patty</description>
<calories>875</calories>
</food>
<food>
<name>Farmhouse Breakfast</name>
<price>$6.95</price>
<description>Two eggs, bacon or sausage, toast, and hash brown. Bottomless coffee</description>
<calories>820</calories>
</food>
</restaurant>
Listing 6-7Sample XML
为此,直接进入编辑 ➤ 选择性粘贴 ➤ 将 XML 粘贴为类或将 JSON 粘贴为类菜单(图 6-76 )。这将自动输出一个映射到您之前复制的 XML 或 JSON 代码的类。
图 6-76
选择性粘贴
对于任何使用大量 XML 或 JSON 的人来说,这是一个非常好的时间节省器。下面我们再来看看 SYSPRO 的开发者们。
C# 交互式
有多少次你正在编写还没有准备好运行的代码,但是你真的需要测试一些功能?仅仅为了测试代码的一小部分而注释掉代码可能会很痛苦。启动一个完整的调试会话也是一件痛苦的事情,尤其是当您在一个带有登录页面的 web 应用上工作,并且您想要测试的代码位于几层深的子菜单中时。调试五次。
在登录、导航到需要调试的页面并等待断点命中的最初几次迭代之后,世界似乎变得不那么明亮了。再加上一点冗长的编译,你就有了一些令人沮丧的东西。
这才是 C# Interactive 真正大放异彩的地方。假设我有一些想要调试的小代码。考虑图 6-77 中的例子。
图 6-77
一些要测试的代码
代码并不复杂,但我想确保我的代码正确地重写了字符串,将改为而不是主要是。
选择想要运行的代码,点击右键,在上下文菜单中点击执行交互(图 6-78 )。
图 6-78
以交互方式执行
你也可以按住 Ctrl+E,Ctrl+E 来做同样的事情。
您选择的代码将显示在 C# 交互窗口中,如图 6-79 所示,并将运行以产生输出。
图 6-79
C# 交互式
如你所见,我的代码中有一个 bug。单词主要是没有被替换,我的文本中没有空格。
图 6-80
找到窃丨听丨器
我现在需要找到我的 bug 并修复它,你能相信吗……它就在那里(图 6-80 )。我忘了给newText
变量加上textToAppend
变量值。我需要重写我的代码,如下所示:newText += $"{textToAppend} ";
修复代码很快,然后我可以在 C# 交互中再次运行代码,以检查它是否正常工作(图 6-81 ): https://docs.microsoft.com/en-us/visualstudio/ide/using-intellisense?view=vs-2017
。
图 6-81
测试错误修复
这一次,输出是我所期望的。
有更好的方式来编写这个逻辑,但我只是在这里说明一点。
C# Interactive 是一个调试工具,它允许快速、迭代地运行代码,而无需求助于完整的调试会话来测试一小部分代码。
包扎
Visual Studio 是一个功能丰富的 IDE,这一点毋庸置疑。我可以没完没了地谈论 Visual Studio 中的技巧和诀窍、特性和精华。在本章中,我们了解了 Visual Studio 2019 中可供开发人员使用的新功能。
我们讨论了 Visual Studio Live Share,并了解了如何在不同的 ide 和不同的平台上协作完成一个项目。我们看到了如何执行一些有用的重构和代码修复。然后我们看了看如何定义代码的样式规则,并将它们导出到一个。编辑器配置文件。我们还看到了 Visual Studio IntelliCode 如何为开发人员带来人工智能的力量。
Visual Studio 2019 的发布为开发者的工具带带来了更多的生产力特性。我们能够更快更准确地编写代码。我们可以更容易地与团队成员合作,更清楚地表达我们的意图。对于开发人员来说,未来看起来非常光明,而 Visual Studio 让它更加光明。