C#中的9个“黑魔法”与“骚操作”

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

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

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

  1. LINQ操作,与IEnumerable<T>类型;
  2. async/await,与Task/ValueTask类型;
  3. 表达式树,与Expression<T>类型;
  4. 插值字符串,与FormattableString类型;
  5. yield return,与IEnumerable<T>类型;
  6. foreach循环,与IEnumerable<T>类型;
  7. using关键字,与IDisposable接口;
  8. T?,与Nullable<T>类型;
  9. 任意类型的Index/Range泛型操作。

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

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

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

如我上一篇博客提到,LINQ不一定要基于IEnumerable<T>,只需定定义一个类型,实现所需要的LINQ表达式即可,LINQselect关键字,会调用.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()的接口,该接口必须返回一个继承于INotifyCompletionICriticalNotifyCompletion的类,该类还需要实现GetResult()方法和IsComplete属性。

这一点在C#语言规范中有说明,调用await t本质会按如下顺序执行:

  1. 先调用t.GetAwaiter()方法,取得等待器a
  2. 调用a.IsCompleted取得布尔类型b
  3. 如果b=true,则立即执行a.GetResult(),取得运行结果;
  4. 如果b=false,则看情况:
    1. 如果a没实现ICriticalNotifyCompletion,则执行(a as INotifyCompletion).OnCompleted(action)
    2. 如果a实现了ICriticalNotifyCompletion,则执行(a as ICriticalNotifyCompletion).OnCompleted(action)
    3. 执行随后暂停,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>以外,还可以用于IEnumerableIEnumerator<T>IEnumerator

因此,如果想用C#来模拟C++/Javagenerator<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 foreachC# 8.0随着异步流一起发布的,具体可见我之前写的《代码演示C#各版本新功能》。

7. using关键字,与IDisposable接口

是,也不是。

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

ref structIAsyncDisposable就是另一个故事了,由于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个“黑魔法”的更多相关文章

  1. C#中的9个“黑魔法”与“骚操作”

    C#中的9个"黑魔法"与"骚操作" 我们知道C#是非常先进的语言,因为是它很有远见的"语法糖".这些"语法糖"有时过于好 ...

  2. 深度解析Java中的5个“黑魔法”

    现在的编程语言越来越复杂,尽管有大量的文档和书籍,这些学习资料仍然只能描述编程语言的冰山一角.而这些编程语言中的很多功能,可能被永远隐藏在黑暗角落.本文将为你解释其中5个Java中隐藏的秘密,可以称其 ...

  3. 经典文摘:饿了么的 PWA 升级实践(结合Vue.js)

    自 Vue.js 官方推特第一次公开到现在,我们就一直在进行着将饿了么移动端网站升级为 Progressive Web App 的工作.直到近日在 Google I/O 2017 上登台亮相,才终于算 ...

  4. Python 携程

    一.协程 1.又称微线程,纤程.英文名Coroutine.一句话说明什么是协程:协程是一种用户态的轻量级线程(相当于操作系统不知道它的存在,是用户控制的). 2.协程拥有自己的寄存器上下文和栈(代码的 ...

  5. transform与position:fixed的那些恩怨

    1. 前言 在写这篇文章之前,我理解的fixed元素是这样的:(摘自CSS布局基础) 固定定位与absolute定位类型类似,但它的相对移动的坐标是视图(屏幕内的网页窗口)本身.由于视图本身是固定的, ...

  6. transform与position:fixed的那些恩怨--摘抄

    1. 前言 在写这篇文章之前,我理解的fixed元素是这样的:(摘自CSS布局基础) 固定定位与absolute定位类型类似,但它的相对移动的坐标是视图(屏幕内的网页窗口)本身.由于视图本身是固定的, ...

  7. CTF 入门笔记

    站点:http://www.moctf.com/ web1:水题非常简单的题目,直接F12查看元素即可,在HTML代码中,flag被注释了. web2:水题 该题的核心 就是通过HTML代码对输入框进 ...

  8. Python开源框架

    info:更多Django信息url:https://www.oschina.net/p/djangodetail: Django 是 Python 编程语言驱动的一个开源模型-视图-控制器(MVC) ...

  9. 徒手撸一个 Spring Boot 中的 Starter ,解密自动化配置黑魔法!

    我们使用 Spring Boot,基本上都是沉醉在它 Stater 的方便之中.Starter 为我们带来了众多的自动化配置,有了这些自动化配置,我们可以不费吹灰之力就能搭建一个生产级开发环境,有的小 ...

  10. Python中的”黑魔法“与”骚操作“

    本文主要介绍Python的高级特性:列表推导式.迭代器和生成器,是面试中经常会被问到的特性.因为生成器实现了迭代器协议,可由列表推导式来生成,所有,这三个概念作为一章来介绍,是最便于大家理解的,现在看 ...

随机推荐

  1. 零基础学习人工智能—Python—Pytorch学习(九)

    前言 本文主要介绍卷积神经网络的使用的下半部分. 另外,上篇文章增加了一点代码注释,主要是解释(w-f+2p)/s+1这个公式的使用. 所以,要是这篇文章的代码看不太懂,可以翻一下上篇文章. 代码实现 ...

  2. Element-UI 中使用rules验证

    第一种:写在data中进行验证 <el-form>:代表这是一个表单 <el-form> -> ref:表单被引用时的名称,标识 <el-form> -> ...

  3. Linux | Ubuntu 16.04.4 通过docker安装单机FastDFS

    Ubuntu 16.04.4 通过docker安装单机fastdfs 前言 很久没有写技术播客了,这是一件很不应该的事情,做完了事情应该有沉淀的. 我先说一点前情提要,公司的fastdfs突然就挂了, ...

  4. python requests 报错 Caused by ProxyError ('Unable to connect to proxy', OSError('Tunnel connection failed: 403 Tunnel or SSL Forbidden'))

    背景:访问https接口,使用http代理 版本:requests: 2.31.0 从报错可以看出,是proxy相关的报错 调整代码,设定不使用代理,将http与https对应的proxy值置空即可( ...

  5. Pandas从入门到放弃

    公众号本文地址:https://mp.weixin.qq.com/s/mSkA5KvL1390Js8_1ZBiyw Pandas简介 Pandas是Panel data(面板数据)和Data anal ...

  6. MS SQL的ROUND函数用来数值的四舍五入

    MS SQL的ROUND函数用来数值的四舍五入 MS SQL要进行数值的四舍五入,有一好用的函数ROUND. 语法 ROUND ( numeric_expression , length [ ,fun ...

  7. WPF 保姆级教程怎么实现一个树形菜单

    先看一下效果吧: 我们直接通过改造一下原版的TreeView来实现上面这个效果 我们先创建一个普通的TreeView 代码很简单: <TreeView> <TreeViewItem ...

  8. 通过 maven 命令来查看 jar 包的引用关系

    通过 maven 命令来查看 jar 包的引用关系 1.可以通过maven命令来查看jar包的引用关系 mvn dependency:tree -Dverbose -Dincludes=org.cod ...

  9. JAVAEE——MySQL安装

    一.下载MySQL(两种方式) 1.官网下载 官网下载地址:https://www.mysql.com/downloads   2.点击下载(版本:mysql-8.0.28-winx64) 链接:ht ...

  10. Tomcat——idea集成本地Tomcat

    IDEA 集成本地Tomcat 添加配置      添加本地Tomcat服务器      配置本地Tomcat路径      部署项目             在 webapp 中添加一个简单的页面作 ...