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

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

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

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

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

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

  2. async/await,与 TaskValueTask类型;

  3. 表达式树,与 Expression<T>类型;

  4. 插值字符串,与 FormattableString类型;

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

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

  7. using关键字,与 IDisposable接口;

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

  9. 任意类型的 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,与 TaskValueTask类型

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

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

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

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

  1. 先调用 t.GetAwaiter()方法,取得等待器 a

  2. 调用 a.IsCompleted取得布尔类型 b

  3. 如果 b=true,则立即执行 a.GetResult(),取得运行结果;

  4. 如果 b=false,则看情况:

  5. 如果 a没实现 ICriticalNotifyCompletion,则执行 (aasINotifyCompletion).OnCompleted(action)

  6. 如果 a实现了 ICriticalNotifyCompletion,则执行 (aasICriticalNotifyCompletion).OnCompleted(action)

  7. 执行随后暂停, 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骚操作】

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/310785.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

LeetCode 872叶子相似的树-简单

请考虑一棵二叉树上所有的叶子&#xff0c;这些叶子的值按从左到右的顺序排列形成一个 叶值序列 。 举个例子&#xff0c;如上图所示&#xff0c;给定一棵叶值序列为 (6, 7, 4, 9, 8) 的树。 如果有两棵二叉树的叶值序列是相同&#xff0c;那么我们就认为它们是 叶相似 的。 …

.NET Core开发实战(第35课:MediatR:让领域事件处理更加优雅)--学习笔记

35 | MediatR&#xff1a;让领域事件处理更加优雅核心对象IMediatorINotificationINotificationHandler这两个与之前的 Request 的行为是不一样的&#xff0c;接下来看一下代码internal class MyEvent : INotification {public string EventName { get; set; } }internal class…

LeetCode 559N叉树的最大深度-简单

给定一个 N 叉树&#xff0c;找到其最大深度。 最大深度是指从根节点到最远叶子节点的最长路径上的节点总数。 N 叉树输入按层序遍历序列化表示&#xff0c;每组子节点由空值分隔&#xff08;请参见示例&#xff09;。 示例 1&#xff1a; 输入&#xff1a;root [1,null,3,…

android 5.0状态栏下载地址,Android沉浸式状态栏(5.0以上系统)

Android沉浸式状态栏(5.0以上系统)沉浸式状态栏可以分为两种:1.直接给状态栏设置颜色 (如下图:)这里写图片描述java代码形式:if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) {Window window activity.getWindow();window.addFlags(WindowManager.LayoutParams…

解析“60k”大佬的19道C#面试题(下)

解析“60k”大佬的19道C#面试题&#xff08;下&#xff09;在上篇中&#xff0c;我解析了前 10 道题目&#xff0c;本篇我将尝试解析后面剩下的所有题目。解析“60k”大佬的19道C#面试题&#xff08;上&#xff09;这些题目确实不怎么经常使用&#xff0c;因此在后文中&#xf…

android 卡顿代码定位,Android 性能优化实例:通过 TraceView 定位卡顿问题

8种机械键盘轴体对比本人程序员&#xff0c;要买一个写代码的键盘&#xff0c;请问红轴和茶轴怎么选&#xff1f;背景项目中使用了鸿洋大神的TreeView树状结构控件&#xff0c; 但是由于在主线程中使用了注解/反射来定位节点&#xff0c; 内容一多就有点卡顿。因此通过android …

DotNetCore三大Redis客户端对比和使用心得

前言稍微复杂一点的互联网项目&#xff0c;技术选型都会涉及Redis&#xff0c;.NetCore的生态越发完善&#xff0c;支持.NetCore的Redis客户端越来越多&#xff0c;下面三款常见的Redis客户端&#xff0c;相信大家平时或多或少用到一些&#xff0c;结合三款客户端的使用经历&am…

android elevation 白色,Android Elevation

简介&#xff1a;在Android API21&#xff0c;新添加了一个属性&#xff1a;android:elevation&#xff0c;用以在xml定义View的深度(高度)&#xff0c;也即z方向的值。除了elevation之外&#xff0c;类似于已有的translationX、translationY&#xff0c;也相对应地新增了一个t…

(译)创建.NET Core多租户应用程序-租户解析

介绍本系列博客文章探讨了如何在ASP.NET Core Web应用程序中实现多租户。这里有很多代码段&#xff0c;因此您可以按照自己的示例应用程序进行操作。在此过程的最后&#xff0c;没有对应的NuGet程序包&#xff0c;但这是一个很好的学习和练习。它涉及到框架的一些“核心”部分。…

【要闻】Kubernetes无用论诞生、Elasticsearch 7.6.2 发布

导读&#xff1a;本期要闻包含OpenStack网络如何给组织带来好处、Portworx CEO分享的如何让Kubernetes跑得快还不出错的秘籍等精彩内容。大数据要闻Elasticsearch 7.6.2 发布&#xff0c;分布式搜索和数据分析引擎Elasticsearch 7.6.2 发布了&#xff0c;Elasticsearch 是一个分…

玩转控件:对Dev中GridControl控件的封装和扩展

清明节清明时节雨纷纷路上行人欲断魂借问酒家何处有牧童遥指杏花村又是一年清明节至&#xff0c;细雨绵绵犹如泪光&#xff0c;树叶随风摆动....转眼间&#xff0c;一年又过去了三分之一&#xff0c;疫情的严峻让不少企业就跟清明时节的树叶一样&#xff0c;摇摇欲坠。裁员的裁…

创业5年,我有5点关于人的思考

点击蓝字关注&#xff0c;回复“职场进阶”获取职场进阶精品资料一份不知不觉创业五年了&#xff0c;也算一个屡战屡败、屡败屡战的创业老兵了。从第一次失败要靠吃抗抑郁的药&#xff0c;到现在理性的看待成败得失&#xff0c;不得不说&#xff0c;创业这条路对我还是有不小提…

C++实现具有[数组]相似特征的类DoubleSubscriptArray

#include <iostream> using namespace std;class DoubleSubscriptArray {public:DoubleSubscriptArray(int x, int y) {p new int *[x];//行 //申请行的空间for (int i 0; i < x; i) {p[i] new int [y];//每行的列申请空间}for (int i 0; i < x; i)for (int j …

Docker-HealthCheck指令探测ASP.NET Core容器健康状态

写在前面HealthCheck 不仅是对应用程序内运行情况、数据流通情况进行检查&#xff0c;还包括应用程序对外部服务或依赖资源的健康检查。健康检查通常是以暴露应用程序的HTTP端点的形式实施&#xff0c;可用于配置健康探测的的场景有 &#xff1a;容器或负载均衡器 探测应用状态…

ASP.NET Core分布式项目实战(课程介绍,MVP,瀑布与敏捷)--学习笔记

任务1&#xff1a;课程介绍课程目标&#xff1a;1、进一步理解 ASP.NET Core 授权认证框架、MVC 管道2、掌握 Oauth2&#xff0c;结合 Identity Sercer4 实现 OAuth2 和 OpenID Connect Server3、掌握 ASP.NET Core 与 Redis, MongoDB, RabitMQ, MySQL 配合使用4、理解 DDD&…

html坐标轴背景色,CSS 背景(css background)

CSS 背景-CSS background一、Css background背景语法 - TOPCSS背景基础知识CSS 背景这里指通过CSS对对象设置背景属性&#xff0c;如通过CSS设置背景各种样式。背景语法&#xff1a;background: background-color || background-image || background-repeat || background-…

LeetCode 965单值二叉树-简单

如果二叉树每个节点都具有相同的值&#xff0c;那么该二叉树就是单值二叉树。 只有给定的树是单值二叉树时&#xff0c;才返回 true&#xff1b;否则返回 false。 示例 1&#xff1a; 输入&#xff1a;[1,1,1,1,1,null,1] 输出&#xff1a;true 示例 2&#xff1a; 输入&…

使用EF.Core将同一模型映射到多个表

在 EntityFramework Core 中&#xff0c;我们可以使用属性或Fluent API来配置模型映射。有一天&#xff0c;我遇到了一个新的需求&#xff0c;有一个系统每天会生成大量数据&#xff0c;每天生成一个新的表存储数据。例如&#xff0c;数据库如下所示&#xff1a;所有表都具有相…

EntityFramework Core 3.x添加查询提示(NOLOCK)

前几天看到有博客园中有园友写了一篇关于添加NOLOCK查询提示的博文&#xff0c;这里呢&#xff0c;我将介绍另外一种添加查询提示的方法&#xff0c;此方式源于我看过源码后的实现&#xff0c;孰好孰歹&#xff0c;请自行判之&#xff0c;接下来我们一起来看看。在EntityFramew…

Xamarin.Forms客户端第一版

1. 功能简介1.1. 读取手机基本信息主要使用Xamarin.Essentials库获取设备基本信息&#xff0c;Xam.Plugin.DeviceInfo插件获取App Id&#xff0c;其实该插件也能获取设备基本信息。1.2. 读取手机联系人信息Android和iOS工程具体实现联系人读取服务&#xff0c;使用到Dependency…