日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程语言 > C# >内容正文

C#

新版 C# 高效率编程指南

發(fā)布時間:2023/12/4 C# 40 豆豆
生活随笔 收集整理的這篇文章主要介紹了 新版 C# 高效率编程指南 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

前言

C# 從 7 版本開始一直到如今的 9 版本,加入了非常多的特性,其中不乏改善性能、增加程序健壯性和代碼簡潔性、可讀性的改進,這里我整理一些使用新版 C# 的時候個人推薦的寫法,可能不適用于所有的人,但是還是希望對你們有所幫助。

注意:本指南適用于 .NET 5 或以上版本。

使用 ref struct 做到 0 GC

C# 7 開始引入了一種叫做?ref struct?的結(jié)構(gòu),這種結(jié)構(gòu)本質(zhì)是?struct?,結(jié)構(gòu)存儲在棧內(nèi)存。但是與?struct?不同的是,該結(jié)構(gòu)不允許實現(xiàn)任何接口,并由編譯器保證該結(jié)構(gòu)永遠(yuǎn)不會被裝箱,因此不會給 GC 帶來任何的壓力。相對的,使用中就會有不能逃逸出棧的強制限制。

Span<T>?就是利用?ref struct?的產(chǎn)物,成功的封裝出了安全且高性能的內(nèi)存訪問操作,且可在大多數(shù)情況下代替指針而不損失任何的性能。

Copyref struct MyStruct {public int Value { get; set; } }class RefStructGuide {static void Test(){MyStruct x = new MyStruct();x.Value = 100;Foo(x); // okBar(x); // error, x cannot be boxed}static void Foo(MyStruct x) { }static void Bar(object x) { } }

使用 in 關(guān)鍵字傳遞不可修改的引用

當(dāng)參數(shù)以?ref?傳遞時,雖然傳遞的是引用但是無法確保引用值不被對方修改,這個時候只需要將?ref?改為?in,便能確保安全性:

CopySomeBigReadonlyStruct x = ...; Foo(x);void Foo(in SomeBigReadonlyStruct v) {v = ...; // error }

在使用大的?readonly struct?時收益非常明顯。

使用 stackalloc 在棧上分配連續(xù)內(nèi)存

對于部分性能敏感卻需要使用少量的連續(xù)內(nèi)存的情況,不必使用數(shù)組,而可以通過?stackalloc?直接在棧上分配內(nèi)存,并使用?Span<T>?來安全的訪問,同樣的,這么做可以做到 0 GC 壓力。

stackalloc?允許任何的值類型結(jié)構(gòu),但是要注意,Span<T>?目前不支持?ref struct?作為泛型參數(shù),因此在使用?ref struct?時需要直接使用指針。

Copyref struct MyStruct {public int Value { get; set; } }class AllocGuide {static unsafe void RefStructAlloc(){MyStruct* x = stackalloc MyStruct[10];for (int i = 0; i < 10; i++){*(x + i) = new MyStruct { Value = i };}}static void StructAlloc(){Span<int> x = stackalloc int[10];for (int i = 0; i < x.Length; i++){x[i] = i;}} }

使用 Span?操作連續(xù)內(nèi)存

C# 7 開始引入了?Span<T>,它封裝了一種安全且高性能的內(nèi)存訪問操作方法,可用于在大多數(shù)情況下代替指針操作。

Copystatic void SpanTest() {Span<int> x = stackalloc int[10];for (int i = 0; i < x.Length; i++){x[i] = i;}ReadOnlySpan<char> str = "12345".AsSpan();for (int i = 0; i < str.Length; i++){Console.WriteLine(str[i]);} }

性能敏感時對于頻繁調(diào)用的函數(shù)使用 SkipLocalsInit

C# 為了確保代碼的安全會將所有的局部變量在聲明時就進行初始化,無論是否必要。一般情況下這對性能并沒有太大影響,但是如果你的函數(shù)在操作很多棧上分配的內(nèi)存,并且該函數(shù)還是被頻繁調(diào)用的,那么這一消耗的副作用將會被放大變成不可忽略的損失。

因此你可以使用?SkipLocalsInit?這一特性禁用自動初始化局部變量的行為。

Copy[SkipLocalsInit] unsafe static void Main() {Guid g;Console.WriteLine(*&g); }

上述代碼將輸出不可預(yù)期的結(jié)果,因為?g?并沒有被初始化為 0。另外,訪問未初始化的變量需要在?unsafe?上下文中使用指針進行訪問。

使用函數(shù)指針代替 Marshal 進行互操作

C# 9 帶來了函數(shù)指針功能,該特性支持 managed 和 unmanaged 的函數(shù),在進行 native interop 時,使用函數(shù)指針將能顯著改善性能。

例如,你有如下 C++ 代碼:

Copy#define UNICODE #define WIN32 #include <cstring>extern "C" __declspec(dllexport) char* __cdecl InvokeFun(char* (*foo)(int)) {return foo(5); }

并且你編寫了如下 C# 代碼進行互操作:

Copy[DllImport("./Test.dll")] static extern string InvokeFun(delegate* unmanaged[Cdecl]<int, IntPtr> fun);[UnmanagedCallersOnly(CallConvs = new[] { typeof(CallConvCdecl) })] public static IntPtr Foo(int x) {var str = Enumerable.Repeat("x", x).Aggregate((a, b) => $"{a}");return Marshal.StringToHGlobalAnsi(str); }static void Main(string[] args) {var callback = (delegate* unmanaged[Cdecl]<int, nint>)(delegate*<int, nint>)&Foo;Console.WriteLine(InvokeFun(callback)); }

上述代碼中,首先 C# 將自己的?Foo?方法作為函數(shù)指針傳給了 C++ 的?InvokeFun?函數(shù),然后 C++ 用參數(shù) 5 調(diào)用該函數(shù)并返回其返回值到 C# 的調(diào)用方。

注意到上述代碼還用了?UnmanagedCallersOnly?這一特性,這樣可以告訴編譯器該方法只會從 unmanaged 的代碼被調(diào)用,因此編譯器可以做一些額外的優(yōu)化。

使用函數(shù)指針產(chǎn)生的 IL 指令非常高效:

Copyldftn native int Test.Program::Foo(int32) stloc.0 ldloc.0 call string Test.Program::InvokeFun(method native int *(int32))

除了 unmanaged 的情況外,managed 函數(shù)也是可以使用函數(shù)指針的:

Copystatic void Foo(int v) { } unsafe static void Main(string[] args) {delegate* managed<int, void> fun = &Foo;fun(4); }

產(chǎn)生的代碼相對于原本的 Delegate 來說更加高效:

Copyldftn void Test.Program::Foo(int32) stloc.0 ldc.i4.4 ldloc.0 calli void(int32)

使用模式匹配

有了if-else、as和強制類型轉(zhuǎn)換,為什么要使用模式匹配呢?有三方面原因:性能、魯棒性和可讀性。

為什么說性能也是一個原因呢?因為 C# 編譯器會根據(jù)你的模式編譯出最優(yōu)的匹配路徑。

考慮一下以下代碼(代碼 1):

Copyint Match(int v) {if (v > 3){return 5;}if (v < 3){if (v > 1){return 6;}if (v > -5){return 7;}else{return 8;}}return 9; }

如果改用模式匹配,配合?switch?表達式寫法則變成(代碼 2):

Copyint Match(int v) {return v switch{> 3 => 5,< 3 and > 1 => 6,< 3 and > -5 => 7,< 3 => 8,_ => 9}; }

以上代碼會被編譯器編譯為:

Copyint Match(int v) {if (v > 1){if (v <= 3){if (v < 3){return 6;}return 9;}return 5;}if (v > -5){return 7;}return 8; }

我們計算一下平均比較次數(shù):

代碼56789總數(shù)平均
代碼 113442142.8
代碼 223223122.4

可以看到使用模式匹配時,編譯器選擇了更優(yōu)的比較方案,你在編寫的時候無需考慮如何組織判斷語句,心智負(fù)擔(dān)降低,并且代碼 2 可讀性和簡潔程度顯然比代碼 1 更好,有哪些條件分支一目了然。

甚至遇到類似以下的情況時:

Copyint Match(int v) {return v switch{1 => 5,2 => 6,3 => 7,4 => 8,_ => 9}; }

編譯器會直接將代碼從條件判斷語句編譯成?switch?語句:

Copyint Match(int v) {switch (v){case 1:return 5;case 2:return 6;case 3:return 7;case 4:return 8;default:return 9;} }

如此一來所有的判斷都不需要比較(因為?switch?可根據(jù) HashCode 直接跳轉(zhuǎn))。

編譯器非常智能地為你選擇了最佳的方案。

那魯棒性從何談起呢?假設(shè)你漏掉了一個分支:

Copyint v = 5; var x = v switch {> 3 => 1,< 3 => 2 };

此時編譯的話,編譯器就會警告你漏掉了?v?可能為 3 的情況,幫助減少程序出錯的可能性。

最后一點,可讀性。

假設(shè)你現(xiàn)在有這樣的東西:

Copyabstract class Entry { }class UserEntry : Entry {public int UserId { get; set; } }class DataEntry : Entry {public int DataId { get; set; } }class EventEntry : Entry {public int EventId { get; set; }// 如果 CanRead 為 false 則查詢的時候直接返回空字符串public bool CanRead { get; set; } }

現(xiàn)在有接收類型為?Entry?的參數(shù)的一個函數(shù),該函數(shù)根據(jù)不同類型的?Entry?去數(shù)據(jù)庫查詢對應(yīng)的?Content,那么只需要寫:

Copystring QueryMessage(Entry entry) {return entry switch{UserEntry u => dbContext1.User.FirstOrDefault(i => i.Id == u.UserId).Content,DataEntry d => dbContext1.Data.FirstOrDefault(i => i.Id == d.DataId).Content,EventEntry { EventId: var eventId, CanRead: true } => dbContext1.Event.FirstOrDefault(i => i.Id == eventId).Content,_ => throw new InvalidArgumentException("無效的參數(shù)")}; }

更進一步,假如?Entry.Id?分布在了數(shù)據(jù)庫 1 和 2 中,如果在數(shù)據(jù)庫 1 當(dāng)中找不到則需要去數(shù)據(jù)庫 2 進行查詢,如果 2 也找不到才返回空字符串,由于 C# 的模式匹配支持遞歸模式,因此只需要這樣寫:

Copystring QueryMessage(Entry entry) {return entry switch{UserEntry u => dbContext1.User.FirstOrDefault(i => i.Id == u.UserId) switch{null => dbContext2.User.FirstOrDefault(i => i.Id == u.UserId)?.Content ?? "",var found => found.Content},DataEntry d => dbContext1.Data.FirstOrDefault(i => i.Id == d.DataId) switch{null => dbContext2.Data.FirstOrDefault(i => i.Id == u.DataId)?.Content ?? "",var found => found.Content},EventEntry { EventId: var eventId, CanRead: true } => dbContext1.Event.FirstOrDefault(i => i.Id == eventId) switch{null => dbContext2.Event.FirstOrDefault(i => i.Id == eventId)?.Content ?? "",var found => found.Content},EventEntry { CanRead: false } => "",_ => throw new InvalidArgumentException("無效的參數(shù)")}; }

就全部搞定了,代碼非常簡潔,而且數(shù)據(jù)的流向一眼就能看清楚,就算是沒有接觸過這部分代碼的人看一下模式匹配的過程,也能一眼就立刻掌握各分支的情況,而不需要在一堆的?if-else?當(dāng)中梳理這段代碼到底干了什么。

使用記錄類型和不可變數(shù)據(jù)

record?作為 C# 9 的新工具,配合?init?僅可初始化屬性,為我們帶來了高效的數(shù)據(jù)交互能力和不可變性。

消除可變性意味著無副作用,一個無副作用的函數(shù)無需擔(dān)心數(shù)據(jù)同步互斥問題,因此在無鎖的并行編程中非常有用。

Copyrecord Point(int X, int Y);

簡單的一句話等價于我們寫了如下代碼,幫我們解決了?ToString()?格式化輸出、基于值的?GetHashCode()?和相等判斷等等各種問題:

Copyinternal class Point : IEquatable<Point> {private readonly int x;private readonly int y;protected virtual Type EqualityContract => typeof(Point);public int X{get => x;set => x = value;}public int Y{get => y;set => y = value;}public Point(int X, int Y){x = X;y = Y;}public override string ToString(){StringBuilder stringBuilder = new StringBuilder();stringBuilder.Append("Point");stringBuilder.Append(" { ");if (PrintMembers(stringBuilder)){stringBuilder.Append(" ");}stringBuilder.Append("}");return stringBuilder.ToString();}protected virtual bool PrintMembers(StringBuilder builder){builder.Append("X");builder.Append(" = ");builder.Append(X.ToString());builder.Append(", ");builder.Append("Y");builder.Append(" = ");builder.Append(Y.ToString());return true;}public static bool operator !=(Point r1, Point r2){return !(r1 == r2);}public static bool operator ==(Point r1, Point r2){if ((object)r1 != r2){if ((object)r1 != null){return r1.Equals(r2);}return false;}return true;}public override int GetHashCode(){return (EqualityComparer<Type>.Default.GetHashCode(EqualityContract) * -1521134295 + EqualityComparer<int>.Default.GetHashCode(x)) * -1521134295 + EqualityComparer<int>.Default.GetHashCode(y);}public override bool Equals(object obj){return Equals(obj as Point);}public virtual bool Equals(Point other){if ((object)other != null && EqualityContract == other.EqualityContract && EqualityComparer<int>.Default.Equals(x, other.x)){return EqualityComparer<int>.Default.Equals(y, other.y);}return false;}public virtual Point Clone(){return new Point(this);}protected Point(Point original){x = original.x;y = original.y;}public void Deconstruct(out int X, out int Y){X = this.X;Y = this.Y;} }

注意到?x?與?y?都是?readonly?的,因此一旦實例創(chuàng)建了就不可變,如果想要變更可以通過?with?創(chuàng)建一份副本,于是這種方式徹底消除了任何的副作用。

Copyvar p1 = new Point(1, 2); var p2 = p1 with { Y = 3 }; // (1, 3)

當(dāng)然,你也可以自己使用?init?屬性表示這個屬性只能在初始化時被賦值:

Copyclass Point {public int X { get; init; }public int Y { get; init; } }

這樣一來,一旦?Point?被創(chuàng)建,則?X?和?Y?的值就不會被修改了,可以放心地在并行編程模型中使用,而不需要加鎖。

Copyvar p1 = new Point { X = 1, Y = 2 }; p1.Y = 3; // error var p2 = p1 with { Y = 3 }; //ok

使用 readonly 類型

上面說到了不可變性的重要性,當(dāng)然,struct?也可以是只讀的:

Copyreadonly struct Foo {public int X { get; set; } // error }

上面的代碼會報錯,因為違反了?X?只讀的約束。

如果改成:

Copyreadonly struct Foo {public int X { get; } }

Copyreadonly struct Foo {public int X { get; init; } }

則不會存在問題。

Span<T>?本身是一個?readonly ref struct,通過這樣做保證了?Span<T>?里的東西不會被意外的修改,確保不變性和安全。

使用局部函數(shù)而不是 lambda 創(chuàng)建臨時委托

在使用?Expression<Func<>>?作為參數(shù)的 API 時,使用 lambda 表達式是非常正確的,因為編譯器會把我們寫的 lambda 表達式編譯成 Expression Tree,而非直觀上的函數(shù)委托。

而在單純只是?Func<>、Action<>?時,使用 lambda 表達式恐怕不是一個好的決定,因為這樣做必定會引入一個新的閉包,造成額外的開銷和 GC 壓力。從 C# 8 開始,我們可以使用局部函數(shù)很好的替換掉 lambda:

Copyint SomeMethod(Func<int, int> fun) {if (fun(3) > 3) return 3;else return fun(5); }void Caller() {int Foo(int v) => v + 1;var result = SomeMethod(Foo);Console.WriteLine(result); }

以上代碼便不會導(dǎo)致一個多余的閉包開銷。

使用 ValueTask 代替 Task

我們在遇到?Task<T>?時,大多數(shù)情況下只是需要簡單的對其進行?await?而已,而并不需要將其保存下來以后再?await,那么?Task<T>?提供的很多的功能則并沒有被使用,反而在高并發(fā)下,由于反復(fù)分配?Task?導(dǎo)致 GC 壓力增加。

這種情況下,我們可以使用?ValueTask<T>?代替?Task<T>:

CopyValueTask<int> Foo() {return ValueTask.FromResult(1); }async ValueTask Caller() {await Foo(); }

由于?ValueTask<T>?是值類型結(jié)構(gòu),因此該對象本身不會在堆上分配內(nèi)存,于是可以減輕 GC 壓力。

實現(xiàn)解構(gòu)函數(shù)代替創(chuàng)建元組

如果我們想要把一個類型中的數(shù)據(jù)提取出來,我們可以選擇返回一個元組,其中包含我們需要的數(shù)據(jù):

Copyclass Foo {private int x;private int y;public Foo(int x, int y){this.x = x;this.y = y;}public (int, int) Deconstruct(){return (x, y);} }class Program {static void Bar(Foo v){var (x, y) = v.Deconstruct();Console.WriteLine($"X = {x}, Y = {y}");} }

上述代碼會導(dǎo)致一個?ValueTuple<int, int>?的開銷,如果我們將代碼改成實現(xiàn)解構(gòu)方法:

Copyclass Foo {private int x;private int y;public Foo(int x, int y){this.x = x;this.y = y;}public void Deconstruct(out int x, out int y){x = this.x;y = this.y;} }class Program {static void Bar(Foo v){var (x, y) = v;Console.WriteLine($"X = {x}, Y = {y}");} }

則不僅省掉了?Deconstruct()?的調(diào)用,同時還沒有任何的額外開銷。你可以看到實現(xiàn) Deconstruct 函數(shù)并不需要讓你的類型實現(xiàn)任何的接口,從根本上杜絕了裝箱的可能性,這是一種 0 開銷抽象。另外,解構(gòu)函數(shù)還能用于做模式匹配,你可以像使用元組一樣地使用解構(gòu)函數(shù)(下面代碼的意思是,當(dāng)?x?為 3 時取?y,否則取?x + y):

Copyvoid Bar(Foo v) {var result = v switch{Foo (3, var y) => y,Foo (var x, var y) => x + y,_ => 0};Console.WriteLine(result); }

Null 安全

在項目屬性文件 csproj 中啟用 null 安全后即可對整個項目的代碼啟用 null 安全靜態(tài)分析:

Copy<PropertyGroup><Nullable>enable</Nullable> </PropertyGroup>

這樣便可以在編譯的時候檢查一切潛在的導(dǎo)致 NRE 的問題。例如如下代碼:

Copyvar list = new List<Entry>(); var value = list.FirstOrDefault(i => i.Id == 3).Value; Console.WriteLine(value);

list.FirstOrDefault()?可能返回?null,因此啟用 null 安全之后編譯器將會給出警告,這有助于避免不必要的 NRE 異常發(fā)生。

另外,啟用 null 安全之后,對于可空引用類型,也可以通過在類型后加一個???來表示可為?null:

Copystring? x = null;

總結(jié)

在合適的時候使用 C# 的新特性,不但可以提升開發(fā)效率,同時還能兼顧代碼質(zhì)量和運行效率的提升。

但是切忌濫用。新特性的引入對于我們寫高質(zhì)量的代碼無疑有很大的幫助,但是如果不分時宜地使用,可能會帶來反效果。

希望本文能對各位開發(fā)者使用新版 C# 時帶來一定的幫助,感謝閱讀。

總結(jié)

以上是生活随笔為你收集整理的新版 C# 高效率编程指南的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。