В апреле 2003-его года был выпущен C# 1.2 и с тех пор все версии имели только major версию.
И вот сейчас, если верить официальной страничке roslyn на github, в работе версии 7.1 и 7.2.

Для того, чтобы попробовать версию 7.1 необходимо установить pre-release Visual Studio. Скачать ее можно скачать с официального сайта

Зайдя в свойства решения можно выбрать используемую версию языка.



Далее я хочу рассказать о понравившихся мне новых фичах языка.

Асинхронный Main (планируется в версии 7.1)


В наше время очень часто встречаются программы, которые почти полностью асинхронные.
До сих пор Main мог быть void или int. И мог содержать в себе аргументы в виде массива строк. Сейчас добавятся несколько новых перегрузок:

public static Task Main();
public static Task<int> Main();
public static Task Main(string[] args);
public static Task<int> Main(string[] args);

Так как CLR не поддерживает асинхронные entrypoints, то компилятор сам создает boilerplate code (то есть автоматически вставляет какой-то дополнительный код, который позволяет привести void к Task)
Примерно такой код будет сгенерирован компилятором:

async Task<int> Main(string[] args) {
    // здесь какой-то ваш код
}
// этот метод сгенерируется «за кулисами» автоматически
int $GeneratedMain(string[] args) {
    return Main(args).GetAwaiter().GetResult();
}

Я как и многие ожидал этот функционал еще в 7-ой версии языка.

Литерал default (планируется в версии 7.1)


У Visual Basic и у C# возможности примерно одинаковые. Но в самих языках есть определенные различия. Скажем у C# есть null, а у VB.NET есть Nothing. Так вот, Nothing может конвертироваться в любой системный тип, представляя собой значение по умолчанию для этого типа. Понятно, что null этого не делает. Хотя, значением по умолчанию вполне может быть и null.

Сейчас уже есть и применяется выражение default(T). Рассмотрим пример его применения. Допустим, что у нас есть метод, который принимает массив строк в качестве параметра:

void SomeMethod(string[] args)
{
            
}

Можно выполнить метод и передать ему значение массива строк по умолчанию так:

SomeMethod(default(string[]));

Но зачем писать default(string[]), если можно просто написать default?

Вот, начиная с C# 7.1 и можно будет пользоваться литералом default. Что еще можно будет с ним сделать, кроме как передать значение по умолчанию в метод? Например, его можно использовать в качестве значения необязательного параметра:

void SomeMethod(string[] args = default)
{
            
}

или инициализировать переменную значением по умолчанию:

int i = default;

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

int i = 1; 
if (i == default) { }   // значением по умолчанию типа int является 0
if (i is default) { }     // точно такая же проверка

Что нельзя будет сделать? Следующие примеры того как литерал default использовать нельзя:

const int? y = default;  
if (default == default)
if (default is T) // оператор is нельзя использовать с default
var i = default
throw default
default as int; // 'as' может быть только reference тип

Readonly ref (планируется в версии 7.2)


При отправке структур в методы в качестве by value (по значению) происходит копирование объекта, что стоит ресурсов. А иногда бывает необходимость отправить только значение ссылочного объекта. В таком случае разработчики отправляют значение по ссылке ref для того, чтобы сэкономить память, но при этом пишут в комментариях что значение изменять нельзя. Особенно часто это происходит при математических операциях с объектами vector и matrix.

Понятный пример того что нас ждет:

    static Vector3 Add (ref readonly Vector3 v1, ref readonly Vector3 v2)
    {
        // так нельзя!
        v1 = default(Vector3);

        // и так нельзя!
        v1.X = 0;

        // так тоже нельзя!
        foo(ref v1.X);

        // а вот теперь нормально
        return new Vector3(v1.X +v2.X, v1.Y + v2.Y, v1.Z + v2.Z);
    }

Эта функция пока что в состоянии прототипа, но я уверен в ее необходимости. Одним из предлагаемых вариантов является использование ключевого слова in вместо ref readonly

    static Vector3 Add (in Vector3 v1, in Vector3 v2)
   {
       return new Vector3(v1.X +v2.X, v1.Y + v2.Y, v1.Z + v2.Z);
   }

Почему in, ведь ref readonly понятнее? А потому что in короче.

Методы интерфейсов по умолчанию (планируется в версии 8.0)


Эти методы мы увидим не так скоро.

Представьте себе, что есть какой-то класс или структура, которые реализуют интерфейс. И вот они смогут унаследовать реализацию одного метода из интерфейса(!) или должны будут реализовать свою версию этого метода.

Уже само понятие реализации в интерфейсе звучит довольно странно. Хотя у Java 8 есть методы интерфейсов по умолчанию, и разработчики использующие C# тоже захотели себе подобный функционал.

Этот функционал позволит автору API изменить код метода для всех уже существующих реализаций интерфейса.

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

interface IA
{
 void SomeMethod() { WriteLine("Вызван SomeMethod интерфейса IA"); }
}

Теперь создадим класс, которые реализует интерфейс:

class C : IA { }

И создадим экземпляр интерфейса:

IA i = new C();

Теперь мы можем вызвать метод SomeMethod:

i.SomeMethod(); // выведет на экран "Вызван SomeMethod интерфейса IA"

Предположительные имена кортежей (планируется в версии 7.1)


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

Пример:

Вместо того, чтобы писать (f1: x.f1, f2: x?.f2) можно просто написать (x.f1, x?.f2).
Первый элемент кортежа в таком случчае получит имя f1, а второй f2
Сейчас же кортеж стал бы неименнованным (к элементам которого можно обратиться только по item1, item2 ...)

Предположительные имена особенно удобны при использовании кортежей в LINQ

// c и result содержат в себе элементы с именами f1 и f2
var result = list.Select(c => (c.f1, c.f2)).Where(t => t.f2 == 1);

Отрицательные стороны

Основным недостатком введения этого функционала является совместимость с C# 7.0. Пример:

Action y = () => M();
var t = (x: x, y);
t.y(); // ранее был бы выбран extension method y(), а сейчас будет вызвана лямбда

Но с этим небольшим нарушением совместимости можно смириться, так как период между выходом 7.0 и 7.1 будет небольшим.

Собственно, уже сейчас Visual Studio при использовании 7.0 предупреждает, что
Tuple element name 'y' is inferred. Please use language version 7.1 or greater to access an element by its inferred name.

Полный код примера:

class Program
{
   static void Main(string[] args)
   {
       string x = "demo";
       Action y = () => M();
       var t = (x: x, y);
       t.y(); // ранее был бы выбран extension method y(), а сейчас будет вызвана лямбда  
   }

   private static void M()
   {
       Console.WriteLine("M");
   }
}

public static class MyExtensions
{
   public static void y(this (string s, Action a) tu)
   {
       Console.WriteLine("extension method");
   }
}

Если вы используете .NET 4.6.2 и ниже, то для того чтобы попробовать пример вам необходимо установить NuGet пакет System.ValueTuple.
Поделиться с друзьями
-->

Комментарии (68)


  1. AndrewN
    27.06.2017 08:09
    +9

    Язык развивается, это всегда приятно. С другой стороны, я рад что начал изучать C# достаточно давно и теперь все эти новые фичи поступают малыми порциями и легко усваиваются. А каково будет только начинающему изучать язык через пару лет новичку? Он захлебнется в куче этих фич… Да даже если я сейчас начну изучать тот же С++, там та же картина? Или я заблуждаюсь?


    1. AgentFire
      27.06.2017 12:49
      +3

      Там вообще жесть :)


    1. alexeykuzmin0
      27.06.2017 13:14
      +2

      Насколько я могу судить, в c++ еще хуже


      1. Flowneee
        27.06.2017 21:21
        -1

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


        1. alexeykuzmin0
          27.06.2017 21:23

          В онгоуинге освоить вообще нормально, и даже не хватает, приходится boost использовать и иже с ним.
          А вот осваивать современный c++ с нуля, мне кажется, жесть


        1. mayorovp
          27.06.2017 21:24
          +1

          А каково будет только начинающему изучать язык через пару лет новичку?


    1. Hydro
      27.06.2017 13:43

      В точку.
      Прям как с питоном ситуация.


      1. Idot
        27.06.2017 14:38

        А что с Питоном?


        1. Yngvie
          27.06.2017 15:33

          Ну тоже появляются новые вещи, в том числе и в синтаксисе. Всякие yield from, f-strings, async/await, dict literals, raise from.

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


      1. waverunner
        27.06.2017 15:34

        Да, а что с питоном?


  1. MonkAlex
    27.06.2017 08:18
    +8

    А зачем нужны методы интерфейса с собственной реализацией?

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


    1. Illivion
      27.06.2017 08:42
      +5

      И почему бы не использовать для этого абстрактный класс с виртуальными методами…


      1. viteralex
        27.06.2017 09:10
        +2

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


        1. devel0per
          27.06.2017 21:22
          +3

          Раз такие дела, то может и без интерфейса обойтись? Интерфейсы хорошо бы создавать с целью, а не потому, что модно!


      1. Mishkun
        27.06.2017 13:37
        +4

        Потому что множественное наследование запрещено. А методы по умолчанию позволяют расширять legacy API не ломая старый код


      1. svekl
        27.06.2017 13:37
        +1

        Нельзя наследоваться от двух или более абстрактных классов, сейчас самое близкое к тому, что это даст — это два интерфейса с методами расширения.


      1. futureader
        27.06.2017 17:44
        -3

        Потому что dependency injection на абстрактном классе не сделать


        1. mayorovp
          27.06.2017 19:12
          +1

          А в чем, собственно, проблема-то?


        1. denismaster
          27.06.2017 22:02

          Можно и на абстрактном классе. У М.Симана в его книжке про DI в .NET об этом есть сноска.


          1. Serg046
            28.06.2017 16:44
            +1

            Как минимум, чисто технически, можно и обычный неабстрактный класс внедрять.


    1. gem81
      27.06.2017 09:10

      Возник тот же вопрос. Тема реализации методов в интерфейсе не раскрыта ((


    1. NeoCode
      27.06.2017 09:21
      +2

      Интересно чем интерфейсы с реализацией методов будут отличаться от множественного наследования C++?


      1. mayorovp
        27.06.2017 09:52
        +5

        У интерфейса не может быть нестатических полей.


        1. vchs
          27.06.2017 22:26
          -1

          Наоборот, интерфейс не может содержать статические поля!


      1. indestructable
        27.06.2017 12:18
        -2

        Думаю, отличается правилами вызова. Например:


        1. Один интерфейс с реализованным методом — класс использует реализацию интерфейса.
        2. Переопределяем метод у класса — класс использует свою реализацию. Каст к интерфейсу вызывает реализацию из интерфейса.
        3. Несколько интерфейсов с реализацией — работает как будто несколько интерфейсов реализованно явно.


        1. mayorovp
          27.06.2017 12:21

          Каст к интерфейсу вызывает реализацию из интерфейса.

          Если бы все было именно так — то в этой фиче не было бы ни малейшего смысла, потому что сейчас именно так работают Extension Methods. Даже само название "Default Interface Methods" подразумевает, что реализация из интерфейса используется только если она не переопределена в классе.


    1. AndreiShenets
      27.06.2017 09:21
      +8

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


      1. kaiZer_dragomir
        27.06.2017 14:41
        +1

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


    1. TheKnight
      27.06.2017 09:35
      +5

      А это кажись стырили у Java :) Там это было нужно для нормальной работы стримов со старым кодом.


      1. izzholtik
        27.06.2017 12:04

        так вот оно что.


    1. mayorovp
      27.06.2017 09:52
      +2

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


      Посмотрите сколько явных проверок типа сделано в реализации Enumerable — каждую из них можно было бы заменить переопределением виртуальной функции. Не уверен насчет скорости работы — но расширяемее это бы получилось однозначно.


    1. Shatun
      27.06.2017 12:25
      +3

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


    1. White_Scorpion
      27.06.2017 12:29

      Хм, снова возвращаемся к проблеме ромбовидного наследовния? И для решения постоянно прийдётся писать explicit?


      1. mayorovp
        27.06.2017 12:34
        +1

        Отсутствие полей у интерфейсов, а также тот факт что реализация интерфейса — всегда виртуальная в терминах С++ снимают главные проблемы ромбовидного наследования. Остается только проблема конфликтов имен.


    1. wheercool
      27.06.2017 12:36
      +2

      Я думаю это попытка воплотить typeclass из Haskell.
      Самый простой пример:

      interface Eq {
        bool Equal(a, b) { return !NotEqual(a, b);}
        bool NotEqual(a, b) {return !Equal(a, b);}
      }
      

      Таким образом для реализации интерфейсов у нас минимальный набор операций для реализации это или Equal или NotEqual. В нек-ых случаях может оказаться, что операцию NotEqual реализовать проще чем Equal, а оставшуюся операцию мы получаем «бесплатно».


      1. impwx
        27.06.2017 14:57
        +2

        Имхо это плохой пример.

        Во-первых, зачем нужен отдельный метод NotEqual, когда можно написать !Equal(a, b)? Если вдруг проще вычислить неравенство, можно просто инвертировать это значение перед возвратом из метода.

        Во-вторых, добавляя такой интерфейс к классу, мы по умолчанию получаем возможность свалиться в бесконечную рекурсию. Компилятор уже не напомнит, что мы забыли реализовать «нормальное» сравнение объектов. Довольно дорогая цена за сомнительное удобство!


        1. 0xd34df00d
          28.06.2017 22:11

          Пример действительно плохой, со сравнениями всё слишком тривиально.


          Лучше рассмотреть, скажем, тайпкласс для монад (для некоторых удобнее написать join :: m (m a) -> m a, для некоторых — bind aka >>= :: m a -> (a -> m b) -> m b. Или, скажем, Traversable, или вообще Foldable, в котором вообще мясо, и минимальным необходимым определением является одна из двух функций, остальную дюжину функций этого тайпкласса можно вывести из них.


    1. sasha1024
      27.06.2017 12:45

      Это позволяет использовать интерфейсы, как некое подобие trait'ов.


    1. ZerGer
      27.06.2017 13:36

      Методы для интерфейсов и сейчас без проблем добавляются через extensions-методы:


      public interface IRectangle
      {
          int Width { get; }
          int Height { get; }
      }
      
      public static class IRectangleExtensions
      {
          public static int GetArea(this IRectangle r) => r.Width * r.Height;
      }

      Как по мне, так вполне вменяемое применение. Просто упростят реализацию.
      И будет логично, что переопределить метод интерфейса будет невозможно.


      1. wheercool
        27.06.2017 13:51
        +4

        И будет логично, что переопределить метод интерфейса будет невозможно

        Как раз-таки в этом и вся соль, что можно переопределить метод по умолчанию.


    1. IL_Agent
      27.06.2017 22:53
      +1

      Получается, мы делаем интерфейс и сразу extension метод к нему, но который можно переопределять в реализациях… Функционально, но засоряет интерфейс, на мой взгляд. Лучше б какие-нибудь виртуальные extension методы запилили.


    1. sidristij
      28.06.2017 09:05
      +1

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


      1. mayorovp
        28.06.2017 09:14

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


        1. sidristij
          28.06.2017 10:31

          Абстрактные классы? Описание протокола взаимодействия — это, считай, документация ожиданий в реализации. Не более того


      1. mayorovp
        28.06.2017 10:35

        Зайду с другой стороны.


        В чем вы видите принципиальное отличие между двумя интерфейсами ниже и почему первый — это нормально, а второй — лютая жесть и поломанная концепция?


        interface A {
          void Foo(int x = 42);
        }
        
        interface B {
          void Foo(int x);
          void Foo() => Foo(42);
        }


        1. MonkAlex
          28.06.2017 11:40

          А что, можно в первом случае = 42 написать в интерфейс?


          1. mayorovp
            28.06.2017 12:11

            Вообще-то да. Более того, если класс используется через интерфейс, как часто бывает при использовании DI, то нигде кроме интерфейса писать = 42 не имеет смысла.


            1. MonkAlex
              28.06.2017 12:40

              Ну, довольно подозрительное поведение, на мой взгляд. Как впрочем и везде с дефолтными аргументами.
              Если я в классе (в реализации интерфейса) установил другое значение, то теперь от типизации переменной будет зависеть какое значение придёт по умолчанию.
              И это я молчу о потенциальной смене дефолтных значений, которое и для классов не работает без полной пересборки.


        1. sidristij
          28.06.2017 12:23

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


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


          interface A {
            void Foo(int x = 42);
          }
          
          interface B {
            void Foo(int x);
            void Foo() { Foo(Unity.Resolve<DatabaseContext>().Set<Entity>().First(x => x.Id >=1000).Offset) };
          }


          1. mayorovp
            28.06.2017 12:27

            Ваш пример говорит только он том, что он сам — кривая лажа.


            Писать фигню можно на любом языке и с использованием любых инструментов, от написания фигни не застрахован ни один язык (кроме, возможно, HQ9+).


            А потому оценивать языковые возможности надо по их лучшим проявлениям, а не по худшим.


            1. sidristij
              28.06.2017 12:49

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


              1. mayorovp
                28.06.2017 12:52
                +1

                The principal motivations for this feature are
                • Default interface methods enable an API author to add methods to an interface in future versions without breaking source or binary compatibility with existing implementations of that interface.
                • The feature enables C# to interoperate with APIs targeting Android (Java) and iOS (Swift), which support similar features.
                • As it turns out, adding default interface implementations provides the elements of the "traits" language feature (https://en.wikipedia.org/wiki/Trait_(computer_programming)). Traits have proven to be a powerful programming technique (http://scg.unibe.ch/archive/papers/Scha03aTraits.pdf).

                https://github.com/dotnet/csharplang/blob/master/proposals/default-interface-methods.md#motivation


              1. Atomosk
                30.06.2017 11:51
                +2

                Например ToList() который сейчас экстеншн метод к IEnumerable<>. Сейчас если я делаю свой класс, имплементящий IEnumerable, то метод ToList() пройдет по моему IEnumerable<> и если элементов много, то создаст в процессе много массивов, копируя их туда-сюда в процессе.
                Если в моей реализации заранее известно количество элементов, то я мог бы в принципе сделать оптимальнее, только это очень убого будет. Нужно будет экстеншн MyToList() делать, который проверит тип IEnumerable<>, является ли он моей реализацией, вызовет оптимальную реализацию если да, или стандартную если нет.


                Ботомлайн — эту фичу уже можно делать костыльно с помощью экстеншн методов. Те кому не нравится фича, должны ненавидеть и экстеншн методы, потому что они те же самые методы на интерфейсах, только хуже.


      1. IL_Agent
        28.06.2017 10:49
        +1

        Следует рассматривать методы с реализацией не как часть протокола, а как заранее определенную возможность работы с протоколом ( как extension method), которую можно переопредлить для конкретных имплементаций.


  1. low_noize
    27.06.2017 09:12
    +7

    «Почему in, ведь ref readonly понятнее? А потому что in короче.» Уверены?) а можем, потому что in — противоположность out?) «Передан по ссылке БЕЗ возможности записи» против «Передан по ссылке ОБЯЗАТЕЛЬНО для записи»


  1. asommer
    27.06.2017 09:12

    One of proposed syntaxes is to use existing in keyword as a shorter form of ref readonly. It could be that both syntaxes are allowed or that we pick just one of them.



  1. alex1t
    27.06.2017 10:01
    +2

           string x = "demo";
           Action y = () => M();
           var t = (x: x, y);
           t.y(); // ранее был бы выбран extension method y(), а сейчас будет вызвана лямбда  
    

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


    1. ForNeVeR
      27.06.2017 10:15

      Мне кажется, что то же самое было бы с анонимным объектом (а-ля new { x, y }.y()), так что это не новая грабля :)


    1. impwx
      27.06.2017 11:21

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


  1. Alex_ME
    27.06.2017 11:36
    -1

    Где-то читал, что в C# 7 хотели сделать not null reference types. Предлагали использовать


    string! str1 = "123";  //not null
    string! str2;          //ошибка, not-null типу присваивается null
    string str3;           //стандартное поведение

    Как я понял, это не сделали и это трансформировалось в Static null checking in C# в C# 8.


    DefaultInterfaceMethods выглядят каким-то жутким хаком и нарушением логики — если это интерфейс, то он по-определению не должен содержать реализаций! Тогда это приведет к множественному наследованию со всеми вытекающими проблемами. Если есть необходимость во множественном наследовании, можно пойти путем, как многие языки — mixins. На мой взгляд лучше.


    1. mayorovp
      27.06.2017 12:03
      +2

      На самом деле, если это интерфейс, то он по определению не должен содержать полей. Default Interface Method принципиально ничем не отличается от пары из метода-расширения и доп. интерфейса, только пишется проще, а работает быстрее:


      interface A {
          void Foo(int x);
          void Foo2() { Foo(2); }
      }
      
      // Почти эквивалентно
      
      interface A {
          void Foo(int x);
      }
      
      interface AExt {
          void Foo2();
      }
      
      static class AExtStatic {
          public static void Foo2(this A obj) {
               var objext = obj as AExt;
               if (objext == null)
                 obj.Foo(2);
               else
                 objext.Foo2();
          }
      }


  1. ARad
    27.06.2017 12:26

    Трейты будут?


  1. SonicGD
    27.06.2017 14:45

    Будет здорово в 8.0 увидеть Extension Everything


  1. 0x1000000
    28.06.2017 01:11
    +2

    Лучше бы, наконец, records, доделали. Анонсированные фичи, как-то не особо впечатляют.


  1. Danov
    28.06.2017 22:09
    -2

    static Vector3 Add (ref readonly Vector3 v1, ref readonly Vector3 v2)
    


    static Vector3 Add (in Vector3 v1, in Vector3 v2)
    


    Почему in, ведь ref readonly понятнее? А потому что in короче.

    Можно еще короче:
    static Vector3 Add (=Vector3 v1, =Vector3 v2)
    

    Немного непривычно, но глаз быстро привыкнет.


  1. xystarcha
    05.07.2017 13:26

    int $GeneratedMain(string[] args)

    Я как и многие ожидал этот функционал еще в 7-ой версии языка.


    А можно пример, зачем это может быть нужно?


    1. mayorovp
      05.07.2017 13:58
      +1

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