C# 8.0 中的新增功能
官方文档见 这里。本文在其基础上添加了一些自己理解及示例代码,如有不正确的地方欢迎指正。
Readonly 成员
可将 readonly
修饰符应用于 结构的成员 。它指示 该成员不会修改状态。
在 7.2 中新增了 readonly struct
声明,将整个结构声明为只读的。这会导致该结构中不能有非只读的属性,也就是属性不能有 set 方法,这会使结构一旦创建后就不能再修改。
在属性上添加 readonly
修饰符可以提供更细粒度的控制,使结构性能提高的同时,使用也可以更加灵活。
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 代码:
.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
的两行,其它处理都是一样的。
另外还可以看到 X 和 Y 属性的 get 方法上也自动加上了 IsReadOnlyAttribute
特性,这说明编译器默认 自动生成属性 的 getter 方法是只读的。 非自动生成属性 则需要手动添加 readonly
修饰符。
由于 readonly
修饰符标示属性或方法是不可以修改状态的,所以如果尝试在其中修改成员,编译时报 CS1604 错误。
CS1604 无法为“X”赋值,因为它是只读的
默认接口方法
这个是对 接口(interface
)的扩展,以解决在扩展接口方法时的麻烦。原本必须对所有实现类都进行升级后才可以发布,现在指定其默认实现后,可以在完全不修改实现类的状态下安全的发布。
每个接口实现仍然可以像重写 虚方法(virtual
)那样自定义方法处理,但是不需要使用 override
关键字。默认接口方法 感觉就像是 接口版的虚方法。
调用 默认接口方法 时,需要隐式转换为接口类型后才可调用。如果实现类中已经实现了该方法,则会覆盖 默认接口方法。
具体示例如下:
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.");
}
}
为了对比创建了一个 抽象类 及其实现。
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.");
}
}
对各个方法的调用及打印结果。
// 接口实现类
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
表达式时的写法。
public enum Rainbow
{
Red,
Orange,
Yellow,
Green,
Blue,
Indigo,
Violet
}
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
事例替换为_
弃元。 - 正文是表达式,不是语句。
上述代码等效于:
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));
};
}
属性模式
借助属性模式,可以匹配所检查的对象的属性。
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." ... 。
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 方法,就可以使用 位置模式 检查对象的属性并将这些属性用于模式。
下面示例将坐标转换为象限。
坐标类:
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);
}
象限枚举:
public enum Quadrant
{
Unknown,
Origin,
One,
Two,
Three,
Four,
OnBorder
}
转换方法:
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
关键字的变量声明。它指示编译器声明的变量应在 封闭范围的末尾 进行处理。
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
语句的代码:
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>
”的引用。
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
声明。
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})";
}
using var point = new Point3D(3, 4, 5);
Console.WriteLine(point.ToString());
对 Point3D 实例使用 using 声明 时会报 CS1674
错误:
CS1674 “Point3D”: using 语句中使用的类型必须可隐式转换为 "System.IDisposable" 或实现适用的 "Dispose" 方法。
需要在 Point3D 结构中添加 Dispose() 方法。
public void Dispose()
{
Console.WriteLine("Point3D is disposing.");
}
运行结果:
(3, 4, 5)
Point3D is disposing.
可为空引用类型
需要手动修改 工程文件(.csproj)。在 PropertyGroup 标签下添加 Nullable 标签。
<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 中警告消息中使用的也是 可为空注释上下文)
下面是一小段测试代码,如果启用可为空警告,则会显示备注中的警告。
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?
后赋值给 不可为空引用类型 。
#nullable enable warnings
#nullable enable annotations
Staff staff = new Staff("A staff");
string name = staff.Name; // CS8600 将 null 文本或可能的 null 值转换为非 null 类型。
异步流
从 C# 8.0 开始,可以创建并以异步方式使用流。返回异步流的方法有三个属性:
它是用
async
修饰符声明的。它将返回
IAsyncEnumerable<T>
。该方法包含用于在异步流中返回连续元素的
yield return
语句。
使用异步流需要在枚举流元素时在 foreach
关键字前面添加 await
关键字。
添加 await
关键字需要枚举异步流的方法,以使用 async
修饰符进行声明并返回 async
方法允许的类型。通常这意味着返回 Task
或 Task<TResult>
。也可以为 ValueTask
或 ValueTask<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
语句来枚举序列:
await foreach (var number in GenerateSequence())
{
Console.WriteLine(number);
}
上面是从 MSDN 上拷贝过来的,已经很清楚的解释了 异步流 的用法。
下面是完整的控制台程序的示例代码及打印结果:
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 毫秒打印一个数字,最终结果如下:
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
,索引^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];
不仅数组支持索引和范围。也可以将索引和范围用于 string
、Span<T>
或 ReadOnlySpan<T>
。
有关详细信息,请参阅 索引和范围的类型支持 。
可在有关 索引和范围 的教程中详细了解索引和范围。
Null 合并赋值
C# 8.0 引入了 null
合并赋值运算符 ??=
。仅当左操作数计算为 null
时,才能使用运算符 ??=
将其右操作数的值分配给左操作数。
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>
类型有以下定义:
public struct Coords<T>
{
public T X;
public T Y;
}
Coords<int>
类型为 C# 8.0 及更高版本中的非托管类型。与任何非托管类型一样,可以创建指向此类型的变量的指针,或针对此类型的实例在堆栈上分配内存块:
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 中可以正常运行。
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
约束,来限制构造类型,但其仍然不会被当做 非托管构造类型 。
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
表达式:
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# 版本中,$
标记必须出现在 @
标记之前。