前言
自從 2017 年 C# 7.0 版本開始引入聲明模式和常數(shù)模式匹配開始,到 2022 年的 C# 11 為止,最后一個板塊列表模式和切片模式匹配也已經(jīng)補齊,當初計劃的模式匹配內(nèi)容已經(jīng)基本全部完成。
C# 在模式匹配方面下一步計劃則是支持活動模式(active pattern),這一部分將在本文最后進行介紹,而在介紹未來的模式匹配計劃之前,本文主題是對截止 C# 11 模式匹配的~~(不)~~完全指南,希望能對各位開發(fā)者們提升代碼編寫效率、可讀性和質(zhì)量有所幫助。
模式匹配
要使用模式匹配,首先要了解什么是模式。在使用正則表達式匹配字符串時,正則表達式自己就是一個模式,而對字符串使用這段正則表達式進行匹配的過程就是模式匹配。而在代碼中也是同樣的,我們對對象采用某種模式進行匹配的過程就是模式匹配。
C# 11 支持的模式有很多,包含:
- 聲明模式(declaration pattern)
- 類型模式(type pattern)
- 常數(shù)模式(constant pattern)
- 關系模式(relational pattern)
- 邏輯模式(logical pattern)
- 屬性模式(property pattern)
- 位置模式(positional pattern)
- var 模式(var pattern)
- 丟棄模式(discard pattern)
- 列表模式(list pattern)
- 切片模式(slice pattern)
而其中,不少模式都支持遞歸,也就意味著可以模式嵌套模式,以此來實現(xiàn)更加強大的匹配功能。
模式匹配可以通過switch
表達式來使用,也可以在普通的switch
語句中作為case
使用,還可以在if
條件中通過is
來使用。本文主要在switch
表達式中使用模式匹配。
那么接下來就對這些模式進行介紹。
實例:表達式計算器
為了更直觀地介紹模式匹配,我們接下來利用模式匹配來編寫一個表達式計算器。
為了編寫表達式計算器,首先我們需要對表達式進行抽象:
publicabstractpartialclassExpr<T>whereT:IBinaryNumber<T>
{
publicabstractTEval(params(stringName,TValue)[]args);
}
我們用上面這個Expr
來表示一個表達式,其中T
是操作數(shù)的類型,然后進一步將表達式分為常數(shù)表達式ConstantExpr
、參數(shù)表達式ParameterExpr
、一元表達式UnaryExpr
、二元表達式BinaryExpr
和三元表達式TernaryExpr
。最后提供一個Eval
方法,用來計算表達式的值,該方法可以傳入一個args
來提供表達式計算所需要的參數(shù)。
有了一、二元表達式自然也需要運算符,例如加減乘除等,我們也同時定義Operator
來表示運算符:
publicabstractrecordOperator
{
publicrecordUnaryOperator(OperatorsOperator):Operator;
publicrecordBinaryOperator(BinaryOperatorsOperator):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
的運算能如何實現(xiàn)邏輯與或非,關于這一點,我們直接使用0
來代表false
,非0
代表true
。
接下來就是分別實現(xiàn)各類表達式的時間!
常數(shù)表達式
常數(shù)表達式很簡單,它保存一個常數(shù)值,因此只需要在構造方法中將用戶提供的值存儲下來。它的Eval
實現(xiàn)也只需要簡單返回存儲的值即可:
publicabstractpartialclassExpr<T>whereT:IBinaryNumber<T>
{
publicclassConstantExpr:Expr<T>
{
publicConstantExpr(Tvalue)=>Value=value;
publicTValue{get;}
publicvoidDeconstruct(outTvalue)=>value=Value;
publicoverrideTEval(params(stringName,TValue)[]args)=>Value;
}
}
參數(shù)表達式
參數(shù)表達式用來定義表達式計算過程中的參數(shù),允許用戶在對表達式執(zhí)行Eval
計算結果的時候傳參,因此只需要存儲參數(shù)名。它的Eval
實現(xiàn)需要根據(jù)參數(shù)名在args
中找出對應的參數(shù)值:
publicabstractpartialclassExpr<T>whereT:IBinaryNumber<T>
{
publicclassParameterExpr:Expr<T>
{
publicParameterExpr(stringname)=>Name=name;
publicstringName{get;}
publicvoidDeconstruct(outstringname)=>name=Name;
//對args進行模式匹配
publicoverrideTEval(params(stringName,TValue)[]args)=>argsswitch
{
//如果args有至少一個元素,那我們把第一個元素拿出來存為(name,value),
//然后判斷 name 是否和本參數(shù)表達式中存儲的參數(shù)名 Name 相同。
//如果相同則返回 value,否則用 args 除去第一個元素剩下的參數(shù)繼續(xù)匹配。
[var(name,value),..vartail]=>name==Name?value:Eval(tail),
//如果args是空列表,則說明在args中沒有找到名字和Name相同的參數(shù),拋出異常
[]=>thrownewInvalidOperationException($"Expectedanargumentnamed{Name}.")
};
}
}
模式匹配會從上往下依次進行匹配,直到匹配成功為止。
上面的代碼中你可能會好奇[var (name, value), .. var tail]
是個什么模式,這個模式整體看是列表模式,并且列表模式內(nèi)組合使用聲明模式、位置模式和切片模式。例如:
-
[]
:匹配一個空列表。 -
[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)
是位置模式,用于將第一個元素的解構結果根據(jù)位置分別賦值給name
和value
,也可以寫成(var name, var value)
。
一元表達式
一元表達式用來處理只有一個操作數(shù)的計算,例如非、取反等。
publicabstractpartialclassExpr<T>whereT:IBinaryNumber<T>
{
publicclassUnaryExpr:Expr<T>
{
publicUnaryExpr(UnaryOperatorop,Exprexpr)=>(Op,Expr)=(op,expr);
publicUnaryOperatorOp{get;}
publicExprExpr{get;}
publicvoidDeconstruct(outUnaryOperatorop,outExprexpr)=>(op,expr)=(Op,Expr);
//對Op進行模式匹配
publicoverrideTEval(params(stringName,TValue)[]args)=>Opswitch
{
//如果Op是UnaryOperator,則將其解構結果賦值給op,然后對op進行匹配,op是一個枚舉,而.NET中的枚舉值都是整數(shù)
UnaryOperator(varop)=>opswitch
{
//如果op是Operators.Inv
Operators.Inv=>~Expr.Eval(args),
//如果op是Operators.Min
Operators.Min=>-Expr.Eval(args),
//如果op是Operators.LogicalNot
Operators.LogicalNot=>Expr.Eval(args)==T.Zero?T.One:T.Zero,
//如果op的值大于LogicalNot或者小于0,表示不是一元運算符
>Operators.LogicalNotor0=>thrownewInvalidOperationException($"Expectedanunaryoperator,butgot{op}.")
},
//如果Op不是UnaryOperator
_=>thrownewInvalidOperationException("Expectedanunaryoperator.")
};
}
}
上面的代碼中,首先利用了 C# 元組可作為左值的特性,分別使用一行代碼就做完了構造方法和解構方法的賦值:(Op, Expr) = (op, expr)
和(op, expr) = (Op, Expr)
。如果你好奇能否利用這個特性交換多個變量,答案是可以!
在Eval
中,首先將類型模式、位置模式和聲明模式組合成UnaryOperator(var op)
,表示匹配UnaryOperator
類型、并且能解構出一個元素的東西,如果匹配則將解構出來的那個元素賦值給op
。
然后我們接著對解構出來的op
進行匹配,這里用到了常數(shù)模式,例如Operators.Inv
用來匹配op
是否是Operators.Inv
。常數(shù)模式可以使用各種常數(shù)對對象進行匹配。
這里的> Operators.LogicalNot
和< 0
則是關系模式,分別用于匹配大于Operators.LogicalNot
的值和小于0
的指。然后利用邏輯模式or
將兩個模式組合起來表示或的關系。邏輯模式除了or
之外還有and
和not
。
由于我們在上面窮舉了枚舉中所有的一元運算符,因此也可以將> Operators.LogicalNot or < 0
換成丟棄模式_
或者 var 模式var foo
,兩者都用來匹配任意的東西,只不過前者匹配到后直接丟棄,而后者聲明了個變量foo
將匹配到的值放到里面:
opswitch
{
//...
_=>thrownewInvalidOperationException($"Expectedanunaryoperator,butgot{op}.")
}
或
opswitch
{
//...
varfoo=>thrownewInvalidOperationException($"Expectedanunaryoperator,butgot{foo}.")
}
二元表達式
二元表達式用來表示操作數(shù)有兩個的表達式。有了一元表達式的編寫經(jīng)驗,二元表達式如法炮制即可。
publicabstractpartialclassExpr<T>whereT:IBinaryNumber<T>
{
publicclassBinaryExpr:Expr<T>
{
publicBinaryExpr(BinaryOperatorop,Exprleft,Exprright)=>(Op,Left,Right)=(op,left,right);
publicBinaryOperatorOp{get;}
publicExprLeft{get;}
publicExprRight{get;}
publicvoidDeconstruct(outBinaryOperatorop,outExprleft,outExprright)=>(op,left,right)=(Op,Left,Right);
publicoverrideTEval(params(stringName,TValue)[]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)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.LogicalOr=>thrownewInvalidOperationException($"Unexpectedabinaryoperator,butgot{op}.")
},
_=>thrownewInvalidOperationException("Unexpectedabinaryoperator.")
};
}
}
同理,也可以將< Operators.Add or > Operators.LogicalOr
換成丟棄模式或者 var 模式。
三元表達式
三元表達式包含三個操作數(shù):條件表達式Cond
、為真的表達式Left
、為假的表達式Right
。該表達式中會根據(jù)Cond
是否為真來選擇取Left
還是Right
,實現(xiàn)起來較為簡單:
publicabstractpartialclassExpr<T>whereT:IBinaryNumber<T>
{
publicclassTernaryExpr:Expr<T>
{
publicTernaryExpr(Exprcond,Exprleft,Exprright)=>(Cond,Left,Right)=(cond,left,right);
publicExprCond{get;}
publicExprLeft{get;}
publicExprRight{get;}
publicvoidDeconstruct(outExprcond,outExprleft,outExprright)=>(cond,left,right)=(Cond,Left,Right);
publicoverrideTEval(params(stringName,TValue)[]args)=>Cond.Eval(args)==T.Zero?Right.Eval(args):Left.Eval(args);
}
}
完成。我們用了僅僅幾十行代碼就完成了全部的核心邏輯!這便是模式匹配的強大之處:簡潔、直觀且高效。
表達式判等
至此為止,我們已經(jīng)完成了所有的表達式構造、解構和計算的實現(xiàn)。接下來我們?yōu)槊恳粋€表達式實現(xiàn)判等邏輯,即判斷兩個表達式(字面上)是否相同。
例如a == b ? 2 : 4
和a == b ? 2 : 5
不相同,a == b ? 2 : 4
和c == d ? 2 : 4
不相同,而a == b ? 2 : 4
和a == b ? 2 : 4
相同。
為了實現(xiàn)該功能,我們重寫每一個表達式的Equals
和GetHashCode
方法。
常數(shù)表達式
常數(shù)表達式判等只需要判斷常數(shù)值是否相等即可:
publicoverrideboolEquals(object?obj)=>objisConstantExpr(varvalue)&&value==Value;
publicoverrideintGetHashCode()=>Value.GetHashCode();
參數(shù)表達式
參數(shù)表達式判等只需要判斷參數(shù)名是否相等即可:
publicoverrideboolEquals(object?obj)=>objisParameterExpr(varname)&&name==Name;
publicoverrideintGetHashCode()=>Name.GetHashCode();
一元表達式
一元表達式判等,需要判斷被比較的表達式是否是一元表達式,如果也是的話則判斷運算符和操作數(shù)是否相等:
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))
。
二元表達式
和一元表達式差不多,區(qū)別在于這次多了一個操作數(shù):
publicoverrideboolEquals(object?obj)=>objisBinaryExpr({Operator:varop},varleft,varright)&&(op,left,right).Equals((Op.Operator,Left,Right));
publicoverrideintGetHashCode()=>(Op,Left,Right).GetHashCode();
三元表達式
和二元表達式差不多,只不過運算符Op
變成了操作數(shù)Cond
:
publicoverrideboolEquals(object?obj)=>objisTernaryExpr(varcond,varleft,varright)&&cond.Equals(Cond)&&left.Equals(Left)&&right.Equals(Right);
publicoverrideintGetHashCode()=>(Cond,Left,Right).GetHashCode();
到此為止,我們?yōu)樗械谋磉_式都實現(xiàn)了判等。
一些工具方法
我們重載一些Expr
的運算符方便我們使用:
publicstaticExproperator~(Exproperand)=>newUnaryExpr(new(Operators.Inv),operand);
publicstaticExproperator!(Exproperand)=>newUnaryExpr(new(Operators.LogicalNot),operand);
publicstaticExproperator-(Exproperand)=>newUnaryExpr(new(Operators.Min),operand);
publicstaticExproperator+(Exprleft,Exprright)=>newBinaryExpr(new(Operators.Add),left,right);
publicstaticExproperator-(Exprleft,Exprright)=>newBinaryExpr(new(Operators.Sub),left,right);
publicstaticExproperator*(Exprleft,Exprright)=>newBinaryExpr(new(Operators.Mul),left,right);
publicstaticExproperator/(Exprleft,Exprright)=>newBinaryExpr(new(Operators.Div),left,right);
publicstaticExproperator&(Exprleft,Exprright)=>newBinaryExpr(new(Operators.And),left,right);
publicstaticExproperator|(Exprleft,Exprright)=>newBinaryExpr(new(Operators.Or),left,right);
publicstaticExproperator^(Exprleft,Exprright)=>newBinaryExpr(new(Operators.Xor),left,right);
publicstaticExproperator>(Exprleft,Exprright)=>newBinaryExpr(new(Operators.Gt),left,right);
publicstaticExproperator<(Expr?left,?Expr?right)?=>newBinaryExpr(new(Operators.Lt),left,right);
publicstaticExproperator>=(Exprleft,Exprright)=>newBinaryExpr(new(Operators.Ge),left,right);
publicstaticExproperator<=(Expr?left,?Expr?right)?=>newBinaryExpr(new(Operators.Le),left,right);
publicstaticExproperator==(Exprleft,Exprright)=>newBinaryExpr(new(Operators.Eq),left,right);
publicstaticExproperator!=(Exprleft,Exprright)=>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
,這里實際上并不需要重寫,因此直接調(diào)用base
上的方法保持默認行為即可。
然后編寫兩個擴展方法用來方便構造三元表達式,和從Description
中獲取運算符的名字:
publicstaticclassExtensions
{
publicstaticExprSwitch(thisExprcond,Exprleft,Exprright)whereT:IBinaryNumber=>newExpr.TernaryExpr(cond,left,right);
publicstaticstring?GetName(thisTop)whereT:Enum=>typeof(T).GetMember(op.ToString()).FirstOrDefault()?.GetCustomAttribute()?.Description;
}
由于有參數(shù)表達式參與時需要我們提前提供參數(shù)值才能調(diào)用Eval
進行計算,因此我們寫一個交互式的Eval
來在計算過程中遇到參數(shù)表達式時提示用戶輸入值,起名叫做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,TValue)[]GetArgs(Exprexpr,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
{
[varhead,..]whenhead==name=>Array.Empty<(stringName,TValue)>(),
[_,..vartail]=>GetArgs(expr,refassigned,reftail),
[]=>new[]{(name,GetArg(name,refassigned))}
},
_=>Array.Empty<(stringName,TValue)>()
};
這里在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,即可得到運算結果:
Parameterx:42
Parametery:27
Parameterz: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));
得到輸出:
True
False
活動模式
在未來,C# 將會引入活動模式,該模式允許用戶自定義模式匹配的方法,例如:
staticboolEven(thisTvalue)whereT:IBinaryInteger=>value%2==0;
上述代碼定義了一個T
的擴展方法Even
,用來匹配value
是否為偶數(shù),于是我們便可以這么使用:
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。
總結
模式匹配極大的方便了我們編寫出簡潔且可讀性高的高質(zhì)量代碼,并且會自動幫我們做窮舉檢查,防止我們漏掉情況。此外,使用模式匹配時,編譯器也會幫我們優(yōu)化代碼,減少完成匹配所需要的比較次數(shù),最終減少分支并提升運行效率。
本文中的例子為了覆蓋到全部的模式,不一定采用了最優(yōu)的寫法,這一點各位讀者們也請注意。
來源:新機器視覺
審核編輯:湯梓紅
-
Switch
+關注
關注
1文章
533瀏覽量
58167 -
C語言
+關注
關注
180文章
7601瀏覽量
136251 -
代碼
+關注
關注
30文章
4753瀏覽量
68369 -
計算器
+關注
關注
16文章
437瀏覽量
37298 -
模式匹配
+關注
關注
0文章
9瀏覽量
5843
原文標題:【光電智造】C# 模式匹配完全指南
文章出處:【微信號:今日光電,微信公眾號:今日光電】歡迎添加關注!文章轉載請注明出處。
發(fā)布評論請先 登錄
相關推薦
評論