C#中的9个“黑魔法”与“骚操作”
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>
类型
不是“黑魔法”,是“鸭子类型”。
LINQ
是 C# 3.0
发布的新功能,可以非常便利地操作数据。现在 12
年过去了,虽然有些功能有待增强,但相比其它语言还是方便许多。
如我上一篇博客提到, LINQ
不一定要基于 IEnumerable<T>
,只需定定义一个类型,实现所需要的 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 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()
的接口,该接口必须返回一个继承于 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()
:
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. yieldreturn
,与 IEnumerable<T>
类型;
是“黑魔法”,但有补充说明。
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)yield return i;
}
yieldreturn
——“迭代器”发布于 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()
,甚至也可以一样使用 awaitforeach
异步循环:
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;}}
}
awaitforeach
是 C# 8.0
随着 异步流
一起发布的,具体可见我之前写的《代码演示C#各版本新功能》。
7. using
关键字,与 IDisposable
接口
是,也不是。
引用类型
和正常的 值类型
用 using
关键字,必须基于 IDisposable
接口。
但 refstruct
和 IAsyncDisposable
就是另一个故事了,由于 refstruct
不允许随便移动,而引用类型——托管堆,会允许内存移动,所以 refstruct
不允许和 引用类型
产生任何关系,这个关系就包含继承 接口
——因为 接口
也是 引用类型
。
但释放资源的需求依然存在,怎么办,“鸭子类型”来了,可以手写一个 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: nop
IL_0001: ldloca.s 0
IL_0003: initobj valuetype [System.Runtime]System.Nullable`1<int32>
IL_0009: ldloca.s 1
IL_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
将添加“鸭子类型”的元祖——TypeClasses
,到时候“操作空间”肯定比现在更大,非常期待!
喜欢的朋友请关注我的微信公众号:【DotNet骚操作】
C#中的9个“黑魔法”与“骚操作”相关推荐
- Element-UI中关于table表格的那些骚操作
最近的项目中使用到element-ui组件库,由于做的是后台管理系统,所以经常需要操作表格,编辑样式的过程中遇到一些问题,官网针对table给出了很多的api,自己可以自定义,基本能满足产品需求,但是 ...
- elementui表格复制_Element-UI中关于table表格的那些骚操作
最近的项目中使用到element-ui组件库,由于做的是后台管理系统,所以经常需要操作表格,编辑样式的过程中遇到一些问题,官网针对table给出了很多的api,自己可以自定义,基本能满足产品需求,但是 ...
- Python中浅拷贝与深拷贝的骚操作
作者:缪斯mius@阿里云Python训练营 博客地址:https://blog.csdn.net/m0_37759382/article/details/108489108 [例子]浅拷贝与深拷贝中 ...
- Python 骚操作!如何让自己在斗图中立于不败之地?
点击⬆️"小詹学Python",选择"星标公众号" 福利干货,第一时间送达! 本文授权转载自AirPython,禁二次转载 阅读文本大概需要 6 分钟. 1 目 ...
- Python 中让你相见恨晚的 20 个骚操作
今天和大家分享 20 个 Python 编程中新手必会的"骚操作",使用的频率超高!记得点赞,收藏哦!话不多说,进入正题! 1.列表推导式 使用列表推导式创建一个列表. >& ...
- java putifabsent_java8中Map的一些骚操作总结
一 前言 本篇内容是关于 map 新特性的一些方法使用上的介绍,如果有不足之处欢迎补充!! 二 map新特性 关于以下函数式编程的函数的计算知识追寻者都使用 简单字符串代替了,参数无非就是Key,va ...
- lisp 提取字符串中的數字_Redis 数据结构之字符串的那些骚操作
Redis 字符串底层用的是 sds 结构,该结构同 c 语言的字符串相比,其优点是可以节省内存分配的次数,还可以... 这样写是不是读起来很无聊?这些都是别人咀嚼过后,经过一轮两轮三轮的再次咀嚼,吐 ...
- python各种文件_Python中对 文件 的各种骚操作
Python中对 文件 的各种骚操作 python中对文件.文件夹(文件操作函数)的操作需要涉及到os模块和shutil模块. 得到当前工作目录,即当前Python脚本工作的目录路径: os.getc ...
- leetcode中的一些骚操作
记录刷leetcode中的一些意想不到的用法 14. 最长公共前缀 题目:编写一个函数来查找字符串数组中的最长公共前缀.如果不存在公共前缀,返回空字符串 "". 思路:这道题lee ...
最新文章
- 用计算机写试卷反思,100分试卷反思怎么写
- Java经典面试题:一个线程两次调用start()方法会出现什么情况?
- linux的fork语句,Linux C/C++——fork()函数基础
- 【基础】jquery全选、反选、全不选代码
- Linux 驱动开发之内核模块开发(四)—— 符号表的导出
- 设计模式学习笔记——模板(Template)模式
- 安卓开发:用ImageView放上图片后上下有间隙
- 别再被三次握手和四次挥手所支配!把TCP这玩意儿给你掰开了说
- PETERSON互斥算法解析
- treeview的checkbox展开节点
- Linux下安装mysql(yum、二进制包、源码包)
- 点到直线的距离直线的交点及夹角
- php市场调查问卷模板,市场调查问卷范文
- 聚合支付系统业务分析
- ADS,AXD基本使用说明
- P2676 [USACO07DEC]Bookshelf B
- 谈谈Web端性能测试
- pytorch基于GAN生成对抗网络的数据集扩充
- STM32L4的待机模式闹钟唤醒方法
- C/C++语言入门——鸡兔同笼问题
热门文章
- 数据结构(java语言描述)顺序栈的使用
- 构造不可变类及其优点
- 我到底要选择一种什么样的生活方式,度过这一辈子呢:人生自由与职业发展方向(下)...
- Mac OS使用技巧之三:发射无线网络信号的方法
- facebook 邀请好友_如何在Facebook上与某人解除好友
- 如何重新打开Windows防火墙提示?
- xbox360链接pc_如何将实时电视从Xbox One流式传输到Windows PC,iPhone或Android Phone
- 上周面试回来后写的Java面试总结,想进BAT必看
- 用Cocos2dx开发棋牌游戏的观点解析
- SpringBoot获取ApplicationContext