C#中的9个“黑魔法”与“骚操作”
我们知道 C#是超级先进的语言,由于是它很有远见的“语法糖”。这些“语法糖”有时过于好用,导致有人觉得它是C#编译器写死的东西,没有道理可讲的——有点像“黑魔法”。
那么我们可以看看 C#这些高级语言功能,是编译器写死的东西(“黑魔法”),还是可以扩展(骚操作)的“鸭子类型”。
我先列一个目录,大家可以对着这个目录试着下判断,说说是“黑魔法”(编译器写死),还是“鸭子类型”(可以自定义“骚操作”):
-
LINQ操作,与IEnumerable<T>类型; -
async/await,与Task/ValueTask类型; -
表达式树,与
Expression<T>类型; -
插值字符串,与
FormattableString类型; -
yieldreturn,与IEnumerable<T>类型; -
foreach循环,与IEnumerable<T>类型; -
using关键字,与IDisposable接口; -
T?,与able<T>类型; -
任意类型的
Index/Range泛型操作。
1. LINQ操作,与 IEnumerable
不是“黑魔法”,是“鸭子类型”。
LINQ是C# 3.0发布的新功能,可以超级便利地操作数据。目前12年过去了,虽然有些功能有待增强,但相比其它语言还是方便许多。
如我上一篇博客提到, LINQ不必定要基于IEnumerable<T>,只需定定义一个类型,实现所需要的LINQ表达式即可,LINQ的select关键字,会调用.Select方法,可以用如下的“骚操作”,实现“移花接木”的效果:
voidMain
{
var query =
from i innew F
select3 ;
Console.WriteLine(string.Join(",", query));// 0,1,2,3,4
}
class F
{
publicIEnumerable<int>Select<R>(Func<int, R> t)
{
for(var i =0 ; i <5 ;++i)
{
yieldreturn i;
}
}
}
2. async/await,与 Task/ ValueTask类型
不是“黑魔法”,是“鸭子类型”。
async/await发布于C# 5.0,可以超级便利地做异步编程,其本质是状态机。
async/await的本质是会寻找类型下一个名字叫GetAwaiter的接口,该接口必须返回一个继承于INotifyCompletion或ICriticalNotifyCompletion的类,该类还需要实现GetResult方法和IsComplete属性。
这一点在 C#语言规范中有说明,调用awaitt本质会按如下顺序执行:
-
先调用
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#
runtime-evaluation-of-await-expressions
正常 Task.Delay是基于线程池计时器的,可以用如下“骚操作”,来实现一个单线程的TaskEx.Delay:
staticActionTick=;
voidMain
{
Start;
while(true)
{
if(Tick!=)Tick;
Thread.Sleep(1 );
}
}
asyncvoidStart
{
Console.WriteLine("执行开始");
for(int i =1 ; i <=4 ;++i)
{
Console.WriteLine($"第{i}次,时间:{DateTime.Now.ToString("HH:mm:ss")} - 线程号:{
Thread.CurrentThread.ManagedThreadId}");
awaitTaskEx.Delay(1000);
}
Console.WriteLine("执行完成");
}
classTaskEx
{
publicstaticMyDelayDelay(int ms)=>newMyDelay(ms);
}
classMyDelay:INotifyCompletion
{
privatereadonlydouble _start;
privatereadonlyint _ms;
publicMyDelay(int ms)
{
_start =Util.ElapsedTime.TotalMilliseconds;
_ms = ms;
}
internalMyDelayGetAwaiter=>this;
publicvoidOnCompleted(Action continuation)
{
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
是“黑魔法”,没有“操作空间”,只有当类型是 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. yieldreturn,与 IEnumerable
是“黑魔法”,但有补充说明。
yieldreturn除了用于IEnumerable<T>以外,还可以用于IEnumerable、IEnumerator<T>、IEnumerator。
因此,如果想用 C#来模拟C++/Java的generator<T>的行为,会比较简单:
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<int>GetNumbers
{
for(var i =0 ; i <5 ;++i)
yieldreturn i;
}
yieldreturn——“迭代器”发布于C# 2.0。
6. foreach循环,与 IEnumerable
是“鸭子类型”,有“操作空间”。
foreach不必定非要配合使用IEnumerable<T>类型,只要对象存在GetEnumerator方法即可:
voidMain
{
foreach(var i innew F)
{
Console.Write(i +", ");// 1, 2, 3, 4, 5,
}
}
class F
{
publicIEnumerator<int>GetEnumerator
{
for(var i =0 ; i <5 ;++i)
{
yieldreturn i;
}
}
}
另外,如果对象实现了 GetAsyncEnumerator,甚至也可以一样使用awaitforeach异步循环:
asyncTaskMain
{
awaitforeach(var i innew F)
{
Console.Write(i +", ");// 1, 2, 3, 4, 5,
}
}
class F
{
publicasyncIAsyncEnumerator<int>GetAsyncEnumerator
{
for(var i =0 ; i <5 ;++i)
{
awaitTask.Delay(1 );
yieldreturn i;
}
}
}
awaitforeach是C# 8.0随着异步流一起发布的,具体可见我之前写的《代码演示C#各版本新功能》。
7. using关键字,与 IDisposable接口
是,也不是。
引用类型和正常的值类型用using关键字,必须基于IDisposable接口。
但 refstruct和IAsyncDisposable就是另一个故事了,由于refstruct不允许随意移动,而引用类型——托管堆,会允许内存移动,所以refstruct不允许和引用类型产生任何关系,这个关系就包含继承接口——由于接口也是引用类型。
但释放资源的需求依然存在,怎么办,“鸭子类型”来了,可以手写一个 Dispose方法,不需要继承任何接口:
void S1Demo
{
using S1 s1 =new S1;
}
refstruct S1
{
publicvoidDispose
{
Console.WriteLine("正常释放");
}
}
同样的道理,如果用 IAsyncDisposable接口:
asyncTask S2Demo
{
awaitusing S2 s2 =new S2;
}
struct S2 :IAsyncDisposable
{
publicasyncValueTaskDisposeAsync
{
awaitTask.Delay(1 );
Console.WriteLine("Async释放");
}
}
8. T?,与 able
是“黑魔法”,只有 able<T>才能接受T?,able<T>作为一个值类型,它还能直接接受 值(正常值类型不允许接受 值)。
示例代码如下:
int? t1 =;
able<int> t2 =;
int t3 =;// Error CS0037: Cannot convert to 'int' because it is a non-able value type
生成代码如下( int?与able<int>完全一样,跳过了编译失败的代码):
IL_0000: nop
IL_0001: ldloca.s 0
IL_0003: initobj valuetype [System.Runtime]System.able`1<int32>
IL_0009: ldloca.s 1
IL_000b: initobj valuetype [System.Runtime]System.able`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>(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:
var range2 =new F[2. .];
Console.WriteLine(range2);// 2 -> -2
class F
{
publicintLength{get;set;}
publicIEnumerable<int>Slice(int start,intend)
{
yieldreturn start;
yieldreturnend;
}
}
生成代码如下:
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将添加“鸭子类型”的元祖—— TypeClasses,到时候“操作空间”肯定比目前更大,超级期待!



收藏了,感谢分享