C# 切换/模式匹配思路
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/156467/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
switch / pattern matching idea
提问by Marc Gravell
I've been looking at F# recently, and while I'm not likely to leap the fence any time soon, it definitely highlights some areas where C# (or library support) could make life easier.
我最近一直在研究 F#,虽然我不太可能在短期内跨越围栏,但它确实突出了 C#(或库支持)可以使生活更轻松的一些领域。
In particular, I'm thinking about the pattern matching capability of F#, which allows a very rich syntax - much more expressive than the current switch/conditional C# equivalents. I won't try to give a direct example (my F# isn't up to it), but in short it allows:
特别是,我正在考虑 F# 的模式匹配功能,它允许使用非常丰富的语法 - 比当前的 switch/conditional C# 等价物更具表现力。我不会试图给出一个直接的例子(我的 F# 达不到它),但简而言之,它允许:
- match by type (with full-coverage checking for discriminated unions) [note this also infers the type for the bound variable, giving member access etc]
- match by predicate
- combinations of the above (and possibly some other scenarios I'm not aware of)
- 按类型匹配(对可区分联合进行全覆盖检查)[注意这也推断绑定变量的类型,提供成员访问等]
- 按谓词匹配
- 以上的组合(可能还有一些我不知道的其他场景)
While it would be lovely for C# to eventually borrow [ahem] some of this richness, in the interim I've been looking at what can be done at runtime - for example, it is fairly easy to knock together some objects to allow:
虽然 C# 最终借用 [ahem] 一些丰富的东西会很可爱,但在此期间,我一直在研究可以在运行时完成的操作 - 例如,将一些对象组合在一起是相当容易的:
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
where getRentPrice is a Func<Vehicle,int>.
其中 getRentPrice 是一个 Func<Vehicle,int>。
[note - maybe Switch/Case here is the wrong terms... but it shows the idea]
[注意 - 也许这里的 Switch/Case 是错误的术语......但它表明了这个想法]
To me, this is a lot clearer than the equivalent using repeated if/else, or a composite ternary conditional (which gets very messy for non-trivial expressions - brackets galore). It also avoids a lotof casting, and allows for simple extension (either directly or via extension methods) to more-specific matches, for example an InRange(...) match comparable to the VB Select...Case "x To y" usage.
对我来说,这比使用重复的 if/else 或复合三元条件(对于非平凡的表达式变得非常混乱 - 大量括号)的等价物要清晰得多。它还避免了大量的转换,并允许简单的扩展(直接或通过扩展方法)到更具体的匹配,例如一个 InRange(...) 匹配与 VB Select...Case "x To y “ 用法。
I'm just trying to gauge if people think there is much benefit from constructs like the above (in the absence of language support)?
我只是想衡量人们是否认为上述结构有很多好处(在没有语言支持的情况下)?
Note additionally that I've been playing with 3 variants of the above:
另外请注意,我一直在玩上述的 3 个变体:
- a Func<TSource,TValue> version for evaluation - comparable to composite ternary conditional statements
- an Action<TSource> version - comparable to if/else if/else if/else if/else
- an Expression<Func<TSource,TValue>> version - as the first, but usable by arbitrary LINQ providers
- 用于评估的 Func<TSource,TValue> 版本 - 与复合三元条件语句相当
- 一个 Action<TSource> 版本 - 相当于 if/else if/else if/else if/else
- 一个 Expression<Func<TSource,TValue>> 版本 - 作为第一个版本,但可由任意 LINQ 提供程序使用
Additionally, using the Expression-based version enables Expression-tree re-writing, essentially inlining all the branches into a single composite conditional Expression, rather than using repeated invocation. I haven't checked recently, but in some early Entity Framework builds I seem to recall this being necessary, as it didn't like InvocationExpression very much. It also allows more efficient usage with LINQ-to-Objects, since it avoids repeated delegate invocations - tests show a match like the above (using the Expression form) performing at the same speed [marginally quicker, in fact] compared to the equivalent C# composite conditional statement. For completeness, the Func<...> based-version took 4 times as long as the C# conditional statement, but is still very quick and is unlikely to be a major bottleneck in most use-cases.
此外,使用基于表达式的版本可以重写表达式树,基本上将所有分支内联到单个复合条件表达式中,而不是使用重复调用。我最近没有检查过,但在一些早期的实体框架构建中,我似乎记得这是必要的,因为它不太喜欢 InvocationExpression。它还允许更有效地使用 LINQ-to-Objects,因为它避免了重复的委托调用 - 与等效的 C# 相比,测试显示与上述匹配(使用表达式形式)以相同的速度执行[实际上略快]复合条件语句。为完整起见,基于 Func<...> 的版本花费的时间是 C# 条件语句的 4 倍,但仍然非常快,并且不太可能成为大多数用例的主要瓶颈。
I welcome any thoughts / input / critique / etc on the above (or on the possibilities of richer C# language support... here's hoping ;-p).
我欢迎任何关于上述(或更丰富的 C# 语言支持的可能性......这里希望;-p)的想法/输入/批评/等。
采纳答案by Marcus Pierce
I know it's an old topic, but in c# 7 you can do :
我知道这是一个老话题,但在 c# 7 中你可以这样做:
switch(shape)
{
case Circle c:
WriteLine($"circle with radius {c.Radius}");
break;
case Rectangle s when (s.Length == s.Height):
WriteLine($"{s.Length} x {s.Height} square");
break;
case Rectangle r:
WriteLine($"{r.Length} x {r.Height} rectangle");
break;
default:
WriteLine("<unknown shape>");
break;
case null:
throw new ArgumentNullException(nameof(shape));
}
回答by Simon Buchan
Although it's not very 'C-sharpey' to switch on type, I know that construct would be pretty helpful in general use - I have at least one personal project that could use it (although its managable ATM). Is there much of a compile performance problem, with the expression tree re-writing?
尽管打开类型不是很“C-sharpey”,但我知道该构造在一般用途中非常有用 - 我至少有一个可以使用它的个人项目(尽管它是可管理的 ATM)。重新编写表达式树是否存在很多编译性能问题?
回答by Brian
I don't think these sorts of libraries (which act like language extensions) are likely to gain wide acceptance, but they are fun to play with, and can be really useful for small teams working in specific domains where this is useful. For instance, if you are writing tons of 'business rules/logic' that does arbitrary type tests like this and whatnot, I can see how it would be handy.
我不认为这些类型的库(就像语言扩展一样)可能会被广泛接受,但它们玩起来很有趣,并且对于在特定领域工作的小团队来说非常有用,因为这很有用。例如,如果您正在编写大量“业务规则/逻辑”来执行诸如此类之类的任意类型测试,我可以看到它会很方便。
I've no clue if this is ever likely to be a C# language feature (seems doubtful, but who can see the future?).
我不知道这是否有可能成为 C# 语言功能(似乎值得怀疑,但谁能看到未来?)。
For reference, the corresponding F# is approximately:
作为参考,对应的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"
assuming you'd defined a class hierarchy along the lines of
假设你已经定义了一个类层次结构
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
回答by Greg Beech
Arguably the reason that C# doesn't make it simple to switch on type is because it is primarily an object-oriented language, and the 'correct' way to do this in object-oriented terms would be to define a GetRentPrice method on Vehicle and override it in derived classes.
可以说,C# 不能简单地切换类型的原因是因为它主要是一种面向对象的语言,而在面向对象的术语中这样做的“正确”方法是在 Vehicle 和在派生类中覆盖它。
That said, I've spent a bit of time playing with multi-paradigm and functional languages like F# and Haskell which have this type of capability, and I've come across a number of places where it would be useful before (e.g. when you are not writing the types you need to switch on so you cannot implement a virtual method on them) and it's something I'd welcome into the language along with discriminated unions.
也就是说,我花了一些时间来使用多范式和函数式语言,如 F# 和 Haskell,它们具有这种类型的功能,而且我遇到了许多以前有用的地方(例如,当你没有编写您需要打开的类型,因此您无法在它们上实现虚拟方法),这是我欢迎的语言以及有区别的联合。
[Edit: Removed part about performance as Marc indicated it could be short-circuited]
[编辑:删除了关于性能的部分,因为 Marc 表示它可能是短路的]
Another potential problem is a usability one - it's clear from the final call what happens if the match fails to meet any conditions, but what is the behaviour if it matches two or more conditions? Should it throw an exception? Should it return the first or the last match?
另一个潜在问题是可用性问题 - 从最终调用中可以清楚地看出,如果匹配不满足任何条件会发生什么,但是如果匹配两个或多个条件会出现什么行为?它应该抛出异常吗?它应该返回第一场比赛还是最后一场比赛?
A way I tend to use to solve this kind of problem is to use a dictionary field with the type as the key and the lambda as the value, which is pretty terse to construct using object initializer syntax; however, this only accounts for the concrete type and doesn't allow additional predicates so may not be suitable for more complex cases. [Side note - if you look at the output of the C# compiler it frequently converts switch statements to dictionary-based jump tables, so there doesn't appear to be a good reason it couldn't support switching on types]
我倾向于用来解决此类问题的一种方法是使用以类型为键、以 lambda 为值的字典字段,使用对象初始值设定项语法构造起来非常简洁;然而,这只说明了具体类型,不允许额外的谓词,因此可能不适合更复杂的情况。[旁注 - 如果您查看 C# 编译器的输出,它经常将 switch 语句转换为基于字典的跳转表,因此似乎没有充分的理由不支持切换类型]
回答by Keith
I think this looks really interesting (+1), but one thing to be careful of: the C# compiler is pretty good at optimising switch statements. Not just for short circuiting - you get completely different IL depending on how many cases you have and so on.
我认为这看起来很有趣 (+1),但需要注意一件事:C# 编译器非常擅长优化 switch 语句。不仅仅是为了短路 - 你会得到完全不同的 IL,这取决于你有多少案例等等。
Your specific example does do something I'd find very useful - there is no syntax equivalent to case by type, as (for instance) typeof(Motorcycle)
is not a constant.
您的具体示例确实做了一些我认为非常有用的事情 - 没有与 case by type 等效的语法,因为(例如)typeof(Motorcycle)
不是常量。
This gets more interesting in dynamic application - your logic here could be easily data-driven, giving 'rule-engine' style execution.
这在动态应用程序中变得更有趣 - 您在这里的逻辑可以很容易地由数据驱动,提供“规则引擎”风格的执行。
回答by MichaelGG
After trying to do such "functional" things in C# (and even attempting a book on it), I've come to the conclusion that no, with a few exceptions, such things don't help too much.
在尝试用 C# 做这样的“功能性”事情(甚至尝试写一本关于它的书)之后,我得出的结论是,不,除了少数例外,这些事情并没有太大帮助。
The main reason is that languages such as F# get a lot of their power from truly supporting these features. Not "you can do it", but "it's simple, it's clear, it's expected".
主要原因是像 F# 这样的语言从真正支持这些功能中获得了很大的力量。不是“你可以做到”,而是“它很简单,很清楚,在意料之中”。
For instance, in pattern matching, you get the compiler telling you if there's an incomplete match or when another match will never be hit. This is less useful with open ended types, but when matching a discriminated union or tuples, it's very nifty. In F#, you expect people to pattern match, and it instantly makes sense.
例如,在模式匹配中,编译器会告诉您是否存在不完整的匹配或何时永远不会命中另一个匹配。这对于开放式类型不太有用,但是在匹配可区分的联合或元组时,它非常漂亮。在 F# 中,您希望人们进行模式匹配,这立即变得有意义。
The "problem" is that once you start using some functional concepts, it's natural to want to continue. However, leveraging tuples, functions, partial method application and currying, pattern matching, nested functions, generics, monad support, etc. in C# gets veryugly, very quickly. It's fun, and some very smart people have done some very cool things in C#, but actually usingit feels heavy.
“问题”在于,一旦您开始使用某些功能概念,就很自然想要继续使用。然而,在 C# 中利用元组、函数、部分方法应用和柯里化、模式匹配、嵌套函数、泛型、monad 支持等会变得非常难看,非常快。挺好玩的,一些非常聪明的人用C#做了一些很酷的事情,但是实际使用起来感觉很沉重。
What I have ended up using often (across-projects) in C#:
我最终在 C# 中经常(跨项目)使用的内容:
- Sequence functions, via extension methods for IEnumerable. Things like ForEach or Process ("Apply"? -- do an action on a sequence item as it's enumerated) fit in because C# syntax supports it well.
- Abstracting common statement patterns. Complicated try/catch/finally blocks or other involved (often heavily generic) code blocks. Extending LINQ-to-SQL fits in here too.
- Tuples, to some extent.
- 序列函数,通过 IEnumerable 的扩展方法。ForEach 或 Process(“Apply”? - 在枚举的序列项上执行操作)之类的东西很适合,因为 C# 语法支持它。
- 抽象常见的语句模式。复杂的 try/catch/finally 块或其他涉及(通常非常通用)的代码块。扩展 LINQ-to-SQL 也适用于此。
- 元组,在某种程度上。
** But do note: The lack of automatic generalization and type inference really hinder the use of even these features. **
** 但请注意:缺乏自动泛化和类型推断确实阻碍了这些功能的使用。**
All this said, as someone else mentioned, on a small team, for a specific purpose, yes, perhaps they can help if you're stuck with C#. But in my experience, they usually felt like more hassle than they were worth - YMMV.
正如其他人提到的,所有这些都说,在一个小团队中,出于特定目的,是的,如果您坚持使用 C#,也许他们可以提供帮助。但根据我的经验,他们通常觉得比他们的价值更麻烦 - YMMV。
Some other links:
其他一些链接:
- Mono.Rocks playgroundhas many similar things (as well as non-functional-programming-but-useful additions).
- Luca Bolognese's functional C# library
- Matthew Podwysocki's functional C# on MSDN
- Mono.Rocks playground有很多类似的东西(以及非功能性编程但有用的补充)。
- Luca Bolognese 的函数式 C# 库
- MSDN 上的 Matthew Podwysocki 函数式 C#
回答by bacila
IMHO the OO way of doing such things is the Visitor pattern. Your visitor member methods simply act as case constructs and you let the language itself handle the appropriate dispatch without having to "peek" at types.
恕我直言,做这些事情的 OO 方式是访问者模式。您的访问者成员方法只是充当案例构造,您让语言本身处理适当的调度,而不必“窥视”类型。
回答by John Leidegren
Pattern matching (as described here), its purpose is to deconstruct values according to their type specification. However, the concept of a class (or type) in C# doesn't agree with you.
模式匹配(如所描述的在这里),其目的是根据其类型规范来解构值。但是,C# 中的类(或类型)的概念与您不同。
There's noting wrong with multi-paradigm language design, on the contrary, it's very nice to have lambdas in C#, and Haskell can do imperative stuff to e.g. IO. But it's not a very elegant solution, not in Haskell fashion.
多范式语言设计有错误,相反,在 C# 中有 lambdas 非常好,Haskell 可以对例如 IO 执行命令式操作。但这不是一个非常优雅的解决方案,不是 Haskell 时尚。
But since sequential procedural programming languages can be understood in terms of lambda calculus, and C# happens to fit well within the parameters of a sequential procedural language, it's a good fit. But, taking something from the pure functional context of say Haskell, and then putting that feature into a language which is not pure, well, doing just that, will not guarantee a better outcome.
但是由于可以根据 lambda 演算来理解顺序过程编程语言,而且 C# 恰好适合顺序过程语言的参数,因此非常适合。但是,从 say Haskell 的纯函数上下文中获取一些东西,然后将该特性放入一种不纯的语言中,好吧,这样做并不能保证更好的结果。
My point is this, what makes pattern matching tick is tied to the language design and data model. Having said that, I don't believe pattern matching to be an useful feature of C# because it does not solve typical C# problems nor does it fit well within the imperative programming paradigm.
我的观点是,使模式匹配打勾的原因与语言设计和数据模型有关。话虽如此,我不认为模式匹配是 C# 的有用特性,因为它不能解决典型的 C# 问题,也不能很好地适应命令式编程范式。
回答by cdiggins
To answer your question, yes I think pattern matching syntactic constructs are useful. I for one would like to see syntactic support in C# for it.
要回答您的问题,是的,我认为模式匹配句法结构很有用。我希望看到 C# 中的语法支持。
Here is my implementation of a class that provides (nearly) the same syntax as you describe
这是我对一个类的实现,该类提供(几乎)与您描述的语法相同的语法
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");
}
}
Here is some test code:
下面是一些测试代码:
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));
}
}
回答by mcintyre321
You can achieve what you are after by using a library I wrote, called OneOf
The major advantage over switch
(and if
and exceptions as control flow
) is that it is compile-time safe - there is no default handler or fall through
在主要优点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
)
);
It's on Nuget and targets net451 and netstandard1.6
它在 Nuget 上,针对 net451 和 netstandard1.6