C# 很少人知道的科技

本文來告訴大家在C#很少有人會發現的科技。即使是工作了好多年的老司機也不一定會知道這些科技,如果覺得我是在騙你,那麼請看看本文的內容

原本最初 C# 的設計是簡單和高效開發的,在經過了這麼多年眾多公司和開發者的努力下,整個 C# 裡面包含了大量有趣的功能。其中一部分功能是針對於某些特殊需求設計的,例如高效能或高併發或無記憶體回收等。在經過了 10 多年的迭代,很少人能完全瞭解整個 C# 語言和框架級做了哪些有趣的功能

我在網上找了很多大神的部落格,然後和很多大神聊天,知道了一些科技,於是就在本文和大家分享一下。如果大家有了解本部落格裡面沒有收藏的科技,還請告訴我

現在整個 C# 從編譯器到執行時都是開源的,所有權在 dotnet 基金會上,全部開源的專案都基於最友好的 MIT 協議和 Apache 2 開源協議,文件協議遵循CC-BY協議。這將允許任何人任何組織和企業任意處置,包括使用,複製,修改,合併,發表,分發,再授權,或者銷售。唯一的限制是,軟體中必須包含上述版 權和許可提示,後者協議將會除了為使用者提供版權許可之外,還有專利許可,並且授權是免費,無排他性的(任何個人和企業都能獲得授權)並且永久不可撤銷,使用者使用。NET 和 C# 完全不用擔心收費問題和版權問題,以及後續無法維護問題。而 dotnet 基金會是一個開放的平臺,我也是 dotnet 基金會的成員之一。微軟在 2020 的時候依然是 dotnet 基金會最大的支援組織

現在最火的 dotnet 倉庫是 dotnet csharplang 倉庫,當前的 C# 語言特性由整個社群決定,這是一個官方開放用來討論 C# 語言未來的倉庫,天天都有大佬們在討論語言的特性,歡迎大家加入

接下來讓我告訴大家一些很少有人會發現的科技

無限級判斷空

在 C# 6。0 可以使用

??

判斷空,那麼就可以使用下面程式碼

var v1 = “123”; string v2 = null; string v3 = null; var v = v1 ?? v2 ?? v3;

實際上可以無限的使用

??

判斷前面一個函式為空,那麼問題來了,下面的程式碼輸出的是多少?

var n = 2 + foo?。N ?? 1;

上面程式碼的 foo 就是空的,那麼 n 是多少?是 1 還是 2 還是 3 還是空?

想要了解這道題的推導過程請看C# 高階面試題 裡面寫了很多老司機都不一定能解出

使用 using 關鍵詞省略長的定義

例如有下面這個程式碼,在這個程式碼裡面使用了很多的 List 巢狀,如下面程式碼所示裡面有很多定義的程式碼

var foo = new System。Collections。Generic。Dictionary>, string>();

可以看到上面程式碼中,有大量的程式碼都是用來作為型別的定義,假設這個值作為某個方法的引數,那才是可怕

一個簡單的方法是使用 using 關鍵詞,如在檔案的開頭新增如下程式碼

using HvcnrclHnlfk = System。Collections。Generic。Dictionary>,string>;

在添加了上面程式碼之後,在這個檔案裡的所有用到如上面很長的定義的程式碼都可以使用

using

後面的值可以代替,如本文上面使用了

HvcnrclHnlfk

這個詞,來看看替換之後的程式碼長度

var foo = new HvcnrclHnlfk();

辣麼大

實際上寫到這裡我有些不好意思,好像剛剛說的都是大家都知道的,那麼我就要開始寫大家很少知道的科技

等等,什麼是

辣麼大

大哇?其實這是 lambda 表示式的翻譯

請看看下面這段有趣的程式碼

Func foo = (x, y) => (s, e) => { var button = (Button) s; button。Left = x; button。Top = y; }; Button1。Click += foo(0, -1);

上面的程式碼透過一個 lambda 表示式返回一個另一個 lambda 表示式,或者說用一個委託返回另一個委託。這是一個特別有趣的寫法,透過函式返回函式的思想可以用來寫出一些有趣的邏輯,特別是在多層巢狀的時候

當然使用委託可是會出現另一個問題的,請問下面的程式碼實際呼叫的是哪個委託,下面程式碼的 a 和 b 和 c 都是

Action

委託,同時都不是空的

((a + b + c) - (a + c))();

在數學上,其實函式也可以視為變數,很有科技範的 C# 當然也支援如此的功能,將函式包裝為委託的時候,可以讓委託本身支援加減法哦,只是這個加減法的規則有些詭異。不信,請猜猜上面程式碼執行了什麼函式

衝突的型別

在遇到某些型別,特別是放在 NuGet 上的多個不同的庫裡面的型別,這些型別有相同的類名,如 Data 或 Control 等很通用的命名的時候,在程式碼中如果需要同時使用這兩個類,就需要補全整個名稱空間,如下面程式碼

var webControl = new System。Web。UI。WebControls。Control();var formControl = new System。Windows。Forms。Control();

如果經常使用這兩個控制元件,那麼就需要寫很多補全名稱空間的程式碼,程式碼很多。好在微軟的大佬們給出了一個坑方法,使用這個方法可以不寫名稱空間,或者說只需要在檔案開始 using 一次,請看程式碼

using web = System。Web。UI。WebControls;using win = System。Windows。Forms;web::Control webControl = new web::Control();win::Control formControl = new win::Control();

參見:https://stackoverflow。com/a/9099/6116637

extern alias

如果使用了兩個不同的程式集放在兩個不同的 dll 檔案裡面,這兩個程式集都有相同名稱空間和型別,那麼如何使用指定的庫

如下面程式碼所示,在兩個 dll 裡面都定義了

F。Foo

型別

//a。dllnamespace F{ public class Foo { }}//b。dllnamespace F{ public class Foo { }}

這時就可以使用 extern alias 關鍵詞

參見:C#用extern alias解決兩個assembly中相同的型別全名 - fresky - 部落格園

字串

大家看到了 C# 6。0 的

$

,是不是可以和

@

一起?

var str = “kktpqfThiq”; string foo = $@“換行{str}”;

注意兩個的順序,反過來直接告訴你程式碼不能這樣寫

此知識點不再適用,因為在 C# 8。0 的時候,可以按照任意的順序使用

$

@

標記。詳細請看 $ - 字串內插 - C# 參考 特別感謝 592844340 群內熱心人員勘誤

特殊關鍵字

實際上有下面幾個關鍵字是沒有詳細的文件,可能只有微軟的編譯器才知道

__makeref__reftype__refvalue__arglist

不過在 C# 7。2 可以使用其他的關鍵字做到一些功能,詳細請看我的 C# 7。0 部落格

使用 Unions (C++ 一樣的)

如果看到 C++ 可以使用內聯,不要說 C# 沒有這個功能,實際上也可以使用 FieldOffset 特性實現和 C++ 一樣的內聯的功能 ,請看下面程式碼

[StructLayout(LayoutKind。Explicit)]public class A{ [FieldOffset(0)] public byte One; [FieldOffset(1)] public byte Two; [FieldOffset(2)] public byte Three; [FieldOffset(3)] public byte Four; [FieldOffset(0)] public int Int32;}

如下面程式碼就定義了

int

變數,修改這個變數就是修改其他的三個變數

static void Main(string[] args) { A a = new A { Int32 = int。MaxValue }; Console。WriteLine(a。Int32); Console。WriteLine(“{0:X} {1:X} {2:X} {3:X}”, a。One, a。Two, a。Three, a。Four); a。Four = 0; a。Three = 0; Console。WriteLine(a。Int32); }

執行程式碼可以看到輸出如下

2147483647FF FF FF 7F65535

可以看到修改其中某個值都會相互影響,這幾個值共用了相同的一個記憶體空間

介面預設方法

實際上可以給介面使用預設方法,使用的方式如下

public static void Foo(this IF1 foo){ //實際上大家也看到是如何定義}

當然了,在 C# 8。0 還有更直接的方法,詳細請看 在 C# 中使用預設介面方法安全地更新介面

stackalloc

很多人都不知道這個科技,這是不安全程式碼,從棧申請空間

int* block = stackalloc int[100];

使用的時候需要小心你的棧也許會炸掉

參見:stackalloc

指定編譯

這個是一個有趣的特性實現的功能,是一個編譯器技術,寫給編譯器看的特性。使用 Conditional 特性可以讓程式碼在指定條件不使用,如下面的程式碼,規定了只有在 DEBUG 宏定義的時候才讓 F2 方法生效。因此在 Release 下就不會使用 F2 方法了

public sealed clas Foo { public Foo F1() { Console。WriteLine(“進入F1”); return this; } [Conditional(“DEBUG”)] public void F2() { Console。WriteLine(“F2”); } }

簡單讓程式碼跑一下

static void Main(string[] args) { var foo = new Foo(); foo。F1(); foo。F2(); }

結果是什麼,大家也知道,在 Debug 和 Release 輸出是不相同。但是這麼簡單的怎麼會在這裡說呢,請大家看看這個程式碼輸出什麼

static void Main(string[] args) { var foo = new Foo(); foo。F1()。F2(); }

實際上在 Release 下什麼都不會輸出,此時的 F1 不會被執行

true 判斷

下面寫個見鬼的程式碼

var foo = new Foo(10); if (foo) { Console。WriteLine(“我的類沒有繼承 bool ,居然可以這樣寫”); }

沒錯 Foo 沒有繼承 bool 居然可以這樣寫

實際上就是重寫 true 方法,請看程式碼

public class Foo { public Foo(int value) { _count = value; } private readonly int _count; public static bool operator true(Foo mt) { return mt。_count > 0; } public static bool operator false(Foo mt) { return mt。_count < 0; } }

是不是覺得很多有人這樣寫,下面讓大家看一個很少人會知道的科技,感謝walterlv 提供

重寫運算返回

很少人知道實際上重寫

==

可以返回任意的型別,而不是隻有 bool 型別,請看下面程式碼

C# 很少人知道的科技

是可以編譯透過的,因為我重寫運算

class Foo { public int Count { get; set; } public static string operator ==(Foo f1, Foo f2) { if (f1?。Count == f2?。Count) { return “lindexi”; } return “”; } public static string operator !=(Foo f1, Foo f2) { return “”; } }

可以重寫的運算很多,返回值可以自己隨意定義

await 任何型別

等待任意的型別,包括已定義的基礎型別,如下面程式碼

await “林德熙逗比”;await “不告訴你”;

這個程式碼是可以編譯透過的,但是隻有在我的裝置。在看了這個部落格之後,可能你也可以在你的裝置編譯

其實 await 是可以寫很多次的,如下面程式碼

await await await await await await await await await await await await await await await await await await await await await await await “林德熙逗比”;

變數名使用中文

實際上在C#支援所有 Unicode 字元,這是編譯器支援的,所以變數名使用中文也是可以的,而且可以使用特殊的字元

public string H\u00e5rføner() { return “可以編譯”; }

C# 很少人知道的科技

if this == null

一般看到下面的程式碼都覺得是不可能進入輸出的

if (this == null) Console。WriteLine(“this is null”);

如果在 if 裡面都能使用 this == null 成立,那麼一定是vs炸了。實際上這個程式碼還是可以執行的

在一般的函式,如下面的 Foo 函式,在呼叫就需要使用

f。Foo()

的方法,方法裡 this 就是 f 這個物件,如果

f == null

那麼在呼叫方法就直接不讓執行,如何到方法裡的判斷

f。Foo(); //如果 f 為空,那麼這裡就不執行void Foo(){ // 如果 this 為空,怎麼可以呼叫這個方法 if (this == null) Console。WriteLine(“this is null”);}

實際上是可以做的,請看(C#)if (this == null)?你在逗我,this 怎麼可能為 null!用 IL 編譯和反編譯看穿一切 - walterlv 這篇部落格

如上面部落格,關鍵在修改

callvirt

call

呼叫,直接修改 IL 可以做出很多特殊的寫法

那麼這個可以用在哪裡?可以用在防止大神反編譯,如需要使用下面邏輯

//執行的程式碼//不執行的程式碼

此時簡單的反編譯也許會這麼寫

if(true){ //執行的程式碼}else{ //不執行的程式碼 }

但是直接寫 true 很容易讓反編譯看到不使用程式碼,而且在最佳化程式碼會被去掉,所以可以使用下面程式碼

if(this == null){ //執行的程式碼}else{ //不執行的程式碼 }

實際在微軟程式碼也是這樣寫,點選string的實現原始碼可以看到微軟程式碼

過載的運算子

實際上我可以將 null 強轉某個類,建立一個新的物件,請看程式碼

Fantastic fantastic = (FantasticInfo) null;fantastic。Foo();

這裡的 FantasticInfo 和 Fantastic 沒有任何繼承關係,而且呼叫 Foo 不會出現空引用,也就是 fantastic 是從一個空的物件創建出來的

是不是覺得上面的科技很黑,實際原理沒有任何黑的科技,請看程式碼

public class Fantastic { private Fantastic() { } public static implicit operator Fantastic(FantasticInfo value) => new Fantastic(); public void Foo() { } } public class FantasticInfo { }

透過這個方式可以讓開發者無法直接建立 Fantastic 類,而且在不知道 FantasticInfo 的情況無法建立 Fantastic 也就是讓大家需要了解 FantasticInfo 才可以透過上面的方法建立,具體請看只有你能 new 出來!。NET 隱藏建構函式的 n 種方法(Builder Pattern / 構造器模式) - walterlv

課件連結: https://r302。cc/J4gxOX

當然還有新的 C# 7。0 和 C# 8。0 的新的語法

例如下面的內部方法返回自身

方法返回自身可以接近無限呼叫

有一天我看到了下面的程式碼,你猜小夥伴用什麼程式碼定義了 Foo 這個程式碼?

Foo()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()()();

其實只需要定義一個委託,用內部方法實現委託,因為內部方法是可以返回自身,於是就可以使用5行程式碼寫出 Foo 的定義

delegate Foo Foo(); // 定義委託static void Main(string[] args){ Foo Foo() // 定義內部方法 { return Foo; }}

不過括號還不可以無限使用,因為編譯器有一個表示式的長度限制

無限長度的委託呼叫

試試這個程式碼,也許你可以無限寫下去,只要 Roslyn 不會炸就可以

delegate Fx Fx(Fx fx); Fx fx = fx => fx => fx => fx => fx => fx => fx => fx => fx => fx => fx => fx => fx => fx => fx => fx => fx => fx => fx => fx => fx => fx => fx => fx => fx => fx;

以下部分準確來說是 。NET 提供的功能,請問 C# 和 。NET 是什麼關係?其實我也無法用一兩句話說清,扔掉了 。NET 依然可以用 C# 寫程式,反過來扔掉 C# 也依然能用 。NET 寫程式

表示式樹獲取函式命名

定義一個類,下面透過表示式樹從類獲得函式命名

class Foo { public void KzcSevfio() { } }

static void Main(string[] args) { GetMethodName(foo => foo。KzcSevfio()); } private static void GetMethodName(Expression> action) where T : class { if (action。Body is MethodCallExpression expression) { Console。WriteLine(expression。Method。Name); } }

這樣就可以拿到函式的命名

DebuggerDisplay

如果想要在除錯的時候,滑鼠移動到變數顯示他的資訊,可以重寫類的 ToString

public sealed class Foo { public int Count { get; set; } public override string ToString() { return Count。ToString(); } }

C# 很少人知道的科技

但是如果 ToString 被其他地方用了,如何顯示?

微軟告訴大家,使用 DebuggerDisplay 特性

[DebuggerDisplay(“{DebuggerDisplay}”)] public sealed class Foo { public int Count { get; set; } private string DebuggerDisplay => $“(count {Count})”; }

他可以使用私有的屬性、欄位,使用方法很簡單

參見Using the DebuggerDisplay Attribute

數字格式

string format = “000;-#;(0)”;string pos = 1。ToString(format); // 001string neg = (-1)。ToString(format); // -1string zer = 0。ToString(format); // (0)

參見:自定義數字格式字串

呼叫堆疊

如果需要獲得呼叫方法的堆疊,可以使用這個文章的方法

class Program { static void Main(string[] args) { var foo = new Foo(); foo。F1(); } } public sealed class Foo { public void F1() { F2(); } void F2() { var stackTrace = new StackTrace(); var n = stackTrace。FrameCount; for (int i = 0; i < n; i++) { Console。WriteLine(stackTrace。GetFrame(i)。GetMethod()。Name); } } }

輸出

F2F1

參見:WPF 判斷呼叫方法堆疊

本文會經常更新,請閱讀原文: https://blog。lindexi。com/post/C-%E5%BE%88%E5%B0%91%E4%BA%BA%E7%9F%A5%E9%81%93%E7%9A%84%E7%A7%91%E6%8A%80。html ,以避免陳舊錯誤知識的誤導,同時有更好的閱讀體驗。