C# 二十年语法变迁之 C# 8参考

C# 二十年语法变迁之 C# 8参考

自从 C# 于 2000 年推出以来,该语言的规模已经大大增加,我不确定任何人是否有可能在任何时候都对每一种语言特性都有深入的了解。因此,我想写一系列快速参考文章,总结自 C# 2.0 以来所有主要的新语言特性。我不会详细介绍它们中的任何一个,但我希望这个系列可以作为我自己(希望你也是!)的参考,我可以不时回过头来记住我使用的工具工具箱里有。😃

开始之前的一个小提示:我将跳过一些更基本的东西(例如 C# 2.0 引入了泛型,但它们的使用范围如此广泛,以至于它们不值得包括在内);而且我还可以将一些功能“粘合”在一起,以使其更简洁。本系列并不打算成为该语言的权威或历史记录。相反,它更像是可能派上用场的重要语言功能的“备忘单”。您可能会发现浏览左侧的目录以搜索您不认识或需要快速提醒的任何功能很有用。

C# 8.0

可空引用类型

此功能是对 C# 的一个重要补充,旨在通过添加编译时正确性检查来帮助防止运行时出现空引用异常。

启用功能

要在项目中启用可空检查,请在 .csproj 文件中的目标框架声明下方添加enable

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

•“在项目文件中启用可空检查”
或者,对于您想要慢慢转换为可空检查的现有代码库,编译时指令可以在源代码中启用/禁用此功能:

#nullable enable
// Nullable references enabled here

#nullable disable
// Nullable references disabled here

#nullable restore
// Resets status to project settings (i.e. disabled unless <Nullable>enable</Nullable> is specified in csproj, in which case enabled)

// See also https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/preprocessor-directives/preprocessor-nullable

• “可空指令”

如果您在项目范围内关闭了可空引用,建议使用#nullable enable和#nullable restore(而不是#nullable disable),这样当您项目范围内启用它时,您不会在某些地方意外禁用它。

基本用法

程序员可以将每个字段、参数或属性(引用类型,即类)划分为可空或不可空。未使用问号语法(即字符串?)声明的所有字段/属性/参数都被视为non-nullable。这些成员不应该设置空值。

如果编译器检测到不可为空的字段/属性/参数可能具有分配给它的空值,则编译器将发出警告。

如果编译器检测到您尝试取消引用可空字段/属性/参数而不确保其值不为空,编译器将发出警告。
以下示例显示了一个包含可空和不可空字段和属性的类,以及采用可空和不可空参数的方法:

#nullable enable
class TestClass {
	// If we don't assign a value to _nonNullableField here or in the constructor, the compiler will warn us
	string _nonNullableField;
	
	// We don't have to supply any initial value for a nullable field, the default value of null is acceptable
	string? _nullableField;

	// If we don't assign a value to NonNullableProperty here or in the constructor, the compiler will warn us
	public string NonNullableProperty { get; set; } = "Hello";
	
	// We don't have to supply any initial value for a nullable property, the default value of null is acceptable
	public string? NullableProperty { get; set; }

	public TestClass(string? initialFieldValue) {
		// If we just assign initialFieldValue to _nonNullableField without the null-coalesced fallback value, the compiler will warn us
		_nonNullableField = initialFieldValue ?? "Hi";
	}
	
	public void PrintStringLengths(string nonNullableParameter, string? nullableParameter) {
		Console.WriteLine($"Non-nullable parameter length is: {nonNullableParameter.Length}");
		
		// If we don't use the null-propagation operator (?.) or check nullableParameter for null first, the compiler will warn us
		Console.WriteLine($"Nullable parameter length is: {nullableParameter?.Length.ToString() ?? "<null>"}");
	}
}

• “可空和不可空字段、属性和参数”
总之:

如果我们在构造函数返回之前没有给_nonNullableField和NonNullableProperty赋值非空值,编译器会警告我们;

如果我们在构造函数中将initialFieldValue分配给_nonNullableField时没有提供非空替代值,编译器会警告我们;

如果我们尝试访问字符串的Length属性,编译器会警告我们吗?参数而不检查是否为空。
容错运算符 ( ! ) 请求编译器忽略潜在的空值 :

public void PrintFieldLengthsIfNonNull() {
    if (_nullableField != null) PrintFieldLengths();
}

void PrintFieldLengths() {
    Console.WriteLine($"Non-nullable field length is: {_nonNullableField.Length}");
    
	// Null-forgiving operator (!) disables compiler warning here caused by accessing .Length property of potentially-null _nullableField without null check
	Console.WriteLine($"Nullable field length is: {_nullableField!.Length}");
}

• “Null-forgiving Operator”

在您希望将null作为不可为空引用的值的地方(例如,您知道该值将始终通过其他方式设置,或者您将 null 传递给对类进行单元测试等),您可以使用带有null或默认文字(即null!或default!)的 null-forgiving 运算符:

// We know this property will have a non-null value set before it is used no matter what so
// we can tell the compiler to not worry about it being null here:
public string Name { get; } = null!; 

• “Null-Forgiven Property”

泛型

无约束和无注释的泛型方法仅使用调用站点中所述的给定类型参数:

static T ReturnInput<T>(T t) => t;

var x = ReturnInput<string>(null); // Emits a warning: Passing a null reference to an input of type 'string'
var y = ReturnInput<int?>(null); // No warning, passing null to an input of type 'int?' is fine
var z = ReturnInput((object?) null); // No warning, passing null to an input of type 'object?' is fine

• “无注释的通用方法”

请注意,如果我们从ReturnInput返回默认值,编译器仍会发出警告,即使它没有空注释。这是因为如果T不可为空但属于引用类型(即string),则返回default将返回无效的null值。
要在泛型上下文中使用可空值,泛型类型应被约束为struct或class:

static T? ReturnInput<T>(T? t) where T : struct => t;

var p = ReturnInput("test"); // Doesn't compile, 'string' isn't a struct
var q = ReturnInput(3); // Doesn't compile, 'int' is a struct but the compiler can't make the leap that you want to 
						// implictly convert an object of type 'int' to 'int?', and that therefore T should be 'int' 
						// (add explicit type parameter indication in angle brackets to fix)
var r = ReturnInput<int>(null); // Compiles, T is 'int' and the input parameter is default(Nullable<int>)

全屏查看代码• “使用带有可为空引用的 'struct' 约束:”

static T? ReturnInput<T>(T? t) where T : class => t;

var x = ReturnInput<string>(null); // Compiles absolutely fine, T is 'string' and therefore the parameter type is 'string?'
var y = ReturnInput<int?>(null); // Does not compile: 'int?' is not a reference type (it's an alias for Nullable<T>)
var z = ReturnInput((object?) null); // Compiles absolutely fine, T is inferred to be 'object'

全屏查看代码• “使用带有可空引用的‘类’约束:”
当约束到接口/子类型时,也可以指示类型参数可能是可为空的类型:

static T? ReturnInput<T>(T? t) where T : IComparable? => t; // Notice nullable token (?) after IComparable constraint

var p = ReturnInput<string?>("test"); // No warning. Compiler would warn us if the constraint was 'IComparable' without the nullable token (?)
var q = ReturnInput(3); // No warning. Int32 implements IComparable, so this is fine. 
						// The nullable token (?) on the constraint indicates that the type MAY be nullable, not that it MUST be
var r = ReturnInput<int?>(3); // Doesn't compile. Nullable<T> has never been able to satisfy interface constraints

全屏查看代码• “使用可为空的接口约束”
目前,没有约束或任何其他方式来编写接受任何可空类型(即可空引用类型和可空结构)的泛型方法。相反,新的notnull约束可用于禁止可为空的类型:

static T? ReturnInput<T>(T? t) where T : notnull => t; // Notice nullable token (?) after IComparable constraint

var x = ReturnInput<string?>("test"); // Emits a warning, 'string?' invalidates the notnull constraint for T
var y = ReturnInput((int?) 3); // Emits a warning, 'int?' invalidates the notnull constraint for T
var z = ReturnInput((object?) null); // No warning. T is implictly set to 'object' rather than 'object?'. 
									 // Notice that we can still accept maybe-null parameters and return maybe-null values, 
									 // the 'notnull' constraint applies to the type parameter itself, not method parameters
									 // or return values.

全屏查看代码• “使用非空约束”

属性

System.Diagnostics.CodeAnalysis命名空间提供了一些新属性,可应用于您自己的 API/代码中的各种代码元素,以帮助编译器确定 null 正确性 :

// AllowNull indicates that a null value is permitted when setting/passing a value.

// Although the type of this property is string (and not string?) we will allow people to 'set' a null value,
// which will actually be replaced with a non-null fallback value. Hence we [AllowNull].

[AllowNull] 
public string NonNullableProperty {
	get => _nonNullableField;
	set => _nonNullableField = value ?? "fallback";
}

• “Null-Correctness Assisting Attributes: AllowNull”

// DisallowNull indicates that a null value is not permitted when setting/passing a value.

// Although the type of this property is string? we don't wish to allow anyone to actually *set* a null value,
// it's just that the value may be still be null when retrieved. Hence we mark it with [DisallowNull].

[DisallowNull]
public string? NullableProperty {
	get => _nullableField;
	set => _nullableField = value ?? throw new ArgumentNullException(nameof(value));
}

• “Null-Correctness Assisting Attributes: DisallowNull”

// MaybeNull applied to a return element indicates that the returned value may be null.
// This is useful when working with generics, as 'T?' isn't always valid.

[return: MaybeNull]
public T GetElementByName<T>(string name) { /* ... */ }

• “Null-Correctness Assisting Attributes: MaybeNull”

// NotNull indicates that a ref or out parameter, that is marked as nullable, will never be set as null after the method returns.

public void AppendHello([NotNull] ref string? a) { 
	if (a == null) a = "Hello";
	else a = a + "Hello";
}

• “Null-Correctness Assisting Attributes: NotNull”

// NotNullWhen indicates that a parameter is not null when the return value of a method is true or false.

// The annotation here indicates that when 'TryParseUser()' returns true, 'u' will always have a value assigned.

public bool TryParseUser(string userIdentifier, [NotNullWhen(true)] out User u) { /* ... */ }

• “Null-Correctness Assisting Attributes: NotNullWhen”
还有一些适用于罕见用例的附加属性,都可以在这里找到:保留属性有助于编译器的空状态静态分析

隐式类型变量

任何用var 声明的局部变量总是被声明为可为空的,即使右边的表达式没有计算为可为空的类型:

img
原因在语言设计会议的笔记中给出:

“在这一点上,我们已经看到大量代码需要人们拼出类型而不是使用 var,因为代码可能稍后会分配 null。”
不过,别担心。即使该类型被标记为可为空,编译器仍使用流分析来确定该值是否实际上可以为空。假设您分配的值不可为空,这意味着您仍然可以将隐式类型变量传递给期望不可为空引用的方法,并在没有警告的情况下取消引用该变量;直到/除非您为该变量分配一个新的可为值。

在某种意义上,在可为空的上下文中使用var创建的局部变量可以被认为处于“可以分配一个可为空的值,但编译器正在跟踪实际的空状态”的状态。因此,我个人喜欢将var -declared locals 视为混合的“可跟踪可空”类型。

覆盖/实现方法

C# 编译器知道覆盖/实现方法的上下文中的可空性。它考虑了协变和逆变;这意味着您可以删除返回类型的可空性并在输入上添加可空性,但不能相反:

public interface ITest {
	public string? GetStr();
	public void SetStr(string? s);

	public string GetStrNonNull();
	public void SetStrNonNull(string s);
}

public class Test : ITest {
	public string GetStr() => "Hello"; // Fine!
	public void SetStr(string s) { } // Warning here because you can not remove nullability on an input (i.e. parameter)

	public string? GetStrNonNull() => null; // Warning here because you can not add nullability on an output (i.e. return value)
	public void SetStrNonNull(string? s) { } // Fine!
}

• “覆盖/实现可空性”

易错性

不幸的是,仍然有可能在没有任何警告的情况下创建可能出现空引用异常的情况:

var stringArray = new string[10];
Console.WriteLine(stringArray[0].Length); // NRE here, no warning!

• “使用数组的可空上下文中的简单空引用异常”
即使我们的数组类型是字符串(而不是字符串?),编译器也无法强制我们用非空值初始化数组中的每个元素。因此,第二行的取消引用通过了“空测试”(因为表达式stringArray[0]返回的类型是不可为空的类型),所以没有发出警告,但我们最终得到一个空引用无论如何在运行时异常。

使用结构可以看到类似的效果:

readonly struct TestStruct {
	public readonly string S;
	public TestStruct(string s) => S = s;
}

sealed class TestClass {
	TestStruct _ts;

	public void SetTS(TestStruct ts) => _ts = ts;

	public void PrintSLength() {
		Console.WriteLine(_ts.S.Length); // NRE here if _ts hasn't been assigned a value yet
	}
}

• “使用结构的可空上下文中的简单空引用异常”
因为任何结构的默认值对于每个字段都是简单的零,所以任何引用类型字段都将设置为 null。因此,在有人调用SetTS()之前,_ts将等于default(TestStruct),这意味着_ts.S将为null。

和以前一样,因为_ts.S返回的是字符串而不是字符串?,编译器不会发出取消引用的警告,我们最终会在运行时出现空引用异常。

默认接口实现

此功能允许为接口方法指定默认实现:

interface IExampleInterface {
    int GetAlpha();

    int GetBravo() => 456;
}

class ExampleClass : IExampleInterface {
    public int GetAlpha() => 123;
}

class Program {
    static void Main() {
        IExampleInterface e = new ExampleClass();

        Console.WriteLine(e.GetAlpha()); // Prints '123'
        Console.WriteLine(e.GetBravo()); // Prints '456'
    }
}

• “基本 DIM 示例”
即使ExampleClass没有实现IExampleInterface.GetBravo(),因为指定了默认实现,我们仍然可以调用e.GetBravo()。

此功能主要旨在帮助库/API 维护人员向现有接口添加新方法,而不会有破坏下游实现接口的现有类的风险。如果您有成百上千个实现一个接口的类,那么在不使用 DIM 的情况下更改该接口可能会变得非常昂贵。

一些人担心这个特性会“破坏”接口的目的(即“接口是一种契约,不应该有实现”)。但是,接口的“含义”并没有改变:它仍然是一种用于前向声明一组方法的机制,类型必须实现这些方法以支持功能的一个方面。唯一的区别是,现在在可以提供合理的默认实现的情况下,我们可以提供该默认。
请注意,默认实现仅作为显式实现导入,因此不能用作实现类的常规公共方法:

ExampleClass e = new ExampleClass(); // Note 'e' is now of type ExampleClass rather than IExampleInterface

Console.WriteLine(e.GetAlpha());
Console.WriteLine(e.GetBravo()); // This line does not compile. We must cast 'e' to type IExampleInterface to use this method.

•“DIM 显式实现示例”
不幸的是,在撰写本文时,还没有官方支持的方法可以从覆盖实现中调用方法的默认实现(很像类继承的base.Method())。它是计划好的,但最终在发布前放弃了

但是,如果您希望在实现类可访问的接口中创建默认实现,则可以将其移入受保护的静态或公共静态方法。接口现在可以声明静态成员(方法/属性和字段)。就像类或结构上的静态成员一样,这些成员在通过接口名称本身而不是通过实例调用时是可访问的:

interface IExampleInterface {
    static readonly object _staticMutationLock = new object();
    static double _curValueMultiplier = 1d;
    
	public static double CurValueMultiplier {
        get {
            lock (_staticMutationLock) return _curValueMultiplier;
		}
        set {
            lock (_staticMutationLock) _curValueMultiplier = value;
        }
	}

    int CurValue { get; }

    void PrintCurValue() => PrintCurValueDefaultImpl(this);
	
	protected static void PrintCurValueDefaultImpl(IExampleInterface @this) => Console.WriteLine(@this.CurValue * CurValueMultiplier);
}

class ExampleClass : IExampleInterface {
    public int CurValue { get; } = 100;

    public void PrintCurValue() => IExampleInterface.PrintCurValueDefaultImpl(this); // Deliberately defer to default implementation
}

// ...

class Program {
    static void Main() {
        IExampleInterface e = new ExampleClass();

        IExampleInterface.CurValueMultiplier = 2d; // Static property access
        e.PrintCurValue(); // Prints 200
    }
}

• “静态接口方法”

接口成员可见性和多态行为

默认情况下,在接口上声明的成员始终是public。然而,现在可以将接口成员(静态或实例)声明为private、protected、internal或public(还有private protected和protected internal,我不会在此详述)。

私有成员仅对声明它们的接口中的其他成员可见。

内部成员对同一程序集中的任何其他源都是可见的。

公共成员对任何其他来源都是可见的。
不幸的是,受保护的接口成员更复杂:

受保护的 实例成员只能由子接口(而不是类)访问。

受保护的 静态成员可以被子接口和实现类访问。

外部代码根本无法访问受保护的成员,即使它们在实现类中被覆盖(为此,该类必须显式实现接口成员)。

当一个类覆盖或提供受保护成员的实现时,仍然应用多态/虚拟化。这意味着当受保护的成员被调用时,仍然使用类的实现:

interface IExampleInterface {
	protected void Test() => Console.WriteLine("Interface");

    void InvokeTest() => Test();
}

class ExampleClass : IExampleInterface {
	void IExampleInterface.Test() => Console.WriteLine("Class"); // This MUST be implemented explicitly
}

class Program {
    static void Main() {
        IExampleInterface e = new ExampleClass();

        e.InvokeTest(); // Prints "Class"
    }
}

• “受保护的接口方法多态性”
当一个类实现了两个接口,它们都为同一个父接口成员提供默认实现时,实现类必须提供它自己的实现:

interface IBase {
	char GetValue();
}

interface IDerivedAlpha : IBase {
	char IBase.GetValue() => 'A';
}

interface IDerivedBravo : IBase {
	char IBase.GetValue() => 'B';
}

class ExampleClass : IDerivedAlpha, IDerivedBravo {
	public char GetValue() => 'C'; // We must provide an implementation here or the compiler will emit an error
}

class Program {
    static void Main() {
        var e = new ExampleClass();

		Console.WriteLine(e.GetValue()); // Prints 'C'
		Console.WriteLine(((IDerivedAlpha) e).GetValue()); // Prints 'C'
		Console.WriteLine(((IDerivedBravo) e).GetValue()); // Prints 'C'
	}
}

• “DIM Diamond 问题解决方案”
子接口(即从其他接口扩展而来的接口)可以为其父级成员提供默认实现;以及覆盖现有的默认实现,甚至将成员重新声明为抽象:

interface IExampleInterface {
	int GetValue();
}

interface IExampleInterfaceChild : IExampleInterface {
	int IExampleInterface.GetValue() => 123; // Provides a default implementation for GetValue() in parent interface 'IExampleInterface'
}

interface IExampleInterfaceChildChild : IExampleInterfaceChild {
	abstract int IExampleInterface.GetValue(); // Re-abstracts (i.e. removes the default implementation) for GetValue()
}

• “带 DIM 的子接口”
也可以将成员标记为已密封。

子接口不能为密封成员提供新的实现。如果进行尝试,编译器会发出错误。

实现类也不能为密封成员提供新的实现,但是编译器允许声明具有相同名称的成员并且不会警告接口方法被隐藏:

interface IExampleInterface {
	sealed int GetValue() => 123;
}

class ExampleClass : IExampleInterface {
	public int GetValue() => 456; // No warning
}

class Program {
    static void Main() {
        var e = new ExampleClass();

		Console.WriteLine(e.GetValue()); // Prints 456
		Console.WriteLine(((IExampleInterface) e).GetValue()); // Prints 123
    }
}

•“密封接口成员”

高级模式匹配

此版本的 C# 添加了更多模式匹配功能。

切换表达式允许“切换”一个变量以产生一个新值:

// Assuming 'user' is a variable of type 'User':
var salary = user switch {
	Manager m when m.ManagerialLevel is ManagerialLevel.CLevel => 100_000, // C-Level managers get 100,000
	Manager m when m.ManagerialLevel is ManagerialLevel.UpperManagement => 70_000, // Upper managers get 70,000
	Manager _ => 50_000, // All other managers get 50,000
	_ => 30_000 // Everyone else gets 30,000
};

• “切换表达式”
新的属性模式允许更简洁的方法来匹配对象的属性:

var salary = user switch {
	Manager { ManagerialLevel: ManagerialLevel.CLevel } => 100_000, // C-Level managers get 100,000
	Manager { ManagerialLevel: ManagerialLevel.UpperManagement } => 70_000, // Upper managers get 70,000
	Manager _ => 50_000, // All other managers get 50,000
	{ LastAppraisal: { Rating: 10 } } => 40_000, // Users whose last appraisal gave them a 10/10 rating get 40,000
	_ => 30_000 // Everyone else gets 30,000
};

• “属性模式”
当类型提供Deconstruct方法(包括元组)时,我们可以使用位置模式来代替:

// For sake of example, imagine User has a method declared with the signature:
// "public void Deconstruct(out int lastAppraisalRating, out bool isOverEighteen)"

var salary = user switch {
	Manager m when m.ManagerialLevel is ManagerialLevel.CLevel => 100_000,
	Manager m when m.ManagerialLevel is ManagerialLevel.UpperManagement => 70_000,
	Manager => 50_000,
	User (10, true) => 45_000, // Users with a 10/10 rating who are also over 18 get 45,000 
	User (9, true) => 40_000, // Users with a 9/10 rating who are also over 18 get 40,000 
	User (8, true) => 35_000, // Users with an 8/10 rating who are also over 18 get 35,000
	_ => 30_000
};

• “使用解构切换表达式”
如果您只想以位置模式解构对象,则类型说明符是可选的:

var salary = user switch {
	(10, true) => 45_000,
	(9, true) => 40_000,
	(8, true) => 35_000,
	_ => 30,000
};

• “使用元组切换表达式”
上面描述的所有模式也可以在“传统的”switch 语句中使用。

IAsyncEnumerable

简而言之,此功能允许遍历等待项的枚举(即Task、ValueTask等),并创建异步迭代器。

假设DelayedSequence是一个实现IAsyncEnumerable的类,它简单地产生序列 [1..n] 中的每个整数,每次迭代之间有一个延迟:

var delayedSequence = new DelayedSequence(5, TimeSpan.FromSeconds(1d)); // Sequence of 1 to 5 with one second delay between each iteration

await foreach (var i in delayedSequence) {
	Console.WriteLine(i);
}

• “简单的异步可枚举示例”
await foreach告诉编译器我们要在执行循环体之前等待延迟序列 的每次迭代。IAsyncEnumerable < T> 每次迭代都会返回一个ValueTask,这就是我们在每个循环中等待的内容。使用WithCancellation()方法进行迭代时,可以传递CancellationToken :

await foreach (var i in someAsyncEnumerable.WithCancellation(someToken)) {
	/* ... */
}

• “将 CancellationToken 传递给异步迭代”
此功能的最大优势可能是编写异步生成器的能力,这是创建IAsyncEnumerable的最简单方法。举个例子,它从某个资源返回一个分页的结果列表:

public async IAsyncEnumerable<DataBatch> GetDataPaginated([EnumeratorCancellation] CancellationToken cancellationToken = default) {
	var paginationToken = new PaginationToken();
	
	try {		
		var results = await _database.GetNextResultBatch(paginationToken, cancellationToken);
		if (!results.ContainsValues) yield break;
		else yield return results.Batch;
	}
	catch (TaskCancelledException) {
		yield break;
	}
}

“异步生成器”
在这个实现中,我们通过await从数据库中读取一批新项目来 构造一个异步生成器;然后要么完成迭代(yield break),要么传递下一个要迭代的DataBatch 。 编译器将自动为我们将其转换为IAsyncEnumerable实现(您仍然可以手动实现此接口并在需要时提供手动实现 - 它与实现IEnumerable没有什么不同)。cancelToken上的[EnumeratorCancellation]属性

需要参数来告诉编译器该参数是我们在通过WithCancellation()方法迭代返回的IAsyncEnumerable时要使用的参数(请记住,调用GetDataPaginated()的调用者可能不是迭代返回的参数IAsyncEnumerable,所以我们不能总是在可枚举的创建点传入一个CancellationToken )。

索引和范围

此功能将两种新类型添加到框架中,它们可以协同工作,即Index和Range,以及两种新的相应语法。

指数

索引表示集合或某种可枚举的元素索引 。它没有任何链接或对任何特定可枚举/集合的引用;相反,它只是一个独立的值。

索引es 可以指定为从集合开头的偏移量(如传统)或从结尾:

var characterArray = new[] { 'A', 'B', 'C', 'D', 'E', 'F', 'G' };

Index indexA = 0; // characterArray[indexA] is 'A'
Index indexB = 3; // characterArray[indexB] is 'D'
Index indexC = ^0; // characterArray[indexC] throws an IndexOutOfRangeException
Index indexD = ^3; // characterArray[indexD] is 'E'

Index indexA2 = Index.Start; // characterArray[indexA2] is 'A'
Index indexB2 = Index.FromStart(3); // characterArray[indexB2] is 'D'
Index indexC2 = Index.End; // characterArray[indexC2] throws an IndexOutOfRangeException
Index indexD2 = Index.FromEnd(3); // characterArray[indexD2] is 'E'

“索引创建示例”

指定索引的最简单方法是通过整数的隐式转换;它创建一个索引,指定从可枚举/集合开始的偏移量。indexA和indexB都证明了这一点。Index.Start等价于(Index) 0或Index.FromStart(0)。

但是,Index es 也可以指定从可枚举/集合末尾的偏移量。indexC和indexD都证明了这一点。N语法表示我们正在创建一个从末尾倒数的索引。0指向任何给定可枚举/集合的“结束”的一个元素;因此为什么characterArray[0]会引发异常。1将始终为您提供最后一个元素。Index.End等价于^0或Index.FromEnd(0)。在上面的示例中,indexA2与 indexA 相同, indexB2与 indexB相同,等等。

一些人(包括我)最初对^0索引集合末尾的一个元素感到惊讶。但是在处理范围时它很有意义。实际上,我在 2018 年写了一些关于此的文章:C# 8 Concerns - A Followup
Index类型 的一些其他成员:

// The Value and IsFromEnd properties can be used to deconstruct the index:

Console.WriteLine($"Index A: {indexA.Value}{(indexA.IsFromEnd ? " (from end)" : "")}"); // Index A: 0
Console.WriteLine($"Index B: {indexB.Value}{(indexB.IsFromEnd ? " (from end)" : "")}"); // Index B: 3
Console.WriteLine($"Index C: {indexC.Value}{(indexC.IsFromEnd ? " (from end)" : "")}"); // Index C: 0 (from end)
Console.WriteLine($"Index D: {indexD.Value}{(indexD.IsFromEnd ? " (from end)" : "")}"); // Index D: 3 (from end)

// GetOffset() will tell you what value the Index translates to for a collection of a given length:

Console.WriteLine($"Index A in characterArray: {indexA.GetOffset(characterArray.Length)}"); // Index A in characterArray: 0
Console.WriteLine($"Index B in characterArray: {indexB.GetOffset(characterArray.Length)}"); // Index B in characterArray: 3
Console.WriteLine($"Index C in characterArray: {indexC.GetOffset(characterArray.Length)}"); // Index C in characterArray: 7
Console.WriteLine($"Index D in characterArray: {indexD.GetOffset(characterArray.Length)}"); // Index D in characterArray: 4

全屏查看代码• “索引其他成员”

因为“从头开始”的索引在内部表示为 Index 结构中的负整数,所以Index的值永远不会是负数。

Range

一个Range实例包含两个Index;一个开始和一个结束。

请注意,此处讨论的Range结构位于System命名空间中。System.Data中还有另一个不相关的Range类型。

var characterArray = new[] { 'A', 'B', 'C', 'D', 'E', 'F', 'G' };

var rangeA = 0..3; // characterArray[rangeA] is ['A', 'B', 'C']
var rangeB = 3..^0; // characterArray[rangeB] is ['D', 'E', 'F']
var rangeC = 0..^0; // characterArray[rangeC] is ['A', 'B', 'C', 'D', 'E', 'F', 'G']
var rangeD = 4..^4; // characterArray[rangeD] throws ArgumentOutOfRangeException

var rangeA2 = Range.EndAt(3);
var rangeB2 = Range.StartAt(3);
var rangeC2 = Range.All;
var rangeD2 = new Range(4, ^4);

var rangeA3 = ..3;
var rangeB3 = 3..;
var rangeC3 = ..;

“范围创建示例”

创建Range的最简单方法是使用..语法(称为Range 运算符);每边都有一个索引。运算符左侧的索引是包含开始索引,而运算符右侧的索引是排他结束索引。

如果要创建Start为0的范围,可以省略第一个参数(请参阅rangeA3)。

如果要创建End为^0的范围,可以省略第二个参数(请参阅rangeB3)。

这两个快捷方式可以组合起来创建一个代表所有元素的范围(请参阅rangeC3)。

像以前一样,rangeA与rangeA2相同(与rangeA3 一样),等等。

请记住,N是一种创建索引的语法,该索引表示从给定可枚举/集合*末尾开始的*N个值。因此,范围0..0表示任何可枚举/集合中的每个项目。这与Range.All和..相同。
Range类型 的一些其他成员:

var characterArray = new[] { 'A', 'B', 'C', 'D', 'E', 'F', 'G' };

var range = ^5..7;

Console.WriteLine(range.Start); // ^5
Console.WriteLine(range.End); // 7
Console.WriteLine(range.GetOffsetAndLength(characterArray.Length).Offset); // 2
Console.WriteLine(range.GetOffsetAndLength(characterArray.Length).Length); // 5

“范围其他成员”

请注意,如果给定的集合长度太小而无法容纳目标范围,则GetOffsetAndLength()将引发ArgumentOutOfRangeException 。

支持索引和范围的类型

数组具有对Range的 内置支持,如上所示。使用范围创建子数组会返回一个新数组,其值是从原始数组中复制的:

var characterArray = new[] { 'A', 'B', 'C', 'D', 'E', 'F', 'G' };

// This line is translated by the compiler to: var subArray = RuntimeHelpers.GetSubArray(characterArray, 1..^1);
var subArray = characterArray[1..^1];

characterArray[3] = 'X'; // Altering values in the original array does not affect the subArray

Console.WriteLine(subArray.Length); // 5
Console.WriteLine(String.Join(", ", subArray.Select(c => '\'' + c.ToString() + '\''))); // 'B', 'C', 'D', 'E', 'F'

•“数组范围支持”
此外,任何具有公共Count或Length的类型都可以自动支持Index es,如果它们提供索引运算符和Range es,如果它们提供带有签名Slice(int, int)的方法:

// This class has everything required for automatic Index and Range support
class NumberLine {
	public int StartValue { get; }
	public int Length { get; }

	public NumberLine(int startValue, int length) {
		StartValue = startValue;
		Length = length;
	}

	public int this[int index] {
		get {
			if (index >= Length) throw new ArgumentOutOfRangeException(nameof(index));
			return StartValue + index;
		}
	}

	public IEnumerable<int> Slice(int offset, int length) {
		if (offset + length > Length) throw new ArgumentOutOfRangeException(nameof(length));
		for (var i = 0; i < length; ++i) yield return StartValue + offset + i;
	}
}

// Here we demonstrate the automatic support
var numberLine = new NumberLine(0, 10);

Console.WriteLine(numberLine[Index.Start]); // 0
Console.WriteLine(numberLine[Index.FromEnd(1)]); // 9

Console.WriteLine(String.Join(", ", numberLine[..])); // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
Console.WriteLine(String.Join(", ", numberLine[3..7])); // 3, 4, 5, 6
Console.WriteLine(String.Join(", ", numberLine[^7..^3])); // 3, 4, 5, 6

• “自动索引和范围支持”
框架中的一些其他类型也提供自动索引和/或范围支持,包括Span和许多集合类型。

RAII 风格的 using 语句

此功能允许声明应在封闭范围结束时自动释放的变量:

void Test() {
	using var fileStream = File.OpenRead("somefile.txt");
	
	// ...
	
	// fileStream.Dispose() automatically invoked here at the end of this method
}

• “RAII 使用语句”

静态局部函数

这种面向性能的特性允许您确保本地函数不捕获任何变量。

以非静态局部函数(CreateUserDetailsS tring()中的CombineData() )为例:

class User {
	public string Name { get; }

	public string PermanentData { get; }

	public string CreateUserDetailsString(string additionalData) {
		string CombineData() {
			return PermanentData + additionalData;
		}

		return $"{Name} ({CombineData()})";
	}
}

• “非静态局部函数示例”
CombineData()从其本地范围之外捕获两个变量,additionalData和this(这使它可以访问this.PermanentData)。在性能敏感的场景中,变量捕获会增加垃圾收集器的压力,这是有害的。

将局部函数声明为静态将导致编译器不允许捕获任何变量。反过来,这将导致编译器错误,直到程序员手动将这些变量传递给本地函数。以当前形式将CombineData()

标记为静态将产生两个编译器错误,告诉我们不能引用它并且我们不能引用additionalData。为了解决这个问题,我们必须像标准方法调用一样传入我们想要使用的参数:

public string CreateUserDetailsString(string additionalData) {
	static string CombineData(string permanentData, string additionalData) {
		return permanentData + additionalData;
	}

	return $"{Name} ({CombineData(PermanentData, additionalData)})";
}

•“静态局部函数示例”

只读结构成员

这种面向性能的添加允许将结构的特定成员标记为不可修改/不可修改。

正如前面在参数中讨论的那样,只读结构对于允许编译器灵活地不创建参数的防御性副本很重要。但是,有时结构必须是可变的并且不能标记为readonly。此功能允许将结构的某些部分设为只读,因此允许编译器在某些情况下仍然避免防御性副本。

在这种情况下,只读成员在某种程度上可以比作C++ 中的const成员。

struct MyStruct {
	public int Alpha { get; set; }

	// Readonly property
	public readonly int Bravo { get; }

	public void IncrementAlpha() {
		Alpha += 1;
	}

	// Readonly method: Does not alter any state in this struct
	public readonly void PrintBravo() {
		Console.WriteLine(Bravo);
	}
}

• “只读结构成员示例”
尝试将IncrementAlpha()标记为只读将导致引发编译器错误,因为操作Alpha += 1修改了Alpha。

空合并赋值

这个小功能允许您仅在变量为空时使用简洁的语法为变量分配值。以下示例中的两行具有相同的含义:

// Classic example
if (myStr == null) myStr = "Hello";

// New way with null-coalescing assignment
myStr ??= "Hello";

•“空合并分配”

posted @ 2022-09-04 22:38  溪源More  阅读(127)  评论(0编辑  收藏  举报