Новинки C# 7.0

Основные новшества C# 7 призваны облегчить работу с данными, упростить код и улучшить производительность. В основном C# движется в сторону функционального языка.

Улучшения литералов

В C# 7 появилась возможность добавлять _ в качестве разделителя в числовые литералы:

var d = 123_456;
 var x = 0xAB_CD_EF;

Разделитель можно добавить в любом месте между цифрами, на значение он не влияет.

Также в C# 7 появились бинарные литералы:

var b = 0b1010_1011_1100_1101_1110_1111;

Out переменные

В C#6.0 использовать out переменные было не так легко, как нам хотелось бы. Перед тем как вызвать метод с out аргументами, необходимо было объявить переменные, которые будут переданы в этот метод. Так как обычно значения этим переменным во время объявления не присваиваются (что логично — они все равно будут перезаписаны методом), ключевое слово var использовать не получалось. Нужно было объявить переменные с указанием их типа:

public void PrintCoordinates(Point p)
{
  int x, y; // нужно объявить переменные
  p.GetCoordinates(out x, out y);
  WriteLine($"({x}, {y})");
}

В C# 7 добавлены out переменные, которые позволяют объявить переменные сразу в вызове метода:

public void PrintCoordinates(Point p)
{
  p.GetCoordinates(out int x, out int y);
  WriteLine($"({x}, {y})");
}

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

Так как объявление out переменных происходит в том же выражении, что и их передача в качестве аргументов метода, компилятор может вывести их тип (если для этого метода нет конфликтующих перегрузок), поэтому вместо типа можно использовать ключевое слово var:

p.GetCoordinates(out var x, out var y);

Out аргументы широко используются в семействе методов Try…, где возвращаемое булево значение показывает успех операции и out аргументы содержат полученное значение:

public void PrintStars(string s)
{
  if (int.TryParse(s, out var i)) { WriteLine(new string('*', i)); }
  else { WriteLine("Облачно - звезд нет!"); }
}

Сопоставление с шаблоном

В седьмой версии C# появляется понятие шаблона (pattern), который в общем случае представляет собой синтаксическую конструкцию, позволяющую проверить соответствие переменной определенному шаблону и извлечь из нее информацию, если такое соответствие имеется.

В C# 7 есть следующие шаблоны:

  • Константные шаблоныc (где c — константное выражение C#); проверяют, равна ли переменная этой константе или нет.
  • Шаблоны типаT x (где T — тип и x — переменная); проверяют, имеет ли переменная тип T, и если да, то извлекает ее значение в новую переменную x типа T.
  • Var шаблоныvar x (где x — переменная); этот шаблон всегда вычисляется в true, используется для создания новой переменной того же типа и с тем же значением.

Для поддержки шаблонов были изменены 2 уже существующих языковых конструкции:

  • is может использоваться не только с типом, но и с шаблоном (в качестве правого аргумента).
  • case в операторе switch может использовать шаблоны, а не только константы.

Шаблоны с is

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

public void PrintStars(object o)
{
  if (o is null) return;     // константный шаблон "null"
  if (!(o is int i)) return; // шаблон типа "int i"
  WriteLine(new string('*', i));
}

Как видно из примера, переменные шаблона (которые были объявлены в шаблоне), имеют ту же область видимости, что и out переменные, поэтому могут использоваться внутри внешнего блока видимости.

Шаблоны и Try-методы могут использоваться вместе:

if (o is int i || (o is string s && int.TryParse(s, out i)) { /* можно использовать i типа int */ }

Шаблоны и выражение switch

Варианты использования switch были расширены, теперь можно:

  • Использовать любые типы (не только примитивные).
  • Использовать шаблоны в выражениях case.
  • Добавлять дополнительные условия к выражениям case (используя ключевое слово when).

Теперь рассмотрим пример:

switch(shape)
{
  case Circle c:
    WriteLine($"круг с радиусом {c.Radius}");
    break;
  case Rectangle s when (s.Length == s.Height):
    WriteLine($"{s.Length} x {s.Height} квадрат");
    break;
  case Rectangle r:
    WriteLine($"{r.Length} x {r.Height} прямоугольник");
    break;
  default:
    WriteLine("<неизвестная фигура>");
    break;
  case null:
    throw new ArgumentNullException(nameof(shape));
}

Имеются следующие особенности нового расширенного switch:

  • Порядок выражений case имеет значение. Логика сопоставления такая же, как у выражений catch: будет выбрано первое по порядку выражение, удовлетворяющее условию. Поэтому в данном примере важно, что более специфичное условие для квадрата идет перед более общим условием для прямоугольника, если поменять их местами, то условие для квадрата никогда не сработает. В таких случаях на помощь придет компилятор, который будет помечать явные недостижимые условия (так же, как и для catch). Данное изменение не является изменением уже существующего поведения: до C# 7 порядок выполнения выражений case не был определен.
  • Условие по умолчанию (default) всегда вычисляется последним. Даже несмотря на то, что после него идет условие null, условие по умолчанию будет проверено после него. Это было сделано для поддержки существующей логики, однако хорошим тоном будет сделать условие по умолчанию последним.
  • null условие в конце достижимо. Это происходит потому, что шаблон типа следует текущей логике оператора is и не срабатывает для null. Благодаря такому поведению, null не будет сопоставлен с первым же шаблоном типа; вы должны явно указать шаблон для него или оставить логику для условия по молчанию.

Областью видимости для переменных шаблона, объявленных в case, является выражение switch.

Кортежи

Иногда хочется вернуть несколько значений из метода. Ни один из доступных в C# 6.0 способов не выглядел оптимальным:

  • Out параметры: синтаксис выглядит перегруженным (даже если использовать рассмотренные выше нововведения), неприменимо к асинхронным методам.
  • System.Tuple<…>: опять же выглядит многословно и требует создание дополнительного объекта.
  • Отдельный класс для каждого такого случая: слишком много кода для типа, единственной целью которого служит временная группировка нескольких значений.
  • Объект dynamic: потери в производительности и отсутствие проверки типов на этапе компиляции.

Для упрощения этой задачи в C# 7 были добавлены кортежи и литералы кортежей:

(string, string, string) LookupName(long id) // возвращаемый тип - кортеж
{
   // инициализируем данные
   return (first, middle, last); // литерал кортежа
}

Теперь метод возвращает 3 строки, объединенных в кортеж. Вызывающий код может их использовать следующим образом:

var names = LookupName(id);
WriteLine($"найдены {names.Item1} {names.Item3}.");

Имена полей Item1, Item2, … являются именами по умолчанию для каждого кортежа, однако, есть возможность дать данным, объединенным в кортеж, имена получше:

(string first, string middle, string last) LookupName(long id) // элементы кортежа теперь имеют собственные имена

Теперь к элементам кортежа можно обратиться так:

var names = LookupName(id);
 WriteLine($"найдены {names.first} {names.last}.");

Также имена элементов можно указать сразу в литерале кортежа:

return (first: first, middle: middle, last: last); //указываем имена элементов в литерале кортежа

Кортежи можно присваивать друг другу, если имена их элементов не совпадают: главное, чтобы сами элементы можно было присваивать друг другу.

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

Локальные функции

Иногда вспомогательная функция имеет смысл только внутри одного метода, в котором вызывается. Такую функцию можно объявить внутри метода:

public int Fibonacci(int x)
{
  if (x < 0) throw new ArgumentException("Не надо негатива!", nameof(x));
  return Fib(x).current;

  (int current, int previous) Fib(int i)
  {
    if (i == 0) return (1, 0);
    var (p, pp) = Fib(i - 1);
    return (p + pp, p);
  }
}

Аргументы внешнего метода и его локальные переменные доступны для локальной функции, также, как и для лямбда-выражений.

Локальные переменные и возвращаемые значения по ссылке

Можно не только передать параметры в метод по ссылке (с помощью ключевого слова ref), но и возвратить данные из метода по ссылке, а также сохранить в локальной переменной тоже по ссылке.

public ref int Find(int number, int[] numbers)
{
  for (int i = 0; i < numbers.Length; i++)
  {
    if (numbers[i] == number)
    {
      return ref numbers[i]; // возвращаем ссылку на место хранения, а не значение элемента массива
    }
  }
  throw new IndexOutOfRangeException($"{nameof(number)} не найден");
}

int[] array = { 1, 15, -39, 0, 7, 14, -12 };
ref int place = ref Find(7, array); // ссылка на место, где находится 7 в массиве
place = 9; // заменяем 7 на 9
WriteLine(array[4]); // выведет 9

Удобно передавать ссылки на определенные места в больших структурах данных.

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

  • Можно возвращать только ссылки, которые возвращать безопасно: ссылки на объекты, переданные в метод и ссылки на поля объектов.
  • Переменные инициализируются определенной ссылкой и в будущем не меняются.

Расширение списка типов, возвращаемых асинхронными методами

До сегодняшнего дня, async методы могли возвращать только voidTask or Task<T>. В C# 7 появилась возможность создания типов, которые также могут быть возвращены асинхронным методом. Например, можно создать структуру ValueTask<T>, которая поможет избежать создания объекта Task<T> в случае, когда результат асинхронной операции уже доступен. Для многих асинхронных сценариев, например, где используется буферизация, такой подход может значительно уменьшить число выделений памяти и таким образом повысить производительность.

Throw выражения

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

class Person
{
  public string Name { get; }
  public Person(string name) => Name = name ?? throw new ArgumentNullException(name);
  public string GetFirstName()
  {
    var parts = Name.Split(" ");
    return (parts.Length > 0) ? parts[0] : throw new InvalidOperationException("No name!");
  }
  public string GetLastName() => throw new NotImplementedException();
}