這篇文章主要介紹“C#模式匹配有哪些及怎么實現”,在日常操作中,相信很多人在C#模式匹配有哪些及怎么實現問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”C#模式匹配有哪些及怎么實現”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
要使用模式匹配,首先要了解什么是模式。在使用正則表達式匹配字符串時,正則表達式自己就是一個模式,而對字符串使用這段正則表達式進行匹配的過程就是模式匹配。而在代碼中也是同樣的,我們對對象采用某種模式進行匹配的過程就是模式匹配。
C# 11 支持的模式有很多,包含:
聲明模式(declaration pattern)
類型模式(type pattern)
常數模式(constant pattern)
關系模式(relational pattern)
邏輯模式(logical pattern)
屬性模式(property pattern)
位置模式(positional pattern)
var 模式(var pattern)
丟棄模式(discard pattern)
列表模式(list pattern)
切片模式(slice pattern)
而其中,不少模式都支持遞歸,也就意味著可以模式嵌套模式,以此來實現更加強大的匹配功能。
模式匹配可以通過 switch 表達式來使用,也可以在普通的 switch 語句中作為 case 使用,還可以在 if 條件中通過 is 來使用。本文主要在 switch 表達式中使用模式匹配。
那么接下來就對這些模式進行介紹。
為了更直觀地介紹模式匹配,我們接下來利用模式匹配來編寫一個表達式計算器。
為了編寫表達式計算器,首先我們需要對表達式進行抽象:
public abstract partial class Expr<T> where T : IBinaryNumber<T>
{
public abstract T Eval(params (string Name, T Value)[] args);
}我們用上面這個 Expr<T> 來表示一個表達式,其中 T 是操作數的類型,然后進一步將表達式分為常數表達式 ConstantExpr、參數表達式 ParameterExpr、一元表達式 UnaryExpr、二元表達式 BinaryExpr 和三元表達式 TernaryExpr。最后提供一個 Eval 方法,用來計算表達式的值,該方法可以傳入一個 args 來提供表達式計算所需要的參數。
有了一、二元表達式自然也需要運算符,例如加減乘除等,我們也同時定義 Operator 來表示運算符:
public abstract record Operator
{
public record UnaryOperator(Operators Operator) : Operator;
public record BinaryOperator(BinaryOperators Operator) : Operator;
}然后設置允許的運算符,其中前三個是一元運算符,后面的是二元運算符:
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 的運算能如何實現邏輯與或非,關于這一點,我們直接使用 0 來代表 false,非 0 代表 true。
接下來就是分別實現各類表達式的時間!
常數表達式很簡單,它保存一個常數值,因此只需要在構造方法中將用戶提供的值存儲下來。它的 Eval 實現也只需要簡單返回存儲的值即可:
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;
}
}參數表達式用來定義表達式計算過程中的參數,允許用戶在對表達式執行 Eval 計算結果的時候傳參,因此只需要存儲參數名。它的 Eval 實現需要根據參數名在 args 中找出對應的參數值:
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 進行模式匹配
public override T Eval(params (string Name, T Value)[] args) => args switch
{
// 如果 args 有至少一個元素,那我們把第一個元素拿出來存為 (name, value),
// 然后判斷 name 是否和本參數表達式中存儲的參數名 Name 相同。
// 如果相同則返回 value,否則用 args 除去第一個元素剩下的參數繼續匹配。
[var (name, value), .. var tail] => name == Name ? value : Eval(tail),
// 如果 args 是空列表,則說明在 args 中沒有找到名字和 Name 相同的參數,拋出異常
[] => throw new InvalidOperationException($"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)。
一元表達式用來處理只有一個操作數的計算,例如非、取反等。
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 進行模式匹配
public override T Eval(params (string Name, T Value)[] args) => Op switch
{
// 如果 Op 是 UnaryOperator,則將其解構結果賦值給 op,然后對 op 進行匹配,op 是一個枚舉,而 .NET 中的枚舉值都是整數
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# 元組可作為左值的特性,分別使用一行代碼就做完了構造方法和解構方法的賦值:(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 將匹配到的值放到里面:
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}.")
}二元表達式用來表示操作數有兩個的表達式。有了一元表達式的編寫經驗,二元表達式如法炮制即可。
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 模式。
三元表達式包含三個操作數:條件表達式 Cond、為真的表達式 Left、為假的表達式 Right。該表達式中會根據 Cond 是否為真來選擇取 Left 還是 Right,實現起來較為簡單:
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);
}
}完成。我們用了僅僅幾十行代碼就完成了全部的核心邏輯!這便是模式匹配的強大之處:簡潔、直觀且高效。
至此為止,我們已經完成了所有的表達式構造、解構和計算的實現。接下來我們為每一個表達式實現判等邏輯,即判斷兩個表達式(字面上)是否相同。
例如 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 方法。
常數表達式判等只需要判斷常數值是否相等即可:
public override bool Equals(object? obj) => obj is ConstantExpr(var value) && value == Value; public override int GetHashCode() => Value.GetHashCode();
參數表達式判等只需要判斷參數名是否相等即可:
public override bool Equals(object? obj) => obj is ParameterExpr(var name) && name == Name; public override int GetHashCode() => Name.GetHashCode();
一元表達式判等,需要判斷被比較的表達式是否是一元表達式,如果也是的話則判斷運算符和操作數是否相等:
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# 中的元組可以組合起來進行判等操作,因此不需要寫 op.Equals(Op.Operator) && expr.Equals(Expr),而是可以直接寫 (op, expr).Equals((Op.Operator, Expr))。
和一元表達式差不多,區別在于這次多了一個操作數:
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();和二元表達式差不多,只不過運算符 Op 變成了操作數 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();
到此為止,我們為所有的表達式都實現了判等。
我們重載一些 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,這里實際上并不需要重寫,因此直接調用 base 上的方法保持默認行為即可。
然后編寫兩個擴展方法用來方便構造三元表達式,和從 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;
}由于有參數表達式參與時需要我們提前提供參數值才能調用 Eval 進行計算,因此我們寫一個交互式的 Eval 來在計算過程中遇到參數表達式時提示用戶輸入值,起名叫做 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 用來給模式匹配指定額外的條件,僅當條件滿足時才匹配成功,因此 [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));得到輸出:
True
False
在未來,C# 將會引入活動模式,該模式允許用戶自定義模式匹配的方法,例如:
static bool Even<T>(this T value) where T : IBinaryInteger<T> => value % 2 == 0;
上述代碼定義了一個 T 的擴展方法 Even,用來匹配 value 是否為偶數,于是我們便可以這么使用:
var x = 3;
var y = x switch
{
Even() => "even",
_ => "odd"
};此外,該模式還可以和解構模式結合,允許用戶自定義解構行為,例如:
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 這個字符串進行匹配,如果 x 可以被解析為 int,就取解析結果 result,否則取 0。
到此,關于“C#模式匹配有哪些及怎么實現”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。