-
C#教程之C#8.0 新增功能
试听地址 https://www.xin3721.com/eschool/CSharpxin3721/
1、Readonly成员
可将readonly修饰符应用于结构的任何成员,它指示该成员不会修改状态。这比将readonly修饰符应用于struct声明更精细。
public struct Point { public double X { get; set; } public double Y { get; set; } public double Distance => Math.Sqrt(X * X + Y * Y); public override string ToString() => $"({X}, {Y}) is {Distance} from the origin"; }
像大多数结构一样ToString()方法不会修改状态。可以通过readonly修饰符添加到ToString()的声明来对此进行指示:
public readonly override string ToString() => $"({X}, {Y}) is {Distance} from the origin";
上述更改会发生编译器警告,因为ToString访问Distance属性,该属性未标记为readonly,如下图所示:
需要创建防御性副本时,编译器会发出警告,Distance属性不会更改状态,因此可以通过将readonly修饰符添加到声明来修复此警告:
public readonly double Distance => Math.Sqrt(X * X + Y * Y);
请注意,readonly修饰符对于只读属性是必须的。编译器不会假设get访问器不修改状态;必须明确声明readonly,编译器会强制实施以下规则:
readonly成员不修改状态,除非readonly修饰符,否则不会编译以下方法:
public readonly void Translate(int xOffset,int yOffset) { X += xOffset; Y += yOffset; }
通过此功能,可以指定设计意图,使编译器可以强制执行该意图,并基于该意图进行优化。
2、默认接口成员
现在可以将成员添加到接口,并未这些成员提供实现。借助次语言功能,API坐着可以将方法添加到以后版本的接口中,而不会破坏与该接口当前实现的源或耳机中南海文件兼容性。现在有的实现继承默认接口。此功能使C#与面向Android 或Swift的API进行互操作,此类API支持类似功能。默认接口成员还支持类似于“特征”语言功能的方案。
默认接口成员会影响很多方案和语言元素。https://docs.microsoft.com/zh-cn/dotnet/csharp/tutorials/default-interface-methods-versions
3、在更多位置中使用更多模式
模式匹配提供了在相关但不同类型的数据中提供形状相关功能的工具。C#7.0通过使用is表达式和switch语句引入了类型模式和常量模式的语法。这些功能代表了支持数据和功能分离时的编程范例的初步尝试。随着行业转向更多微服务和其他基于云的体系结构,还需要其他语言功能。
C#8.0扩展了此词汇表。这样就可以在代码中的更多位置使用更多模式表达式。当数据和功能分离时,请考虑使用这些功能。当算法依赖于对象运行时类型以外的事实时,请考虑使用模式匹配。这些技术提供了另一种表达设计的方式。
除了烤鱼在新位置使用新模式之外,C#8.0还添加了“递归模式”。任何模式表达式的结果都是一个表达式。递归模式只是应用于另外一个模式表达式输出的模式表达式。
switch表达式
通常情况下,switch语句在其每个case块中生成一个值。借助Switch表达式,可以使用更简洁的表达式语句,只是些许重复的case和break关键字额大括号。以下面列出彩虹颜色的枚举为例:
public enum Rainbow { Red, Orange, Yellow, Green, Blue, Indigo, Violet }
如果应用定义了通过R、G、B组件构造而成的RGBColor类型,可使用以下包含switch表达式的方法,将Rainbow转换为RGB值:
public static RGBColor FromRainbow(Rainbow colorBand) => colorBand switch { Rainbow.Red => new RGBColor(0xFF, 0x00, 0x00), Rainbow.Orange => new RGBColor(0xFF, 0x7F, 0x00), Rainbow.Yellow => new RGBColor(0xFF, 0xFF, 0x00), Rainbow.Green => new RGBColor(0x00, 0xFF, 0x00), Rainbow.Blue => new RGBColor(0x00, 0x00, 0xFF), Rainbow.Indigo => new RGBColor(0x4B, 0x00, 0x82), Rainbow.Violet => new RGBColor(0x94, 0x00, 0xD3), _ => throw new ArgumentException(message: "invalid enum value", paramName: nameof(colorBand)), };
这里有几个语法改进:
1、变量位于switch关键字之前。不同的顺序使得在视觉上可以很轻松的区分switch表达式和switch语句。
2、将case和:元素替换为=>,它更简洁,更直观。
3、将default事例替换为_弃元。
4、正文是表达式,不是语句。
将其与使用经典switch语句的等效代码进行对比:
public static RGBColor FromRainbowClassic(Rainbow colorBand) { switch (colorBand) { case Rainbow.Red: return new RGBColor(0xFF, 0x00, 0x00); case Rainbow.Orange: return new RGBColor(0xFF, 0x7F, 0x00); case Rainbow.Yellow: return new RGBColor(0xFF, 0xFF, 0x00); case Rainbow.Green: return new RGBColor(0x00, 0xFF, 0x00); case Rainbow.Blue: return new RGBColor(0x00, 0x00, 0xFF); case Rainbow.Indigo: return new RGBColor(0x4B, 0x00, 0x82); case Rainbow.Violet: return new RGBColor(0x94, 0x00, 0xD3); default: throw new ArgumentException(message: "invalid enum value", paramName: nameof(colorBand)); }; }
属性模式:
借助属性模式,可以匹配所有检查的对象的属性。请看一个电子商务网站的事例额,该网站必须根据买家地址计算销售税,这种计算不时Address类的核心职责。它会随时间变化,可能比地址格式的更改更频繁,销售税的金额取决于地址的State属性。下面的方法使用属性模式从地址和价格计算销售税:
public static decimal ComputeSalesTax(Address location, decimal salePrice) => location switch { { State: "WA" } => salePrice * 0.06M, { State: "MN" } => salePrice * 0.75M, { State: "MI" } => salePrice * 0.05M, // other cases removed for brevity... _ => 0M };
模式匹配为表达此算法创建了简介的语法
元素模式:
一些算法依赖于多个输入,使用元组模式,可以根据表示为元组的多个值进行。以下代码显示了游戏“rock,pack,scissors(石头剪刀布)”的切换表达式
public static string RockPaperScissors(string first, string second) => (first, second) switch { ("rock", "paper") => "rock is covered by paper. Paper wins.", ("rock", "scissors") => "rock breaks scissors. Rock wins.", ("paper", "rock") => "paper covers rock. Paper wins.", ("paper", "scissors") => "paper is cut by scissors. Scissors wins.", ("scissors", "rock") => "scissors is broken by rock. Rock wins.", ("scissors", "paper") => "scissors cuts paper. Scissors wins.", (_, _) => "tie" };
消息显示获胜者,弃元表示平局(石头剪刀布游戏)的三种组合或其他文本输入。
位置模式
某些类型包含Deconstruct方法,就可以使用位置模式,该方法将其属性解构为离散变量。如果可以访问Deconstruct方法,就可以使用位置模式检查对象的属性并将这些属性用于模式。考虑以下Point类。其中包含用于为X和Y创建离散变量的Deconstruct方法:
public class Point { public int X { get; set; } public int Y { get; set; } public Point(int x, int y) => (X, Y) = (x, y); public void Deconstruct(out int x, out int y) => (x, y) = (X, Y); }
此外,请考虑以下表示象限的各种位置的枚举:
public enum Quadrant { Unknown, Origin, One, Two, Three, Four, OnBorder }
下面的方法使用位置模式来提取x和y的值,然后,它使用when自居来确定改点的Quadrant:
static Quadrant GetQuadrant(Point point) => point switch { (0, 0) => Quadrant.Origin, var (x, y) when x > 0 && y > 0 => Quadrant.One, var (x, y) when x < 0 && y > 0 => Quadrant.Two, var (x, y) when x < 0 && y < 0 => Quadrant.Three, var (x, y) when x > 0 && y < 0 => Quadrant.Four, var (_, _) => Quadrant.OnBorder, _ => Quadrant.Unknown };
当x或y为0(但不是两者同时为0)时,前一个开关中的弃元模式匹配,switch表达式必须要么生成值,要么引发异常。如果这些情况都不匹配,则switch表达式将引发异常。如果没有switch表达式中涵盖所有可能的情况,编译器将生成一个警告。
可以在模式匹配高级教程中探索模式匹配方法。https://docs.microsoft.com/zh-cn/dotnet/csharp/tutorials/pattern-matching
4、using声明
using声明是前面带using关键字的变量声明。它指示编译器声明的变量应该在封闭范围的末尾进行处理。以下面编写文本文件的代码为例:
static void WriteLinesToFile(IEnumerable<string> lines) { using var file = new System.IO.StreamWriter("WriteLines2.txt"); foreach (var line in lines) { //如果该行不包含单词“Second”,则将该行写入文件 if (!line.Contains("Second")) { file.WriteLine(line); } } //文件已在此处释放 }
在前面的示例中,当到达方法的右括号时,将对该文件进行处理,这是声明file的范围的末尾,前面的代码相当于下面使用经典using 语句的代码:
static void WriteLinesToFile(IEnumerable<string> lines) { using (var file=new System.IO.StreamWriter("WriteLines2.txt")) { foreach (var line in lines) { //如果该行不包含单词“Second”,则将该行写入文件 if (!line.Contains("Second")) { file.WriteLine(line); } } }//文件已在此处被释放 }
在前面的示例中,当到达与using语句关联的右括号时,将对该文件进行处理。在这两种情况下,编译器将生成对Dispose()的调用。如果using语句中的表达式不可处置,编译器将生成一个错误。
5、静态本地函数
现在可以向本地函数添加static修饰符,以确保本地函数不会从封闭范围捕获(引用)任何变量。这样做会生成CS8421,“静态本地函数不能包含对<variable>的引用”。
考虑下列代码,本地函数LocalFunction访问在封闭范围(方法M)中声明的变量y,因此,不能用static修饰符来声明LocalFunction:
int M() { int y; LocalFunction(); return y; void LocalFunction() => y = 0; }
如果在LocalFunction方法前面加上static:
下面的代码包含一个静态本地函数,它可以是静态的,因为它不访问封闭范围中的热呢变量:
int M() { int y = 5; int x = 7; return Add(x, y); static int Add(int left, int right) => left + right; }
6、可处置的ref结构
用ref修饰符声明的struct可能无法实现任何借口,因此无法实现IDisposable。因此,要能够处理ref struct,它必须有一个可访问的void Dispose()方法。这同样适用于readonly ref struct声明。
7、可谓空引用类型
在可谓空注释上下文中,引用类型的任何变量都被视为不可谓空引用类型。若要指示一个变量可能为null,必须在类型名称后面附加?,以将该变量声明为可谓空引用类型。
如果,不用?来声明,传统代码中,这样写,会报错:
static void Main(string[] args) { List<string> list = null; var newList = list.Where(s => s.Length > 2) ?? new List<string>(); foreach (var item in newList) { Console.WriteLine(item); } Console.WriteLine("ok"); Console.ReadLine(); }
改进之后,就没问题了:
static void Main(string[] args) { List<string> list = null; var newList = list?.Where(s => s.Length > 2) ?? new List<string>(); foreach (var item in newList) { Console.WriteLine(item); } Console.WriteLine("ok"); Console.ReadLine(); }
对于不可为空引用类型,编译器使用流分析来确保在声明时将本地变量初始化为非Null值,字段必须在构造过程中初始化,如果没有通过调用任何可用的构造函数或通过初始化表达式来设置变量,编译器将生成警告。此外,不能向不可为空引用类型分配一个可以为Null的值。
不对可为空引用类型进行检查以确保他们没有被赋予Null值或初始化为Null。不过,编译器使用流分析来确保可为空引用类型的任何变量在被访问或分配给不可为空引用类型之前,都会对其Null性进行检查。
8、异步流
从C#8.0开始,可以创建并以异步方式使用流。返回异步流的方法有三个属性:
1、它是用async修饰符声明的
2、它将返回IAsyncEnumerable<T>
3、该方法包含用于在异步流中返回连续元素的yield return 语句
使用异步流需要在枚举流元素时在foreach关键字前面添加await关键字。谈价await关键字需要枚举异步流的方法,以使用async修饰符进行声明并返回async方法允许的类型。通常在意味着返回Task或Task<TResult>。也可以为ValueTask或ValueTask<TResult>。方法既可以使用异步流,也可以生成异步流,这意味着它将返回IAsyncEnumerable<T>。下面的代码生成一个从0到10的序列,在生成没个数字之间等待100毫秒:
public static async System.Collections.Generic.IAsyncEnumerable<int> GenerateSequence() { for (int i = 0; i < 20; i++) { await Task.Delay(100); yield return i; } }
可以使用await foreach语句来枚举序列:
public static async void TestGenerateSequence() { await foreach (var item in GenerateSequence()) { Console.WriteLine(item); } }
9、索引和范围
范围和索引为在数组中指定资范围(Span<T>或ReadOnlySpan<T>)提供了简洁语法。
此语言支持依赖于两个新类型和两个新运算符。
1、System.Index表示一个序列索引
2、^运算符,指定一个索引与序列末尾相关。
3、System.Range表示序列的子范围
4、范围运算符(..),用于指定范围的开始和末尾,就像操作数一样。
让我们从索引规则开始。请考虑数组sequence。0索引与sequence[0]相同。^0索引与sequence[sequence.Length]相同。请注意,sequence[^0]不会引发异常,就像是sequence[sequence.Length]一样。对于任何数字n,索引^n与sequence.Length-n相同。
范围指定范围的开始和末尾。包括此范围的开始,但是不包括此范围的末尾,这表示此范围包含开始但不包含末尾。范围[0..^0]表示整个范围,就像[0..sequence.Length]表示整个范围。
请看以下一个示例,请考虑以下数组,用其顺数索引和倒数索引进行注释:
var words = new string[] { // index from start index from end "The", // 0 ^9 "quick", // 1 ^8 "brown", // 2 ^7 "fox", // 3 ^6 "jumped", // 4 ^5 "over", // 5 ^4 "the", // 6 ^3 "lazy", // 7 ^2 "dog" // 8 ^1 }; // 9 (or words.Length) ^0
可以使用^1索引检索最后一个词:
Console.WriteLine($"The last word is {words[^1]}");
以下代码创建了一个包含单词"quick"、"brown"和"fox"的子范围。它包括words[1]到words[3],元素words[4]不在此范围内。