Что нового в C# 7
Ну что, всех можно поздравить с релизом VS2017? Для начала, я пожалуй признаюсь что я давно уже (время наверное на месяцы пошло) использую Rider (и даже запилил по нему видеокурс), но релиз VS2017 — это не только релиз слегка медленной, 32-битной Windows-only IDE которая даже не умеет правильно рисовать лигатуры. Это еще и релиз новых версий компиляторов — именно того, что Microsoft нам обещали делать out-of-band, но обещание оказалось наглой ложью, как для C++ так и для C#.
Что же, давайте посмотрим что там в новом сишарпике…
Локальные методы
Вообще, проблемы с локальными методами толком не было, т.к. в любой функции можно декларировать лямбда-переменную, что и есть по сути метод. Так что в C#7 это разве что привели в более удобоваримый вид:
public static Tuple<double,double> Solve(double a, double b, double c) { double CalculateDiscriminant() { return b * b - 4 * a * c; } var disc = CalculateDiscriminant(); return Tuple.Create( (-b + Math.Sqrt(disc)) / (2 * a), (-b - Math.Sqrt(disc)) / (2 * a) ); }
Что мы можем заметить в примере выше? Правильно, вложенная функция умеет захватывать окружение, в т.ч. аргументы внешней функции. И конечно же дать функции CalculateDiscriminant
аргументы a,b,c
не получилось бы, т.к. эти имена уже заняты.
Ложка дегтя — расположение метода в другом методе может быть где угодно, то есть можно сначала использовать а потом задекларировать. Это на любителя, конечно, но тут главное — прямые руки и здравый смысл.
Касательно полезности фичи: мне норм. Этот подход позволяет плодить меньше членов на уровне класса, а баловаться с лямбдами напряжно т.к. для любой лямбды нужно определить тип Func<int,Foo,string>
и это порой бывает утомительно — хотя в некоторых случаях Решарпер помогает. Вообще было бы лучше если бы нам позволяли еще делать локальные статические переменные прямо в методах, как это позволяет делать C++.
Out-переменные
Обычно как было: декларация переменной это statement. Все, теперь это по сути expression т.к.:
string s; if (DateTime.TryParse(s, out DateTime dt)) { // используем dt }
Это чисто «сахарная» фича, и резонным будет вопрос — а что если DateTime
не пропарсится а мы все равно попытаемся ей воспользоваться? Ну, тут все просто — у этой штуки будет дефолтное значение. Но зато заметьте, dt
имеет тот же scope, что и строка s
.
Кортежи
Кортежи уже были в C#, но вставлены они были криво: все эти Item1
, Item2
не добавляли к удобству пользования, и кому-то могло показаться что рано или поздно нам вообще подсунут variadic templates. Но разрабы поймали себя и все-таки сделали что-то годное:
public static (double x1, double x2) Solve(double a, double b, double c) { var disc = CalculateDiscriminant(); double CalculateDiscriminant() { return b * b - 4 * a * c; } return ( (-b + Math.Sqrt(disc)) / (2 * a), (-b - Math.Sqrt(disc)) / (2 * a) ); }
И вызвать можно это вот так:
var (x1, x2) = QuadraticEquationSolver.Solve(1, 10, 16); Console.WriteLine(x1 + ", " + x2);
Ну а если декларировать кортежи на месте, то правила те же самые, и есть варианты — давать элементам кортежа имена или нет:
var names = (first: "Dmitri", last: "Nesteruk"); Console.WriteLine(names.first);
Как видите, кортеж определяется как тип через скобочки и список типов (т.е. (double,double)
условно-эквивалентно ValueTuple<double,double>
). Но ложка дегтя тут огромная: для всего этого нужен внешний пакет System.ValueTuple
. Что?!?!?
Спокойно, очевидно что просто эту тему недопилили и ValueTuple<T, ...>
, который должен был быть в .NET 4.6.2 BCL просто туда не попал. Что это за класс? Да просто еще одна реализация кортежа, но в этот раз вся ее суть закрыта синтаксическим сахаром вроде того что я привел выше. Если кому интересно что под капотом, вот сорцы.
Expression-bodied members
На момент C#6, некоторые вещи можно было писать как expression bodies, т.е например void Foo(x) => x+1;
делал вполне себе валидный метод. Сейчас же, это счастье распространилось помимо методов и пропертей еще на конструкторы/деструкторы, геттеры и сеттеры:
class Person { Person() => Names = new[]{"Nameless One"}; public int FirstName { get => Names[0]; // ну вы поняли } }
Вообще вся эта expression bodied тематика экономит хорошо так символов. Единственное что мне не нравится это каша вроде
class Person { bool CanVote => Age <= 16; // без лигатур - коряво :( }
но это просто один конкретный пример того как можно получить нечитаемый код.
Throw Expressions
Тут мне 2017 студия осмелилась подправить Решарперный код. Было что-то вроде этого:
class Foo { Foo(Bar bar) { if (bar == null) throw new SomeException(); this.bar = bar; } }
но теперь оказывается можно писать вот так
class Foo { Foo(Bar bar) { this.bar = bar ?? throw new SomeException(); } }
Интересный ход конем, однако. Хорошо ли бросать из места где ожидается конкретный return value? Ну, если компилятор может это безопасно обработать, почему бы и нет — главное потом не пытаться прочитать поле, которое мы так и недописали.
Pattern matching
Когда-то я писал про самопальный pattern matching, но тут сделали реальную реализацию.
Начнем с простого — теперь проверку на тип можно сделать прямо в if
-е и сразу получить и проверку и приведенный тип:
if (shape is Rectangle rc) { var area = rc.Width * rc.Height; }
Ну и потом нам разрешили делать switch
на этом месте:
Shape s = new Rectangle { width = 0, height = 0 }; switch (s) { case Rectangle rc when (rc.width == rc.height): Console.WriteLine("It's a square!"); break; case Circle c: break; }
Заметьте ключевое слово when
выше. Ничего не напоминает? Правильно, F#. Но конечно F# все еще лидирует в этом плане. Хотя бы потому, что в F# можно делать алгебраические типы данных, что очень полезно когда ты, например, что-нибудь структурированное парсишь, например MathML.
Ну хорошо, смотрите, хоть что-то сделали. Конечно, нельзя мэтчить на Rectangle(42, double h)
— это откинули на будущее. И switch
на кортежах тоже пока не работает. Грусть-печаль.
Ref returns
Как вернуть элемент массива by reference? Ну строго говоря, можно запинить весь массив и попытаться вернуть указатель в unsafe
. Но теперь есть более гуманный способ — ref
на чем угодно.
int[] numbers = new int[] { 1, 2, 3 }; ref int refToSecond = ref numbers[1]; refToSecond = 123; Console.WriteLine(string.Join(",",numbers)); // 1, 123, 3
Хмм, ну это хорошо конечно, но со списком такое не пройдет
List<int> numbers = new List<int> { 1, 2, 3 }; ref int second = ref numbers[1]; // A property or indexer cannot be used⋮
Хмм, да, полезность сей затеи стремится к нулю. Нельзя например сослаться на букву в строке.
Все это — простой aliasing, то есть еще одно имя для той же самой переменной. Это не «ссылка на область памяти» т.к. это обычно требует слово fixed
чтобы вменяемо работать в unsafe контексте, а у нас все тут safe.
Да, теперь можно и из метода ref
на переданный ref
возвращать, то есть возможно вот такое мракобесие:
static ref int Min(ref int x, ref int y) { if (x < y) return ref x; else return ref y; } ⋮ int a = 1, b = 2; ref int c = ref Min(ref a, ref b); c = 123; // a == 123!!!
Holy s~t, посмотрите сколько тут раз ref
написано. Даже в вызове используется ref Min(...)
потому что если ты опустишь этот ref
это значит что тебе не нужен референс на значение, а нужно само значение.
int d = Min(ref a, ref b); // вполне валидно, d == 1
Проблема тут в том, что этот ref
— это то же самое что референсы в C++, из-за которых теперь в уютном сишарпике можно писать треш вроде Min(ref a, ref b) += 123
(по сути делает a += 123
), что абсолютно отвратительно для читания и понимания. К тому же, неопытные программисты тут же будут ломиться и возвращать адреса локальных переменных, что делать нельзя:
static ref int Foo() { int x = 0; return ref x; // не скомпилируется - cannot return local by reference }
Хотя помнится что году так в 2012, Липперт угрожал что они могут и этот сценарий поддержать.
Что еще тут?
Теперь можно делать подчеркивания в литералах: int x = 123_456;
. Также появились бинарные литералы — теперь можно писать var literal = 1100_1011_____0101_1010_1001;
. Подчеркиваний может быть сколько угодно.
Pattern matching — все самое вкусное не попало в C#7, ждем следующих версий. Должны быть всякие вот такие штуки поддержаны:
switch (foo) { case 123: ⋮ case MyEnum.Stuff: ⋮ case string s: ⋮ case Point(int x, 321) where x > 0: ⋮ case Point(12, 23) : ⋮ default: ⋮ }
Record types — не вошли, будут в C#8 наверное.
Ну вообщем как-то так. А вы заметили насколько шустрее стал компилятор C#7? Серьезно, видимо опять что-то подкрутили с инкрементальностью. Кстати, в VS2015 сделали обалденные улучшения инкрементальности C++, а тут кажется улучшили C#. У меня «горячая» компиляция летает, хотя «холодная» все еще тормозит как и раньше.
Наверняка что-то еще из фич забыл, напишите в комментариях. ■
А есть идеи почему в pattern matching’е используется ключевое слово `when` вместо привычного (как по смыслу так и по значению) `&&`? Т.е. почему не так:
`case Rectangle rc && rc.width == rc.height:`
T
15 марта 2017 at 2:50
Там вроде были 2 варианта,
where
иwhen
.where
наверняка побоялись потому что это уже ключевое слово в LINQ, в то время какwhen
используется в F# и как бы привычно. Насчет&&
— чисто теоретически можно было так сделать, но && обычно подразумевает что все операнды типаbool
, а в нашем случае это совсем не так. Так что это было бы сложно парсить, т.е. пришлось бы менять семантику. А этого никто не хочет. Вот представьте, что если бы вы написалиcase IAmConvertibleToBool z && ...
— если уz
естьimplicit operator bool
, уже непонятно, тестируем ли мы только на тип или на тип и его конвертируемость вtrue
. Такие неоднозначности никто не любит.Dmitri
15 марта 2017 at 11:34