三剑客时 發表於 2022-3-4 11:03:00

C# 模式匹配完全指南

<h2 id="前言">前言</h2>
<p>自从 2017 年 C# 7.0 版本开始引入声明模式和常数模式匹配开始,到 2022 年的 C# 11 为止,最后一个板块列表模式和切片模式匹配也已经补齐,当初计划的模式匹配内容已经基本全部完成。</p>
<p>C# 在模式匹配方面下一步计划则是支持活动模式(active pattern),这一部分将在本文最后进行介绍,而在介绍未来的模式匹配计划之前,本文主题是对截止 C# 11 模式匹配的<del>(不)</del>完全指南,希望能对各位开发者们提升代码编写效率、可读性和质量有所帮助。</p>
<h2 id="模式匹配">模式匹配</h2>
<p>要使用模式匹配,首先要了解什么是模式。在使用正则表达式匹配字符串时,正则表达式自己就是一个模式,而对字符串使用这段正则表达式进行匹配的过程就是模式匹配。而在代码中也是同样的,我们对对象采用某种模式进行匹配的过程就是模式匹配。</p>
<p>C# 11 支持的模式有很多,包含:</p>
<ul>
<li>声明模式(declaration pattern)</li>
<li>类型模式(type pattern)</li>
<li>常数模式(constant pattern)</li>
<li>关系模式(relational pattern)</li>
<li>逻辑模式(logical pattern)</li>
<li>属性模式(property pattern)</li>
<li>位置模式(positional pattern)</li>
<li>var 模式(var pattern)</li>
<li>丢弃模式(discard pattern)</li>
<li>列表模式(list pattern)</li>
<li>切片模式(slice pattern)</li>
</ul>
<p>而其中,不少模式都支持递归,也就意味着可以模式嵌套模式,以此来实现更加强大的匹配功能。</p>
<p>如果你不清楚这些模式的话,可以访问 https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/operators/patterns 进行了解。</p>
<p>模式匹配可以通过 <code>switch</code> 表达式来使用,也可以在普通的 <code>switch</code> 语句中作为 <code>case</code> 使用,还可以在 <code>if</code> 条件中通过 <code>is</code> 来使用。本文主要在 <code>switch</code> 表达式中使用模式匹配。</p>
<p>那么接下来就对这些模式进行介绍。</p>
<h2 id="实例表达式计算器">实例:表达式计算器</h2>
<p>为了更直观地介绍模式匹配,我们接下来利用模式匹配来编写一个表达式计算器。</p>
<p>为了编写表达式计算器,首先我们需要对表达式进行抽象:</p>
<pre><code class="language-csharp">public abstract partial class Expr&lt;T&gt; where T : IBinaryNumber&lt;T&gt;
{
    public abstract T Eval(params (string Name, T Value)[] args);
}
</code></pre>
<p>我们用上面这个 <code>Expr&lt;T&gt;</code> 来表示一个表达式,其中 <code>T</code> 是操作数的类型,然后进一步将表达式分为常数表达式 <code>ConstantExpr</code>、参数表达式 <code>ParameterExpr</code>、一元表达式 <code>UnaryExpr</code>、二元表达式 <code>BinaryExpr</code> 和三元表达式 <code>TernaryExpr</code>。最后提供一个 <code>Eval</code> 方法,用来计算表达式的值,该方法可以传入一个 <code>args</code> 来提供表达式计算所需要的参数。</p>
<p>有了一、二元表达式自然也需要运算符,例如加减乘除等,我们也同时定义 <code>Operator</code> 来表示运算符:</p>
<pre><code class="language-csharp">public abstract record Operator
{
    public record UnaryOperator(Operators Operator) : Operator;
    public record BinaryOperator(Operators Operator) : Operator;
}
</code></pre>
<p>然后设置允许的运算符,其中前三个是一元运算符,后面的是二元运算符:</p>
<pre><code class="language-csharp">public enum Operators
{
    Inv, Min, LogicalNot,
    Add, Sub, Mul, Div,
    And, Or, Xor,
    Eq, Ne,
    Gt, Lt, Ge, Le,
    LogicalAnd, LogicalOr,
}
</code></pre>
<p>你可以能会好奇对 <code>T</code> 的运算能如何实现逻辑与或非,关于这一点,我们直接使用 <code>0</code> 来代表 <code>false</code>,非 <code>0</code> 代表 <code>true</code>。</p>
<p>接下来就是分别实现各类表达式的时间!</p>
<h3 id="常数表达式">常数表达式</h3>
<p>常数表达式很简单,它保存一个常数值,因此只需要在构造方法中将用户提供的值存储下来。它的 <code>Eval</code> 实现也只需要简单返回存储的值即可:</p>
<pre><code class="language-csharp">public abstract partial class Expr&lt;T&gt; where T : IBinaryNumber&lt;T&gt;
{
    public class ConstantExpr : Expr&lt;T&gt;
    {
      public ConstantExpr(T value) =&gt; Value = value;

      public T Value { get; }
      public void Deconstruct(out T value) =&gt; value = Value;

      public override T Eval(params (string Name, T Value)[] args) =&gt; Value;
    }
}
</code></pre>
<h3 id="参数表达式">参数表达式</h3>
<p>参数表达式用来定义表达式计算过程中的参数,允许用户在对表达式执行 <code>Eval</code> 计算结果的时候传参,因此只需要存储参数名。它的 <code>Eval</code> 实现需要根据参数名在 <code>args</code> 中找出对应的参数值:</p>
<pre><code class="language-csharp">public abstract partial class Expr&lt;T&gt; where T : IBinaryNumber&lt;T&gt;
{
    public class ParameterExpr : Expr&lt;T&gt;
    {
      public ParameterExpr(string name) =&gt; Name = name;

      public string Name { get; }
      public void Deconstruct(out string name) =&gt; name = Name;

      // 对 args 进行模式匹配
      public override T Eval(params (string Name, T Value)[] args) =&gt; args switch
      {
            // 如果 args 有至少一个元素,那我们把第一个元素拿出来存为 (name, value),
            // 然后判断 name 是否和本参数表达式中存储的参数名 Name 相同。
            // 如果相同则返回 value,否则用 args 除去第一个元素剩下的参数继续匹配。
             =&gt; name == Name ? value : Eval(tail),
            // 如果 args 是空列表,则说明在 args 中没有找到名字和 Name 相同的参数,抛出异常
            [] =&gt; throw new InvalidOperationException($"Expected an argument named {Name}.")
      };
    }
}
</code></pre>
<p>模式匹配会从上往下依次进行匹配,直到匹配成功为止。</p>
<p>上面的代码中你可能会好奇 <code></code> 是个什么模式,这个模式整体看是列表模式,并且列表模式内组合使用声明模式、位置模式和切片模式。例如:</p>
<ul>
<li><code>[]</code>:匹配一个空列表。</li>
<li><code></code>:匹配一个长度是 3,并且首尾元素分别是 1、3 的列表。其中 <code>_</code> 是丢弃模式,表示任意元素。</li>
<li><code></code>:匹配一个末元素是 3,并且 3 不是首元素的列表。其中 <code>..</code> 是切片模式,表示任意切片。</li>
<li><code></code>:匹配一个首元素是 1 的列表,并且将除了首元素之外元素的切片赋值给 <code>tail</code>。其中 <code>var tail</code> 是声明模式,用于将匹配结果赋值给变量。</li>
<li><code></code>:匹配一个列表,将它第一个元素赋值给 <code>head</code>,剩下元素的切片赋值给 <code>tail</code>,这个切片里可以没有元素。</li>
<li><code></code>:匹配一个列表,将它第一个元素赋值给 <code>(name, value)</code>,剩下元素的切片赋值给 <code>tail</code>,这个切片里可以没有元素。其中 <code>(name, value)</code> 是位置模式,用于将第一个元素的解构结果根据位置分别赋值给 <code>name</code> 和 <code>value</code>,也可以写成 <code>(var name, var value)</code>。</li>
</ul>
<h3 id="一元表达式">一元表达式</h3>
<p>一元表达式用来处理只有一个操作数的计算,例如非、取反等。</p>
<pre><code class="language-csharp">public abstract partial class Expr&lt;T&gt; where T : IBinaryNumber&lt;T&gt;
{
    public class UnaryExpr : Expr&lt;T&gt;
    {
      public UnaryExpr(UnaryOperator op, Expr&lt;T&gt; expr) =&gt; (Op, Expr) = (op, expr);

      public UnaryOperator Op { get; }
      public Expr&lt;T&gt; Expr { get; }
      public void Deconstruct(out UnaryOperator op, out Expr&lt;T&gt; expr) =&gt; (op, expr) = (Op, Expr);

      // 对 Op 进行模式匹配
      public override T Eval(params (string Name, T Value)[] args) =&gt; Op switch
      {
            // 如果 Op 是 UnaryOperator,则将其解构结果赋值给 op,然后对 op 进行匹配,op 是一个枚举,而 .NET 中的枚举值都是整数
            UnaryOperator(var op) =&gt; op switch
            {
                // 如果 op 是 Operators.Inv
                Operators.Inv =&gt; ~Expr.Eval(args),
                // 如果 op 是 Operators.Min
                Operators.Min =&gt; -Expr.Eval(args),
                // 如果 op 是 Operators.LogicalNot
                Operators.LogicalNot =&gt; Expr.Eval(args) == T.Zero ? T.One : T.Zero,
                // 如果 op 的值大于 LogicalNot 或者小于 0,表示不是一元运算符
                &gt; Operators.LogicalNot or &lt; 0 =&gt; throw new InvalidOperationException($"Expected an unary operator, but got {op}.")
            },
            // 如果 Op 不是 UnaryOperator
            _ =&gt; throw new InvalidOperationException("Expected an unary operator.")
      };
    }
}
</code></pre>
<p>上面的代码中,首先利用了 C# 元组可作为左值的特性,分别使用一行代码就做完了构造方法和解构方法的赋值:<code>(Op, Expr) = (op, expr)</code> 和 <code>(op, expr) = (Op, Expr)</code>。如果你好奇能否利用这个特性交换多个变量,答案是可以!</p>
<p>在 <code>Eval</code> 中,首先将类型模式、位置模式和声明模式组合成 <code>UnaryOperator(var op)</code>,表示匹配 <code>UnaryOperator</code> 类型、并且能解构出一个元素的东西,如果匹配则将解构出来的那个元素赋值给 <code>op</code>。</p>
<p>然后我们接着对解构出来的 <code>op</code> 进行匹配,这里用到了常数模式,例如 <code>Operators.Inv</code> 用来匹配 <code>op</code> 是否是 <code>Operators.Inv</code>。常数模式可以使用各种常数对对象进行匹配。</p>
<p>这里的 <code>&gt; Operators.LogicalNot</code> 和 <code>&lt; 0</code> 则是关系模式,分别用于匹配大于 <code>Operators.LogicalNot</code> 的值和小于 <code>0</code> 的指。然后利用逻辑模式 <code>or</code> 将两个模式组合起来表示或的关系。逻辑模式除了 <code>or</code> 之外还有 <code>and</code> 和 <code>not</code>。</p>
<p>由于我们在上面穷举了枚举中所有的一元运算符,因此也可以将 <code>&gt; Operators.LogicalNot or &lt; 0</code> 换成丢弃模式 <code>_</code> 或者 var 模式 <code>var foo</code>,两者都用来匹配任意的东西,只不过前者匹配到后直接丢弃,而后者声明了个变量 <code>foo</code> 将匹配到的值放到里面:</p>
<pre><code class="language-csharp">op switch
{
    // ...
    _ =&gt; throw new InvalidOperationException($"Expected an unary operator, but got {op}.")
}
</code></pre>
<p>或</p>
<pre><code class="language-csharp">op switch
{
    // ...
    var foo =&gt; throw new InvalidOperationException($"Expected an unary operator, but got {foo}.")
}
</code></pre>
<h3 id="二元表达式">二元表达式</h3>
<p>二元表达式用来表示操作数有两个的表达式。有了一元表达式的编写经验,二元表达式如法炮制即可。</p>
<pre><code class="language-csharp">public abstract partial class Expr&lt;T&gt; where T : IBinaryNumber&lt;T&gt;
{
    public class BinaryExpr : Expr&lt;T&gt;
    {
      public BinaryExpr(BinaryOperator op, Expr&lt;T&gt; left, Expr&lt;T&gt; right) =&gt; (Op, Left, Right) = (op, left, right);

      public BinaryOperator Op { get; }
      public Expr&lt;T&gt; Left { get; }
      public Expr&lt;T&gt; Right { get; }
      public void Deconstruct(out BinaryOperator op, out Expr&lt;T&gt; left, out Expr&lt;T&gt; right) =&gt; (op, left, right) = (Op, Left, Right);

      public override T Eval(params (string Name, T Value)[] args) =&gt; Op switch
      {
            BinaryOperator(var op) =&gt; op switch
            {
                Operators.Add =&gt; Left.Eval(args) + Right.Eval(args),
                Operators.Sub =&gt; Left.Eval(args) - Right.Eval(args),
                Operators.Mul =&gt; Left.Eval(args) * Right.Eval(args),
                Operators.Div =&gt; Left.Eval(args) / Right.Eval(args),
                Operators.And =&gt; Left.Eval(args) &amp; Right.Eval(args),
                Operators.Or =&gt; Left.Eval(args) | Right.Eval(args),
                Operators.Xor =&gt; Left.Eval(args) ^ Right.Eval(args),
                Operators.Eq =&gt; Left.Eval(args) == Right.Eval(args) ? T.One : T.Zero,
                Operators.Ne =&gt; Left.Eval(args) != Right.Eval(args) ? T.One : T.Zero,
                Operators.Gt =&gt; Left.Eval(args) &gt; Right.Eval(args) ? T.One : T.Zero,
                Operators.Lt =&gt; Left.Eval(args) &lt; Right.Eval(args) ? T.One : T.Zero,
                Operators.Ge =&gt; Left.Eval(args) &gt;= Right.Eval(args) ? T.One : T.Zero,
                Operators.Le =&gt; Left.Eval(args) &lt;= Right.Eval(args) ? T.One : T.Zero,
                Operators.LogicalAnd =&gt; Left.Eval(args) == T.Zero || Right.Eval(args) == T.Zero ? T.Zero : T.One,
                Operators.LogicalOr =&gt; Left.Eval(args) == T.Zero &amp;&amp; Right.Eval(args) == T.Zero ? T.Zero : T.One,
                &lt; Operators.Add or &gt; Operators.LogicalOr =&gt; throw new InvalidOperationException($"Unexpected a binary operator, but got {op}.")
            },
            _ =&gt; throw new InvalidOperationException("Unexpected a binary operator.")
      };
    }
}
</code></pre>
<p>同理,也可以将 <code>&lt; Operators.Add or &gt; Operators.LogicalOr</code> 换成丢弃模式或者 var 模式。</p>
<h3 id="三元表达式">三元表达式</h3>
<p>三元表达式包含三个操作数:条件表达式 <code>Cond</code>、为真的表达式 <code>Left</code>、为假的表达式 <code>Right</code>。该表达式中会根据 <code>Cond</code> 是否为真来选择取 <code>Left</code> 还是 <code>Right</code>,实现起来较为简单:</p>
<pre><code class="language-csharp">public abstract partial class Expr&lt;T&gt; where T : IBinaryNumber&lt;T&gt;
{
    public class TernaryExpr : Expr&lt;T&gt;
    {
      public TernaryExpr(Expr&lt;T&gt; cond, Expr&lt;T&gt; left, Expr&lt;T&gt; right) =&gt; (Cond, Left, Right) = (cond, left, right);

      public Expr&lt;T&gt; Cond { get; }
      public Expr&lt;T&gt; Left { get; }
      public Expr&lt;T&gt; Right { get; }
      public void Deconstruct(out Expr&lt;T&gt; cond, out Expr&lt;T&gt; left, out Expr&lt;T&gt; right) =&gt; (cond, left, right) = (Cond, Left, Right);

      public override T Eval(params (string Name, T Value)[] args) =&gt; Cond.Eval(args) == T.Zero ? Right.Eval(args) : Left.Eval(args);
    }
}
</code></pre>
<p>完成。我们用了仅仅几十行代码就完成了全部的核心逻辑!这便是模式匹配的强大之处:简洁、直观且高效。</p>
<h3 id="表达式判等">表达式判等</h3>
<p>至此为止,我们已经完成了所有的表达式构造、解构和计算的实现。接下来我们为每一个表达式实现判等逻辑,即判断两个表达式(字面上)是否相同。</p>
<p>例如 <code>a == b ? 2 : 4</code> 和 <code>a == b ? 2 : 5</code> 不相同,<code>a == b ? 2 : 4</code> 和 <code>c == d ? 2 : 4</code> 不相同,而 <code>a == b ? 2 : 4</code> 和 <code>a == b ? 2 : 4</code> 相同。</p>
<p>为了实现该功能,我们重写每一个表达式的 <code>Equals</code> 和 <code>GetHashCode</code> 方法。</p>
<h4 id="常数表达式-1">常数表达式</h4>
<p>常数表达式判等只需要判断常数值是否相等即可:</p>
<pre><code class="language-csharp">public override bool Equals(object? obj) =&gt; obj is ConstantExpr(var value) &amp;&amp; value == Value;
public override int GetHashCode() =&gt; Value.GetHashCode();
</code></pre>
<h4 id="参数表达式-1">参数表达式</h4>
<p>参数表达式判等只需要判断参数名是否相等即可:</p>
<pre><code class="language-csharp">public override bool Equals(object? obj) =&gt; obj is ParameterExpr(var name) &amp;&amp; name == Name;
public override int GetHashCode() =&gt; Name.GetHashCode();
</code></pre>
<h4 id="一元表达式-1">一元表达式</h4>
<p>一元表达式判等,需要判断被比较的表达式是否是一元表达式,如果也是的话则判断运算符和操作数是否相等:</p>
<pre><code class="language-csharp">public override bool Equals(object? obj) =&gt; obj is UnaryExpr({ Operator: var op }, var expr) &amp;&amp; (op, expr).Equals((Op.Operator, Expr));
public override int GetHashCode() =&gt; (Op, Expr).GetHashCode();
</code></pre>
<p>上面的代码中用到了属性模式 <code>{ Operator: var op }</code>,用来匹配属性的值,这里直接组合了声明模式将属性 <code>Operator</code> 的值赋值给了 <code>op</code>。另外,C# 中的元组可以组合起来进行判等操作,因此不需要写 <code>op.Equals(Op.Operator) &amp;&amp; expr.Equals(Expr)</code>,而是可以直接写 <code>(op, expr).Equals((Op.Operator, Expr))</code>。</p>
<h4 id="二元表达式-1">二元表达式</h4>
<p>和一元表达式差不多,区别在于这次多了一个操作数:</p>
<pre><code class="language-csharp">public override bool Equals(object? obj) =&gt; obj is BinaryExpr({ Operator: var op }, var left, var right) &amp;&amp; (op, left, right).Equals((Op.Operator, Left, Right));
public override int GetHashCode() =&gt; (Op, Left, Right).GetHashCode();
</code></pre>
<h4 id="三元表达式-1">三元表达式</h4>
<p>和二元表达式差不多,只不过运算符 <code>Op</code> 变成了操作数 <code>Cond</code>:</p>
<pre><code class="language-csharp">public override bool Equals(object? obj) =&gt; obj is TernaryExpr(var cond, var left, var right) &amp;&amp; cond.Equals(Cond) &amp;&amp; left.Equals(Left) &amp;&amp; right.Equals(Right);
public override int GetHashCode() =&gt; (Cond, Left, Right).GetHashCode();
</code></pre>
<p>到此为止,我们为所有的表达式都实现了判等。</p>
<h3 id="一些工具方法">一些工具方法</h3>
<p>我们重载一些 <code>Expr&lt;T&gt;</code> 的运算符方便我们使用:</p>
<pre><code class="language-csharp">public static Expr&lt;T&gt; operator ~(Expr&lt;T&gt; operand) =&gt; new UnaryExpr(new(Operators.Inv), operand);
public static Expr&lt;T&gt; operator !(Expr&lt;T&gt; operand) =&gt; new UnaryExpr(new(Operators.LogicalNot), operand);
public static Expr&lt;T&gt; operator -(Expr&lt;T&gt; operand) =&gt; new UnaryExpr(new(Operators.Min), operand);
public static Expr&lt;T&gt; operator +(Expr&lt;T&gt; left, Expr&lt;T&gt; right) =&gt; new BinaryExpr(new(Operators.Add), left, right);
public static Expr&lt;T&gt; operator -(Expr&lt;T&gt; left, Expr&lt;T&gt; right) =&gt; new BinaryExpr(new(Operators.Sub), left, right);
public static Expr&lt;T&gt; operator *(Expr&lt;T&gt; left, Expr&lt;T&gt; right) =&gt; new BinaryExpr(new(Operators.Mul), left, right);
public static Expr&lt;T&gt; operator /(Expr&lt;T&gt; left, Expr&lt;T&gt; right) =&gt; new BinaryExpr(new(Operators.Div), left, right);
public static Expr&lt;T&gt; operator &amp;(Expr&lt;T&gt; left, Expr&lt;T&gt; right) =&gt; new BinaryExpr(new(Operators.And), left, right);
public static Expr&lt;T&gt; operator |(Expr&lt;T&gt; left, Expr&lt;T&gt; right) =&gt; new BinaryExpr(new(Operators.Or), left, right);
public static Expr&lt;T&gt; operator ^(Expr&lt;T&gt; left, Expr&lt;T&gt; right) =&gt; new BinaryExpr(new(Operators.Xor), left, right);
public static Expr&lt;T&gt; operator &gt;(Expr&lt;T&gt; left, Expr&lt;T&gt; right) =&gt; new BinaryExpr(new(Operators.Gt), left, right);
public static Expr&lt;T&gt; operator &lt;(Expr&lt;T&gt; left, Expr&lt;T&gt; right) =&gt; new BinaryExpr(new(Operators.Lt), left, right);
public static Expr&lt;T&gt; operator &gt;=(Expr&lt;T&gt; left, Expr&lt;T&gt; right) =&gt; new BinaryExpr(new(Operators.Ge), left, right);
public static Expr&lt;T&gt; operator &lt;=(Expr&lt;T&gt; left, Expr&lt;T&gt; right) =&gt; new BinaryExpr(new(Operators.Le), left, right);
public static Expr&lt;T&gt; operator ==(Expr&lt;T&gt; left, Expr&lt;T&gt; right) =&gt; new BinaryExpr(new(Operators.Eq), left, right);
public static Expr&lt;T&gt; operator !=(Expr&lt;T&gt; left, Expr&lt;T&gt; right) =&gt; new BinaryExpr(new(Operators.Ne), left, right);
public static implicit operator Expr&lt;T&gt;(T value) =&gt; new ConstantExpr(value);
public static implicit operator Expr&lt;T&gt;(string name) =&gt; new ParameterExpr(name);
public static implicit operator Expr&lt;T&gt;(bool value) =&gt; new ConstantExpr(value ? T.One : T.Zero);

public override bool Equals(object? obj) =&gt; base.Equals(obj);
public override int GetHashCode() =&gt; base.GetHashCode();
</code></pre>
<p>由于重载了 <code>==</code> 和 <code>!=</code>,编译器为了保险起见提示我们重写 <code>Equals</code> 和 <code>GetHashCode</code>,这里实际上并不需要重写,因此直接调用 <code>base</code> 上的方法保持默认行为即可。</p>
<p>然后编写两个扩展方法用来方便构造三元表达式,和从 <code>Description</code> 中获取运算符的名字:</p>
<pre><code class="language-csharp">public static class Extensions
{
    public static Expr&lt;T&gt; Switch&lt;T&gt;(this Expr&lt;T&gt; cond, Expr&lt;T&gt; left, Expr&lt;T&gt; right) where T : IBinaryNumber&lt;T&gt; =&gt; new Expr&lt;T&gt;.TernaryExpr(cond, left, right);
    public static string? GetName&lt;T&gt;(this T op) where T : Enum =&gt; typeof(T).GetMember(op.ToString()).FirstOrDefault()?.GetCustomAttribute&lt;DescriptionAttribute&gt;()?.Description;
}
</code></pre>
<p>由于有参数表达式参与时需要我们提前提供参数值才能调用 <code>Eval</code> 进行计算,因此我们写一个交互式的 <code>Eval</code> 来在计算过程中遇到参数表达式时提示用户输入值,起名叫做 <code>InteractiveEval</code>:</p>
<pre><code class="language-csharp">public T InteractiveEval()
{
    var names = Array.Empty&lt;string&gt;();
    return Eval(GetArgs(this, ref names, ref names));
}
private static T GetArg(string name, ref string[] names)
{
    Console.Write($"Parameter {name}: ");
    string? str;
    do { str = Console.ReadLine(); }
    while (str is null);
    names = names.Append(name).ToArray();
    return T.Parse(str, NumberStyles.Number, null);
}
private static (string Name, T Value)[] GetArgs(Expr&lt;T&gt; expr, ref string[] assigned, ref string[] filter) =&gt; expr switch
{
    TernaryExpr(var cond, var left, var right) =&gt; GetArgs(cond, ref assigned, ref assigned).Concat(GetArgs(left, ref assigned,ref assigned)).Concat(GetArgs(right, ref assigned, ref assigned)).ToArray(),
    BinaryExpr(_, var left, var right) =&gt; GetArgs(left, ref assigned, ref assigned).Concat(GetArgs(right, ref assigned, refassigned)).ToArray(),
    UnaryExpr(_, var uexpr) =&gt; GetArgs(uexpr, ref assigned, ref assigned),
    ParameterExpr(var name) =&gt; filter switch
    {
       when head == name =&gt; Array.Empty&lt;(string Name, T Value)&gt;(),
       =&gt; GetArgs(expr, ref assigned, ref tail),
      [] =&gt; new[] { (name, GetArg(name, ref assigned)) }
    },
    _ =&gt; Array.Empty&lt;(string Name, T Value)&gt;()
};
</code></pre>
<p>这里在 <code>GetArgs</code> 方法中,模式 <code></code> 后面跟了一个 <code>when head == name</code>,这里的 <code>when</code> 用来给模式匹配指定额外的条件,仅当条件满足时才匹配成功,因此 <code> when head == name</code> 的含义是,匹配至少含有一个元素的列表,并且将头元素赋值给 <code>head</code>,且仅当 <code>head == name</code> 时匹配才算成功。</p>
<p>最后我们再重写 <code>ToString</code> 方法方便输出表达式,就全部大功告成了。</p>
<h3 id="测试">测试</h3>
<p>接下来让我测试测试我们编写的表达式计算器:</p>
<pre><code class="language-csharp">Expr&lt;int&gt; a = 4;
Expr&lt;int&gt; b = -3;
Expr&lt;int&gt; x = "x";
Expr&lt;int&gt; c = !((a + b) * (a - b) &gt; x);
Expr&lt;int&gt; y = "y";
Expr&lt;int&gt; z = "z";
Expr&lt;int&gt; expr = (c.Switch(y, z) - a &gt; x).Switch(z + a, y / b);
Console.WriteLine(expr);
Console.WriteLine(expr.InteractiveEval());
</code></pre>
<p>运行后得到输出:</p>
<pre><code>((((! ((((4) + (-3)) * ((4) - (-3))) &gt; (x))) ? (y) : (z)) - (4)) &gt; (x)) ? ((z) + (4)) : ((y) / (-3))
</code></pre>
<p>然后我们给 <code>x</code>、<code>y</code> 和 <code>z</code> 分别设置成 42、27 和 35,即可得到运算结果:</p>
<pre><code>Parameter x: 42
Parameter y: 27
Parameter z: 35
-9
</code></pre>
<p>再测测表达式判等逻辑:</p>
<pre><code class="language-csharp">Expr&lt;int&gt; expr1, expr2, expr3;
{
    Expr&lt;int&gt; a = 4;
    Expr&lt;int&gt; b = -3;
    Expr&lt;int&gt; x = "x";
    Expr&lt;int&gt; c = !((a + b) * (a - b) &gt; x);
    Expr&lt;int&gt; y = "y";
    Expr&lt;int&gt; z = "z";
    expr1 = (c.Switch(y, z) - a &gt; x).Switch(z + a, y / b);
}

{
    Expr&lt;int&gt; a = 4;
    Expr&lt;int&gt; b = -3;
    Expr&lt;int&gt; x = "x";
    Expr&lt;int&gt; c = !((a + b) * (a - b) &gt; x);
    Expr&lt;int&gt; y = "y";
    Expr&lt;int&gt; z = "z";
    expr2 = (c.Switch(y, z) - a &gt; x).Switch(z + a, y / b);
}

{
    Expr&lt;int&gt; a = 4;
    Expr&lt;int&gt; b = -3;
    Expr&lt;int&gt; x = "x";
    Expr&lt;int&gt; c = !((a + b) * (a - b) &gt; x);
    Expr&lt;int&gt; y = "y";
    Expr&lt;int&gt; w = "w";
    expr3 = (c.Switch(y, w) - a &gt; x).Switch(w + a, y / b);
}

Console.WriteLine(expr1.Equals(expr2));
Console.WriteLine(expr1.Equals(expr3));
</code></pre>
<p>得到输出:</p>
<pre><code>True
False
</code></pre>
<h2 id="活动模式">活动模式</h2>
<p>在未来,C# 将会引入活动模式,该模式允许用户自定义模式匹配的方法,例如:</p>
<pre><code class="language-csharp">static bool Even&lt;T&gt;(this T value) where T : IBinaryInteger&lt;T&gt; =&gt; value % 2 == 0;
</code></pre>
<p>上述代码定义了一个 <code>T</code> 的扩展方法 <code>Even</code>,用来匹配 <code>value</code> 是否为偶数,于是我们便可以这么使用:</p>
<pre><code class="language-csharp">var x = 3;
var y = x switch
{
    Even() =&gt; "even",
    _ =&gt; "odd"
};
</code></pre>
<p>此外,该模式还可以和解构模式结合,允许用户自定义解构行为,例如:</p>
<pre><code class="language-csharp">static bool Int(this string value, out int result) =&gt; int.TryParse(value, out result);
</code></pre>
<p>然后使用的时候:</p>
<pre><code class="language-csharp">var x = "3";
var y = x switch
{
    Int(var result) =&gt; result,
    _ =&gt; 0
};
</code></pre>
<p>即可对 <code>x</code> 这个字符串进行匹配,如果 <code>x</code> 可以被解析为 <code>int</code>,就取解析结果 <code>result</code>,否则取 0。</p>
<h2 id="后记">后记</h2>
<p>模式匹配极大的方便了我们编写出简洁且可读性高的高质量代码,并且会自动帮我们做穷举检查,防止我们漏掉情况。此外,使用模式匹配时,编译器也会帮我们优化代码,减少完成匹配所需要的比较次数,最终减少分支并提升运行效率。</p>
<p>本文中的例子为了覆盖到全部的模式,不一定采用了最优的写法,这一点各位读者们也请注意。</p>
<p>本文中的表达式计算器全部代码可以前往我的 GitHub 仓库获取:https://github.com/hez2010/PatternMatchingExpr</p><br><br>
来源:https://www.cnblogs.com/hez2010/p/a-complete-guide-for-pattern-matching-in-csharp.html
頁: [1]
查看完整版本: C# 模式匹配完全指南