程序员求职经验分享与学习资料整理平台

网站首页 > 文章精选 正文

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

balukai 2025-07-17 17:13:01 文章精选 4 ℃

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

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

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

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

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

  2. async/await,与 Task/ ValueTask类型;

  3. 表达式树,与 Expression<T>类型;

  4. 插值字符串,与 FormattableString类型;

  5. yieldreturn,与 IEnumerable<T>类型;

  6. foreach循环,与 IEnumerable<T>类型;

  7. using关键字,与 IDisposable接口;

  8. T?,与 able<T>类型;

  9. 任意类型的 Index/Range泛型操作。

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

不是“黑魔法”,是“鸭子类型”。

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

如我上一篇博客提到, LINQ不一定要基于 IEnumerable<T>,只需定定义一个类型,实现所需要的 LINQ表达式即可, LINQselect关键字,会调用 .Select方法,可以用如下的“骚操作”,实现“移花接木”的效果:

  1. voidMain()

  2. {

  3. var query =

  4. from i innew F()

  5. select3;


  6. Console.WriteLine(string.Join(",", query));// 0,1,2,3,4

  7. }


  8. class F

  9. {

  10. publicIEnumerable<int>Select<R>(Func<int, R> t)

  11. {

  12. for(var i =0; i <5;++i)

  13. {

  14. yieldreturn i;

  15. }

  16. }

  17. }

2. async/await,与 Task/ ValueTask类型

不是“黑魔法”,是“鸭子类型”。

async/await发布于 C# 5.0,可以非常便利地做异步编程,其本质是状态机。

async/await的本质是会寻找类型下一个名字叫 GetAwaiter()的接口,该接口必须返回一个继承于 INotifyCompletionICriticalNotifyCompletion的类,该类还需要实现 GetResult()方法和 IsComplete属性。

这一点在 C#语言规范中有说明,调用 awaitt本质会按如下顺序执行:

  1. 先调用 t.GetAwaiter()方法,取得等待器 a

  2. 调用 a.IsCompleted取得布尔类型 b

  3. 如果 b=true,则立即执行 a.GetResult(),取得运行结果;

  4. 如果 b=false,则看情况:

  5. 如果 a没实现 ICriticalNotifyCompletion,则执行 (aasINotifyCompletion).OnCompleted(action)

  6. 如果 a实现了 ICriticalNotifyCompletion,则执行 (aasICriticalNotifyCompletion).OnCompleted(action)

  7. 执行随后暂停, OnCompleted完成后重新回到状态机;

有兴趣的可以访问 Github具体规范说明:https://github.com/dotnet/csharplang/blob/master/spec/expressions.md#runtime-evaluation-of-await-expressions

正常 Task.Delay()是基于 线程池计时器的,可以用如下“骚操作”,来实现一个单线程的 TaskEx.Delay()

  1. staticActionTick=;


  2. voidMain()

  3. {

  4. Start();

  5. while(true)

  6. {

  7. if(Tick!=)Tick();

  8. Thread.Sleep(1);

  9. }

  10. }


  11. asyncvoidStart()

  12. {

  13. Console.WriteLine("执行开始");

  14. for(int i =1; i <=4;++i)

  15. {

  16. Console.WriteLine($"第{i}次,时间:{DateTime.Now.ToString("HH:mm:ss")} - 线程号:{
    Thread.CurrentThread.ManagedThreadId}"
    );

  17. awaitTaskEx.Delay(1000);

  18. }

  19. Console.WriteLine("执行完成");

  20. }


  21. classTaskEx

  22. {

  23. publicstaticMyDelayDelay(int ms)=>newMyDelay(ms);

  24. }


  25. classMyDelay:INotifyCompletion

  26. {

  27. privatereadonlydouble _start;

  28. privatereadonlyint _ms;


  29. publicMyDelay(int ms)

  30. {

  31. _start =Util.ElapsedTime.TotalMilliseconds;

  32. _ms = ms;

  33. }


  34. internalMyDelayGetAwaiter()=>this;


  35. publicvoidOnCompleted(Action continuation)

  36. {

  37. Tick+=Check;


  38. voidCheck()

  39. {

  40. if(Util.ElapsedTime.TotalMilliseconds- _start > _ms)

  41. {

  42. continuation();

  43. Tick-=Check;

  44. }

  45. }

  46. }


  47. publicvoidGetResult(){}


  48. publicboolIsCompleted=>false;

  49. }

运行效果如下:

  1. 执行开始

  2. 1次,时间:17:38:03-线程号:1

  3. 2次,时间:17:38:04-线程号:1

  4. 3次,时间:17:38:05-线程号:1

  5. 4次,时间:17:38:06-线程号:1

  6. 执行完成

注意不需要非得使用 TaskCompletionSource<T>才能创建定定义的 async/await

3. 表达式树,与 Expression<T>类型

是“黑魔法”,没有“操作空间”,只有当类型是 Expression<T>时,才会创建为表达式树。

表达式树C# 3.0随着 LINQ一起发布,是有远见的“黑魔法”。

如以下代码:

  1. Expression<Func<int>> g3 =()=>3;

会被编译器翻译为:

  1. Expression<Func<int>> g3 =Expression.Lambda<Func<int>>(

  2. Expression.Constant(3,typeof(int)),

  3. Array.Empty<ParameterExpression>());

4. 插值字符串,与 FormattableString类型

是“黑魔法”,没有“操作空间”。

插值字符串发布于 C# 6.0,在此之前许多语言都提供了类似的功能。

只有当类型是 FormattableString,才会产生不一样的编译结果,如以下代码:

  1. FormattableString x1 = $"Hello {42}";

  2. string x2 = $"Hello {42}";

编译器生成结果如下:

  1. FormattableString x1 =FormattableStringFactory.Create("Hello {0}",42);

  2. string x2 =string.Format("Hello {0}",42);

注意其本质是调用了 FormattableStringFactory.Create来创建一个类型。

5. yieldreturn,与 IEnumerable<T>类型;

是“黑魔法”,但有补充说明。

yieldreturn除了用于 IEnumerable<T>以外,还可以用于 IEnumerableIEnumerator<T>IEnumerator

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

  1. var seq =GetNumbers();

  2. seq.MoveNext();

  3. Console.WriteLine(seq.Current);// 0

  4. seq.MoveNext();

  5. Console.WriteLine(seq.Current);// 1

  6. seq.MoveNext();

  7. Console.WriteLine(seq.Current);// 2

  8. seq.MoveNext();

  9. Console.WriteLine(seq.Current);// 3

  10. seq.MoveNext();

  11. Console.WriteLine(seq.Current);// 4


  12. IEnumerator<int>GetNumbers()

  13. {

  14. for(var i =0; i <5;++i)

  15. yieldreturn i;

  16. }

yieldreturn——“迭代器”发布于 C# 2.0

6. foreach循环,与 IEnumerable<T>类型

是“鸭子类型”,有“操作空间”。

foreach不一定非要配合使用 IEnumerable<T>类型,只要对象存在 GetEnumerator()方法即可:

  1. voidMain()

  2. {

  3. foreach(var i innew F())

  4. {

  5. Console.Write(i +", ");// 1, 2, 3, 4, 5,

  6. }

  7. }


  8. class F

  9. {

  10. publicIEnumerator<int>GetEnumerator()

  11. {

  12. for(var i =0; i <5;++i)

  13. {

  14. yieldreturn i;

  15. }

  16. }

  17. }

另外,如果对象实现了 GetAsyncEnumerator(),甚至也可以一样使用 awaitforeach异步循环:

  1. asyncTaskMain()

  2. {

  3. awaitforeach(var i innew F())

  4. {

  5. Console.Write(i +", ");// 1, 2, 3, 4, 5,

  6. }

  7. }


  8. class F

  9. {

  10. publicasyncIAsyncEnumerator<int>GetAsyncEnumerator()

  11. {

  12. for(var i =0; i <5;++i)

  13. {

  14. awaitTask.Delay(1);

  15. yieldreturn i;

  16. }

  17. }

  18. }

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

7. using关键字,与 IDisposable接口

是,也不是。

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

refstructIAsyncDisposable就是另一个故事了,由于 refstruct不允许随便移动,而引用类型——托管堆,会允许内存移动,所以 refstruct不允许和 引用类型产生任何关系,这个关系就包含继承 接口——因为 接口也是 引用类型

但释放资源的需求依然存在,怎么办,“鸭子类型”来了,可以手写一个 Dispose()方法,不需要继承任何接口:

  1. void S1Demo()

  2. {

  3. using S1 s1 =new S1();

  4. }


  5. refstruct S1

  6. {

  7. publicvoidDispose()

  8. {

  9. Console.WriteLine("正常释放");

  10. }

  11. }

同样的道理,如果用 IAsyncDisposable接口:

  1. asyncTask S2Demo()

  2. {

  3. awaitusing S2 s2 =new S2();

  4. }


  5. struct S2 :IAsyncDisposable

  6. {

  7. publicasyncValueTaskDisposeAsync()

  8. {

  9. awaitTask.Delay(1);

  10. Console.WriteLine("Async释放");

  11. }

  12. }

8. T?,与 able<T>类型

是“黑魔法”,只有 able<T>才能接受 T?able<T>作为一个 值类型,它还能直接接受 值(正常 值类型不允许接受 值)。

示例代码如下:

  1. int? t1 =;

  2. able<int> t2 =;

  3. int t3 =;// Error CS0037: Cannot convert to 'int' because it is a non-able value type

生成代码如下( int?able<int>完全一样,跳过了编译失败的代码):

  1. IL_0000: nop

  2. IL_0001: ldloca.s 0

  3. IL_0003: initobj valuetype [System.Runtime]System.able`1<int32>

  4. IL_0009: ldloca.s 1

  5. IL_000b: initobj valuetype [System.Runtime]System.able`1<int32>

  6. IL_0011: ret

9. 任意类型的 Index/Range泛型操作

有“黑魔法”,也有“鸭子类型”——存在操作空间。

Index/Range发布于 C# 8.0,可以像 Python那样方便地操作索引位置、取出对应值。以前需要调用 Substring等复杂操作的,现在非常简单。

  1. string url ="https://www.super-cool.com/product/7705a33a-4d2c-455d-a42c-c95e6ac8ee99/summary";

  2. string productId = url[35..url.LastIndexOf("/")];

  3. Console.WriteLine(productId);

生成代码如下:

  1. string url ="https://www.super-cool.com/product/7705a33a-4d2c-455d-a42c-c95e6ac8ee99/amd-r7-3800x";

  2. int num =35;

  3. int length = url.LastIndexOf("/")- num;

  4. string productId = url.Substring(num, length);

  5. Console.WriteLine(productId);// 7705a33a-4d2c-455d-a42c-c95e6ac8ee99

可见, C#编译器忽略了 Index/Range,直接翻译为调用 Substring了。

但数组又不同:

  1. var range =new[]{1,2,3,4,5}[1..3];

  2. Console.WriteLine(string.Join(", ", range));// 2, 3

生成代码如下:

  1. int[] range =RuntimeHelpers.GetSubArray<int>(newint[5]

  2. {

  3. 1,

  4. 2,

  5. 3,

  6. 4,

  7. 5

  8. },newRange(1,3));

  9. Console.WriteLine(string.Join<int>(", ", range));

可见它确实创建了 Range类型,然后调用了 RuntimeHelpers.GetSubArray<int>,完全属于“黑魔法”。

但它同时也是“鸭子”类型,只要代码中实现了 Length属性和 Slice(int,int)方法,即可调用 Index/Range

  1. var range2 =new F()[2..];

  2. Console.WriteLine(range2);// 2 -> -2


  3. class F

  4. {

  5. publicintLength{get;set;}

  6. publicIEnumerable<int>Slice(int start,intend)

  7. {

  8. yieldreturn start;

  9. yieldreturnend;

  10. }

  11. }

生成代码如下:

  1. F f =new F();

  2. int length2 = f.Length;

  3. length =2;

  4. num = length2 - length;

  5. string range2 = f.Slice(length, num);

  6. Console.WriteLine(range2);

总结

如上所见, C#的“黑魔法”确实挺多,但“鸭子类型”也有很多,“骚操作”的“操作空间”很大。

据传 C# 9.0将添加“鸭子类型”的元祖—— TypeClasses,到时候“操作空间”肯定比现在更大,非常期待!


Tags:

最近发表
标签列表