闲适人生 發表於 2020-3-31 20:51:00

C#中的9个“黑魔法”

<h1 id="c中的9个黑魔法与骚操作">C#中的9个“黑魔法”与“骚操作”</h1>
<p>我们知道<code>C#</code>是非常先进的语言,因为是它很有远见的“语法糖”。这些“语法糖”有时<strong>过于好用</strong>,导致有人觉得它是<code>C#</code>编译器写死的东西,没有道理可讲的——有点像“黑魔法”。</p>
<p>那么我们可以看看<code>C#</code>这些<strong>高级</strong>语言功能,是编译器写死的东西(“黑魔法”),还是可以扩展(骚操作)的“鸭子类型”。</p>
<p>我先列一个目录,大家可以对着这个目录试着下判断,说说是“黑魔法”(编译器写死),还是“鸭子类型”(可以自定义“骚操作”):</p>
<ol>
<li><code>LINQ</code>操作,与<code>IEnumerable&lt;T&gt;</code>类型;</li>
<li><code>async/await</code>,与<code>Task</code>/<code>ValueTask</code>类型;</li>
<li>表达式树,与<code>Expression&lt;T&gt;</code>类型;</li>
<li>插值字符串,与<code>FormattableString</code>类型;</li>
<li><code>yield return</code>,与<code>IEnumerable&lt;T&gt;</code>类型;</li>
<li><code>foreach</code>循环,与<code>IEnumerable&lt;T&gt;</code>类型;</li>
<li><code>using</code>关键字,与<code>IDisposable</code>接口;</li>
<li><code>T?</code>,与<code>Nullable&lt;T&gt;</code>类型;</li>
<li>任意类型的<code>Index/Range</code>泛型操作。</li>
</ol>
<h2 id="1-linq操作与ienumerablet类型">1. <code>LINQ</code>操作,与<code>IEnumerable&lt;T&gt;</code>类型</h2>
<p>不是“黑魔法”,是“鸭子类型”。</p>
<p><code>LINQ</code>是<code>C# 3.0</code>发布的新功能,可以非常便利地操作数据。现在<code>12</code>年过去了,虽然有些功能有待增强,但相比其它语言还是方便许多。</p>
<p>如我上一篇博客提到,<code>LINQ</code>不一定要基于<code>IEnumerable&lt;T&gt;</code>,只需定定义一个类型,实现所需要的<code>LINQ</code>表达式即可,<code>LINQ</code>的<code>select</code>关键字,会调用<code>.Select</code>方法,可以用如下的“骚操作”,实现“移花接木”的效果:</p>
<pre><code class="language-csharp">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&lt;int&gt; Select&lt;R&gt;(Func&lt;int, R&gt; t)
    {
      for (var i = 0; i &lt; 5; ++i)
      {
            yield return i;
      }
    }
}
</code></pre>
<h2 id="2-asyncawait与taskvaluetask类型">2. <code>async/await</code>,与<code>Task</code>/<code>ValueTask</code>类型</h2>
<p>不是“黑魔法”,是“鸭子类型”。</p>
<p><code>async/await</code>发布于<code>C# 5.0</code>,可以非常便利地做异步编程,其本质是状态机。</p>
<p><code>async/await</code>的本质是会寻找类型下一个名字叫<code>GetAwaiter()</code>的接口,该接口必须返回一个继承于<code>INotifyCompletion</code>或<code>ICriticalNotifyCompletion</code>的类,该类还需要实现<code>GetResult()</code>方法和<code>IsComplete</code>属性。</p>
<p>这一点在<code>C#</code>语言规范中有说明,调用<code>await t</code>本质会按如下顺序执行:</p>
<ol>
<li>先调用<code>t.GetAwaiter()</code>方法,取得等待器<code>a</code>;</li>
<li>调用<code>a.IsCompleted</code>取得布尔类型<code>b</code>;</li>
<li>如果<code>b=true</code>,则立即执行<code>a.GetResult()</code>,取得运行结果;</li>
<li>如果<code>b=false</code>,则看情况:
<ol>
<li>如果<code>a</code>没实现<code>ICriticalNotifyCompletion</code>,则执行<code>(a as INotifyCompletion).OnCompleted(action)</code></li>
<li>如果<code>a</code>实现了<code>ICriticalNotifyCompletion</code>,则执行<code>(a as ICriticalNotifyCompletion).OnCompleted(action)</code></li>
<li>执行随后暂停,<code>OnCompleted</code>完成后重新回到状态机;</li>
</ol>
</li>
</ol>
<p>有兴趣的可以访问<code>Github</code>具体规范说明:https://github.com/dotnet/csharplang/blob/master/spec/expressions.md#runtime-evaluation-of-await-expressions</p>
<p>正常<code>Task.Delay()</code>是基于<code>线程池计时器</code>的,可以用如下“骚操作”,来实现一个单线程的<code>TaskEx.Delay()</code>:</p>
<pre><code class="language-csharp">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 &lt;= 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) =&gt; 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() =&gt; this;
   
    public void OnCompleted(Action continuation)
    {
      Tick += Check;
      
      void Check()
      {
            if (Util.ElapsedTime.TotalMilliseconds - _start &gt; _ms)
            {
                continuation();
                Tick -= Check;
            }
      }
    }

    public void GetResult() {}
   
    public bool IsCompleted =&gt; false;
}
</code></pre>
<p>运行效果如下:</p>
<pre><code>执行开始
第1次,时间:17:38:03 - 线程号:1
第2次,时间:17:38:04 - 线程号:1
第3次,时间:17:38:05 - 线程号:1
第4次,时间:17:38:06 - 线程号:1
执行完成
</code></pre>
<blockquote>
<p>注意不需要非得使用<code>TaskCompletionSource&lt;T&gt;</code>才能创建定定义的<code>async/await</code>。</p>
</blockquote>
<h2 id="3-表达式树与expressiont类型">3. 表达式树,与<code>Expression&lt;T&gt;</code>类型</h2>
<p>是“黑魔法”,没有“操作空间”,只有当类型是<code>Expression&lt;T&gt;</code>时,才会创建为表达式树。</p>
<p><code>表达式树</code>是<code>C# 3.0</code>随着<code>LINQ</code>一起发布,是有远见的“黑魔法”。</p>
<p>如以下代码:</p>
<pre><code class="language-csharp">Expression&lt;Func&lt;int&gt;&gt; g3 = () =&gt; 3;
</code></pre>
<p>会被编译器翻译为:</p>
<pre><code class="language-csharp">Expression&lt;Func&lt;int&gt;&gt; g3 = Expression.Lambda&lt;Func&lt;int&gt;&gt;(
    Expression.Constant(3, typeof(int)),
    Array.Empty&lt;ParameterExpression&gt;());
</code></pre>
<h2 id="4-插值字符串与formattablestring类型">4. 插值字符串,与<code>FormattableString</code>类型</h2>
<p>是“黑魔法”,没有“操作空间”。</p>
<p><code>插值字符串</code>发布于<code>C# 6.0</code>,在此之前许多语言都提供了类似的功能。</p>
<p>只有当类型是<code>FormattableString</code>,才会产生不一样的编译结果,如以下代码:</p>
<pre><code class="language-csharp">FormattableString x1 = $"Hello {42}";
string x2 = $"Hello {42}";
</code></pre>
<p>编译器生成结果如下:</p>
<pre><code class="language-csharp">FormattableString x1 = FormattableStringFactory.Create("Hello {0}", 42);
string x2 = string.Format("Hello {0}", 42);
</code></pre>
<p>注意其本质是调用了<code>FormattableStringFactory.Create</code>来创建一个类型。</p>
<h2 id="5-yield-return与ienumerablet类型">5. <code>yield return</code>,与<code>IEnumerable&lt;T&gt;</code>类型;</h2>
<p>是“黑魔法”,但有补充说明。</p>
<p><code>yield return</code>除了用于<code>IEnumerable&lt;T&gt;</code>以外,还可以用于<code>IEnumerable</code>、<code>IEnumerator&lt;T&gt;</code>、<code>IEnumerator</code>。</p>
<p>因此,如果想用<code>C#</code>来模拟<code>C++</code>/<code>Java</code>的<code>generator&lt;T&gt;</code>的行为,会比较简单:</p>
<pre><code class="language-csharp">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&lt;int&gt; GetNumbers()
{
    for (var i = 0; i &lt; 5; ++i)
      yield return i;
}
</code></pre>
<p><code>yield return</code>——“迭代器”发布于<code>C# 2.0</code>。</p>
<h2 id="6-foreach循环与ienumerablet类型">6. <code>foreach</code>循环,与<code>IEnumerable&lt;T&gt;</code>类型</h2>
<p>是“鸭子类型”,有“操作空间”。</p>
<p><code>foreach</code>不一定非要配合使用<code>IEnumerable&lt;T&gt;</code>类型,只要对象存在<code>GetEnumerator()</code>方法即可:</p>
<pre><code class="language-csharp">void Main()
{
    foreach (var i in new F())
    {
      Console.Write(i + ", "); // 1, 2, 3, 4, 5,
    }
}

class F
{
    public IEnumerator&lt;int&gt; GetEnumerator()
    {
      for (var i = 0; i &lt; 5; ++i)
      {
            yield return i;
      }
    }
}
</code></pre>
<p>另外,如果对象实现了<code>GetAsyncEnumerator()</code>,甚至也可以一样使用<code>await foreach</code>异步循环:</p>
<pre><code class="language-csharp">async Task Main()
{
    await foreach (var i in new F())
    {
      Console.Write(i + ", "); // 1, 2, 3, 4, 5,
    }
}

class F
{
    public async IAsyncEnumerator&lt;int&gt; GetAsyncEnumerator()
    {
      for (var i = 0; i &lt; 5; ++i)
      {
            await Task.Delay(1);
            yield return i;
      }
    }
}
</code></pre>
<p><code>await foreach</code>是<code>C# 8.0</code>随着<code>异步流</code>一起发布的,具体可见我之前写的《代码演示C#各版本新功能》。</p>
<h2 id="7-using关键字与idisposable接口">7. <code>using</code>关键字,与<code>IDisposable</code>接口</h2>
<p>是,也不是。</p>
<p><code>引用类型</code>和正常的<code>值类型</code>用<code>using</code>关键字,<strong>必须</strong>基于<code>IDisposable</code>接口。</p>
<p>但<code>ref struct</code>和<code>IAsyncDisposable</code>就是另一个故事了,由于<code>ref struct</code>不允许随便移动,而引用类型——托管堆,会允许内存移动,所以<code>ref struct</code>不允许和<code>引用类型</code>产生任何关系,这个关系就包含继承<code>接口</code>——因为<code>接口</code>也是<code>引用类型</code>。</p>
<p>但释放资源的需求依然存在,怎么办,“鸭子类型”来了,可以手写一个<code>Dispose()</code>方法,不需要继承任何接口:</p>
<pre><code class="language-csharp">void S1Demo()
{
    using S1 s1 = new S1();
}

ref struct S1
{
    public void Dispose()
    {
      Console.WriteLine("正常释放");
    }
}
</code></pre>
<p>同样的道理,如果用<code>IAsyncDisposable</code>接口:</p>
<pre><code class="language-csharp">async Task S2Demo()
{
    await using S2 s2 = new S2();
}

struct S2 : IAsyncDisposable
{
    public async ValueTask DisposeAsync()
    {
      await Task.Delay(1);
      Console.WriteLine("Async释放");
    }
}
</code></pre>
<h2 id="8-t与nullablet类型">8. <code>T?</code>,与<code>Nullable&lt;T&gt;</code>类型</h2>
<p>是“黑魔法”,只有<code>Nullable&lt;T&gt;</code>才能接受<code>T?</code>,<code>Nullable&lt;T&gt;</code>作为一个<code>值类型</code>,它还能直接接受<code>null</code>值(正常<code>值类型</code>不允许接受<code>null</code>值)。</p>
<p>示例代码如下:</p>
<pre><code class="language-csharp">int? t1 = null;
Nullable&lt;int&gt; t2 = null;
int t3 = null; // Error CS0037: Cannot convert null to 'int' because it is a non-nullable value type
</code></pre>
<p>生成代码如下(<code>int?</code>与<code>Nullable&lt;int&gt;</code>完全一样,跳过了编译失败的代码):</p>
<pre><code class="language-cil">IL_0000: nop
IL_0001: ldloca.s 0
IL_0003: initobj valuetype System.Nullable`1&lt;int32&gt;
IL_0009: ldloca.s 1
IL_000b: initobj valuetype System.Nullable`1&lt;int32&gt;
IL_0011: ret
</code></pre>
<h2 id="9-任意类型的indexrange泛型操作">9. 任意类型的<code>Index/Range</code>泛型操作</h2>
<p>有“黑魔法”,也有“鸭子类型”——存在操作空间。</p>
<p><code>Index/Range</code>发布于<code>C# 8.0</code>,可以像<code>Python</code>那样方便地操作索引位置、取出对应值。以前需要调用<code>Substring</code>等复杂操作的,现在非常简单。</p>
<pre><code class="language-csharp">string url = "https://www.super-cool.com/product/7705a33a-4d2c-455d-a42c-c95e6ac8ee99/summary";
string productId = url;
Console.WriteLine(productId);
</code></pre>
<p>生成代码如下:</p>
<pre><code class="language-csharp">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
</code></pre>
<p>可见,<code>C#</code>编译器忽略了<code>Index/Range</code>,直接翻译为调用<code>Substring</code>了。</p>
<p>但数组又不同:</p>
<pre><code class="language-csharp">var range = new[] { 1, 2, 3, 4, 5 };
Console.WriteLine(string.Join(", ", range)); // 2, 3
</code></pre>
<p>生成代码如下:</p>
<pre><code class="language-csharp">int[] range = RuntimeHelpers.GetSubArray&lt;int&gt;(new int
{
    1,
    2,
    3,
    4,
    5
}, new Range(1, 3));
Console.WriteLine(string.Join&lt;int&gt;(", ", range));
</code></pre>
<p>可见它确实创建了<code>Range</code>类型,然后调用了<code>RuntimeHelpers.GetSubArray&lt;int&gt;</code>,完全属于“黑魔法”。</p>
<p>但它同时也是“鸭子”类型,只要代码中实现了<code>Length</code>属性和<code>Slice(int, int)</code>方法,即可调用<code>Index/Range</code>:</p>
<pre><code class="language-csharp">var range2 = new F();
Console.WriteLine(range2); // 2 -&gt; -2

class F
{
    public int Length { get; set; }
    public IEnumerable&lt;int&gt; Slice(int start, int end)
    {
      yield return start;
      yield return end;
    }
}
</code></pre>
<p>生成代码如下:</p>
<pre><code class="language-csharp">F f = new F();
int length2 = f.Length;
length = 2;
num = length2 - length;
string range2 = f.Slice(length, num);
Console.WriteLine(range2);
</code></pre>
<h1 id="总结">总结</h1>
<p>如上所见,<code>C#</code>的“黑魔法”确实挺多,但“鸭子类型”也有很多,“骚操作”的“操作空间”很大。</p>
<blockquote>
<p>据传<code>C# 9.0</code>将添加“鸭子类型”的元祖——<code>Type Classes</code>,到时候“操作空间”肯定比现在更大,非常期待!</p>
</blockquote>
<p>喜欢的朋友请关注我的微信公众号:【DotNet骚操作】</p>
<p><img src="https://img2018.cnblogs.com/blog/233608/201908/233608-20190825165420518-990227633.jpg" alt="DotNet骚操作" loading="lazy"></p><br><br>
来源:https://www.cnblogs.com/sdcb/p/20200331-black-magic-in-csharp.html
頁: [1]
查看完整版本: C#中的9个“黑魔法”