Especifique nomes de parâmetros opcionais, mesmo que não sejam necessários?

25

Considere o seguinte método:

public List<Guid> ReturnEmployeeIds(bool includeManagement = false)
{

}

E a seguinte chamada:

var ids = ReturnEmployeeIds(true);

Para um desenvolvedor iniciante no sistema, seria muito difícil adivinhar o que truefez. A primeira coisa a fazer é passar o mouse sobre o nome do método ou ir para a definição (nenhuma das quais são grandes tarefas, no mínimo). Mas, para facilitar a leitura, faz sentido escrever:

var ids = ReturnEmployeeIds(includeManagement: true);

Existe algum lugar que, formalmente, discuta se é necessário especificar explicitamente parâmetros opcionais quando o compilador não precisa de você?

O seguinte artigo discute certas convenções de codificação: https://msdn.microsoft.com/en-gb/library/ff926074.aspx

Algo semelhante ao artigo acima seria ótimo.

JᴀʏMᴇᴇ
fonte
2
Estas são duas perguntas não relacionadas: (1) Você deve escrever argumentos opcionais para maior clareza? (2) Deve-se usar booleanargumentos de método e como?
precisa saber é o seguinte
5
Tudo isso se resume à preferência / estilo / opinião pessoal. Pessoalmente, gosto de nomes de parâmetros quando o valor passado é literal (uma variável já pode transmitir informações suficientes por meio de seu nome). Mas essa é minha opinião pessoal.
MetaFight
1
talvez inclua esse link como uma fonte de exemplo na sua pergunta?
MetaFight
4
Se ele acrescenta algo, eu o executei no StyleCop & ReSharper - nenhum deles tinha uma visão clara. O ReSharper simplesmente diz que o parâmetro nomeado pode ser removido e continua dizendo que um parâmetro nomeado pode ser adicionado. O conselho é gratuito por uma razão, eu acho ...: - /
Robbie Dee

Respostas:

28

Eu diria que no mundo C #, uma enumeração seria uma das melhores opções aqui.

Com isso, você seria forçado a explicar o que está fazendo e qualquer usuário verá o que está acontecendo. Também é seguro para futuras extensões;

public enum WhatToInclude
{
    IncludeAll,
    ExcludeManagement
}

var ids = ReturnEmployeeIds(WhatToInclude.ExcludeManagement);

Então, na minha opinião aqui:

enum> enum opcional> bool opcional

Edit: Devido à discussão com LeopardSkinPillBoxHat abaixo sobre uma [Flags]enumeração que, nesse caso específico, pode ser uma boa opção (já que estamos falando especificamente sobre incluir / excluir coisas), proponho usar ISet<WhatToInclude>como parâmetro.

É um conceito mais "moderno", com várias vantagens, principalmente devido ao fato de ele se encaixar na família de coleções LINQ, mas também [Flags]com um máximo de 32 grupos. A principal desvantagem do uso ISet<WhatToInclude>é o quão mal os conjuntos são suportados na sintaxe C #:

var ids = ReturnEmployeeIds(
    new HashSet<WhatToInclude>(new []{ 
        WhatToInclude.Cleaners, 
        WhatToInclude.Managers});

Algumas delas podem ser atenuadas por funções auxiliares, tanto para gerar o conjunto quanto para criar "conjuntos de atalhos", como

var ids = ReturnEmployeeIds(WhatToIncludeHelper.IncludeAll)
NiklasJ
fonte
Eu tendem a concordar; e geralmente tenho usado enums no meu novo código, se houver alguma chance vaga de que as condições sejam expandidas no futuro. Substituir um bool por um enum quando algo se torna um estado tri acaba criando diferenças realmente barulhentas e tornando o código de culpa muito mais tedioso; quando você precisar adaptar seu código novamente um ano depois para uma terceira opção.
Dan Neely
3
Gosto da enumabordagem, mas não sou muito fã de mixagens Includee Exclude, da mesma enumforma, é mais difícil entender o que está acontecendo. Se você quiser que isso seja realmente extensível, você pode torná-lo um [Flags] enum, torná-lo todos IncludeXxxe fornecer um IncludeAllque esteja definido como Int32.MaxValue. Em seguida, você pode fornecer duas ReturnEmployeeIdssobrecargas - uma que retorna todos os funcionários e outra que aceita um WhatToIncludeparâmetro de filtro que pode ser uma combinação de sinalizadores de enumeração.
precisa saber é o seguinte
@LeopardSkinPillBoxHat Ok, concordo com a exclusão / inclusão. Portanto, este é um exemplo artificial, mas eu poderia ter chamado de IncludeAllButManagement. No seu outro ponto, não concordo - acho que [Flags]é uma relíquia e só deve ser usada por motivos de legado ou desempenho. Eu acho que a ISet<WhatToInclude>é um conceito muito melhor (infelizmente o C # está faltando um pouco de açúcar sintático para torná-lo agradável de usar).
precisa saber é o seguinte
@ NiklasJ - Eu gosto da Flagsabordagem porque permite que o chamador passe WhatToInclude.Managers | WhatToInclude.Cleanerse o bit a bit ou expressa exatamente como o chamador irá processá-lo (ou seja, gerentes ou limpadores). Intuitivo de açúcar sintático :-)
LeopardSkinPillBoxOt
@LeopardSkinPillBoxHat Exatamente, mas essa é a única vantagem desse método. As desvantagens incluem, por exemplo, número limitado de opções e não são compatíveis com outros conceitos do tipo linq.
NiklasJ
20

faz sentido escrever:

 var ids=ReturnEmployeeIds(includeManagement: true);

É discutível se este é "bom estilo", mas IMHO este é o estilo menos não ruim e útil, desde que a linha de código não se torne "muito longa". Sem parâmetros nomeados, também é um estilo comum introduzir uma variável explicativa:

 bool includeManagement=true;
 var ids=ReturnEmployeeIds(includeManagement);

Esse estilo é provavelmente mais comum entre os programadores de C # do que a variante de parâmetro nomeado, pois os parâmetros nomeados não faziam parte do idioma anterior à Versão 4.0. O efeito na legibilidade é quase o mesmo, apenas precisa de uma linha de código adicional.

Portanto, minha recomendação: se você acha que usar uma enumeração ou duas funções com nomes diferentes é "exagero" ou não deseja ou não pode alterar a assinatura por um motivo diferente, vá em frente e use o parâmetro named.

Doc Brown
fonte
Pode-se notar que você está usando memória adicional no segundo exemplo
Alvaro
10
@ Alvaro É altamente improvável que isso aconteça em um caso tão trivial (em que a variável tem um valor constante). Mesmo que fosse, dificilmente vale a pena mencionar. A maioria de nós não está executando em 4KB de RAM atualmente.
Chris Hayes
3
Como esse é o C #, você pode marcar includeManagementcomo local conste evitar o uso de qualquer memória adicional.
precisa
8
Pessoal, não vou acrescentar nada à minha resposta que possa desviar o que acho importante aqui.
Doc Brown
17

Se você encontrar código como:

public List<Guid> ReturnEmployeeIds(bool includeManagement = false)
{

}

você pode praticamente garantir o que está dentro do {}algo como:

public List<Guid> ReturnEmployeeIds(bool includeManagement = false)
{
    if (includeManagement)
        return something
    else
        return something else
}

Em outras palavras, o booleano é usado para escolher entre dois cursos de ação: o método tem duas responsabilidades. É praticamente garantido um cheiro de código . Devemos sempre nos esforçar para garantir que os métodos façam apenas uma coisa; eles só têm uma responsabilidade. Portanto, eu diria que as duas soluções são a abordagem errada. O uso de parâmetros opcionais é um sinal de que o método está fazendo mais do que uma coisa. Parâmetros booleanos também são um sinal disso. Tendo os dois definitivamente deveria tocar os alarmes. O que você deve fazer, portanto, é refatorar os dois conjuntos diferentes de funcionalidades em dois métodos separados. Dê aos nomes dos métodos que deixem claro o que eles fazem, por exemplo:

public List<Guid> GetNonManagementEmployeeIds()
{

}

public List<Guid> GetAllEmployeeIds()
{

}

Isso melhora a legibilidade do código e garante que você siga melhor os princípios do SOLID.

EDIT Como apontado nos comentários, o caso aqui de que esses dois métodos provavelmente terão funcionalidade compartilhada e que funcionalidade compartilhada provavelmente serão agrupados em uma função privada que precisará de um parâmetro booleano para controlar algum aspecto do que ele faz .

Nesse caso, o nome do parâmetro deve ser fornecido, mesmo que o compilador não precise dele? Eu sugiro que não haja uma resposta simples para isso e que o julgamento seja necessário caso a caso:

  • O argumento para especificar o nome é que outros desenvolvedores (incluindo você dentro de seis meses) devem ser o público principal do seu código. O compilador é definitivamente um público secundário. Portanto, sempre escreva código para facilitar a leitura de outras pessoas; em vez de apenas fornecer o que o compilador precisa. Um exemplo de como usamos essa regra todos os dias é que não preenchemos nosso código com variáveis ​​_1, _2 etc., usamos nomes significativos (que não significam nada para o compilador).
  • O contra-argumento é que métodos privados são detalhes de implementação. Pode-se razoavelmente esperar que qualquer pessoa que esteja olhando para um método como o abaixo, rastreie o código para entender a finalidade do parâmetro booleano, pois está dentro das partes internas do código:
public List<Guid> GetNonManagementEmployeeIds()
{
    return ReturnEmployeeIds(true);
}

O arquivo de origem e os métodos são pequenos e fáceis de entender? Se sim, então o parâmetro nomeado provavelmente equivale a ruído. Se não, pode ser muito útil. Portanto, aplique a regra de acordo com as circunstâncias.

David Arno
fonte
12
Eu ouvi o que você está dizendo, mas você meio que entendeu o que eu estava perguntando. Assumindo um cenário pelo qual é mais apropriado usar parâmetros opcionais (esses cenários existem), é correto nomear os parâmetros mesmo que desnecessários?
JᴀʏMᴇᴇ
4
Tudo verdade, mas a pergunta era sobre uma chamada para esse método. Além disso, um parâmetro flag não garante uma ramificação dentro do método. Pode simplesmente ser passado para outra camada.
Robbie Dee
Isso não está errado, mas é simplista demais. No código real, você encontrará public List<Guid> ReturnEmployeeIds(bool includeManagement) { calculateSomethingHereForBothBranches; if (includeManagement) return something else return something else }uma divisão tão simples que, em dois métodos, provavelmente significará que esses dois métodos precisarão do resultado do primeiro cálculo como parâmetro.
Doc Brown
4
Acho que o que estamos dizendo é que a face pública da sua classe provavelmente deve expor dois métodos (cada um com uma responsabilidade), mas internamente cada um desses métodos pode encaminhar razoavelmente a solicitação para um único método privado com um parâmetro bool ramificado .
MetaFight
1
Eu posso imaginar três casos para o comportamento que difere entre as duas funções. 1. Existe um pré-processamento diferente. Faça com que as funções públicas pré-processem argumentos na função privada. 2. Existe um pós-processamento diferente. Faça com que as funções públicas pós-processem o resultado da função privada. 3. Há um comportamento intermediário que difere. Isso pode ser passado como um lambda para a função privada, assumindo que seu idioma seja compatível. Muitas vezes, isso leva a uma nova abstração reutilizável que realmente não havia ocorrido antes.
Jpmc26
1

Para um desenvolvedor iniciante no sistema, seria muito difícil adivinhar o que era verdade. A primeira coisa a fazer é passar o mouse sobre o nome do método ou ir para a definição (nenhuma das quais são grandes tarefas, no mínimo)

Sim, verdade. O código de auto-documentação é uma coisa bonita. Como outras respostas apontaram, existem maneiras de remover a ambiguidade da chamada ReturnEmployeeIdsusando uma enumeração, nomes de métodos diferentes etc. No entanto, às vezes, você não pode evitar o caso, mas não deseja usar o enum. em todos os lugares (a menos que você goste da verbosidade do visual basic).

Por exemplo, pode ajudar a esclarecer uma ligação, mas não necessariamente outra.

Um argumento nomeado pode ser útil aqui:

var ids = ReturnEmployeeIds(includeManagement: true);

Não adicione muita clareza adicional (acho que isso está analisando uma enumeração):

Enum.Parse(typeof(StringComparison), "Ordinal", ignoreCase: true);

Na verdade, pode reduzir a clareza (se uma pessoa não entender qual é a capacidade de uma lista):

var employeeIds = new List<int>(capacity: 24);

Ou onde isso não importa, porque você está usando bons nomes de variáveis:

bool includeManagement = true;
var ids = ReturnEmployeeIds(includeManagement);

Existe algum lugar que, formalmente, discuta se é necessário especificar explicitamente parâmetros opcionais quando o compilador não precisa de você?

Não é AFAIK. Vamos abordar as duas partes: o único momento em que você precisa usar explicitamente os parâmetros nomeados é porque o compilador exige que você faça isso (geralmente é para remover a ambiguidade da instrução). Além disso, você pode usá-los sempre que quiser. Este artigo da MS tem algumas sugestões sobre quando você pode usá-los, mas não é particularmente definitivo https://msdn.microsoft.com/en-us/library/dd264739.aspx .

Pessoalmente, acho que os uso com mais frequência quando estou criando atributos personalizados ou criando um novo método no qual meus parâmetros podem ser alterados ou reordenados (os atributos nomeados b / c podem ser listados em qualquer ordem). Além disso, eu os uso apenas quando estou fornecendo um valor estático inline, caso contrário, se estou passando uma variável, tento usar bons nomes de variáveis.

TL; DR

Em última análise, tudo se resume à preferência pessoal. É claro que existem alguns casos de uso em que você precisa usar parâmetros nomeados, mas depois disso você precisa fazer uma chamada de julgamento. IMO - Use-o em qualquer lugar que você acredite que ajude a documentar seu código, reduza a ambiguidade ou permita proteger assinaturas de métodos.


fonte
0

Se o parâmetro é óbvio a partir do nome (totalmente qualificado) do método e sua existência é natural para o que o método deve fazer, você não deve usar a sintaxe do parâmetro nomeado. Por exemplo, ReturnEmployeeName(57)é bastante óbvio que esse 57é o ID do funcionário, portanto é redundante anotá-lo com a sintaxe do parâmetro nomeado.

Com ReturnEmployeeIds(true), como você disse, a menos que você observe a declaração / documentação da função, é impossível descobrir o que truesignifica - portanto, você deve usar a sintaxe do parâmetro nomeado.

Agora, considere este terceiro caso:

void PrintEmployeeNames(bool includeManagement) {
    foreach (id; ReturnEmployeeIds(includeManagement)) {
        Console.WriteLine(ReturnEmployeeName(id));
    }
}

A sintaxe do parâmetro nomeado não é usada aqui, mas como estamos passando uma variável para ReturnEmployeeIds, e essa variável tem um nome, esse nome significa a mesma coisa que o parâmetro para o qual estamos passando (esse geralmente é o caso - mas nem sempre!) - não precisamos da sintaxe do parâmetro nomeado para entender o significado do código.

Portanto, a regra é simples - se você puder entender facilmente apenas da chamada do método o que o parâmetro deve significar, não use o parâmetro nomeado. Se você não puder - isso é uma deficiência na legibilidade do código, e você provavelmente deseja corrigi-los (não a qualquer preço, é claro, mas você geralmente deseja que o código seja legível). A correção não precisa ter nomes de parâmetros - você também pode colocar o valor em uma variável primeiro ou usar os métodos sugeridos nas outras respostas - desde que o resultado final facilite a compreensão do que o parâmetro faz.

Idan Arye
fonte
7
Eu discordo que ReturnEmployeeName(57)torna imediatamente óbvio que esse 57é o ID. GetEmployeeById(57)por outro lado ...
Hugo Zink 19/01
@HugoZink Inicialmente, eu queria usar algo parecido no exemplo, mas intencionalmente o mudei ReturnEmployeeNamepara demonstrar casos em que, mesmo sem mencionar explicitamente o parâmetro no nome do método, é bastante razoável esperar que as pessoas descubram o que é. De qualquer forma, vale ressaltar que ReturnEmployeeName, diferentemente , você não pode renomear razoavelmente ReturnEmployeeIdspara algo que indique o significado por trás dos parâmetros.
Idan Arye
1
De qualquer forma, é muito improvável que escrevamos ReturnEmployeeName (57) em um programa real. É muito mais provável que escrevamos ReturnEmployeeName (employeeId). Por que codificaríamos uma identificação de funcionário? A menos que seja o ID do programador e que haja algum tipo de fraude, adicione um pouco ao salário desse funcionário. :-)
Jay
0

Sim, acho que é um bom estilo.

Isso faz mais sentido para constantes booleanas e inteiras.

Se você estiver passando uma variável, o nome da variável deve deixar claro o significado. Caso contrário, você deve dar um nome melhor à variável.

Se você está passando uma string, o significado geralmente é claro. Como se eu vir uma chamada para GetFooData ("Oregon"), acho que um leitor pode adivinhar que o parâmetro é um nome de estado.

Nem todas as cordas são óbvias, é claro. Se eu vir GetFooData ("M"), a menos que conheça a função, não faço ideia do que "M" significa. Se for algum tipo de código, como M = "funcionários em nível de gerenciamento", provavelmente devemos ter uma enumeração em vez de uma literal, e o nome da enumeração deve ser claro.

E suponho que uma string possa ser enganosa. Talvez "Oregon" no meu exemplo acima se refira, não ao estado, mas a um produto ou cliente ou qualquer outra coisa.

Mas GetFooData (true) ou GetFooData (42) ... isso pode significar qualquer coisa. Os parâmetros nomeados são uma maneira maravilhosa de se auto-documentar. Eu os usei exatamente para esse fim em várias ocasiões.

Jay
fonte
0

Não há motivos super claros para usar esse tipo de sintaxe em primeiro lugar.

Em geral, tente evitar ter argumentos booleanos que à distância podem parecer arbitrários. includeManagement(provavelmente) afetará bastante o resultado. Mas o argumento parece ter "pouco peso".

O uso de um enum foi discutido, não apenas parecerá que o argumento "carrega mais peso", mas também fornecerá escalabilidade para o método. No entanto, essa pode não ser a melhor solução em todos os casos, já que o seu ReturnEmployeeIdsmétodo-terá que ser escalado ao lado do WhatToInclude-enum (consulte a resposta de NiklasJ ). Isso pode causar dor de cabeça mais tarde.

Considere o seguinte: Se você dimensionar o WhatToIncludemétodo -enum, mas não o ReturnEmployeeIdsmétodo. Pode então lançar um ArgumentOutOfRangeException(melhor caso) ou retornar algo completamente indesejado ( nullou vazio List<Guid>). O que, em alguns casos, pode confundir o programador, especialmente se você ReturnEmployeeIdsestiver em uma biblioteca de classes, onde o código-fonte não está prontamente disponível.

Supõe-se que WhatToInclude.Traineesfuncionará se WhatToInclude.Allfuncionar, visto que os trainees são um "subconjunto" de todos.

Isso depende, é claro, de como ReturnEmployeeIds é implementado.


Nos casos em que um argumento booleano pode ser passado, tento dividi-lo em dois métodos (ou mais, se necessário), no seu caso; pode-se extrair ReturnAllEmployeeIds, ReturnManagementIdse ReturnRegularEmployeeIds. Isso cobre todas as bases e é absolutamente auto-explicativo para quem a implementa. Isso também não terá o item "scaling", mencionado acima.

Como existem apenas dois resultados para algo que possui um argumento booleano. A implementação de dois métodos exige muito pouco esforço extra.

Menos código, raramente é melhor.


Com isto dito, não são alguns casos onde explicitamente declarar o argumento melhora a legibilidade. Considere, por exemplo. GetLatestNews(max: 10). aindaGetLatestNews(10) é bastante auto-explicativo, a declaração explícita de ajudará a esclarecer qualquer confusão.max

E se você absolutamente deve ter um argumento booleano, qual uso não pode ser inferido apenas lendo trueou false. Então eu diria que:

var ids = ReturnEmployeeIds(includeManagement: true);

.. é absolutamente melhor e mais legível que:

var ids = ReturnEmployeeIds(true);

Como no segundo exemplo, isso truepode significar absolutamente qualquer coisa . Mas eu tentaria evitar esse argumento.

die maus
fonte