zoukankan      html  css  js  c++  java
  • C#中的9个“黑魔法”

    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骚操作】

    DotNet骚操作

  • 相关阅读:
    Codeforces Global Round 11 E Xum
    【NFLSPC #2】Polynomial
    【SHOI2015】脑洞治疗仪 题解 (线段树)
    CDQ分治与整体二分 学习笔记
    二维树状数组 学习笔记
    博弈论 学习笔记
    【JSOI2007】文本生成器 题解(AC自动机+动态规划)
    【NOI2018】归程 题解(kruskal重构树+最短路)
    【NOI2017】游戏 题解(2-SAT+缩点)
    【BZOJ4398】福慧双修 题解(建图优化)
  • 原文地址:https://www.cnblogs.com/sdflysha/p/20200331-black-magic-in-csharp.html
Copyright © 2011-2022 走看看