打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
C#中的9个“黑魔法”与“骚操作”

我们知道C#是非常先进的语言,因为是它很有远见的'语法糖'。这些'语法糖'有时过于好用,导致有人觉得它是C#编译器写死的东西,没有道理可讲的------有点像'黑魔法'。

那么我们可以看看C#这些高级语言功能,是编译器写死的东西('黑魔法'),还是可以扩展(骚操作)的'鸭子类型'。

我先列一个目录,大家可以对着这个目录试着下判断,说说是'黑魔法'(编译器写死),还是'鸭子类型'(可以自定义'骚操作'):

  1. LINQ操作,与IEnumerable<T>类型;
  2. async/await,与Task/ValueTask类型;
  3. 表达式树,与Expression<T>类型;
  4. 插值字符串,与FormattableString类型;
  5. yield return,与IEnumerable<T>类型;
  6. foreach循环,与IEnumerable<T>类型;
  7. using关键字,与IDisposable接口;
  8. T?,与Nullable<T>类型;
  9. 任意类型的Index/Range泛型操作。

1. LINQ操作,与IEnumerable<T>类型

不是'黑魔法',是'鸭子类型'。

LINQC# 3.0发布的新功能,可以非常便利地操作数据。现在12年过去了,虽然有些功能有待增强,但相比其它语言还是方便许多。

如我上一篇博客提到,LINQ不一定要基于IEnumerable<T>,只需定定义一个类型,实现所需要的LINQ表达式即可,LINQselect关键字,会调用.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()的接口,该接口必须返回一个继承于INotifyCompletionICriticalNotifyCompletion的类,该类还需要实现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<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>以外,还可以用于IEnumerableIEnumerator<T>IEnumerator

因此,如果想用C#来模拟C++/Javagenerator<T>的行为,会比较简单:

var seq = GetNumbers();seq.MoveNext();Console.WriteLine(seq.Current); // 0seq.MoveNext();Console.WriteLine(seq.Current); // 1seq.MoveNext();Console.WriteLine(seq.Current); // 2seq.MoveNext();Console.WriteLine(seq.Current); // 3seq.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, 2, 3, 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 + ', '); // 1, 2, 3, 4, 5,    }}
class F{ public async IAsyncEnumerator<int> GetAsyncEnumerator() { for (var i = 0; i < 5; ++i) { await Task.Delay(1); yield return i; } }}

await foreachC# 8.0随着异步流一起发布的,具体可见我之前写的《代码演示C#各版本新功能》。

7. using关键字,与IDisposable接口

是,也不是。

引用类型和正常的值类型using关键字,必须基于IDisposable接口。

ref structIAsyncDisposable就是另一个故事了,由于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: nopIL_0001: ldloca.s 0IL_0003: initobj valuetype [System.Runtime]System.Nullable`1<int32>IL_0009: ldloca.s 1IL_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, 2, 3, 4, 5 }[1..3];Console.WriteLine(string.Join(', ', range)); // 2, 3

生成代码如下:

int[] range = RuntimeHelpers.GetSubArray<int>(new int[5]{    1,    2,    3,    4,    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,到时候'操作空间'肯定比现在更大,非常期待!

End


本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
C#语法——await与async的正确打开方式
异步编程基础
C# 7
Linq之前的C#知识
从未来看 C#
数组
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服