您好,登錄后才能下訂單哦!
這篇文章主要介紹“C#模式匹配有哪些及怎么實現(xiàn)”,在日常操作中,相信很多人在C#模式匹配有哪些及怎么實現(xiàn)問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”C#模式匹配有哪些及怎么實現(xiàn)”的疑惑有所幫助!接下來,請跟著小編一起來學(xué)習(xí)吧!
要使用模式匹配,首先要了解什么是模式。在使用正則表達(dá)式匹配字符串時,正則表達(dá)式自己就是一個模式,而對字符串使用這段正則表達(dá)式進(jìn)行匹配的過程就是模式匹配。而在代碼中也是同樣的,我們對對象采用某種模式進(jìn)行匹配的過程就是模式匹配。
C# 11 支持的模式有很多,包含:
聲明模式(declaration pattern)
類型模式(type pattern)
常數(shù)模式(constant pattern)
關(guān)系模式(relational pattern)
邏輯模式(logical pattern)
屬性模式(property pattern)
位置模式(positional pattern)
var 模式(var pattern)
丟棄模式(discard pattern)
列表模式(list pattern)
切片模式(slice pattern)
而其中,不少模式都支持遞歸,也就意味著可以模式嵌套模式,以此來實現(xiàn)更加強大的匹配功能。
模式匹配可以通過 switch
表達(dá)式來使用,也可以在普通的 switch
語句中作為 case
使用,還可以在 if
條件中通過 is
來使用。本文主要在 switch
表達(dá)式中使用模式匹配。
那么接下來就對這些模式進(jìn)行介紹。
為了更直觀地介紹模式匹配,我們接下來利用模式匹配來編寫一個表達(dá)式計算器。
為了編寫表達(dá)式計算器,首先我們需要對表達(dá)式進(jìn)行抽象:
public abstract partial class Expr<T> where T : IBinaryNumber<T> { public abstract T Eval(params (string Name, T Value)[] args); }
我們用上面這個 Expr<T>
來表示一個表達(dá)式,其中 T
是操作數(shù)的類型,然后進(jìn)一步將表達(dá)式分為常數(shù)表達(dá)式 ConstantExpr
、參數(shù)表達(dá)式 ParameterExpr
、一元表達(dá)式 UnaryExpr
、二元表達(dá)式 BinaryExpr
和三元表達(dá)式 TernaryExpr
。最后提供一個 Eval
方法,用來計算表達(dá)式的值,該方法可以傳入一個 args
來提供表達(dá)式計算所需要的參數(shù)。
有了一、二元表達(dá)式自然也需要運算符,例如加減乘除等,我們也同時定義 Operator
來表示運算符:
public abstract record Operator { public record UnaryOperator(Operators Operator) : Operator; public record BinaryOperator(BinaryOperators Operator) : Operator; }
然后設(shè)置允許的運算符,其中前三個是一元運算符,后面的是二元運算符:
public enum Operators { [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)邏輯與或非,關(guān)于這一點,我們直接使用 0
來代表 false
,非 0
代表 true
。
接下來就是分別實現(xiàn)各類表達(dá)式的時間!
常數(shù)表達(dá)式很簡單,它保存一個常數(shù)值,因此只需要在構(gòu)造方法中將用戶提供的值存儲下來。它的 Eval
實現(xiàn)也只需要簡單返回存儲的值即可:
public abstract partial class Expr<T> where T : IBinaryNumber<T> { public class ConstantExpr : Expr<T> { public ConstantExpr(T value) => Value = value; public T Value { get; } public void Deconstruct(out T value) => value = Value; public override T Eval(params (string Name, T Value)[] args) => Value; } }
參數(shù)表達(dá)式用來定義表達(dá)式計算過程中的參數(shù),允許用戶在對表達(dá)式執(zhí)行 Eval
計算結(jié)果的時候傳參,因此只需要存儲參數(shù)名。它的 Eval
實現(xiàn)需要根據(jù)參數(shù)名在 args
中找出對應(yīng)的參數(shù)值:
public abstract partial class Expr<T> where T : IBinaryNumber<T> { public class ParameterExpr : Expr<T> { public ParameterExpr(string name) => Name = name; public string Name { get; } public void Deconstruct(out string name) => name = Name; // 對 args 進(jìn)行模式匹配 public override T Eval(params (string Name, T Value)[] args) => args switch { // 如果 args 有至少一個元素,那我們把第一個元素拿出來存為 (name, value), // 然后判斷 name 是否和本參數(shù)表達(dá)式中存儲的參數(shù)名 Name 相同。 // 如果相同則返回 value,否則用 args 除去第一個元素剩下的參數(shù)繼續(xù)匹配。 [var (name, value), .. var tail] => name == Name ? value : Eval(tail), // 如果 args 是空列表,則說明在 args 中沒有找到名字和 Name 相同的參數(shù),拋出異常 [] => throw new InvalidOperationException($"Expected an argument named {Name}.") }; } }
模式匹配會從上往下依次進(jìn)行匹配,直到匹配成功為止。
上面的代碼中你可能會好奇 [var (name, value), .. var tail]
是個什么模式,這個模式整體看是列表模式,并且列表模式內(nèi)組合使用聲明模式、位置模式和切片模式。例如:
[]
:匹配一個空列表。[1, _, 3]
:匹配一個長度是 3,并且首尾元素分別是 1、3 的列表。其中 _
是丟棄模式,表示任意元素。
[_, .., 3]
:匹配一個末元素是 3,并且 3 不是首元素的列表。其中 ..
是切片模式,表示任意切片。
[1, ..var tail]
:匹配一個首元素是 1 的列表,并且將除了首元素之外元素的切片賦值給 tail
。其中 var tail
是 var
模式,用于將匹配結(jié)果賦值給變量。
[var head, ..var tail]
:匹配一個列表,將它第一個元素賦值給 head
,剩下元素的切片賦值給 tail
,這個切片里可以沒有元素。
[var (name, value), ..var tail]
:匹配一個列表,將它第一個元素賦值給 (name, value)
,剩下元素的切片賦值給 tail
,這個切片里可以沒有元素。其中 (name, value)
是位置模式,用于將第一個元素的解構(gòu)結(jié)果根據(jù)位置分別賦值給 name
和 value
,也可以寫成 (var name, var value)
。
一元表達(dá)式用來處理只有一個操作數(shù)的計算,例如非、取反等。
public abstract partial class Expr<T> where T : IBinaryNumber<T> { public class UnaryExpr : Expr<T> { public UnaryExpr(UnaryOperator op, Expr<T> expr) => (Op, Expr) = (op, expr); public UnaryOperator Op { get; } public Expr<T> Expr { get; } public void Deconstruct(out UnaryOperator op, out Expr<T> expr) => (op, expr) = (Op, Expr); // 對 Op 進(jìn)行模式匹配 public override T Eval(params (string Name, T Value)[] args) => Op switch { // 如果 Op 是 UnaryOperator,則將其解構(gòu)結(jié)果賦值給 op,然后對 op 進(jìn)行匹配,op 是一個枚舉,而 .NET 中的枚舉值都是整數(shù) UnaryOperator(var op) => op switch { // 如果 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.LogicalNot or < 0 => throw new InvalidOperationException($"Expected an unary operator, but got {op}.") }, // 如果 Op 不是 UnaryOperator _ => throw new InvalidOperationException("Expected an unary operator.") }; } }
上面的代碼中,首先利用了 C# 元組可作為左值的特性,分別使用一行代碼就做完了構(gòu)造方法和解構(gòu)方法的賦值:(Op, Expr) = (op, expr)
和 (op, expr) = (Op, Expr)
。如果你好奇能否利用這個特性交換多個變量,答案是可以!
在 Eval
中,首先將類型模式、位置模式和聲明模式組合成 UnaryOperator(var op)
,表示匹配 UnaryOperator
類型、并且能解構(gòu)出一個元素的東西,如果匹配則將解構(gòu)出來的那個元素賦值給 op
。
然后我們接著對解構(gòu)出來的 op
進(jìn)行匹配,這里用到了常數(shù)模式,例如 Operators.Inv
用來匹配 op
是否是 Operators.Inv
。常數(shù)模式可以使用各種常數(shù)對對象進(jìn)行匹配。
這里的 > Operators.LogicalNot
和 < 0
則是關(guān)系模式,分別用于匹配大于 Operators.LogicalNot
的值和小于 0
的指。然后利用邏輯模式 or
將兩個模式組合起來表示或的關(guān)系。邏輯模式除了 or
之外還有 and
和 not
。
由于我們在上面窮舉了枚舉中所有的一元運算符,因此也可以將 > Operators.LogicalNot or < 0
換成丟棄模式 _
或者 var 模式 var foo
,兩者都用來匹配任意的東西,只不過前者匹配到后直接丟棄,而后者聲明了個變量 foo
將匹配到的值放到里面:
op switch { // ... _ => throw new InvalidOperationException($"Expected an unary operator, but got {op}.") }
或
op switch { // ... var foo => throw new InvalidOperationException($"Expected an unary operator, but got {foo}.") }
二元表達(dá)式用來表示操作數(shù)有兩個的表達(dá)式。有了一元表達(dá)式的編寫經(jīng)驗,二元表達(dá)式如法炮制即可。
public abstract partial class Expr<T> where T : IBinaryNumber<T> { public class BinaryExpr : Expr<T> { public BinaryExpr(BinaryOperator op, Expr<T> left, Expr<T> right) => (Op, Left, Right) = (op, left, right); public BinaryOperator Op { get; } public Expr<T> Left { get; } public Expr<T> Right { get; } public void Deconstruct(out BinaryOperator op, out Expr<T> left, out Expr<T> right) => (op, left, right) = (Op, Left, Right); public override T Eval(params (string Name, T Value)[] args) => Op switch { BinaryOperator(var op) => op switch { 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 => throw new InvalidOperationException($"Unexpected a binary operator, but got {op}.") }, _ => throw new InvalidOperationException("Unexpected a binary operator.") }; } }
同理,也可以將 < Operators.Add or > Operators.LogicalOr
換成丟棄模式或者 var 模式。
三元表達(dá)式包含三個操作數(shù):條件表達(dá)式 Cond
、為真的表達(dá)式 Left
、為假的表達(dá)式 Right
。該表達(dá)式中會根據(jù) Cond
是否為真來選擇取 Left
還是 Right
,實現(xiàn)起來較為簡單:
public abstract partial class Expr<T> where T : IBinaryNumber<T> { public class TernaryExpr : Expr<T> { public TernaryExpr(Expr<T> cond, Expr<T> left, Expr<T> right) => (Cond, Left, Right) = (cond, left, right); public Expr<T> Cond { get; } public Expr<T> Left { get; } public Expr<T> Right { get; } public void Deconstruct(out Expr<T> cond, out Expr<T> left, out Expr<T> right) => (cond, left, right) = (Cond, Left, Right); public override T Eval(params (string Name, T Value)[] args) => Cond.Eval(args) == T.Zero ? Right.Eval(args) : Left.Eval(args); } }
完成。我們用了僅僅幾十行代碼就完成了全部的核心邏輯!這便是模式匹配的強大之處:簡潔、直觀且高效。
至此為止,我們已經(jīng)完成了所有的表達(dá)式構(gòu)造、解構(gòu)和計算的實現(xiàn)。接下來我們?yōu)槊恳粋€表達(dá)式實現(xiàn)判等邏輯,即判斷兩個表達(dá)式(字面上)是否相同。
例如 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)該功能,我們重寫每一個表達(dá)式的 Equals
和 GetHashCode
方法。
常數(shù)表達(dá)式判等只需要判斷常數(shù)值是否相等即可:
public override bool Equals(object? obj) => obj is ConstantExpr(var value) && value == Value; public override int GetHashCode() => Value.GetHashCode();
參數(shù)表達(dá)式判等只需要判斷參數(shù)名是否相等即可:
public override bool Equals(object? obj) => obj is ParameterExpr(var name) && name == Name; public override int GetHashCode() => Name.GetHashCode();
一元表達(dá)式判等,需要判斷被比較的表達(dá)式是否是一元表達(dá)式,如果也是的話則判斷運算符和操作數(shù)是否相等:
public override bool Equals(object? obj) => obj is UnaryExpr({ Operator: var op }, var expr) && (op, expr).Equals((Op.Operator, Expr)); public override int GetHashCode() => (Op, Expr).GetHashCode();
上面的代碼中用到了屬性模式 { Operator: var op }
,用來匹配屬性的值,這里直接組合了聲明模式將屬性 Operator
的值賦值給了 expr
。另外,C# 中的元組可以組合起來進(jìn)行判等操作,因此不需要寫 op.Equals(Op.Operator) && expr.Equals(Expr)
,而是可以直接寫 (op, expr).Equals((Op.Operator, Expr))
。
和一元表達(dá)式差不多,區(qū)別在于這次多了一個操作數(shù):
public override bool Equals(object? obj) => obj is BinaryExpr({ Operator: var op }, var left, var right) && (op, left, right).Equals((Op.Operator, Left, Right)); public override int GetHashCode() => (Op, Left, Right).GetHashCode();
和二元表達(dá)式差不多,只不過運算符 Op
變成了操作數(shù) Cond
:
public override bool Equals(object? obj) => obj is TernaryExpr(var cond, var left, var right) && cond.Equals(Cond) && left.Equals(Left) && right.Equals(Right); public override int GetHashCode() => (Cond, Left, Right).GetHashCode();
到此為止,我們?yōu)樗械谋磉_(dá)式都實現(xiàn)了判等。
我們重載一些 Expr<T>
的運算符方便我們使用:
public static Expr<T> operator ~(Expr<T> operand) => new UnaryExpr(new(Operators.Inv), operand); public static Expr<T> operator !(Expr<T> operand) => new UnaryExpr(new(Operators.LogicalNot), operand); public static Expr<T> operator -(Expr<T> operand) => new UnaryExpr(new(Operators.Min), operand); public static Expr<T> operator +(Expr<T> left, Expr<T> right) => new BinaryExpr(new(Operators.Add), left, right); public static Expr<T> operator -(Expr<T> left, Expr<T> right) => new BinaryExpr(new(Operators.Sub), left, right); public static Expr<T> operator *(Expr<T> left, Expr<T> right) => new BinaryExpr(new(Operators.Mul), left, right); public static Expr<T> operator /(Expr<T> left, Expr<T> right) => new BinaryExpr(new(Operators.Div), left, right); public static Expr<T> operator &(Expr<T> left, Expr<T> right) => new BinaryExpr(new(Operators.And), left, right); public static Expr<T> operator |(Expr<T> left, Expr<T> right) => new BinaryExpr(new(Operators.Or), left, right); public static Expr<T> operator ^(Expr<T> left, Expr<T> right) => new BinaryExpr(new(Operators.Xor), left, right); public static Expr<T> operator >(Expr<T> left, Expr<T> right) => new BinaryExpr(new(Operators.Gt), left, right); public static Expr<T> operator <(Expr<T> left, Expr<T> right) => new BinaryExpr(new(Operators.Lt), left, right); public static Expr<T> operator >=(Expr<T> left, Expr<T> right) => new BinaryExpr(new(Operators.Ge), left, right); public static Expr<T> operator <=(Expr<T> left, Expr<T> right) => new BinaryExpr(new(Operators.Le), left, right); public static Expr<T> operator ==(Expr<T> left, Expr<T> right) => new BinaryExpr(new(Operators.Eq), left, right); public static Expr<T> operator !=(Expr<T> left, Expr<T> right) => new BinaryExpr(new(Operators.Ne), left, right); public static implicit operator Expr<T>(T value) => new ConstantExpr(value); public static implicit operator Expr<T>(string name) => new ParameterExpr(name); public static implicit operator Expr<T>(bool value) => new ConstantExpr(value ? T.One : T.Zero); public override bool Equals(object? obj) => base.Equals(obj); public override int GetHashCode() => base.GetHashCode();
由于重載了 ==
和 !=
,編譯器為了保險起見提示我們重寫 Equals
和 GetHashCode
,這里實際上并不需要重寫,因此直接調(diào)用 base
上的方法保持默認(rèn)行為即可。
然后編寫兩個擴(kuò)展方法用來方便構(gòu)造三元表達(dá)式,和從 Description
中獲取運算符的名字:
public static class Extensions { public static Expr<T> Switch<T>(this Expr<T> cond, Expr<T> left, Expr<T> right) where T : IBinaryNumber<T> => new Expr<T>.TernaryExpr(cond, left, right); public static string? GetName<T>(this T op) where T : Enum => typeof(T).GetMember(op.ToString()).FirstOrDefault()?.GetCustomAttribute<DescriptionAttribute>()?.Description; }
由于有參數(shù)表達(dá)式參與時需要我們提前提供參數(shù)值才能調(diào)用 Eval
進(jìn)行計算,因此我們寫一個交互式的 Eval
來在計算過程中遇到參數(shù)表達(dá)式時提示用戶輸入值,起名叫做 InteractiveEval
:
public T InteractiveEval() { var names = Array.Empty<string>(); 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<T> expr, ref string[] assigned, ref string[] filter) => expr switch { TernaryExpr(var cond, var left, var right) => 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) => GetArgs(left, ref assigned, ref assigned).Concat(GetArgs(right, ref assigned, refassigned)).ToArray(), UnaryExpr(_, var uexpr) => GetArgs(uexpr, ref assigned, ref assigned), ParameterExpr(var name) => filter switch { [var head, ..] when head == name => Array.Empty<(string Name, T Value)>(), [_, .. var tail] => GetArgs(expr, ref assigned, ref tail), [] => new[] { (name, GetArg(name, ref assigned)) } }, _ => Array.Empty<(string Name, T Value)>() };
這里在 GetArgs
方法中,模式 [var head, ..]
后面跟了一個 when head == name
,這里的 when
用來給模式匹配指定額外的條件,僅當(dāng)條件滿足時才匹配成功,因此 [var head, ..] when head == name
的含義是,匹配至少含有一個元素的列表,并且將頭元素賦值給 head
,且僅當(dāng) head == name
時匹配才算成功。
最后我們再重寫 ToString
方法方便輸出表達(dá)式,就全部大功告成了。
接下來讓我測試測試我們編寫的表達(dá)式計算器:
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
分別設(shè)置成 42、27 和 35,即可得到運算結(jié)果:
Parameter x: 42
Parameter y: 27
Parameter z: 35
-9
再測測表達(dá)式判等邏輯:
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# 將會引入活動模式,該模式允許用戶自定義模式匹配的方法,例如:
static bool Even<T>(this T value) where T : IBinaryInteger<T> => value % 2 == 0;
上述代碼定義了一個 T
的擴(kuò)展方法 Even
,用來匹配 value
是否為偶數(shù),于是我們便可以這么使用:
var x = 3; var y = x switch { Even() => "even", _ => "odd" };
此外,該模式還可以和解構(gòu)模式結(jié)合,允許用戶自定義解構(gòu)行為,例如:
static bool Int(this string value, out int result) => int.TryParse(value, out result);
然后使用的時候:
var x = "3"; var y = x switch { Int(var result) => result, _ => 0 };
即可對 x
這個字符串進(jìn)行匹配,如果 x
可以被解析為 int
,就取解析結(jié)果 result
,否則取 0。
到此,關(guān)于“C#模式匹配有哪些及怎么實現(xiàn)”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識,請繼續(xù)關(guān)注億速云網(wǎng)站,小編會繼續(xù)努力為大家?guī)砀鄬嵱玫奈恼拢?/p>
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。