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 usstring _nonNullableField;// We don't have to supply any initial value for a nullable field, the default value of null is acceptablestring? _nullableField;// If we don't assign a value to NonNullableProperty here or in the constructor, the compiler will warn uspublic string NonNullableProperty { get; set; } = "Hello";// We don't have to supply any initial value for a nullable property, the default value of null is acceptablepublic 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 usConsole.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 checkConsole.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>)

全屏查看代码[1]• “使用带有可为空引用的 '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'

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

static T? ReturnInput<T>(T? t) where T : IComparable? => t; // Notice nullable token (?) after IComparable constraintvar 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

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

static T? ReturnInput<T>(T? t) where T : notnull => t; // Notice nullable token (?) after IComparable constraintvar 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.

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

属性

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” 还有一些适用于罕见用例的附加属性,都可以在这里找到:保留属性有助于编译器的空状态静态分析[5]

隐式类型变量

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

5ca0793a649251acab92ca1a5e6c0570.png
img

[6] 原因在语言设计会议的笔记中[7]给出:

“在这一点上,我们已经看到大量代码需要人们拼出类型而不是使用 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 的情况下更改该接口可能会变得非常昂贵。

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

ExampleClass e = new ExampleClass(); // Note 'e' is now of type ExampleClass rather than IExampleInterfaceConsole.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())。它是计划好的,但最终在发布前放弃了[9]

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

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 accesse.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 456Console.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,000Manager m when m.ManagerialLevel is ManagerialLevel.UpperManagement => 70_000, // Upper managers get 70,000Manager _ => 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,000Manager { ManagerialLevel: ManagerialLevel.UpperManagement } => 70_000, // Upper managers get 70,000Manager _ => 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 iterationawait 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[10]。 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

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

因为“从头开始”的索引在内部表示为 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 ArgumentOutOfRangeExceptionvar 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 subArrayConsole.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)]); // 9Console.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 使用语句”

静态局部函数

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

以非静态局部函数(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)})";
}

•“静态局部函数示例”

只读结构成员

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

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

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

struct MyStruct {public int Alpha { get; set; }// Readonly propertypublic readonly int Bravo { get; }public void IncrementAlpha() {Alpha += 1;}// Readonly method: Does not alter any state in this structpublic 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";

•“空合并分配”

References

[1] 全屏查看代码: https://benbowen.blog/post/two_decades_of_csharp_iv/using_'struct'_constraint_with_nullable_references-.html
[2] 全屏查看代码: https://benbowen.blog/post/two_decades_of_csharp_iv/using_'class'_constraint_with_nullable_references-.html
[3] 全屏查看代码: https://benbowen.blog/post/two_decades_of_csharp_iv/using_nullable_interface_constraint.html
[4] 全屏查看代码: https://benbowen.blog/post/two_decades_of_csharp_iv/using_notnull_constraint.html
[5] 保留属性有助于编译器的空状态静态分析: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/attributes/nullable-analysis
[6]

118b5af02c898d78b551002bb73f0aad.png
img

https://img2022.cnblogs.com/blog/191302/202209/191302-20220904224809805-1987561416.png
[7] 语言设计会议的笔记中: https://github.com/dotnet/csharplang/blob/master/meetings/2019/LDM-2019-12-18.md#var
[8] 显式实现: https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/interfaces/explicit-interface-implementation
[9] 最终在发布前放弃了: https://github.com/dotnet/csharplang/blob/6b5a397cc4faef6d481d9bace13d7e3c19de63bb/meetings/2019/LDM-2019-04-29.md#conclusion
[10] C# 8 Concerns - A Followup: https://benbowen.blog/post/csharp_8_concerns_followup/#range_operator_and_'hat'_operator
[11] 全屏查看代码: https://benbowen.blog/post/two_decades_of_csharp_iv/index_additional_members.html
[12] 捕获: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/lambda-expressions#capture-of-outer-variables-and-variable-scope-in-lambda-expressions
[13] 正如前面: https://benbowen.blog/post/two_decades_of_csharp_iii/#in_parameters,_readonly_structs,_readonly_ref_returns

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/283024.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

windows 提权 cve-2018-8897

windows 提权 cve-2018-8897影响范围&#xff1a;基本上是全版本具体影响范围看详情&#xff1a;https://portal.msrc.microsoft.co … isory/CVE-2018-8897http://www.o2oxy.cn/wp-content/uploads/2018/06/cve-2018-8897.rar转载于:https://blog.51cto.com/9861015/2126608

java servlet练习测试

步骤&#xff1a; 0、首先创建web project&#xff0c;工程名&#xff1a;test_servlet 1、编写Servlet&#xff0c;TestServlet.java文件内容&#xff1a; package com.ouyang.servlet;import java.io.IOException; import java.sql.Connection; import java.sql.DriverManage…

《ASP.NET Core 6框架揭秘》实例演示[19]:数据加解密与哈希

数据保护&#xff08;Data Protection&#xff09;框架旨在解决数据在传输与持久化存储过程中的一致性&#xff08;Integrity&#xff09;和机密性&#xff08;confidentiality&#xff09;问题&#xff0c;前者用于检验接收到的数据是否经过篡改&#xff0c;后者通过对原始的数…

如何在ABAP Netweaver和CloudFoundry里记录并查看日志

Netweaver 要记录日志需要有一个checkpoint group&#xff0c;可以自行创建也可以使用标准的。这里我重用标准的group&#xff1a;DEMO_CHECKPOINT_GROUP。 tcode SAAB&#xff0c;点Display <->Activate进入编辑模式&#xff0c;将Logpoints设置为"Log"&#…

如何成为有效学习的高手(许岑)——思维导图

总结自许岑精品课《如何成为有效学习的高手》&#xff0c;图片看不清的可以看下面。 最后有彩蛋&#xff01;最后有彩蛋&#xff01;最后有彩蛋&#xff01; 定义 高效学习的定义&#xff1a;找到最适合自己的学习手法&#xff0c;在相对短的时间内集中注意力&#xff0c;以解决…

WPF Canvas 平滑笔迹

WPF Canvas 平滑笔迹控件名&#xff1a;CanvasHandWriting作者&#xff1a;小封&#xff08;邝攀升&#xff09;原文链接&#xff1a; https://github.com/WPFDevelopersOrg/WPFDevelopers编辑&#xff1a;驚鏵完整的思路如下收集路径点集。平均采样路径点集。将路径点集转为…

NetSpeed

NetSpeed公司提供的NOC包括三部分&#xff0c;可以通过NocStudio进行配置生成。 1)NetSpeed Orion&#xff0c;面向快速SoC design的可综合平台。 2)Linley NetSpeed NoC面向复杂的interconnect实现&#xff0c;同时优化内部physical implementation和timing closure. NoC是基于…

js ajax java传参_ajax参数传递与后台接收

ajax参数传递与后台接收Servlet中读取http参数的方法Enumeration getParameterNames() 返回一个 String 对象的枚举&#xff0c;包含在该请求中包含的参数的名称String getParameter(String name) 以字符串形式返回请求参数的值&#xff0c;或者如果参数不存在则返回 null。Str…

init 访问器只能初始化时赋值,是真的吗?

前言C# 提供的 init 关键字用于在属性中定义访问器方法&#xff0c;可以让属性仅能在对象初始化的时候被赋值&#xff0c;其他时候只能为只读属性的形式。例如下面代码可以正常执行&#xff1a;public class Demo {public string Name { get; init; } }var demo new Demo { Na…

eclipse实现代码块折叠-类似于VS中的#region……#endregion

背 景 刚才在写代码的时候&#xff0c;写了十几行可以说是重复的代码&#xff1a; 如果整个方法或类中代码多了&#xff0c;感觉它们太TM占地方了&#xff0c;给读者在阅读代码上造成很大的困难&#xff0c;于是想到能不能把他们“浓缩”成一行&#xff0c;脑子里第一个闪现出的…

java定义基础变量语句_java语言基础-变量

一丶变量的基本概念1.什么是变量(1).内存中的一个存储区域(2).该区域有自己的名称(变量名),和类型(数据类型)(3.)该区域的数据可以在同一类型范围内不断变化(定义变量的主要目的是因为数据的不确定性)2.为什么要定义变量用来不断存放同一类型的常量&#xff0c;并可以重复使用3…

C# WPF MVVM模式[经典]案例

01—前言Caliburn.Micro(简称CM)一经推出便备受推崇&#xff0c;作为一款MVVM开发模式的经典框架&#xff0c;越来越多的受到wpf开发者的青睐.我们看一下官方的描述&#xff1a;Caliburn是一个为Xaml平台设计的小型但功能强大的框架。Micro实现了各种UI模式&#xff0c;用于解决…

shell数组

定义数组[rootwy shell]# a(1 2 3 4)显示数组[rootwy shell]# echo ${a[]}1 2 3 4[rootwy shell]# echo ${a[*]}1 2 3 4显示数组中的某个元素[rootwy shell]# echo ${a[0]}1增加元素[rootwy shell]# a[4]9[rootwy shell]# echo ${a[*]}1 2 3 4 9修改元素值 [rootwy shell]# a[2…

LINUX中常用操作命令

LINUX中常用操作命令 引用&#xff1a;http://www.daniubiji.cn/archives/25 Linux简介及Ubuntu安装 常见指令系统管理命令打包压缩相关命令关机/重启机器Linux管道Linux软件包管理vim使用用户及用户组管理文件权限管理Linux简介及Ubuntu安装 Linux&#xff0c;免费开源&#x…

Log4j编写

来自: http://www.blogjava.net/zJun/archive/2006/06/28/55511.html Log4J的配置文件(Configuration File)就是用来设置记录器的级别、存放器和布局的&#xff0c;它可接keyvalue格式的设置或xml格式的设置信息。通过配置&#xff0c;可以创建出Log4J的运行环境。1. 配置文件L…

C# 为什么高手喜欢用StartsWith而不是Substring进行字符串匹配?

字符串的截取匹配操作在开发中非常常见&#xff0c;比如下面这个示例&#xff1a;我要匹配查找出来字符串数组中以“abc”开头的字符串并打印&#xff0c;我下面分别用了两种方式实现&#xff0c;代码如下&#xff1a;using System;namespace ConsoleApp23 {class Program{stat…

Nginx 服务器开启status页面检测服务状态

原文&#xff1a;http://www.cnblogs.com/hanyifeng/p/5830013.html 一、Nginx status monitor 和apache 中服务器状态一样。输出的内容如&#xff1a; 第1列&#xff1a; 当前与http建立的连接数&#xff0c;包括等待的客户端连接&#xff1a;2第2列&#xff1a;接受的客户端连…

在OpenCloudOS 上安装.NET 6

开源操作系统社区 OpenCloudOS 由腾讯与合作伙伴共同倡议发起&#xff0c;是完全中立、全面开放、安全稳定、高性能的操作系统及生态。OpenCloudOS 沉淀了多家厂商在软件和开源生态的优势&#xff0c;继承了腾讯在操作系统和内核层面超过10年的技术积累&#xff0c;在云原生、稳…

java产生的数字发送到页面_JAVA中数字证书的维护及生成方法

Java中的keytool.exe可以用来创建数字证书&#xff0c;所有的数字证书是以一条一条(采用别名区别)的形式存入证书库的中&#xff0c;证书库中的一条证书包含该条证书的私钥&#xff0c;公钥和对应的数字证书的信息。证书库中的一条证书可以导出数字证书文件&#xff0c;数字证书…

IDEA破解 2017 IDEA license server 激活(可用)

进入ide主页面&#xff0c;help-register-license server,然后输入 http://idea.iteblog.com/key.PHP&#xff08;注意&#xff1a;php要小写&#xff09;即可~ 转载于:https://www.cnblogs.com/austinspark-jessylu/p/7232982.html