Always keep a beginner's m|

石起起

园龄:1年10个月粉丝:1关注:0

2025-02-05 17:51阅读: 15评论: 0推荐: 0

C# 版本 8 新增特性

发布时间:2019 年 9 月

C# 8.0 版是专门面向 .NET C# Core 的第一个主要 C# 版本。 一些功能依赖于新的公共语言运行时 (CLR) 功能,而其他功能则依赖于仅在 .NET Core 中添加的库类型。 C# 8.0 向 C# 语言添加了以下功能和增强功能:

默认接口成员需要 CLR 中的增强功能。 这些功能已添加到 .NET Core 3.0 的 CLR 中。 范围和索引以及异步流需要 .NET Core 3.0 库中的新类型。 在编译器中实现时,可为 null 的引用类型在批注库时更有用,因为它可以提供有关参数和返回值的 null 状态的语义信息。 这些批注将添加到 .NET Core 库中。


参考文章
C#8.0 新增功能 - 张传宁 - 博客园
C#8.0 中使用默认接口成员更新接口 - 张传宁 - 博客园 ❓ 暂未阅读


笔记

Readonly 成员

可将 readonly 修饰符应用于结构的任何成员。它指示该成员不会修改状态。这比将 readonly 修饰符应用于 struct 声明更精细。请考虑以下可变结构:A

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

warning CS8656: Call to non-readonly member 'Point.Distance.get' from a 'readonly' member results in an implicit copy of 'this'

需要创建防御性副本时,编译器会发出警告。 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;
}

![[Pasted image 20241218204523.png]]

通过此功能,可以指定设计意图,使编译器可以强制执行该意图,并基于该意图进行优化。

默认接口成员 ❗

接口关键字 - C# reference | Microsoft Learn

现在可以将成员添加到接口,并为这些成员提供实现。 借助此语言功能,API 作者可以将方法添加到以后版本的接口中,而不会破坏与该接口当前实现的源或二进制文件兼容性。 现有的实现继承默认实现 。 此功能使 C# 与面向 Android 或 Swift 的 API 进行互操作,此类 API 支持类似功能。 默认接口成员还支持类似于“特征”语言功能的方案。

默认接口成员会影响很多方案和语言元素。 请参考 C#8.0 中使用默认接口成员更新接口

模式匹配增强功能

模式 - 使用 is 和 switch 表达式进行模式匹配。 - C# reference | Microsoft Learn

switch 表达式

public abstract class Vehicle {}
public class Car : Vehicle {}
public class Truck : Vehicle {}

public static class TollCalculator
{
    public static decimal CalculateToll(this Vehicle vehicle) => vehicle switch
    {
        Car _ => 2.00m,
        Truck _ => 7.50m,
        null => throw new ArgumentNullException(nameof(vehicle)),
        _ => throw new ArgumentException("Unknown type of a vehicle", nameof(vehicle)),
    };
}

这里有几个语法改进:

  • 变量位于 switch 关键字之前。 不同的顺序使得在视觉上可以很轻松地区分 switch 表达式和 switch 语句。
  • 将 case 和 : 元素替换为 =>。 它更简洁,更直观。
  • 将 default 事例替换为 _ 弃元。
  • 正文是表达式,不是语句。

属性模式 ⭐

public static decimal ComputeSalesTax(Address location, decimal salePrice) => location switch
{
	{ State: "WA" } => salePrice * 0.06M,  // 💡 如果location的属性State==WA...
	{ State: "MN" } => salePrice * 0.75M,
	{ State: "MI" } => salePrice * 0.05M,
	// other cases removed for brevity...
	_ => 0M
};

可以使用属性模式将表达式的属性或字段与嵌套模式进行匹配,如以下示例所示:

static bool IsConferenceDay(DateTime date) => date is { Year: 2020, Month: 5, Day: 19 or 20 or 21 };
// 💡 尖括号中的内容为嵌套模式

当表达式结果为非 NULL 且每个嵌套模式都与表达式结果的相应属性或字段匹配时,属性模式将与表达式匹配。

还可将运行时类型检查和变量声明添加到属性模式,如以下示例所示:

Console.WriteLine(TakeFive("Hello, world!"));  // output: Hello
Console.WriteLine(TakeFive("Hi!"));  // output: Hi!
Console.WriteLine(TakeFive(new[] { '1', '2', '3', '4', '5', '6', '7' }));  // output: 12345
Console.WriteLine(TakeFive(new[] { 'a', 'b', 'c' }));  // output: abc

static string TakeFive(object input) => input switch
{
    string { Length: >= 5 } s => s.Substring(0, 5),
    string s => s,

    ICollection<char> { Count: >= 5 } symbols => new string(symbols.Take(5).ToArray()),
    ICollection<char> symbols => new string(symbols.ToArray()),

    null => throw new ArgumentNullException(nameof(input)),
    _ => throw new ArgumentException("Not supported input type."),
};

属性模式是一种递归模式。 也就是说,可以将任何模式用作嵌套模式。 使用属性模式将部分数据与嵌套模式进行匹配,如以下示例所示:

public record Point(int X, int Y);
public record Segment(Point Start, Point End);

static bool IsAnyEndOnXAxis(Segment segment) =>
    segment is { Start: { Y: 0 } } or { End: { Y: 0 } }; // ❓ is

上一示例使用 or 模式结合法记录类型

从 C# 10 开始,可引用属性模式中嵌套的属性或字段。 该功能称为“扩展属性模式”。 例如,可将上述示例中的方法重构为以下等效代码:

static bool IsAnyEndOnXAxis(Segment segment) =>
    segment is { Start.Y: 0 } or { End.Y: 0 };

元组模式

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 readonly struct Point
{
    public int X { get; }
    public int Y { get; }

    public Point(int x, int y) => (X, Y) = (x, y);

    public void Deconstruct(out int x, out int y) => (x, y) = (X, Y);
}

static string Classify(Point point) => point switch
{
    (0, 0) => "Origin",
    (1, 0) => "positive X basis end",
    (0, 1) => "positive Y basis end",
    _ => "Just a point",
};

此外,请考虑以下表示象限的各种位置的枚举:

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 表达式中涵盖所有可能的情况,编译器将生成一个警告。

可在此模式匹配高级教程中探索模式匹配方法。

using 声明

using 语句 - 确保正确使用可释放对象 - C# reference | Microsoft Learn

using 声明 是前面带 using 关键字的变量声明。 它指示编译器声明的变量应在封闭范围的末尾进行处理。 以下面编写文本文件的代码为例:

static void WriteLinesToFile(IEnumerable<string> lines)
{
    using var file = new System.IO.StreamWriter("WriteLines2.txt");
    foreach (string line in lines)
    {
        // 如果该行不包含单词“second”,则将该行写入文件。
        if (!line.Contains("Second"))
        {
            file.WriteLine(line);
        }
    }
    // 文件已在此处释放
}

静态本地函数

本地函数 - C# | Microsoft Learn

现在可以向本地函数添加 static 修饰符,以确保本地函数不会从封闭范围捕获(引用)任何变量。 这样做会生成 CS8421,“静态本地函数不能包含对<variable>的引用”。

考虑下列代码。 本地函数 LocalFunction 访问在封闭范围(方法 M)中声明的变量 y。 因此,不能用 static 修饰符来声明 LocalFunction

int M()
{
    int y;
    LocalFunction();
    return y;

    void LocalFunction() => y = 0;
}

下面的代码包含一个静态本地函数。 它可以是静态的,因为它不访问封闭范围中❓的任何变量:

int M()
{
    int y = 5;
    int x = 7;
    return Add(x, y);  // ❓

    static int Add(int left, int right) => left + right;
}

可处理的 ref 结构

ref 结构类型 - C# reference | Microsoft Learn

用 ref 修饰符声明的 struct 可能无法实现任何接口,因此无法实现 IDisposable。 因此,要能够处理 ref struct,它必须有一个可访问的 void Dispose() 方法。 这同样适用于 readonly ref struct 声明。

可为空引用类型

可为空引用类型 - C# reference | Microsoft Learn

在可为空注释上下文中,引用类型的任何变量都被视为不可为空引用类型 。 若要指示一个变量可能为 null,必须在类型名称后面附加 ?,以将该变量声明为可为空引用类型 。

对于不可为空引用类型,编译器使用流分析来确保在声明时将本地变量初始化为非 Null 值。 字段必须在构造过程中初始化。 如果没有通过调用任何可用的构造函数或通过初始化表达式来设置变量,编译器将生成警告。 此外,不能向不可为空引用类型分配一个可以为 Null 的值。

不对可为空引用类型进行检查以确保它们没有被赋予 Null 值或初始化为 Null。 不过,编译器使用流分析来确保可为空引用类型的任何变量在被访问或分配给不可为空引用类型之前,都会对其 Null 性进行检查。

可以在可为空引用类型的概述中了解该功能的更多信息。 可以在此可为空引用类型教程中的新应用程序中自行尝试。 在迁移应用程序以使用可为空引用类型教程中了解迁移现有代码库以使用可为空引用类型的步骤。

异步流 ⭐ ❓需要加强理解

迭代语句 - for、foreach、do 和 while - C# reference | Microsoft Learn

创建并以异步方式使用流❗❓
返回异步流的方法有三个属性:

  1. 它是用 async修饰符声明的;
  2. 它将返回 IAsyncEnumerable<T>
  3. 该方法包含用于在异步流中返回连续元素的 yield return 语句。

使用异步流需要在枚举流元素时在 foreach 关键字前面添加 await 关键字。添加 await 关键字需要枚举异步流的方法,以使用 async 修饰符进行声明并返回 async 方法允许的类型。通常这意味着返回 TaskTask<Result> 。也可以为 ValueTaskValueTask<TResult> 。方法既可以使用异步流,也可以生成异步流❓,这意味着它将返回 IAsyncEnumerable<T> 。下面代码生成一个从 0 到 19 的序列,在生成每个数字之间等待 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 语句来使用异步数据流,即实现 IAsyncEnumerable<T>(https://learn.microsoft.com/zh-cn/dotnet/api/system.collections.generic.iasyncenumerable-1) 接口的集合类型。 异步检索下一个元素时,可能会挂起循环的每次迭代。 下面的示例演示如何使用 await foreach 语句:

await foreach (var number in GenerateSequence())
{
    Console.WriteLine(number);
}

还可以将 await foreach 语句与满足以下条件的任何类型的实例一起使用:

  • 类型具有公共无参数 GetAsyncEnumerator 方法。 该方法可以是类型的扩展方法
  • GetAsyncEnumerator 方法的返回类型具有公共 Current 属性和公共无参数 MoveNextAsync 方法(其返回类型为 Task<bool>ValueTask<bool> 或任何其他可等待类型,其 awaiter 的 GetResult 方法返回 bool 值)。

默认情况下,在捕获的上下文中处理流元素。 如果要禁用上下文捕获,请使用 TaskAsyncEnumerableExtensions.ConfigureAwait 扩展方法。 有关同步上下文并捕获当前上下文的详细信息,请参阅使用基于任务的异步模式。 有关异步流的详细信息,请参阅异步流教程

索引和范围

成员访问和 Null 条件运算符和表达式: - C# reference | Microsoft Learn

范围和索引为在数组中指定子范围 Span<T> 或 ReadOnlySpan<T>提供了简洁语法。

此语言支持依赖于两个新类型和两个新运算符。

  • System.Index 表示一个序列索引。
  • ^ 运算符,指定一个索引与序列末尾相关。
  • System.Range 表示序列的子范围。
  • 范围运算符 (..),用于指定范围的开始和末尾,就像操作数一样。

让我们从索引规则开始。 请考虑数组 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]}");
// writes "dog"

以下代码创建了一个包含单词“quick”、“brown”和“fox”的子范围。 它包括 words[1] 到 words[3]。 元素 words[4] 不在此范围内。

var quickBrownFox = words[1..4];

以下代码使用“lazy”和“dog”创建一个子范围。 它包括 words[^2] 和 words[^1]。 不包括结束索引 words[^0]

var lazyDog = words[^2..^0];

下面的示例为开始和/或结束创建了开放范围:

var allWords = words[..];      // contains "The" through "dog".
var firstPhrase = words[..4];  // contains "The" through "fox"
var lastPhrase = words[6..];   // contains "the", "lazy" and "dog"

此外可以将范围声明为变量:

Range phrase = 1..4;

然后可以在 [ 和 ] 字符中使用该范围:

var text = words[phrase];

可在有关索引和范围的教程中详细了解索引和范围。

本文作者:石起起

本文链接:https://www.cnblogs.com/myshiqiqi/p/18699906

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   石起起  阅读(15)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起