Функционал F#, который потихоньку появляется и в C#


    Почему-то мы зачастую не используем этот функционал. Может быть еще не успели к нему привыкнуть. А иногда используем, при этом не имея представления, что это функционал из F#.

    Прежде чем перейти к его рассмотрению, давайте стремительно пробежимся домкратом небольшой ретроспективой по наиболее интересным фичам, которые появлялись в разные годы в разных версиях языка. Обратите внимание, что каждый раз новая версия языка выходит с новой версией Visual Studio. Для некоторых это может быть очевидно, но даже для разработчиков несколько лет отработавших с C# это может оказаться новостью (не все обращают на это внимание).

    Ретроспектива
    C# 1.0 Visual Studio 2002
    C# 1.1 Visual Studio 2003 — #line, pragma, xml doc comments
    C# 2.0 Visual Studio 2005 — Generics, Anonymous methods, iterators/yield, static classes
    C# 3.0 Visual Studio 2008 — LINQ, Lambda Expressions, Implicit typing, Extension methods
    C# 4.0 Visual Studio 2010 — dynamic, Optional parameters and named arguments
    C# 5.0 Visual Studio 2012 — async/await, Caller Information, some breaking changes
    C# 6.0 Visual Studio 2015 — Null-conditional operators, String Interpolation
    C# 7.0 Visual Studio 2017 — Tuples, Pattern matching, Local functions

    Некоторый функционал довольно редко используется, но что-то используется постоянно. Скажем, даже сейчас довольно часто все еще можно встретить использование OnPropertyChanged с указанием имени свойства. То есть что-то вроде OnPropertyChanged(«Price»); Хотя уже с 5-ой версии языка стало возможным получить имя вызываемого объекта с помощью CallerMemberName.

       public event PropertyChangedEventHandler PropertyChanged;
    
       public void OnPropertyChanged([CallerMemberName] string prop = "")
       {
           PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(prop));
       }
    

    Таким же образом можно организовать логирование. Получить имя метода из которого происходит вызов поможет все тот же атрибут CallerMemberName. Таким же образом можно получить имя файла и номер строки с помощью [CallerFilePath] и [CallerLineNumber]. Кстати, эти атрибуты можно использовать и в F#, но речь не о них.

    Говоря о новых возможностях, которые появились в C#, в последнее время никак нельзя не упомянуть «интервенцию» F#, которая началась, начиная с 6-ой версии языка. Все началось со всеми любимого LINQ. Вот перечисление только некоторых возможностей, которые появились в C#: LINQ, иммутабельность, Exception filters, Auto-property initializers, Expression-bodied function members, Pattern matching, Tuples
    Судя по всему, даже если вы не начнете в ближайшее время изучать F#, то скоро функциональное программирование станет вам чуть-чуть знакомым. Давайте рассмотрим несколько «функциональных» возможностей C#.

    Иммутабельность


    Это ни что иное, как неизменяемость объектов. То есть, значение объекта не может быть изменено после создания. В F# все переменные по умолчанию неизменяемы. Каким образом это может быть реализовано в C#? Начиная с 6-ой версии можно создать read-only свойства не указывая set. Например, так:

       public string Name { get;}
    

    Таким образом становится возможным комфортным образом создавать свои иммутабельные объекты.

    Exception filters


    А это возможность при «ловле» ошибок указать параметр, при котором сработает захват. Например, так:

       try
       {
          SomeMethod(param);
       }
       catch (Exception e) when (param == null)
       {
       }
       catch (Exception e)
       {
       }
    

    В первом блоке ошибка будет отловлена только в том случае, если param==null. Во второй попадут ошибки, возникшие при значениях param отличных от null.

    Auto-property initializers


    Это возможность инициализировать свойство сразу после методов доступа (аксессоров). Например, так:

       public string AppUrl { get; set; } = "http://lalala.com";
    

    Или можно даже вынести инициализацию в отдельный метод, возвращающий значение.

       public string AppUrl { get; set; } = InitializeProperty();
       public static string InitializeProperty()
       {
         return "http://lalala.com ";
       }
    


    Expression-bodied function members


    Удобная возможность сократить код с помощью лямбда выражений. Методы, которые раньше записывались так:

       public int Sum(int x, int y)
       {
         return x+y;
       }
    
       public string ServerIP { get { return "65.23.135.201";  } }
    

    теперь можно записать гораздо короче:

       public int Sum(int x, int y) => x+y;
    
       public string ServerIP => "65.23.135.201";
    

    Все эти только что перечисленные возможности появились в 6-ой версии C#.
    Теперь давайте разберем чуть более подробно то, что пришло из F# в 7-ой версии языка.

    Кортежи или Tuples


    Этот функционал по мнению многих может сравнится с LINQ по степени важности и удобности использования.
    Что по себе он нам дает. В первую очередь это возможность вернуть из метода несколько значений, не создавая при этом экземпляр какого-либо класса. Потому как передать в метод мы можем несколько параметров, а вот вернуть получится только что-то одно. Повторюсь, что типичным решением для данной потребности до сих пор было создание экземпляра класса. Сейчас же мы можем создать кортеж.
    Типичный пример кортежа:

       var unnamed = (35, "What is your age?");
    

    Как вы можете заметить это ни что иное как переменная, которая содержит в скобках два значения. В данном случае кортеж называется неименованным и к значениям можно обратится по имени Item с номером. Например, unnamed.Item1 содержит в себе 35, а unnamed.Item2 строку с текстом «What is your age?»

    Если кто-то заметит, что кортежи похожи на anonymous types, то будет прав. Но есть нюанс, о котором не стоит забывать. Anonymous types могут использоваться только в области видимости (scope) метода.

    Бывают именованные кортежи.

       var named = (Answer: 35, Question: "What’s your age again?");
    

    К переменным из именованного кортежа можно обратится по именам. В данном случае named.Answer хранит 35, а named.Question строку с текстом вопроса.
    Простейший пример. Метод, который возвращает значение в виде кортежа:

       (int, string) GetXY()
       {
           int x = 1;
           string y = "One";
           return (x, y);
       }
    

    Возвращаемым значением метода является кортеж, первым значением которого является целое число типа int, а вторым строка.
    Получаем значение в переменную так:

       var xy = GetXY();
    

    Теперь можем обращаться к элементам кортежа по xy.Item1 и xy.Item2

    У кортежей есть такая интересная возможность как deconstruction/деконструкция. Это получение обычными переменными значений из кортежа. Ну или можно сказать разложение кортежа на отдельные переменные. Например, есть такой вот метод, возвращающий список и какой-то ID-шник, относящийся к данному списку:

       (int, List<string>) GetListWithId()
       {
           int x = 1;
           List<string> y = new List<string>();
           return (x, y);
       }
    

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

       (int x, List<string> y) = GetListWithId();
       var (x, y) = GetXY();
    

    В C# версии 7.1 появилась возможность под названием infer tuple names. На русский можно перевести примерно, как: предполагаемые имена кортежей. Это значит, что можно будет обратиться к неименованному элементу кортежа не только по Item1, Item2 и т.д., но и по имени, которое формируется исходя из названия переменной принимавшей участие в создании кортежа. Пример:

       var tuple = (x.a, y);
    

    К элементам этого кортежа можно обратиться по именам tuple.a и tuple.y

    Что интересно, так это то, что это изменение 7.1 является breaking back compatibility. То есть обратно несовместимым. Но так как промежуток между выходом C# 7 и C# 7.1 небольшой, то его решили внести. Суть в чем. Какой-то код, который работал определенным образом в C# 7 будет работать иначе в C# 7.1.
    В данном случае возможность встретить такой код в реальном проекте чрезвычайно мала.
    Пример. Допустим у вас есть такой код:

       int x=1;
       Action y = () => SomeMethod();
       var tuple = (a: x, y);
       tuple.y();
    

    Обратите внимание на последнюю строчку, которая очевидно вызывает метод с именем SomeMethod (и она действительно вызывает этот метод, но только начиная с C# 7.1)
    Так вот. В C# 7.0 был бы вызван не SomeMethod, а extension метод с именем y. Допустим, такой:

       public static class ExtClass
       {
           public static void y(this (int, Action) z)
           {
             // some code 
           }
       }
    

    Согласитесь, что extension методы для tuples это редкость.

    Если вы используете проект с версией .NET Framework ниже чем 4.7, то вы сможете использовать кортежи только установив NuGet пакет System.ValueTuple. Впрочем, Visual Studio 2017 должна сама вам подсказать это, если вы начнете использовать синтаксис кортежей. Может быть поэтому кортежи пока что все еще не так часто используются. Необходимо не только работать в последней версии Visual Studio, но и на одной из последних версий фреймворка (или устанавливать пакет NuGet).

    Pattern Matching


    Под этим довольно неоднозначным названием содержится на удивление довольно простой функционал.
    Давайте рассмотрим пример кода, который использовался ранее и, конечно же, может использоваться сейчас:

       if (someObject is Customer)
       {
          var c = (Customer)someObject; 
          c.Balance = c.Balance + 1000;
       }
    

    Код совершенно нормальный, типичный, часто используемый и нельзя сказать, что в нем что-то не так. Однако, теперь появилась возможность его немного сократить:

       if (someObject is Customer c) c.Balance = c.Balance + 1000;
    

    Получается, что если someObject относится к классу Customer, то он преобразуется в переменную с типа Customer. И с этой переменной сразу можно начинать работать.
    Можно добавить, что pattern matching это не только синтаксический сахар, но и возможность разработчикам имевших дело с подобными конструкциями в других языках использовать их в C#.
    Pattern matching можно использовать не только с if, но и с конструкциями Swith. Пример:

       switch (userRole)
        {
            case Manager m:
                return m.Salary;
            case Partner p:
                return p.Income;
        }
    

    Кроме того, можно делать уточнения с помощью условий with

       switch (userRole)
        {
            case Manager m with Salary<1500:
                return m.Salary*1.2;
           case Manager m:
                return m.Salary;
           case Partner p:
                return p.Income;
        }
    

    В данном случае первый case будет выполнен только если userRole это менеджер и значение его Salary меньше 1500. То есть m.Salary<1500

    В списке предполагаемых нововведений в C# 8 есть функционал, который не так давно появился в Java. А именно: методы интерфейсов по умолчанию (default interface methods). В частности, с помощью данного функционала можно будет изменять legacy-код. Например, самим разработчикам Java удалось с помощью данного функционала улучшить API коллекций и добавить поддержку лямбда выражений. Может быть и разработчики C# тоже хотят что-то изменить в самом C# с помощью этого функционала?

    Не смотря на появившееся сходство, интерфейсы все еще довольно отличаются от абстрактных классов. От множественного наследования в C# отказались давно из-за множества возникающих сложностей. Но в данном случае, используя только один метод, сложностей должно оказаться меньше. В случае, если класс наследует от двух или большего количества интерфейсов и в нескольких интерфейсах присутствует метод с одним и тем же названием, то класс должен обязательно указать имя метода с уточнением имени интерфейса. Отследить только один метод проще (в частности и с помощью IDE).

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

    Подробнее
    Реклама
    Комментарии 42
    • –3

      Статья была про "функционал F# перенесенный в С#". Ожидал увидеть как реализуется на F#, а потом аналоги в C#.


      Некоторые новые фичи наоборот огорчают, например, описанные выше кортежи/деструктуризация. Если тебе нужно вернуть сложные значения — вводи класс, делай интерфейс, вспоминай Фаулера и рефактори, чтобы код был поддерживаемым, расширяемым и тестируемым… А не вкрячивай Tuple с хитрожопым синтаксисом просто потому что лень. Есть небольшие опасения, что можно C# довести до состояния JS-помойки напихивая в него всё подряд.

      • +5
        вводи класс, делай интерфейс, вспоминай Фаулера и рефактори, чтобы код был поддерживаемым, расширяемым и тестируемым… А не вкрячивай Tuple с хитрожопым синтаксисом просто потому что лень

        Поначалу также думал про анонимные типы, потом ничего — втянулся:)
        • 0

          Анонимные типы, которые используются в пределах одного метода — это нормально. Но если возвращать из методов, то имхо та же проблема, что и возврата кортежей — придётся выдавать наружу object/dynamic, абсолютно неподдерживаемо.

          • 0
            А зачем возвращать?
            Это же та же ситуация с именованием переменных: если переменная «очень временная», то можно её обозвать коротким именем, не несущим смысла, а если полноценная, будь добр придумать достойное имя.
            И анонимные типы, и кортежи расширяют возможности программиста, который обязан пользоваться здравым смыслом.
            • 0
              А зачем возвращать?

              Я, наверное, неточно выразился. Про возврат кортежей — это про деструктуризацию. Она, как раз на результаты методов только и работает.

            • +1

              Зависит от метода. Можно попросить компилятор помочь вернуть анонимный тип через дженерики, без всяких путающих кастов. Что-то типа этого:


              T SomeMethod(Func<SomeType, T> callback)
              {
                  ...
                  return callback(_someObjOfSomeType);
              }
              
              void Main()
              {
                  var anonObj = SomeMethod(someType => new {X = someType.SomeProp});
                  Console.WriteLine(anonObj.X);
              }
          • +2
            Для себя взял простое правило и другим советую: использовать кортежи и тому подобные анонимные структуры только в приватных методах.
            • +1

              Кортежи можно рассматривать как альтернативу out-параметрам.
              Вот есть же в стандартной библиотеке соглашение об именовании:


              bool TryDoSomething(out TResult result)

              Но с async/await такой фокус не прокатит: out-параметры запрещены.
              А теперь мы сможем написать что-то вроде:


              Task<(TResult result, bool ok)> TryDoSomethingAsync()
              
              var (result, ok) = await TryDoSomethingAsync();
              if (!ok) { ... }
              • +2
                Здесь лучше использовать паттерн Opition (или Maybe), благо библиотек, его реализующих, тьма — LanguageExt, например
                • –1

                  Позвольте с Вами не согласиться. Точечное применение Maybe в императивном коде не даст особых преимуществ по сравнению с кортежами. А если притащить LanguageExt, то весь код придется писать в функциональном стиле. Иначе получится каша.


                  А писать полностью в функциональной парадигме на C# мало кто может себе позволить:


                  • Если у Вас открытый проект – Вы сделаете его API сложным для новичков.
                  • Вся Ваша команда должна разбираться в ФП. Потому что без знания какого-то языка заточенного под ФП, и без знания теории, очень трудно понять что происходит в LanguageExt, а главное, зачем.
                  • И наконец, если у Вас есть такая команда, почему бы Вам не писать сразу на F# ?
            • 0

              Туплы — это реализация sum type. Используются когда надо запретить каррирование некого набора типов.


              То что в C# sum type проще делать через классы — проблема C#

              • +2
                То что в C# sum type проще делать через классы — проблема C#

                Это не проблема. При проектировании системы типов C# просто делались другие допущения. И то что логически в F# — sum type, в C# — интерфейс. Про это и речь, не надо тянуть элементы других языков и парадигм туда, где всё спроектировано иначе, в итоге получим мешанину без логики.

                • +3
                  Кортежи — это типы-произведения, а не суммы.
                  • 0

                    Да, косякнул)

                • +2

                  Иногда проще в каком-нибудь методе для Dictionary/HashSet заюзать Tuple, чем городить под это дело целый класс с однотипной реализацией override Equals.

                  • +1

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


                    После разделения на отдельные логические части нам требуется способ обменяться данными. Есть два варианта — предложенные кортежи и куча DTO.


                    Чем огромная куча DTO лучше? Она волшебным образом делает код "поддерживаемым, расширяемым и тестируемым"? Нет, просто появится кучка новых классов, которые используются в одном месте.

                    • 0
                      Чем огромная куча DTO лучше? Она волшебным образом делает код "поддерживаемым, расширяемым и тестируемым"? Нет, просто появится кучка новых классов, которые используются в одном месте.

                      Если у вас классы, которые осуществляют "обмен между отдельными логическими частями" системы используются "в одном месте", то у вас где-то присутствует ошибка проектирования. Интерфейс между логическими частями должен обладать определённой стабильностью и реиспользоваться много-много раз. Иерархия DTO — наследоваться и документироваться. Скорее всего в таком случае у вас "абстракции протекают" и вы выставляете детали реализации наружу посредством динамическим "разовых" данных. В таком случае это надо прятать подальше от глаз в приватные методы/внутренности методов посредством рефакторинга, а не оборачивать синтаксическим сахаром и говорить "вот теперь норм".

                      • +1

                        У вас ошибка в предположениях, имелись в виду отдельные логические части не системы, но процесса обработки. Одного, но большого. Который вполне себе разделяется на более маленькие части. Хотя они сильно связанные, но их публичными делать не надо. Переиспользоваться 99% из них внешними тоже не будут, ибо они имеют смысл только в контексте данного процесса. И из-за переменчивой структуры реальности смысла вводить жесткий интерфейс между ними нет.


                        Но даже если рассматривать более большие части, в чем принципиальное преимущество DTO перед именованными кортежами из двух-трех значений?


                        Их можно наследовать? На мой взгляд, если вам нужно наследовать DTO, то всё свернуло куда-то не туда. Наследование вообще нужно применять минимально и стараться избегать наследования данных. А так как DTO из себя представляют чистые данные, которые кто-то запихнул в оболочку объекта (из-за отсутсвия более подходящих средств для их удержания), то наследовать их не надо.


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

                    • 0
                      «вводи класс, делай интерфейс»… интерфейс не нужен когда речь идет о структурных immutable типах, что касется классов в них тоже может не быть необходимости. Например на уровне абстрактных библиотек ввода/вывода: вы можете предоставлять пользователю возможность считать из потока три записи byte[] Read(), затем предлогая пользователю самому привести их к ожидаемым типам, а можете предоставлять возможность считать кортеж (задавая спецификацию приведения типов как generic параметры: Tuple<int, string, double> Read<int, string, double>() ). Тоже самое можно сделать и передавая через generic параметер любой class и используя reflection обойти его свойства, но на мой взгляд это будет гораздо более медленно, многословно и ненадежно. Неужели вы так и поступите?
                      • 0

                        Причем тут рефлексия? Что вам мешает вместо Tuple<int, string, double> использовать


                        IReadResult
                        { 
                        int Length {get;set;}
                        string LogicName {get;set;}
                        double Length {get;set;}
                         }

                        ? Например IReadResult Read()?
                        Во-первых, логичные имена переменных.
                        Во-вторых, операцию ввода-вывода вы сможете нормально замокать и протестить родительский метод.
                        В-третьих, можно будет отнаследоваться от интерфейса и расширить эту операцию в дочерних классах.
                        В-четвертых, возможна инкапсуляция некоторой логики в свойствах.

                        • 0
                          Я имел ввиду абстрактную функциональность ввода вывода, которая может возвратить разные кортежи разной длины. Семью функций T1 Read>T1>(), Tuple<T1,T2> Read<T1, T2>() ,Tuple<T1,T2,T3> Read<T1, T2,T3>() и т.д.
                          А так вы правы, что Reflection не обязателен, возможно чтобы вернуть IReadResult передавать еще и «DataAdapter», где руками прописать как порождать IReadResult из трех byte[]. Все же это будет сложнее, чем задать спецификацию через generic параметры.

                          Пришел в голову еще один арумент, если первая функция T1 Read >T1>() не вызывает паники то и остальные (с кортежами, типа Tuple<T1,T2,T3,T4> Read<T1,T2,T3,T4>() ) не должны, они просто логическое ее продолжение.

                          Логичные имена дать можно var (id, type, weight) = Read<int, string, double>(); а про замокать, отнаследовать и инкапсулировать — повторю, далеко не всем необходимо иметь возможность мокать, наследовать и инкапсулировать иммутабельные структуры данных.
                          • 0
                            виду абстрактную функциональность ввода вывода, которая может возвратить разные кортежи разной длины
                            если первая функция T1 Read >T1>() не вызывает паники то и остальные (с кортежами, типа Tuple<T1,T2,T3,T4> Read<T1,T2,T3,T4>() ) не должны, они просто логическое ее продолжение.

                            Вот как раз немного вызывает панику. :) Почему? Потому что в методе T1 Read<T1>() where T1: class как ты не меняй внутренности класса T1 сигнатура и контракт метода остается одинаковым. Что позволяет не переписывать тонну кода, которые с этим связано и проектировать систему внешних классов на этом интерфейсе. А вот при использовании примитивов и переменного количества аргументов как минимум это будет плыть.

                            • 0
                              T1,T2,T3 тут примитивы, поэтому за фразой «как ты не меняй внутренности класса T1» я опять не вижу никакой реальной проблемы. если поменялся протокол потока (добавилось поле в считываемый рекордсет) то переписал и спецификацию считывания: было Tuple<T1,T2,T3> Read<T1,T2,T3>() стало Tuple<T1,T2,T3,T4> Read<T1,T2,T3,T4>() У интерфейса построенного на кортежах не может поплыть более чем у пользователе интерфейса построенного на class и dataadapter.

                              Я понимаю так, что ваша озадаченность, это вопрос: «а что вы потом с кортежем делать будете», и далее сами отвечате: «вот были бы классы могли бы вовне скинуть»! Ответ: во-первых если не уходит IReadResult во вне то зачем его объявлять (например потому что то к тому что мы считали, должны добавить четвертое поле dateTime — и это другой class), и во-вторых не всегда внешней системе нужны C# classы, например при сериализации в json во внешнюю систему уйдет {5,«typeA»,0.1123} в любом случае, сериализуйте вы instance class или tuple.

                              • 0
                                Пример кода, для конкретики.

                                var source = transaction.ReadList<int, string, double>("SELECT id, type, weight FROM dbo.ITEM_VIEW");
                                foreach ( var (id, type, weight) in source)
                                {
                                    //...
                                }
                                


                                Я понимаю какие вопросы могут быть к такому Data Access Layer, и наверно ими можно оправдать замечания к языку «новые фичи которые огорчают», тем не мнее, программисты с удовольствием будут использовать кортежи где архитектура им это позволяет.
                    • +3
                      Как мне кажется, введение методов по умолчанию в интерфейсах — это бред. Есть же абстрактные классы?
                      Можно было бы ввести концепты, трейты, методы расширения для всего, как предлагали на github, но вводить реализацию по умолчанию в интерфейсы(которые как бы контракт) — какой-то бред.
                      • 0
                        введение методов по умолчанию в интерфейсах — это бред. Есть же абстрактные классы?

                        Если что, напоминаю, что в c# множественного наследования НЕТ.
                      • +1

                        Хотелось бы вместо Pattern Matching получить Smart Cast:
                        Pattern Matching:


                        object some = other();
                        if (some is string s)
                            return s.Length;

                        Smart Cast:


                        object some = other();
                        if (some is string)
                            return some.Length;
                        • 0
                          Да пользуюсь этим в type script, очень удобно.
                          • 0
                            Вот здесь я описывал, почему в C# это сложно.
                            • 0

                              Эрик Липперт где-то писал почему этого не сделали, а вообще я тоже хотел эту фичу (понравилась в котлине) но вариант с ПМ не сильно многословнее

                              • 0
                                Возникает неопределённость в случае явно (explicit) реализованных методов интерфейса. Конечно, писать разный код для методов с одинаковой сигнатурой — это те ещё грабли, но такая фича есть и теперь из песни, как говорится, слов не выкинешь.
                              • 0
                                Иммутабельность
                                Это ни что иное, как неизменяемость объектов.

                                А чем это отличается от старых добрых констант const?
                                (реально любопытно)

                                • 0

                                  const в C# используется для констант времени компиляции, здесь же после установки свойства при создании объекта оно больше не будет меняться

                                  • 0

                                    То что объявлено как const, разве можно потом изменять? (O_O)

                                    • +1
                                      То что объявлено как const нужно инициализировать сразу.
                                      • 0

                                        Нет, но у const принципиально иное поведение. Они не хранят значение, но компилятор подставляет его во все места, где они используются. Поэтому пока что возможны только константы примитивных типов.


                                        const int c = 3;
                                        ...
                                        var x = c; // компилятор подставит 3 на место c
                                        ...
                                        object p { get; } = new object();
                                        ...
                                        var y = p; // произойдет обращение к свойству
                                    • +2
                                      Это просто синтаксический сахар, упрощающий такую конструкцию:
                                      readonly string _s;
                                      public string S => _s;
                                      До такой:
                                      public string S { get; }
                                    • +1
                                      А еще можно написать extension для KeyValuePair который будет декомпозировать его:
                                      public static class KeyValueExt 
                                      {
                                          public static void Deconstruct<K,V>(this KeyValuePair<K,V> pair, out K key, out V val) 
                                          {
                                              key = pair.Key;
                                              val = pair.Value;
                                          }
                                          
                                          public static void Test()
                                          {
                                              var dict = new Dictionary<string, int> { {"123", 213} };
                                              
                                              foreach(var (k,v) in dict)
                                              {
                                                  var s = $"key {k}, value {v}";
                                              }
                                          } 
                                      }
                                      • –1
                                        Если уж заговорили про Java, то я из «вражеского лагеря» забрал бы их функционал Enum.
                                        • +2

                                          Надо забирать discriminated union из F#. Очень.

                                          • 0
                                            Стоит ещё упомянуть различные implicit в scala…
                                            • 0
                                              Очень мечтаю о Case-классах и автогенерируемом copy() в частности.

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