Erro de invocação ambígua do compilador - método anônimo e grupo de métodos com Func <> ou Action

102

Eu tenho um cenário em que desejo usar a sintaxe de grupo de método em vez de métodos anônimos (ou sintaxe lambda) para chamar uma função.

A função tem duas sobrecargas, uma que leva um Actione a outra leva a Func<string>.

Posso chamar alegremente as duas sobrecargas usando métodos anônimos (ou sintaxe lambda), mas recebo um erro do compilador de invocação ambígua se usar a sintaxe de grupo de métodos. Posso contornar o cast explícito para Actionou Func<string>, mas não acho que isso seja necessário.

Alguém pode explicar por que os casts explícitos devem ser exigidos.

Exemplo de código abaixo.

class Program
{
    static void Main(string[] args)
    {
        ClassWithSimpleMethods classWithSimpleMethods = new ClassWithSimpleMethods();
        ClassWithDelegateMethods classWithDelegateMethods = new ClassWithDelegateMethods();

        // These both compile (lambda syntax)
        classWithDelegateMethods.Method(() => classWithSimpleMethods.GetString());
        classWithDelegateMethods.Method(() => classWithSimpleMethods.DoNothing());

        // These also compile (method group with explicit cast)
        classWithDelegateMethods.Method((Func<string>)classWithSimpleMethods.GetString);
        classWithDelegateMethods.Method((Action)classWithSimpleMethods.DoNothing);

        // These both error with "Ambiguous invocation" (method group)
        classWithDelegateMethods.Method(classWithSimpleMethods.GetString);
        classWithDelegateMethods.Method(classWithSimpleMethods.DoNothing);
    }
}

class ClassWithDelegateMethods
{
    public void Method(Func<string> func) { /* do something */ }
    public void Method(Action action) { /* do something */ }
}

class ClassWithSimpleMethods
{
    public string GetString() { return ""; }
    public void DoNothing() { }
}

Atualização C # 7.3

De acordo com o comentário de 0xcde abaixo em 20 de março de 2019 (nove anos após eu postar esta pergunta!), Este código compila a partir do C # 7.3 graças aos candidatos a sobrecarga aprimorados .

Richard Ev
fonte
Tentei seu código e estou recebendo um erro adicional de tempo de compilação: 'void test.ClassWithSimpleMethods.DoNothing ()' tem o tipo de retorno errado (que está na linha 25, que é onde está o erro de ambigüidade)
Matt Ellen
@Matt: Também vejo esse erro. Os erros que citei em meu post foram os problemas de compilação que o VS destaca antes mesmo de você tentar uma compilação completa.
Richard Ev
1
A propósito, essa foi uma ótima pergunta. Eu amo qualquer coisa que me force a seguir as especificações :)
Jon Skeet
1
Observe que seu código de amostra será compilado se você usar C # 7.3 ( <LangVersion>7.3</LangVersion>) ou posterior, graças aos candidatos a sobrecarga aprimorados .
0xced

Respostas:

97

Em primeiro lugar, deixe-me apenas dizer que a resposta de Jon está correta. Esta é uma das partes mais cabeludas da especificação, tão boa para Jon mergulhar nela de cabeça.

Em segundo lugar, deixe-me dizer que esta linha:

Existe uma conversão implícita de um grupo de métodos para um tipo de delegado compatível

(ênfase adicionada) é profundamente enganosa e infeliz. Vou ter uma conversa com Mads sobre como remover a palavra "compatível" aqui.

A razão pela qual isso é enganoso e lamentável é porque parece que isso está chamando a atenção para a seção 15.2, "Compatibilidade de delegados". A Seção 15.2 descreveu a relação de compatibilidade entre métodos e tipos de delegado , mas esta é uma questão de conversibilidade de grupos de métodos e tipos de delegado , que é diferente.

Agora que já resolvemos isso, podemos percorrer a seção 6.6 da especificação e ver o que temos.

Para fazer a resolução de sobrecarga, precisamos primeiro determinar quais sobrecargas são candidatas aplicáveis . Um candidato é aplicável se todos os argumentos forem implicitamente conversíveis para os tipos de parâmetros formais. Considere esta versão simplificada do seu programa:

class Program
{
    delegate void D1();
    delegate string D2();
    static string X() { return null; }
    static void Y(D1 d1) {}
    static void Y(D2 d2) {}
    static void Main()
    {
        Y(X);
    }
}

Então, vamos analisar linha por linha.

Existe uma conversão implícita de um grupo de métodos para um tipo de delegado compatível.

Já discuti como a palavra "compatível" é lamentável aqui. Se movendo. Estamos nos perguntando, ao fazer a resolução de sobrecarga em Y (X), o grupo de métodos X é convertido para D1? Ele converte para D2?

Dado um delegado tipo D e uma expressão E que é classificada como um grupo de métodos, existe uma conversão implícita de E para D se E contiver pelo menos um método que seja aplicável [...] a uma lista de argumentos construída pelo uso do parâmetro tipos e modificadores de D, conforme descrito a seguir.

Por enquanto, tudo bem. X pode conter um método aplicável às listas de argumentos de D1 ou D2.

A aplicação de tempo de compilação de uma conversão de um grupo de métodos E para um tipo delegado D é descrita a seguir.

Esta linha realmente não diz nada de interessante.

Observe que a existência de uma conversão implícita de E para D não garante que a aplicação de tempo de compilação da conversão será bem-sucedida sem erros.

Essa linha é fascinante. Isso significa que existem conversões implícitas que existem, mas que estão sujeitas a serem transformadas em erros! Esta é uma regra bizarra do C #. Para divagar um momento, aqui está um exemplo:

void Q(Expression<Func<string>> f){}
string M(int x) { ... }
...
int y = 123;
Q(()=>M(y++));

Uma operação de incremento é ilegal em uma árvore de expressão. No entanto, o lambda ainda é conversível para o tipo de árvore de expressão, mesmo que se a conversão for usada, é um erro! O princípio aqui é que podemos querer mudar as regras do que pode ir em uma árvore de expressão posteriormente; alterar essas regras não deve alterar as regras do sistema de tipo . Queremos forçá-lo a tornar seus programas inequívocos agora , de modo que, quando alterarmos as regras para árvores de expressão no futuro para torná-las melhores, não introduzamos alterações significativas na resolução de sobrecarga .

De qualquer forma, este é outro exemplo desse tipo de regra bizarra. Uma conversão pode existir para fins de resolução de sobrecarga, mas pode ser um erro para realmente usar. Embora, na verdade, essa não seja exatamente a situação em que estamos aqui.

Se movendo:

Um único método M é selecionado correspondendo a uma invocação de método da forma E (A) [...] A lista de argumentos A é uma lista de expressões, cada uma classificada como uma [...] variável do parâmetro correspondente no formal -lista de parâmetros de D.

ESTÁ BEM. Portanto, sobrecarregamos a resolução em X em relação a D1. A lista de parâmetros formal de D1 está vazia, então fazemos resolução de sobrecarga em X () e joy, encontramos um método "string X ()" que funciona. Da mesma forma, a lista formal de parâmetros de D2 está vazia. Novamente, descobrimos que "string X ()" é um método que funciona aqui também.

O princípio aqui é que determinar a conversibilidade do grupo de métodos requer a seleção de um método de um grupo de métodos usando a resolução de sobrecarga , e a resolução de sobrecarga não considera os tipos de retorno .

Se o algoritmo [...] produzir um erro, ocorrerá um erro em tempo de compilação. Caso contrário, o algoritmo produz um único melhor método M com o mesmo número de parâmetros que D e a conversão é considerada existente.

Existe apenas um método no grupo de métodos X, portanto, deve ser o melhor. Provamos com sucesso que existe uma conversão de X para D1 e de X para D2.

Agora, esta linha é relevante?

O método selecionado M deve ser compatível com o delegado tipo D, caso contrário, ocorrerá um erro de tempo de compilação.

Na verdade, não, não neste programa. Nunca chegamos ao ponto de ativar essa linha. Porque, lembre-se, o que estamos fazendo aqui é tentar resolver a sobrecarga em Y (X). Temos dois candidatos Y (D1) e Y (D2). Ambos são aplicáveis. Qual é melhor ? Em nenhum lugar da especificação descrevemos a melhor relação entre essas duas conversões possíveis .

Agora, alguém certamente poderia argumentar que uma conversão válida é melhor do que aquela que produz um erro. Isso significaria efetivamente, neste caso, que a resolução de sobrecarga considera os tipos de retorno, que é algo que queremos evitar. A questão então é qual princípio é melhor: (1) manter o invariante de que a resolução de sobrecarga não considera os tipos de retorno, ou (2) tentar escolher uma conversão que sabemos que funcionará sobre outra que sabemos que não funcionará?

Este é um julgamento. Com lambdas , nós fazer considerar o tipo de retorno nesses tipos de conversões, na seção 7.4.3.3:

E é uma função anônima, T1 e T2 são tipos de delegado ou tipos de árvore de expressão com listas de parâmetros idênticas, um tipo de retorno inferido X existe para E no contexto dessa lista de parâmetros e um dos seguintes é válido:

  • T1 tem um tipo de retorno Y1 e T2 tem um tipo de retorno Y2, e a conversão de X para Y1 é melhor do que a conversão de X para Y2

  • T1 tem um tipo de retorno Y e T2 não tem retorno

É lamentável que as conversões de grupo de métodos e conversões lambda sejam inconsistentes a esse respeito. No entanto, posso viver com isso.

De qualquer forma, não temos uma regra de "melhoras" para determinar qual conversão é melhor, X para D1 ou X para D2. Portanto, fornecemos um erro de ambigüidade na resolução de Y (X).

Eric Lippert
fonte
8
Cracking - muito obrigado pela resposta e (com sorte) pela melhoria resultante na especificação :) Pessoalmente, acho que seria razoável para a resolução de sobrecarga levar em conta o tipo de retorno para conversões de grupo de métodos a fim de tornar o comportamento mais intuitivo, mas Eu entendo que faria isso à custa da consistência. (O mesmo pode ser dito da inferência de tipo genérico aplicada a conversões de grupo de métodos quando há apenas um método no grupo de métodos, como acho que discutimos antes.)
Jon Skeet
35

EDIT: Acho que entendi.

Como diz zinglon, é porque há uma conversão implícita de GetStringpara Action, embora o aplicativo de tempo de compilação falhe. Aqui está a introdução à seção 6.6, com alguma ênfase (minha):

Existe uma conversão implícita (§6.1) de um grupo de métodos (§7.1) para um tipo de delegado compatível. Dado um delegado tipo D e uma expressão E que é classificada como um grupo de métodos, existe uma conversão implícita de E para D se E contiver pelo menos um método que seja aplicável em sua forma normal (§7.4.3.1) para uma lista de argumentos construída pelo uso dos tipos de parâmetro e modificadores de D , conforme descrito a seguir.

Agora, eu estava ficando confuso com a primeira frase - que fala sobre uma conversão para um tipo de delegado compatível. Actionnão é um delegado compatível para qualquer método no GetStringgrupo de métodos, mas o GetString()método é aplicável em sua forma normal a uma lista de argumentos construída pelo uso dos tipos de parâmetro e modificadores de D. Observe que isso não fala sobre o tipo de retorno de D. É por isso que está ficando confuso ... porque ele apenas verificaria a compatibilidade do delegado GetString()ao aplicar a conversão, não verificando sua existência.

Acho que é instrutivo deixar a sobrecarga fora da equação brevemente e ver como essa diferença entre a existência de uma conversão e sua aplicabilidade pode se manifestar. Aqui está um exemplo curto, mas completo:

using System;

class Program
{
    static void ActionMethod(Action action) {}
    static void IntMethod(int x) {}

    static string GetString() { return ""; }

    static void Main(string[] args)
    {
        IntMethod(GetString);
        ActionMethod(GetString);
    }
}

Nenhuma das expressões de chamada de método em Maincompilações, mas as mensagens de erro são diferentes. Aqui está aquele para IntMethod(GetString):

Test.cs (12,9): erro CS1502: A melhor correspondência de método sobrecarregado para 'Program.IntMethod (int)' tem alguns argumentos inválidos

Em outras palavras, a seção 7.4.3.1 da especificação não pode encontrar nenhum membro de função aplicável.

Ora aqui está o erro para ActionMethod(GetString):

Test.cs (13,22): erro CS0407: 'string Program.GetString ()' tem o tipo de retorno errado

Desta vez, ele descobriu o método que deseja chamar - mas não conseguiu realizar a conversão necessária. Infelizmente, não consigo descobrir a parte da especificação em que a verificação final é realizada - parece que pode ser no 7.5.5.1, mas não consigo ver exatamente onde.


A resposta antiga foi removida, exceto por esta parte - porque espero que Eric possa esclarecer o "porquê" dessa pergunta ...

Ainda procurando ... entretanto, se dissermos "Eric Lippert" três vezes, você acha que receberemos uma visita (e, portanto, uma resposta)?

Jon Skeet
fonte
@Jon - poderia ser isso classWithSimpleMethods.GetStringe classWithSimpleMethods.DoNothingnão são delegados?
Daniel A. White
@Daniel: Não - essas expressões são expressões de grupo de métodos, e os métodos sobrecarregados só devem ser considerados aplicáveis ​​quando há uma conversão implícita do grupo de métodos para o tipo de parâmetro relevante. Consulte a seção 7.4.3.1 da especificação.
Jon Skeet
Lendo a seção 6.6, parece que a conversão de classWithSimpleMethods.GetString para Action é considerada existente, uma vez que as listas de parâmetros são compatíveis, mas que a conversão (se tentada) falha no tempo de compilação. Portanto, uma conversão implícita não existem para ambos os tipos de delegado e a chamada é ambígua.
zinglon
@zinglon: Como você está lendo §6.6 para determinar se uma conversão de ClassWithSimpleMethods.GetStringpara Actioné válida? Para um método Mser compatível com um tipo de delegado D(§15.2) "uma identidade ou conversão de referência implícita existe do tipo de retorno de Mpara o tipo de retorno de D."
Jason
@Jason: A especificação não diz que a conversão é válida, mas sim que existe . Na verdade, é inválido, pois falha em tempo de compilação. Os primeiros dois pontos de §6.6 determinam se a conversão existe. Os pontos a seguir determinam se a conversão será bem-sucedida. Do ponto 2: "Caso contrário, o algoritmo produz um único melhor método M com o mesmo número de parâmetros que D e a conversão é considerada existente." §15.2 é invocado no ponto 3.
zinglon
1

Usar Func<string>e Action<string>(obviamente muito diferente de Actione Func<string>) no ClassWithDelegateMethodsremove a ambigüidade.

A ambigüidade também ocorre entre Actione Func<int>.

Também recebo o erro de ambiguidade com este:

class Program
{ 
    static void Main(string[] args) 
    { 
        ClassWithSimpleMethods classWithSimpleMethods = new ClassWithSimpleMethods(); 
        ClassWithDelegateMethods classWithDelegateMethods = new ClassWithDelegateMethods(); 

        classWithDelegateMethods.Method(classWithSimpleMethods.GetOne);
    } 
} 

class ClassWithDelegateMethods 
{ 
    public void Method(Func<int> func) { /* do something */ }
    public void Method(Func<string> func) { /* do something */ } 
}

class ClassWithSimpleMethods 
{ 
    public string GetString() { return ""; } 
    public int GetOne() { return 1; }
} 

Outras experiências mostram que, ao passar em um grupo de métodos por conta própria, o tipo de retorno é completamente ignorado ao determinar qual sobrecarga usar.

class Program
{
    static void Main(string[] args)
    {
        ClassWithSimpleMethods classWithSimpleMethods = new ClassWithSimpleMethods();
        ClassWithDelegateMethods classWithDelegateMethods = new ClassWithDelegateMethods();

        //The call is ambiguous between the following methods or properties: 
        //'test.ClassWithDelegateMethods.Method(System.Func<int,int>)' 
        //and 'test.ClassWithDelegateMethods.Method(test.ClassWithDelegateMethods.aDelegate)'
        classWithDelegateMethods.Method(classWithSimpleMethods.GetX);
    }
}

class ClassWithDelegateMethods
{
    public delegate string aDelegate(int x);
    public void Method(Func<int> func) { /* do something */ }
    public void Method(Func<string> func) { /* do something */ }
    public void Method(Func<int, int> func) { /* do something */ }
    public void Method(Func<string, string> func) { /* do something */ }
    public void Method(aDelegate ad) { }
}

class ClassWithSimpleMethods
{
    public string GetString() { return ""; }
    public int GetOne() { return 1; }
    public string GetX(int x) { return x.ToString(); }
} 
Matt Ellen
fonte
0

A sobrecarga com Funce Actioné semelhante (porque ambos são delegados) para

string Function() // Func<string>
{
}

void Function() // Action
{
}

Se você notar, o compilador não sabe qual chamar porque eles diferem apenas pelos tipos de retorno.

Daniel A. White
fonte
Eu não acho que seja bem assim - porque você não pode converter a Func<string>em Action... e não pode converter um grupo de métodos consistindo apenas em um método que retorna uma string em um Action.
Jon Skeet
2
Você não pode lançar um delegado que não tem parâmetros e retorna stringpara um Action. Não vejo por que existe ambigüidade.
jason
3
@dtb: Sim, remover a sobrecarga remove o problema - mas isso não explica realmente por que existe um problema.
Jon Skeet