C# 二十年语法变迁之 C# 7参考
C# 二十年语法变迁之 C# 7参考
https://benbowen.blog/post/two_decades_of_csharp_iii/
自从 C# 于 2000 年推出以来,该语言的规模已经大大增加,我不确定任何人是否有可能在任何时候都对每一种语言特性都有深入的了解。因此,我想写一系列快速参考文章,总结自 C# 2.0 以来所有主要的新语言特性。我不会详细介绍它们中的任何一个,但我希望这个系列可以作为我自己(希望你也是!)的参考,我可以不时回过头来记住我使用的工具工具箱里有。😃
开始之前的一个小提示:我将跳过一些更基本的东西(例如 C# 2.0 引入了泛型,但它们的使用范围如此广泛,以至于它们不值得包括在内);而且我还可以将一些功能“粘合”在一起,以使其更简洁。本系列并不打算成为该语言的权威或历史记录。相反,它更像是可能派上用场的重要语言功能的“备忘单”。您可能会发现浏览左侧的目录以搜索您不认识或需要快速提醒的任何功能很有用。
C# 7.0
元组
元组是包含一组两个或多个相关对象的结构。它们对于处理或返回通常不会一起关联到类或结构中的多个相关值很有用。
static void Test() {
// This line creates a 3-tuple (a tuple with 3 items) named 'userDetails'.
// The tuple's three properties are an int named Age, a string named Name, and a float named TestScore
var userDetails = (Age: 30, Name: "Ben", TestScore: 50f);
// ... Later on we can use the tuple like any old struct/class:
var age = userDetails.Age;
var name = userDetails.Name;
var testScore = userDetails.TestScore;
}
• “元组声明”
如果我们想将元组传递给另一个函数或将其存储在容器中怎么办?我们可以使用类似的语法声明一个元组类型:
static readonly List<(int Age, string Name, float TestScore)> _userList;
static void AddUserToList((int Age, string Name, float TestScore) user) {
_userList.Add(user);
}
// ...
static void Test() {
AddUserToList((30, "Ben", 50f));
}
• “元组类型声明”
元组的一个很好的用途是更适合替代方法上的“输出”参数:
static (int Min, int Max) GetLimits(IReadOnlyCollection<int> values) {
return (values.Min(), values.Max());
}
• “元组返回类型”
static void Test() {
var values = new[] { 1, 2, 3, 4, 5, 6, 7 };
// Using the GetLimits() function from previous example
var (min, max) = GetLimits(values);
Console.WriteLine($"Min value is {min}");
Console.WriteLine($"Min value is {max}");
}
// Alternative syntax for pre-existing variables:
static int _min;
static int _max;
static void Test() {
var values = new[] { 1, 2, 3, 4, 5, 6, 7 };
(_min, _max) = GetLimits(values); // No 'var' because we're not declaring new variables.
Console.WriteLine($"Min value is {_min}");
Console.WriteLine($"Min value is {_max}");
}
• “元组解构”
但是请注意,元组不应该被过度使用。当你真正需要的是一个适当的类或结构时,到处使用元组是很危险的。将相关数据一起封装成“真实”类型!例如,实际上,这些示例中的字段应封装为User对象。我建议不要在公共 API 中使用元组(我只在方法/类的实现和私有帮助函数中使用它们)。
可以使用或不使用程序员定义的属性名称来声明元组(从 C# 7.1 开始):
static void Test() {
var age = 30;
var name = "Ben";
var testScore = 50f;
var implicitlyNamedTuple = (age, name, testScore);
var explicitlyNamedTuple = (Age: age, Name: name, TestScore: testScore);
//var userAge = implicitlyNamedTuple.age; // Looks ugly! 'age' property is lower-case
var userAge = explicitlyNamedTuple.Age; // Much better :)
}
• “元组属性命名”
请注意,当自动创建名称时,编译器只需复制传入的参数、字段、本地或属性的名称来创建元组;因此我们的implicitlyNamedTuple的属性是小写的。正是出于这个原因,我总是喜欢使用显式命名(因为当然,C# 中的驼峰命名法对于公共成员来说是非常规的)。
任何元组的基础类型是ValueTuple<T1, T2, ..., Tn>,其中类型参数的数量等于元组中的项目数。注意不要使用Tuple<>代替ValueTuple<>;这是一种较旧的、大部分已弃用的类型,效率较低,与ValueTuple<>相比几乎没有任何优势。
元组成员的命名实际上是一个编译器技巧。ValueTuple<>中属性的“真实”名称始终是Item1、Item2、Item3等。在声明元组类型时,编译器会添加一个专门的属性在幕后让它工作,这也是智能感知的。
尽管元组是值类型,但有趣的是它们是可变的。这有一些性能影响,以及使用可变值类型时通常的“陷阱”。
自定义解构
您可以通过声明公共Deconstruct()方法 使任何类型像元组一样可解构。该方法必须返回 void,并且所有参数都必须是参数- 这些将成为将被分配的解构变量。这是一个例子:
class User {
public string Name { get; set; }
public int Age { get; set; }
public void Deconstruct(out string name, out int age) {
name = Name;
age = Age;
}
}
// ...
static void Test() {
var user = new User { Name = "Ben", Age = 30 };
var (name, age) = user;
}
• “用户类解构器”
简单模式匹配
这是一系列旨在使编写某些程序更容易的各种新功能。
is 表达式
对于检查对象是否是给定类型的实例并同时为所述对象创建该类型的本地别名最有用:
var user = GetUser();
if (user is Manager manager) {
Console.WriteLine($"User is a manager and has {manager.Reports.Count} direct reports!");
}
• “类型模式的“是”表达式”
Null 值永远不会匹配is 表达式,因此is 表达式可用于过滤掉 null 值:
var userList = GetUserList();
if (userList.FirstOrDefault() is User user) {
Console.WriteLine($"User database contains at least one user!");
}
• “用于空检查的“是”表达式”
除了类型模式,还支持常量模式。这些都可以在 switch 表达式中使用。当与允许过滤匹配的when 表达式结合使用时,常量模式最有用。
当匹配多个 switch case 时,只输入遇到的第一个匹配 case(你甚至不能用goto case故意在 case 之间跳转)。一个例外是默认情况,它总是最后评估。
var user = GetUser();
// Type pattern matching
switch (user) {
case null:
Console.WriteLine("No user found.");
break;
case Manager m when m.Department == Department.Engineering:
Console.WriteLine($"User is a manager in the engineering department and has {m.Reports.Count} direct reports.");
break;
case Manager m when m.Department == Department.Marketing:
Console.WriteLine($"User is a manager in the marketing department and manages {m.CustomerAccounts.Count} customer accounts.");
break;
case Trainee t when t.AgeInYears >= 18:
Console.WriteLine($"User is a trainee and has completed {t.Courses.Count(c => c.IsCompleted)} of their training courses.");
break;
case Trainee t: // This case will only be entered if the one above was not
Console.WriteLine($"User is a junior trainee.");
break;
default:
Console.WriteLine($"User is just a user.");
break;
}
• “使用 When 表达式切换模式匹配”
与is expression类似,null 用户无法匹配首先检查其类型的任何大小写。即使我们将user声明为类型为Manager的局部变量,如果GetUser()返回null值,则永远不会输入 case case Manager m:(即使我们删除了case null:)。
局部函数
此功能允许在函数内声明函数。这些内部(即本地)函数只能在外部函数的范围内访问。
static void PrintUserReport(List<User> users) {
string CreateBioString(User u) {
var bioStart = $"{u.Name}: {u.AgeInYears} years old, {(DateTime.Now - u.JoinDate).TotalYears:N1} years at company";
if (u.AgeInYears <= 18) return bioStart;
else return $"{bioStart}; marital status: {u.MaritalStatus}";
}
foreach (var user in users) {
Console.WriteLine(CreateBioString(user));
}
}
// ... On User.cs ...
bool DueForPayRaise {
get {
bool IsEligible() {
return AgeInYears >= 18 && (DateTime.Now - u.JoinDate).TotalYears >= 1d;
}
return IsEligible() && (DateTime.Now - u.LastPayRaise).TotalYears >= 1d;
}
}
• “本地函数”
内联“Out”变量声明
这个简单的特性允许在使用 out 变量时更加简洁:
// BEFORE
static void Test() {
int parseResult;
if (Int32.TryParse(someString, out parseResult)) {
// ... Use parseResult here
}
}
// AFTER
static void Test() {
if (Int32.TryParse(someString, out var parseResult)) {
// ... Use parseResult here
}
}
• “内联输出变量”
抛出表达式
这个非常方便的功能也有助于简洁。它允许您在通常期望值的地方抛出异常。例子:
User _currentUser;
public User CurrentUser {
get {
return _currentUser;
}
set {
_currentUser = value ?? throw new ArgumentNullException(nameof(value));
}
}
• “Throw Expressions Example A”
public MaritalStatus MaritalStatus {
get {
return _currentUser.AgeInYears >= 18
? _currentUser.MaritalStatus
: throw new InvalidOperationException($"Can not disclose marital status of non-adult.");
}
}
• “Throw Expressions Example B”
参考本地和返回
这个与性能相关的特性允许使用、存储和返回对变量/数据位置的引用。
从早期的 C# 开始,引用参数允许我们将变量的引用传递给方法。现在我们还可以返回对属性、字段或其他堆分配变量(例如数组值)的引用:
int _viewMatricesStartIndex;
int _projectionMatricesStartIndex;
int _worldMatricesStartIndex;
Matrix4x4[] _matrices;
public ref Matrix4x4 GetMatrix(MatrixType type, int offset) {
switch (type) {
case MatrixType.ViewMatrix:
return ref _matrices[_viewMatricesStartIndex + offset];
case MatrixType.ProjectionMatrix:
return ref _matrices[_projectionMatricesStartIndex + offset];
case MatrixType.WorldMatrix:
return ref _matrices[_worldMatricesStartIndex + offset];
default:
throw new ArgumentOutOfRangeException(nameof(type));
}
}
• “Ref Returns”
此方法返回对 _matrices 数组中 Matrix4x4 的引用,而不是其value 的副本。对于复制大型值类型实例将不可避免的情况,这可以带来性能优势。 在方法中使用返回的引用需要声明一个ref local:
static void Test() {
// Sets _matrices[_viewMatricesStartIndex + 3] to Matrix4x4.Identity
ref var viewMatrix = ref GetMatrix(MatrixType.ViewMatrix, 3);
viewMatrix = Matrix4x4.Identity;
// We can dereference the reference and copy its value to a local here by using the standard local variable declaration syntax
var projMatrix = GetMatrix(MatrixType.ProjectionMatrix, 2);
projMatrix.M11 = 3f; // Changes only the local 'projMatrix', does not affect anything in _matrices
}
• “Ref Locals”
这两种单独的语法允许“选择加入”在按引用返回的方法上使用 ref locals;当我们不想要或不需要它时,一直忽略它。
我们还可以通过直接从 ref-returning 方法返回的引用来设置值:
static void Test() {
// Sets _matrices[_viewMatricesStartIndex + 3] to Matrix4x4.Identity
GetMatrix(MatrixType.ViewMatrix, 3) = Matrix4x4.Identity;
}
• “通过返回的引用设置值”
弃元
此功能允许声明您忽略所需参数的意图。使用下划线 ( _ ) 表示您不想使用 out 参数、表达式的结果或 lambda 参数:
static void Test() {
// Just want to test if this is a valid value; we don't need the parsed value
if (!Int32.TryParse(_userInput, out _)) {
// ..
}
// Don't want the result of this method, just need to invoke it
_ = _someInterface.SomeMethod();
// Don't want to use these parameters in a lambda (C# 9+ only)
_someInterface.DoThing((_, _, param3, _) => param3 == "hello");
}
数字分隔符
此功能允许您使用下划线分隔整数文字的数字:
const int DecimalConstant = 123_456_789;
const int HexadecimalConstant = 0xAB_CD_EF;
• “数字分隔符”
二进制字面量
此功能允许以二进制格式声明整数常量:
const int BinaryConstant = 0b1110_0011_1101_0001;
• “二进制文字”
C# 7.1
ValueTask/ValueTask 和 IValueTaskSource
在 C# 中 封装future的主要方法是使用Task和Task
ValueTask和ValueTask
对于异步函数,您希望该函数成为热路径的一部分,频繁调用,并且该函数通常能够同步完成,ValueTask很有意义:
// If we assume most users are not logged in, we can avoid allocating a Task object every time we invoke this method for most cases
// In the case where the user IS logged in, we wrap an actual Task<int> which will then be deferred to
public ValueTask<int> GetUserIDAsync(User u) {
if (!u.IsLoggedIn) return ValueTask.FromResult(0);
else return new ValueTask<int>(FetchUserIDFromDatabaseAsync(u)); // Assume FetchUserIDFromDatabaseAsync() returns a Task<int>
}
• “ValueTask 示例”
可以像常规Task或Task
注意:C# 为声明公共GetAwaiter()方法(或具有通过扩展方法定义的方法)的任何类型提供await支持,该方法返回具有一小组先决条件公共成员的对象。ValueTask和ValueTask
注意:实际上,框架缓存了一些常见的 Task 结果。
当方法可以同步完成时,这种方法可以消除不必要的垃圾。
ValueTask和ValueTask
实现方法有以下三种:
异步状态机将调用GetStatus以获取异步操作的当前状态。
异步状态机将调用GetResult以获取异步操作完成时的结果。
OnCompleted将由异步状态机调用,以将延续传递给您的实现,在异步操作完成时必须调用该延续;或者如果已经完成则立即调用。
如上所述,多次等待或从任何 ValueTask 获取结果是错误的;这允许我们假设GetResult每次操作只会被调用一次(超过这个次数是用户的错误,可以被认为是不受支持的)。同样,它还允许我们假设一旦调用GetResult,IValueTaskSource实例就可以重新用于下一个异步操作。
传递给所有方法的短令牌可用于确保遵守此条件。
默认文字
这个小功能允许在指定类型的默认值时省略类型名称:
// Before
const int Zero = default(int);
// After
const int Zero = default;
• “默认文字常量”
public string GetUserName(User u) {
// ...
}
// Before
GetUserName(default(User)); // Passes null
// After
GetUserName(default); // Passes null
• “默认文字方法调用”
异步Main函数
此功能允许“一直向上”使用 async/await。它允许使Main()函数(应用程序的入口点)异步。
public static async Task<int> Main(string[] args) {
try {
await Engine.Initialise();
return 0;
}
catch (Exception e) {
Log.Error(e);
return 1;
}
}
• “Async Main”
C# 7.2
在参数中,只读结构,只读引用返回
继 ref locals 和 return 之后,此功能添加了一些更多功能来传递对结构的引用。这些功能主要是为性能敏感的场景提供的。只读结构是其字段永远不能修改的结构(即它是不可变的):
readonly struct BigStruct {
public readonly int Alpha;
public readonly float Bravo;
public readonly int Charlie;
public readonly float Delta;
}
• “只读结构”
除了帮助您保持不变性外,将结构声明为只读还有助于编译器在使用in参数时避免防御性副本。in参数与 ref 参数一样,是通过引用传递的参数。然而,另外,in参数是只读的:
void Test(in Matrix4x4 viewMatrix) {
viewMatrix.M11 = 123f; // Won't compile even though Matrix4x4 is a mutable struct, 'in' parameters are readonly
}
•In 参数
尽管编译器尽一切努力防止直接修改通过引用传入的结构;并不总是可以保证不进行任何修改。因此,为了确保正确性,编译器必须在某些情况下对参数进行防御性复制,除非结构类型本身被标记为readonly。
因为in参数是一种性能特性,所以将它们与非只读结构一起使用几乎总是一个坏主意。有关详细信息,请参阅MSDN 上的避免将可变结构作为 In 参数。
当调用带有in参数的方法时,in调用站点的说明符是可选的。但是,指定它有两个用途:
// First case: Explicitly invoking an overloaded method that takes an [c]in[/c] parameter:
static void PrintFirstElement(Matrix4x4 m) => Console.WriteLine(m.M11);
static void PrintFirstElement(in Matrix4x4 m) => Console.WriteLine(m.M11);
static void Test() {
var m = GetMatrix();
PrintFirstElement(m); // Invokes first method, passes 'm' by value (i.e. copied)
PrintFirstElement(in m); // Invokes second method, passes 'm' by readonly reference
}
• “在方法重载调用时”
// Second case: Forcing the passing of an 'in' parameter to be a reference to live variable
static void PrintFirstElement(in Matrix4x4 m) => Console.WriteLine(m.M11);
static void Test() {
// Matrix4x4.Identity is a static property that returns a new Matrix4x4
PrintFirstElement(Matrix4x4.Identity); // Compiles, because the compiler creates a temporary variable on the stack that is what is referred to
PrintFirstElement(in Matrix4x4.Identity); // Fails, because we're creating a reference to something that only exists as a temporary variable
}
•“在调用中以显式通过引用”
最后,只读引用返回允许返回对不允许修改它所引用的变量的变量的引用。要使用这样的引用(而不是获取返回引用的副本),局部变量也必须声明为ref readonly:
static Matrix4x4 _viewMat;
static ref readonly Matrix4x4 GetMatrix() => ref _viewMat;
static void Test() {
ref readonly var mat = ref GetMatrix();
var matCopy = mat;
mat.M11 = 3f; // This line won't compile, we can not modify a readonly ref
matCopy.M11 = 3f; // This line is fine, 'matCopy' is a local stack copy of the variable pointed to by 'mat'
}
• “只读 Ref Returns and Locals”
Ref struct、Span、Memory
Ref struct是一种新的结构类型(即值类型),包含“内部指针”;即对对象的数据或偏移量的引用(与对对象本身的引用相反)。ref struct的实例只能存在于堆栈中;因此对它们的使用方式有一些限制(参见下面的第二个示例)。ref struct最突出的用法是Span类型。跨度是对包含 0 个或多个相同类型元素的连续内存块的引用。声明和存储此内存的方式无关紧要 - Span
static char[] _charArray = { 'A', 'l', 'p', 'h', 'a' };
static List<char> _charList = new List<char> { 'T', 'a', 'u' };
static void PrintCharSpanData(ReadOnlySpan<char> charSpan) {
Console.Write($"Given span is {charSpan.Length} characters long: ");
Console.WriteLine($"\"{new String(charSpan)}\"");
}
unsafe static void Test() {
var heapArraySpan = _charArray.AsSpan();
var listSpan = CollectionsMarshal.AsSpan(_charList);
Span<char> stackArraySpan = stackalloc char[] { 'O', 'm', 'e', 'g', 'a' };
const string UnmanagedDataString = "Epsilon";
var numBytesToAlloc = sizeof(char) UnmanagedDataString.Length;
var pointerSpan = new Span<char>((void) Marshal.AllocHGlobal(numBytesToAlloc), UnmanagedDataString.Length);
UnmanagedDataString.AsSpan().CopyTo(pointerSpan);
var singleCharOnStack = 'O';
var stackSpan = new Span<char>(&singleCharOnStack, 1);
var stringSpan = "Delta".AsSpan();
// =======
PrintCharSpanData(heapArraySpan); // Given span is 5 characters long: "Alpha"
PrintCharSpanData(listSpan); // Given span is 3 characters long: "Tau"
PrintCharSpanData(stackArraySpan); // Given span is 5 characters long: "Omega"
PrintCharSpanData(pointerSpan); // Given span is 7 characters long: "Epsilon"
PrintCharSpanData(stackSpan); // Given span is 1 characters long: "O"
PrintCharSpanData(stringSpan); // Given span is 5 characters long: "Delta"
}
• “Span
上面的示例演示了创建char跨度的六种不同方法。但无论如何创建Span
ReadOnlySpan
上面的代码仅作为创建和使用Span
因为Span
// Invalid: Ref struct types can not be the element type of an array
// Because arrays are stored on the heap
static readonly Span<int>[] _intSpanArray;
// Invalid: Ref struct types can not be fields or properties of any class or struct except ref structs
// Because class instances are stored on the heap, and struct instances may be boxed (i.e. a copy stored on the heap)
public Span<int> SomeSpan { get; set; }
// Invalid: Ref struct types can not implement interfaces
// Because using them as their interface type would always require boxing
readonly ref struct MyRefStruct : IEquatable<MyRefStruct> { }
// Invalid: Ref struct types can not be cast to object (or boxed in any way)
// Because boxed copies of structs are stored on the heap
var boxedSpan = (object) mySpan;
// Invalid: Ref struct types can not be type arguments
// Because usage of elements can not currently be verified as valid (and some usages will never be valid, i.e. List<T>)
var list = new List<Span<int>>();
// Invalid: Ref struct types can not be closed-over (captured) by a lambda/anonymous function
// Because captured variables must be stored in a heap object so that they're still available when the lambda is executed
var filtered = someEnumerable.Where(x => x[0] == mySpan[0]);
// Invalid: Ref struct types can not be used in an async method (locals or parameters)
// Because locals in async methods may be stored in heap objects to become part of the internal state machine built by the compiler
async Task SomeMethodAsync(Span<int> mySpan) { / ... / }
• “Ref Struct 使用限制”
由于这些限制,提供了另一种称为Memory
static char[] _charArray = { 'A', 'l', 'p', 'h', 'a' };
static List<char> _charList = new List<char> { 'T', 'a', 'u' };
unsafe static void Test() {
// Create a Memory<T> that wraps a new array copy of the data,
// rather than pointing to the actual list data directly like we did with the Span<T> example:
var charListAsMemory = _charList.ToArray().AsMemory();
// Alternatively, create a Memory<T> that encapsulates just part of an existing array
// (this can also be done with Span<T>)
var charArraySubstringAsMemory = new Memory<char>(_charArray, 1, 3);
PrintCharMemoryData(charListAsMemory); // Given memory is 3 characters long: "Tau"
PrintCharMemoryData(charArraySubstringAsMemory); // Given memory is 3 characters long: "lph"
}
static void PrintCharMemoryData(ReadOnlyMemory<char> charMemory) {
Console.Write($"Given memory is {charMemory.Length} characters long: ");
Console.WriteLine($"\"{new String(charMemory.Span)}\""); // Can use the .Span property to create a Span<T> when required
}
• “Memory
Span
私有保护访问修饰符
private protected访问修饰符 将成员的可见性限制为仅在同一程序集中的派生类,而不是预先存在的受保护的内部访问修饰符将可见性限制为仅派生类或同一程序集中的类。
C# 7.3
枚举、委托和非托管通用约束
枚举约束允许指定类型参数类型必须是枚举:
// 'Enum' constraint lets us ensure that T is an enum type
// 'struct' constraint is optional but lets us make 'valueToHighlight' nullable
static void PrintAllEnumNames<T>(T? valueToHighlight) where T : struct, Enum {
foreach (var value in (T[]) Enum.GetValues(typeof(T))) {
if (value.Equals(valueToHighlight)) Console.WriteLine($"{value} <----");
else Console.WriteLine(value.ToString());
}
}
• “枚举约束”
同样,委托约束允许指定类型参数类型必须是委托:
// Example from MSDN: https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/generics/constraints-on-type-parameters#delegate-constraints
public static TDelegate TypeSafeCombine<TDelegate>(this TDelegate source, TDelegate target)
where TDelegate : System.Delegate
=> Delegate.Combine(source, target) as TDelegate;
• “委托约束”
非托管泛型约束允许指定类型参数类型必须适合直接/基于指针的操作和/或“blittable”。使用此约束允许您将指针和其他“不安全”构造与您的通用类型变量一起使用:
// This method copies a T reference to a T value via pointer
static unsafe void Copy<T>(ref T src, T dest) where T : unmanaged => dest = src;
static unsafe void Test() {
int dest = 0;
int src = 3;
Copy(ref src, &dest);
Console.WriteLine(dest); // Prints '3'
}
• “非托管约束”
Stackalloc 初始化器
这些允许通过内联初始化程序初始化堆栈分配的内存:
var intArray = stackalloc[] { 1, 2, 3 };
•“堆栈分配的 int 数组的初始化”