重庆分公司,新征程启航
为企业提供网站建设、域名注册、服务器等服务
小编给大家分享一下C#中“黑魔法”是什么,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!
成都创新互联公司是一家集成都网站建设、成都做网站、网站页面设计、网站优化SEO优化为一体的专业网络公司,已为成都等多地近百家企业提供网站建设服务。追求良好的浏览体验,以探求精品塑造与理念升华,设计最适合用户的网站页面。 合作只是第一步,服务才是根本,我们始终坚持讲诚信,负责任的原则,为您进行细心、贴心、认真的服务,与众多客户在蓬勃发展的市场环境中,互促共生。
我们知道C#
是非常先进的语言,因为是它很有远见的“语法糖”。这些“语法糖”有时过于好用,导致有人觉得它是C#
编译器写死的东西,没有道理可讲的——有点像“黑魔法”。
那么我们可以看看C#
这些高级语言功能,是编译器写死的东西(“黑魔法”),还是可以扩展(骚操作)的“鸭子类型”。
我先列一个目录,大家可以对着这个目录试着下判断,说说是“黑魔法”(编译器写死),还是“鸭子类型”(可以自定义“骚操作”):
LINQ操作,与IEnumerable
async/await,与Task/ValueTask类型;
表达式树,与Expression
插值字符串,与FormattableString类型;
yield return,与IEnumerable
foreach循环,与IEnumerable
using关键字,与IDisposable接口;
T?,与Nullable
任意类型的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 IEnumerableSelect (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
属性。
先调用t.GetAwaiter()方法,取得等待器a;
调用a.IsCompleted取得布尔类型b;
如果b=true,则立即执行a.GetResult(),取得运行结果;
如果b=false,则看情况:
如果a没实现ICriticalNotifyCompletion,则执行(a as INotifyCompletion).OnCompleted(action)
如果a实现了ICriticalNotifyCompletion,则执行(a as ICriticalNotifyCompletion).OnCompleted(action)
执行随后暂停,OnCompleted完成后重新回到状态机;
有兴趣的可以访问Github
具体规范说明:https://github.com/dotnet/csharplang/blob/master/spec/expressions.md
正常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 IEnumeratorGetNumbers() { 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 IEnumeratorGetEnumerator() { 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 IAsyncEnumeratorGetAsyncEnumerator() { 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; Nullablet2 = 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`1IL_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[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(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()[2..]; Console.WriteLine(range2); // 2 -> -2 class F { public int Length { get; set; } public IEnumerableSlice(int start, int end) { yield return start; yield return end; } }
生成代码如下:
F f = new F(); int length3 = f.Length; length = 2; num = length3 - length; string range2 = f.Slice(length, num); Console.WriteLine(range2);
如上所见,C#
的“黑魔法”确实挺多,但“鸭子类型”也有很多,“骚操作”的“操作空间”很大。
据传C# 9.0
将添加“鸭子类型”的元祖——Type Classes
,到时候“操作空间”肯定比现在更大,非常期待!
以上是“C#中“黑魔法”是什么”这篇文章的所有内容,感谢各位的阅读!相信大家都有了一定的了解,希望分享的内容对大家有所帮助,如果还想学习更多知识,欢迎关注创新互联行业资讯频道!