Skip to content

C# 8.0 中的新增功能

🏷️ C# C# 新增功能

官方文档见 这里。本文在其基础上添加了一些自己理解及示例代码,如有不正确的地方欢迎指正。

Readonly 成员

可将 readonly 修饰符应用于 结构的成员 。它指示 该成员不会修改状态

7.2 中新增了 readonly struct 声明,将整个结构声明为只读的。这会导致该结构中不能有非只读的属性,也就是属性不能有 set 方法,这会使结构一旦创建后就不能再修改。

在属性上添加 readonly 修饰符可以提供更细粒度的控制,使结构性能提高的同时,使用也可以更加灵活。

csharp
public struct Point
{
    public double X { get; set; }
    public double Y { get; set; }
    public readonly double Distance => Math.Sqrt(X * X + Y * Y);

    public readonly override string ToString() =>
        $"({X}, {Y}) is {Distance} from the origin";
}

上例中的 Distance 属性和 ToString() 方法指定为 readonly 后,不必在每次调用时都重新计算,从而提高了性能。

下面是上面示例的 IL 代码:

csharp
.method /*06000001*/ public hidebysig specialname 
        instance float64  get_X() cil managed
// SIG: 20 00 0D
{
  .custom /*0C000001:0A00000D*/ instance void [System.Runtime/*23000001*/]System.Runtime.CompilerServices.IsReadOnlyAttribute/*01000010*/::.ctor() /* 0A00000D */ = ( 01 00 00 00 ) 
  .custom /*0C000002:0A00000B*/ instance void [System.Runtime/*23000001*/]System.Runtime.CompilerServices.CompilerGeneratedAttribute/*0100000D*/::.ctor() /* 0A00000B */ = ( 01 00 00 00 ) 
  // 方法在 RVA 0x2050 处开始
  // 代码大小       7 (0x7)
  .maxstack  8
  IL_0000:  /* 02   |                  */ ldarg.0
  IL_0001:  /* 7B   | (04)000001       */ ldfld      float64 ReadonlyMember.Point/*02000002*/::'<X>k__BackingField' /* 04000001 */
  IL_0006:  /* 2A   |                  */ ret
} // end of method Point::get_X
.method /*06000002*/ public hidebysig specialname 
        instance void  set_X(float64 'value') cil managed
// SIG: 20 01 01 0D
{
  .custom /*0C00000F:0A00000B*/ instance void [System.Runtime/*23000001*/]System.Runtime.CompilerServices.CompilerGeneratedAttribute/*0100000D*/::.ctor() /* 0A00000B */ = ( 01 00 00 00 ) 
  // 方法在 RVA 0x2058 处开始
  // 代码大小       8 (0x8)
  .maxstack  8
  IL_0000:  /* 02   |                  */ ldarg.0
  IL_0001:  /* 03   |                  */ ldarg.1
  IL_0002:  /* 7D   | (04)000001       */ stfld      float64 ReadonlyMember.Point/*02000002*/::'<X>k__BackingField' /* 04000001 */
  IL_0007:  /* 2A   |                  */ ret
} // end of method Point::set_X
.method /*06000003*/ public hidebysig specialname 
        instance float64  get_Y() cil managed
// SIG: 20 00 0D
{
  .custom /*0C000012:0A00000D*/ instance void [System.Runtime/*23000001*/]System.Runtime.CompilerServices.IsReadOnlyAttribute/*01000010*/::.ctor() /* 0A00000D */ = ( 01 00 00 00 ) 
  .custom /*0C000013:0A00000B*/ instance void [System.Runtime/*23000001*/]System.Runtime.CompilerServices.CompilerGeneratedAttribute/*0100000D*/::.ctor() /* 0A00000B */ = ( 01 00 00 00 ) 
  // 方法在 RVA 0x2061 处开始
  // 代码大小       7 (0x7)
  .maxstack  8
  IL_0000:  /* 02   |                  */ ldarg.0
  IL_0001:  /* 7B   | (04)000002       */ ldfld      float64 ReadonlyMember.Point/*02000002*/::'<Y>k__BackingField' /* 04000002 */
  IL_0006:  /* 2A   |                  */ ret
} // end of method Point::get_Y
.method /*06000004*/ public hidebysig specialname 
        instance void  set_Y(float64 'value') cil managed
// SIG: 20 01 01 0D
{
  .custom /*0C000014:0A00000B*/ instance void [System.Runtime/*23000001*/]System.Runtime.CompilerServices.CompilerGeneratedAttribute/*0100000D*/::.ctor() /* 0A00000B */ = ( 01 00 00 00 ) 
  // 方法在 RVA 0x2069 处开始
  // 代码大小       8 (0x8)
  .maxstack  8
  IL_0000:  /* 02   |                  */ ldarg.0
  IL_0001:  /* 03   |                  */ ldarg.1
  IL_0002:  /* 7D   | (04)000002       */ stfld      float64 ReadonlyMember.Point/*02000002*/::'<Y>k__BackingField' /* 04000002 */
  IL_0007:  /* 2A   |                  */ ret
} // end of method Point::set_Y

.method /*06000005*/ public hidebysig specialname 
        instance float64  get_Distance() cil managed
// SIG: 20 00 0D
{
  .custom /*0C000015:0A00000D*/ instance void [System.Runtime/*23000001*/]System.Runtime.CompilerServices.IsReadOnlyAttribute/*01000010*/::.ctor() /* 0A00000D */ = ( 01 00 00 00 ) 
  // 方法在 RVA 0x2072 处开始
  // 代码大小       33 (0x21)
  .maxstack  8
  IL_0000:  /* 02   |                  */ ldarg.0
  IL_0001:  /* 28   | (06)000001       */ call       instance float64 ReadonlyMember.Point/*02000002*/::get_X() /* 06000001 */
  IL_0006:  /* 02   |                  */ ldarg.0
  IL_0007:  /* 28   | (06)000001       */ call       instance float64 ReadonlyMember.Point/*02000002*/::get_X() /* 06000001 */
  IL_000c:  /* 5A   |                  */ mul
  IL_000d:  /* 02   |                  */ ldarg.0
  IL_000e:  /* 28   | (06)000003       */ call       instance float64 ReadonlyMember.Point/*02000002*/::get_Y() /* 06000003 */
  IL_0013:  /* 02   |                  */ ldarg.0
  IL_0014:  /* 28   | (06)000003       */ call       instance float64 ReadonlyMember.Point/*02000002*/::get_Y() /* 06000003 */
  IL_0019:  /* 5A   |                  */ mul
  IL_001a:  /* 58   |                  */ add
  IL_001b:  /* 28   | (0A)00000E       */ call       float64 [System.Runtime.Extensions/*23000003*/]System.Math/*01000012*/::Sqrt(float64) /* 0A00000E */
  IL_0020:  /* 2A   |                  */ ret
} // end of method Point::get_Distance
.method /*06000006*/ public hidebysig virtual 
        instance string  ToString() cil managed
// SIG: 20 00 0E
{
  .custom /*0C000016:0A00000D*/ instance void [System.Runtime/*23000001*/]System.Runtime.CompilerServices.IsReadOnlyAttribute/*01000010*/::.ctor() /* 0A00000D */ = ( 01 00 00 00 ) 
  // 方法在 RVA 0x2094 处开始
  // 代码大小       44 (0x2c)
  .maxstack  8
  IL_0000:  /* 72   | (70)000001       */ ldstr      "({0}, {1}) is {2} from the origin" /* 70000001 */
  IL_0005:  /* 02   |                  */ ldarg.0
  IL_0006:  /* 28   | (06)000001       */ call       instance float64 ReadonlyMember.Point/*02000002*/::get_X() /* 06000001 */
  IL_000b:  /* 8C   | (01)000013       */ box        [System.Runtime/*23000001*/]System.Double/*01000013*/
  IL_0010:  /* 02   |                  */ ldarg.0
  IL_0011:  /* 28   | (06)000003       */ call       instance float64 ReadonlyMember.Point/*02000002*/::get_Y() /* 06000003 */
  IL_0016:  /* 8C   | (01)000013       */ box        [System.Runtime/*23000001*/]System.Double/*01000013*/
  IL_001b:  /* 02   |                  */ ldarg.0
  IL_001c:  /* 28   | (06)000005       */ call       instance float64 ReadonlyMember.Point/*02000002*/::get_Distance() /* 06000005 */
  IL_0021:  /* 8C   | (01)000013       */ box        [System.Runtime/*23000001*/]System.Double/*01000013*/
  IL_0026:  /* 28   | (0A)00000F       */ call       string [System.Runtime/*23000001*/]System.String/*01000014*/::Format(string,
                                                                                                                          object,
                                                                                                                          object,
                                                                                                                          object) /* 0A00000F */
  IL_002b:  /* 2A   |                  */ ret
} // end of method Point::ToString

通过 IL 代码可以看出,readonly 修饰符最终会解释为 System.Runtime.CompilerServices.IsReadOnlyAttribute 特性。如果对比没有添加 readonly 修饰符时的 IL 代码,可以发现正是少了 IsReadOnlyAttribute 的两行,其它处理都是一样的。

另外还可以看到 XY 属性的 get 方法上也自动加上了 IsReadOnlyAttribute 特性,这说明编译器默认 自动生成属性getter 方法是只读的。 非自动生成属性 则需要手动添加 readonly 修饰符。

由于 readonly 修饰符标示属性或方法是不可以修改状态的,所以如果尝试在其中修改成员,编译时报 CS1604 错误。

CS1604 无法为“X”赋值,因为它是只读的

默认接口方法

这个是对 接口(interface)的扩展,以解决在扩展接口方法时的麻烦。原本必须对所有实现类都进行升级后才可以发布,现在指定其默认实现后,可以在完全不修改实现类的状态下安全的发布。

每个接口实现仍然可以像重写 虚方法(virtual)那样自定义方法处理,但是不需要使用 override 关键字。默认接口方法 感觉就像是 接口版的虚方法

调用 默认接口方法 时,需要隐式转换为接口类型后才可调用。如果实现类中已经实现了该方法,则会覆盖 默认接口方法。

具体示例如下:

csharp
interface IA
{
    void A();

    public void AS()
    {
        Console.WriteLine("A default interface method in IA.");
    }
}

interface IB
{
    void B();

    public void BS()
    {
        Console.WriteLine("A default interface method in IB.");
    }

    // CS0535 '“C”不实现接口成员“IB.BN()”
    //void BN();
}

class C : IA, IB
{
    public void A()
    {
        throw new NotImplementedException();
    }

    public void B()
    {
        throw new NotImplementedException();
    }

    // 重写 默认接口方法 时不需要使用 override 关键字。
    public void BS()
    {
        Console.WriteLine("A inheriting classe method of IB.BS in class C.");
    }

    public virtual void CV()
    {
        Console.WriteLine("A virtual method in class C.");
    }
}

class CS : C
{
    // 重写 虚方法 时需使用 override 关键字。
    public override void CV()
    {
        Console.WriteLine("A override method in CS.");
    }
}

为了对比创建了一个 抽象类 及其实现。

csharp
abstract class AC
{
    // 通过 abstract 关键字创建抽象方法。
    // 抽象方法没有实现,实现类必须重写抽象方法。
    public abstract void VM();

    public void M()
    {
        Console.WriteLine("A default method in abstrac class AC.");
    }
}

class ACI : AC
{
    // 实现抽象方法时需要使用 override 关键字。
    public override void VM()
    {
        Console.WriteLine("A override method in class ACI.");
    }
}

对各个方法的调用及打印结果。

csharp
// 接口实现类
var cs = new CS();

// 未被实现类重写的接口默认方法不能被实例对象直接调用,必须隐式转换为接口类型后再调用
// CS1061 '“C”未包含“AS”的定义,并且找不到可接受第一个“C”类型参数的可访问扩展方法“AS”(是否缺少 using 指令或程序集引用?)
//cs.AS();
IA ia = cs;
ia.AS(); // print "A default interface method in IA."

// 已被实现类重写的接口默认方法可以直接被实例对象直接调用,也可隐式转换为接口后再调用,而且调用的都是重写后的方法。
cs.BS(); // print "A inheriting classe method of IB.BS in class C."
IB ib = cs;
ib.BS(); // print "A inheriting classe method of IB.BS in class C."

// 虚方法测试
// 虚方法跟接口的默认实现比较类似,区别就是即使未重写也可以被调用。
cs.CV(); // print "A override method in CS."
C c = cs;
// 这里需要特别注意:虽然隐式转换为父类型,但是实际调用的仍然是子类的方法。
c.CV(); // print "A override method in CS."

// 抽象类调用(对比用)
// 抽象类中的抽象方法必须被实现类重写
var aci = new ACI();
aci.M(); // print "A default method in abstrac class AC."
aci.VM(); // print "A override method in class ACI."

另外 默认接口实现 支持在其中使用 静态字段、静态方法 。详细信息请参阅 MSDN

在更多位置中使用更多模式

C# 7.0 通过使用 is 表达式和 switch 语句引入了 类型模式常量模式 的语法。

C# 8.0 扩展了此词汇表,这样就可以在代码中的更多位置使用更多模式表达式。

C# 8.0 还添加了 “递归模式” 。任何模式表达式的结果都是一个表达式。递归模式只是应用于另一个模式表达式输出的模式表达式。

switch 表达式

下面示例展示了对 Rainbow 枚举类应用 switch 表达式时的写法。

csharp
public enum Rainbow
{
    Red,
    Orange,
    Yellow,
    Green,
    Blue,
    Indigo,
    Violet
}
csharp
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)),
    };

这里有几个语法改进:

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

上述代码等效于:

csharp
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));
    };
}

属性模式

借助属性模式,可以匹配所检查的对象的属性。

csharp
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
    };

上述示例中 State 是参数 Address 的属性,当 location.State == "WA" 时,返回 salePrice * 0.06M ... 。

元组模式

这里可以理解为 元组的比较。当 (first, second) == ("rock", "paper") 时返回 "rock is covered by paper. Paper wins." ... 。

csharp
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 方法,就可以使用 位置模式 检查对象的属性并将这些属性用于模式。

下面示例将坐标转换为象限。

坐标类:

csharp
public class 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);
}

象限枚举:

csharp
public enum Quadrant
{
    Unknown,
    Origin,
    One,
    Two,
    Three,
    Four,
    OnBorder
}

转换方法:

csharp
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
};

上例中将形参 point 解构为离散变量 (x, y) ,然后使用 when 关键字指定筛选条件。

当结构结果不被使用时,可以解构到 弃元(_)。

switch 表达式必须要么生成值,要么引发异常。如果这些情况都不匹配,则 switch 表达式将引发异常。

using 声明

using 声明 是前面带 using 关键字的变量声明。它指示编译器声明的变量应在 封闭范围的末尾 进行处理。

csharp
static int WriteLinesToFile(IEnumerable<string> lines)
{
    using var file = new System.IO.StreamWriter("WriteLines2.txt");
    // Notice how we declare skippedLines after the using statement.
    int skippedLines = 0;
    foreach (string line in lines)
    {
        if (!line.Contains("Second"))
        {
            file.WriteLine(line);
        }
        else
        {
            skippedLines++;
        }
    }
    // Notice how skippedLines is in scope here.
    return skippedLines;
    // file is disposed here
}

在前面的示例中,当到达 方法的右括号 时,将对该文件进行处理。这是声明 file 的范围的末尾。

前面的代码相当于下面使用经典 using 语句的代码:

csharp
static int WriteLinesToFile(IEnumerable<string> lines)
{
    // We must declare the variable outside of the using block
    // so that it is in scope to be returned.
    int skippedLines = 0;
    using (var file = new System.IO.StreamWriter("WriteLines2.txt"))
    {
        foreach (string line in lines)
        {
            if (!line.Contains("Second"))
            {
                file.WriteLine(line);
            }
            else
            {
                skippedLines++;
            }
        }
    } // file is disposed here
    return skippedLines;
}

在前面的示例中,当到达与 using 语句关联的右括号时,将对该文件进行处理。

在这两种情况下,编译器将生成对 Dispose() 的调用。如果 using 语句中的表达式不可用,编译器将生成一个错误。

静态本地函数

C# 7.0 开始可以使用 本地函数(区域函数) ,现在可以向本地函数添加 static 修饰符,以确保本地函数不会从 封闭范围 捕获(引用)任何变量。否则会生成 CS8421 静态本地函数不能包含对“<variable>”的引用。

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

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

可处置的 ref 结构

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

csharp
public ref struct Point3D
{
    public double X { get; }
    public double Y { get; }
    public double Z { get; }
    public double Distance => Math.Sqrt(X * X + Y * Y + Z * Z);

    public Point3D(double x, double y, double z)
    {
        X = x;
        Y = y;
        Z = z;
    }

    public override string ToString()
        => $"({X}, {Y}, {Z})";
}
csharp
using var point = new Point3D(3, 4, 5);
Console.WriteLine(point.ToString());

Point3D 实例使用 using 声明 时会报 CS1674 错误:

CS1674 “Point3D”: using 语句中使用的类型必须可隐式转换为 "System.IDisposable" 或实现适用的 "Dispose" 方法。

需要在 Point3D 结构中添加 Dispose() 方法。

csharp
public void Dispose()
{
    Console.WriteLine("Point3D is disposing.");
}

运行结果:

bash
(3, 4, 5)
Point3D is disposing.

可为空引用类型

需要手动修改 工程文件(.csproj)。在 PropertyGroup 标签下添加 Nullable 标签。

xml
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.0</TargetFramework>
    <Nullable>enable</Nullable>
  </PropertyGroup>

</Project>

Nullable 标签允许的值:

  • enable:“启用”可为空声明上下文。 “启用”可为空警告上下文。

    引用类型的变量,例如 string 是“不可为空”。启用所有为 Null 性警告。

  • warnings:“禁用”可为空声明上下文。 “启用”可为空警告上下文。

    引用类型的变量是“无视”。启用所有为 Null 性警告。

  • annotations:“启用”可为空声明上下文。 “禁用”可为空警告上下文。

    引用类型的变量(例如字符串)不可为 null。禁用所有为 Null 性警告。

  • disable:“禁用”可为空声明上下文。 “禁用”可为空警告上下文。

    引用类型的变量是“无视”,就像早期版本的 C# 一样。禁用所有为 Null 性警告。

原文中将 annotations 翻译为 注释 ,我觉得应该翻译为 声明 比较好理解。原文中的 可为空注释上下文 这里都改为 可为空声明上下文 。(注意:Visual Studio 中警告消息中使用的也是 可为空注释上下文

下面是一小段测试代码,如果启用可为空警告,则会显示备注中的警告。

csharp
class Program
{
    static void Main(string[] args)
    {
        Staff staff = GetStaff();
        string str = null; // warning: CS8600 将 null 文本或可能的 null 值转换为非 null 类型。

        Console.WriteLine(staff.ToString());
        Console.WriteLine(str.ToString()); // warning: CS8602 取消引用可能出现的空引用。
    }

    private static Staff GetStaff()
    {
        return null; // warning: CS8603 可能的 null 引用返回。
    }
}

除了直接修改项目文件之外,还可以在代码中的任意位置通过 #nullable 指令指定当前上下文是启用可为空警告。
因为 #nullable 在 VS 中默认定位到顶格,从缩放来看,将其放在 namespace 前显得更加工整。

  • #nullable enable :将可为空声明上下文和可为空警告上下文设置为“已启用” 。
  • #nullable disable :将可为空声明上下文和可为空警告上下文设置为“已禁用” 。
  • #nullable restore :将可为空声明上下文和可为空警告上下文还原到项目设置。
  • #nullable disable warnings :将可为空警告上下文设置为“已禁用” 。
  • #nullable enable warnings :将可为空警告上下文设置为“已启用” 。
  • #nullable restore warnings :将可为空警告上下文还原到项目设置。
  • #nullable disable annotations :将可为空声明上下文设置为“禁用” 。
  • #nullable enable annotations :将可为空声明上下文设置为“启用” 。
  • #nullable restore annotations :将注释警告上下文还原到项目设置。

可为空声明上下文

这里看的迷迷糊糊,还是翻译的问题。 dereference 翻译成了 取消引用 ,我的理解应该是 间接引用间接访问 的意思,通俗点说就是 访问变量 。(Visual Studio 中的消息使用的也是 取消引用

  • 编译器在 已禁用的可为空声明上下文 中使用以下规则:

    • 不能在已禁用的上下文中声明可为空引用。

      csharp
      #nullable disable annotations
      string? str = null; // CS8632 只能在 "#nullable" 注释上下文内的代码中使用可为 null 的引用类型的注释。

      如果是在 C# 7.3 及以前的版本定义 string? 类型变量,则会有 CS8370 功能“可为 null 的引用类型”在 C# 7.3 中不可用。请使用 8.0 或更高的语言版本。 的错误。

    • 可以将所有引用变量分配为 null

      csharp
      #nullable disable annotations
      string str = null; // no warning
      Staff aStaff = null; // no warning
    • 间接访问引用类型的变量时不会生成警告。

      csharp
      #nullable disable annotations
      Staff aStaff = null; // no warning
      Console.WriteLine(aStaff.ToString()); // no warning
    • 可能不会在禁用的上下文中使用 null 包容运算符。

      这个可能不知道具体是指什么时候,下面的代码均没有警告。

      csharp
      #nullable disable annotations
      Staff aStaff = new Staff(null); // no warning
      aStaff = new Staff(null!); // no warning
      Console.WriteLine(aStaff.ToString());  // no warning

      关于包容运算符的详情请参阅 ! (null 包容)运算符

    该行为与以前版本的 C# 相同。(这个就是字面意思,在 C# 7.3 及以前是没有 可为空引用类型 的(只允许定义 可为空的非引用类型,如 int? ),上面的表现同这些版本是一样的)

  • 编译器在 已启用的可为空声明上下文 中使用以下规则:

    • 引用类型的任何变量都是 “不可为空引用”

      这里跟我的想象中的有些不同。并不是说引用类型的变量赋值为 null 时会报警,而是编译器认为该变量是非空的,而不管他实际上是不是空值。

      csharp
      #nullable enable annotations
      Staff anotherStaff = null; // no warning
      Console.WriteLine(anotherStaff.ToString()); // no warning

      上述代码编译时不会有任何警告,只有到代码运行时才会报空引用的异常。

    • 任何不可为空引用都可以安全地间接访问。

      同上一条

    • 任何可为空引用类型(在变量声明中的类型之后由 ? 标记)可为 null。静态分析确定在间接访问该值时是否已知该值不为 null。否则,编译器会发出警告。

      可以声明 可为空引用类型 的变量。但后半部分说的 静态分析 不知道是如何运作的,下面的代码编译器并没有发出警告,不知道什么情况下会触发警告(可能是要启用 可为空警告上下文 时才会发出警告)。

      csharp
      #nullable enable annotations
      Staff? anotherStaff = null; // no warning
      Console.WriteLine(anotherStaff.ToString()); // no warning
    • 你可以使用 null 包容运算符声明可为空引用不为 null

      写法如下,但由于不使用包容运算符时就没有警告,所以看不出效果。

      csharp
      #nullable enable annotations
      Staff? anotherStaff = null; // no warning
      Console.WriteLine(anotherStaff!.ToString()); // no warning

    在已启用的可为空声明上下文中,附加到引用类型的 ? 字符声明 “可为空引用类型” 。可将 NULL 包容运算符 ! 附加到表达式以声明表达式不为 NULL。

可为空警告上下文

可为空警告上下文可为空声明上下文 不同。即使禁用新声明,也可以启用警告。

编译器使用 静态流分析 来确定任何引用的 “空状态” 。当 “可为空警告上下文” 启用时,空状态“非空”“可能为空”

如果在编译器确定引用 “可能为空” 时间接访问该引用,编译器会向你发出警告。除非编译器可以确定以下两个条件之一,否则引用的状态为 “可能为空”

  • 该变量已明确分配给非 null 值。

    csharp
    #nullable enable warnings
    Staff? aStaff = new Staff("A staff"); // no warning
    Console.WriteLine(aStaff.ToString()); // no warning
  • 在间接访问之前,已检查变量或表达式是否为 null

    csharp
    #nullable enable warnings
    Staff? aStaff = null; // no warning
    if (aStaff != null)
    {
        Console.WriteLine(aStaff.ToString()); // no warning
    }

可为空警告上下文 处于启用状态时,只要间接访问 “可能为空” 状态的变量或表达式,编译器就会生成警告。

  • 不可为空引用类型 值为空时

    csharp
    #nullable enable warnings
    Staff aStaff = null; // CS8600 将 null 文本或可能的 null 值转换为非 null 类型。
    Console.WriteLine(aStaff.ToString()); // CS8602 取消引用可能出现的空引用。
  • 可为空引用类型 值为空时

    csharp
    #nullable enable warnings
    Staff? aStaff = null; // no warning
    Console.WriteLine(aStaff.ToString()); // CS8602 取消引用可能出现的空引用。

    这里就可以看出,即使未启用 可为空声明上下文 ,声明 可为空引用类型 的变量也没有警告。如果明确禁用 可为空声明上下文 ,还会显示 CS8632 警告。

    csharp
    #nullable enable warnings
    #nullable disable annotations
    Staff? aStaff = null; // CS8632 只能在 "#nullable" 注释上下文内的代码中使用可为 null 的引用类型的注释。
    Console.WriteLine(aStaff.ToString()); // CS8602 取消引用可能出现的空引用。

此外,在将 “可能为空” 变量或表达式分配给已启用的 可为空声明上下文 中的 不可为空引用类型 时,将生成警告。

例:将 Staff.Name 属性的类型修改为 string? 后赋值给 不可为空引用类型

csharp
#nullable enable warnings
#nullable enable annotations
Staff staff = new Staff("A staff");
string name = staff.Name; // CS8600 将 null 文本或可能的 null 值转换为非 null 类型。

异步流

从 C# 8.0 开始,可以创建并以异步方式使用流。返回异步流的方法有三个属性:

  1. 它是用 async 修饰符声明的。

  2. 它将返回 IAsyncEnumerable<T>

  3. 该方法包含用于在异步流中返回连续元素的 yield return 语句。

使用异步流需要在枚举流元素时在 foreach 关键字前面添加 await 关键字。

添加 await 关键字需要枚举异步流的方法,以使用 async 修饰符进行声明并返回 async 方法允许的类型。通常这意味着返回 TaskTask<TResult>。也可以为 ValueTaskValueTask<TResult>

方法既可以使用异步流,也可以生成异步流,这意味着它将返回 IAsyncEnumerable<T>

下面的代码生成一个从 0 到 19 的序列,在生成每个数字之间等待 100 毫秒:

csharp
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 语句来枚举序列:

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

上面是从 MSDN 上拷贝过来的,已经很清楚的解释了 异步流 的用法。

下面是完整的控制台程序的示例代码及打印结果:

csharp
using System;
using System.Threading.Tasks;

namespace AsynchronousStreams
{
    class Program
    {
        static async Task Main(string[] args)
        {
            await foreach (var number in GenerateSequence())
            {
                Console.WriteLine(number);
            }
        }

        public static async System.Collections.Generic.IAsyncEnumerable<int> GenerateSequence()
        {
            for (int i = 0; i < 20; i++)
            {
                await Task.Delay(100);
                yield return i;
            }
        }
    }
}

会每隔 100 毫秒打印一个数字,最终结果如下:

bash
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

索引和范围

索引范围 为访问序列中的单个元素或范围提供了简洁的语法。

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

  • System.Index 表示一个序列索引。

  • 来自末尾运算符 ^ 的索引,指定一个索引与序列末尾相关。

  • System.Range 表示序列的子范围。

  • 范围运算符 ..,用于指定范围的开始和末尾,就像操作数一样。

索引 规则: (假设有个数组 sequence

  • 0 索引与 sequence[0] 相同。

  • ^0 索引与 sequence[sequence.Length] 相同。

  • 请注意,sequence[^0] 不会引发异常,就像 sequence[sequence.Length] 一样。

  • 对于任何数字 n ,索引 ^nsequence.Length - n 相同。

范围 指定范围的开始和末尾。 包括此范围的开始,但不包括此范围的末尾,这表示此范围包含开始但不包含末尾。范围 [0..^0] 表示整个范围,就像 [0..sequence.Length] 表示整个范围。

请看以下几个示例。请考虑以下数组,用其顺数索引和倒数索引进行注释:

csharp
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 索引检索最后一个词:

csharp
Console.WriteLine($"The last word is {words[^1]}");
// writes "dog"

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

csharp
var quickBrownFox = words[1..4];

以下代码使用 “lazy”“dog” 创建一个子范围。它包括 words[^2]words[^1]。末尾索引 words[^0] 不包括在内:

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

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

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

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

csharp
Range phrase = 1..4;

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

csharp
var text = words[phrase];

不仅数组支持索引和范围。也可以将索引和范围用于 stringSpan<T>ReadOnlySpan<T>

有关详细信息,请参阅 索引和范围的类型支持

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

Null 合并赋值

C# 8.0 引入了 null 合并赋值运算符 ??= 。仅当左操作数计算为 null 时,才能使用运算符 ??= 将其右操作数的值分配给左操作数。

csharp
List<int> numbers = null;
int? i = null;

numbers ??= new List<int>();
numbers.Add(i ??= 17);
numbers.Add(i ??= 20);

Console.WriteLine(string.Join(" ", numbers));  // output: 17 17
Console.WriteLine(i);  // output: 17

有关详细信息,请参阅 ?? 和 ??= 运算符 一文。

非托管构造类型

在 C# 7.3 及更低版本中,构造类型(包含至少一个类型参数的类型)不能为 非托管类型。从 C# 8.0 开始,如果构造的值类型仅包含非托管类型的字段,则该类型不受管理。

例如,假设泛型 Coords<T> 类型有以下定义:

csharp
public struct Coords<T>
{
    public T X;
    public T Y;
}

Coords<int> 类型为 C# 8.0 及更高版本中的非托管类型。与任何非托管类型一样,可以创建指向此类型的变量的指针,或针对此类型的实例在堆栈上分配内存块:

csharp
Span<Coords<int>> coordinates = stackalloc[]
{
    new Coords<int> { X = 0, Y = 0 },
    new Coords<int> { X = 0, Y = 3 },
    new Coords<int> { X = 4, Y = 0 }
};

下面的代码在 C# 8.0 中可以正常运行。

csharp
using System;

namespace UnmanagedConstructedTypes
{
    class Program
    {
        static void Main(string[] args)
        {
            DisplaySize<Coords<int>>();
            // UnmanagedConstructedTypes.Coords`1[System.Int32] is unmanaged and its size is 8 bytes
            DisplaySize<Coords<double>>();
            // UnmanagedConstructedTypes.Coords`1[System.Double] is unmanaged and its size is 16 bytes
        }

        private unsafe static void DisplaySize<T>() where T : unmanaged
        {
            Console.WriteLine($"{typeof(T)} is unmanaged and its size is {sizeof(T)} bytes");
        }
    }

    public struct Coords<T>
    {
        public T X;
        public T Y;
    }
}

但是在 C# 7.3 及以前会报 CS8370 功能“非托管构造类型”在 C# 7.3 中不可用。请使用 8.0 或更高的语言版本。 错误。
另外即使使用 C# 7.3 新增的 unmanaged 约束,来限制构造类型,但其仍然不会被当做 非托管构造类型

csharp
public struct Coords<T> where T : unmanaged
{
    public T X;
    public T Y;
}

嵌套表达式中的 stackalloc

从 C# 8.0 开始,如果 stackalloc 表达式的结果为 System.Span<T>System.ReadOnlySpan<T> 类型,则可以在其他表达式中使用 stackalloc 表达式:

csharp
Span<int> numbers = stackalloc[] { 1, 2, 3, 4, 5, 6 };
var ind = numbers.IndexOfAny(stackalloc[] { 2, 4, 6 ,8 });
Console.WriteLine(ind);  // output: 1

内插逐字字符串的增强功能

内插逐字字符串中 $@ 标记的顺序可以任意安排:$@"..."@$"..." 均为有效的内插逐字字符串。在早期 C# 版本中,$ 标记必须出现在 @ 标记之前。