В 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 в своей справке:
Но внутренний манчкин не терпит такую растрату ключевых слов. Неужели 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#, какие богатые возможности, позволяющие выразить свою мысль короче, яснее.
Комментарии (22)
SShtole
04.10.2021 18:02+1Это, типа, «Даже я, адвокат, требую для подсудимых смертной казни»? В смысле, как-то такие примеры не очень вдохновляют на использование.
А по существу:
1. Никогда не любил смешивать SQL-like syntax и ЯП. Данный же конкретный пример наводит на мысль, что что-то неправильно спроектировано. И очень хорошо, что это заметно на уровне языка, гораздо хуже, когда язык это скрывает.
2.так как комбинация статической и динамической типизации могла быть невероятно мощным инструментом
ИМХО, очень вряд ли это могло бы случиться в C#. Мне больше всего нравится, как комбинация статики/динамики сделана, например, в TIS. С одной стороны — типов нет в принципе (при описании), мысль разработчика ничто не сдерживает, с другой — шаг влево/вправо,
— сразу летят exception'ы, которые отлично заменяют компиляцию в плане безопасности. Это, конечно, много где сделано, можно хоть тот же OLE_VARIANT вспомнить, но без глубочайшей поддержки на уровне языка / стандартной библиотеки эффективности не добиться, а генетика у Шарпа для этой цели не та.5 + '3'
mayorovp
05.10.2021 12:42+3С одной стороны — типов нет в принципе (при описании), мысль разработчика ничто не сдерживает, с другой — шаг влево/вправо,
5 + '3'
— сразу летят exception'ыЭто называется "строгая динамическая типизация", а не "комбинация статики/динамики"
pankraty
04.10.2021 20:25+4Из моей практики, на одном проекте активно использовались dynamic типы для работы с JSON -ами, приходящими из базы. С одной стороны, не надо делать массу DTO- шек, и если структура документа в базе со временем меняется, то вроде бы как код продолжает работать без необходимости поддерживать сущности v1, v2, vN+1... Но преимущества мнимые, т. к. все начинает падать в рантайме, принося в .Net все "прелести" языков без строгой типизации. Бррр, до сих передергивает, как вспоминаю.
Другой случай, где я применял dynamic, на этот раз самостоятельно, заключался в том, что внешняя библиотека (OpenXML, но это не важно) предоставляла несколько почти идентичных классов, с одинаковым набором полей, но в разных пространствах имён. Никаких общих интерфейсов или предков они не имели. Расширить внешнюю библиотеку тоже нельзя. И нужно было написать конвертацию из обоих типов в наш внутренний, по возможности, избежав копипасты. Для этого у меня было два internal метода, принимающих аргументы из разных пространств имён, и перенаправляющих вызовы в private метод, с dynamic аргументом. Чтобы это все не отвалилось при апгрейде OpenXML, код был плотненько покрыт тестами. И долгое время для меня это был единственный более-менее оправданного применения dynamic на практике, но и то потом оказалось, что это вызывало проблемы у пользователей, работающих в sandbox environment-ах (подробностей не помню, если кому будет интересно, найду соответствующий тикет на гитхабе). Так что и от такого применения пришлось отказаться.
А в защиту query syntax скажу, что тоже не понимал, для чего им вообще пользоваться, пока не столкнулся с запросом, в котором цепочка джойнов добавляла по одному новому полю к анонимному объекту. С query- синтаксисом такой запрос оказался в несколько раз короче, и читается легче.
pankraty
04.10.2021 20:58+2Немного из другой серии, но тоже из разряда бесполезного - это возможность иметь internal abstract член в публичном классе. Если унаследоваться из внешней сборки, переопределять этот член нельзя, т.к. он internal, но не переопределять тоже нельзя, т. к. он абстрактный. Такой вот курьез. Вроде и не баг, но и практической пользы ноль. Будет интересно, если кто-то придумает юзкейс, в котором это применимо.
Ещё одна вещь, которую вряд ли придумали бы в такой форме, если придумывали сразу, с нуля. "protected internal" является не взаимоусиливающей комбинацией "protected" и "internal", а действует как "или": чтобы получить доступ к члену надо быть наследником ИЛИ (не И) находиться в той же сборке (ср. с private static, например - на него одновременно распространяются ограничения private и static, усиливая друг друга). Зато логическое И выражается в виде модификатора "private protected", хотя от private в нём вообще ничего. Мне кажется, если бы не обратная совместимость, то "protected internal" должен был бы стать тем, что сейчас называется "private protected", а нынешний protected internal (с логическим ИЛИ) должен быть упразднен: если уж член protected, то ему обычно незачем быть публичным внутри своей сборки.
mayorovp
05.10.2021 12:53+3Немного из другой серии, но тоже из разряда бесполезного — это возможность иметь internal abstract член в публичном классе. Если унаследоваться из внешней сборки, переопределять этот член нельзя, т.к. он internal, но не переопределять тоже нельзя, т. к. он абстрактный. Такой вот курьез. Вроде и не баг, но и практической пользы ноль. Будет интересно, если кто-то придумает юзкейс, в котором это применимо.
Практическая польза тут — в возможности создания закрытых иерархий классов, которые невозможно расширить в другой сборке. Пример из Windows Forms: Image — Bitmap, Metafile. Эти классы являются обёртками над объектами GDI, и новых типов объектов в этой иерархии не предусмотрено.
Фактически, таким образом можно делать ООПшные аналоги Union-типов из ФП.
navferty
04.10.2021 23:19+2Странный аргумент в пользу query синтаксиса. Method chain так же можно сделать без вложенности:
public IEnumerable<User> GetFriendsOfBlaBlaBla(User user, string name) => user.Friends .Where(x => x.Name == name) .SelectMany(x => x.Friends) .SelectMany(x => x.Friends) .SelectMany(x => x.Friends) .SelectMany(x => x.Friends) .SelectMany(x => x.Friends) .SelectMany(x => x.Friends) .SelectMany(x => x.Friends);
koodeer
05.10.2021 07:35+7dynamic хорош для двойной диспетчеризации. В частности, паттерн Visitor с его помощью выражается намного проще.
Query expression с применением let сильно упрощает сложные запросы. В синтаксисе методов придётся протаскивать сложный анонимный тип.
Например, в библиотеке lin2d используется такая запись. Переписать на синтаксис методов, конечно, можно, но выглядеть будет жутко.
Krey
06.10.2021 07:52-2Вот жеж ахинея. Чуть позже разверну, если будет в этом смысл, после прочтения каментов...
Krey
06.10.2021 08:37Делегат это объявление сигнатуры функции. Его для этого и используют. И экспортируют. А не внутри функций. Как описать event лямбдами?
Про query и expression tree и сахар
Попробуйте написать аналог
From var1 in set
let var2=func(var1)
let var3=func2(var1)
From var4 in set2 join on...
Where var4 between var2 and var3
Select (f2=var2, f4=var4)
Ну смысл думаю понятен. Нельзя подобные конструкции яснее написать...В строчку, с трансляцией переменных.
Ну а в целом примеры какието не жизненные. Придуманные из пальца для статьи.
ksbes
06.10.2021 14:01-2Как изначально джавист, могу сказать, что как реализованы евенты в C# — это то ещё зло. Мало того что «застолбили» такое удобное название переменной в ключевые слова (туда же идёт и params), так ещё и постоянно приходится думать обо всяких утечках и «застрявших событиях». И ещё не очень совместимо с «новой идеологией», как вы и написали.
Я-то ладно, уже давно на с# пишу — уже знаю хорошие практики, но джуны у меня регулярно косячат на event'ах. Приходится буквально семинары им устраивать с доской и партами.Попробуйте написать аналог ...
Ну есть всякие SelectMany. Да и вообще иногда интересно декомпильнуть и посмотреть что именно скрывается за сахаром (Всякие Where() и лямбды — тоже сахар, кстати).Refridgerator
06.10.2021 15:20А что не так с событиями на шарпе?
ksbes
06.10.2021 16:02Это целую статью писать надо. Но если совсем-совсем коротко: не стоит вводить в «ядро» языка то, чему место в стандартной библиотеке.
VictorNS
08.10.2021 20:21Я думаю, как минимум имеется ввиду, что если не отпишешься, то сборщик мусора не может подчистить объект. И потихоньку отъедается память.
Sequd
03.11.2021 14:17-1Вот простой пример, обходите в глубину сколько влезет. Без убогих конструкций.
private static void SelectAll(IEnumerable<Friend> friends, List<Friend> all) { if (friends?.Any() == true) { all.AddRange(friends); var sub = friends .Where(x=>x.Friends != null) .SelectMany(x => x.Friends); SelectAll(sub, all); } }
Дальше оптимизируйте под свои нужды. Избавляйтесь от рекурсии, от параметра all и тд
kenoma
Странно, из личного опыта могу сказать, что:
Удобно использовать dynamic на уровне маппинга в ORMах типа Dapper'а. Кроме этого при десериализации можно порой обойтись dynamic'ом если надо быстро что то накостылять, а дтошки нет.
Query expressions очень зашли в синтаксисе DSL фреймворка Sprache.
KAW Автор
Хорошее замечание.
Относительно Sprache, query expression как раз удобен из-за необходимости "выпрямления" запроса
umbarov
Иногда API сервиса может диктовать внешний сервис (в бизнесе всё возможно). В одном проекте для входных данных эндпоинта мне пришлось использовать свойства `dynamic From ... dynamic To ... dynamic Cc ...` отправитель(и) и получатель(и) почт, а потом полученные данные я маппил в массив строк. В c# пока нет discriminated unions, чтоб можно было сделать проще `string | string[]`. Вот так и иногда приходится использовать `dynamic`.
KAW Автор
Ну а как же наследование?