开关/模式匹配的想法
我最近一直在研究F#,尽pipe我不太可能马上跳过栅栏,但它确实突出了C#(或者库支持)可以让生活更轻松的一些领域。
特别是,我正在考虑F#的模式匹配function,它允许使用非常丰富的语法 – 比当前的开关/条件C#等价物更富有performance力。 我不会试图给出一个直接的例子(我的F#没有达到它),但总之它允许:
- 按types进行匹配(对被区分的联合体进行全覆盖检查)[注意,这也推断了绑定variables的types,赋予成员访问等]
- 按谓词匹配
- 上面的组合(可能还有一些我不知道的其他场景)
虽然C#最终可以借用一些丰富的内容,但在此期间,我一直在研究在运行时可以做些什么 – 例如,将一些对象敲在一起以便允许:
var getRentPrice = new Switch<Vehicle, int>() .Case<Motorcycle>(bike => 100 + bike.Cylinders * 10) // "bike" here is typed as Motorcycle .Case<Bicycle>(30) // returns a constant .Case<Car>(car => car.EngineType == EngineType.Diesel, car => 220 + car.Doors * 20) .Case<Car>(car => car.EngineType == EngineType.Gasoline, car => 200 + car.Doors * 20) .ElseThrow(); // or could use a Default(...) terminator
getRentPrice是一个Func <Vehicle,int>。
[注意 – 也许切换/案例是错误的条款…但它显示的想法]
对我来说,这比使用重复的if / else或者复合的三元条件(对于非重要的expression式变得非常混乱 – 括号内容丰富)等价得多。 它也避免了大量的强制转换,并允许简单的扩展(直接或通过扩展方法)到更具体的匹配,例如InRange(…)匹配与VB Select … Case“x to y “用法。
我只是想估计一下,如果人们认为从上面这样的结构(在没有语言支持的情况下)中有很多好处?
另外请注意,我已经玩了上面的3个变种:
- 用于评估的Func <TSource,TValue>版本 – 与组合三元条件语句相当
- 一个Action <TSource>版本 – 相当于if / else if / else if / else if / else
- expression式<Func <TSource,TValue >>版本 – 作为第一个,但任意LINQ提供者都可以使用
此外,使用基于expression式的版本可以使expression式树重新写入,基本上将所有分支内联到单个复合条件expression式中,而不是使用重复的调用。 我最近没有检查过,但在一些早期的Entity Framework构build中,我似乎回想起这是必要的,因为它不像InvocationExpression很多。 它还允许更有效地使用LINQ到对象,因为它避免了重复的委托调用 – testing显示了与上面(使用Expression表单)相同的速度(相比于等效的C#复合条件语句。 为了完整性,基于Func的版本花了C#条件语句的4倍,但仍然非常快,在大多数使用情况下不太可能成为主要瓶颈。
我欢迎任何想法/input/批评/等(关于更丰富的C#语言支持的可能性…这里希望; -p)。
Bart De Smet的优秀博客有一个8部分的系列文章,讲述你所描述的内容。 在这里find第一部分。
在尝试用C#做这种“function性”的事情之后(甚至试图写一本书),我得出的结论是,除了less数例外,这样的事情没有太多的帮助。
主要原因是像F#这样的语言从真正支持这些function中获得了很多的力量。 不是“你可以做到”,而是“很简单,很明显,它是预期的”。
例如,在模式匹配中,编译器会告诉您是否存在不完整的匹配或者何时不会再有匹配。 这对于开放式types来说是不太有用的,但是当匹配歧义的联合体或元组时,它非常漂亮。 在F#中,你期望人们模式匹配,并且它立即是有道理的。
这个“问题”是,一旦你开始使用一些function概念,想要继续是自然的。 但是,在C#中利用元组,函数,部分方法应用程序和currying,模式匹配,嵌套函数,generics,monad支持等非常快速。 这很有趣,一些非常聪明的人在C#中做了一些非常酷的事情,但实际上使用它感觉很沉重。
我最终在C#中经常使用(跨项目):
- 序列函数,通过IEnumerable的扩展方法。 像ForEach或Process(“Apply”)这样的东西 – 因为C#语法支持它,因此它适合在序列项上进行操作。
- 抽象通用语句模式。 复杂的try / catch / finally块或其他涉及的(通常是generics的)代码块。 扩展LINQ到SQL也适合在这里。
- 元组,在一定程度上。
**但请注意:缺less自动泛化和types推断确实阻碍了这些function的使用。 **
所有这些都像别人提到的那样,在一个小团队中,为了一个特定的目的,是的,也许他们可以帮助你,如果你坚持使用C#。 但根据我的经验,他们通常觉得比他们的价值更麻烦 – YMMV。
其他一些链接:
- Mono.Rocks操场有许多类似的东西(以及非function性编程但是有用的附加)。
- Luca Bolognese的functionC#库
- Matthew Podwysocki在MSDN上的function性C#
可以这么说,C#并没有简化开启types的原因是因为它主要是一个面向对象的语言,而以面向对象的方式来做这件事的“正确”方法是在Vehicle上定义一个GetRentPrice方法,在派生类中重写它。
也就是说,我花了一些时间来玩类似F#和Haskell这样具有这种function的多范式和函数式语言,而且我遇到过很多有用的地方(例如当你没有写出你需要打开的types,所以你不能在它们上面实现一个虚拟的方法),这是我欢迎的语言和歧视的工会。
[编辑:删除部分关于performance马克表示可能会短路]
另一个潜在的问题是可用性问题 – 从最终的调用中可以明显地看出,如果匹配不符合任何条件,会发生什么情况,但是如果符合两个或更多的条件,会出现什么情况呢? 它应该抛出一个exception? 应该返回第一场还是最后一场比赛?
我倾向于用来解决这种问题的方法是使用一个字典字段,types为key,lambda为值,这非常简洁,可以使用对象初始化语法来构造; 然而,这只是说明具体types,不允许附加谓词,因此可能不适合更复杂的情况。 [注意 – 如果你看看C#编译器的输出,它经常会把switch语句转换成基于字典的跳转表,所以看起来不是一个很好的理由,它不能支持types切换]
我不认为这些类似于语言扩展的库可能会被广泛接受,但是它们很有趣,对于在特定领域工作的小团队来说非常有用。 例如,如果你正在编写大量的“业务规则/逻辑”,可以进行任意types的testing,比如这样或那样,我可以看到它是如何得心应手的。
我不知道如果这可能是一个C#语言function(似乎有疑问,但谁能看到未来?)。
作为参考,相应的F#近似为:
let getRentPrice (v : Vehicle) = match v with | :? Motorcycle as bike -> 100 + bike.Cylinders * 10 | :? Bicycle -> 30 | :? Car as car when car.EngineType = Diesel -> 220 + car.Doors * 20 | :? Car as car when car.EngineType = Gasoline -> 200 + car.Doors * 20 | _ -> failwith "blah"
假设你已经沿着行定义了一个类层次结构
type Vehicle() = class end type Motorcycle(cyl : int) = inherit Vehicle() member this.Cylinders = cyl type Bicycle() = inherit Vehicle() type EngineType = Diesel | Gasoline type Car(engType : EngineType, doors : int) = inherit Vehicle() member this.EngineType = engType member this.Doors = doors
要回答你的问题,是的,我认为模式匹配句法结构是有用的。 我希望能看到它在C#中的语法支持。
这里是我的一个类的实现,它提供了(几乎)与你描述的相同的语法
public class PatternMatcher<Output> { List<Tuple<Predicate<Object>, Func<Object, Output>>> cases = new List<Tuple<Predicate<object>,Func<object,Output>>>(); public PatternMatcher() { } public PatternMatcher<Output> Case(Predicate<Object> condition, Func<Object, Output> function) { cases.Add(new Tuple<Predicate<Object>, Func<Object, Output>>(condition, function)); return this; } public PatternMatcher<Output> Case<T>(Predicate<T> condition, Func<T, Output> function) { return Case( o => o is T && condition((T)o), o => function((T)o)); } public PatternMatcher<Output> Case<T>(Func<T, Output> function) { return Case( o => o is T, o => function((T)o)); } public PatternMatcher<Output> Case<T>(Predicate<T> condition, Output o) { return Case(condition, x => o); } public PatternMatcher<Output> Case<T>(Output o) { return Case<T>(x => o); } public PatternMatcher<Output> Default(Func<Object, Output> function) { return Case(o => true, function); } public PatternMatcher<Output> Default(Output o) { return Default(x => o); } public Output Match(Object o) { foreach (var tuple in cases) if (tuple.Item1(o)) return tuple.Item2(o); throw new Exception("Failed to match"); } }
这里是一些testing代码:
public enum EngineType { Diesel, Gasoline } public class Bicycle { public int Cylinders; } public class Car { public EngineType EngineType; public int Doors; } public class MotorCycle { public int Cylinders; } public void Run() { var getRentPrice = new PatternMatcher<int>() .Case<MotorCycle>(bike => 100 + bike.Cylinders * 10) .Case<Bicycle>(30) .Case<Car>(car => car.EngineType == EngineType.Diesel, car => 220 + car.Doors * 20) .Case<Car>(car => car.EngineType == EngineType.Gasoline, car => 200 + car.Doors * 20) .Default(0); var vehicles = new object[] { new Car { EngineType = EngineType.Diesel, Doors = 2 }, new Car { EngineType = EngineType.Diesel, Doors = 4 }, new Car { EngineType = EngineType.Gasoline, Doors = 3 }, new Car { EngineType = EngineType.Gasoline, Doors = 5 }, new Bicycle(), new MotorCycle { Cylinders = 2 }, new MotorCycle { Cylinders = 3 }, }; foreach (var v in vehicles) { Console.WriteLine("Vehicle of type {0} costs {1} to rent", v.GetType(), getRentPrice.Match(v)); } }
模式匹配(如此处所述),其目的是根据types规范解构值。 但是,C#中类(或types)的概念与您不一致。
多范式语言devise没有错,相反,在C#中使用lambdaexpression式是非常好的,而Haskell可以为IO等做必要的事情。 但这不是一个非常优雅的解决scheme,而不是Haskell时尚。
但是,因为顺序过程式编程语言可以用lambda演算来理解,并且C#恰好适合顺序过程式语言的参数,所以很适合。 但是,从Haskell的纯粹的function语境中获取一些东西,然后把这个function变成一种不纯粹的语言,那么做到这一点,并不能保证有更好的结果。
我的意思是,这使得模式匹配勾选与语言devise和数据模型相关联。 话虽如此,我并不认为模式匹配是C#的一个有用特性,因为它不能解决典型的C#问题,也不适合于命令式编程范例。
恕我直言OO做这种事情的方式是访问者模式。 您的访问者成员方法只是作为案例结构,您可以让语言本身处理相应的调度,而不必“偷看”types。
尽pipe打开types并不是很“C-sharpey”,但我知道这个构造对于一般用途来说是非常有用的 – 我至less有一个可以使用它的个人项目(尽pipe它是可pipe理的ATM)。 有很多的编译性能问题,expression式树重写?
我认为这看起来非常有趣(+1),但有一点需要注意:C#编译器在优化switch语句方面非常出色。 不仅仅是短路 – 你会得到完全不同的IL,具体取决于你有多less个案例等等。
你的具体例子确实做了一些我觉得非常有用的事情 – 没有语法等同于case的types,因为(例如) typeof(Motorcycle)
不是一个常量。
这在dynamic应用程序中变得更有趣 – 这里的逻辑可以很容易地由数据驱动,从而实现“规则引擎”风格的执行。
你可以通过使用我写的名为OneOf的库来实现你的目标
switch
的主要优点(以及if
和exceptions as control flow
)是编译时安全 – 没有默认处理程序或通过
OneOf<Motorcycle, Bicycle, Car> vehicle = ... //assign from one of those types var getRentPrice = vehicle .Match( bike => 100 + bike.Cylinders * 10, // "bike" here is typed as Motorcycle bike => 30, // returns a constant car => car.EngineType.Match( diesel => 220 + car.Doors * 20 petrol => 200 + car.Doors * 20 ) );
它在Nuget上,目标是net451和netstandard1.6