Você deve declarar métodos usando sobrecargas ou parâmetros opcionais em C # 4.0?


93

Eu estava assistindo a palestra de Anders sobre C # 4.0 e uma prévia do C # 5.0 , e isso me fez pensar sobre quando parâmetros opcionais estão disponíveis em C # qual será a maneira recomendada de declarar métodos que não precisam de todos os parâmetros especificados?

Por exemplo, algo como a FileStreamclasse tem cerca de quinze construtores diferentes que podem ser divididos em 'famílias' lógicas, por exemplo, os que estão abaixo de uma string, os de an IntPtre os de a SafeFileHandle.

FileStream(string,FileMode);
FileStream(string,FileMode,FileAccess);
FileStream(string,FileMode,FileAccess,FileShare);
FileStream(string,FileMode,FileAccess,FileShare,int);
FileStream(string,FileMode,FileAccess,FileShare,int,bool);

Parece-me que esse tipo de padrão poderia ser simplificado tendo três construtores, e usando parâmetros opcionais para aqueles que podem ser padronizados, o que tornaria as diferentes famílias de construtores mais distintas [nota: eu sei que essa mudança não será feita na BCL, estou falando hipoteticamente para esse tipo de situação].

O que você acha? A partir do C # 4.0, fará mais sentido transformar grupos intimamente relacionados de construtores e métodos em um único método com parâmetros opcionais ou há um bom motivo para continuar com o mecanismo tradicional de sobrecarga?

Respostas:


120

Eu consideraria o seguinte:

  • Você precisa que seu código seja usado em linguagens que não suportam parâmetros opcionais? Nesse caso, considere incluir as sobrecargas.
  • Você tem algum membro em sua equipe que se opõe violentamente aos parâmetros opcionais? (Às vezes é mais fácil conviver com uma decisão da qual você não gosta do que discutir o caso.)
  • Você tem certeza de que seus padrões não mudarão entre as compilações de seu código ou, se forem, seus chamadores concordarão com isso?

Não verifiquei como os padrões irão funcionar, mas presumo que os valores padrão serão incorporados ao código de chamada, da mesma forma que as referências aos constcampos. Normalmente não há problema - mudanças em um valor padrão são bastante significativas de qualquer maneira - mas essas são as coisas a serem consideradas.


20
+1 pela sabedoria sobre o pragmatismo: às vezes é mais fácil conviver com uma decisão da qual você não gosta do que discutir o caso.
legends2k

13
@romkyns: Não, o efeito das sobrecargas não é a mesma coisa com o ponto 3. Com sobrecargas fornecendo os padrões, os padrões estão dentro do código da biblioteca - então, se você alterar o padrão e fornecer uma nova versão da biblioteca, os chamadores irão veja o novo padrão sem recompilação. Enquanto que com parâmetros opcionais, você precisa recompilar para "ver" os novos padrões. Muitas vezes não é uma distinção importante, mas é uma distinção.
Jon Skeet

oi @JonSkeet, gostaria de saber se usamos ambos, ou seja, função com parâmetro opcional e outro com sobrecarga, qual método será chamado ?? por exemplo, Add (int a, int b) e Add (int a, int b, int c = 0) e a chamada de função diz: Add (5,10); qual método será chamado de função sobrecarregada ou função de parâmetro opcional? obrigado :)
SHEKHAR SHETE

@Shekshar: Você já experimentou? Leia as especificações para obter detalhes, mas basicamente em um desempate, um método em que o compilador não precise preencher nenhum parâmetro opcional vence.
Jon Skeet

@JonSkeet acabei de tentar com o acima ... a sobrecarga de função vence o parâmetro opcional :)
SHEKHAR SHETE

19

Quando uma sobrecarga de método normalmente executa a mesma coisa com um número diferente de argumentos, os padrões são usados.

Quando uma sobrecarga de método executa uma função de maneira diferente com base em seus parâmetros, a sobrecarga continuará a ser usada.

Eu usei opcional nos meus dias de VB6 e desde então perdi, ele reduzirá muito a duplicação de comentários XML em C #.


11

Tenho usado Delphi com parâmetros opcionais desde sempre. Em vez disso, mudei para o uso de sobrecargas.

Porque quando você criar mais sobrecargas, invariavelmente entrará em conflito com um formulário de parâmetro opcional e, de qualquer maneira, terá que convertê-los em não opcionais.

E eu gosto da noção de que geralmente há um super método e o resto são invólucros mais simples desse.


1
Eu concordo plenamente com isso, no entanto, há a ressalva de que quando você tem um método que usa vários (3+) parâmetros, que são por natureza todos "opcionais" (podem ser substituídos por um padrão), você pode acabar com muitas permutações da assinatura do método para não mais benefício. Considere Foo(A, B, C)requer Foo(A), Foo(B), Foo(C), Foo(A, B), Foo(A, C), Foo(B, C).
Dan Lugg

7

Definitivamente estarei usando o recurso de parâmetros opcionais do 4.0. Livra-se do ridículo ...

public void M1( string foo, string bar )
{
   // do that thang
}

public void M1( string foo )
{
  M1( foo, "bar default" ); // I have always hated this line of code specifically
}

... e coloca os valores exatamente onde o chamador pode vê-los ...

public void M1( string foo, string bar = "bar default" )
{
   // do that thang
}

Muito mais simples e menos sujeito a erros. Na verdade, eu vi isso como um bug no caso de sobrecarga ...

public void M1( string foo )
{
   M2( foo, "bar default" );  // oops!  I meant M1!
}

Ainda não joguei com o compilador 4.0, mas não ficaria chocado em saber que o compilador simplesmente emite sobrecargas para você.


6

Os parâmetros opcionais são essencialmente um pedaço de metadados que direciona um compilador que está processando uma chamada de método para inserir os padrões apropriados no site da chamada. Por outro lado, as sobrecargas fornecem um meio pelo qual um compilador pode selecionar um de vários métodos, alguns dos quais podem fornecer os próprios valores padrão. Observe que se alguém tentar chamar um método que especifica parâmetros opcionais de código escrito em uma linguagem que não os suporta, o compilador exigirá que os parâmetros "opcionais" sejam especificados, mas desde que chamar um método sem especificar um parâmetro opcional é equivalente a chamá-lo com um parâmetro igual ao valor padrão, não há obstáculo para que essas linguagens chamem esses métodos.

Uma consequência significativa da associação de parâmetros opcionais no site da chamada é que eles receberão valores com base na versão do código-alvo que está disponível para o compilador. Se um assembly Footiver um método Boo(int)com valor padrão 5 e o assembly Barcontiver uma chamada para Foo.Boo(), o compilador irá processá-lo como um Foo.Boo(5). Se o valor padrão for alterado para 6 e o ​​assembly Foofor recompilado, Barcontinuará a chamar, a Foo.Boo(5)menos ou até que seja recompilado com essa nova versão do Foo. Portanto, deve-se evitar o uso de parâmetros opcionais para coisas que podem mudar.


Re: "Deve-se, portanto, evitar o uso de parâmetros opcionais para coisas que podem mudar." Concordo que isso pode ser problemático se a alteração passar despercebida pelo código do cliente. No entanto, o mesmo problema existe quando o valor padrão é escondida dentro de uma sobrecarga de método: void Foo(int value) … void Foo() { Foo(42); }. Do lado de fora, o chamador não sabe qual valor padrão é usado, nem quando pode ser alterado; seria preciso monitorar a documentação escrita para isso. Os valores padrão para parâmetros opcionais podem ser vistos apenas como: documentação no código qual é o valor padrão.
stakx - não contribuindo mais em

@stakx: Se uma sobrecarga sem parâmetros for ligada a uma sobrecarga com um parâmetro, alterar o valor "padrão" desse parâmetro e recompilar a definição da sobrecarga irá alterar o valor que ela usa, mesmo se o código de chamada não for recompilado .
supercat

Verdade, mas isso não o torna mais problemático do que a alternativa. Em um caso (sobrecarga do método), o código de chamada não tem voz no valor padrão. Isso pode ser apropriado se o código de chamada realmente não se importar com o parâmetro opcional e o que ele significa. No outro caso (parâmetro opcional com valor padrão), o código de chamada compilado anteriormente não será afetado quando o valor padrão mudar. Isso também pode ser apropriado quando o código de chamada realmente se preocupa com o parâmetro; omiti-lo no código-fonte é como dizer, "o valor padrão sugerido atualmente está OK para mim."
stakx - não contribuindo mais em

O que estou tentando enfatizar aqui é que, embora haja consequências para ambas as abordagens (como você apontou), elas não são inerentemente vantajosas ou desvantajosas. Isso depende também das necessidades e objetivos do código de chamada. A partir desse ponto de vista, achei o veredicto na última frase de sua resposta um tanto rígido.
stakx - não contribuindo mais em

@stakx: Eu disse "evite usar" ao invés de "nunca use". Se mudar X significará que a próxima recompilação de Y mudará o comportamento de Y, isso exigirá que um sistema de construção seja configurado para que cada recompilação de X também recompile Y (tornando as coisas mais lentas), ou criará o risco de que um programador mude X de uma forma que irá quebrar Y na próxima vez que for compilado, e só descobrirá tal quebra mais tarde, quando Y for alterado por algum motivo totalmente não relacionado. Os parâmetros padrão só devem ser usados ​​quando suas vantagens superam esses custos.
supercat

4

Pode-se argumentar se argumentos opcionais ou sobrecargas devem ser usados ​​ou não, mas o mais importante, cada um tem sua própria área onde são insubstituíveis.

Argumentos opcionais, quando usados ​​em combinação com argumentos nomeados, são extremamente úteis quando combinados com algumas listas de argumentos longos com todos os opcionais de chamadas COM.

Sobrecargas são extremamente úteis quando o método é capaz de operar em muitos tipos de argumentos diferentes (apenas um dos exemplos) e faz castings internamente, por exemplo; você apenas o alimenta com qualquer tipo de dado que faça sentido (que seja aceito por alguma sobrecarga existente). Não consigo superar isso com argumentos opcionais.


3

Estou ansioso para os parâmetros opcionais porque mantém o que os padrões estão mais próximos do método. Portanto, em vez de dezenas de linhas para as sobrecargas que apenas chamam o método "expandido", você apenas define o método uma vez e pode ver como os parâmetros opcionais são padronizados na assinatura do método. Prefiro olhar para:

public Rectangle (Point start = Point.Zero, int width, int height)
{
    Start = start;
    Width = width;
    Height = height;
}

Em vez disso:

public Rectangle (Point start, int width, int height)
{
    Start = start;
    Width = width;
    Height = height;
}

public Rectangle (int width, int height) :
    this (Point.Zero, width, height)
{
}

Obviamente, este exemplo é muito simples, mas no caso do OP com 5 sobrecargas, as coisas podem ficar lotadas rapidamente.


7
Ouvi dizer que os parâmetros opcionais devem ser os últimos, não é?
Ilya Ryzhenkov

Depende do seu design. Talvez o argumento 'start' seja normalmente importante, exceto quando não é. Talvez você tenha a mesma assinatura em outro lugar, significando algo diferente. Para um exemplo artificial, public Rectangle (int width, int height, Point innerSquareStart, Point innerSquareEnd) {}
Robert P

13
Pelo que eles disseram na conversa, os parâmetros opcionais devem estar após os parâmetros obrigatórios.
Greg Beech

3

Um dos meus aspectos favoritos dos parâmetros opcionais é que você vê o que acontece com seus parâmetros se não os fornecer, mesmo sem ir para a definição do método. O Visual Studio simplesmente mostrará o valor padrão do parâmetro quando você digitar o nome do método. Com um método de sobrecarga, você fica preso em ler a documentação (se ainda estiver disponível) ou em navegar diretamente para a definição do método (se disponível) e para o método que a sobrecarga envolve.

Em particular: o esforço de documentação pode aumentar rapidamente com a quantidade de sobrecargas, e você provavelmente terminará copiando comentários já existentes das sobrecargas existentes. Isso é muito irritante, pois não produz nenhum valor e quebra o princípio DRY ). Por outro lado, com um parâmetro opcional, há exatamente um lugar onde todos os parâmetros são documentados e você vê seu significado, bem como seus valores padrão durante a digitação.

Por último, mas não menos importante, se você é o consumidor de uma API, pode nem mesmo ter a opção de inspecionar os detalhes de implementação (se você não tiver o código-fonte) e, portanto, não terá chance de ver para qual super método os sobrecarregados estão envolvendo. Assim, você fica preso com a leitura do documento e esperando que todos os valores padrão estejam listados lá, mas nem sempre é o caso.

Claro, esta não é uma resposta que trate de todos os aspectos, mas acho que adiciona uma que não foi abordada até agora.


1

Embora sejam (supostamente?) Duas maneiras conceitualmente equivalentes disponíveis para você modelar sua API do zero, eles infelizmente têm algumas diferenças sutis quando você precisa considerar a compatibilidade com versões anteriores do tempo de execução para seus clientes antigos. Meu colega (obrigado, Brent!) Me indicou este post maravilhoso: Problemas de versionamento com argumentos opcionais . Algumas citações dele:

O motivo pelo qual os parâmetros opcionais foram introduzidos no C # 4 em primeiro lugar foi para oferecer suporte à interoperabilidade COM. É isso aí. E agora, estamos aprendendo sobre todas as implicações desse fato. Se você tiver um método com parâmetros opcionais, nunca poderá adicionar uma sobrecarga com parâmetros opcionais adicionais, por medo de causar uma alteração de interrupção no tempo de compilação. E você nunca pode remover uma sobrecarga existente, pois essa sempre foi uma alteração que interrompeu o tempo de execução. Você precisa tratá-lo como uma interface. Seu único recurso neste caso é escrever um novo método com um novo nome. Portanto, esteja ciente disso se você planeja usar argumentos opcionais em suas APIs.


1

Uma ressalva dos parâmetros opcionais é o controle de versão, em que uma refatoração tem consequências indesejadas. Um exemplo:

Código inicial

public string HandleError(string message, bool silent=true, bool isCritical=true)
{
  ...
}

Suponha que este seja um dos muitos chamadores do método acima:

HandleError("Disk is full", false);

Aqui, o evento não é silencioso e é tratado como crítico.

Agora, digamos que, após uma refatoração, descobrimos que todos os erros solicitam ao usuário de qualquer maneira, portanto, não precisamos mais do sinalizador de silêncio. Então, nós o removemos.

Depois de refatorar

A chamada anterior ainda compila, e digamos que ela passe pelo refatorador inalterada:

public string HandleError(string message, /*bool silent=true,*/ bool isCritical=true)
{
  ...
}

...

// Some other distant code file:
HandleError("Disk is full", false);

Agora falseterá um efeito indesejado, o evento não será mais tratado como crítico.

Isso pode resultar em um defeito sutil, já que não haverá erro de compilação ou tempo de execução (ao contrário de algumas outras advertências de opcionais, como este ou este ).

Observe que existem muitas formas desse mesmo problema. Um outro formulário é descrito aqui .

Note também que estritamente usando parâmetros nomeados ao chamar o método vai evitar o problema, como assim: HandleError("Disk is full", silent:false). No entanto, pode não ser prático presumir que todos os outros desenvolvedores (ou usuários de uma API pública) o farão.

Por esses motivos, eu evitaria usar parâmetros opcionais em uma API pública (ou até mesmo um método público, se pudesse ser usado amplamente), a menos que haja outras considerações convincentes.


0

Ambos os parâmetros opcionais e a sobrecarga do método têm suas próprias vantagens ou desvantagens. Depende de sua preferência para escolher entre eles.

Parâmetro opcional: disponível apenas em .Net 4.0. parâmetro opcional reduz o tamanho do código. Você não pode definir o parâmetro out e ref

métodos sobrecarregados: Você pode definir os parâmetros Out e ref. O tamanho do código aumentará, mas os métodos sobrecarregados são fáceis de entender.


0

Em muitos casos, parâmetros opcionais são usados ​​para alternar a execução. Por exemplo:

decimal GetPrice(string productName, decimal discountPercentage = 0)
{

    decimal basePrice = CalculateBasePrice(productName);

    if (discountPercentage > 0)
        return basePrice * (1 - discountPercentage / 100);
    else
        return basePrice;
}

O parâmetro de desconto aqui é usado para alimentar a instrução if-then-else. Existe o polimorfismo que não foi reconhecido e então foi implementado como uma instrução if-then-else. Nesses casos, é muito melhor dividir os dois fluxos de controle em dois métodos independentes:

decimal GetPrice(string productName)
{
    decimal basePrice = CalculateBasePrice(productName);
    return basePrice;
}

decimal GetPrice(string productName, decimal discountPercentage)
{

    if (discountPercentage <= 0)
        throw new ArgumentException();

    decimal basePrice = GetPrice(productName);

    decimal discountedPrice = basePrice * (1 - discountPercentage / 100);

    return discountedPrice;

}

Dessa forma, protegemos até mesmo a turma de receber uma chamada com desconto zero. Essa ligação significaria que o chamador pensa que existe o desconto, mas na verdade não existe desconto algum. Esse mal-entendido pode facilmente causar um bug.

Em casos como este, prefiro não ter parâmetros opcionais, mas forçar o chamador a selecionar explicitamente o cenário de execução que se adapta à sua situação atual.

A situação é muito semelhante a ter parâmetros que podem ser nulos. Isso é igualmente má ideia quando a implementação ferve para declarações como if (x == null).

Você pode encontrar análises detalhadas nestes links: Evitando Parâmetros Opcionais e Evitando Parâmetros Nulos


0

Para adicionar um acéfalo ao usar uma sobrecarga em vez de opcionais:

Sempre que você tiver vários parâmetros que só façam sentido juntos, não introduza opcionais neles.

Ou, de forma mais geral, sempre que as assinaturas de método permitem padrões de uso que não fazem sentido, restrinja o número de permutações de chamadas possíveis. Por exemplo, usando sobrecargas em vez de opcionais (essa regra também é válida quando você tem vários parâmetros do mesmo tipo de dados, a propósito; aqui, dispositivos como métodos de fábrica ou tipos de dados personalizados podem ajudar).

Exemplo:

enum Match {
    Regex,
    Wildcard,
    ContainsString,
}

// Don't: This way, Enumerate() can be called in a way
//         which does not make sense:
IEnumerable<string> Enumerate(string searchPattern = null,
                              Match match = Match.Regex,
                              SearchOption searchOption = SearchOption.TopDirectoryOnly);

// Better: Provide only overloads which cannot be mis-used:
IEnumerable<string> Enumerate(SearchOption searchOption = SearchOption.TopDirectoryOnly);
IEnumerable<string> Enumerate(string searchPattern, Match match,
                              SearchOption searchOption = SearchOption.TopDirectoryOnly);
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.