CodeSnippet.Cn
代码片段
Csharp
架构设计
.NetCore
西班牙语
kubernetes
MySql
Redis
Algorithm
Ubuntu
Linux
Other
.NetMvc
VisualStudio
Git
pm
Python
WPF
java
Plug-In
分布式
CSS
微服务架构
JavaScript
DataStructure
Shared
C#中的9个“黑魔法”与“骚操作”
0
Csharp
小笨蛋
发布于:2023年06月25日
更新于:2023年06月25日
88
#custom-toc-container
## 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`方法,可以用如下的骚操作,实现移花接木的效果: ``` 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 正常`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
` 才能创建定定义的`async/await`。 ### 3. 表达式树,与`Expression
`类型 是黑魔法,没有操作空间,只有当类型是`Expression
`时,才会创建为表达式树。 表达式树是C# 3.0随着LINQ一起发布,是有远见的黑魔法。 如以下代码: ``` Expression
> g3 = () => 3; ``` 会被编译器翻译为: ``` Expression
> g3 = Expression.Lambda
>( Expression.Constant(3, typeof(int)), Array.Empty
()); ``` ### 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
`类型; 是黑魔法,但有补充说明。 `yield return`除了用于`IEnumerable
`以外,还可以用于`IEnumerable`、`IEnumerator
`、`IEnumerator`。 因此,如果想用`C#`来模拟`C++`/`Java`的`generator
`的行为,会比较简单: ``` 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()`方法即可: ``` 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`异步循环: ``` 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()`方法,不需要继承任何接口: ``` 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
`类型 是黑魔法,只有`Nullable
`才能接受`T?`,`Nullable
`作为一个`值类型`,它还能直接接受`null`值(正常`值类型`不允许接受`null`值)。 示例代码如下: ``` 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
`完全一样,跳过了编译失败的代码): ``` 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`等复杂操作的,现在非常简单。 ``` string url = "https://www.super-cool.com/product/7705a33a-4d2c-455d-a42c-c95e6ac8ee99/summary"; string productId = url[.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, 2, 3, 4, 5 }[.3]; Console.WriteLine(string.Join(", ", range)); // 2, 3 ``` 生成代码如下: ``` 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`: ``` var range2 = new F()[.]; 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; } } ``` 生成代码如下: ``` 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`,到时候操作空间肯定比现在更大,非常期待!
这里⇓感觉得写点什么,要不显得有点空,但还没想好写什么...
返回顶部
About
京ICP备13038605号
© 代码片段 2024