parameters opcionais C # em methods substituídos

Parece que no .NET Framework há um problema com os parâmetros opcionais quando você substitui o método. A saída do código abaixo é: “bbb” “aaa”. Mas a saída que estou esperando é: “bbb” “bbb”. Existe uma solução para isso. Eu sei que isso pode ser resolvido com sobrecarga de método, mas imaginando o motivo disso. Além disso, o código funciona bem no Mono.

class Program { class AAA { public virtual void MyMethod(string s = "aaa") { Console.WriteLine(s); } public virtual void MyMethod2() { MyMethod(); } } class BBB : AAA { public override void MyMethod(string s = "bbb") { base.MyMethod(s); } public override void MyMethod2() { MyMethod(); } } static void Main(string[] args) { BBB asd = new BBB(); asd.MyMethod(); asd.MyMethod2(); } } 

Uma coisa que vale a pena notar aqui é que a versão substituída é chamada a cada vez. Altere a substituição para:

 public override void MyMethod(string s = "bbb") { Console.Write("derived: "); base.MyMethod(s); } 

E a saída é:

 derived: bbb derived: aaa 

Um método em uma class pode fazer um ou dois dos seguintes:

  1. Ele define uma interface para outro código a ser chamado.
  2. Define uma implementação para executar quando chamada.

Pode não fazer as duas coisas, pois um método abstrato só faz o primeiro.

Dentro de BBB a chamada MyMethod() chama um método definido no AAA .

Como há uma substituição em BBB , chamar esse método resulta em uma implementação em BBB sendo chamada.

Agora, a definição em AAA informa o código de chamada de duas coisas (bem, algumas outras que também não importam aqui).

  1. A assinatura void MyMethod(string) .
  2. (Para as linguagens que o suportam) o valor padrão para o único parâmetro é "aaa" e, portanto, ao compilar o código da forma MyMethod() se nenhum método correspondente MyMethod() puder ser encontrado, você poderá substituí-lo por uma chamada ` MyMethod (“aaa”).

Então, é isso que a chamada em BBB faz: O compilador vê uma chamada para MyMethod() , não encontra um método MyMethod() mas encontra um método MyMethod(string) . Ele também vê que, no local em que é definido, há um valor padrão de “aaa”, portanto, em tempo de compilation, isso muda para uma chamada para MyMethod("aaa") .

De dentro do BBB , o AAA é considerado o local onde os methods do AAA são definidos, mesmo que sejam substituídos no BBB , para que possam ser cancelados.

No tempo de execução, MyMethod(string) é chamado com o argumento “aaa”. Como há um formulário substituído, esse é o formulário chamado, mas não é chamado com “bbb” porque esse valor não tem nada a ver com a implementação de tempo de execução, mas com a definição de tempo de compilation.

Adicionando this. altera qual definição é examinada e, portanto, altera qual argumento é usado na chamada.

Edit: Por que isso parece mais intuitivo para mim.

Pessoalmente, e já que estou falando do que é intuitivo, só pode ser pessoal, acho isso mais intuitivo pelo seguinte motivo:

Se eu estivesse codificando BBB seguida, se chamando ou substituindo MyMethod(string) , eu pensaria nisso como “fazendo coisas AAA ” – é BBB s assumir “fazendo coisas AAA “, mas está fazendo coisas AAA mesmo assim. Daí se chamando ou substituindo, eu vou estar ciente do fato de que foi AAA que definiu MyMethod(string) .

Se eu estivesse chamando o código que usava BBB , eu pensaria em “usar material BBB “. Eu posso não estar muito ciente do que foi originalmente definido no AAA , e talvez eu pense nisso como meramente um detalhe de implementação (se eu também não usasse a interface AAA perto).

O comportamento do compilador corresponde à minha intuição, e é por isso que, ao ler pela primeira vez a pergunta, pareceu-me que o Mono tinha um bug. Após a consideração, não consigo ver como um dos dois cumpre o comportamento especificado melhor que o outro.

No que diz respeito a esse assunto, embora permaneça em um nível pessoal, eu nunca usaria parâmetros opcionais com methods abstratos, virtuais ou substituídos, e se replace os de outra pessoa, eu combinaria com os deles.

Você pode desambiguar chamando:

 this.MyMethod(); 

(in MyMethod2() )

Se é um bug é complicado; parece inconsistente, no entanto. O Resharper avisa que você simplesmente não deve ter alterações no valor padrão em uma substituição, se isso ajudar; p Naturalmente, o resharper também informa this. é redundante, e se oferece para removê-lo para você … o que muda o comportamento – então o resharper também não é perfeito.

Parece que ele pode se qualificar como um bug do compilador, eu concedo a você. Eu preciso olhar com muito cuidado para ter certeza … onde está Eric quando você precisa dele, hein?


Editar:

O ponto chave aqui é a especificação de idioma; vamos olhar para o §7.5.3:

Por exemplo, o conjunto de candidatos para uma chamada de método não inclui methods marcados como override (§7.4) e os methods em uma class base não são candidatos se qualquer método em uma class derivada for aplicável (§7.6.5.1).

(e, de fato, o §7.4 claramente omite os methods de override da consideração)

Há algum conflito aqui … ele afirma que os methods base não são usados ​​se houver um método aplicável em uma class derivada – o que nos levaria ao método derivado , mas, ao mesmo tempo, diz que os methods marcados como override são não considerado.

Mas, §7.5.1.1, então, afirma:

Para methods virtuais e indexadores definidos em classs, a lista de parâmetros é selecionada a partir da declaração ou substituição mais específica do membro da function, iniciando com o tipo estático do receptor e pesquisando através de suas classs base.

e, em seguida, §7.5.1.2 explica como os valores são avaliados no momento da chamada:

Durante o processamento em tempo de execução de uma invocação de membro de function (§7.5.4), as expressões ou referências de variables ​​de uma lista de argumentos são avaliadas em ordem, da esquerda para a direita, da seguinte maneira:

…(recorte)…

Quando argumentos são omitidos de um membro de function com parâmetros opcionais correspondentes, os argumentos padrão da declaração do membro da function são implicitamente passados. Porque estes são sempre constantes, sua avaliação não afetará a ordem de avaliação dos argumentos restantes.

Isso explicitamente destaca que ele está olhando para a lista de argumentos, que foi previamente definida em §7.5.1.1 como proveniente da declaração ou substituição mais específica . Parece razoável que essa seja a “declaração de método” referida no item 7.5.1.2, portanto, o valor passado deve ser do tipo mais derivado até o estático.

Isto sugeriria: csc tem um bug, e deveria estar usando a versão derivada (“bbb bbb”) a menos que seja restrito (via base. , Ou conversão para um tipo base) para olhar as declarações do método base (§7.6 8).

Isso parece um bug para mim. Eu acredito que é bem especificado, e que deve se comportar da mesma maneira como se você chamasse o método com o prefixo this explícito.

Eu simplifiquei o exemplo para usar apenas um único método virtual e mostrar qual implementação é chamada e qual é o valor do parâmetro:

 using System; class Base { public virtual void M(string text = "base-default") { Console.WriteLine("Base.M: {0}", text); } } class Derived : Base { public override void M(string text = "derived-default") { Console.WriteLine("Derived.M: {0}", text); } public void RunTests() { M(); // Prints Derived.M: base-default this.M(); // Prints Derived.M: derived-default base.M(); // Prints Base.M: base-default } } class Test { static void Main() { Derived d = new Derived(); d.RunTests(); } } 

Então, tudo o que precisamos nos preocupar são as três chamadas dentro de RunTests. Os bits importantes da especificação para as duas primeiras chamadas são a seção 7.5.1.1, que fala sobre a lista de parâmetros a ser usada ao encontrar os parâmetros correspondentes:

Para methods virtuais e indexadores definidos em classs, a lista de parâmetros é selecionada a partir da declaração ou substituição mais específica do membro da function, iniciando com o tipo estático do receptor e pesquisando através de suas classs base.

E seção 7.5.1.2:

Quando argumentos são omitidos de um membro de function com parâmetros opcionais correspondentes, os argumentos padrão da declaração do membro da function são implicitamente passados.

O “parâmetro opcional correspondente” é o bit que liga o 7.5.2 ao 7.5.1.1.

Para M() e this.M() , essa lista de parâmetros deve ser aquela em Derived como o tipo estático do receptor é Derived , De fato, você pode dizer que o compilador trata isso como a lista de parâmetros anteriormente na compilation, como se você tornar o parâmetro obrigatório em Derived.M() , ambas as chamadas falharão – portanto, a chamada M() exige que o parâmetro tenha um valor padrão em Derived , mas depois o ignora!

Na verdade, fica ainda pior: se você fornecer um valor padrão para o parâmetro em Derived mas torná-lo obrigatório em Base , a chamada M() terminará usando null como o valor do argumento. Se nada mais, eu diria que isso prova que é um erro: esse valor null não pode vir de qualquer lugar válido. (É null devido a esse ser o valor padrão do tipo string ; ele sempre usa apenas o valor padrão para o tipo de parâmetro.)

A seção 7.6.8 da especificação lida com base.M (), que diz que , assim como o comportamento não virtual, a expressão é considerada como ((Base) this).M() ; Portanto, é totalmente correto que o método base seja usado para determinar a lista de parâmetros efetiva. Isso significa que a linha final está correta.

Apenas para facilitar as coisas para quem quer ver o erro realmente estranho descrito acima, onde um valor não especificado em qualquer lugar é usado:

 using System; class Base { public virtual void M(int x) { // This isn't called } } class Derived : Base { public override void M(int x = 5) { Console.WriteLine("Derived.M: {0}", x); } public void RunTests() { M(); // Prints Derived.M: 0 } static void Main() { new Derived().RunTests(); } } 

Você tentou:

  public override void MyMethod2() { this.MyMethod(); } 

Então você realmente diz ao seu programa para usar o método overriden.

O comportamento é definitivamente muito estranho; Não está claro para mim se é de fato um bug no compilador, mas pode ser.

O campus recebeu uma quantidade razoável de neve na noite passada e Seattle não é muito boa em lidar com a neve. Meu ônibus não está funcionando esta manhã, então eu não vou poder entrar no escritório para comparar o que C # 4, C # 5 e Roslyn dizem sobre este caso e se eles discordam. Eu tentarei postar uma análise mais tarde, uma vez que eu esteja de volta ao escritório e possa usar ferramentas de debugging adequadas.

Pode ser que isso se deva à ambigüidade e o compilador está dando prioridade à class base / super. A alteração abaixo no código da sua class BBB com a adição de referência a this palavra this chave fornece a saída ‘bbb bbb’:

 class BBB : AAA { public override void MyMethod(string s = "bbb") { base.MyMethod(s); } public override void MyMethod2() { this.MyMethod(); //added this keyword here } } 

Uma das coisas que isso implica é que você deve sempre usar a palavra this chave this sempre que estiver chamando propriedades ou methods na instância atual da class como uma prática recomendada .

Eu ficaria preocupado se essa ambigüidade no método base e filho nem sequer levantasse um aviso de compilador (se não erro), mas se isso acontecer, isso não é visto, suponho.

================================================== ================

EDIT: Considere trechos de amostra abaixo desses links:

http://geekswithblogs.net/BlackRabbitCoder/archive/2011/07/28/c.net-little-pitfalls-default-parameters-are-compile-time-substitutions.aspx

http://geekswithblogs.net/BlackRabbitCoder/archive/2010/06/17/c-optional-parameters—pros-and-pitfalls.aspx

Pitfall: Os valores de parâmetro opcionais são tempo de compilation Há apenas uma coisa e uma coisa para se lembrar ao usar parâmetros opcionais. Se você mantiver isso em mente, é bem provável que você entenda e evite as potenciais armadilhas com o uso: Essa é uma coisa: os parâmetros opcionais são açúcar sintático em tempo de compilation!

Pitfall: Cuidado com os parâmetros padrão na implementação de inheritance e interface

Agora, as segundas armadilhas potenciais têm a ver com a implementação de inheritance e interface. Eu vou ilustrar com um quebra-cabeça:

  1: public interface ITag 2: { 3: void WriteTag(string tagName = "ITag"); 4: } 5: 6: public class BaseTag : ITag 7: { 8: public virtual void WriteTag(string tagName = "BaseTag") { Console.WriteLine(tagName); } 9: } 10: 11: public class SubTag : BaseTag 12: { 13: public override void WriteTag(string tagName = "SubTag") { Console.WriteLine(tagName); } 14: } 15: 16: public static class Program 17: { 18: public static void Main() 19: { 20: SubTag subTag = new SubTag(); 21: BaseTag subByBaseTag = subTag; 22: ITag subByInterfaceTag = subTag; 23: 24: // what happens here? 25: subTag.WriteTag(); 26: subByBaseTag.WriteTag(); 27: subByInterfaceTag.WriteTag(); 28: } 29: } 

O que acontece? Bem, mesmo que o object em cada caso seja SubTag cuja tag seja “SubTag”, você terá:

1: Subtag 2: BaseTag 3: ITag

Mas lembre-se de garantir que você:

Não insira novos parâmetros padrão no meio de um conjunto existente de parâmetros padrão, isso pode causar um comportamento imprevisível que pode não necessariamente lançar um erro de syntax – adicionar ao final da lista ou criar um novo método. Seja extremamente cuidadoso ao usar os parâmetros padrão em hierarquias e interfaces de inheritance – escolha o nível mais apropriado para adicionar os padrões com base no uso esperado.

================================================== ========================

Isso eu acho que é porque esses valores padrão são fixos no tempo de compilation. Se você usar o refletor, verá o seguinte para MyMethod2 em BBB.

 public override void MyMethod2() { this.MyMethod("aaa"); } 

Concordo em geral com @Marc Gravell.

No entanto, eu gostaria de mencionar que o problema é velho o suficiente no mundo C ++ ( http://www.devx.com/tips/Tip/12737 ), e a resposta parece “ao contrário de funções virtuais, que são resolvidas na execução tempo, os argumentos padrão são resolvidos estaticamente, isto é, em tempo de compilation. ” Portanto, este comportamento do compilador C # foi bastante aceito deliberadamente devido à consistência, apesar de sua inesperação, parece.

De qualquer maneira, precisa de uma correção

Eu definitivamente consideraria isso como um bug, ou porque os resultados estão errados ou se os resultados são esperados, então o compilador não deve permitir que você o declare como “override”, ou pelo menos forneça um aviso.

Eu recomendo que você relate isso ao Microsoft.Connect

Mas está certo ou errado?

No entanto, em relação a se este é o comportamento esperado ou não, vamos primeiro analisar as duas visões sobre ele.

considere que temos o seguinte código:

 void myfunc(int optional = 5){ /* Some code here*/ } //Function implementation myfunc(); //Call using the default arguments 

Existem duas maneiras de implementá-lo:

  1. Que argumentos opcionais são tratados como funções sobrecarregadas, resultando no seguinte:

     void myfunc(int optional){ /* Some code here*/ } //Function implementation void myfunc(){ myfunc(5); } //Default arguments implementation myfunc(); //Call using the default arguments 
  2. Que o valor padrão é incorporado no chamador, resultando no seguinte código:

     void myfunc(int optional){ /* Some code here*/ } //Function implementation myfunc(5); //Call and embed default arguments 

Existem muitas diferenças entre as duas abordagens, mas primeiro vamos dar uma olhada em como a estrutura .Net a interpreta.

  1. Em .Net, você só pode replace um método por um método que contenha o mesmo número de argumentos, mas não pode replace um método que contenha mais argumentos, mesmo que sejam todos opcionais (o que resultaria em uma chamada com a mesma assinatura que a método sobrescrito), digamos por exemplo que você tem:

     class bassClass{ public virtual void someMethod()} class subClass :bassClass{ public override void someMethod()} //Legal //The following is illegal, although it would be called as someMethod(); //class subClass:bassClass{ public override void someMethod(int optional = 5)} 
  2. Você pode sobrecarregar um método com argumentos padrão com outro método sem argumentos (isso tem implicações desastrosas, como discutirei em instantes), então o código a seguir é legal:

     void myfunc(int optional = 5){ /* Some code here*/ } //Function with default void myfunc(){ /* Some code here*/ } //No arguments myfunc(); //Call which one?, the one with no arguments! 
  3. Ao usar a reflection, deve-se sempre fornecer um valor padrão.

Tudo o que é suficiente para provar que o .Net tomou a segunda implementação, então o comportamento que o OP viu está certo, pelo menos de acordo com .Net.

Problemas com a abordagem .Net

No entanto, existem problemas reais com a abordagem .Net.

  1. Consistência

    • Como no problema do OP ao replace o valor padrão em um método herdado, os resultados podem ser imprevisíveis

    • Quando a implantação original do valor padrão é alterada, e como os chamadores não precisam ser recompilados, podemos acabar com os valores padrão que não são mais válidos

    • Reflexão requer que você forneça o valor padrão, que o chamador não precisa saber
  2. Código de quebra

    • Quando temos uma function com argumentos padrão e, por último, adicionamos uma function sem argumentos, todas as chamadas serão encaminhadas para a nova function, quebrando assim todo o código existente, sem qualquer notificação ou aviso!

    • Similar acontecerá, se mais tarde tirarmos a function sem argumentos, então todas as chamadas serão roteadas automaticamente para a function com os argumentos padrão, novamente sem nenhuma notificação ou aviso! embora isso possa não ser a intenção do programador

    • Além disso, não precisa ser um método de instância regular, um método de extensão fará os mesmos problemas, já que um método de extensão sem parâmetros terá precedência sobre um método de instância com parâmetros padrão!

Resumo: FIQUE LONGE DE ARGUMENTOS OPCIONAIS, E USE SOBRECARGA SUPERIOR (COMO O FRAMEWORK NET FAZ MESMO)