К старту курса по разработке на C# рассказываем о новых конструкциях в предварительной версии языка C# 11. Среди них шаблоны списка, проверка Parameter на null и возможность переноса строки при интерполяции строк. За подробностями приглашаем под кат.


Visual Studio 17.1 (Visual Studio 2022 Update 1) и .NET SDK 6.0.200 содержат предварительный функционал C# 11. Чтобы поработать с ним, обновите Visual Studio или загрузите последний SDK .NET. Также посмотрите пост о выходе Visual Studio 2022 17.1, чтобы узнать о нововведениях в Visual Studio, а также пост об анонсе .NET 7 Preview 7.

Проектирование C# 11

Мы любим проектировать и разрабатывать открыто. Предложения по функциональности C# и заметки о проектировании языка вы найдёте в репозитории CSharpLang. Главная страница объясняет процесс проектирования, а ещё можно послушать Мэдса Торгерсена на .NET Community Runtime and Languages Standup, где мы говорим о процессе проектирования.

Как только работа над функцией спланирована, она вместе с отслеживанием прогресса переносится в репозиторий Roslyn. Статус ближайших функций вы найдёте на странице Feature Status. Там вы увидите, над чем мы работаем и что добавляем во все предварительные версии, а ещё можно посмотреть, что вы могли пропустить.

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

Файл .csproj будет выглядеть так:

<Project Sdk="Microsoft.NET.Sdk">
    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net6.0</TargetFramework>
        <ImplicitUsings>enable</ImplicitUsings>
        <Nullable>enable</Nullable>
        <LangVersion>preview</LangVersion>
    </PropertyGroup>
</Project>

Символ новой строки в элементах формата интерполированных строк

Прочитать об этом изменении больше можно в предложении Remove restriction that interpolations within a non-verbatim interpolated string cannot contain new-lines. #4935

C# поддерживает два стиля интерполированных строк: буквальный и небуквальный, $@"" и $"" соответственно. Ключевое различие между ними заключается в том, что небуквальные интерполированные строки не могут содержать символ новой строки в сегментах текста, вместо этого должен использоваться символ экранирования (например, \r\n).

Буквальные интерполированные строки в своих сегментах могут содержать символы новых строк без экранирования. Символы "" экранируют кавычки. Всё это поведение остаётся неизменным.

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

Элементы формата сами по себе — не текст, они не должны следовать правилам экранирования и переноса строк для интерполированных сегментов строчного текста. Например, код ниже может привести к ошибкам в C# 10, но он корректен в предварительной версии C# 11:

var v = $"Count ist: { this.Is.Really.Something()
                            .That.I.Should(
                                be + able)[
                                    to.Wrap()] }.";

Шаблоны списка

Больше об этом изменении читайте в предложении о списке шаблонов.

Новый шаблон списка позволяет сопоставлять списки и массивы. Можно сопоставлять элементы и опционально включать шаблон слайса, который сопоставляет 0 и более элементов. При помощи этого шаблона можно отбросить или захватить 0 или более элементов.

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

В коде ниже шаблон [1, 2, .., 10] сопоставляет всё:

int[] arr1 = { 1, 2, 10 };
int[] arr1 = { 1, 2, 5, 10 };
int[] arr1 = { 1, 2, 5, 6, 7, 8, 9, 10 };

Чтобы разобраться с шаблоном списка, посмотрите этот код:

public static int CheckSwitch(int[] values)
    => values switch
    {
        [1, 2, .., 10] => 1,
        [1, 2] => 2,
        [1, _] => 3,
        [1, ..] => 4,
        [..] => 50
    };

Вот результат передачи нескольких массивов этому коду:

WriteLine(CheckSwitch(new[] { 1, 2, 10 }));          // prints 1
WriteLine(CheckSwitch(new[] { 1, 2, 7, 3, 3, 10 })); // prints 1
WriteLine(CheckSwitch(new[] { 1, 2 }));              // prints 2
WriteLine(CheckSwitch(new[] { 1, 3 }));              // prints 3
WriteLine(CheckSwitch(new[] { 1, 3, 5 }));           // prints 4
WriteLine(CheckSwitch(new[] { 2, 5, 6, 7 }));        // prints 50

Также можно захватить результаты шаблона слайса:

public static string CaptureSlice(int[] values)
    => values switch
    {
        [1, .. var middle, _] => $"Middle {String.Join(", ", middle)}",
        [.. var all] => $"All {String.Join(", ", all)}"
    };
  • Шаблоны списка работают с любым счётным и индексируемым типом. Это означает, что шаблонам доступны свойства Length или Count, а у индексатора — параметр int или System.Index. 

  • Шаблоны слайса работают с любым нарезаемым типом. Это означает, что шаблону доступен индексатор, аргументом принимающий Range или метод Slice с двумя параметрами int.

Мы рассматриваем добавление поддержки шаблона списков типов IEnumerable. Если у вас есть возможность поэкспериментировать с этой фичей, дайте нам знать, что вы думаете о ней.

Проверка Parameter на null

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

Довольно часто для проверки аргумента на null используются такие вариации бойлерплейта:

public static void M(string s)
{
    if (s is null)
    {
        throw new ArgumentNullException(nameof(s));
    }
    // Body of the method
}

С помощью проверки Parameter на null можно сократить код, добавив к имени параметра !!:

public static void M(string s!!)
{
    // Body of the method
}

Здесь генерируется код проверки на null. Выполняться он будет перед выполнением любого кода внутри метода. В конструкторах проверка на null происходит перед инициализацией поля, вызовом базовых конструкторов и вызовом конструкторов через this.

Эта функциональность независима от обнуляемых ссылочных типов, но они работают вместе. Уже на этапе проектирования она помогает узнать, возможно ли обнуление. Проверка Parameter на null упрощает проверку во время выполнения того факта, передавались ли null в ваш код. Это определённо важно, когда ваш код взаимодействует с внешним кодом, проверка обнуляемых типов в котором может быть отключена.

Сама проверка — эквивалент кода if (param is null) throw new ArgumentNullException(...). Когда !! содержат несколько параметров, операторы !! выполняются в порядке объявления этих параметров.

Вот несколько рекомендаций, ограничивающих применение !!:

  • Проверки применимы только к имеющим реализацию параметрам. Их не может быть, например, у параметра абстрактного метода.

 Другие случаи, где проверку использовать нельзя, включают:

  • параметры метода с extern;

  • параметры делегата;

  • параметры интерфейсного метода, когда нет интерфейсного метода по умолчанию;

Проверку можно применить только к параметрам, которые возможно проверить.

Пример ситуаций, которые исключаются на основании второго правила, — переменные _ и параметры out. Проверка на null возможна для параметров ref и in. Она разрешена для параметров индексатора и добавляет в акцессоры get и set:

public string this[string key!!] { get { ... } set { ... } }

Применима проверка и к параметрам лямбда-выражений, со скобками или без — не важно:

// An identity lambda which throws on a null input
Func<string, string> s = x!! => x;

Асинхронные методы также могут иметь проверяемые на null параметры. Параметр проверяется при вызове метода.

Новый синтаксис проверки на null допустим в методах итератора. Параметр проверяется при вызове метода итератора, но не при проходе нижележащего перечислителя. Это верно для традиционных асинхронных итераторов:

class Iterators {
    IEnumerable<char> GetCharacters(string s!!) {
        foreach (var c in s) {
            yield return c;
        }
    }

    void Use() {
        // The invocation of GetCharacters will throw
        IEnumerable<char> e = GetCharacters(null);
    }
}

Итераторы с обнуляемыми ссылочными типами

Любой имеющий в конце имени !! параметр вначале будет иметь отличное от null значение. Это верно, даже если тип параметра сам по себе потенциально null. Такое может случиться с явно обнуляемым типом, скажем, string, или с параметром типа без ограничений. Если синтаксис !! в параметре комбинируется с явно обнуляемым типом этого параметра, компилятор выдаст предупреждение:

void WarnCase<T>(
    string? name!!,     // CS8995   Nullable type 'string?' is null-checked and will throw if null. 
    T value1!!        // Okay
)

Конструкторы

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

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

Заметки о дизайне

Может быть, вы слышали Джареда Парсонса на .NET Languages and Runtime Community Standup 9 февраля 2022 года. Ролик начинается примерно на 45-й минуте стрима, когда Джаред присоединяется к нам, чтобы рассказать подробности о решениях по функциональности предварительной версии C# 11 и ответить на некоторые распространённые отзывы.

Кто-то узнал об этой особенности, увидев PR, использующие эту функцию в .NET Runtime. Другие команды Microsoft оставляют важные отзывы, используя собственные разработки. Удивительно, что при помощи нового синтаксиса проверки на null мы смогли удалить из .NET Runtime около 20 000 строк.

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

Мы рассмотрели и отклонили глобальную установку проверки на null для всех обнуляемых параметров. Проверка параметров на null заставляет выбирать, как будет обрабатываться null.

Есть много методов, где аргумент null — допустимое значение, а значит, выполнять такую проверку везде, где тип не null, чрезмерно и это скажется на производительности. Ограничить только уязвимые null методы (например, public interface) крайне сложно.

По работе с .NET Runtime мы знаем, что во многих местах проверка не происходит, а значит, потребуется механизм отказа от использования каждого параметра. Пока мы не думаем, что глобальная проверка на null во время выполнения может быть уместной, и если когда-нибудь рассмотрим такую проверку, то это будет другая функция языка.

Резюме

Visual Studio 17.1 и .NET SDK 6.0.200 предлагают взглянуть на ранний C# 11. Поэкспериментировать можно с проверкой параметров на null, шаблонами списка и символами новых строк в фигурных скобках интерполированных строк. Мы будем рады услышать ваше мнение здесь [в комментариях к оригинальной статье] или в дискуссии репозитория CSharpLang на Github.

Погрузиться в IT впервые или прокачать ваши навыки вы сможете на наших курсах:

Выбрать другую востребованную профессию:

Краткий каталог курсов и профессий

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


  1. Ordos
    01.03.2022 11:43
    +9

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

    А потом ты такой сидишь и смотришь на метод с expression body, который вызывает другой метод, которому в параметры передаётся результат ещё какого-то метода, да теперь ещё и у которого в параметры передаётся строка с интерполяцией, где теперь ещё и многострочное выражение. И во всём этом хаосе где-то случается exception, а ты даже не понимаешь, как его продебажить...


    1. XaBoK
      01.03.2022 21:11
      +3

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


    1. vabka
      02.03.2022 14:21

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


    1. a-tk
      02.03.2022 16:37

      Хотите много кода - пишите на Java.


  1. shai_hulud
    01.03.2022 13:15
    +2

    myMethod(string param1!!, string megaparam!!, string megaparam3!!, string megaparam4!!)

    Выглядит и читается крайне плохо. Лучшего синтаксиса они похоже не нашли.

    myMethod(string! param1, string! megaparam, string! megaparam3, string! megaparam4)  

    Хотя когда то давно было предложение расширить синтаксис для code contracts вот так:

    myMethod(string param1, string megaparam, string megaparam3, int megaparam4)
    	require param1 not null, megaparam not null, megaparam4 > 0
    {
    }


    1. UnclShura
      01.03.2022 13:31
      +6

      Да в чем проблема-то была с [NonNull] string myParam? Все эти символы выглядят все хуже и хуже (привет оператору "летающая тарелка").

      Кроме того есть ??. Почему его нельзя было расширить и сюда тоже? Почему нельзя вставить свое исключение после !!. Почему наконец именно два !! а не один и не три?

      Выглядит плохо. Полезность под вопросом.


      1. tohodov
        03.03.2022 09:18

        А в чем польза рантайм проверки на null при наличии возможности проверки перед компиляцией?



    1. NN1
      01.03.2022 18:55

      https://endjin.com/blog/2022/02/csharp-11-preview-parameter-null-checking

      Тут разъяснение почему это не может быть частью типа.

      По хорошему это должно быть расширяемо.

      А вообще неясно какую реальную проблему это решает.

      Из примера разве что нельзя писать сегодня кратко f(a!! => a.b());

      И проверки на null будут в начале метода как полагается до всей логики, а не как пишут некоторые:

      this._a = a ?? throw new Exception();

      this._b = b ?? throw new Exception();


  1. jesaiah4
    01.03.2022 14:46
    +3

    Так чем отличается

    string param1!!

    [nonnull ]string param1!!

    [nonnull ]string param1

    [nonnull ]string? param1!!

    [nonnull ]string? param1

    string? param1!!


    1. Deosis
      02.03.2022 07:01
      +1

      Атрибут и знак вопроса (или его отсутствие) - это советы для IDE, которая будет выдавать предупреждение, если в коде написать вызов с нулевым переметром. Но при этом на рантайм они никак не влияют.


  1. ETCDema
    01.03.2022 22:45
    +5

    Забавно то, что в коде, с которым мне приходилось иметь дело, для string самая частая проверка это IsNullOrEmpty, а не просто на null и тут получается, что для string параметров все равно if писать придется.

    А еще есть массивы, которые проверяют не только на null, но и на Length>0, а так же элементы массива на null + если массив строк, то и IsNullOrEmpty :)


  1. tyofi
    02.03.2022 11:08

    "Проверка Parameter на null" — эта же вещь нужна только если <Nullable>disable</Nullable>, так ведь?


    1. mayorovp
      02.03.2022 11:26
      +1

      Эта штука нужна если вы пишете библиотеку с публичным API. Вы ведь не можете полагаться на то, что у пользователя вашей библиотеки будут выставлены нужные вам настройки компиляции!


      1. tyofi
        02.03.2022 19:45

        Если библиотека написана на C# 11 с включёнными nullability, то публичное API будет сформировано с учётом этого, нет?


        1. mayorovp
          02.03.2022 21:13

          Если библиотека написана на C# 11 с включёнными nullability, то у вас нет никаких гарантий что использующая библиотеку программа будет написана на C# 11 со включёнными nullability.


          1. tohodov
            03.03.2022 19:57
            -1

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


            1. mayorovp
              03.03.2022 21:25
              +1

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


              Самоисполняющееся пророчество, очень удобно.

              Где именно вы видите пророчество, и в чём заключается его самоисполнение?


              1. tohodov
                03.03.2022 21:46
                -1

                Вы утверждаете что автору библиотеки с nullability стоит переживать о том что её могут использовать без nullability => выглядит как минус новых версий C# для тех кто еще не перешел => снижает заинтересованность в переходе на них => снижает количество проектов на них => приводит к высокой вероятности того самого использования библиотеки из не nullability контекста