C#中的9个“黑魔法”
.NET骚操作 人气:1
# 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.comhttps://img.qb5200.com/download-x/dotnet/csharplang/blob/master/spec/expressions.md#runtime-evaluation-of-await-expressions](https://github.comhttps://img.qb5200.com/download-x/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-990227633.jpg)
加载全部内容