идея переключения / сопоставления с образцом

151

Я недавно смотрел на 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 или составного троичного условного выражения (которое становится очень грязным для нетривиальных выражений - скобки в изобилии). Это также позволяет избежать большого количества приведения и допускает простое расширение (либо напрямую, либо с помощью методов расширения) до более конкретных совпадений, например совпадение 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

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

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

Марк Гравелл
источник
«Я просто пытаюсь оценить, считают ли люди, что конструкции, подобные приведенным выше, приносят большую пользу (при отсутствии языковой поддержки)?» ИМХО, да. Разве что-то подобное уже не существует? Если нет, то рекомендуется написать облегченную библиотеку.
Конрад Рудольф
10
Вы можете использовать VB .NET, который поддерживает это в своем операторе select case. Ик!
Джим Бургер
Я также соберу свой рог и добавлю ссылку на свою библиотеку: functions-dotnet
Алексей Романов
1
Мне нравится эта идея, и она создает очень красивую и более гибкую форму распределительного шкафа; однако разве это не приукрашенный способ использования Linq-подобного синтаксиса в качестве оболочки if-then? Я бы не рекомендовал кому-либо использовать это вместо реальной сделки, то есть switch-caseзаявления. Не поймите меня неправильно, я думаю, что это имеет место, и я, вероятно, буду искать способ реализации.
IAbstract
2
Хотя этому вопросу уже более двух лет, стоит упомянуть, что C # 7 скоро выйдет (ish) с возможностями сопоставления с образцом.
Abion47 12.12.16

Ответы:

22

Я знаю, что это старая тема, но в 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));
}
Маркус Пирс
источник
Заметная разница между C # и F # заключается в полноте сопоставления с образцом. То, что сопоставление с образцом охватывает все возможные, полностью описанные, предупреждения, предупреждения от компилятора, если вы этого не сделаете. Хотя вы можете с полным основанием утверждать, что случай по умолчанию делает это, на практике он также часто является исключением во время выполнения.
ВоронойПотато
37

После попыток сделать такие «функциональные» вещи в C # (и даже попытаться написать книгу об этом), я пришел к выводу, что нет, за некоторыми исключениями, такие вещи не слишком помогают.

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

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

«Проблема» заключается в том, что, как только вы начнете использовать некоторые функциональные концепции, естественно захотеть продолжить. Однако использование кортежей, функций, частичного применения методов и каррирования, сопоставления с образцом, вложенных функций, обобщений, поддержки монад и т. Д. В C # становится очень уродливым и очень быстрым. Это забавно, и некоторые очень умные люди сделали некоторые очень крутые вещи в C #, но на самом деле их использование кажется тяжелым.

То, что я часто использовал (через проекты) в C #:

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

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

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

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

MichaelGG
источник
25

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

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

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

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

Для решения этой проблемы я обычно использую поле словаря с типом в качестве ключа и лямбда-значением в качестве значения, которое довольно лаконично построить с использованием синтаксиса инициализатора объекта; однако это учитывает только конкретный тип и не допускает дополнительных предикатов, поэтому может не подходить для более сложных случаев. [Примечание: если вы посмотрите на выходные данные компилятора C #, он часто преобразует операторы switch в таблицы переходов на основе словаря, поэтому, по-видимому, нет веской причины, по которой он не может поддерживать переключение типов]

Грег Бич
источник
1
На самом деле - версия, которую я имею, делает короткое замыкание в версиях делегата и выражения. Версия выражения компилируется в составное условие; версия делегата - это просто набор предикатов и func / actions - после совпадения он останавливается.
Марк Гравелл
Интересно - из беглого взгляда я предположил, что он должен будет выполнить хотя бы базовую проверку каждого условия, так как оно выглядит как цепочка методов, но теперь я понимаю, что методы на самом деле связывают экземпляр объекта, чтобы построить его, чтобы вы могли это сделать. Я отредактирую свой ответ, чтобы удалить это утверждение.
Грег Бич
22

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

Я понятия не имею, может ли это когда-либо быть особенностью языка 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
Брайан
источник
2
Спасибо за версию F #. Полагаю, мне нравится, как F # справляется с этим, но я не уверен, что (в целом) F # является правильным выбором в данный момент, поэтому мне приходится идти по этому пути ...
Марк Гравелл
13

Чтобы ответить на ваш вопрос, да, я думаю, что синтаксические конструкции сопоставления с образцом полезны. Я хотел бы видеть синтаксическую поддержку в 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));
        }
    }
cdiggins
источник
9

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

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

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

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

Джон Лейдегрен
источник
1
Может быть. В самом деле, мне было бы трудно придумать убедительный «убийственный» аргумент о том, зачем это нужно (в отличие от «возможно, хорошего в нескольких крайних случаях ценой усложнения языка»).
Марк Гравелл
5

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

Băcilă
источник
4

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

Саймон Бьюкен
источник
Нет, если вы кешируете объект для повторного использования (во многом так работает лямбда-выражение C #, за исключением того, что компилятор скрывает код). Переписывание определенно улучшает производительность компиляции - однако для регулярного использования (а не LINQ-to-Something) я ожидаю, что версия делегата может быть более полезной.
Марк Гравелл
Также обратите внимание - это не обязательно тип включения - его также можно использовать в качестве составного условия (даже через LINQ) - но без грязного теста x =>? Результат1: (Тест2? Результат2: (Тест3? Результат 3: Результат4))
Марк Гравелл
Приятно знать, хотя я имел в виду производительность самой компиляции : сколько времени занимает csc.exe - я недостаточно знаком с C #, чтобы понять, действительно ли это когда-либо является проблемой, но это большая проблема для C ++.
Саймон Бьюкен
csc не будет моргать - это так похоже на работу LINQ, а компилятор C # 3.0 довольно хорош в LINQ / методах расширения и т. д.
Марк Грэвелл
3

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

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

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

Кит
источник
0

Вы можете достичь того, что вы хотите, с помощью библиотеки, которую я написал, под названием 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

mcintyre321
источник