不想被你记住 發表於 2024-7-27 01:16:00

提高 C# 的生产力:C# 13 更新完全指南

<h2 id="前言">前言</h2>
<p>预计在 2024 年 11 月,C# 13 将与 .NET 9 一起正式发布。今年的 C# 更新主要集中在 <code>ref struct</code> 上进行了许多改进,并添加了许多有助于进一步提高生产力的便利功能。</p>
<p>本文将介绍预计将在 C# 13 中添加的功能。</p>
<p>注意:目前 C# 13 还未正式发布,因此以下内容可能会发生变化。</p>
<h2 id="在迭代器和异步方法中使用-ref-和-ref-struct">在迭代器和异步方法中使用 <code>ref</code> 和 <code>ref struct</code></h2>
<p>在使用 C# 进行编程时,你是否经常使用 <code>ref</code> 变量和 <code>Span</code> 等 <code>ref struct</code> 类型?然而,这些不能在迭代器和异步方法中使用,于是必须使用局部函数等来避免在迭代器和异步方法中直接使用 <code>ref</code> 变量 <code>ref struct</code> 类型,这非常不方便。</p>
<p>这个缺点在 C# 13 中得到了改善,现在迭代器和异步方法也可以使用 <code>ref</code> 和 <code>ref struct</code> 了!</p>
<p>在迭代器中使用 <code>ref</code> 和 <code>ref struct</code> 的例子:</p>
<pre><code class="language-csharp">IEnumerable&lt;float&gt; GetFloatNumberFromIntArray(int[] array)
{
    for (int i = 0; i &lt; array.Length; i++)
    {
      Span&lt;int&gt; span = array.AsSpan();
      // 进行一些处理...
      ref float v = ref Unsafe.As&lt;int, float&gt;(ref array);
      yield return v;
    }
}
</code></pre>
<p>在异步方法中使用 <code>ref struct</code> 的例子:</p>
<pre><code class="language-csharp">async Task ProcessDataAsync(int[] array)
{
    Span&lt;int&gt; span = array.AsSpan();
    // 进行一些处理...
    ref int element = ref span;
    element++;
    await Task.Yield();
}
</code></pre>
<p>为了展示功能,我使用了不适当且含糊不清的“一些处理”,不过重要的是现在可以使用 <code>ref</code> 和 <code>ref struct</code> 了!</p>
<p>但是,有一点需要注意,<code>ref</code> 变量和 <code>ref struct</code> 类型的变量不能超出 <code>yield</code> 和 <code>await</code> 的边界使用。例如,以下示例将导致编译错误。</p>
<pre><code class="language-csharp">async Task ProcessDataAsync(int[] array)
{
    Span&lt;int&gt; span = array.AsSpan();
    // 进行一些处理...
    ref int element = ref span;
    element++;
    await Task.Yield();
    element++; // 错误:对 element 的访问超出了 await 的边界
}
</code></pre>
<p>虽然我们已经说到这里,但我想可能有人会疑惑,到底 <code>ref</code> 和 <code>ref struct</code> 是什么,所以我稍微解释一下。</p>
<p>在 C# 中,可以使用 <code>ref</code> 来获取变量的引用。这样,就可以通过引用来更改原始变量。以下是一个例子:</p>
<pre><code class="language-csharp">void Swap(ref int a, ref int b) // ref 表示引用
{
    int temp = a;
    a = b;
    b = temp; // 到这里,a 和 b 已经交换了
}

int x = 1;
int y = 2;
Swap(ref x, ref y); // 获取 x 和 y 的引用,调用 Swap 来交换 x 和 y
</code></pre>
<p>另一方面,<code>ref struct</code> 是用于定义只能存在于堆栈上的值类型的。这是为了避免垃圾收集的开销。然而,由于 <code>ref struct</code> 只能存在于堆栈上,所以在 C# 13 之前,它不能在迭代器和异步方法等地方使用。</p>
<p>顺便一提,<code>ref struct</code> 之所以带有 <code>ref</code>,是因为 <code>ref struct</code> 的实例只能存在于堆栈上,其遵循的生命周期规则与 <code>ref</code> 变量相同。</p>
<h2 id="allows-ref-struct-泛型约束"><code>allows ref struct</code> 泛型约束</h2>
<p>在以前,<code>ref struct</code> 不能作为泛型类型参数使用,因此,考虑到代码的可重用性,引入了泛型,但最终 <code>ref struct</code> 不能使用,必须为 <code>Span</code> 或 <code>ReadOnlySpan</code> 重新编写相同的处理,于是就很麻烦。</p>
<p>在 C# 13 中,泛型类型也可以使用 <code>ref struct</code> 了:</p>
<pre><code class="language-csharp">using System;
using System.Numerics;

Process(, Sum); // 10
Process(, Multiply); // 24

T Process&lt;T&gt;(ReadOnlySpan&lt;T&gt; span, Func&lt;ReadOnlySpan&lt;T&gt;, T&gt; method)
{
    return method(span);
}

T Sum&lt;T&gt;(ReadOnlySpan&lt;T&gt; span) where T : INumberBase&lt;T&gt;
{
    T result = T.Zero;
    foreach (T value in span)
    {
      result += value;
    }
    return result;
}

T Multiply&lt;T&gt;(ReadOnlySpan&lt;T&gt; span) where T : INumberBase&lt;T&gt;
{
    T result = T.One;
    foreach (T value in span)
    {
      result *= value;
    }
    return result;
}
</code></pre>
<p>为什么像 <code>ReadOnlySpan&lt;T&gt;</code> 这样的 <code>ref struct</code> 类型可以作为 <code>Func</code> 的类型参数呢?为了调查这个问题,我查看了 .NET 的 源代码,发现 <code>Func</code> 类型的泛型参数是这样定义的:</p>
<pre><code class="language-csharp">public delegate TResult Func&lt;in T, out TResult&gt;(T arg)
    where T : allows ref struct
    where TResult : allows ref struct;
</code></pre>
<p>如果在泛型参数上添加 <code>allow ref struct</code> 约束,那么就可以将 <code>ref struct</code> 类型传递给该参数。</p>
<p>这确实是一个方便的功能。</p>
<h2 id="ref-struct-也可以实现接口"><code>ref struct</code> 也可以实现接口</h2>
<p>在 C# 13 中,<code>ref struct</code> 可以实现接口。</p>
<p>如果将此功能与 <code>allows ref struct</code> 结合使用,那么也可以通过泛型类型传递引用:</p>
<pre><code class="language-csharp">using System;
using System.Numerics;

int a = 10;
// 使用 Ref&lt;int&gt; 保存 a 的引用
Ref&lt;int&gt; aRef = new Ref&lt;int&gt;(ref a);
// 传递 Ref&lt;int&gt;
Increase&lt;Ref&lt;int&gt;, int&gt;(aRef);
Console.WriteLine(a); // 11

void Increase&lt;T, U&gt;(T data) where T : IRef&lt;U&gt;, allows ref struct where U : INumberBase&lt;U&gt;
{
    ref U value = ref data.GetRef();
    value++;
}

interface IRef&lt;T&gt;
{
    ref T GetRef();
}

// 为 Ref&lt;T&gt; 这样的 ref struct 实现接口
ref struct Ref&lt;T&gt; : IRef&lt;T&gt;
{
    private ref T _value;

    public Ref(ref T value)
    {
      _value = ref value;
    }

    public ref T GetRef()
    {
      return ref _value;
    }
}
</code></pre>
<p>这样一来,编写 <code>ref struct</code> 相关的代码就变得更容易了。另外,也能给各种 <code>ref struct</code> 实现的枚举器实现 <code>IEnumerator</code> 之类的接口了。</p>
<h2 id="集合类型和-span-也可以使用-params">集合类型和 <code>Span</code> 也可以使用 <code>params</code></h2>
<p>在以前,<code>params</code> 只能用于数组类型,但从 C# 13 开始,它也可以用于其他集合类型和 <code>Span</code>。</p>
<p><code>params</code> 是一种功能,允许在调用方法时直接指定任意数量的参数。</p>
<p>例如,</p>
<pre><code class="language-csharp">Test(1, 2, 3, 4, 5, 6);
void Test(params int[] values) { }
</code></pre>
<p>如上所示,可以直接指定任意数量的 <code>int</code> 参数。</p>
<p>从 C# 13 开始,除了数组类型外,其他集合类型、<code>Span</code>、<code>ReadOnlySpan</code> 类型以及与集合相关的接口也可以添加 <code>params</code>:</p>
<pre><code class="language-csharp">Test(1, 2, 3, 4, 5, 6);
void Test(params ReadOnlySpan&lt;int&gt; values) { }

// 或者
Test(1, 2, 3, 4, 5, 6);
void Test(params List&lt;int&gt; values) { }

// 接口也可以
Test(1, 2, 3, 4, 5, 6);
void Test(params IEnumerable&lt;int&gt; values) { }
</code></pre>
<p>这也很方便!</p>
<h2 id="field-关键字"><code>field</code> 关键字</h2>
<p>在实现 C# 的属性时,经常需要定义一大堆字段,如下所示...</p>
<pre><code class="language-csharp">partial class ViewModel : INotifyPropertyChanged
{
    // 定义字段
    private int _myProperty;

    public int MyProperty
    {
      get =&gt; _myProperty;
      set
      {
            if (_myProperty != value)
            {
                _myProperty = value;
                OnPropertyChanged();
            }
      }
    }
}
</code></pre>
<p>因此,从 C# 13 开始,<code>field</code> 关键字将派上用场!</p>
<pre><code class="language-csharp">partial class ViewModel : INotifyPropertyChanged
{
    public int MyProperty
    {
      // 只需使用 field
      get =&gt; field;
      set
      {
            if (field != value)
            {
                field = value;
                OnPropertyChanged();
            }
      }
    }
}
</code></pre>
<p>不再需要自己定义字段,只需使用 <code>field</code> 关键字,字段就会自动生成。</p>
<p>这也非常方便!</p>
<h2 id="部分属性">部分属性</h2>
<p>在编写 C# 时,常见的问题之一是:属性不能添加 <code>partial</code> 修饰符。</p>
<p>在 C# 中,可以在类或方法上添加 <code>partial</code>,以便分别进行声明和实现。此外,还可以分散类的各个部分。它的主要用途是在使用源代码生成器等自动生成工具时,指定要生成的内容。</p>
<p>例如:</p>
<pre><code class="language-csharp">partial class ViewModel
{
    // 这里只声明方法,实现部分由工具自动生成
    partial void OnPropertyChanged(string propertyName);
}
</code></pre>
<p>然后自动生成工具会生成以下代码:</p>
<pre><code class="language-csharp">partial class ViewModel : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler? PropertyChanged;

    partial void OnPropertyChanged(string propertyName)
    {
      PropertyChanged?.Invoke(this, new(propertyName));
    }
}
</code></pre>
<p>开发者只需要声明 <code>OnPropertyChanged</code>,其实现将全部由自动生成,从而节省了开发者的时间。</p>
<p>从 C# 13 开始,属性也支持 <code>partial</code>:</p>
<pre><code class="language-csharp">partial class ViewModel
{
    // 声明部分属性
    public partial int MyProperty { get; set; }
}

partial class ViewModel
{
    // 部分属性的实现
    public partial int MyProperty
    {
      get
      {
            // ...
      }
      set
      {
            // ...
      }
    }
}
</code></pre>
<p>这样,属性也可以由工具自动生成了。</p>
<h2 id="锁对象">锁对象</h2>
<p>众所周知,<code>lock</code> 是一种功能,通过监视器用于线程同步。</p>
<pre><code class="language-csharp">object lockObject = new object();
lock (lockObject)
{
    // 关键区
}
</code></pre>
<p>但是,这个功能的开销其实很大,会影响性能。</p>
<p>为了解决这个问题,C# 13 实现了锁对象。要使用此功能,只需用 <code>System.Threading.Lock</code> 替换被锁定的对象即可:</p>
<pre><code class="language-csharp">using System.Threading;

Lock lockObject = new Lock();
lock (lockObject)
{
    // 关键区
}
</code></pre>
<p>这样就可以轻松提高性能了。</p>
<h2 id="初始化器中的尾部索引">初始化器中的尾部索引</h2>
<p>索引运算符 <code>^</code> 可用于表示集合末尾的相对位置。从 C# 13 开始,初始化器也支持此功能:</p>
<pre><code class="language-csharp">var x = new Numbers
{
    Values =
    {
       = 111,
      [^1] = 999 // ^1 是从末尾开始的第一个元素
    }
    // x.Values 是 111
    // x.Values 是 999,因为 Values 是最后一个元素
};

class Numbers
{
    public int[] Values { get; set; } = new int;
}
</code></pre>
<h2 id="escape-字符">ESCAPE 字符</h2>
<p>在 Unicode 字符串中,可以使用 <code>\e</code> 代替 <code>\u001b</code> 和 <code>\x1b</code>。<code>\u001b</code>、<code>\x1b</code> 和 <code>\e</code> 都表示 ESCAPE 字符。它们通常用于表示控制字符。</p>
<ul>
<li><code>\u001b</code> 表示 Unicode 转义序列,<code>\u</code> 后面的 4 位十六进制数表示 Unicode 代码点</li>
<li><code>\x1b</code> 表示十六进制转义序列,<code>\x</code> 后面的 2 位十六进制数表示 ASCII 代码</li>
<li><code>\e</code> 表示 ESCAPE 字符本身</li>
</ul>
<p>推荐使用 <code>\e</code> 的原因是,可以避免在十六进制中的混淆。</p>
<p>例如,如果 <code>\x1b</code> 后面跟着 <code>3</code>,则变为 <code>\x1b3</code>,由于 <code>\x1b</code> 和 <code>3</code> 之间没有明确的分隔,因此不清楚应该分别解释成 <code>\x1b</code> 和 <code>3</code>,还是放在一起解释。</p>
<p>如果使用 <code>\e</code>,则可以避免混淆。</p>
<h2 id="其他">其他</h2>
<p>除了上述功能外,方法组中的自然类型和方法重载中的优先级也有一些改进,但在本文中省略。如果想了解更多信息,请参阅文档。</p>
<h2 id="结语">结语</h2>
<p>C# 正在年复一年地进化,对我来说 C# 13 的更新中实现了许多非常实用且方便的功能,解决了不少实际的痛点。期待 .NET 9 和 C# 13 的正式发布~</p><br><br>
来源:https://www.cnblogs.com/hez2010/p/18326521/whats-new-in-csharp-13
頁: [1]
查看完整版本: 提高 C# 的生产力:C# 13 更新完全指南