首页 火币app官网下载文章正文

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

火币app官网下载 2022年10月21日 11:02 72 Connor

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

/

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

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

C# 7.0 元组

元组是包含一组两个或多个相关对象的结构。它们对于处理或返回通常不会一起关联到类或结构中的多个相关值很有用。

// ... Later on we can use the tuple like any old struct/class:varage =userDetails.Age;varname =userDetails.Name;vartestScore =userDetails.TestScore;}

• “元组声明” 如果我们想将元组传递给另一个函数或将其存储在容器中怎么办?我们可以使用类似的语法声明一个元组类型:

staticvoidAddUserToList((intAge,stringName,floatTestScore)user){_userList.Add(user);}

staticvoidTest{AddUserToList((30,"Ben",50f));}

• “元组类型声明” 元组的一个很好的用途是更适合替代方法上的“输出”参数:

• “元组返回类型”

// Using the GetLimits function from previous examplevar(min,max)=GetLimits(values);

Console.WriteLine($"Min value is {min}");Console.WriteLine($"Min value is {max}");}

展开全文

// Alternative syntax for pre-existing variables:

staticint_min;staticint_max;

staticvoidTest{varvalues =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 开始):

varimplicitlyNamedTuple =(age,name,testScore);varexplicitlyNamedTuple =(Age:age,Name:name,TestScore:testScore);

//var userAge = implicitlyNamedTuple.age; // Looks ugly! 'age' property is lower-casevaruserAge =explicitlyNamedTuple.Age;// Much better :)}

• “元组属性命名”

请注意,当自动创建名称时,编译器只需复制传入的参数、字段、本地或属性的名称来创建元组;因此我们的implicitlyNamedTuple的属性是小写的。正是出于这个原因,我总是喜欢使用显式命名(因为当然,C# 中的驼峰命名法对于公共成员来说是非常规的)。

任何元组的基础类型是ValueTuple<T1, T2, ..., Tn>,其中类型参数的数量等于元组中的项目数。注意不要使用Tuple<>代替ValueTuple<>;这是一种较旧的、大部分已弃用的类型,效率较低,与ValueTuple<>相比几乎没有任何优势。

元组成员的命名实际上是一个编译器技巧。ValueTuple<>中属性的“真实”名称始终是Item1、Item2、Item3等。在声明元组类型时,编译器会添加 一个专门的属性 [1] 在幕后让它工作,这也是智能感知的。

尽管元组是值类型,但有趣的是它们是可变的。这有一些性能影响,以及使用可变值类型时通常的“陷阱”。

自定义解构

您可以通过声明公共Deconstruct方法 使任何类型像元组一样可解构。该方法必须返回 void,并且所有参数都必须是参数- 这些将成为将被分配的解构变量。这是一个例子:

publicvoidDeconstruct(outstringname,outintage){name =Name;age =Age;}}

staticvoidTest{varuser =newUser{Name="Ben",Age=30};

var(name,age)=user;}

• “用户类解构器”

简单模式匹配

这是一系列旨在使编写某些程序更容易的各种新功能。

is 表达式

对于检查对象是否是给定类型的实例并同时为所述对象创建该类型的本地别名最有用:

if(user isManagermanager){Console.WriteLine($"User is a manager and has {manager.Reports.Count} direct reports!");}

• “类型模式的“是”表达式”

Null 值永远不会匹配is 表达式,因此is 表达式可用于过滤掉 null 值:

if(userList.FirstOrDefaultisUseruser){Console.WriteLine($"User database contains at least one user!");}

• “用于空检查的“是”表达式” 除了类型模式,还支持常量模式。这些都可以在 switch 表达式中使用。当与允许过滤匹配的when 表达式结合使用时,常量模式最有用。

当匹配多个 switch case 时,只输入遇到的第一个匹配 case(你甚至不能用goto case故意在 case 之间跳转)。一个例外是默认情况,它总是最后评估。

// Type pattern matchingswitch(user){casenull:Console.WriteLine("No user found.");break;caseManagerm whenm.Department==Department.Engineering:Console.WriteLine($"User is a manager in the engineering department and has {m.Reports.Count} direct reports.");break;caseManagerm whenm.Department==Department.Marketing:Console.WriteLine($"User is a manager in the marketing department and manages {m.CustomerAccounts.Count} customer accounts.");break;caseTraineet whent.AgeInYears>=18:Console.WriteLine($"User is a trainee and has completed {t.Courses.Count(c => c.IsCompleted)} of their training courses.");break;caseTraineet:// This case will only be entered if the one above was notConsole.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:)。

局部函数

此功能允许在函数内声明函数。这些内部(即本地)函数只能在外部函数的范围内访问。

foreach(varuser inusers){Console.WriteLine(CreateBioString(user));}}

// ... On User.cs ...

boolDueForPayRaise{get{boolIsEligible{returnAgeInYears>=18&&(DateTime.Now-u.JoinDate).TotalYears>=1d;}

returnIsEligible&&(DateTime.Now-u.LastPayRaise).TotalYears>=1d;}}

• “本地函数”

内联“Out”变量声明

这个简单的特性允许在使用 out 变量时更加简洁:

// AFTERstaticvoidTest{if(Int32.TryParse(someString,outvarparseResult)){// ... Use parseResult here}}

• “内联输出变量”

抛出表达式

这个非常方便的功能也有助于简洁。它允许您在通常期望值的地方抛出异常。例子:

publicUserCurrentUser{get{return_currentUser;}set{_currentUser =value ??thrownewArgumentNullException(nameof(value));}}

• “Throw Expressions Example A”

• “Throw Expressions Example B”

参考本地和返回

这个与性能相关的特性允许使用、存储和返回对变量/数据位置的引用。

从早期的 C# 开始,引用参数允许我们将变量的引用传递给方法。现在我们还可以返回对属性、字段或其他堆分配变量(例如数组值)的引用:

publicrefMatrix4x4GetMatrix(MatrixTypetype,intoffset){switch(type){caseMatrixType.ViewMatrix:returnref_matrices[_viewMatricesStartIndex +offset];caseMatrixType.ProjectionMatrix:returnref_matrices[_projectionMatricesStartIndex +offset];caseMatrixType.WorldMatrix:returnref_matrices[_worldMatricesStartIndex +offset];default:thrownewArgumentOutOfRangeException(nameof(type));}}

• “Ref Returns” 此方法返回对 _matrices 数组中 Matrix4x4 的引用,而 不是 [2] 其value 的副本。对于复制大型值类型实例将不可避免的情况,这可以带来性能优势。 在方法中使用返回的引用需要声明一个ref local:

// We can dereference the reference and copy its value to a local here by using the standard local variable declaration syntaxvarprojMatrix =GetMatrix(MatrixType.ProjectionMatrix,2);projMatrix.M11 =3f;// Changes only the local 'projMatrix', does not affect anything in _matrices}

• “Ref Locals” 这两种单独的语法允许“选择加入”在按引用返回的方法上使用 ref locals;当我们不想要或不需要它时,一直忽略它。

我们还可以通过直接从 ref-returning 方法返回的引用来设置值:

• “通过返回的引用设置值”

弃元

此功能允许声明您忽略所需参数的意图。使用下划线 ( _ ) 表示您不想使用 out 参数、表达式的结果或 lambda 参数:

// 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");}

数字分隔符

此功能允许您使用下划线分隔整数文字的数字:

• “数字分隔符”

二进制字面量

此功能允许以二进制格式声明整数常量:

• “二进制文字”

C# 7.1 ValueTask/ValueTask 和 IValueTaskSource

在 C# 中 封装 future的主要方法是使用 [3] Task和Task 类。在大多数情况下,此范例运行良好,但在严格控制的性能场景中,Task / Task 对象的持续创建会对垃圾收集器施加不必要的压力。

ValueTask和ValueTask 是允许使用类似任务的语义(包括 async/await)但不总是创建引用类型的实例来跟踪异步操作的两种类型。

对于异步函数,您希望该函数成为热路径的一部分,频繁调用,并且该函数通常能够同步完成,ValueTask很有意义:

• “ValueTask 示例” 可以像常规Task或Task 一样等待 返回的ValueTask或ValueTask 对象-但只能等待一次。

注意:C# 为声明公共GetAwaiter方法(或具有通过扩展方法定义的方法)的任何类型提供await支持,该方法返回具有一 小组先决条件公共成员 [4] 的对象。ValueTask和ValueTask 实现了这个接口。

注意:实际上,框架 缓存了一些常见的 Task 结果 [5] 。 当方法可以同步完成时,这种方法可以消除不必要的垃圾。

ValueTask和ValueTask 都有可以采用 IValueTaskSource/IValueTaskSource [6] 类型的对象的构造函数。这些类型允许您重用/池化对象来处理异步状态机和继续调用。这些类型只需要实现IValueTaskSource / IValueTaskSource 。

实现方法有以下三种:

异步状态机将调用GetStatus以获取异步操作的当前状态。

异步状态机将调用GetResult以获取异步操作完成时的结果。

OnCompleted将由异步状态机调用,以将延续传递给您的实现,在异步操作完成时必须调用该延续;或者如果已经完成则立即调用。 如上所述, 多次等待或从任何 ValueTask 获取结果是错误的 [7] ;这允许我们假设GetResult每次操作只会被调用一次(超过这个次数是用户的错误,可以被认为是不受支持的)。同样,它还允许我们假设一旦调用GetResult,IValueTaskSource实例就可以重新用于下一个异步操作。

传递给所有方法的短令牌可用于确保遵守此条件。

默认文字

这个小功能允许在指定类型的默认值时省略类型名称:

// AfterconstintZero=default;

• “默认文字常量”

// BeforeGetUserName(default(User));// Passes null

// AfterGetUserName(default);// Passes null

• “默认文字方法调用”

异步Main函数

此功能允许“一直向上”使用 async/await。它允许使Main函数(应用程序的入口点)异步。

• “Async Main”

C# 7.2 在参数中,只读结构,只读引用返回

继 ref locals 和 return 之后,此功能添加了一些更多功能来传递对结构的引用。这些功能主要是为性能敏感的场景提供的。只读结构是其字段永远不能修改的结构(即它是不可变的):

• “只读结构” 除了帮助您保持不变性外,将结构声明为只读还有助于编译器在使用in参数时避免防御性副本。in参数与 ref 参数一样,是通过引用传递的参数。然而,另外,in参数是只读的:

•In 参数 尽管编译器尽一切努力防止直接修改通过引用传入的结构;并不总是可以保证不进行任何修改。因此,为了确保正确性,编译器必须在某些情况下对参数进行防御性复制,除非结构类型本身被标记为readonly。

因为in参数是一种性能特性,所以将它们与非只读结构一起使用几乎总是一个坏主意。有关详细信息,请参阅MSDN 上 的避免将可变结构作为 In 参数 [8] 。

当调用带有in参数的方法时,in调用站点的说明符是可选的。但是,指定它有两个用途:

staticvoidPrintFirstElement(Matrix4x4m)=>Console.WriteLine(m.M11);staticvoidPrintFirstElement(inMatrix4x4m)=>Console.WriteLine(m.M11);

staticvoidTest{varm =GetMatrix;

PrintFirstElement(m);// Invokes first method, passes 'm' by value (i.e. copied)PrintFirstElement(inm);// Invokes second method, passes 'm' by readonly reference }

• “在方法重载调用时”

staticvoidPrintFirstElement(inMatrix4x4m)=>Console.WriteLine(m.M11);

staticvoidTest{// 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 toPrintFirstElement(inMatrix4x4.Identity);// Fails, because we're creating a reference to something that only exists as a temporary variable}

•“在调用中以显式通过引用” 最后,只读引用返回允许返回对不允许修改它所引用的变量的变量的引用。要使用这样的引用(而不是获取返回引用的副本),局部变量也必须声明为ref readonly:

staticrefreadonlyMatrix4x4GetMatrix=>ref_viewMat;

staticvoidTest{refreadonlyvarmat =refGetMatrix;varmatCopy =mat;

mat.M11 =3f;// This line won't compile, we can not modify a readonly refmatCopy.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 [9] 类型。跨度是对包含 0 个或多个相同类型元素的连续内存块的引用。声明和存储此内存的方式无关紧要 - Span 始终可以指向数据。

staticvoidPrintCharSpanData(ReadOnlySpan<char>charSpan){Console.Write($"Given span is {charSpan.Length} characters long: ");Console.WriteLine($"\"{new String(charSpan)}\"");}

unsafestaticvoidTest{varheapArraySpan =_charArray.AsSpan;

varlistSpan =CollectionsMarshal.AsSpan(_charList);

Span<char>stackArraySpan =stackallocchar[]{'O','m','e','g','a'};

conststringUnmanagedDataString="Epsilon";varnumBytesToAlloc =sizeof(char)UnmanagedDataString.Length;varpointerSpan =newSpan<char>((void)Marshal.AllocHGlobal(numBytesToAlloc),UnmanagedDataString.Length);UnmanagedDataString.AsSpan.CopyTo(pointerSpan);

varsingleCharOnStack ='O';varstackSpan =newSpan<char>(&singleCharOnStack,1);

varstringSpan ="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 可隐式转换为ReadOnlySpan (假设类型参数T相同);这允许我们将Span 传递给PrintCharSpanData,即使该方法采用ReadOnlySpan 。

上面的代码仅作为创建和使用Span / ReadOnlySpan 的示例。有些操作是“不安全的”或在使用时需要小心。特别注意,手动分配的内存(使用AllocHGlobal)应该再次释放,并且在访问支持列表的数组时(通过CollectionsMarshal ),重要的是在相关Span 的使用完成之前不修改列表. 因为Span 、ReadOnlySpan 和任何其他ref struct不得转义堆栈(或内部引用可能无效),因此对其类型的变量有使用限制:

// 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)publicSpan<int>SomeSpan{get;set;}

// Invalid: Ref struct types can not implement interfaces// Because using them as their interface type would always require boxingreadonlyrefstructMyRefStruct: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 heapvarboxedSpan =(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>)varlist =newList<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 executedvarfiltered =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 compilerasync TaskSomeMethodAsync(Span<int>mySpan){/ ... /}

• “Ref Struct 使用限制” 由于这些限制,提供了另一种称为Memory 的 非引用结构类型。Memory 必须仅封装托管堆分配的、GC 跟踪的内存。

unsafestaticvoidTest{// 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:varcharListAsMemory =_charList.ToArray.AsMemory;

// Alternatively, create a Memory<T> that encapsulates just part of an existing array// (this can also be done with Span<T>)varcharArraySubstringAsMemory =newMemory<char>(_charArray,1,3);

PrintCharMemoryData(charListAsMemory);// Given memory is 3 characters long: "Tau"PrintCharMemoryData(charArraySubstringAsMemory);// Given memory is 3 characters long: "lph"}

staticvoidPrintCharMemoryData(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 和Memory 的 使用指南非常广泛,但如果编写使用它们的 API,则应阅读这些指南。Microsoft 在此处有一个专门讨论此主题的页面: Memory 和 Span 使用指南 [10] 。

私有保护访问修饰符

private protected访问修饰符 将成员的可见性限制为仅在同一程序集中的派生类,而不是预先存在的受保护的内部访问修饰符将可见性限制为仅派生类或同一程序集中的类。

C# 7.3 枚举、委托和非托管通用约束

枚举约束允许指定类型参数类型必须是枚举:

• “枚举约束” 同样,委托约束允许指定类型参数类型必须是委托:

publicstaticTDelegateTypeSafeCombine<TDelegate>(thisTDelegatesource,TDelegatetarget)whereTDelegate:System.Delegate=>Delegate.Combine(source,target)asTDelegate;

• “委托约束” 非托管泛型约束允许指定类型参数类型必须适合直接/基于指针的操作和/或“blittable”。使用此约束允许您将指针和其他“不安全”构造与您的通用类型变量一起使用:

staticunsafevoidTest{intdest =0;intsrc =3;Copy(refsrc,&dest);

Console.WriteLine(dest);// Prints '3'}

• “非托管约束”

Stackalloc 初始化器

这些允许通过内联初始化程序初始化堆栈分配的内存:

•“堆栈分配的 int 数组的初始化”

References

[1] 一个专门的属性:

[2] 不是:

[3] future的主要方法是使用:

[4] 小组先决条件公共成员:

[5] 缓存了一些常见的 Task 结果:

[6] IValueTaskSource/IValueTaskSource:

[7] 多次等待或从任何 ValueTask 获取结果是错误的:

[8] 的避免将可变结构作为 In 参数:

[9] Span:

[10] Memory 和 Span 使用指南:

发表评论

火币交易所(huobi) | 火币全球站官网入口 备案号:川ICP备66666666号