Отражение имени параметра: злоупотребление лямбда-выражениями C # или синтаксическая яркость?

428

Я смотрю на компонент MvcContrib Grid, и меня восхищает, но в то же время отталкивает синтаксический прием, используемый в синтаксисе Grid :

.Attributes(style => "width:100%")

Синтаксис выше устанавливает атрибут стиля сгенерированного HTML в width:100%. Теперь, если вы обратите внимание, «стиль» нигде не указан. Это выводится из имени параметра в выражении! Я должен был покопаться в этом и найти, где происходит «волшебство»:

Hash(params Func<object, TValue>[] hash)
{
    foreach (var func in hash)
    {
        Add(func.Method.GetParameters()[0].Name, func(null));
    }
}

Таким образом, код использует формальное время компиляции, имя параметров для создания словаря пар имя-значение атрибута. Полученная синтаксическая конструкция действительно очень выразительна, но в то же время очень опасна.

Общее использование лямбда-выражений позволяет заменять имена, используемые без побочных эффектов. Я вижу пример в книге, который говорит, что collection.ForEach(book => Fire.Burn(book))я знаю, что могу написать в своем коде, collection.ForEach(log => Fire.Burn(log))и это означает то же самое . Но с синтаксисом MvcContrib Grid здесь я неожиданно обнаружил код, который активно просматривает и принимает решения на основе имен, выбранных для моих переменных!

Так это обычная практика с сообществом C # 3.5 / 4.0 и любителями лямбда-выражений? Или мне не стоит беспокоиться о мошенниках?

Remus Rusanu
источник
34
Я бы сказал, что это выглядит очевидным, если вы хотите посмотреть на намерения кода, а не просто анализировать синтаксис. Что, если вы читаете хороший код, это то, что вы должны делать в любом случае. Синтаксис - это просто средство для намерения, и я бы сказал, что это код, раскрывающий намерение.
Джейми Пенни
191
Я только спросил Андерса (и остальную часть команды дизайнеров), что они думают. Скажем так: результаты не будут напечатаны в газете для семейного просмотра.
Эрик Липперт
22
В C # в настоящее время отсутствует чистый, легкий синтаксис для карт, особенно карт, передаваемых в функции. Различные динамические языки (Ruby, Python, JavaScript, ColdFusion 9) имеют чистый, легкий синтаксис для того или иного расширения.
yfeldblum
28
О, я думаю, это выглядит восхитительно. Что касается синтаксиса для карт, да, было бы здорово, если бы мы могли делать новые {{"Do", "A deer"}, {"Re", "golden sun"} ...} и иметь компилятор выводить построение карты, так же как new [] {1, 2,3} выводит построение массива int. Мы рассматриваем такие вещи.
Эрик Липперт
17
Эрик Липперт, я вас очень уважаю, но я думаю, что вы и группа (включая Андерса, которого я уважаю FREAKIN 'TREMENDOUSLY) критикуют это слишком резко. Как вы признаете, в C # отсутствует строгий синтаксис для карт, а некоторые другие языки (например, Ruby) имеют отличный. Этот парень нашел способ получить синтаксис, который он хотел. Я допускаю, что есть подобные способы выразить это, которые почти так же выразительны, как его синтаксис, с меньшим количеством недостатков. Но его синтаксис и тот факт, что он так усердно работал, чтобы получить его, ясно указывают на необходимость улучшения языка. Прошлые выступления показывают, что вы, ребята, создадите что-то отличное для этого
Чарли Флауэрс

Ответы:

147

Это имеет плохое взаимодействие. Например, рассмотрим этот пример C # - F #

C #:

public class Class1
{
    public static void Foo(Func<object, string> f)
    {
        Console.WriteLine(f.Method.GetParameters()[0].Name);
    }
}

F #:

Class1.Foo(fun yadda -> "hello")

Результат:

«arg» печатается (не «yadda»).

В результате разработчики библиотек должны либо избегать такого рода «злоупотреблений», либо, по крайней мере, предоставлять «стандартную» перегрузку (например, которая принимает имя строки в качестве дополнительного параметра), если они хотят иметь хорошее взаимодействие между языками .Net.

Брайан
источник
11
Вы не Эта стратегия просто непереносима. (Если это помогает, например, F # мог бы перегрузить методы, которые отличаются только типом возврата (это может сделать вывод типа). Это может быть выражено в CLR. Но F # запрещает это, главным образом потому, что если вы сделали это, эти API не будут вызываться из C #.) Когда дело доходит до взаимодействия, всегда есть компромиссы в «крайних» особенностях относительно того, какие преимущества вы получаете по сравнению с тем, какое взаимодействие вы торгуете.
Брайан
32
Я не люблю не совместимость как причину, чтобы не делать что-то. Если совместимость является требованием, тогда сделайте это, если нет, то зачем беспокоиться об этом? Это ЯГНИ ИМХО.
Джон Фаррелл
15
@jfar: В .NET CLR совместимость земель имеет совершенно новое измерение, поскольку сборки, генерируемые в любом компиляторе, должны использоваться из любого другого языка.
Ремус Русану
25
Я согласен с тем, что вам не обязательно быть CLS-совместимым, но, кажется, неплохо, если вы пишете библиотеку или элемент управления (а фрагмент, который начинает это с сетки, да?) В противном случае вы просто ограничиваете Ваша аудитория / клиентская база
JMarsch
4
Может быть, стоит изменить: Func <object, string> на Expression << Func <object, string >> И если вы ограничите правую часть выражения только константами, у вас может быть реализация, которая делает это: public static IDictionary <string, string> Hash (выражение params <Func <object, string >> [] hash) {Dictionary <string, string> values ​​= new Dictionary <string, string> (); foreach (var func in hash) {values ​​[func.Parameters [0] .Name] = (строка) ((ConstantExpression) func.Body) .Value; } возвращаемые значения; }
Дэвидфоул
154

Я нахожу это странным не столько из-за названия , сколько из-за того , что лямбда не нужна ; он может использовать анонимный тип и быть более гибким:

.Attributes(new { style = "width:100%", @class="foo", blip=123 });

Этот шаблон используется в большинстве ASP.NET MVC (например) и имеет другое применение ( предостережение , обратите внимание также на мысли Айенде, если имя является магическим значением, а не специфичным для вызывающего)

Марк Гравелл
источник
4
Это также имеет проблемы взаимодействия; не все языки поддерживают создание анонимного типа на лету.
Брайан
5
Мне нравится, что никто не ответил на этот вопрос, вместо этого люди приводят аргумент «это лучше». : p Это оскорбление или нет?
Сэм Шафран
7
Мне было бы интересно увидеть реакцию Эрика Липперта на это. Потому что это в рамочном коде . И это так же ужасно.
Мэтт Хинце
26
Я не думаю, что проблема заключается в читабельности после написания кода. Я думаю, что реальная проблема заключается в обучаемости кода. Что вы будете думать, когда ваш intellisense говорит. Атрибуты (объект obj) ? Вам нужно будет прочитать документацию (чего никто не хочет делать), потому что вы не будете знать, что передать методу. Я не думаю, что это действительно лучше, чем пример в вопросе.
NotDan
2
@Arnis - почему более гибкий: он не полагается на подразумеваемое имя параметра, что может (не приводить меня в кавычки) вызывать проблемы с некоторыми реализациями лямбды (другими языками) - но вы также можете использовать обычные объекты с определенными свойствами. Например, вы можете иметь HtmlAttributesкласс с ожидаемыми атрибутами (для intellisense) и просто игнорировать атрибуты со nullзначениями ...
Марк Грэвелл
137

Просто хотел скинуть по моему мнению (я автор компонента сетки MvcContrib).

Это определенно злоупотребление языком - нет сомнений. Однако я бы не стал считать это противоречащим интуиции - когда вы смотрите на вызов, Attributes(style => "width:100%", @class => "foo")
я думаю, что это довольно очевидно, что происходит (это, конечно, не хуже, чем подход с анонимным типом). С точки зрения интеллигенции, я согласен, что это довольно непрозрачно.

Для тех, кто заинтересован, некоторая справочная информация о его использовании в MvcContrib ...

Я добавил это в таблицу в качестве личного предпочтения - мне не нравится использование анонимных типов в качестве словарей (параметр, который принимает «объект», столь же непрозрачен, как параметр, принимающий параметры Func []), и инициализатор коллекции словаря скорее многословный (я также не фанат многословных беглых интерфейсов, например, необходимости связывать воедино множественные вызовы атрибута («style», «display: none»). Attribute («class», «foo») и т. д.)

Если бы C # имел менее подробный синтаксис для литералов словаря, то я бы не стал включать этот синтаксис в компонент сетки :)

Я также хочу отметить, что использование этого в MvcContrib совершенно необязательно - это методы расширения, которые обертывают перегрузки, которые вместо этого принимают IDictionary. Я думаю, что важно, чтобы, если вы предоставляете такой метод, вы также поддерживали более «нормальный» подход, например, для взаимодействия с другими языками.

Кроме того, кто-то упомянул «издержки на рефлексию», и я просто хотел отметить, что при таком подходе не так много служебной информации - не требуется никакого отражения во время выполнения или компиляции выражений (см. Http://blog.bittercoder.com /PermaLink,guid,206e64d1-29ae-4362-874b-83f5b103727f.aspx ).

Джереми Скиннер
источник
1
Я также попытался более подробно рассмотреть некоторые вопросы, поднятые здесь, в своем блоге: jeremyskinner.co.uk/2009/12/02/lambda-abuse-the-mvccontrib-hash
Джереми Скиннер,
4
Это не менее непрозрачно в Intellisense, чем анонимные объекты.
Брэд Уилсон
22
+1 за упоминание о том, что интерфейс добавляется с помощью дополнительных методов расширения. Пользователи C # (и все, кто обижен из-за нарушения языка) могут просто воздержаться от его использования.
Йорн Шоу-Роде
50

я бы предпочел

Attributes.Add(string name, string value);

Это намного более явно и стандартно, и ничего не получается с помощью лямбд.

NotDan
источник
20
Это правда? html.Attributes.Add("style", "width:100%");не читается так хорошо, как style = "width:100%"(фактический сгенерированный html), в то время style => "width:100%"как очень близко к тому, как это выглядит в результирующем HTML.
Джейми Пенни
6
Их синтаксис допускает такие хитрости, как .Attributes (id => 'foo', @class => 'bar', style => 'width: 100%'). Подпись функции использует синтаксис params для переменного числа аргументов: Атрибуты (params Func <объект, объект> [] аргументы). Это очень мощный, но мне потребовалось много времени, чтобы понять, что он делает.
Ремус Русану
27
@Jamie: Попытка сделать код C # похожим на HTML-код был бы плохой причиной для дизайнерских решений. Это совершенно разные языки для совершенно разных целей, и они не должны выглядеть одинаково.
Гуффа
17
С таким же успехом можно было бы использовать анонимный объект, не жертвуя «красотой»? .Attributes (new {id = "foo", @class = "bar", style = "width: 100%"}) ??
Funka
10
@Guffa Почему это плохая причина для дизайнерских решений? Почему они не должны выглядеть одинаково? По этой логике , если они намеренно выглядят по- разному? Я не говорю, что вы не правы, я просто говорю, что вы, возможно, захотите более подробно изложить свою точку зрения.
Саманта Бранхам
45

Добро пожаловать в Rails Land :)

В этом нет ничего плохого, если вы знаете, что происходит. (Это когда такая вещь плохо документирована, поэтому возникает проблема).

Вся структура Rails построена на идее соглашения по конфигурации. Обозначение вещей определенным образом вводит вас в соглашение, которое они используют, и вы получаете множество функциональных возможностей бесплатно. Следование соглашению об именах поможет вам двигаться быстрее. Все это работает блестяще.

Еще одно место, где я видел такой трюк, - это утверждения вызовов методов в Moq. Вы проходите в лямбду, но лямбда никогда не выполняется. Они просто используют выражение, чтобы убедиться, что вызов метода произошел, и выдают исключение, если нет.

Джейсон Пуньон
источник
3
Я немного колебался, но я согласен. Помимо издержек на отражение, нет существенной разницы между использованием строки, как в Add (), и использованием имени параметра лямбда-выражения. По крайней мере, я могу придумать. Вы можете испортить это и напечатать "sytle", не замечая обоих способов.
Саманта Бранхам
5
Я не мог понять, почему это не было странно для меня, а потом я вспомнил Rails. : D
Аллин
43

Это ужасно на нескольких уровнях. И нет, это не похоже на Ruby. Это злоупотребление C # и .NET.

Было много предложений о том, как сделать это более простым способом: кортежи, анонимные типы, свободный интерфейс и так далее.

То, что делает это настолько плохим, - то, что его просто воображают для его собственного блага:

  • Что происходит, когда вам нужно вызвать это из Visual Basic?

    .Attributes(Function(style) "width:100%")

  • Это абсолютно противоречит интуиции, и intellisense мало поможет в выяснении того, как передать материал.

  • Это излишне неэффективно.

  • Никто не будет иметь ни малейшего понятия, как его поддерживать.

  • Какой тип аргумента используется для атрибутов? это так Func<object,string>? Как это намерение раскрывается? Что ваша документация intellisense собирается сказать: «Пожалуйста, не обращайте внимания на все значения объекта»?

Я думаю, что вы полностью оправдываете чувство отвращения.

Сэм Шафран
источник
4
Я бы сказал - это полностью интуитивно понятно. :)
Арнис Лапса
4
Вы говорите, что это не что иное, как Руби. Но это ужасно чертовски похоже на синтаксис Ruby для определения ключей и значений хеш-таблицы.
Чарли Флауэрс
3
Код, который ломается при альфа-преобразовании! Yey!
Фил
3
@ Чарли, синтаксически это выглядит похоже, семантически это сильно отличается.
Сэм Шафран
39

Я нахожусь в лагере "синтаксического блеска", если они четко это документируют, и это выглядит чертовски круто, то с этим почти нет проблем, imo!

Blindy
источник
10
Аминь, брат. Аминь (2-й аминь требуется, чтобы встретить минимальную длину для комментариев :)
Чарли Флауэрс
Ваш комментарий был намного больше, чем нужно. Но тогда, вы не можете просто Аминь один раз, а затем добавить свой комментарий: D
Sleeper Smith
37

Оба из них. Это злоупотребление лямбда-выражениями И синтаксическим блеском.

Арнис Лапса
источник
16
Значит, это блестящее синтаксическое злоупотребление лямбда-выражениями? Я думаю, что я согласен :)
Сет Петри-Джонсон
21

Я почти никогда не сталкивался с такого рода использованием. Я думаю, что это "неуместно" :)

Это не обычный способ использования, он не согласуется с общими соглашениями. Этот вид синтаксиса имеет плюсы и минусы, конечно:

Cons

  • Код не интуитивно понятен (обычные соглашения отличаются)
  • Это имеет тенденцию быть хрупким (переименование параметра нарушит функциональность).
  • Это немного сложнее для тестирования (подделка API потребует использования отражения в тестах).
  • Если выражение используется интенсивно, оно будет медленнее из-за необходимости анализировать параметр, а не только значение (стоимость отражения)

Pros

  • Это становится более читабельным после того, как разработчик адаптируется к этому синтаксису.

Итог - в публичном дизайне API я бы выбрал более явный путь.

Елисей
источник
2
@Елиша - ваши плюсы и минусы меняются местами. По крайней мере, я надеюсь, что вы не говорите, что Pro имеет код "не интуитивно". ;-)
Метро Смурф
Для этого конкретного случая - лямбда-имя параметра и строковый параметр оба хрупки. Это похоже на использование динамического анализа для xml - это уместно, потому что вы все равно не можете быть уверены в xml.
Арнис Лапса
19

Нет, это, конечно, не обычная практика. Это нелогично, просто посмотреть на код, чтобы понять, что он делает, просто невозможно. Вы должны знать, как это используется, чтобы понять, как это используется.

Вместо предоставления атрибутов с использованием массива делегатов методы сцепления будут более понятными и будут работать лучше:

.Attribute("style", "width:100%;").Attribute("class", "test")

Хотя это немного больше, чтобы напечатать, это ясно и интуитивно понятно.

Guffa
источник
6
В самом деле? Я точно знал, для чего предназначен этот фрагмент кода, когда я смотрел на него. Это не так глупо, если вы не очень строги. Можно привести тот же аргумент о перегрузке + для конкатенации строк, и вместо этого мы всегда должны использовать метод Concat ().
Саманта Бранхам
4
@Stuart: Нет, вы точно не знали, вы просто догадались, основываясь на используемых значениях. Любой может сделать это предположение, но предположение не является хорошим способом понимания кода.
Гуффа
11
Я предполагаю, что использование .Attribute("style", "width:100%")дает мне style="width:100%", но, насколько я знаю, это может дать мне foooooo. Я не вижу разницы.
Саманта Бранхам
5
«Гадание на основе используемых значений» - ВСЕГДА, что вы делаете, когда смотрите на код. Если вы сталкиваетесь с вызовом stream.close (), вы предполагаете, что он закрывает поток, но он может сделать что-то совершенно другое.
Ваут Ливенс
1
@Wouter: Если вы ВСЕГДА гадаете при чтении кода, у вас должны быть большие трудности с чтением кода ... Если я вижу вызов метода с именем "close", я могу понять, что автор класса не знает о соглашениях об именах , поэтому я бы очень колебался воспринимать что-либо как должное в отношении метода.
Guffa
17

Могу ли я использовать это, чтобы написать фразу?

магическая лямбда (n): лямбда-функция, используемая исключительно для замены магической строки.

citizenmatt
источник
да ... это смешно и, может быть, это волшебно, в смысле безопасности во время компиляции, есть ли место, где это использование могло бы вызвать время выполнения вместо ошибок во время компиляции?
Маслоу
17

Что не так со следующим:

html.Attributes["style"] = "width:100%";
Томми Карли
источник
16

Все эти разговоры о "ужасности" - это куча давних ребят из c #, которые слишком остро реагируют (а я давний программист на C # и до сих пор очень большой поклонник языка). В этом синтаксисе нет ничего ужасного. Это просто попытка сделать синтаксис похожим на то, что вы пытаетесь выразить. Чем меньше «шума» в синтаксисе чего-либо, тем легче программисту это понять. Уменьшение шума в одной строке кода только немного помогает, но пусть это накапливается во все большем и большем количестве кода, и это оказывается существенным преимуществом.

Это попытка автора стремиться к тем же преимуществам, которые дает вам DSL - когда код просто «похож» на то, что вы пытаетесь сказать, вы достигли волшебного места. Вы можете обсудить, хорошо ли это для взаимодействия, или же оно более приятное, чем анонимные методы, чтобы оправдать некоторые затраты на «сложность». Достаточно справедливо ... поэтому в вашем проекте вы должны сделать правильный выбор, использовать ли этот тип синтаксиса. Но все же ... это умная попытка программиста сделать то, что в конце концов мы все пытаемся сделать (осознаем мы это или нет). И то, что мы все пытаемся сделать, это: «Скажите компьютеру, что мы хотим, чтобы он делал на языке, максимально приближенном к тому, как мы думаем о том, что мы хотим, чтобы он делал».

Становление ближе к тому, чтобы выразить наши инструкции компьютерам таким же образом, как мы думаем, является ключом к тому, чтобы сделать программное обеспечение более понятным и более точным.

РЕДАКТИРОВАТЬ: я сказал «ключ к созданию программного обеспечения более удобной и точной», что является наивно-завышенным преувеличением единорога. Я изменил его на «ключ».

Чарли Флауэрс
источник
12

Это одно из преимуществ деревьев выражений - можно проверить сам код на наличие дополнительной информации. Таким .Where(e => e.Name == "Jamie")образом, можно преобразовать в эквивалентное предложение SQL Where. Это умное использование деревьев выражений, хотя я надеюсь, что оно не пойдет дальше этого. Что-то более сложное, вероятно, будет сложнее, чем код, который он надеется заменить, поэтому я подозреваю, что это будет самоограничением.

Джейми Пенни
источник
Это верный аргумент, но правда в рекламе: LINQ поставляется с целым набором атрибутов, таких как TableAttribute и ColumnAttribute, которые делают это более законным делом. Также в linq mapping рассматриваются имена классов и имен свойств, которые, возможно, более стабильны, чем имена параметров.
Ремус Русану
Я согласен с вами там. Я немного изменил свою позицию по этому поводу после прочтения того, что по этому поводу должен был сказать Эрик Липперт / Андерс Хельсберг / и т.д. Думаю, я бы оставил этот ответ, поскольку он все еще несколько полезен. Что бы это ни стоило, я думаю, что этот стиль работы с HTML хорош, но он не соответствует языку.
Джейми Пенни
7

Это интересный подход. Если вы ограничите правую часть выражения только константами, то вы можете реализовать с помощью

Expression<Func<object, string>>

Я думаю, что вы действительно хотите вместо делегата (вы используете лямбду, чтобы получить имена обеих сторон). См. Наивную реализацию ниже:

public static IDictionary<string, string> Hash(params Expression<Func<object, string>>[] hash) {
    Dictionary<string, string> values = new Dictionary<string,string>();
    foreach (var func in hash) {
        values[func.Parameters[0].Name] = ((ConstantExpression)func.Body).Value.ToString();
    }
    return values;
}

Это может даже решить проблему межязыкового взаимодействия, о которой упоминалось ранее в теме.

оборота дфоулер
источник
6

Код очень умный, но потенциально он вызывает больше проблем, которые он решает.

Как вы указали, теперь существует неясная зависимость между именем параметра (стилем) и атрибутом HTML. Проверка времени компиляции не производится. Если имя параметра набрано неправильно, вероятно, на странице не будет сообщения об ошибке во время выполнения, но будет гораздо сложнее найти логическую ошибку (без ошибок, но некорректное поведение).

Лучшим решением было бы иметь элемент данных, который можно проверить во время компиляции. Итак, вместо этого:

.Attributes(style => "width:100%");

код со свойством Style может быть проверен компилятором:

.Attributes.Style = "width:100%";

или даже:

.Attributes.Style.Width.Percent = 100;

Это большая работа для авторов кода, но этот подход использует преимущества сильной проверки типов в C #, которая в первую очередь помогает предотвратить попадание ошибок в код.

Крис Р. Тиммонс
источник
3
Я ценю проверку во время компиляции, но я думаю, что это сводится к вопросу мнения. Возможно, что-то вроде new Attributes () {Style: "width: 100%"} привлечет больше людей к этому, так как это более кратко. Тем не менее, реализовать все, что позволяет HTML, - огромная работа, и я не могу винить кого-то за то, что он просто использует strings / lambdas / anonymous классы.
Саманта Бранхам
5

на самом деле это похоже на Ruby =), по крайней мере для меня использование статического ресурса для более позднего динамического «поиска» не подходит для соображений проектирования API, надеюсь, что этот умный прием не является обязательным в этом API.

Мы могли бы наследовать от IDictionary (или нет) и предоставить индексатор, который ведет себя как массив php, когда вам не нужно добавлять ключ для установки значения. Это будет правильное использование семантики .net, а не только c #, и все еще нуждается в документации.

надеюсь это поможет

Орасио Н. Хдез.
источник
4

На мой взгляд, это злоупотребление лямбдами.

Что касается блеска синтаксиса, то я нахожу это style=>"width:100%"довольно запутанным. Особенно из-за того, что =>вместо=

mfeingold
источник
4

ИМХО, это классный способ сделать это. Мы все любим тот факт, что присвоение класса Controller сделает его контроллером в MVC, верно? Так что есть случаи, когда наименование имеет значение.

Также намерение здесь очень ясно. Это очень легко понять, что .Attribute( book => "something")приведет book="something"и .Attribute( log => "something")приведет кlog="something"

Я думаю, это не должно быть проблемой, если вы относитесь к этому как к соглашению. Я считаю, что то, что заставляет вас писать меньше кода и делает намерение очевидным, - это хорошо.

madaboutcode
источник
4
Имена класса Controller не будут приседать, если вы тоже не наследуете от контроллера ...
Джордан Воллворк,
3

Если имена методов (функций) выбраны правильно, то это отличный способ избежать проблем с обслуживанием (т. Е. Добавить новую функцию, но забыл добавить ее в список отображения параметров функции). Конечно, вам нужно тщательно документировать это, и вам лучше будет автоматически сгенерировать документацию для параметров из документации для функций этого класса ...

Крейг Трейдер
источник
1

Я думаю, что это не лучше, чем "волшебные струны". Я не большой поклонник анонимных типов для этого. Это требует лучшего и строго типизированного подхода.

JamesK
источник