电子说
C# 11 支持的模式有很多,包含:
而其中,不少模式都支持递归,也就意味着可以模式嵌套模式,以此来实现更加强大的匹配功能。
模式匹配可以通过switch
表达式来使用,也可以在普通的switch
语句中作为case
使用,还可以在if
条件中通过is
来使用。本文主要在switch
表达式中使用模式匹配。
那么接下来就对这些模式进行介绍。
实例:表达式计算器
为了更直观地介绍模式匹配,我们接下来利用模式匹配来编写一个表达式计算器。
为了编写表达式计算器,首先我们需要对表达式进行抽象:
publicabstractpartialclassExpr<T>whereT:IBinaryNumber<T> {publicabstractTEval(params(stringName, T Value)[] args); }
我们用上面这个Expr
来表示一个表达式,其中T
是操作数的类型,然后进一步将表达式分为常数表达式ConstantExpr
、参数表达式ParameterExpr
、一元表达式UnaryExpr
、二元表达式BinaryExpr
和三元表达式TernaryExpr
。最后提供一个Eval
方法,用来计算表达式的值,该方法可以传入一个args
来提供表达式计算所需要的参数。
有了一、二元表达式自然也需要运算符,例如加减乘除等,我们也同时定义Operator
来表示运算符:
publicabstractrecord Operator {publicrecordUnaryOperator(Operators Operator) : Operator;publicrecordBinaryOperator(BinaryOperators Operator) : Operator; }
然后设置允许的运算符,其中前三个是一元运算符,后面的是二元运算符:
publicenumOperators { [Description("~")] Inv, [Description("-")] Min, [Description("!")] LogicalNot, [Description("+")] Add, [Description("-")] Sub, [Description("*")] Mul, [Description("/")] Div, [Description("&")] And, [Description("|")] Or, [Description("^")] Xor, [Description("==")] Eq, [Description("!=")] Ne, [Description(">")] Gt, [Description("<")] Lt, [Description(">=")] Ge, [Description("<=")] Le, [Description("&&")] LogicalAnd, [Description("||")] LogicalOr, }
你可以能会好奇对T
的运算能如何实现逻辑与或非,关于这一点,我们直接使用0
来代表false
,非0
代表true
。
接下来就是分别实现各类表达式的时间!
常数表达式
常数表达式很简单,它保存一个常数值,因此只需要在构造方法中将用户提供的值存储下来。它的Eval
实现也只需要简单返回存储的值即可:
publicabstractpartialclassExpr<T>whereT:IBinaryNumber<T> {publicclassConstantExpr:Expr<T> {publicConstantExpr(Tvalue)=> Value =value;publicT Value {get; }publicvoidDeconstruct(outTvalue)=>value= Value;publicoverrideTEval(params(stringName, T Value)[] args)=> Value; } }
参数表达式用来定义表达式计算过程中的参数,允许用户在对表达式执行Eval
计算结果的时候传参,因此只需要存储参数名。它的Eval
实现需要根据参数名在args
中找出对应的参数值:
publicabstractpartialclassExpr<T>whereT:IBinaryNumber<T> {publicclassParameterExpr:Expr<T> {publicParameterExpr(stringname)=> Name = name;publicstringName {get; }publicvoidDeconstruct(outstringname)=> name = Name;// 对 args 进行模式匹配publicoverrideTEval(params(stringName, T Value)[] args)=> argsswitch{// 如果 args 有至少一个元素,那我们把第一个元素拿出来存为 (name, value),// 然后判断 name 是否和本参数表达式中存储的参数名 Name 相同。// 如果相同则返回 value,否则用 args 除去第一个元素剩下的参数继续匹配。[var (name, value), .. var tail] => name == Name ?value: Eval(tail),// 如果 args 是空列表,则说明在 args 中没有找到名字和 Name 相同的参数,抛出异常[] =>thrownewInvalidOperationException($"Expected an argument named{Name}.") }; } }
模式匹配会从上往下依次进行匹配,直到匹配成功为止。
上面的代码中你可能会好奇[var (name, value), .. var tail]
是个什么模式,这个模式整体看是列表模式,并且列表模式内组合使用声明模式、位置模式和切片模式。例如:
[]
:匹配一个空列表。[1, _, 3]
:匹配一个长度是 3,并且首尾元素分别是 1、3 的列表。其中_
是丢弃模式,表示任意元素。[_, .., 3]
:匹配一个末元素是 3,并且 3 不是首元素的列表。其中..
是切片模式,表示任意切片。[1, ..var tail]
:匹配一个首元素是 1 的列表,并且将除了首元素之外元素的切片赋值给tail
。其中var tail
是var
模式,用于将匹配结果赋值给变量。[var head, ..var tail]
:匹配一个列表,将它第一个元素赋值给head
,剩下元素的切片赋值给tail
,这个切片里可以没有元素。[var (name, value), ..var tail]
:匹配一个列表,将它第一个元素赋值给(name, value)
,剩下元素的切片赋值给tail
,这个切片里可以没有元素。其中(name, value)
是位置模式,用于将第一个元素的解构结果根据位置分别赋值给name
和value
,也可以写成(var name, var value)
。一元表达式用来处理只有一个操作数的计算,例如非、取反等。
publicabstractpartialclassExpr<T>whereT:IBinaryNumber<T> {publicclassUnaryExpr:Expr<T> {publicUnaryExpr(UnaryOperator op, Expr expr)=> (Op, Expr) = (op, expr);publicUnaryOperator Op {get; }publicExpr Expr {get; }publicvoidDeconstruct(outUnaryOperator op,outExpr expr)=> (op, expr) = (Op, Expr);// 对 Op 进行模式匹配publicoverrideTEval(params(stringName, T Value)[] args)=> Opswitch{// 如果 Op 是 UnaryOperator,则将其解构结果赋值给 op,然后对 op 进行匹配,op 是一个枚举,而 .NET 中的枚举值都是整数UnaryOperator(varop) => opswitch{// 如果 op 是 Operators.InvOperators.Inv => ~Expr.Eval(args),// 如果 op 是 Operators.MinOperators.Min => -Expr.Eval(args),// 如果 op 是 Operators.LogicalNotOperators.LogicalNot => Expr.Eval(args) == T.Zero ? T.One : T.Zero,// 如果 op 的值大于 LogicalNot 或者小于 0,表示不是一元运算符> Operators.LogicalNot or <0=>thrownewInvalidOperationException($"Expected an unary operator, but got{op}.") },// 如果 Op 不是 UnaryOperator_ =>thrownewInvalidOperationException("Expected an unary operator.") }; } }
上面的代码中,首先利用了 C# 元组可作为左值的特性,分别使用一行代码就做完了构造方法和解构方法的赋值:(Op, Expr) = (op, expr)
和(op, expr) = (Op, Expr)
。如果你好奇能否利用这个特性交换多个变量,答案是可以!
在Eval
中,首先将类型模式、位置模式和声明模式组合成UnaryOperator(var op)
,表示匹配UnaryOperator
类型、并且能解构出一个元素的东西,如果匹配则将解构出来的那个元素赋值给op
。
然后我们接着对解构出来的op
进行匹配,这里用到了常数模式,例如Operators.Inv
用来匹配op
是否是Operators.Inv
。常数模式可以使用各种常数对对象进行匹配。
这里的> Operators.LogicalNot
和< 0
则是关系模式,分别用于匹配大于Operators.LogicalNot
的值和小于0
的指。然后利用逻辑模式or
将两个模式组合起来表示或的关系。逻辑模式除了or
之外还有and
和not
。
由于我们在上面穷举了枚举中所有的一元运算符,因此也可以将> Operators.LogicalNot or < 0
换成丢弃模式_
或者 var 模式var foo
,两者都用来匹配任意的东西,只不过前者匹配到后直接丢弃,而后者声明了个变量foo
将匹配到的值放到里面:
opswitch{// ..._ =>thrownewInvalidOperationException($"Expected an unary operator, but got{op}.") }
或
opswitch{// ...varfoo =>thrownewInvalidOperationException($"Expected an unary operator, but got{foo}.") }
二元表达式用来表示操作数有两个的表达式。有了一元表达式的编写经验,二元表达式如法炮制即可。
publicabstractpartialclassExpr<T>whereT:IBinaryNumber<T> {publicclassBinaryExpr:Expr<T> {publicBinaryExpr(BinaryOperator op, Expr left, Expr right)=> (Op, Left, Right) = (op, left, right);publicBinaryOperator Op {get; }publicExpr Left {get; }publicExpr Right {get; }publicvoidDeconstruct(outBinaryOperator op,outExpr left,outExpr right)=> (op, left, right) = (Op, Left, Right);publicoverrideTEval(params(stringName, T Value)[] args)=> Opswitch{ BinaryOperator(varop) => opswitch{ Operators.Add => Left.Eval(args) + Right.Eval(args), Operators.Sub => Left.Eval(args) - Right.Eval(args), Operators.Mul => Left.Eval(args) * Right.Eval(args), Operators.Div => Left.Eval(args) / Right.Eval(args), Operators.And => Left.Eval(args) & Right.Eval(args), Operators.Or => Left.Eval(args) | Right.Eval(args), Operators.Xor => Left.Eval(args) ^ Right.Eval(args), Operators.Eq => Left.Eval(args) == Right.Eval(args) ? T.One : T.Zero, Operators.Ne => Left.Eval(args) != Right.Eval(args) ? T.One : T.Zero, Operators.Gt => Left.Eval(args) > Right.Eval(args) ? T.One : T.Zero, Operators.Lt => Left.Eval(args) < Right.Eval(args) ? T.One : T.Zero, Operators.Ge => Left.Eval(args) >= Right.Eval(args) ? T.One : T.Zero, Operators.Le => Left.Eval(args) <= Right.Eval(args) ? T.One : T.Zero, Operators.LogicalAnd => Left.Eval(args) == T.Zero || Right.Eval(args) == T.Zero ? T.Zero : T.One, Operators.LogicalOr => Left.Eval(args) == T.Zero && Right.Eval(args) == T.Zero ? T.Zero : T.One, < Operators.Add or > Operators.LogicalOr =>thrownewInvalidOperationException($"Unexpected a binary operator, but got{op}.") }, _ =>thrownewInvalidOperationException("Unexpected a binary operator.") }; } }
同理,也可以将< Operators.Add or > Operators.LogicalOr
换成丢弃模式或者 var 模式。
三元表达式
三元表达式包含三个操作数:条件表达式Cond
、为真的表达式Left
、为假的表达式Right
。该表达式中会根据Cond
是否为真来选择取Left
还是Right
,实现起来较为简单:
publicabstractpartialclassExpr<T>whereT:IBinaryNumber<T> {publicclassTernaryExpr:Expr<T> {publicTernaryExpr(Expr cond, Expr left, Expr right)=> (Cond, Left, Right) = (cond, left, right);publicExpr Cond {get; }publicExpr Left {get; }publicExpr Right {get; }publicvoidDeconstruct(outExpr cond,outExpr left,outExpr right)=> (cond, left, right) = (Cond, Left, Right);publicoverrideTEval(params(stringName, T Value)[] args)=> Cond.Eval(args) == T.Zero ? Right.Eval(args) : Left.Eval(args); } }
完成。我们用了仅仅几十行代码就完成了全部的核心逻辑!这便是模式匹配的强大之处:简洁、直观且高效。
表达式判等
至此为止,我们已经完成了所有的表达式构造、解构和计算的实现。接下来我们为每一个表达式实现判等逻辑,即判断两个表达式(字面上)是否相同。
例如a == b ? 2 : 4
和a == b ? 2 : 5
不相同,a == b ? 2 : 4
和c == d ? 2 : 4
不相同,而a == b ? 2 : 4
和a == b ? 2 : 4
相同。
为了实现该功能,我们重写每一个表达式的Equals
和GetHashCode
方法。
常数表达式
常数表达式判等只需要判断常数值是否相等即可:
publicoverrideboolEquals(object? obj)=>objisConstantExpr(varvalue) &&value== Value;publicoverrideintGetHashCode()=> Value.GetHashCode();
参数表达式判等只需要判断参数名是否相等即可:
publicoverrideboolEquals(object? obj)=>objisParameterExpr(varname) && name== Name;publicoverrideintGetHashCode()=> Name.GetHashCode();
一元表达式判等,需要判断被比较的表达式是否是一元表达式,如果也是的话则判断运算符和操作数是否相等:
publicoverrideboolEquals(object? obj)=>objisUnaryExpr({ Operator:varop },varexpr) && (op, expr).Equals((Op.Operator, Expr));publicoverrideintGetHashCode()=> (Op, Expr).GetHashCode();
上面的代码中用到了属性模式{ Operator: var op }
,用来匹配属性的值,这里直接组合了声明模式将属性Operator
的值赋值给了expr
。另外,C# 中的元组可以组合起来进行判等操作,因此不需要写op.Equals(Op.Operator) && expr.Equals(Expr)
,而是可以直接写(op, expr).Equals((Op.Operator, Expr))
。
二元表达式
和一元表达式差不多,区别在于这次多了一个操作数:
publicoverrideboolEquals(object? obj)=>objisBinaryExpr({ Operator:varop },varleft,varright) && (op, left, right).Equals((Op.Operator, Left, Right));publicoverrideintGetHashCode()=> (Op, Left, Right).GetHashCode();
和二元表达式差不多,只不过运算符Op
变成了操作数Cond
:
publicoverrideboolEquals(object? obj)=>objisTernaryExpr(varcond,varleft,varright) && cond.Equals(Cond) && left.Equals(Left) && right.Equals(Right);publicoverrideintGetHashCode()=> (Cond, Left, Right).GetHashCode();
到此为止,我们为所有的表达式都实现了判等。
一些工具方法
我们重载一些Expr
的运算符方便我们使用:
publicstaticExproperator~(Expr operand) =>newUnaryExpr(new(Operators.Inv), operand);publicstaticExproperator!(Expr operand) =>newUnaryExpr(new(Operators.LogicalNot), operand);publicstaticExproperator-(Expr operand) =>newUnaryExpr(new(Operators.Min), operand);publicstaticExproperator+(Expr left, Expr right) =>newBinaryExpr(new(Operators.Add), left, right);publicstaticExproperator-(Expr left, Expr right) =>newBinaryExpr(new(Operators.Sub), left, right);publicstaticExproperator*(Expr left, Expr right) =>newBinaryExpr(new(Operators.Mul), left, right);publicstaticExproperator/(Expr left, Expr right) =>newBinaryExpr(new(Operators.Div), left, right);publicstaticExproperator&(Expr left, Expr right) =>newBinaryExpr(new(Operators.And), left, right);publicstaticExproperator|(Expr left, Expr right) =>newBinaryExpr(new(Operators.Or), left, right);publicstaticExproperator^(Expr left, Expr right) =>newBinaryExpr(new(Operators.Xor), left, right);publicstaticExproperator>(Expr left, Expr right) =>newBinaryExpr(new(Operators.Gt), left, right);publicstaticExproperator<(Expr left, Expr right) =>newBinaryExpr(new(Operators.Lt), left, right);publicstaticExproperator>=(Expr left, Expr right) =>newBinaryExpr(new(Operators.Ge), left, right);publicstaticExproperator<=(Expr left, Expr right) =>newBinaryExpr(new(Operators.Le), left, right);publicstaticExproperator==(Expr left, Expr right) =>newBinaryExpr(new(Operators.Eq), left, right);publicstaticExproperator!=(Expr left, Expr right) =>newBinaryExpr(new(Operators.Ne), left, right);publicstaticimplicitoperatorExpr(Tvalue) =>newConstantExpr(value);publicstaticimplicitoperatorExpr(stringname) =>newParameterExpr(name);publicstaticimplicitoperatorExpr(boolvalue) =>newConstantExpr(value? T.One : T.Zero);publicoverrideboolEquals(object? obj)=>base.Equals(obj);publicoverrideintGetHashCode()=>base.GetHashCode();
由于重载了==
和!=
,编译器为了保险起见提示我们重写Equals
和GetHashCode
,这里实际上并不需要重写,因此直接调用base
上的方法保持默认行为即可。
然后编写两个扩展方法用来方便构造三元表达式,和从Description
中获取运算符的名字:
publicstaticclassExtensions{publicstaticExpr Switch(thisExpr cond, Expr left, Expr right)whereT : IBinaryNumber =>newExpr.TernaryExpr(cond, left, right);publicstaticstring? GetName(thisT op)whereT : Enum =>typeof(T).GetMember(op.ToString()).FirstOrDefault()?.GetCustomAttribute()?.Description; }
由于有参数表达式参与时需要我们提前提供参数值才能调用Eval
进行计算,因此我们写一个交互式的Eval
来在计算过程中遇到参数表达式时提示用户输入值,起名叫做InteractiveEval
:
publicTInteractiveEval(){varnames = Array.Empty<string>();returnEval(GetArgs(this,refnames,refnames)); }privatestaticTGetArg(stringname,refstring[] names){ Console.Write($"Parameter{name}: ");string? str;do{ str = Console.ReadLine(); }while(strisnull); names = names.Append(name).ToArray();returnT.Parse(str, NumberStyles.Number,null); }privatestatic(stringName, T Value)[]GetArgs(Expr expr,refstring[] assigned,refstring[] filter)=> exprswitch{ TernaryExpr(varcond,varleft,varright) => GetArgs(cond,refassigned,refassigned).Concat(GetArgs(left,refassigned,refassigned)).Concat(GetArgs(right,refassigned,refassigned)).ToArray(), BinaryExpr(_,varleft,varright) => GetArgs(left,refassigned,refassigned).Concat(GetArgs(right,refassigned, refassigned)).ToArray(), UnaryExpr(_,varuexpr) => GetArgs(uexpr,refassigned,refassigned), ParameterExpr(varname) => filterswitch{ [var head, ..]whenhead == name => Array.Empty<(stringName, T Value)>(), [_, .. var tail] => GetArgs(expr,refassigned,reftail), [] =>new[] { (name, GetArg(name,refassigned)) } }, _ => Array.Empty<(stringName, T Value)>()};
这里在GetArgs
方法中,模式[var head, ..]
后面跟了一个when head == name
,这里的when
用来给模式匹配指定额外的条件,仅当条件满足时才匹配成功,因此[var head, ..] when head == name
的含义是,匹配至少含有一个元素的列表,并且将头元素赋值给head
,且仅当head == name
时匹配才算成功。
最后我们再重写ToString
方法方便输出表达式,就全部大功告成了。
测试
接下来让我测试测试我们编写的表达式计算器:
Expr<int> a =4; Expr<int> b =-3; Expr<int> x ="x"; Expr<int> c = !((a + b) * (a - b) > x); Expr<int> y ="y"; Expr<int> z ="z"; Expr<int> expr = (c.Switch(y, z) - a > x).Switch(z + a, y / b); Console.WriteLine(expr); Console.WriteLine(expr.InteractiveEval());
运行后得到输出:
((((! ((((4) + (-3)) * ((4) - (-3))) > (x))) ? (y) : (z)) - (4)) > (x)) ? ((z) + (4)) : ((y) / (-3))
然后我们给x
、y
和z
分别设置成 42、27 和 35,即可得到运算结果:
Parameter x: 42 Parameter y: 27 Parameter z: 35 -9
再测测表达式判等逻辑:
Expr<int> expr1, expr2, expr3; { Expr<int> a =4; Expr<int> b =-3; Expr<int> x ="x"; Expr<int> c = !((a + b) * (a - b) > x); Expr<int> y ="y"; Expr<int> z ="z"; expr1 = (c.Switch(y, z) - a > x).Switch(z + a, y / b); } { Expr<int> a =4; Expr<int> b =-3; Expr<int> x ="x"; Expr<int> c = !((a + b) * (a - b) > x); Expr<int> y ="y"; Expr<int> z ="z"; expr2 = (c.Switch(y, z) - a > x).Switch(z + a, y / b); } { Expr<int> a =4; Expr<int> b =-3; Expr<int> x ="x"; Expr<int> c = !((a + b) * (a - b) > x); Expr<int> y ="y"; Expr<int> w ="w"; expr3 = (c.Switch(y, w) - a > x).Switch(w + a, y / b); } Console.WriteLine(expr1.Equals(expr2)); Console.WriteLine(expr1.Equals(expr3));
得到输出:
TrueFalse
在未来,C# 将会引入活动模式,该模式允许用户自定义模式匹配的方法,例如:
staticboolEven(thisTvalue)whereT : IBinaryInteger =>value%2==0;
上述代码定义了一个T
的扩展方法Even
,用来匹配value
是否为偶数,于是我们便可以这么使用:
varx =3;vary = xswitch{ Even() =>"even", _ =>"odd"};
此外,该模式还可以和解构模式结合,允许用户自定义解构行为,例如:
staticboolInt(thisstringvalue,outintresult)=>int.TryParse(value,outresult);
然后使用的时候:
varx ="3";vary = xswitch{ Int(varresult) => result, _ =>0};
即可对x
这个字符串进行匹配,如果x
可以被解析为int
,就取解析结果result
,否则取 0。
总结
模式匹配极大的方便了我们编写出简洁且可读性高的高质量代码,并且会自动帮我们做穷举检查,防止我们漏掉情况。此外,使用模式匹配时,编译器也会帮我们优化代码,减少完成匹配所需要的比较次数,最终减少分支并提升运行效率。
本文中的例子为了覆盖到全部的模式,不一定采用了最优的写法,这一点各位读者们也请注意。
来源:新机器视觉
审核编辑:汤梓红
全部0条评论
快来发表一下你的评论吧 !