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属性。

  • 先调用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

正常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,到时候“操作空间”肯定比现在更大,非常期待!

到此这篇关于C#中的9个“黑魔法”的文章就介绍到这了,更多相关c# 黑魔法内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • C#中的9个“黑魔法”

    我们知道C#是非常先进的语言,因为是它很有远见的"语法糖".这些"语法糖"有时过于好用,导致有人觉得它是C#编译器写死的东西,没有道理可讲的--有点像"黑魔法". 那么我们可以看看C#这些高级语言功能,是编译器写死的东西("黑魔法"),还是可以扩展(骚操作)的"鸭子类型". 我先列一个目录,大家可以对着这个目录试着下判断,说说是"黑魔法"(编译器写死),还是"鸭子类型"

  • iOS App开发中使cell高度自适应的黑魔法详解

    在使用 table view 的时侯经常会遇到这样的需求:table view 的 cell 中的内容是动态的,导致在开发的时候不知道一个 cell 的高度具体是多少,所以需要提供一个计算 cell 高度的算法,在每次加载到这个 cell 的时候计算出 cell 真正的高度. 在 iOS 8 之前 没有使用 Autolayout 的情况下,需要实现 table view delegate 的 tableView(tableView: UITableView, heightForRowAtInde

  • Python 中 Meta Classes详解

    接触过 Django 的同学都应该十分熟悉它的 ORM 系统.对于 python 新手而言,这是一项几乎可以被称作"黑科技"的特性:只要你在models.py中随便定义一个Model的子类,Django 便可以: 获取它的字段定义,并转换成表结构 读取Meta内部类,并转化成相应的配置信息.对于特殊的Model(如abstract.proxy),还要进行相应的转换 为没有定义objects的Model加上一个默认的Manager 开发之余,我也曾脑补过其背后的原理.曾经,我认为是这样的

  • python黑魔法之参数传递

    我们都听说,python世界里面,万物皆对象. 怎么说万物皆对象呢?最常见的: > class A: pass > a = A() 我们说a是一个对象. 那么既然是万物了,其实A也是对象.3 也是对象.True 也是对象."hello" 也是对象. > def Func(): pass o~yee, Func 也是对象. 那么对象之间的传递是如何呢?我们看看下面两个简单的例子: > a = 3 > b = a > b = 3 + 1 > pri

  • python黑魔法之编码转换

    我们在使用其他语言的库做编码转换时,对于无法理解的字符,通常的处理也只有两种(或三种): 抛异常 替换成替代字符 跳过 但是在复杂的现实世界中,由于各种不靠谱,我们处理的文本总会出现那么些不和谐因素,比如混合编码.在这种情况下,又回到了上面的处理办法. 那么问题来了,python有没有更好地办法呢? 答案是,有! python的编码转换流程实际上是两段式转换: source -> unicode -> dest 首先将字符串从原始编码转换成unicode.再将unicode转换成目标编码. 第

  • Python中使用装饰器和元编程实现结构体类实例

    Ruby中有一个很方便的Struct类,用来实现结构体.这样就不用费力的去定义一个完整的类来仅仅用作访问属性. 复制代码 代码如下: class Dog < Struct.new(:name, :age) end fred = Dog.new("fred", 5) printf "name:%s age:%d", fred.name, fred.age ##name:fred age:5 Python3.4中也可以这么干,但写法很累赘.其中包含self.nam

  • C#多线程编程中的锁系统(四):自旋锁

    目录 一:基础 二:自旋锁示例 三:SpinLock 四:继续SpinLock 五:总结 一:基础 内核锁:基于内核对象构造的锁机制,就是通常说的内核构造模式.用户模式构造和内核模式构造 优点:cpu利用最大化.它发现资源被锁住,请求就排队等候.线程切换到别处干活,直到接受到可用信号,线程再切回来继续处理请求. 缺点:托管代码->用户模式代码->内核代码损耗.线程上下文切换损耗. 在锁的时间比较短时,系统频繁忙于休眠.切换,是个很大的性能损耗. 自旋锁:原子操作+自循环.通常说的用户构造模式.

  • Python中property属性实例解析

    本文主要讲述的是对Python中property属性(特性)的理解,具体如下. 定义及作用: 在property类中,有三个成员方法和三个装饰器函数. 三个成员方法分别是:fget.fset.fdel,它们分别用来管理属性访问: 三个装饰器函数分别是:getter.setter.deleter,它们分别用来把三个同名的类方法装饰成property. fget方法用来管理类实例属性的获取,fset方法用来管理类实例属性的赋值,fdel方法用来管理类实例属性的删除: getter装饰器把一个自定义类

  • 增强Linux内核中访问控制安全的方法

    背景 前段时间,我们的项目组在帮客户解决一些操作系统安全领域的问题,涉及到windows,Linux,macOS三大操作系统平台.无论什么操作系统,本质上都是一个软件,任何软件在一开始设计的时候,都不能百分之百的满足人们的需求,所以操作系统也是一样,为了尽可能的满足人们需求,不得不提供一些供人们定制操作系统的机制.当然除了官方提供的一些机制,也有一些黑魔法,这些黑魔法不被推荐使用,但是有时候面对具体的业务场景,可以作为一个参考的思路. Linux中常见的拦截过滤 本文着重介绍Linux平台上常见

  • 在vue中使用jsx语法的使用方法

    什么是JSX? JSX就是Javascript和XML结合的一种格式.React发明了JSX,利用HTML语法来创建虚拟DOM.当遇到<,JSX就当HTML解析,遇到{就当JavaScript解析. 我为什么要在vue中用JSX? 想折腾一下呗,开玩笑.最开始是因为近期在学习react,在里面体验了一把jsx语法,发现也并没有别人说的很难受的感觉啊,于是就想尝试在vue中也试下,废话不多说,先来用代码来看下两者的区别吧. ps:vue中大部分场景是不需要用render函数的,还是用模板更简洁直观

随机推荐