Comments 22
Странно, из личного опыта могу сказать, что:
Удобно использовать dynamic на уровне маппинга в ORMах типа Dapper'а. Кроме этого при десериализации можно порой обойтись dynamic'ом если надо быстро что то накостылять, а дтошки нет.
Query expressions очень зашли в синтаксисе DSL фреймворка Sprache.
Хорошее замечание.
Относительно Sprache, query expression как раз удобен из-за необходимости "выпрямления" запроса
Parser<string> identifier =
from leading in Parse.WhiteSpace.Many()
from first in Parse.Letter.Once().Text()
from rest in Parse.LetterOrDigit.Many().Text()
from trailing in Parse.WhiteSpace.Many()
select first + rest;
var id = identifier.Parse(" abc123 ");
Assert.AreEqual("abc123", id);
преимущество на совсем уж маленьких и одноразовых проектах.
Иногда API сервиса может диктовать внешний сервис (в бизнесе всё возможно). В одном проекте для входных данных эндпоинта мне пришлось использовать свойства `dynamic From ... dynamic To ... dynamic Cc ...` отправитель(и) и получатель(и) почт, а потом полученные данные я маппил в массив строк. В c# пока нет discriminated unions, чтоб можно было сделать проще `string | string[]`. Вот так и иногда приходится использовать `dynamic`.
А по существу:
1. Никогда не любил смешивать SQL-like syntax и ЯП. Данный же конкретный пример наводит на мысль, что что-то неправильно спроектировано. И очень хорошо, что это заметно на уровне языка, гораздо хуже, когда язык это скрывает.
2.
так как комбинация статической и динамической типизации могла быть невероятно мощным инструментом
ИМХО, очень вряд ли это могло бы случиться в C#. Мне больше всего нравится, как комбинация статики/динамики сделана, например, в TIS. С одной стороны — типов нет в принципе (при описании), мысль разработчика ничто не сдерживает, с другой — шаг влево/вправо,
5 + '3'
— сразу летят exception'ы, которые отлично заменяют компиляцию в плане безопасности. Это, конечно, много где сделано, можно хоть тот же OLE_VARIANT вспомнить, но без глубочайшей поддержки на уровне языка / стандартной библиотеки эффективности не добиться, а генетика у Шарпа для этой цели не та.Из моей практики, на одном проекте активно использовались dynamic типы для работы с JSON -ами, приходящими из базы. С одной стороны, не надо делать массу DTO- шек, и если структура документа в базе со временем меняется, то вроде бы как код продолжает работать без необходимости поддерживать сущности v1, v2, vN+1... Но преимущества мнимые, т. к. все начинает падать в рантайме, принося в .Net все "прелести" языков без строгой типизации. Бррр, до сих передергивает, как вспоминаю.
Другой случай, где я применял dynamic, на этот раз самостоятельно, заключался в том, что внешняя библиотека (OpenXML, но это не важно) предоставляла несколько почти идентичных классов, с одинаковым набором полей, но в разных пространствах имён. Никаких общих интерфейсов или предков они не имели. Расширить внешнюю библиотеку тоже нельзя. И нужно было написать конвертацию из обоих типов в наш внутренний, по возможности, избежав копипасты. Для этого у меня было два internal метода, принимающих аргументы из разных пространств имён, и перенаправляющих вызовы в private метод, с dynamic аргументом. Чтобы это все не отвалилось при апгрейде OpenXML, код был плотненько покрыт тестами. И долгое время для меня это был единственный более-менее оправданного применения dynamic на практике, но и то потом оказалось, что это вызывало проблемы у пользователей, работающих в sandbox environment-ах (подробностей не помню, если кому будет интересно, найду соответствующий тикет на гитхабе). Так что и от такого применения пришлось отказаться.
А в защиту query syntax скажу, что тоже не понимал, для чего им вообще пользоваться, пока не столкнулся с запросом, в котором цепочка джойнов добавляла по одному новому полю к анонимному объекту. С query- синтаксисом такой запрос оказался в несколько раз короче, и читается легче.
Немного из другой серии, но тоже из разряда бесполезного - это возможность иметь internal abstract член в публичном классе. Если унаследоваться из внешней сборки, переопределять этот член нельзя, т.к. он internal, но не переопределять тоже нельзя, т. к. он абстрактный. Такой вот курьез. Вроде и не баг, но и практической пользы ноль. Будет интересно, если кто-то придумает юзкейс, в котором это применимо.
Ещё одна вещь, которую вряд ли придумали бы в такой форме, если придумывали сразу, с нуля. "protected internal" является не взаимоусиливающей комбинацией "protected" и "internal", а действует как "или": чтобы получить доступ к члену надо быть наследником ИЛИ (не И) находиться в той же сборке (ср. с private static, например - на него одновременно распространяются ограничения private и static, усиливая друг друга). Зато логическое И выражается в виде модификатора "private protected", хотя от private в нём вообще ничего. Мне кажется, если бы не обратная совместимость, то "protected internal" должен был бы стать тем, что сейчас называется "private protected", а нынешний protected internal (с логическим ИЛИ) должен быть упразднен: если уж член protected, то ему обычно незачем быть публичным внутри своей сборки.
Немного из другой серии, но тоже из разряда бесполезного — это возможность иметь internal abstract член в публичном классе. Если унаследоваться из внешней сборки, переопределять этот член нельзя, т.к. он internal, но не переопределять тоже нельзя, т. к. он абстрактный. Такой вот курьез. Вроде и не баг, но и практической пользы ноль. Будет интересно, если кто-то придумает юзкейс, в котором это применимо.
Практическая польза тут — в возможности создания закрытых иерархий классов, которые невозможно расширить в другой сборке. Пример из Windows Forms: Image — Bitmap, Metafile. Эти классы являются обёртками над объектами GDI, и новых типов объектов в этой иерархии не предусмотрено.
Фактически, таким образом можно делать ООПшные аналоги Union-типов из ФП.
Странный аргумент в пользу 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);
dynamic хорош для двойной диспетчеризации. В частности, паттерн Visitor с его помощью выражается намного проще.
Query expression с применением let сильно упрощает сложные запросы. В синтаксисе методов придётся протаскивать сложный анонимный тип.
Например, в библиотеке lin2d используется такая запись. Переписать на синтаксис методов, конечно, можно, но выглядеть будет жутко.
Вот жеж ахинея. Чуть позже разверну, если будет в этом смысл, после прочтения каментов...
Делегат это объявление сигнатуры функции. Его для этого и используют. И экспортируют. А не внутри функций. Как описать 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)
Ну смысл думаю понятен. Нельзя подобные конструкции яснее написать...В строчку, с трансляцией переменных.
Ну а в целом примеры какието не жизненные. Придуманные из пальца для статьи.
Я-то ладно, уже давно на с# пишу — уже знаю хорошие практики, но джуны у меня регулярно косячат на event'ах. Приходится буквально семинары им устраивать с доской и партами.
Попробуйте написать аналог ...
Ну есть всякие SelectMany. Да и вообще иногда интересно декомпильнуть и посмотреть что именно скрывается за сахаром (Всякие Where() и лямбды — тоже сахар, кстати).
Добавьте C# в тэги, пожалуйста
Вот простой пример, обходите в глубину сколько влезет. Без убогих конструкций.
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 и тд
Полезные бесполезные фичи C#