欢迎访问 生活随笔!

凯发k8官方网

当前位置: 凯发k8官方网 > 编程语言 > c# >内容正文

c#

c# 模式匹配完全指南 -凯发k8官方网

发布时间:2023/10/11 c# 161 如意码农
凯发k8官方网 收集整理的这篇文章主要介绍了 c# 模式匹配完全指南 小编觉得挺不错的,现在分享给大家,帮大家做个参考.

前言

自从 2017 年 c# 7.0 版本开始引入声明模式和常数模式匹配开始,到 2022 年的 c# 11 为止,最后一个板块列表模式和切片模式匹配也已经补齐,当初计划的模式匹配内容已经基本全部完成。

c# 在模式匹配方面下一步计划则是支持活动模式(active pattern),这一部分将在本文最后进行介绍,而在介绍未来的模式匹配计划之前,本文主题是对截止 c# 11 模式匹配的(不)完全指南,希望能对各位开发者们提升代码编写效率、可读性和质量有所帮助。

模式匹配

要使用模式匹配,首先要了解什么是模式。在使用正则表达式匹配字符串时,正则表达式自己就是一个模式,而对字符串使用这段正则表达式进行匹配的过程就是模式匹配。而在代码中也是同样的,我们对对象采用某种模式进行匹配的过程就是模式匹配。

c# 11 支持的模式有很多,包含:

  • 声明模式
  • 类型模式
  • 常数模式
  • 关系模式
  • 逻辑模式
  • 属性模式
  • 位置模式
  • var 模式
  • 丢弃模式
  • 列表模式
  • 切片模式

而其中,不少模式都支持递归,也就意味着可以模式嵌套模式,以此来实现更加强大的匹配功能。

那么接下来就对这些模式进行介绍。

实例:表达式计算器

为了更直观地介绍模式匹配,我们接下来利用模式匹配来编写一个表达式计算器。

为了编写表达式计算器,首先我们需要对表达式进行抽象:

public abstract partial class expr where t : ibinarynumber
{
public abstract t eval(params (string name, t value)[] args);
}

我们用上面这个 expr 来表示一个表达式,其中 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 where t : ibinarynumber
{
public class constantexpr : expr
{
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 where t : ibinarynumber
{
public class parameterexpr : expr
{
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 tailvar 模式,用于将匹配结果赋值给变量。
  • [var head, ..var tail]:匹配一个列表,将它第一个元素赋值给 head,剩下元素的切片赋值给 tail,这个切片里可以没有元素。
  • [var (name, value), ..var tail]:匹配一个列表,将它第一个元素赋值给 (name, value),剩下元素的切片赋值给 tail,这个切片里可以没有元素。其中 (name, value) 是位置模式,用于将第一个元素的解构结果根据位置分别赋值给 namevalue,也可以写成 (var name, var value)

一元表达式

一元表达式用来处理只有一个操作数的计算,例如非、取反等。

public abstract partial class expr where t : ibinarynumber
{
public class unaryexpr : expr
{
public unaryexpr(unaryoperator op, expr expr) => (op, expr) = (op, expr); public unaryoperator op { get; }
public expr expr { get; }
public void deconstruct(out unaryoperator op, out expr 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 之外还有 andnot

由于我们在上面穷举了枚举中所有的一元运算符,因此也可以将 > 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 where t : ibinarynumber
{
public class binaryexpr : expr
{
public binaryexpr(binaryoperator op, expr left, expr right) => (op, left, right) = (op, left, right); public binaryoperator op { get; }
public expr left { get; }
public expr right { get; }
public void deconstruct(out binaryoperator op, out expr left, out expr 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 where t : ibinarynumber
{
public class ternaryexpr : expr
{
public ternaryexpr(expr cond, expr left, expr right) => (cond, left, right) = (cond, left, right); public expr cond { get; }
public expr left { get; }
public expr right { get; }
public void deconstruct(out expr cond, out expr left, out expr 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 : 4a == b ? 2 : 5 不相同,a == b ? 2 : 4c == d ? 2 : 4 不相同,而 a == b ? 2 : 4a == b ? 2 : 4 相同。

为了实现该功能,我们重写每一个表达式的 equalsgethashcode 方法。

常数表达式

常数表达式判等只需要判断常数值是否相等即可:

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 string tostring() => $"({left}) {op.operator.getname()} ({right})";
public override bool equals(object? obj) => obj is binaryexpr({ operator: var op }, var left, var right) && (op, left, right).equals((op.operator, left, right));

三元表达式

和二元表达式差不多,只不过运算符 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 的运算符方便我们使用:

public static expr operator ~(expr operand) => new unaryexpr(new(operators.inv), operand);
public static expr operator !(expr operand) => new unaryexpr(new(operators.logicalnot), operand);
public static expr operator -(expr operand) => new unaryexpr(new(operators.min), operand);
public static expr operator (expr left, expr right) => new binaryexpr(new(operators.add), left, right);
public static expr operator -(expr left, expr right) => new binaryexpr(new(operators.sub), left, right);
public static expr operator *(expr left, expr right) => new binaryexpr(new(operators.mul), left, right);
public static expr operator /(expr left, expr right) => new binaryexpr(new(operators.div), left, right);
public static expr operator &(expr left, expr right) => new binaryexpr(new(operators.and), left, right);
public static expr operator |(expr left, expr right) => new binaryexpr(new(operators.or), left, right);
public static expr operator ^(expr left, expr right) => new binaryexpr(new(operators.xor), left, right);
public static expr operator >(expr left, expr right) => new binaryexpr(new(operators.gt), left, right);
public static expr operator <(expr left, expr right) => new binaryexpr(new(operators.lt), left, right);
public static expr operator >=(expr left, expr right) => new binaryexpr(new(operators.ge), left, right);
public static expr operator <=(expr left, expr right) => new binaryexpr(new(operators.le), left, right);
public static expr operator ==(expr left, expr right) => new binaryexpr(new(operators.eq), left, right);
public static expr operator !=(expr left, expr right) => new binaryexpr(new(operators.ne), left, right);
public static implicit operator expr(t value) => new constantexpr(value);
public static implicit operator expr(string name) => new parameterexpr(name);
public static implicit operator expr(bool value) => new constantexpr(value ? t.one : t.zero); public override bool equals(object? obj) => base.equals(obj);
public override int gethashcode() => base.gethashcode();

由于重载了 ==!=,编译器为了保险起见提示我们重写 equalsgethashcode,这里实际上并不需要重写,因此直接调用 base 上的方法保持默认行为即可。

然后编写两个扩展方法用来方便构造三元表达式,和从 description 中获取运算符的名字:

public static class extensions
{
public static expr switch(this expr cond, expr left, expr right) where t : ibinarynumber => new expr.ternaryexpr(cond, left, right);
public static string? getname(this t op) where t : enum => typeof(t).getmember(op.tostring()).firstordefault()?.getcustomattribute()?.description;
}

由于有参数表达式参与时需要我们提前提供参数值才能调用 eval 进行计算,因此我们写一个交互式的 eval 来在计算过程中遇到参数表达式时提示用户输入值,起名叫做 interactiveeval

public t interactiveeval()
{
var names = array.empty();
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 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 a = 4;
expr b = -3;
expr x = "x";
expr c = !((a b) * (a - b) > x);
expr y = "y";
expr z = "z";
expr 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))

然后我们给 xyz 分别设置成 42、27 和 35,即可得到运算结果:

parameter x: 42
parameter y: 27
parameter z: 35
-9

再测测表达式判等逻辑:

expr expr1, expr2, expr3;
{
expr a = 4;
expr b = -3;
expr x = "x";
expr c = !((a b) * (a - b) > x);
expr y = "y";
expr z = "z";
expr1 = (c.switch(y, z) - a > x).switch(z a, y / b);
} {
expr a = 4;
expr b = -3;
expr x = "x";
expr c = !((a b) * (a - b) > x);
expr y = "y";
expr z = "z";
expr2 = (c.switch(y, z) - a > x).switch(z a, y / b);
} {
expr a = 4;
expr b = -3;
expr x = "x";
expr c = !((a b) * (a - b) > x);
expr y = "y";
expr 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(this t value) where t : ibinaryinteger => 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。

后记

模式匹配极大的方便了我们编写出简洁且可读性高的高质量代码,并且会自动帮我们做穷举检查,防止我们漏掉情况。此外,使用模式匹配时,编译器也会帮我们优化代码,减少完成匹配所需要的比较次数,最终减少分支并提升运行效率。

本文中的例子为了覆盖到全部的模式,不一定采用了最优的写法,这一点各位读者们也请注意。

本文中的表达式计算器全部代码可以前往我的 github 仓库获取:https://github.com/hez2010/patternmatchingexpr

总结

以上是凯发k8官方网为你收集整理的c# 模式匹配完全指南的全部内容,希望文章能够帮你解决所遇到的问题。

如果觉得凯发k8官方网网站内容还不错,欢迎将凯发k8官方网推荐给好友。

  • 上一篇:
  • 下一篇:
网站地图