Что нам готовит C# 7 (Часть 2. Pattern matching)

    Продолжая серию статей о новшествах в С#7, акцентирую внимание на, пожалуй, главных нововведениях — Pattern matching и Record type(Примерный перевод «регистрируемые типы»). Эти функционалы дополняют друг друга, поэтому лучше рассказывать о них вместе.

    Начнем с Record type. Он приходит к нам из F#. По сути своей это быстрое определение класса, причем с невозможностью изменения его свойств, т.е. все его поля имеют параметр readonly, а задаются параметры в конструкторе. Описывать это достаточно долго и нудно, поэтому начнём сразу с примера кода и на примере уже все разберём. Вот пример определения record type’а:

    public class Cartesian(double x: X, double y: Y);


    Это определение некоторого класса, хранящего декартовы координаты точки. Транслироваться он должен в такой класс:

    
    public class Cartesian
    {
        private readonly double $X;
        private readonly double $Y;
        public Cartesian(double x, double y)
        {
            this.$X = x;
            this.$Y = y;
        }
        public double X { get { return this.$X; } }
        public double Y { get { return this.$Y; } }
        public static bool operator is(Cartesian c, out double x, out double y)
        {
            x = c.X;
            y = c.Y;
            return true;
        }
        override public bool Equals(object obj)
        {
            if (obj.GetType() != typeof(Cartesian)) return false;
            var $o = obj as Cartesian;
            return object.Equals(X, $o.X) && object.Equals(Y, $o.Y);
        }
        override public int GetHashCode()
        {
            int $v = 1203787;
            $v = ($v * 28341) + X?.GetHashCode().GetValueOrDefault();
            $v = ($v * 28341) + Y?.GetHashCode().GetValueOrDefault();
        }
        override public string ToString()
        {
            return new System.Text.StringBuilder()
                .Append(“Cartesian(X: “)
                .Append(X)
                .Append(“, Y: ”)
                .Append(Y)
                .Append(“)”)
                .ToString();
        }
    }
    

    Разберем свойства класса. В его определении мы указали два double параметра. Эти параметры транслируются в два открытых только для чтения свойства, и два поля только для чтения внутри класса. Затем создается конструктор с параметрами, указанными в определении класса. Также, создаются методы Equals, GetHashCode, ToString.

    Наибольший интерес представляет перегруженный оператор is. Вот он как раз уже больше относится к Pattern matching. Теперь оператор is поддерживает дополнительное сравнение, кроме обычной проверки возможности приведения к типу. Также возможен дополнительный вызов этого перегруженного оператора у класса. Начнем с того, как перегружается оператор и какие действия при этом могут совершаться. Первым параметром в операторе идет передаваемый ему объект класса, он не обязательно должен быть классом этого оператора. Затем идут возвращаемые параметры, с которыми нам нужно сравнивать или которые надо получить при выполнении оператора is. При создании класса через record type создается оператор is с передаваемым record type классом и возвращаемыми значениями этого класса, указанными в определении. Вот пример того, как сделать преобразование декартовых координат в полярные с помощью оператора is:

    
    public static class Polar
    {
        public static bool operator is(Cartesian c, out double R, out double Theta)
        {
            R = Math.Sqrt(c.X*c.X + c.Y*c.Y);
            Theta = Math.Atan2(c.Y, c.X);
            return c.X != 0 || c.Y != 0;
        }
    }
    

    Что мы получаем, если передать оператору объект класса Cartesian: он попытается преобразовать данные этого класса к данным класса Polar и вернет преобразованные данные.

    Pattern matching (или Сопоставление с образцом; хотя это название мне не очень нравится, английское определение кажется более точным), что же это такое? Пришел он к нам из таких языков как Python и F#. По сути своей это расширенный switch, который не только может сравнивать значения одного типа с константами, но и использовать приведение типов и их преобразование к необходимой структуре. И во всем этом нам поможет новый перегруженный оператор is. Начнем с новых возможностей старого оператора проверки возможности преобразования типов. Теперь вместо вот этого:

    
    var v = expr as Type;   
    if (v != null) {
        // Используем v
    }
    

    Можно будет писать вот так:

    
    if (expr is Type v) {
        // используем v
    }
    

    Это, конечно, сократит код с приведением типов. Но вернемся к Pattern matching и узнаем, какие возможности он нам готовит. Напишем проверку приведения конкретных декартовых координат к полярным и получение радиуса:

    
    var c = Cartesian(3, 4);
    if (c is Polar(var R, *)) Console.WriteLine(R);
    

    Итак, что здесь происходит, давайте разберемся. Берется переменная c, получается тип переменой и ищется оператор is, где первым параметром является этот тип. Далее вызывается этот оператор и, если он вернул истину, условие считается выполненным. Далее мы получаем в блоке условия локальную переменную R. Здесь нам не важен угол, и поэтому мы во второй параметр передали * — это означает игнорирование второго параметра. Еще возможно такое использование оператора:

    
    if (c is Polar(5, *)) Console.WriteLine("Радиус равен 5");
    

    Здесь мы накладываем дополнительное условие на возвращаемое значение радиуса, и условие выполнится, только когда радиус равен 5.

    Основное применение новому оператору is — это, конечно, в операторе switch. Приведем пример решения алгебраических выражений с помощью pattern matching. Определим нужные нам классы с помощью record type.

    
    abstract class Expr;
    class X() : Expr;
    class Const(double Value) : Expr;
    class Add(Expr Left, Expr Right) : Expr;
    class Mult(Expr Left, Expr Right) : Expr;
    class Neg(Expr Value) : Expr;
    

    Для начала напишем метод взятия производной:

    
    Expr Deriv(Expr e)
    {
      switch (e) {
        case X(): return Const(1);
        case Const(*): return Const(0);
        case Add(var Left, var Right):
          return Add(Deriv(Left), Deriv(Right));
        case Mult(var Left, var Right):
          return Add(Mult(Deriv(Left), Right), Mult(Left, Deriv(Right)));
        case Neg(var Value):
          return Neg(Deriv(Value));
      }
    }
    

    Или упрощение выражения:

    
    Expr Simplify(Expr e)
    {
      switch (e) {
        case Mult(Const(0), *): return Const(0);
        case Mult(*, Const(0)): return Const(0);
        case Mult(Const(1), var x): return Simplify(x);
        case Mult(var x, Const(1)): return Simplify(x);
        case Mult(Const(var l), Const(var r)): return Const(l*r);
        case Add(Const(0), var x): return Simplify(x);
        case Add(var x, Const(0)): return Simplify(x);
        case Add(Const(var l), Const(var r)): return Const(l+r);
        case Neg(Const(var k)): return Const(-k);
        default: return e;
      }
    }
    

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

    Здесь можно почитать первоисточник
    Поделиться публикацией

    Похожие публикации

    Комментарии 19
      +1
      1. Зачем
      var v = expr as Type;   
      if (v != null) {
          // Используем v
      }
      

      Когда expr is Type возвращает bool?

      2. Обязательно ли record type будет ссылочным? Возможно ли наследование?

      3. Является ли знак " * " литералом, который транслируется в default(T) или тут применено более сильное колдунство?
        +3
        1) потому что expr имеет тип Expr, а v — тип Type. чтобы пользоваться возможностями типа Type и не делать лишних преобразований.

        2) пример class Const(double Value): Expr; показывает, что наследование возможно.

        3) нет, скорее он транслируется в var $temp, где переменная $temp нигде не используется.
          +1
          3) Да, с реализации пока не понятно. Функционал еще не написан.
          0
          2. Дополнение: судя по оригиналу, record struct тоже возможно. Так что не обязательно ссылочный.
            0
            Думаю что конечным ответом на вопрос 2 будет, нет.
            +2
            А откуда в выражении
            if (c is Polar(5, *)) Console.WriteLine( R );
            берется R?
              +1
              Спасибо, я ошибся, уже исправил.
                +2
                Похоже, что опечатка. Интересно другое:
                — верно ли, что «s is Polar(5,*)» полностью эквивалентно «s is Polar(var $R,*) && $R==5»;
                — можно ли использовать такой же трюк в других вызовах с out
                — можно ли в других вызовах с out использовать var и *;
                — можно ли переопределить matching с входным параметром, чтобы вставить свою функцию проверки истинности (например, Math.Abs(R-5)<eps)
                  0
                  1) Мне кажется нет. Судя по всему определение перемененной будет в следующем блоке.
                  2) Вряд ли, это фишка оператора is
                  3) Это вроде как новая фишка C# общая.
                  4) Не понял, что вы имеете ввиду.
                    0
                    4) Что-нибудь такое:
                    public static class Polar
                    {
                        public static bool operator is(Cartesian c, out double R, out double Theta)
                        {
                            R = Math.Sqrt(c.X*c.X + c.Y*c.Y);
                            Theta = Math.Atan2(c.Y, c.X);
                            return c.X != 0 || c.Y != 0;
                        }
                        public static bool operator is(Cartesian c, double R, out double Theta)
                        {
                            if(Math.Abs(Math.Sqrt(c.X*c.X + c.Y*c.Y)-R)>R*1e-6) return false;
                            Theta = Math.Atan2(c.Y, c.X);
                            return c.X != 0 || c.Y != 0;
                        }
                    }
                    

                    Потому что иначе приведённый пример сработает только на 12 точках, а для остальной окружности скажет false.
                      0
                      Вряд ли так можно, компилятор не будет знать какой метод вызвать. А два подряд метода вызывать, мне кажется лишним усложнением.
                        0
                        Почему не будет знать? сигнатуры разные.
                          0
                          Да вы правы, плохо изучил ваш пример, информации об этом в спецификации, я не нашел. Но судя по общей логике это не учли, хотя все может изменится в любой момент
                            0
                            Да и пример не мой :D
                0
                Да вы правы, плохо изучил ваш пример, информации об этом в спецификации, я не нашел. Но судя по общей логике это не учли, хотя все может изменится в любой момент.
                  0
                  Извините, не в тот тред написал.
                  0
                  Пришел он к нам из таких языков как Python и F#.
                  Вроде они называют другой список языков, откуда позаимствовали: github.com/dotnet/roslyn/issues/1572#issue-64339348
                  Sources of Inspiration: Scala, F#, Swift, Rust, Erlang, Nemerle

                  Ещё бы discriminated union добавили, стало бы совсем хорошо.
                    0
                    Ещё бы discriminated union добавили, стало бы совсем хорошо


                    То что они называют записями и есть «discriminated union», точнее алгебраические типы.
                      0
                      Разве?
                      public class Cartesian(double x: X, double y: Y);
                      
                      Это совсем не discriminated union.
                      Алгебраические типы подразумевают два способа конструирования типов из уже имеющихся:
                      1. Произведение — есть почти во всех языках — структуры, кортежи (tuples), и, в данном случае, record type.
                      2. Сумма — это как раз тот самый discriminated union.

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                  Самое читаемое