Csharp/C#教程:C#中的9个“黑魔法”分享

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

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

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

LINQ操作,与IEnumerable<T>类型; async/await,与Task/ValueTask类型; 表达式树,与Expression<T>类型; 插值字符串,与FormattableString类型; yieldreturn,与IEnumerable<T>类型; foreach循环,与IEnumerable<T>类型; using关键字,与IDisposable接口; T?,与Nullable<T>类型; 任意类型的Index/Range泛型操作。

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

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

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

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

voidMain() { varquery= fromiinnewF() select3; Console.WriteLine(string.Join(",",query));//0,1,2,3,4 } classF { publicIEnumerable<int>Select<R>(Func<int,R>t) { for(vari=0;i<5;++i) { yieldreturni; } } }

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

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

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

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

先调用t.GetAwaiter()方法,取得等待器a; 调用a.IsCompleted取得布尔类型b; 如果b=true,则立即执行a.GetResult(),取得运行结果; 如果b=false,则看情况:

如果a没实现ICriticalNotifyCompletion,则执行(aasINotifyCompletion).OnCompleted(action)
如果a实现了ICriticalNotifyCompletion,则执行(aasICriticalNotifyCompletion).OnCompleted(action)
执行随后暂停,OnCompleted完成后重新回到状态机;

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

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

staticActionTick=null; voidMain() { Start(); while(true) { if(Tick!=null)Tick(); Thread.Sleep(1); } } asyncvoidStart() { Console.WriteLine("执行开始"); for(inti=1;i<=4;++i) { Console.WriteLine($"第{i}次,时间:{DateTime.Now.ToString("HH:mm:ss")}-线程号:{Thread.CurrentThread.ManagedThreadId}"); awaitTaskEx.Delay(1000); } Console.WriteLine("执行完成"); } classTaskEx { publicstaticMyDelayDelay(intms)=>newMyDelay(ms); } classMyDelay:INotifyCompletion { privatereadonlydouble_start; privatereadonlyint_ms; publicMyDelay(intms) { _start=Util.ElapsedTime.TotalMilliseconds; _ms=ms; } internalMyDelayGetAwaiter()=>this; publicvoidOnCompleted(Actioncontinuation) { Tick+=Check; voidCheck() { if(Util.ElapsedTime.TotalMilliseconds-_start>_ms) { continuation(); Tick-=Check; } } } publicvoidGetResult(){} publicboolIsCompleted=>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,才会产生不一样的编译结果,如以下代码:

FormattableStringx1=$"Hello{42}"; stringx2=$"Hello{42}";

编译器生成结果如下:

FormattableStringx1=FormattableStringFactory.Create("Hello{0}",42); stringx2=string.Format("Hello{0}",42);

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

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

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

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

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

varseq=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<int>GetNumbers() { for(vari=0;i<5;++i) yieldreturni; }

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

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

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

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

voidMain() { foreach(variinnewF()) { Console.Write(i+",");//1,2,3,4,5, } } classF { publicIEnumerator<int>GetEnumerator() { for(vari=0;i<5;++i) { yieldreturni; } } }

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

asyncTaskMain() { awaitforeach(variinnewF()) { Console.Write(i+",");//1,2,3,4,5, } } classF { publicasyncIAsyncEnumerator<int>GetAsyncEnumerator() { for(vari=0;i<5;++i) { awaitTask.Delay(1); yieldreturni; } } }

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

7.using关键字,与IDisposable接口

是,也不是。

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

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

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

voidS1Demo() { usingS1s1=newS1(); } refstructS1 { publicvoidDispose() { Console.WriteLine("正常释放"); } }

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

asyncTaskS2Demo() { awaitusingS2s2=newS2(); } structS2:IAsyncDisposable { publicasyncValueTaskDisposeAsync() { awaitTask.Delay(1); Console.WriteLine("Async释放"); } }

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

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

示例代码如下:

int?t1=null; Nullable<int>t2=null; intt3=null;//ErrorCS0037:Cannotconvertnullto'int'becauseitisanon-nullablevaluetype

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

IL_0000:nop IL_0001:ldloca.s0 IL_0003:initobjvaluetype[System.Runtime]System.Nullable`1<int32> IL_0009:ldloca.s1 IL_000b:initobjvaluetype[System.Runtime]System.Nullable`1<int32> IL_0011:ret

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

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

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

stringurl="https://www.super-cool.com/product/7705a33a-4d2c-455d-a42c-c95e6ac8ee99/summary"; stringproductId=url[35..url.LastIndexOf("/")]; Console.WriteLine(productId);

生成代码如下:

stringurl="https://www.super-cool.com/product/7705a33a-4d2c-455d-a42c-c95e6ac8ee99/amd-r7-3800x"; intnum=35; intlength=url.LastIndexOf("/")-num; stringproductId=url.Substring(num,length); Console.WriteLine(productId);//7705a33a-4d2c-455d-a42c-c95e6ac8ee99

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

但数组又不同:

varrange=new[]{1,2,3,4,5}[1..3]; Console.WriteLine(string.Join(",",range));//2,3

生成代码如下:

int[]range=RuntimeHelpers.GetSubArray<int>(newint[5] { 1, 2, 3, 4, 5 },newRange(1,3)); Console.WriteLine(string.Join<int>(",",range));

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

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

varrange2=newF()[2..]; Console.WriteLine(range2);//2->-2 classF { publicintLength{get;set;} publicIEnumerable<int>Slice(intstart,intend) { yieldreturnstart; yieldreturnend; } }

生成代码如下:

Ff=newF(); intlength2=f.Length; length=2; num=length2-length; stringrange2=f.Slice(length,num); Console.WriteLine(range2);

上述就是C#学习教程:C#中的9个“黑魔法”分享的全部内容,如果对大家有所用处且需要了解更多关于C#学习教程,希望大家多多关注—计算机技术网(www.ctvol.com)!

本文来自网络收集,不代表计算机技术网立场,如涉及侵权请联系管理员删除。

ctvol管理联系方式QQ:251552304

本文章地址:https://www.ctvol.com/cdevelopment/903942.html

(0)
上一篇 2021年10月21日
下一篇 2021年10月21日

精彩推荐