В 53 выпуске подкаста DotNet & More (site, youtube) мы обсуждали фичи C# 10 и разбирали их применимость в будущем. И уже потом возник вопрос: все ли языковые конструкции из C#1, 2, 3, 4 и т.д. мы применяем? И даже если они безбожно устарели, существуют ли ситуации, в которых им найдется применение?
Говоря о C#, я бы разделил его жизненный путь на 2 вехи: до C#6 и начиная с C#6. Именно с релизом шестой версии Microsoft поменяли подход к дизайну языка, начали активно слушать сообщество. Потому и хотелось затронуть не сколько "современный" C#, а C# образца 2012 года, который многими программистами "старой школы" считается True С# (а так как этим ребятам уже лет 30+, они как раз занимают позиции тимлидов и продукт менеджеров, соответственно, определяют технологический стек проекта).
Если смотреть на C# 5 с точки зрения лишних фичей, то их окажется не так много. На мой взгляд, я бы отметил следующие:
Delegate operator: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/delegate-operator
The dynamic type: https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/builtin-types/reference-types#the-dynamic-type
Query expression: https://docs.microsoft.com/en-us/dotnet/csharp/linq/query-expression-basics
И тем интереснее попробовать найти применимость данных языковых конструкций в 2021 году.
Delegate operator
Тот факт, что данный оператор безбожно устарел подтверждают и сами Microsoft в своей справке:
![](https://habrastorage.org/getpro/habr/upload_files/103/d83/e99/103d83e99dd6f9a9065740b5ac8c9461.png)
Но внутренний манчкин не терпит такую растрату ключевых слов. Неужели lambda expressions по всем параметрам лучше такого "лампового" delegate? Нет, существует одна особенность, в delegate operator можно опускать параметры метода, если они нам не нужны. В lambda expressions можно использовать underscore (_), но если в функции много параметров, получается неприятный, с эстетической точки зрения, код. Давайте сравним:
Func<int, string, double, User, bool> allowAll = (_, _, _, _) => true;
versus
Func<int, string, double, User, bool> allowAll = delegate { return true; };
Данный пример, если честно, очень вдохновляет. Ведь мы нашли применимость такому "динозавру"!
Конечно, я бы не рискнул использовать такой подход в реальных проектах: эстетика эстетикой, но фрустрация у других разработчиков от таких синтаксических конструкций может стоить не один человеко-час. И не хотелось бы тратить время команды впустую.
The dynamic type
Не секрет, что динамический тип был добавлен в C# для упрощения работы с COM (см https://stackoverflow.com/questions/14098958/c-sharp-dynamic-com-objects). Конечно, кто-то скажет что разработчики решили сделать программистам приятное и превратить C# в такой прекрасный и удивительный язык как JavaScript, но давайте будет честны сами с собой, в 2010 году Microsoft шла по пути Балмера с построением собственной закрытой экосистемы, минимально обращая внимание на окружающий мир. И потому сбрасывать со счетов dynamic не стоит, COM еще жив, и будет жить, коль жив MS Office.
Еще иногда приходится использовать dynamic при работе с библиотеками, в которых эта фича задействована, например, ASP Net MVC. Тем не менее я бы не сказал, что такая практика хоть сколь либо распространена.
Но вот если вы не разрабатываете Add-Ins для Excel, есть ли толк от динамических типов? На самом деле да: dynamic type крайне удобен при прототипировании. Когда мысль летит вперед, перепрыгивание на DTO файл с полями будет просто прерывать ее.
dynamic entity = new ExpandoObject();
// the genius code with 'entity' variable
Подобный подход крайне удобен при обсуждении реализации и "накидывании" вариантов решения задачи: вместо рисования на white-board можно сразу в коде расписать приблизительный вариант решения и, более того, запустить это решение.
В остальном же приходится признавать, что dynamic это огромный кусок ненужного функционала. И это очень грустно, так как комбинация статической и динамической типизации могла быть невероятно мощным инструментом. На мой взгляд, этого не случилось, потому что была потеряна одна из самых главных особенностей таких языков программирования как JavaScript и Python: интерпретируемость. Несмотря на то, что Dynamic Language Runtime есть интерпретация, у нас нет возможности избежать перекомпиляции сборки, а значит мы не можем вносить изменения в процессе работы приложения. Например, разработчики 1С очень часто пишут код инкрементально - в обработчик нажатия кнопочки код вносится по мере прохождения по нему интерпретатора, так что нет необходимости перезапускать приложение для любого небольшого фикса. В C# же приходится компенсировать Test First подходом.
Query expression
Данный синтаксический сахар создавал очень много споров лет 10 назад, и это хорошо видно по коду, написанному в те времена, но сегодня всем достаточно очевидно, что код вида
var teenagers = from u in users
where u.Age is > 10 and < 18
select u.Name;
гораздо сложнее с точки зрения расширяемости и поддержки, чем "точечная нотация"
var teenagers = users
.Where(u => u.Age is > 10 and < 18)
.Select(u => u.Name);
Тем не менее существует, как минимум, один случай, когда Query Expression strikes back: работа с множественными SelectMany. Представим, что мы хотим найти друзей нашего пользователя. Напишем в разных нотациях:
private IEnumerable<User> GetFriends(string myName) =>
_users
.Where(u => u.Name == myName)
.SelectMany(u => u.Friends);
private IEnumerable<User> GetFriends(string myName) =>
from u in _users
where u.Name == myName
from f in u.Friends
select f;
В данном случае Dot Notation однозначно лучше, хотя бы с точки зрения количества строк. А если нам понадобится запросить друзей друзей?
private IEnumerable<User> GetFriendsOfFriends(string myName) =>
_users
.Where(us => us.Name == myName)
.SelectMany(u =>
u.Friends.SelectMany(f => f.Friends)
);
private IEnumerable<User> GetFriendsOfFriends(string myName) =>
from u in _users
where u.Name == myName
from f in u.Friends
from fof in f.Friends
select fof;
Тут ситуация уже куда интереснее, так как в первом случае не удается избежать вложенности. И это может быть достаточно серьёзной проблемой в случае действительно больших запросов, например если необходимо найти не просто друзей друзей, а друзей друзей друзей друзей друзей:
private IEnumerable<User> GetFriendsOfFriendsOfFriendsOfFriendsOfFriends(string myName) =>
_users
.Where(us => us.Name == myName)
.SelectMany(u =>
u.Friends.SelectMany(f =>
f.Friends.SelectMany(fof =>
fof.Friends.SelectMany(fofof =>
fofof.Friends.SelectMany(fofofof => fofofof.Friends)
)
)
)
);
private IEnumerable<User> GetFriendsOfFriendsOfFriendsOfFriendsOfFriends(string myName) =>
from u in _users
where u.Name == myName
from f in u.Friends
from fof in f.Friends
from fofof in fof.Friends
from fofofof in fofof.Friends
from fofofofof in fofofof.Friends
select fof;
Конечно, данный пример немного надуман, но чего не бывает в кровавом энтерпрайзе.
Главное, в C# есть инструмент "выпрямления" вложенности LINQ и в некоторых случаях он является просто незаменимым.
P.S.: Так же можно отметить, что использовать Join гораздо удобнее в виде Query Expression, нежели чем в Dot Notation, но на мой взгляд, все зависит от привычки.
Заключение
Давайте будем честны, приведенные выше примеры являются, по сути дела, стрельбой пушкой по воробьям. Нет ни какого смысла тратить время на изучение данных устаревших синтаксических конструкций, просто потому, что они будут немного полезны в некоторых случаях. И даже если вы мастер Query Expression, знаток DLR и не представляете свою жизнь без delegate, не стоит усложнять жизнь своим коллегам.
Но все таки, не с прагматичной, а романтичной точки зрения, приятно осознавать, какую мощь таит в себе C#, какие богатые возможности, позволяющие выразить свою мысль короче, яснее.