C#中的9个“黑魔法”与“骚操作”
C#中的9个“黑魔法”与“骚操作”
我们知道C#
是非常先进的语言,因为是它很有远见的“语法糖”。这些“语法糖”有时过于好用,导致有人觉得它是C#
编译器写死的东西,没有道理可讲的——有点像“黑魔法”。
那么我们可以看看C#
这些高级语言功能,是编译器写死的东西(“黑魔法”),还是可以扩展(骚操作)的“鸭子类型”。
我先列一个目录,大家可以对着这个目录试着下判断,说说是“黑魔法”(编译器写死),还是“鸭子类型”(可以自定义“骚操作”):
LINQ
操作,与IEnumerable<T>
类型;async/await
,与Task
/ValueTask
类型;- 表达式树,与
Expression<T>
类型; - 插值字符串,与
FormattableString
类型; yield return
,与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#
语言规范中有说明,调用await t
本质会按如下顺序执行:
- 先调用
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#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. yield return
,与IEnumerable<T>
类型;
是“黑魔法”,但有补充说明。
yield return
除了用于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;
}
yield return
——“迭代器”发布于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()
,甚至也可以一样使用await foreach
异步循环:
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;
}
}
}
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<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
将添加“鸭子类型”的元祖——Type Classes
,到时候“操作空间”肯定比现在更大,非常期待!
喜欢的朋友请关注我的微信公众号:【DotNet骚操作】
C#中的9个“黑魔法”与“骚操作”的更多相关文章
- Python中的”黑魔法“与”骚操作“
本文主要介绍Python的高级特性:列表推导式.迭代器和生成器,是面试中经常会被问到的特性.因为生成器实现了迭代器协议,可由列表推导式来生成,所有,这三个概念作为一章来介绍,是最便于大家理解的,现在看 ...
- vue开发中的"骚操作"
前言 在与同事协作开发的过程中,见识到了不少"骚操作".因为之前都没用过,所以我愿称之为"高级技巧"! Vue.extend 在交互过程中,有个需求就是点击图标 ...
- Java 12 骚操作, String居然还能这样玩!
Java 13 都快要来了,12必须跟栈长学起! Java 13 即将发布,新特性必须抢先看! 栈长之前在Java技术栈微信公众号分享过<Java 11 已发布,String 还能这样玩!> ...
- Python中对 文件 的各种骚操作
Python中对 文件 的各种骚操作 python中对文件.文件夹(文件操作函数)的操作需要涉及到os模块和shutil模块. 得到当前工作目录,即当前Python脚本工作的目录路径: os.getc ...
- 快来!我从源码中学习到了一招Dubbo的骚操作!
荒腔走板 大家好,我是 why,欢迎来到我连续周更优质原创文章的第 55 篇. 老规矩,先来一个简短的荒腔走板,给冰冷的技术文注入一丝色彩. 魔幻的 2020 年的上半年过去了,很多人都在朋友圈和上半 ...
- 论减少代码中return语句的骚操作
一.写作背景 最近组内在推行checkstyle代码规范的检测,关于checkstyle的介绍可以参考:https://checkstyle.sourceforge.io, 在按照checkstyle ...
- Guava中这些Map的骚操作,让我的代码量减少了50%
原创:微信公众号 码农参上,欢迎分享,转载请保留出处. Guava是google公司开发的一款Java类库扩展工具包,内含了丰富的API,涵盖了集合.缓存.并发.I/O等多个方面.使用这些API一方面 ...
- Typescript骚操作,在TS里面直接插入HTML
Typescript骚操作,在TS里面直接插入HTML,还有语法提示 先给大家看一个图 因为我不喜欢用很重的框架,主要是并非专业UI,但是偶尔会用到,还是觉得直接element组装受不了,想想能在ts ...
- 闪电侠 Netty 小册里的骚操作
前言 即使这是一本小册,但基于"不提笔不读书"的理念,仍然有必要总结一下.此小册对于那些"硬杠 Netty 源码 却不曾在千万级生产环境上使用实操"的用户非常有 ...
随机推荐
- Android编程权威指南(第2版)--第16章 使用intent拍照 挑战练习
16.7挑战练习:优化照片显示 新建dialog_photo.xml 1234567891011121314 <?xml version="1.0" encoding=&qu ...
- CSS——NO.3(CSS选择器)
*/ * Copyright (c) 2016,烟台大学计算机与控制工程学院 * All rights reserved. * 文件名:text.cpp * 作者:常轩 * 微信公众号:Worldhe ...
- 自然语言处理NLTK之入门
环境:window10 + python3 一.安装NLTK pip install nltk # 或者 PyCharm --> File --> Settings --> Proj ...
- audioContext.decodeAudioData 返回null 错误
此问题并不是100%出现.没想到国外大神已经有处理此问题的经验 原贴地址: https://stackoverflow.com/questions/10365335/decodeaudiodata-r ...
- 使用SpringMVC实现文件上传和下载
文件上传 第一步,加入jar包: commons-fileupload-1.3.1.jar commons-io-2.4.jar 第二步,在SpringMVC配置文件中配置CommonsMultipa ...
- IDEA激活码(可用2100年,亲测有效)
三步骤: 1.下载rar包 2.将rar中bin包内容替换IDEA安装目录下bin内容,然后编辑idea.exe.vmoptions和idea64.exe.vmoptions文件,编辑内容一样 将最后 ...
- python社区要放弃了pip?版本信息里带警告很不寻常哦
pip是python的一个包管理器. 今天再查询Pip3 -V 时,除了正常的版本信息外,多了几行信息 WARNING: pip is being invoked by an old script w ...
- 大数据学习之scala-环境搭建
scala 下载网站 https://www.scala-lang.org/download/ 安装scala要先安装java,并且配置java环境,官网也有说明 不过国内的网站下载不下来可以访问: ...
- mimtproxy的使用(windows)
1.安装 pip3 install mitmproxy 或者下载安装指定版本:https://mitmproxy.org/downloads/ 2.配置证书 对于mitmproxy来说,如果想要截获H ...
- ASP.net MVC 构建layui管理后台(构造基础仓储)<1>
本文章为ASP.net MVC 构建layui管理后台,第一篇. 使用EF+ado.net 实体数据模型模式进行底层的数据库连接. 在项目添加一个类库Model 在类库Model上添加一个ado.ne ...