C#中的9個“黑魔法”
我們知道C#
是非常先進的語言,因為是它很有遠見的“語法糖”。這些“語法糖”有時過於好用,導致有人覺得它是C#
編譯器寫死的東西,沒有道理可講的——有點像“黑魔法”。
那麼我們可以看看C#
這些高階語言功能,是編譯器寫死的東西(“黑魔法”),還是可以擴充套件(騷操作)的“鴨子型別”。
我先列一個目錄,大家可以對著這個目錄試著下判斷,說說是“黑魔法”(編譯器寫死),還是“鴨子型別”(可以自定義“騷操作”):
- LINQ操作,與IEnumerable<T>型別;
- async/await,與Task/ValueTask型別;
- 表示式樹,與Expression<T>型別;
- 插值字串,與FormattableString型別;
- yield return,與IEnumerable<T>型別;
- foreach迴圈,與IEnumerable<T>型別;
- using關鍵字,與IDisposable介面;
- T?,與Nullable<T>型別;
- 任意型別的Index/Range泛型操作。
1. LINQ
操作,與IEnumerable<T>
型別
不是“黑魔法”,是“鴨子型別”。
LINQ
是C# 3.0
釋出的新功能,可以非常便利地操作資料。現在12
年過去了,雖然有些功能有待增強,但相比其它語言還是方便許多。
如我上一篇部落格提到,LINQ
不一定要基於IEnumerable<T>
LINQ
表示式即可,LINQ
的select
關鍵字,會呼叫.Select
方法,可以用如下的“騷操作”,實現“移花接木”的效果:
void Main() { var query = from i in new F() select 3; Console.WriteLine(string.Join(",",query)); // 0,1,2,3,4 } class F { public IEnumerable<int> Select<R>(Func<int,R> t) { for (var i = 0; i < 5; ++i) { yield return i; } } }
2. async/await
,與Task
/ValueTask
型別
不是“黑魔法”,是“鴨子型別”。
async/await
釋出於C# 5.0
,可以非常便利地做非同步程式設計,其本質是狀態機。
async/await
的本質是會尋找型別下一個名字叫GetAwaiter()
的介面,該介面必須返回一個繼承於INotifyCompletion
或ICriticalNotifyCompletion
的類,該類還需要實現GetResult()
方法和IsComplete
屬性。
- 先呼叫t.GetAwaiter()方法,取得等待器a;
- 呼叫a.IsCompleted取得布林型別b;
- 如果b=true,則立即執行a.GetResult(),取得執行結果;
- 如果b=false,則看情況:
如果a沒實現ICriticalNotifyCompletion,則執行(a as INotifyCompletion).OnCompleted(action)
如果a實現了ICriticalNotifyCompletion,則執行(a as ICriticalNotifyCompletion).OnCompleted(action)
執行隨後暫停,OnCompleted完成後重新回到狀態機;
有興趣的可以訪問Github
具體規範說明:https://github.com/dotnet/csharplang/blob/master/spec/expressions.md
正常Task.Delay()
是基於執行緒池計時器
的,可以用如下“騷操作”,來實現一個單執行緒的TaskEx.Delay()
:
static Action Tick = null; void Main() { Start(); while (true) { if (Tick != null) Tick(); Thread.Sleep(1); } } async void Start() { Console.WriteLine("執行開始"); for (int i = 1; i <= 4; ++i) { Console.WriteLine($"第{i}次,時間:{DateTime.Now.ToString("HH:mm:ss")} - 執行緒號:{Thread.CurrentThread.ManagedThreadId}"); await TaskEx.Delay(1000); } Console.WriteLine("執行完成"); } class TaskEx { public static MyDelay Delay(int ms) => new MyDelay(ms); } class MyDelay : INotifyCompletion { private readonly double _start; private readonly int _ms; public MyDelay(int ms) { _start = Util.ElapsedTime.TotalMilliseconds; _ms = ms; } internal MyDelay GetAwaiter() => this; public void OnCompleted(Action continuation) { Tick += Check; void Check() { if (Util.ElapsedTime.TotalMilliseconds - _start > _ms) { continuation(); Tick -= Check; } } } public void GetResult() {} public bool IsCompleted => false; }
執行效果如下:
執行開始
第1次,時間:17:38:03 - 執行緒號:1
第2次,時間:17:38:04 - 執行緒號:1
第3次,時間:17:38:05 - 執行緒號:1
第4次,時間:17:38:06 - 執行緒號:1
執行完成
注意不需要非得使用TaskCompletionSource<T>
才能建立定定義的async/await
。
3. 表示式樹,與Expression<T>
型別
是“黑魔法”,沒有“操作空間”,只有當型別是Expression<T>
時,才會建立為表示式樹。
表示式樹
是C# 3.0
隨著LINQ
一起釋出,是有遠見的“黑魔法”。
如以下程式碼:
Expression<Func<int>> g3 = () => 3;
會被編譯器翻譯為:
Expression<Func<int>> g3 = Expression.Lambda<Func<int>>( Expression.Constant(3,typeof(int)),Array.Empty<ParameterExpression>());
4. 插值字串,與FormattableString
型別
是“黑魔法”,沒有“操作空間”。
插值字串
釋出於C# 6.0
,在此之前許多語言都提供了類似的功能。
只有當型別是FormattableString
,才會產生不一樣的編譯結果,如以下程式碼:
FormattableString x1 = $"Hello {42}"; string x2 = $"Hello {42}";
編譯器生成結果如下:
FormattableString x1 = FormattableStringFactory.Create("Hello {0}",42); string x2 = string.Format("Hello {0}",42);
注意其本質是呼叫了FormattableStringFactory.Create
來建立一個型別。
5. yield return
,與IEnumerable<T>
型別;
是“黑魔法”,但有補充說明。
yield return
除了用於IEnumerable<T>
以外,還可以用於IEnumerable
、IEnumerator<T>
、IEnumerator
。
因此,如果想用C#
來模擬C++
/Java
的generator<T>
的行為,會比較簡單:
var seq = GetNumbers(); seq.MoveNext(); Console.WriteLine(seq.Current); // 0 seq.MoveNext(); Console.WriteLine(seq.Current); // 1 seq.MoveNext(); Console.WriteLine(seq.Current); // 2 seq.MoveNext(); Console.WriteLine(seq.Current); // 3 seq.MoveNext(); Console.WriteLine(seq.Current); // 4 IEnumerator<int> GetNumbers() { for (var i = 0; i < 5; ++i) yield return i; }
yield return
——“迭代器”釋出於C# 2.0
。
6. foreach
迴圈,與IEnumerable<T>
型別
是“鴨子型別”,有“操作空間”。
foreach
不一定非要配合使用IEnumerable<T>
型別,只要物件存在GetEnumerator()
方法即可:
void Main() { foreach (var i in new F()) { Console.Write(i + ","); // 1,4,5,} } class F { public IEnumerator<int> GetEnumerator() { for (var i = 0; i < 5; ++i) { yield return i; } } }
另外,如果物件實現了GetAsyncEnumerator()
,甚至也可以一樣使用await foreach
非同步迴圈:
async Task Main() { await foreach (var i in new F()) { Console.Write(i + ",} } class F { public async IAsyncEnumerator<int> GetAsyncEnumerator() { for (var i = 0; i < 5; ++i) { await Task.Delay(1); yield return i; } } }
await foreach
是C# 8.0
隨著非同步流
一起釋出的,具體可見我之前寫的《程式碼演示C#各版本新功能》。
7. using
關鍵字,與IDisposable
介面
是,也不是。
引用型別
和正常的值型別
用using
關鍵字,必須基於IDisposable
介面。
但ref struct
和IAsyncDisposable
就是另一個故事了,由於ref struct
不允許隨便移動,而引用型別——託管堆,會允許記憶體移動,所以ref struct
不允許和引用型別
產生任何關係,這個關係就包含繼承介面
——因為介面
也是引用型別
。
但釋放資源的需求依然存在,怎麼辦,“鴨子型別”來了,可以手寫一個Dispose()
方法,不需要繼承任何介面:
void S1Demo() { using S1 s1 = new S1(); } ref struct S1 { public void Dispose() { Console.WriteLine("正常釋放"); } }
同樣的道理,如果用IAsyncDisposable
介面:
async Task S2Demo() { await using S2 s2 = new S2(); } struct S2 : IAsyncDisposable { public async ValueTask DisposeAsync() { await Task.Delay(1); Console.WriteLine("Async釋放"); } }
8. T?
,與Nullable<T>
型別
是“黑魔法”,只有Nullable<T>
才能接受T?
,Nullable<T>
作為一個值型別
,它還能直接接受null
值(正常值型別
不允許接受null
值)。
示例程式碼如下:
int? t1 = null; Nullable<int> t2 = null; int t3 = null; // Error CS0037: Cannot convert null to 'int' because it is a non-nullable value type
生成程式碼如下(int?
與Nullable<int>
完全一樣,跳過了編譯失敗的程式碼):
IL_0000: nop IL_0001: ldloca.s 0 IL_0003: initobj valuetype [System.Runtime]System.Nullable`1<int32> IL_0009: ldloca.s 1 IL_000b: initobj valuetype [System.Runtime]System.Nullable`1<int32> IL_0011: ret
9. 任意型別的Index/Range
泛型操作
有“黑魔法”,也有“鴨子型別”——存在操作空間。
Index/Range
釋出於C# 8.0
,可以像Python
那樣方便地操作索引位置、取出對應值。以前需要呼叫Substring
等複雜操作的,現在非常簡單。
string url = "https://www.super-cool.com/product/7705a33a-4d2c-455d-a42c-c95e6ac8ee99/summary"; string productId = url[35..url.LastIndexOf("/")]; Console.WriteLine(productId);
生成程式碼如下:
string url = "https://www.super-cool.com/product/7705a33a-4d2c-455d-a42c-c95e6ac8ee99/amd-r7-3800x"; int num = 35; int length = url.LastIndexOf("/") - num; string productId = url.Substring(num,length); Console.WriteLine(productId); // 7705a33a-4d2c-455d-a42c-c95e6ac8ee99
可見,C#
編譯器忽略了Index/Range
,直接翻譯為呼叫Substring
了。
但陣列又不同:
var range = new[] { 1,5 }[1..3]; Console.WriteLine(string.Join(",range)); // 2,3
生成程式碼如下:
int[] range = RuntimeHelpers.GetSubArray<int>(new int[5] { 1,5 },new Range(1,3)); Console.WriteLine(string.Join<int>(",range));
可見它確實建立了Range
型別,然後呼叫了RuntimeHelpers.GetSubArray<int>
,完全屬於“黑魔法”。
但它同時也是“鴨子”型別,只要程式碼中實現了Length
屬性和Slice(int,int)
方法,即可呼叫Index/Range
:
var range2 = new F()[2..]; Console.WriteLine(range2); // 2 -> -2 class F { public int Length { get; set; } public IEnumerable<int> Slice(int start,int end) { yield return start; yield return end; } }
生成程式碼如下:
F f = new F(); int length2 = f.Length; length = 2; num = length2 - length; string range2 = f.Slice(length,num); Console.WriteLine(range2);
總結
如上所見,C#
的“黑魔法”確實挺多,但“鴨子型別”也有很多,“騷操作”的“操作空間”很大。
據傳C# 9.0
將新增“鴨子型別”的元祖——Type Classes
,到時候“操作空間”肯定比現在更大,非常期待!
到此這篇關於C#中的9個“黑魔法”的文章就介紹到這了,更多相關c# 黑魔法內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!