C#中的9個“黑魔法”
阿新 • • 發佈:2020-03-31
# C#中的9個“黑魔法”與“騷操作”
我們知道`C#`是非常先進的語言,因為是它很有遠見的“語法糖”。這些“語法糖”有時**過於好用**,導致有人覺得它是`C#`編譯器寫死的東西,沒有道理可講的——有點像“黑魔法”。
那麼我們可以看看`C#`這些**高階**語言功能,是編譯器寫死的東西(“黑魔法”),還是可以擴充套件(騷操作)的“鴨子型別”。
我先列一個目錄,大家可以對著這個目錄試著下判斷,說說是“黑魔法”(編譯器寫死),還是“鴨子型別”(可以自定義“騷操作”):
1. `LINQ`操作,與`IEnumerable`型別;
2. `async/await`,與`Task`/`ValueTask`型別;
3. 表示式樹,與`Expression`型別;
4. 插值字串,與`FormattableString`型別;
5. `yield return`,與`IEnumerable`型別;
6. `foreach`迴圈,與`IEnumerable`型別;
7. `using`關鍵字,與`IDisposable`介面;
8. `T?`,與`Nullable`型別;
9. 任意型別的`Index/Range`泛型操作。
## 1. `LINQ`操作,與`IEnumerable`型別
不是“黑魔法”,是“鴨子型別”。
`LINQ`是`C# 3.0`釋出的新功能,可以非常便利地操作資料。現在`12`年過去了,雖然有些功能有待增強,但相比其它語言還是方便許多。
如我上一篇部落格提到,`LINQ`不一定要基於`IEnumerable`,只需定定義一個型別,實現所需要的`LINQ`表示式即可,`LINQ`的`select`關鍵字,會呼叫`.Select`方法,可以用如下的“騷操作”,實現“移花接木”的效果:
```csharp
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 Select(Func 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`屬性。
這一點在`C#`語言規範中有說明,呼叫`await t`本質會按如下順序執行:
1. 先呼叫`t.GetAwaiter()`方法,取得等待器`a`;
2. 呼叫`a.IsCompleted`取得布林型別`b`;
3. 如果`b=true`,則立即執行`a.GetResult()`,取得執行結果;
4. 如果`b=false`,則看情況:
1. 如果`a`沒實現`ICriticalNotifyCompletion`,則執行`(a as INotifyCompletion).OnCompleted(action)`
2. 如果`a`實現了`ICriticalNotifyCompletion`,則執行`(a as ICriticalNotifyCompletion).OnCompleted(action)`
3. 執行隨後暫停,`OnCompleted`完成後重新回到狀態機;
有興趣的可以訪問`Github`具體規範說明:[https://github.com/dotnet/csharplang/blob/master/spec/expressions.md#runtime-evaluation-of-await-expressions](https://github.com/dotnet/csharplang/blob/master/spec/expressions.md#runtime-evaluation-of-await-expressions)
正常`Task.Delay()`是基於`執行緒池計時器`的,可以用如下“騷操作”,來實現一個單執行緒的`TaskEx.Delay()`:
```csharp
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`才能建立定定義的`async/await`。
## 3. 表示式樹,與`Expression`型別
是“黑魔法”,沒有“操作空間”,只有當型別是`Expression`時,才會建立為表示式樹。
`表示式樹`是`C# 3.0`隨著`LINQ`一起釋出,是有遠見的“黑魔法”。
如以下程式碼:
```csharp
Expression> g3 = () => 3;
```
會被編譯器翻譯為:
```csharp
Expression> g3 = Expression.Lambda>(
Expression.Constant(3, typeof(int)),
Array.Empty());
```
## 4. 插值字串,與`FormattableString`型別
是“黑魔法”,沒有“操作空間”。
`插值字串`釋出於`C# 6.0`,在此之前許多語言都提供了類似的功能。
只有當型別是`FormattableString`,才會產生不一樣的編譯結果,如以下程式碼:
```csharp
FormattableString x1 = $"Hello {42}";
string x2 = $"Hello {42}";
```
編譯器生成結果如下:
```csharp
FormattableString x1 = FormattableStringFactory.Create("Hello {0}", 42);
string x2 = string.Format("Hello {0}", 42);
```
注意其本質是呼叫了`FormattableStringFactory.Create`來建立一個型別。
## 5. `yield return`,與`IEnumerable`型別;
是“黑魔法”,但有補充說明。
`yield return`除了用於`IEnumerable`以外,還可以用於`IEnumerable`、`IEnumerator`、`IEnumerator`。
因此,如果想用`C#`來模擬`C++`/`Java`的`generator`的行為,會比較簡單:
```csharp
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 GetNumbers()
{
for (var i = 0; i < 5; ++i)
yield return i;
}
```
`yield return`——“迭代器”釋出於`C# 2.0`。
## 6. `foreach`迴圈,與`IEnumerable`型別
是“鴨子型別”,有“操作空間”。
`foreach`不一定非要配合使用`IEnumerable`型別,只要物件存在`GetEnumerator()`方法即可:
```csharp
void Main()
{
foreach (var i in new F())
{
Console.Write(i + ", "); // 1, 2, 3, 4, 5,
}
}
class F
{
public IEnumerator GetEnumerator()
{
for (var i = 0; i < 5; ++i)
{
yield return i;
}
}
}
```
另外,如果物件實現了`GetAsyncEnumerator()`,甚至也可以一樣使用`await foreach`非同步迴圈:
```csharp
async Task Main()
{
await foreach (var i in new F())
{
Console.Write(i + ", "); // 1, 2, 3, 4, 5,
}
}
class F
{
public async IAsyncEnumerator 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()`方法,不需要繼承任何介面:
```csharp
void S1Demo()
{
using S1 s1 = new S1();
}
ref struct S1
{
public void Dispose()
{
Console.WriteLine("正常釋放");
}
}
```
同樣的道理,如果用`IAsyncDisposable`介面:
```csharp
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`型別
是“黑魔法”,只有`Nullable`才能接受`T?`,`Nullable`作為一個`值型別`,它還能直接接受`null`值(正常`值型別`不允許接受`null`值)。
示例程式碼如下:
```csharp
int? t1 = null;
Nullable t2 = null;
int t3 = null; // Error CS0037: Cannot convert null to 'int' because it is a non-nullable value type
```
生成程式碼如下(`int?`與`Nullable`完全一樣,跳過了編譯失敗的程式碼):
```cil
IL_0000: nop
IL_0001: ldloca.s 0
IL_0003: initobj valuetype [System.Runtime]System.Nullable`1
IL_0009: ldloca.s 1
IL_000b: initobj valuetype [System.Runtime]System.Nullable`1
IL_0011: ret
```
## 9. 任意型別的`Index/Range`泛型操作
有“黑魔法”,也有“鴨子型別”——存在操作空間。
`Index/Range`釋出於`C# 8.0`,可以像`Python`那樣方便地操作索引位置、取出對應值。以前需要呼叫`Substring`等複雜操作的,現在非常簡單。
```csharp
string url = "https://www.super-cool.com/product/7705a33a-4d2c-455d-a42c-c95e6ac8ee99/summary";
string productId = url[35..url.LastIndexOf("/")];
Console.WriteLine(productId);
```
生成程式碼如下:
```csharp
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`了。
但陣列又不同:
```csharp
var range = new[] { 1, 2, 3, 4, 5 }[1..3];
Console.WriteLine(string.Join(", ", range)); // 2, 3
```
生成程式碼如下:
```csharp
int[] range = RuntimeHelpers.GetSubArray(new int[5]
{
1,
2,
3,
4,
5
}, new Range(1, 3));
Console.WriteLine(string.Join(", ", range));
```
可見它確實建立了`Range`型別,然後呼叫了`RuntimeHelpers.GetSubArray`,完全屬於“黑魔法”。
但它同時也是“鴨子”型別,只要程式碼中實現了`Length`屬性和`Slice(int, int)`方法,即可呼叫`Index/Range`:
```csharp
var range2 = new F()[2..];
Console.WriteLine(range2); // 2 -> -2
class F
{
public int Length { get; set; }
public IEnumerable Slice(int start, int end)
{
yield return start;
yield return end;
}
}
```
生成程式碼如下:
```csharp
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`,到時候“操作空間”肯定比現在更大,非常期待!
喜歡的朋友請關注我的微信公眾號:【DotNet騷操作】
![DotNet騷操作](https://img2018.cnblogs.com/blog/233608/201908/233608-20190825165420518-990227