Декабрь 2015

ТОМ 30, НОМЕР 13

Главное в .NET - Проектирование C# 7

By Марк Михейлис | Декабрь 2015

Марк МихейлисКогда вы будете читать эту статью, исполнится почти год с того момента, как группа разработки C# 7 начала обсуждения, планирование, эксперименты и программирование. В этой статье я дам примеры некоторых идей, которые сейчас изучаются.

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

Объявление ссылочных типов, допускающих и не допускающих null-значения

По-видимому, некоторые из доминирующих идей в обсуждении C# 7 относятся к дальнейшему совершенствованию работы с null — в том же направлении, что и оператор проверки условия на null (null-conditional operator) в C# 6.0. Одно из простейших улучшений может заключаться в верификации компилятором или анализатором того, что обращение к экземпляру типа, допускающего null-значение (nullable type instance), будет предваряться проверкой фактически на отсутствие null-значения.

В случаях, когда null является нежелательным ссылочным типом, что будет, если вы сможете напрочь избегать null? Идея заключается в объявлении намерения того, что для некоего ссылочного типа допустимы null-значения (string?) или, напротив, недопустимы (string!). В теории можно даже предположить, что объявления всех ссылочных типов в новом коде по умолчанию не будут допускать null-значений (non-nullable). Однако, как было отмечено соавтором моей книги «Essential C# 6.0», Эриком Липпертом (Eric Lippert), гарантировать, что какой-то ссылочный тип никогда не будет содержать null при компиляции неприемлемо сложно (bit.ly/1Rd5ekS). Даже так будет возможно выявить сценарии, где тип мог бы потенциально содержать null и, тем не менее, разыменован без проверки того, что он не равен null. Или сценарии, где типу мог бы быть присвоен null, хотя было объявлено обратное намерение.

Для еще большего выигрыша группа рассматривает возможность объявления не допускающего null-значения типа в параметре, чтобы автоматически генерировать проверку на null (хотя, по-видимому, это должно быть решение в виде явного соглашения, чтобы избежать любой нежелательной деградации производительности, если только это не будет делаться на этапе компиляции).

(Как ни странно, типы, значения которых могут быть null, были добавлены в C# 2.0 из-за распространенности ситуаций, где они нужны, например при извлечении данных из базы данных — в этом случае требуется разрешить целочисленному типу содержать null. А теперь в C# 7 группа подумывает сделать прямо противоположное для ссылочных типов.)

Одно интересное соображение насчет поддержки ссылочных типов для того, что не может быть null (например, string! text), — соответствующая реализация будет в Common Intermediate Language (CIL). Два самых популярных предложения: преобразование в синтаксис типа NonNullable<T> или использовать атрибуты, как в [Nullable] string text. Последний вариант в настоящее время считается предпочтительным.

Кортежи

Кортежи (tuples) — еще одна функциональность, находящаяся на рассмотрении для C# 7. Эта функциональность неоднократно рассматривалась для более ранних версий языка, но так и не была доведена до окончательной реализации. Идея в том, чтобы у программистов была возможность объявлять типы в комплексе, где объявление может содержать более одного значения и где аналогичным образом методы могли возвращать более одного значения. Возьмем следующий пример кода, чтобы понять концепцию:

public class Person
{
  // Кортеж
  public readonly (string firstName, string lastName) Names;
  public Person((string FirstName, string LastName)) names, int Age)
  {
    Names = names;
  }
}

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

public (string FirstName, string LastName) GetNames(string! fullName)
{
  string[] names = fullName.Split(" ", 2);
  return (names[0], names[1]);
}
public void Main()
{
  // ...
  (string first, string last) = GetNames("Inigo Montoya");
  // ...
}

В этом листинге есть метод, который возвращает кортеж, и объявление переменных first и last, которым присваивается результат от GetNames. Заметьте, что присваивание основано на порядке в кортеже (а не на именах принимающих переменных). Учитывая некоторые альтернативные подходы, которые мы должны использовать сегодня (массив или набор, пользовательский тип, выходные параметры), кортежи являются привлекательным вариантом.

Существует множество вариантов, которые могли бы быть реализованы вместе с кортежами. Вот некоторые из тех, которые находятся на рассмотрении.

  • Кортежи могли бы иметь именованные или неименованные свойства:
var name = ("Inigo", "Montoya")

и:

var name = (first: "John", last: "Doe")
  • Результат мог бы быть анонимным типом или явными переменными:
var name = (first: "John", last: "Doe")

или:

(string first, string last) = GetNames("Inigo Montoya")
  • Потенциально вы могли бы преобразовать массив в кортеж:
var names = new[]{ "Inigo", "Montoya" }
  • К индивидуальным элементам кортежа можно обращаться по именам:
Console.WriteLine($”My name is { names.first } { names.last }.”);
  • Типы данных можно было бы логически распознавать там, где они не указаны в явном виде (следуя тому же подходу, что и для анонимных типов в целом).

Хотя в кортежах есть свои сложности, по большей части они соответствуют уже устоявшимся в языке структурам, поэтому они получают сильную поддержку для включения в C# 7.

Поиск совпадения по шаблону

Поиск (проверка) совпадения по шаблону (pattern matching) также является часто поднимаемой темой в дискуссиях, который ведутся в группе разработки C# 7. По-видимому, одним из более понятных вариантов этой концепции были бы расширенные выражения switch (и if), которые поддерживали бы шаблоны выражений в блоках case, а не просто константы. (При этом тип выражения switch не должен был бы ограничиваться типами, имеющими соответствующие константные значения.) С помощью поиска совпадения по шаблону вы могли бы запрашивать выражение switch для шаблона, например имеет ли выражение switch конкретный тип, тип с конкретным членом или даже тип, который соответствует конкретному «шаблону» или выражению. Рассмотрим, к примеру, как obj мог бы быть типом Point, значение x которого больше 2:

object obj;
// ...
switch(obj) {
  case 42:
    // ...
  case Color.Red:
    // ...
  case string s:
    // ...
  case Point(int x, 42) where (Y > 42):
    // ...
  case Point(490, 42):
    // ...
  default:
    // ...
}

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

Для поддержки case с типом Point потребовался бы какой-то тип члена в Point, который обрабатывал бы поиск совпадения по шаблону. В данном случае нужен член, принимающий два аргумента типа int. Например, такой член:

public static bool operator is (Point self out int x, out int y) {...}

Заметьте, что без выражения where блока case Point(490, 42) никогда нельзя было бы достичь, что заставило бы компилятор выдать ошибку или предупреждение.

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

string text = match (e) { pattern => expression; ... ; default => expression }

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

Записи

В продолжение изучавшегося для включения в C# 6.0 сокращенного синтаксиса объявления конструктора (но в конечном счете отвергнутого) имеется поддержка для встраивания объявления конструктора в определение класса — это концепция, известная как записи (records). Например, рассмотрим следующее объявление:

class Person(string Name, int Age);

Это простое выражение будет автоматически генерировать следующее.

  • Конструктор:
public Person(string Name, int Age)
{
  this.Name = Name;
  this.Age = Age;
}
  • Свойства только для чтения, создающие тем самым неизменяемый тип.
  • Реализации проверок равенства (в частности, GetHashCode, Equals, оператор ==, оператор != и др.).
  • Реализация ToString по умолчанию.
  • Поддержка поиска совпадения по шаблону в операторе is.

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

Один из более проблематичных вопросов, связанных с записями, — как обрабатывать сериализацию. Очевидно, использование записей как объектов передачи данных (data transfer objects, DTO) весьма типично, и, тем не менее, не ясно, что можно сделать (если вообще возможно) с поддержкой сериализации таких записей.

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

Person inigo = new Person("Inigo Montoya", 42);
Person humperdink = inigo with { Name = "Prince Humperdink" };

Сгенерированный код, соответствующий выражению with, выглядел бы примерно так:

Person humperdink = new Person(Name: "Prince Humperdink", Age: inigo.42 );

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

Person humperdink = inigo.With(Name: "Prince Humperdink", Age: inigo.42);

Асинхронные потоки данных

Концепция обработки асинхронных последовательностей для улучшения поддержки асинхронности в C# 7 выглядит интригующе. Например, при наличии IAsyncEnumerable со свойством Current и методом Task<bool> MoveNextAsync вы могли бы проходить по экземпляру IAsyncEnumerable в цикле foreach и переложить на компилятор заботу об асинхронном вызове каждого члена в потоке данных (stream) и выполнять await, чтобы выяснить, есть ли в последовательности (возможно, канале) другой элемент для обработки. В таком варианте немало проблем, которые предстоит оценить, самая мелкая из которых — потенциальное разбухание LINQ-кода, которое может произойти со всеми стандартными операторами LINQ-запросов, возвращающих IAsyncEnumerable. Кроме того, нет определенности в том, как предоставлять поддержку CancellationToken и даже Task.ConfigureAwait.

C# в командной строке

Как любитель того, что Windows PowerShell делает Microsoft .NET Framework доступной в интерфейсе командной строки (command-line interface, CLI), меня особенно интересует одна из областей (пожалуй, моя любимая функциональность, находящаяся на рассмотрении): поддержка использования C# в командной строке; эта концепция более обобщенно называется поддержкой Read, Evaluate, Print, Loop (REPL). Как можно надеяться, поддержка REPL будет сопровождаться поддержкой скриптов на C#, которая не требует всех этих обычных формальностей (вроде объявления класса) в тривиальных сценариях, где такие церемонии не нужны. Без этапа компиляции REPL потребовала бы новых директив для ссылок на сборки и NuGet-пакеты наряду с важными дополнительными файлами. Текущее предложение на рассмотрении поддерживало бы следующее:

  • #r для ссылки на дополнительную сборку или NuGet-пакет. Вариация — #r!, которая позволила бы обращаться даже к внутренним членам, хоть и с некоторыми ограничениями. (Это предназначено для сценариев, где вы обращаетесь к сборкам, для которых у вас есть исходный код.)
  • #l для включения целых каталогов (по аналогии с F#);
  • #load для импорта дополнительного файла скрипта на C# точно так же, как вы добавляли бы его в свой проект; единственное исключение — теперь важен порядок. (Заметьте, что импорт файла .cs может не поддерживаться, поскольку пространства имен в скрипте на C# отсутствуют.)
  • #time для включения средств диагностики производительности в процессе выполнения.

Выпуска первой версии C# REPL следует ожидать вместе с выходом Visual Studio 2015 Update 1 (наряду с обновленным Interactive Window, которое поддерживает тот же набор функциональности). Подробнее на эту тему см. по ссылке Itl.tc/CSREPL, а также в следующем выпуске моей рубрики.

Заключение

В наработанных за год материалах и даже в тех немногих идеях, которые я слегка затронул, содержится гораздо больше деталей, которые предстоит тщательно исследовать и взвесить связанные с ними преимущества и проблемы. Однако следует надеяться, что теперь вы получили некоторое представление о том, что исследует группа разработки и как они относятся к дальнейшей шлифовке языка C#. Если вы хотели бы напрямую изучить заметки по проектированию C# 7 и, возможно, оставить свой отзыв, то можете присоединиться к дискуссии по ссылке bit.ly/CSharp7DesignNotes.


Марк Михейлис (Mark Michaelis) — учредитель IntelliTect, где является главным техническим архитектором и тренером. Почти два десятилетия был Microsoft MVP и региональным директором Microsoft с 2007 года. Работал в нескольких группах рецензирования проектов программного обеспечения Microsoft, в том числе C#, Microsoft Azure, SharePoint и Visual Studio ALM. Выступает на конференциях разработчиков, автор множества книг, последняя из которых — «Essential C# 6.0 (5th Edition)» (itl.tc/­EssentialCSharp). С ним можно связаться в Facebook (facebook.com/Mark.Michaelis), через его блог (IntelliTect.com/Mark), в Twitter (@markmichaelis) или по электронной почте mark@IntelliTect.com.

Выражаю благодарность за рецензирование статьи эксперту Microsoft Мэдсу Торгерсону (Mads Torgerson).