我最近一直在研究 F#,虽然我不太可能很快就放弃它,但它确实突出了 C#(或库支持)可以让生活变得更轻松的一些领域。

特别是,我正在考虑 F# 的模式匹配功能,它允许非常丰富的语法 - 比当前的开关/条件 C# 等效项更具表现力。我不会尝试给出一个直接的例子(我的 F# 无法胜任),但简而言之,它允许:

  • 按类型匹配(对可区分联合进行全面覆盖检查)[注意,这也推断了绑定变量的类型,提供成员访问权限等]
  • 按谓词匹配
  • 上述的组合(可能还有我不知道的其他一些场景)

虽然 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>。

[注意 - 也许这里的 Switch/Case 是错误的术语......但它显示了这个想法]

对我来说,这比使用重复的 if/else 或复合三元条件(这对于非平凡的表达式变得非常混乱 - 大量的括号)要清楚得多。它还避免了 很多 转换,并允许简单扩展(直接或通过扩展方法)到更具体的匹配,例如与 VB Select...Case“x To y”用法相当的 InRange(...) 匹配。

我只是想衡量人们是否认为上述结构有很多好处(在没有语言支持的情况下)?

另外请注意,我一直在尝试上述的 3 种变体:

  • 用于评估的 Func<TSource,TValue> 版本 - 与复合三元条件语句相当
  • Action<TSource> 版本 - 与 if/else if/else if/else if/else 相当
  • Expression<Func<TSource,TValue>> 版本 - 作为第一个版本,但可由任意 LINQ 提供程序使用

此外,使用基于表达式的版本可以重写表达式树,本质上是将所有分支内联到单个复合条件表达式中,而不是使用重复调用。我最近没有检查过,但在一些早期的实体框架构建中,我似乎记得这是必要的,因为它不太喜欢 InitationExpression。它还允许更有效地使用 LINQ-to-Objects,因为它避免了重复的委托调用 - 测试显示与等效的 C# 相比,与上面的匹配(使用表达式形式)以相同的速度执行[实际上稍微快一些]复合条件语句。为了完整性,基于 Func<...> 的版本花费的时间是 C# 条件语句的 4 倍,但仍然非常快,并且在大多数用例中不太可能成为主要瓶颈。

我欢迎对上述内容(或更丰富的 C# 语言支持的可能性)提出任何想法/意见/批评/等等...希望如此;-p)。

有帮助吗?

解决方案

我知道这是一个老话题,但在 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));
}

其他提示

巴特·德·斯梅特 优秀的博客 有一个由 8 部分组成的系列,讲述如何完全按照您所描述的方式进行操作。找到第一部分 这里.

在尝试用 C# 做这样的“功能性”事情(甚至尝试写一本关于它的书)之后,我得出的结论是,不,除了少数例外,这样的事情没有太大帮助。

主要原因是 F# 等语言从真正支持这些功能中获得了很大的力量。不是“你能做到”,而是“这很简单,很清楚,是预期的”。

例如,在模式匹配中,编译器会告诉您是否存在不完整的匹配或何时永远不会命中另一个匹配。这对于开放式类型不太有用,但是当匹配可区分的联合或元组时,它非常漂亮。在 F# 中,您期望人们进行模式匹配,这立即就有意义了。

“问题”是,一旦你开始使用一些函数式概念,你很自然地想要继续下去。然而,利用元组、函数、部分方法应用和柯里化、模式匹配、嵌套函数、泛型、monad 支持等。在 C# 中得到 非常 丑陋,很快。这很有趣,一些非常聪明的人用 C# 做了一些非常酷的事情,但实际上 使用 感觉很重。

我最终在 C# 中经常使用(跨项目):

  • 序列函数,通过 IEnumerable 的扩展方法。诸如 ForEach 或 Process(“应用”?-- 对枚举的序列项执行操作)适合,因为 C# 语法很好地支持它。
  • 抽象常见的语句模式。复杂的 try/catch/finally 块或其他涉及的(通常是非常通用的)代码块。扩展 LINQ-to-SQL 也适合于此。
  • 在某种程度上,元组。

** 但请注意:缺乏自动泛化和类型推断确实阻碍了这些功能的使用。**

所有这些都表明,正如其他人提到的,在一个小团队中,为了特定的目的,是的,如果您坚持使用 C#,也许他们可以提供帮助。但根据我的经验,他们通常感觉麻烦多于其价值——YMMV。

其他一些链接:

可以说,C# 没有使类型切换变得简单的原因是因为它主要是一种面向对象的语言,而用面向对象的术语来做到这一点的“正确”方法是在 Vehicle 上定义一个 GetRentPrice 方法,并且在派生类中重写它。

也就是说,我花了一些时间研究具有这种功能的多范式和函数式语言,例如 F# 和 Haskell,并且我遇到过许多它以前有用的地方(例如当您没有编写需要打开的类型时,这样您就无法在它们上实现虚拟方法),这是我欢迎与受歧视的联合一起进入该语言的东西。

[编辑:删除了有关性能的部分,因为 Marc 表示它可能会短路]

另一个潜在的问题是可用性问题 - 从最终调用中可以清楚地看出,如果匹配不满足任何条件,会发生什么,但如果匹配两个或多个条件,会发生什么行为?它应该抛出异常吗?它应该返回第一个还是最后一个匹配项?

我倾向于用来解决此类问题的一种方法是使用字典字段,以类型为键,以 lambda 作为值,使用对象初始值设定项语法构造起来非常简洁;但是,这仅考虑具体类型,并且不允许附加谓词,因此可能不适合更复杂的情况。[旁注 - 如果您查看 C# 编译器的输出,它经常将 switch 语句转换为基于字典的跳转表,因此似乎没有充分的理由它不支持类型切换]

我不认为这些类型的库(其作用类似于语言扩展)可能会获得广泛的接受,但它们玩起来很有趣,并且对于在特定领域工作的小团队来说非常有用。例如,如果您正在编写大量的“业务规则/逻辑”来执行诸如此类的任意类型测试,我可以看到它会多么方便。

我不知道这是否可能成为 C# 语言功能(似乎很可疑,但谁能预见未来呢?)。

作为参考,相应的 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");
    }
}

这是一些测试代码:

    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));
        }
    }

模式匹配(如描述 这里),其目的是根据值的类型规范来解构值。但是,C# 中的类(或类型)的概念不适合您。

多范式语言设计并没有什么问题,相反,在 C# 中使用 lambda 非常好,并且 Haskell 可以执行命令式的操作,例如IO。但这不是一个非常优雅的解决方案,不符合 Haskell 风格。

但由于顺序过程编程语言可以用 lambda 演算来理解,而 C# 恰好非常适合顺序过程语言的参数,因此它是一个很好的选择。但是,从 Haskell 等纯函数环境中获取一些东西,然后将该功能放入不纯粹的语言中,好吧,仅仅这样做,并不能保证更好的结果。

我的观点是,模式匹配的成功与语言设计和数据模型有关。话虽如此,我不认为模式匹配是 C# 的一个有用功能,因为它不能解决典型的 C# 问题,也不能很好地适应命令式编程范例。

恕我直言,执行此类操作的 OO 方式是访问者模式。您的访问者成员方法只是充当案例构造,您让语言本身处理适当的分派,而不必“查看”类型。

虽然打开类型不是很“C-sharpey”,但我知道该构造在一般用途中非常有帮助 - 我至少有一个可以使用它的个人项目(尽管它是易于管理的 ATM)。重写表达式树是否存在很大的编译性能问题?

我认为这看起来真的很有趣(+1),但需要注意一件事:C# 编译器非常擅长优化 switch 语句。不仅仅是短路 - 根据情况的数量等,您会得到完全不同的 IL。

您的具体示例确实做了一些我认为非常有用的事情 - 没有相当于按类型区分大小写的语法,例如(例如) typeof(Motorcycle) 不是一个常数。

这在动态应用程序中变得更有趣 - 这里的逻辑可以轻松地由数据驱动,从而提供“规则引擎”样式的执行。

您可以通过使用我编写的库来实现您的目标,该库称为 一个

主要优势在于 switch (和 ifexceptions 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

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top