Refletindo o nome do parâmetro: abuso de expressões lambda em C # ou brilho de sintaxe?


428

Estou olhando para o componente MvcContrib Grid e estou fascinado, mas ao mesmo tempo repelido, por um truque sintático usado na sintaxe Grid :

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

A sintaxe acima define o atributo de estilo do HTML gerado para width:100%. Agora, se você prestar atenção, 'estilo' não é especificado em nenhum lugar. É deduzido do nome do parâmetro na expressão! Eu tive que cavar isso e descobri onde a 'mágica' acontece:

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

Portanto, de fato, o código está usando o nome formal dos parâmetros em tempo de compilação para criar o dicionário de pares nome-valor do atributo. A construção da sintaxe resultante é realmente muito expressiva, mas ao mesmo tempo muito perigosa.

O uso geral de expressões lambda permite a substituição dos nomes usados ​​sem efeito colateral. Vejo um exemplo em um livro que diz collection.ForEach(book => Fire.Burn(book))que sei que posso escrever no meu código collection.ForEach(log => Fire.Burn(log))e que significa a mesma coisa . Mas com a sintaxe MvcContrib Grid aqui, de repente, encontro código que olha ativamente e toma decisões com base nos nomes que escolho para minhas variáveis!

Então, essa prática comum é com a comunidade C # 3.5 / 4.0 e os amantes de expressões lambda? Ou é um especialista em truques desonestos com o qual não devo me preocupar?


34
Eu argumentaria que isso parece óbvio, desde que você esteja disposto a analisar a intenção do código, em vez de apenas analisar a sintaxe. Qual, se você estiver lendo um bom código, é o que deveria estar fazendo de qualquer maneira. A sintaxe é apenas um veículo para a intenção, e eu argumentaria que esse é um código que revela a intenção.
11139 Jamie Penney

191
Eu apenas perguntei a Anders (e ao restante da equipe de design) o que eles pensavam. Digamos apenas que os resultados não seriam imprimíveis em um jornal familiar.
11139 Eric Lippert

22
No momento, o C # está faltando uma sintaxe clara e clara para mapas, especialmente os mapas passados ​​para as funções. Várias linguagens dinâmicas (Ruby, Python, JavaScript, ColdFusion 9) têm uma sintaxe clara e clara para isso, de certa forma ou de outra.
Yfeldblum 12/11/2009

28
Oh, acho que parece delicioso. Quanto a uma sintaxe para mapas, sim, seria ótimo se pudéssemos fazer novos {{"Do", "A deer"}, {"Re", "golden sun"} ...} e fazer com que o compilador inferisse o construção de um mapa, assim como new [] {1, 2,3} infere a construção de uma matriz int. Estamos considerando esse tipo de coisa.
Eric Lippert

17
Eric Lippert, eu respeito muito você, mas acho que você e o grupo (incluindo Anders, que eu respeito FREAKIN 'TREMENDOUSLY) estão batendo muito forte nisso. Como você admite, o C # não possui uma sintaxe rígida para mapas, e alguns outros idiomas (como Ruby) têm ótimos. Esse cara encontrou uma maneira de obter a sintaxe que queria. Concordo que existem maneiras semelhantes de expressá-lo que são quase tão expressivas quanto a sintaxe dele com menos desvantagens. Mas sua sintaxe e o fato de ele ter trabalhado tanto para obtê-lo mostram claramente a necessidade de aprimoramento de idioma. O desempenho passado indica que vocês criarão algo ótimo para isso.
Charlie Flowers

Respostas:


147

Isso tem interoperabilidade ruim. Por exemplo, considere este exemplo de 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")

Resultado:

"arg" é impresso (não "yadda").

Como resultado, os projetistas de bibliotecas devem evitar esses tipos de 'abusos' ou, pelo menos, fornecer uma sobrecarga 'padrão' (por exemplo, que leva o nome da string como um parâmetro extra) se quiserem ter uma boa interoperabilidade nas linguagens .Net.


11
Você não Essa estratégia é simplesmente não portátil. (Caso ajude, como por exemplo, o F # poderia sobrecarregar métodos que diferem apenas no tipo de retorno (a inferência de tipo pode fazer isso). Isso pode ser expresso no CLR. Mas o F # o desaprova, principalmente porque se você fez isso, essas APIs não poderiam ser chamadas a partir do C #.) Quando se trata de interoperabilidade, sempre existem vantagens e desvantagens nos recursos de ponta em relação a quais benefícios você obtém versus qual interoperabilidade você troca.
Brian

32
Não gosto da não interoperabilidade como razão para não fazer algo. Se a interoperabilidade é um requisito, faça-o, se não, por que se preocupar com isso? Este é YAGNI IMHO.
31410 John Farrell

15
@ jfar: No .NET CLR, a interoperabilidade terrestre tem uma dimensão totalmente nova, pois os assemblies gerados em qualquer compilador devem ser consumidos de qualquer outro idioma.
Remus Rusanu

25
Concordo que você não precisa ser compatível com CLS, mas parece uma boa ideia se você estiver escrevendo uma biblioteca ou controle (e o trecho que inicia isso é de uma grade, não é?) Caso contrário, você está limitando sua audiência / base de clientes
JMarsch 11/11/2009

4
Talvez valha a pena mudar o: Func <objeto, string> para Expressão << Func <objeto, string >> E se você restringir o lado direito da expressão a ser apenas constante, poderá ter uma implementação que faça isso: public static IDictionary <string, string> Hash (params Expressão <Func <objeto, string >> [] hash) {Dicionário <string, string> valores = novo Dicionário <string, string> (); foreach (var func em hash) {values ​​[func.Parameters [0] .Name] = (string) ((ConstantExpression) func.Body) .Value; } retornar valores; }
davidfowl 03/12/2009

154

Acho isso estranho, não tanto por causa do nome , mas porque o lambda é desnecessário ; poderia usar um tipo anônimo e ser mais flexível:

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

Esse é um padrão usado em grande parte do ASP.NET MVC (por exemplo) e tem outros usos (uma advertência , observe também os pensamentos de Ayende se o nome for um valor mágico em vez de específico para o chamador)


4
Isso também tem problemas de interoperabilidade; nem todos os idiomas suportam a criação de um tipo anônimo como esse em tempo real.
Brian

5
Eu amo como ninguém realmente respondeu à pergunta, em vez disso, as pessoas fornecem um argumento "isso é melhor". : p É um abuso ou não?
Sam Saffron

7
Eu estaria interessado em ver a reação de Eric Lippert a isso. Porque está no código FRAMEWORK . E é tão horrível.
Matt Hinze

26
Não acho que o problema seja legível depois que o código foi escrito. Eu acho que o problema real é a capacidade de aprender o código. O que você vai pensar quando o seu intellisense disser .Attributes (object obj) ? Você precisará ler a documentação (que ninguém quer fazer) porque não saberá o que passar para o método. Eu não acho que isso seja realmente melhor do que o exemplo da pergunta.
NotDan

2
@Arnis - por que é mais flexível: não depende do nome do parâmetro implícito, o que pode (não me citar) causar problemas em algumas implementações lambda (outras linguagens) - mas você também pode usar objetos regulares com propriedades definidas. Por exemplo, você poderia ter uma HtmlAttributesclasse com os atributos esperados (para intellisense), e simplesmente ignorar aqueles com nullvalores ...
Marc Gravell

137

Só queria jogar na minha opinião (eu sou o autor do componente de grade MvcContrib).

Definitivamente, isso é abuso de linguagem - não há dúvida sobre isso. No entanto, eu realmente não consideraria isso contra-intuitivo - quando você olha para uma ligação Attributes(style => "width:100%", @class => "foo")
, acho bastante óbvio o que está acontecendo (certamente não é pior do que a abordagem de tipo anônimo). Do ponto de vista intelectual, eu concordo que é bastante opaco.

Para os interessados, algumas informações básicas sobre seu uso no MvcContrib ...

Adicionei isso à grade como uma preferência pessoal - não gosto do uso de tipos anônimos como dicionários (ter um parâmetro que aceite "objeto" seja tão opaco quanto um que use os parâmetros Func []) e o inicializador de coleção de Dicionário seja bastante detalhado (eu também não sou fã de interfaces fluentes verbais, por exemplo, ter que encadear várias chamadas para um Atributo ("estilo", "exibição: nenhuma"). Atributo ("classe", "foo") etc.)

Se o C # tivesse uma sintaxe menos detalhada para literais do dicionário, não teria me incomodado em incluir essa sintaxe no componente de grade :)

Também quero ressaltar que o uso disso no MvcContrib é completamente opcional - esses são métodos de extensão que envolvem sobrecargas que usam um IDictionary. Eu acho importante que, se você fornecer um método como esse, também ofereça suporte a uma abordagem mais "normal", por exemplo, para interoperabilidade com outros idiomas.

Além disso, alguém mencionou a 'sobrecarga de reflexão' e eu só queria ressaltar que realmente não há muita sobrecarga nessa abordagem - não há reflexão em tempo de execução ou compilação de expressão envolvida (consulte http://blog.bittercoder.com /PermaLink,guid,206e64d1-29ae-4362-874b-83f5b103727f.aspx ).


1
Eu também tentei abordar algumas das questões aqui levantadas em mais alguma profundidade no meu blog: jeremyskinner.co.uk/2009/12/02/lambda-abuse-the-mvccontrib-hash
Jeremy Skinner

4
Não é menos opaco no Intellisense do que objetos anônimos.
Brad Wilson

22
+1 por mencionar que a interface foi adicionada por métodos de extensão opcionais . Usuários não C # (e qualquer pessoa ofendida pelo abuso de idioma) podem simplesmente se abster de usá-lo.
Jørn Schou-Rode

50

eu preferiria

Attributes.Add(string name, string value);

É muito mais explícito e padrão e nada está sendo ganho com o uso de lambdas.


20
É mesmo? html.Attributes.Add("style", "width:100%");não lê tão bem quanto style = "width:100%"(o html real gerado), enquanto style => "width:100%"está muito próximo da aparência no HTML resultante.
11139 Jamie Penney

6
Sua sintaxe permite truques como .Attributes (id => 'foo', @class => 'bar', style => 'width: 100%'). A assinatura da função usa a sintaxe params para o número variável de args: Atributos (params Func <objeto, objeto> [] args). É muito poderoso, mas levei um bom tempo para entender o que ele faz.
Remus Rusanu

27
@ Jamie: Tentar tornar o código C # parecido com o código HTML seria um mau motivo para as decisões de design. Eles são idiomas completamente diferentes para propósitos completamente diferentes e não devem ter a mesma aparência.
11289 Guffa

17
Um objeto anônimo poderia muito bem ter sido usado, sem sacrificar a "beleza"? . Atributos (novo {id = "foo", @class = "bar", estilo = "width: 100%"}) ??
Funka

10
@Guffa Por que seria um mau motivo para decisões de design? Por que eles não deveriam ter a mesma aparência? Por esse raciocínio, eles deveriam intencionalmente parecer diferentes? Não estou dizendo o que você está errado, apenas estou dizendo que você pode querer elaborar mais completamente o seu ponto.
Samantha Branham

45

Bem-vindo ao Rails Land :)

Não há realmente nada de errado com isso, desde que você saiba o que está acontecendo. (É quando esse tipo de coisa não está bem documentado que existe um problema).

A totalidade da estrutura do Rails é construída sobre a idéia de convenção sobre configuração. Nomear as coisas de uma certa maneira leva você a uma convenção que eles estão usando e você obtém muitas funcionalidades gratuitamente. Seguir a convenção de nomenclatura leva você aonde você está indo mais rápido. A coisa toda funciona de maneira brilhante.

Outro lugar onde eu já vi um truque como esse é nas asserções de chamada de método no Moq. Você passa em um lambda, mas o lambda nunca é executado. Eles apenas usam a expressão para se certificar de que a chamada do método ocorreu e, se não, gera uma exceção.


3
Eu estava um pouco hesitante, mas concordo. Além da sobrecarga de reflexão, não há diferença significativa entre o uso de uma string como em Add () vs. o uso de um nome de parâmetro lambda. Pelo menos em que consigo pensar. Você pode estragar tudo e digitar "sytle" sem perceber os dois lados.
Samantha Branham

5
Eu não conseguia descobrir por que isso não era estranho para mim, e então me lembrei do Rails. : D
Allyn

43

Isso é horrível em mais de um nível. E não, isso não é nada como Ruby. É um abuso de C # e .NET.

Houve muitas sugestões de como fazer isso de maneira mais direta: tuplas, tipos anônimos, uma interface fluente e assim por diante.

O que o torna tão ruim é que é apenas uma maneira de gostar do seu próprio bem:

  • O que acontece quando você precisa chamar isso do Visual Basic?

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

  • É completamente contra-intuitivo, e o intellisense fornecerá pouca ajuda para descobrir como distribuir coisas.

  • É desnecessariamente ineficiente.

  • Ninguém terá idéia de como mantê-lo.

  • Qual é o tipo de argumento usado nos atributos? é isso Func<object,string>? Como essa intenção é reveladora? O que sua documentação do intellisense vai dizer: "Por favor, desconsidere todos os valores do objeto"?

Eu acho que você está completamente justificado por ter esses sentimentos de repulsa.


4
Eu diria - é completamente intuitivo. :)
Arnis Lapsa 02/12/2009

4
Você diz que não é nada como Ruby. Mas é muito parecido com a sintaxe de Ruby para especificar as chaves e os valores de uma hashtable.
Charlie Flowers

3
Código que quebra sob conversão alfa! Yey!
Phil

3
@ Charlie, sintaticamente parece semelhante, semanticamente é muito diferente.
Sam Saffron

39

Eu estou no campo "brilho da sintaxe", se eles documentarem isso claramente, e parece legal demais, quase não há problema com isso imo!


10
Amém, irmão. Amen (2ª amen necessário para min comprimento se encontram para comentários :)
Charlie Flowers

Seu comentário por si só foi muito mais do que o necessário. Mas então, você não pode simplesmente Amém uma vez e depois colocar seu comentário: D
Sleeper Smith


21

Quase nunca me deparei com esse tipo de uso. Eu acho que é "inadequado" :)

Esta não é uma maneira comum de uso, é inconsistente com as convenções gerais. Esse tipo de sintaxe possui prós e contras, é claro:

Contras

  • O código não é intuitivo (as convenções usuais são diferentes)
  • Tende a ser frágil (a renomeação do parâmetro interromperá a funcionalidade).
  • É um pouco mais difícil de testar (falsificar a API exigirá o uso de reflexão nos testes).
  • Se a expressão for usada intensivamente, será mais lenta devido à necessidade de analisar o parâmetro e não apenas o valor (custo de reflexão)

Prós

  • É mais legível depois que o desenvolvedor se ajustou a essa sintaxe.

Bottom line - no design da API pública, eu teria escolhido uma maneira mais explícita.


2
@Elisha - seus prós e contras são revertidos. Pelo menos, espero que você não esteja dizendo que um profissional está tendo um código "não intuitivo". ;-)
Metro Smurf

Para este caso em particular - o nome do parâmetro lambda e o parâmetro string são frágeis. É como usar dinâmico para análise de xml - é apropriado porque você não pode ter certeza sobre o xml de qualquer maneira.
Arnis Lapsa 02/12/2009

19

Não, certamente não é uma prática comum. É contra-intuitivo, não há como apenas olhar o código para descobrir o que ele faz. Você precisa saber como é usado para entender como é usado.

Em vez de fornecer atributos usando uma matriz de delegados, os métodos de encadeamento seriam mais claros e apresentariam melhor desempenho:

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

Embora isso seja um pouco mais para digitar, é claro e intuitivo.


6
Mesmo? Eu sabia exatamente o que aquele trecho de código pretendia quando o examinei. Não é tão obtuso, a menos que você seja muito rigoroso. Alguém poderia dar o mesmo argumento sobre sobrecarregar + para concatenação de strings, e que deveríamos sempre usar um método Concat ().
Samantha Branham

4
@Stuart: Não, você não sabia exatamente, estava apenas adivinhando com base nos valores usados. Qualquer um pode adivinhar, mas adivinhar não é uma boa maneira de entender o código.
Guffa

11
Eu acho que usar .Attribute("style", "width:100%")me dá style="width:100%", mas pelo que sei, isso pode me dar foooooo. Não estou vendo a diferença.
Samantha Branham

5
"Adivinhar com base nos valores usados" é SEMPRE o que você faz quando olha para o código. Se você encontrar uma chamada para stream.close (), presume que ele fecha um fluxo, mas pode fazer algo completamente diferente.
Wouter Lievens

1
@ Wouter: Se você está sempre adivinhando ao ler o código, deve ter grandes dificuldades em ler o código ... Se eu vir uma chamada para um método chamado "close", posso concluir que o autor da classe não sabe sobre convenções de nomenclatura , portanto, eu hesitaria em aceitar algo como garantido sobre o que o método faz.
Guffa 02/12/2009

17

Posso usar isso para cunhar uma frase?

lambda mágica (n): uma função lambda usada apenas com o objetivo de substituir uma sequência mágica.


sim ... isso é engraçado. e talvez seja meio mágico, no sentido de não haver segurança no tempo de compilação, existe um lugar onde esse uso causaria tempo de execução em vez de erros no tempo de compilação?
Maslow


16

Todo esse discurso retórico sobre "horror" é um monte de caras c # de longa data exagerando (e eu sou um programador de C # de longa data e ainda sou um grande fã da linguagem). Não há nada de horrível nessa sintaxe. É apenas uma tentativa de fazer a sintaxe parecer mais com o que você está tentando expressar. Quanto menos "ruído" houver na sintaxe para algo, mais fácil o programador poderá entender. Diminuir o ruído em uma linha de código ajuda apenas um pouco, mas deixe que isso se acumule em cada vez mais códigos, e isso acaba sendo um benefício substancial.

Esta é uma tentativa do autor de buscar os mesmos benefícios que as DSLs oferecem a você - quando o código simplesmente "se parece" com o que você está tentando dizer, você alcançou um lugar mágico. Você pode discutir se isso é bom para interoperabilidade ou se é suficiente mais agradável que métodos anônimos para justificar parte do custo da "complexidade". É justo o suficiente ... portanto, em seu projeto, você deve fazer a escolha certa sobre usar esse tipo de sintaxe. Mas ainda assim ... esta é uma tentativa inteligente de um programador de fazer o que, no final do dia, todos estamos tentando fazer (se percebemos ou não). E o que estamos tentando fazer é o seguinte: "Diga ao computador o que queremos que ele faça em uma linguagem o mais próxima possível de como pensamos sobre o que ele quer que seja".

Aproximar-se de expressar nossas instruções para computadores da mesma maneira que pensamos internamente é a chave para tornar o software mais sustentável e mais preciso.

EDIT: Eu disse "a chave para tornar o software mais sustentável e mais preciso", que é um pouco exagerado de unicórnio. Eu mudei para "uma chave".


12

Esse é um dos benefícios das árvores de expressão - é possível examinar o próprio código para obter informações adicionais. É assim que .Where(e => e.Name == "Jamie")pode ser convertido na cláusula SQL Where equivalente. Este é um uso inteligente de árvores de expressão, embora eu esperasse que não fosse além disso. Qualquer coisa mais complexa provavelmente será mais difícil do que o código que ela espera substituir, então eu suspeito que seja auto-limitante.


É um ponto válido, mas verdade na publicidade: o LINQ vem com um conjunto inteiro de atributos, como TableAttribute e ColumnAttribute, que tornam esse assunto mais legítimo. Além disso, o mapeamento linq analisa os nomes de classes e nomes de propriedades, que são sem dúvida mais estáveis ​​que os nomes de parâmetros.
Remus Rusanu

Eu concordo com você lá. Eu mudei minha posição sobre isso um pouco depois de ler o que Eric Lippert / Anders Helsberg / etc tinha a dizer sobre o assunto. Pensei em deixar essa resposta em aberto, pois ainda é um pouco útil. Pelo que vale a pena, agora acho que esse estilo de trabalhar com HTML é bom, mas não se encaixa na linguagem.
21330 Jamie Penney

7

É uma abordagem interessante. Se você restringisse o lado direito da expressão apenas a constantes, poderia implementar usando

Expression<Func<object, string>>

O que eu acho que é o que você realmente deseja, em vez do delegado (você está usando o lambda para obter nomes de ambos os lados). Veja a implementação ingênua abaixo:

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;
}

Isso pode até resolver a preocupação de interoperabilidade entre idiomas mencionada anteriormente no encadeamento.


6

O código é muito inteligente, mas potencialmente causa mais problemas que resolve.

Como você apontou, agora existe uma dependência obscura entre o nome do parâmetro (estilo) e um atributo HTML. Nenhuma verificação do tempo de compilação é feita. Se o nome do parâmetro estiver digitado incorretamente, a página provavelmente não terá uma mensagem de erro em tempo de execução, mas será muito mais difícil encontrar um erro lógico (sem erros, mas com comportamento incorreto).

Uma solução melhor seria ter um membro de dados que possa ser verificado em tempo de compilação. Então, em vez disso:

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

O código com uma propriedade Style pode ser verificado pelo compilador:

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

ou até:

.Attributes.Style.Width.Percent = 100;

Isso é mais trabalhoso para os autores do código, mas essa abordagem tira proveito da forte capacidade de verificação de tipo do C #, que ajuda a impedir que erros entrem no código em primeiro lugar.


3
Aprecio a verificação em tempo de compilação, mas acho que isso se resume a uma questão de opinião. Talvez algo como o novo Attributes () {Style: "width: 100%"} ganhe mais pessoas, já que é mais conciso. Ainda assim, implementar tudo o que o HTML permite é um trabalho enorme e não posso culpar alguém por usar apenas strings / lambdas / classes anônimas.
Samantha Branham

5

de fato, parece Ruby =), pelo menos para mim o uso de um recurso estático para uma "pesquisa" dinâmica posterior não se encaixa nas considerações de design da API, espero que esse truque inteligente seja opcional nessa API.

Poderíamos herdar do IDictionary (ou não) e fornecer um indexador que se comporte como uma matriz php quando você não precisar adicionar uma chave para definir um valor. Será um uso válido da semântica .net e não apenas c #, e ainda precisará de documentação.

espero que isto ajude


4

Na minha opinião, é abuso das lambdas.

Quanto ao brilho da sintaxe, acho style=>"width:100%"bastante confuso. Particularmente por causa do em =>vez de=


4

IMHO, é uma maneira legal de fazê-lo. Todos nós gostamos do fato de nomear um controlador de classe para torná-lo um controlador no MVC, certo? Portanto, há casos em que a nomeação é importante.

Também a intenção é muito clara aqui. É muito fácil entender que .Attribute( book => "something")resultará book="something"e.Attribute( log => "something") resultará emlog="something"

Eu acho que não deve ser um problema se você o tratar como uma convenção. Sou da opinião de que tudo o que faz com que você escreva menos código e torne a intenção óbvia é uma coisa boa.


4
Nomear uma classe de controlador não vai fazer agachamento embora se você não herdar de controlador também ...
Jordan Wallwork

3

Se os nomes dos métodos (func) forem bem escolhidos, essa é uma maneira brilhante de evitar dores de cabeça na manutenção (por exemplo: adicione uma nova função, mas esqueça de adicioná-la à lista de mapeamento de parâmetros de função). Obviamente, é necessário documentá-lo muito e é melhor gerar automaticamente a documentação para os parâmetros, a partir da documentação para as funções nessa classe ...


1

Eu acho que isso não é melhor do que "cordas mágicas". Também não sou muito fã dos tipos anônimos. Precisa de uma abordagem melhor e fortemente tipada.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.