C# — есть ли что-то лишнее?
Все будет быстро. Это выступление Анатолия Левенчука, в последнее время не дает мне покоя. Успехи глубинного обучения в последний год говорят о том, что все очень быстро изменится. Слишком долго кричали волки-волки говорили «искусственный интеллект», а его все не было. И вот, когда он, наконец, приходит к нам, многие люди этого просто не осознают, думая, что все закончится очередной победой компьютера в очередной интеллектуальной игре. Очень многие люди, если не все человечество, окажется за бортом прогресса. И этот процесс уже запущен. Думаю, что в этот момент меня не очень будет интересовать вопрос, который вынесен в заголовок статьи. Но, пока этот момент еще не настал, попытаюсь поднять этот потенциально спорный вопрос.
Программируя уже более 25 лет, застал достаточно много различных концепций, что-то смог попробовать, еще больше не успел. Сейчас с интересом наблюдаю за языком Go, который можно отнести к продолжателям «линейки языков Вирта» — Algol-Pascal-Modula-Oberon. Одним из замечательных свойств этой цепочки является то, что каждый последующий язык становится проще предыдущего, но не менее мощным и выразительным.
Думаю, что всем понятно, чем хорош простой язык. Но все же приведу эти критерии, поскольку они будут всплывать позже:
- Простой язык быстрее изучается, значит проще получить необходимых разработчиков.
- Поддержка программы на более простом языке обычно проще (да, это интуитивное ощущение, которое нужно бы доказать, но я приму его сейчас за аксиому).
- У более простого языка проще развивать окружающую его инфраструктуру — переносить на разные платформы, создавать различные утилиты, генераторы, парсеры и т.п.
Почему же тогда существуют сложные языки? Все дело в выразительности. Если какая-то конструкция позволяет коротко описать необходимое действие, то это вполне может окупить негативные стороны усложнения языка.
За относительно недолгое время своего существования, язык C# впитал в себя значительное количество различных концепций, отразившихся в его конструкциях. Скорость их добавления иногда пугает. Мне, поскольку я с C# почти с самого начала — проще. Но каково новичкам, которые только приступают к изучению? Иногда завидую Java-программистам, где новшества внедряются в язык гораздо более консервативно.
То, что добавлено в язык — его ведь реально уже не вырубишь и топором. Конечно, если взять язык, широко распространенный в узких кругах, можно позволить себе несовместимость между версиями. Некоторые «шалости» обратной несовместимости может себе позволить такой язык, как Python (при переходе со 2-й на 3-ю версию). Но не C#, за которым стоит Майкрософт. Мне кажется, что если бы разработчики понимали, что с каждой новой фичей язык становится не только удобнее (в определенных случаях), но и немного ближе к своей смерти от «ожирения», то комментарии были бы чуть менее восторженными, чем это имеет место в первой ветке откликов на новшества C# 7.
То, что описано далее — всего лишь мои спекуляции на тему того, действительно ли это полезная штука. Конечно, это может быть делом вкуса и не все согласятся со мной (смотрите спойлеры). И в любом случае, это останется в C# уже навечно… Ну, до момента сингулярности, по крайней мере.
Список добавленных фич языка по версиям можно найти здесь: C# Features added in versions. Не буду трогать версию 2.0, начну с 3.0.
Практически у любого явления, факта, фичи языка, есть и положительные и отрицательные стороны. То, какие оценки мы этому придаем, во много зависит от наших субъективных особенностей и другой человек может то же самое оценить противоположным способом. Мало того, что это естественно, скорее всего, эти оценки в обоих случаях будут правильными. Просто для разных людей. В спойлерах далее попытаюсь показать примеры таких отличий.
C# 3.0
Implicitly typed local variables
var i = 5;
var s = "Hello";
var d = 1.0;
var numbers = new int[] {1, 2, 3};
var orders = new Dictionary();
Пресловутое var. О введении которого сейчас спорят в мире Java («Var и val в Java?», «Ключевое слово «var» в Java: пожалуйста, только не это»)
Код пишется один раз, читается много (банальная истина). Автоматический вывод типа во многих случаях заставляет делать дополнительные действия для того, чтобы понять, какого типа переменная. А значит, это плохо. Да, это привычно, например, для JavaScript-программистов, но там совершенно другая парадигма типизации.
Раздражение от явного и полного прописывания типов вызывают такие вот куски кода:
List> scores = seeker.getScores(documentAsCentroid);
...
foreach(Pair score in scores)
И это (Pair
using StopWordsTables = System.Collections.Generic.List>;
Эта конструкция позволяла вместо той громоздкой писанины, что стоит справа, использовать идентификатор StopWordsTables. К сожалению, он действителен только в пределах одного исходного файла…
Вот если бы ввели typedef, это бы решило проблему громоздких типов без ущерба для читаемости кода.
Возражения по поводу того, что можно было бы договориться использовать var только там, где тип легко вывести глазами (т.е., он явно виден в инициализаторе) не найдут у меня поддержки по одной простой причине. Аналогичное правило уже ввела в своем Code Agreement Майкрософт (про свою компанию я уже молчу). Только вот практически никто этого не соблюдает. Var победило. Люди ленивы.
Есть еще момент — var очень ограничен. Его можно использовать только в локальных идентификаторах. В свойствах, полях, методах, все также приходится раз за разом писать эти раздражающе длинные идентификаторы коллекций, а в случае изменения типов повторять редактирование во всех местах. С Type/typedef этого все ушло бы в прошлое.
В развитие темы — если уж ввели var, почему бы не довести идею уже до логического завершения, как это сделано в Go? В инициализаторе вместо »=» писать »:=», что означает, что тип выводится автоматически. И тогда вообще не нужно никакого слова писать на месте типа. Еще короче… Кстати, type в Go тоже есть, что очень удобно.
Мой вывод — var в C# был ошибкой. Нужен был всего лишь typedef.
Далее в примерах я буду использовать var лишь потому, что он позволяет сократить размер примера, а в паре-тройке строк не успевают проявиться его отрицательные моменты.
Object and collection initializers
var r = new Rectangle {
P1 = new Point { X = 0, Y = 1 },
P2 = new Point { X = 2, Y = 3 }
};
List digits = new List { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
Штука полезная, сокращающая код не в ущерб читаемости. Одобряю.
Auto-Implemented properties
Теперь, вместо
public Class Point {
private int x;
private int y;
public int X { get { return x; } set { x = value; } }
public int Y { get { return y; } set { y = value; } }
}
Стало возможно писать так:
public Class Point {
public int X { get; set; }
public int Y { get; set; }
}
Насчет свойств в глубине души так и не понял, а нужно ли их было вводить? Вон, в той же Java и без них вполне нормально жить, используя определенные соглашения имен в методах. Но коль уж ввели, то такое упрощение их описания вполне удобно (без ухудшения читабельности) во многих случаях.
Anonymous types
var p1 = new { Name = "Lawnmower", Price = 495.00 };
var p2 = new { Name = "Shovel", Price = 26.95 };
p1 = p2;
Мне данная опция языка так ни разу и не пригодилась. Хотя нет, 1 раз таки нужно было, вспомнил. Я бы не вводил. Хотя те примеры, что видел в учебнике, вроде бы и логичны. В общем, возможно штука и полезная, просто не в моих сценариях (предпочитаю возиться с алгоритмами, а не с базами и JSON, хотя, разное бывает).
Extension methods
namespace Acme.Utilities
{
public static class Extensions
{
public static int ToInt32(this string s) {
return Int32.Parse(s);
}
public static T[] Slice(this T[] source, int index, int count) {
if (index < 0 || count < 0 || source.Length – index < count)
throw new ArgumentException();
T[] result = new T[count];
Array.Copy(source, index, result, 0, count);
return result;
}
}
}
using Acme.Utilities;
...
string s = "1234";
int i = s.ToInt32(); // Same as Extensions.ToInt32(s)
int[] digits = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int[] a = digits.Slice(4, 3); // Same as Extensions.Slice(digits, 4, 3)
Очень удобная штука. Временами теоретики ООП её ругают, но без неё было бы неудобно (громоздко) делать многие вещи.
Query expressions
Он же LINQ. Этот пункт вызывает настолько смешанные чувства! Ну, примерно, как ложка дегтя в бочке чего-то хорошего. Вне всякого сомнения, LINQ явилась одной самых, по настоящему классных возможностей языка. Но зачем нужно было реализовывать это двумя способами? Я про так называемый человеко-понятный синтаксис (ЧПС), который имитировал SQL-запросы, насколько я понимаю.
string[] people = new [] { "Tom", "Dick", "Harry" };
// ЧПС или же синтаксис запросов
var filteredPeople = from p in people where p.Length > 3 select p;
// функциональный стиль или лямбда синтаксис
var filteredPeople = people.Where (p => p.Length > 3);
В результате имеем:
- ЧПС не соответствует SQL напрямую, так что знания SQL недостаточно для того, чтобы написать соответствующий запрос. Есть свои особенности.
- Одно и то же (с небольшими и редкими исключениями, функциональный и ЧПС-стили эквивалентны) можно написать двумя способами.
- Программисту следует учить оба варианта, поскольку они оба могут появиться в коде.
- ЧПС стиль резко контрастирует с остальным кодом, выглядя чем-то чужеродным.
Похожие чувства в плане чужеродности стиля у меня вызывают байндинги WPF. Они представляют собой микроскриптовые конструкции, написанные на своем языке внутри XML. В результате все выглядит громоздко и некрасиво. Не знаю, как можно было бы сделать красивее — может создать специализированный язык разметки, а не городить все в XML? Но я отвлекся. В общем, признаюсь — за последние несколько лет не написал ни одного выражения в ЧПС, при этом совершенно не испытывая в этом потребности. Только немножко редактировал чужие.
В общем, LINQ — очень и очень нужная штука, к которой очень зря привесили гирю ЧПС.
Lambda expressions
x => x + 1 // Implicitly typed, expression body
x => { return x + 1; } // Implicitly typed, statement body
(int x) => x + 1 // Explicitly typed, expression body
(int x) => { return x + 1; } // Explicitly typed, statement body
(x, y) => x * y // Multiple parameters
() => Console.WriteLine() // No parameters
Это было прекрасное приобретение, привнесшее в C# элементы функционального стиля и существенно улучшившего выразительность коротких фрагментов кода, передаваемых как аргументы. Вот только когда лямбды начинают занимать с десяток и более строк, читать код становится очень сложно. Важно вовремя остановиться и в этом случае перейти опять на методы.
Expression trees
Вряд ли стоит рассматривать данную фичу отдельно от LINQ и Lambda.
Partial methods
Неплохой способ разделить автоматически генерируемый и ручной код. Я — за.
Фокал — это был тихий ужас, после которого Бейсик казался образцовым языком, но другого языка «высокого уровня» на БК 0010 зашито не было. С другой стороны, система команд процессора К1801ВМ1 отличалась удобной структурой, позволявшей относительно легко программировать прямо в кодах, вводя команды в восьмеричной системе (16 бит). Почему восьмеричная? 8 регистров, 8 способов адресации. Именно поэтому было удобно использовать именно такой метод и ассемблер/дизассемблер для небольших программ был не нужен. Немножко неудобно было только вычислять смещения, когда программа предварительно писалась в тетрадке.
Потом был университет, МС 1502. Хоть это уже и был IBM-PC совместимый компьютер, но поначалу здесь не было дисководов, MS DOS, ассемблеров. Работали прямо из интерпретатора бейсика, который был зашит в биосе.
И вот тут уже мне стало очевидно, что все люди разные. Был в этой банде нашей группе Вениамин. Меня он всегда поражал тем, что в его программах я ни разу не замечал ошибок. Все, что он писал — работало с первого раза. На МС 1502 система команд (x86) была гораздо менее логична (это моё личное мнение), чем на БК 0010 (PDP-11). Поэтому тут я перестал писать небольшие вставки прямо в кодах. Честно говоря, вообще отошел от ассемблера. А вот Вениамин — продолжал компилировать прямо в голове, писать код в операторе DATA, которые затем выполнялись прямо из программ на бейсике. И все продолжало работать с первого раза! Он часто даже по нескольку часов не сбрасывал на кассету написанную программу —, а ведь тут был большой риск зависания в случае ошибки в коде. В этом случае помогала только перезагрузка.
И вот, настал день, когда у Веника (как мы между собой его иногда называли), программа не заработала. Он был поражен, удивлен, раздосадован и еще много чего (аналогии с анекдотом про учительницу русского тут нет — Веня обходился без особо живой части великорусского языка). Хотя я, например, втайне немного злорадствовал — ведь нельзя же никогда не ошибаться. Но, я был посрамлен! После долгих выяснений, оказалось, что проблема заключалась в ошибке при описании работы одной из команд процессора — какой-то там флаг выставлялся, написано было, что должен был сбрасываться (уже не помню точно — 25 лет прошло).
Так что если кто-то скажет, что ему не нужна типизация, юнит-тесты, он всегда пишет безошибочные программы — я поверю. Я видел такого человека (его следы затерялись после переезда в США и начала работы в Майкрософт). Но я и еще много других людей — не такие.
C# 4.0
Dynamic binding
Потенциально полезный пример — вместо
var doc = new XmlDocument("/path/to/file.xml");
var baz = doc.GetElement("foo").GetElement("qux");
можно написать
dynamic doc = new XmlDocument("/path/to/file.xml");
var baz = doc.foo.qux;
Несмотря на то, что выглядит хорошо, я бы не рекомендовал такое использование. Тип dynamic — очень опасная штука, поскольку теряется весь контроль типов. Из более мелких пакостей — перестают работать подсказки в редакторе. Тем не менее, в определенных сценариях, он полезен. Например, с его помощью я у себя делал подгрузку плагинов (точнее, использование кода из них). За счет того, что вызовы методов здесь кешируются, то получается производительно и не нужно городить это самостоятельно. А насчет безопасности — иначе мне бы все-равно пришлось бы работать через рефлексию, так что в этом случае безопасность не была бы большей. А вот код был бы более сложным и запутанным. Так что осторожное использование динамиков в ограниченном количестве сценариев одобряю. Конечно, вводились они больше с прицелом на скриптовые языки. Ну, нужно, так нужно.
Named and optional arguments
class Car {
public void Accelerate(
double speed, int? gear = null,
bool inReverse = false) {
/* ... */
}
}
Car myCar = new Car();
myCar.Accelerate(55);
Уменьшается количество перегруженных методов — код становится проще и надежнее (меньше возможностей совершить ошибку копипаста, меньше работы при рефакторинге). Одобряю.
Generic co- and contravariance
Вполне логичное уточнение поведения языка. Особой сложности в изучение и синтаксис не вносит и может быть рассмотрено новичками позже. Одобряю.
Embedded interop types («NoPIA»)
Это одна из фич, про которые мне особо нечего сказать, исходя из своей практики — просто читал, что такое есть. Мне она не нужна была, но COM видимо еще долго не умрет и тем, кто (например) работает с MS Office, он еще долго будет нужен.
C# 5.0
Asynchronous methods
public async Task ReadFirstBytesAsync(string filePath1, string filePath2)
{
using (FileStream fs1 = new FileStream(filePath1, FileMode.Open))
using (FileStream fs2 = new FileStream(filePath2, FileMode.Open))
{
await fs1.ReadAsync(new byte[1], 0, 1); // 1
await fs2.ReadAsync(new byte[1], 0, 1); // 2
}
}
Очень удобная конструкция. К сожалению, при практической реализации возникли некоторые ограничения — детали реализации протекали в виде ограничений (Async/await в C#: подводные камни). Часть их была снята в следующих версиях (Await in catch/finally blocks) языка, или библиотек (akka.net поначалу не позволяла смешивать свою модель асинхронного исполнения с рассматриваемой фичей, но потом это поправили). Может быть имело бы смысл рассмотреть и какие-то другие паттерны параллельного взаимодействия — типа горутин. Но тут уже выбор за архитекторами языка. В общем, одобряю.
Caller info attributes
public void DoProcessing()
{
TraceMessage("Something happened.");
}
public void TraceMessage(string message,
[System.Runtime.CompilerServices.CallerMemberName] string memberName = "",
[System.Runtime.CompilerServices.CallerFilePath] string sourceFilePath = "",
[System.Runtime.CompilerServices.CallerLineNumber] int sourceLineNumber = 0)
{
System.Diagnostics.Trace.WriteLine("message: " + message);
System.Diagnostics.Trace.WriteLine("member name: " + memberName);
System.Diagnostics.Trace.WriteLine("source file path: " + sourceFilePath);
System.Diagnostics.Trace.WriteLine("source line number: " + sourceLineNumber);
}
// Sample Output:
// message: Something happened.
// member name: DoProcessing
// source file path: c:\Users\username\Documents\Visual Studio 2012\Projects\CallerInfoCS\CallerInfoCS\Form1.cs
// source line number: 31
Небольшой синтаксический сахар, который не утяжеляет язык, но позволяет в определенных сценариях уменьшить количество копипаста и кода. Одобряю.
C# 6.0
Compiler-as-a-service (Roslyn)
Этот пункт (несмотря на общую важность) пропущу. Я бы отнес его скорее к инфраструктуре, а не непосредственно к языку.
Import of static type members into namespace
using static System.Console;
using static System.Math;
using static System.DayOfWeek;
class Program
{
static void Main()
{
WriteLine(Sqrt(3*3 + 4*4));
WriteLine(Friday - Monday);
}
}
Поначалу мне эта фича показалась полезной. Но попробовав её на практике, вынужден признать, что ошибся. Читаемость кода ухудшается — методы и члены статического класса начинают мешаться с методами текущего класса. И даже ввод стал медленнее, хотя вроде бы количество идентификаторов уменьшилось на единицу. Но за счет того, что теперь в подсказке от Intellisense больше вариантов, нажатий нужно сделать больше. В общем, данная фича, с моей точки зрения — ошибка.
Exception filters
try { … }
catch (MyException e) when (myfilter(e))
{
…
}
Еще не попробовал. Поэтому есть искушение назвать фичу бесполезной, но может просто мои сценарии к ней не сильно подходят? Может, кто расскажет, в каких случаях и насколько часто она реально хороша?
Await in catch/finally blocks
Не считаю это самостоятельной фичей — скорее исправление предыдущих проблем.
Auto property initializers
public class Customer
{
public string First { get; set; } = "Jane";
public string Last { get; set; } = "Doe";
}
Логичное и удобное дополнение к автосвойствам. Код становится чище, а значит одобряю.
Default values for getter-only properties
public class Customer
{
public string First { get; } = "Jane";
public string Last { get; } = "Doe";
}
Аналогично предыдущему пункту.
Expression-bodied members
public string Name => First + " " + Last;
public Customer this[long id] => store.LookupCustomer(id);
Большой практики применения пока нет, но выглядит неплохо. Нужно будет еще пройтись по своему коду и посмотреть, где можно бы применить. Главное здесь как с лямбдами — не переусердствовать и не делать выражений на полэкрана.
Null propagator (succinct null checking)
public static string Truncate(string value, int length)
{
return value?.Substring(0, Math.Min(value.Length, length));
}
Давно напрашивавшаяся штука. Одобряю. Хотя, на практике и оказалось, что применяется не так часто, как ожидалось до того.
String Interpolation
О! Вот это то, чего ждал давным-давно, и что мгновенно прижилось в моем коде. Всегда старался писать идентификаторы в контексте строки примерно так:
"Total lines: " + totalLines + ”, total words: " + totalWords + ”.”;
Иногда меня спрашивали, а знаю ли я про форматирование строк? Да, знаю, но там есть 2 большие проблемы:
- Выражение отнесено далеко от места, где оно вставляется. Это затрудняет чтение кода.
- Строка с литералами форматирования, фактически является микроскриптом, который исполняется в run-time. Соответственно, вся система типизации, проверки соответствия параметров C# летит в тартарары.
Также это приводит к тому, что в методах Format (…) допускается большое количество ошибок при рефакторинге.
Поэтому и использовал такое вот немного громоздкое написание. Но, наконец, дождался от C# такого подарка :) Одобряю однозначно и всеми конечностями!
nameof operator
if (x == null) throw new ArgumentNullException(nameof(x));
WriteLine(nameof(person.Address.ZipCode)); // prints "ZipCode"
Аналогично «Caller info attributes». Одобряю.
Dictionary initializer
var numbers = new Dictionary {
[7] = "seven",
[9] = "nine",
[13] = "thirteen"
};
Еще одно новшество, которое позволяет упростить работу с кодом, который нередко набирается с помощью копипаста и подвержен ошибкам от невнимательности. Любая возможность сделать его чище и удобнее для чтения будет приводить к плюсам в карме архитектора языка. Плюсик.
И вот вышла Java. (Это было еще прошлое тысячелетие)
Прочитал книжку, вдохновился идеей реализации простого http-сервера (просто отдавал статические файлы) и по образу и подобию примера из книги написал свой код с небольшими вариациями. Код именно писал, а не копировал фрагменты. Относительно долго вылавливал всякие неточности, связанные с тем, что язык новый, просто свои обычные опечатки. Наконец, код откомпилировался. Я его запустил, приготовившись к этапу отлова следующих багов. Но программа заработала. И заработала так, как я хотел.
Сказать, что я был удивлен — ничего не сказать. Но именно после этого случая я понял, что для меня строгость языка (и еще автоматическая сборка мусора — это был мой первый язык с этой фичей) оборачивается возможностью продуктивно работать над серьезными проектами. Я верю, что многим не нравится статическая типизация, необходимость явно описывать типы и еще много чего, что относят к строгим языкам. И я верю, что им это действительно мешает. Но их аудитория — это Бэн, но не я.
C# 7.0 proposals
Данными функциями я еще не пользовался — обычно сижу на релизной версии шарпа, иногда приходится спускаться чуть ниже. Поэтому здесь приведу чисто умозрительные аргументы. Список приведу по статье «Новшества C# 7», а не по данным из википедии.
Binary literals
int x = 0b1010000;
int SeventyFive = 0B10_01011;
Новшество выглядит безобидно, не сильно усложняя язык, а для тех, кому нужно работать с битами — удобно. Немного не понял фразу «Можно отделять нули произвольным количеством подчёркиваний» — почему только нули?
Local functions
public void Foo(int z)
{
void Init()
{
Boo = z;
}
Init();
}
Когда только перешел на C# с Объектного Паскаля (Delphi), мне очень не хватало локальных функций, как способа структурировать свой код. Простое вынесение кусков кода в приватные методы приводило к появлению классов с большим количеством методов на одном уровне. Так происходило, пока я не понял, что в C# для этого нужно применять другой метод — объектную декомпозицию. После этого я часто стал выносить относительно громоздкий код во внутренний класс со своими методами. По достижении определенного уровня сложности, этот класс мог быть разделен на несколько связанных классов, которые выносились в отдельную папку и нэймспейс. Это позволило привнести ту иерархию в код, которую в стародавние времена обеспечивали локальные функции и процедуры Паскаля.
Таким образом, мое мнение сейчас изменилось — не стоит давать еще одного способа структурирования кода. Это усложнение языка, усложнение чтения (внешний метод становится большим, поэтому сложно охватить его взглядом от начала, и до конца), но нет принципиальных преимуществ.
Tuples
Пока не понял необходимости данной фичи, в каких ситуациях она будет полезнее, чем вернуть класс/структуру или же использовать out-аргументы. Поэтому для меня это скорее отрицательный вклад в язык.
Pattern matching, conditions in switch
// type pattern
public void Foo(object item)
{
if (item is string s)
{
WriteLine(s.Length);
}
}
// Var Pattern
public void Foo(object item)
{
if(item is var x)
{
WriteLine(item == x); // prints true
}
}
// Wildcard Pattern
public void Foo(object item)
{
if(item is *)
{
WriteLine("Hi there"); //will be executed
}
}
Более полный пример можно посмотреть по ссылке.
Выглядит неплохо, но нужно посмотреть, насколько это окажется полезным на практике. Есть интуитивное подозрение, что усложнение языка не окупится тем количеством кейсов, где эта возможность будет полезна. Так что пока я насторожен.
Ref locals and ref returns
static void Main()
{
var arr = new[] { 1, 2, 3, 4 };
ref int Get(int[] array, int index)=> ref array[index];
ref int item = ref Get(arr, 1);
WriteLine(item);
item = 10;
WriteLine(arr[1]);
ReadLine();
}
// Will print
// 2
// 10
Простое и интуитивно понятное расширение для работы со ссылками. Но реальная нужда в нем пока непонятна.
Описанных далее в статье на Хабре пунктов «Записи» и «Создание неизменяемых объектов» не вижу сейчас в текущих предложениях на 7-ю версию C#, поэтому оценивать их не буду.
Итак, что в итоге?
С моей точки зрения, C# нажил (7-ю версию пока не трогаю, оплакивать буду по факту) себе такие лишние фичи:
- Человеко-понятный синтаксис LINQ. Достаточно было бы остановиться на fluent-стиле.
- Анонимные типы.
- Var. Эта ограниченная локальными переменными фича не дала внедрить нормального определения типов, в то же время существенно ухудшив читабельность кода.
- Импорт статиков — ухудшает читаемость кода.
Что было особенно полезно:
- LINQ (без ЧПС).
- Лямбды.
- Постепенное упрощение инициализации и описания объектов, различных структур данных, свойств.
- Именованные и по умолчанию параметры.
- Async/await.
- Интерполяция строк.