В мае вышла новая версия Visual Studio, где была добавлена поддержка новой версии языка — 7.3. Язык продолжает планомерно развиваться и несмотря на то, что релиз C# 7.3 является минорным в нем исправлено несколько застарелых проблем, живущих в языке начиная с первой и второй версии. Давайте посмотрим более подробно, что именно было добавлено.
Сравнение кортежей
Развивается поддержка кортежей в языке и если в предыдущих версиях стало возможным возвращать из функции несколько значений, то теперь кортежи можно сравнивать между собой. Раньше приходилось писать что-то вроде:
(int, int) t1 = (1, 2);
(int, int) t2 = (1, 2);
if (t1.Item1 == t2.Item1 && t1.Item2 == t2.Item2)
{
Console.WriteLine("t1 и t2 равны");
}
Теперь условие в блоке if может выглядеть более лаконично:
if (t1 == t2)
Отметим, что имена не учитываются — важен порядок элементов в кортеже. То есть следующие два кортежа не равны:
var t1 = (celsius: 5, fahrenheit: 41);
var t2 = (fahrenheit: 41, celsius: 5);
А вот эти равны:
var t1 = (celsius: 5, fahrenheit: 41);
var t2 = (celsius: 5, fahrenheit: 41);
Сняты ограничения на переменные выражения в инициализаторах и LINQ запросах
Переменные выражения теперь можно использовать непосредственно при инициализации поля в классе, в конструкторе и LINQ запросе. То есть следующий код скомпилируется и выведет на консоль 65535:
class Program
{
private static int magicNumber_ = int.TryParse("65535", out var i) ? i : 0;
public static void Main()
{
Console.WriteLine(magicNumber_);
}
}
Хотя лично мне сама идея использования out и ref значений в качестве аргументов функции не нравится. В моем идеализированном представлении входные параметры должны получать значения, а не возвращать результат работы. Но такая возможность в языке есть и ей можно пользоваться.
Новые виды ограничений для обобщенных типов и функций
Теперь у вас имеется больше контроля над обобщенными типами. Разработчикам часто не хватало только двух ограничений class и struct. Поэтому в новой версии языка добавлено еще три: enum, delegate и ограничение для “blittable” типов — unmanaged. С первыми двумя все и так ясно, достаточно лишь только посмотреть на пример обобщенного метода:
public static string Method<T>(T arg)
where T : Enum
{
return arg.ToString();
}
Третий тип ограничений — unmanaged подразумевает, что тип не является ссылочным и не содержит ссылочных полей на любом уровне вложенности. На практике все сводится к использованию данного типа при взаимодействии с низкоуровневым неуправляемым кодом.
Возможность указать атрибуты для полей из автоматически реализуемых свойств.
Здесь все понятно из заголовка. Многим доводилось писать код вроде:
[Serializable]
public class Foo
{
[NonSerialized]
private string mysSecret_;
public string MySecret
{
get
{
return mysSecret_;
}
set
{
mysSecret_ = value;
}
}
public string SecondProperty
{
get;
set;
}
}
Автоматически реализуемое свойство для MySecret использовать не получится, если вы хотите его исключить из сериализации. Однако в новой версии языка можно написать так:
[Serializable]
public class Foo
{
[field: NonSerialized]
public string MySecret
{
get;
set;
}
public string SecondProperty
{
get;
set;
}
}
Таким образом вы можете применить любой атрибут для поля из автоматически сгенерированного свойства. Это похоже на аналогичный функционал для событий доступный еще с первой версии языка.
Переприсвоение для локальных переменных ссылок
Переменные ссылки, которые были введены в язык ранее теперь ведут себя более естественно. Сейчас такой код скомпилируется:
int i = 22;
int j = 33;
ref int i2 = ref i;
i2 = ref j;
j = 44;
Console.WriteLine(i2); // Выведет на экран 44
Раньше на строке с присвоением переменной (i2 = ref j) была бы ошибка.
Инициализация массива при размещении в стеке
Когда приходится работать с большим количеством маленьких массивов их можно разместить в стеке. С одной стороны такое решение работает быстрее и не нагружает лишний раз GC, но с другой, размеры стека ограничены так что нужно аккуратно работать с кодом. В C# размещать массив стеке можно с помощью ключевого слова stackalloc, но инициализировать его непосредственно при объявлении было нельзя. В 7.3 устранили это ограничение, так что теперь можно записать:
Span<int> arr1 = stackalloc int[3]; // Такая конструкция была доступна и раньше
Span<int> arr2 = stackalloc int[3] { 1, 2, 3 };
Span<int> arr3 = stackalloc int[] { 1, 2, 3 };
Span<int> arr4 = stackalloc[] { 1, 2, 3 };
Span был добавлен совсем недавно в версии языка 7.2 и он позволяет ссылаться на хранимые в стеке значения. Естественно, что все вышесказанное можно переписать и без Span с помощью unsafe кода. В таком случае Span<int> нужно будет заменить на int*.
Улучшения в работе правил разрешения перегрузки
Рассмотрим следующий код получения максимального времени создания для списка файлов.
var files = new string[]
{
@"C:\Temp\first.txt",
@"C:\Temp\second.txt"
};
var maxDate = files.Max(System.IO.File.GetCreationTime);
Формально все написано по правилам языка, но этот код не компилируется. Причем если переписать последнюю строчку в виде:
var maxDate = files.Max(x => System.IO.File.GetCreationTime(x));
Все будет компилироваться и работать. В новой версии языка в этом нет необходимости и изначальный пример будет работать как ожидалось. Причина в том, что были внесены изменения в работу правил разрешения перегрузки. По факту исправлен дефект компилятора, он стал умнее, а код теперь писать с большим комфортом.
А также...
Есть еще несколько нововведений, однако мне они показались не настолько интересными для широкой публики. Подробнее про них можно почитать в первоисточнике.