Идея выбора переключателя/образца

Недавно я смотрел F #, и, хотя я вряд ли скоро скачу за ограду, он определенно выделяет некоторые области, где С# (или поддержка библиотеки) может облегчить жизнь.

В частности, я думаю о возможности сопоставления шаблонов F #, что позволяет использовать очень богатый синтаксис - гораздо более выразительный, чем текущий эквивалент С# или условный эквивалент С#. Я не буду пытаться привести прямой пример (мой F # не подходит), но вкратце он позволяет:

  • соответствие по типу (с проверкой полного охвата для дискриминационных объединений) [обратите внимание, что это также указывает тип для связанной переменной, предоставляя доступ к члену и т.д.]
  • совпадение по предикату
  • комбинации вышеуказанных (и, возможно, некоторых других сценариев, о которых я не знаю)

Хотя было бы замечательно, что С# в конечном итоге заимствовал [ах] часть этого богатства, тем временем я смотрел, что можно сделать во время выполнения - например, довольно легко сбить некоторые объекты с позволяют:

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 или сложный тернарный условный (который становится очень грязным для нетривиальных выражений - скобки в изобилии). Он также позволяет избежать большого количества кастингов и позволяет простое расширение (прямо или через методы расширения) для более конкретных совпадений, например сравнение InRange (...), сравнимое с выбором VB... Случай "x To y".

Я просто пытаюсь оценить, думают ли люди, что есть много преимуществ от конструкций, подобных приведенным выше (при отсутствии поддержки языка)?

Обратите внимание, что я играл с 3 вариантами выше:

  • a Func < TSource, TValue > версия для оценки - сопоставима с составными тернальными условными утверждениями
  • Действие <TSource> версия - сопоставима с if/else if/else if/else if/else
  • выражение < Func < TSource, TValue → > версия - как первая, но используемая произвольными поставщиками LINQ

Кроме того, использование версии, основанной на выражении, позволяет перезаписывать репрезентацию дерева выражений, существенно вставляя все ветки в единое составное условное выражение, вместо использования повторного вызова. Я недавно не проверял, но в некоторых ранних сборках Entity Framework я, похоже, напоминаю, что это необходимо, так как это не очень понравилось InvocationExpression. Он также позволяет более эффективно использовать LINQ-to-Objects, поскольку он избегает повторных вызовов делегатов - тесты показывают совпадение, подобное приведенному выше (с использованием формы выражения), выполняющееся с одинаковой скоростью [на самом деле быстрее] по сравнению с эквивалентным С# составной условный оператор. Для полноты, Func <... > основанная на версии, заняла в 4 раза длиннее условного оператора С#, но все еще очень быстро и вряд ли станет основным узким местом в большинстве случаев использования.

Я приветствую любые мысли/ввод/критику/и т.д. выше (или о возможностях более богатой поддержки языка С#... здесь надеясь; -p).

Ответ 1

Я знаю это старую тему, но в С# 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));
}

Ответ 2

Bart De Smet отличный блог содержит 8 частей, посвященных тому, что вы описываете. Найдите первую часть здесь.

Ответ 3

После попытки сделать такие "функциональные" вещи в С# (и даже попытаться книгу на нем), я пришел к выводу, что нет, за некоторыми исключениями, таких вещей не слишком много.

Основная причина в том, что такие языки, как F #, получают большую часть своей силы от подлинной поддержки этих функций. Не "вы можете это сделать", но "это просто, понятно, это ожидалось".

Например, при сопоставлении с образцами вы получаете компилятор, сообщающий вам, есть ли неполное совпадение или когда другое совпадение никогда не будет удалено. Это менее полезно для открытых типов, но при сопоставлении с дискриминационным союзом или кортежами это очень изящно. В F # вы ожидаете, что люди будут сопоставлять шаблон, и это мгновенно имеет смысл.

"Проблема" заключается в том, что после того, как вы начнете использовать некоторые функциональные концепции, естественно продолжать работать. Тем не менее, использование кортежей, функций, частичного применения метода и каррирования, сопоставления шаблонов, вложенных функций, дженериков, поддержки монады и т.д. На С# становится очень уродливым, очень быстро. Это весело, и некоторые очень умные люди сделали некоторые очень классные вещи на С#, но на самом деле он чувствует себя тяжело.

Что я использовал часто (в проектах) в С#:

  • Функции последовательности с помощью методов расширения для IEnumerable. Такие вещи, как ForEach или Process ( "Применить"? - выполняют действие над элементом последовательности, как оно перечислено), вставляются, потому что синтаксис С# поддерживает его хорошо.
  • Абстрактные общие шаблоны операторов. Сложные блоки try/catch/finally или другие задействованные (часто сильно генерируемые) кодовые блоки. Расширение LINQ-to-SQL подходит и здесь.
  • Кортежи в некоторой степени.

** Но заметьте: отсутствие автоматического обобщения и вывода типа действительно препятствуют использованию даже этих функций. **

Все это сказало, как кто-то еще упомянул, в небольшой команде, для определенной цели, да, возможно, они могут помочь, если вы застряли с С#. Но, по моему опыту, они обычно чувствовали себя более сложными, чем они стоили - YMMV.

Некоторые другие ссылки:

Ответ 4

Возможно, причина, по которой С# не упрощает включение типа, заключается в том, что это прежде всего объектно-ориентированный язык, а "правильный" способ сделать это в объектно-ориентированных терминах - это определить метод GetRentPrice на Транспортном средстве и переопределить его в производных классах.

Тем не менее, я провел немного времени, играя с мультипарадигмами и функциональными языками, такими как F # и Haskell, которые имеют этот тип возможностей, и я столкнулся с рядом мест, где это было бы полезно раньше ( например, когда вы не пишете типы, которые вам нужно включить, чтобы вы не могли реализовать на них виртуальный метод), и это то, что я приветствовал бы на языке вместе с дискриминационными объединениями.

[Edit: Удалена часть о производительности, поскольку Марк указал, что она может быть закорочена)

Еще одна потенциальная проблема - удобство использования - ясно из окончательного вызова, что происходит, если совпадение не соответствует каким-либо условиям, но каково поведение, если оно соответствует двум или более условиям? Должно ли это исключение? Должен ли он вернуть первый или последний матч?

Способ, которым я склонен использовать эту проблему, заключается в использовании поля словаря с типом в качестве ключа и лямбда в качестве значения, которое довольно сложно построить с использованием синтаксиса инициализатора объекта; однако это только учет конкретного типа и не допускает дополнительных предикатов, поэтому не может быть подходящим для более сложных случаев. [Замечание. Если вы посмотрите на результат компилятора С#, он часто конвертирует операторы switch в таблицы перехода на основе словаря, поэтому, похоже, нет веской причины, по которым он не может поддерживать типы включений]

Ответ 5

Я не думаю, что подобные библиотеки (которые действуют как расширения языка), скорее всего, получат широкое признание, но с ними интересно играть и могут быть очень полезны для небольших групп, работающих в конкретных доменах, где это полезно, Например, если вы пишете тонны "бизнес-правил/логики", которые делают подобные тесты типа, подобные этому, и еще много чего, я вижу, как это удобно.

У меня нет подсказки, если это когда-либо будет функцией языка С# (кажется сомнительной, но кто может видеть будущее?).

Для справки, соответствующее 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

Ответ 6

Чтобы ответить на ваш вопрос, да, я думаю, что синтаксические конструкции, соответствующие шаблону, полезны. Я бы хотел увидеть синтаксическую поддержку в С#.

Вот моя реализация класса, который обеспечивает (почти) тот же синтаксис, который вы описываете

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

Ответ 7

Соответствие шаблону (как описано здесь), его цель - деконструировать значения в соответствии с их спецификацией типа. Однако концепция класса (или типа) в С# не согласуется с вами.

Отмечая неправильный дизайн языка с несколькими парадигмами, напротив, очень хорошо иметь лямбды в С#, и Haskell может делать императивные вещи, например. IO. Но это не очень изящное решение, а не в стиле Хаскелла.

Но так как последовательные языки процедурного программирования могут быть поняты в терминах лямбда-исчисления, а С# очень хорошо вписывается в параметры последовательного процедурного языка, это хорошо подходит. Но, беря что-то из чистого функционального контекста, скажем, Haskell, а затем помещая эту особенность в язык, который не является чистым, хорошо, делая именно это, не гарантирует лучшего результата.

Моя точка зрения заключается в том, что делает привязку соответствия шаблону привязанной к языковому дизайну и модели данных. Сказав это, я не считаю, что сопоставление образцов является полезной функцией С#, потому что оно не решает типичных проблем С# и не подходит в рамках парадигмы императивного программирования.

Ответ 8

ИМХО способ ОО делать такие вещи - шаблон посетителя. Методы вашего посетителя просто действуют как конструкторы case, и вы позволяете самому языку обрабатывать соответствующую отправку, не "заглядывая" в типы.

Ответ 9

Хотя это не очень "C-sharpey" для включения типа, я знаю, что конструкция будет очень полезна в общем использовании - у меня есть хотя бы один личный проект, который мог бы использовать его (хотя его управляемый банкомат). Существует ли большая часть проблемы с компиляцией, когда регенерация дерева выражений?

Ответ 10

Я думаю, что это выглядит действительно интересно (+1), но нужно быть осторожным: компилятор С# довольно хорош в оптимизации операторов switch. Не только для короткого замыкания - вы получаете совершенно разные ИЛ в зависимости от того, сколько случаев у вас есть и т.д.

Ваш конкретный пример действительно делает что-то, что я считаю очень полезным - нет синтаксиса, эквивалентного case by type, поскольку (например) typeof(Motorcycle) не является константой.

Это становится более интересным в динамическом приложении - ваша логика здесь может быть легко управляемой данными, предоставляя стиль стиля "rule-engine".

Ответ 11

Вы можете добиться того, что вам нужно, используя библиотеку, которую я написал, называемую OneOf

Основным преимуществом над switchif и 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