Por que adicionar um método adicionaria uma chamada ambígua, se não estaria envolvida na ambigüidade

112

Eu tenho essa aula

public class Overloaded
{
    public void ComplexOverloadResolution(params string[] something)
    {
        Console.WriteLine("Normal Winner");
    }

    public void ComplexOverloadResolution<M>(M something)
    {
        Console.WriteLine("Confused");
    }
}

Se eu chamar assim:

        var blah = new Overloaded();
        blah.ComplexOverloadResolution("Which wins?");

Ele grava Normal Winnerno console.

Mas, se eu adicionar outro método:

    public void ComplexOverloadResolution(string something, object somethingElse = null)
    {
        Console.WriteLine("Added Later");
    }

Estou tendo o erro a seguir:

A chamada é ambígua entre os seguintes métodos ou propriedades:> ' Overloaded.ComplexOverloadResolution(params string[])' e ' Overloaded.ComplexOverloadResolution<string>(string)'

Posso entender que adicionar um método pode apresentar uma ambigüidade de chamada, mas é uma ambigüidade entre os dois métodos que já existiam (params string[])e <string>(string)! Claramente, nenhum dos dois métodos envolvidos na ambigüidade é o método recém-adicionado, porque o primeiro é um parâmetro e o segundo é genérico.

Isso é um inseto? Que parte da especificação diz que esse deve ser o caso?

McKay
fonte
2
Não acho que 'Overloaded.ComplexOverloadResolution(string)'se refira ao <string>(string)método; Acho que se refere ao (string, object)método sem objeto fornecido.
phoog
1
@phoog Oh, esses dados foram cortados por StackOverflow porque é uma tag, mas a mensagem de erro tem o designador de modelo. Estou adicionando de volta.
McKay
você me pegou! Citei as seções relevantes das especificações em minha resposta, mas não passei a última meia hora lendo e entendendo-as!
phoog
@phoog, olhando por essas partes da especificação, não vejo nada sobre a introdução de ambigüidade em métodos diferentes de si mesmo e outro método, não dois outros métodos.
McKay
Ocorreu-me que isso é apenas pedra-papel-tesoura : qualquer conjunto de dois valores distintos tem um vencedor, mas o conjunto completo de três valores não.
phoog de

Respostas:

107

Isso é um inseto?

Sim.

Parabéns, você encontrou um bug na resolução de sobrecarga. O bug se reproduz em C # 4 e 5; não se reproduz na versão "Roslyn" do analisador semântico. Informei a equipe de teste do C # 5 e espero que possamos investigar e resolver isso antes do lançamento final. (Como sempre, sem promessas.)

Segue uma análise correta. Os candidatos são:

0: C(params string[]) in its normal form
1: C(params string[]) in its expanded form
2: C<string>(string) 
3: C(string, object) 

O candidato zero é obviamente inaplicável porque stringnão é conversível para string[]. Isso deixa três.

Dos três, devemos determinar o melhor método exclusivo. Fazemos isso fazendo comparações entre pares dos três candidatos restantes. Existem três desses pares. Todos eles têm listas de parâmetros idênticas, uma vez que retiramos os parâmetros opcionais omitidos, o que significa que temos que ir para a rodada de desempate avançada descrita na seção 7.5.3.2 da especificação.

Qual é melhor, 1 ou 2? O desempate relevante é que um método genérico é sempre pior do que um método não genérico. 2 é pior do que 1. Portanto, 2 não pode ser o vencedor.

Qual é melhor, 1 ou 3? O critério de desempate relevante é: um método aplicável apenas em sua forma expandida é sempre pior do que um método aplicável em sua forma normal. Portanto, 1 é pior do que 3. Portanto, 1 não pode ser o vencedor.

Qual é melhor, 2 ou 3? O desempate relevante é que um método genérico é sempre pior do que um método não genérico. 2 é pior do que 3. Portanto, 2 não pode ser o vencedor.

Para ser escolhido a partir de um conjunto de vários candidatos aplicáveis, um candidato deve estar (1) invicto, (2) vencer pelo menos um outro candidato e (3) ser o único candidato que possui as duas primeiras propriedades. O candidato três não é derrotado por nenhum outro candidato e vence pelo menos um outro candidato; é o único candidato com essa propriedade. Portanto, o candidato três é o único melhor candidato . Deve vencer.

Não apenas o compilador C # 4 está errando, como você observa corretamente que ele está relatando uma mensagem de erro bizarra. É um pouco surpreendente que o compilador esteja errando na análise de resolução de sobrecarga. Que ele esteja recebendo a mensagem de erro errada não é nada surpreendente; a heurística de erro de "método ambíguo" basicamente escolhe quaisquer dois métodos do conjunto de candidatos se um melhor método não puder ser determinado. Não é muito bom encontrar a ambigüidade "real", se é que ela existe.

Alguém pode razoavelmente perguntar por que isso acontece. É bastante complicado encontrar dois métodos que sejam "inequivocamente ambíguos" porque a relação de "melhor qualidade" é intransitiva . É possível encontrar situações em que o candidato 1 é melhor que 2, 2 é melhor que 3 e 3 é melhor que 1. Em tais situações, não podemos fazer melhor do que escolher dois deles como "os ambíguos".

Eu gostaria de melhorar essa heurística para Roslyn, mas é uma prioridade baixa.

(Exercício para o leitor: "Elabore um algoritmo de tempo linear para identificar o melhor membro único de um conjunto de n elementos onde a relação de melhor é intransitiva" foi uma das perguntas que me fizeram no dia em que entrevistei para esta equipe. Não é um algoritmo muito difícil; experimente.)

Um dos motivos pelos quais adiamos adicionar argumentos opcionais ao C # por tanto tempo foi o número de situações ambíguas complexas que ele introduz no algoritmo de resolução de sobrecarga; aparentemente, não acertamos.

Se desejar inserir um problema do Connect para rastreá-lo, fique à vontade. Se você apenas deseja que isso seja levado ao nosso conhecimento, considere feito. Vou continuar com testes no próximo ano.

Obrigado por trazer isso à minha atenção. Desculpas pelo erro.

Eric Lippert
fonte
1
Obrigado pela resposta. você disse "1 é pior que 2", mas ele seleciona o método 1 se eu tiver apenas os métodos 1 e 2?
McKay
@McKay: Opa, você está certo, eu disse isso ao contrário. Vou consertar o texto.
Eric Lippert
1
É estranho ler a frase "o resto do ano", considerando que não sobrou nem meia semana :)
BoltClock
2
@BoltClock de fato, a declaração "deixando para o resto do ano" implica um dia de folga.
phoog
1
Acho que sim. Eu li "3) ser o único candidato que tem as duas primeiras propriedades" como "ser o único candidato que (está invicto e vence pelo menos um outro candidato)" . Mas seu comentário mais recente me faz pensar "(seja o único candidato invicto) e vença pelo menos um outro candidato" . O inglês realmente poderia usar símbolos de agrupamento. Se o último for verdade, então eu entendo novamente.
default.kramer
5

Que parte da especificação diz que esse deve ser o caso?

Seção 7.5.3 (resolução de sobrecarga), junto com as seções 7.4 (pesquisa de membro) e 7.5.2 (inferência de tipo).

Observe especialmente a seção 7.5.3.2 (melhor membro de função), que diz em parte "parâmetros opcionais sem argumentos correspondentes são removidos da lista de parâmetros" e "Se M (p) é um método não genérico e M (q) é um método genérico, então M (p) é melhor do que M (q). "

No entanto, não entendo essas partes da especificação completamente o suficiente para saber quais partes da especificação controlam esse comportamento, muito menos para julgar se é compatível.

phoog
fonte
Mas isso não explica por que adicionar um membro causaria uma ambigüidade entre dois métodos que já existiam.
McKay
@McKay justo o suficiente (veja a edição). Teremos apenas que esperar que Eric Lippert nos diga se este é o comportamento correto: ->
phoog
1
Essas são as partes certas da especificação, sim. O problema é que dizem que não deveria ser assim!
Eric Lippert
3

você pode evitar esta ambigüidade mudando o nome do primeiro parâmetro em alguns métodos e especificando o parâmetro que deseja atribuir

como isso :

public class Overloaded
{
    public void ComplexOverloadResolution(params string[] somethings)
    {
        Console.WriteLine("Normal Winner");
    }

    public void ComplexOverloadResolution<M>(M something)
    {
        Console.WriteLine("Confused");
    }

    public void ComplexOverloadResolution(string something, object somethingElse = null)
    {
        Console.WriteLine("Added Later");
    }
}

class Program
{
    static void Main(string[] args)
    {
        Overloaded a = new Overloaded();
        a.ComplexOverloadResolution(something:"asd");
    }
}
MhdSyrwan
fonte
Ah, eu sei que este código é ruim e existem várias maneiras de contornar isso, a pergunta era "por que o compilador se comporta dessa maneira?"
McKay
1

Se você remover paramsdo seu primeiro método, isso não acontecerá. O primeiro e o terceiro método têm chamadas válidas ComplexOverloadResolution(string), mas se o primeiro método for, public void ComplexOverloadResolution(string[] something)não haverá ambigüidade.

Fornecer valor para um parâmetro object somethingElse = nulltorna-o parâmetro opcional e, portanto, não precisa ser especificado ao chamar essa sobrecarga.

Edit: O compilador está fazendo algumas coisas malucas aqui. Se você mover seu terceiro método no código após o primeiro, ele relatará corretamente. Portanto, parece que está pegando as duas primeiras sobrecargas e relatando-as, sem verificar a correta.

'ConsoleApplication1.Program.ComplexOverloadResolution (params string [])' e 'ConsoleApplication1.Program.ComplexOverloadResolution (string, objeto)'

Edit2: Nova descoberta. Remover qualquer método dos três acima não produzirá ambigüidade entre os dois. Portanto, parece que o conflito só aparece se houver três métodos presentes, independentemente da ordem.

Tomislav Markovski
fonte
Mas isso não explica por que adicionar um membro causaria uma ambigüidade entre dois métodos que já existiam.
McKay
A ambigüidade acontece entre o primeiro e o terceiro método, mas por que o compilador relata os outros dois está além da minha compreensão.
Tomislav Markovski
Mas se eu remover o segundo método, não tenho ambigüidade, ele chama com sucesso o terceiro método. Portanto, não parece que o compilador tenha uma ambigüidade entre o primeiro e o terceiro métodos.
McKay
Veja minha edição. Compiladores malucos.
Tomislav Markovski
Na verdade, qualquer combinação de apenas dois métodos não produz ambigüidade. Isso é muito estranho. Edit2.
Tomislav Markovski
1
  1. Se você escrever

    var blah = new Overloaded();
    blah.ComplexOverloadResolution("Which wins?");

    ou apenas escreva

    var blah = new Overloaded();
    blah.ComplexOverloadResolution();

    vai acabar no mesmo método , no método

    public void ComplexOverloadResolution(params string[] something

    Esta é a paramspalavra-chave de causa que faz com que seja a melhor correspondência também para o caso quando nenhum parâmetro especificado

  2. Se você tentar adicionar seu novo método como este

    public void ComplexOverloadResolution(string something)
    {
        Console.WriteLine("Added Later");
    }

    Ele irá compilar e chamar perfeitamente este método, pois é uma combinação perfeita para sua chamada com um stringparâmetro. Muito mais forte então params string[] something.

  3. Você declara o segundo método como você fez

    public void ComplexOverloadResolution(string something, object something=null);

    Compilador, salta na confusão total entre o primeiro método e este, acabou de adicionar um. Porque ele não sabe qual função ele deveria agora em sua chamada

    var blah = new Overloaded();
    blah.ComplexOverloadResolution("Which wins?");

    De fato, se você remover o parâmetro string da chamada, como um código a seguir, tudo compila corretamente e funciona como antes

    var blah = new Overloaded();
    blah.ComplexOverloadResolution(); // will be ComplexOverloadResolution(params string[] something) function called here, like a best match.
Tigran
fonte
Primeiro, você escreve dois casos de chamada que são iguais. Então, é claro que seguirá o mesmo método, ou você pretendia escrever algo diferente?
McKay
Mas, novamente, se estou entendendo o resto da sua resposta corretamente, você não está lendo o que o compilador diz ser a confusão, ou seja, entre o primeiro e o segundo métodos, não o novo terceiro que acabei de adicionar.
McKay
ah obrigado. Mas isso ainda deixa o problema que menciono no meu segundo comentário ao seu post.
McKay
Para ser mais claro, você afirma "Compilador, salta em total confusão entre o primeiro método e este, acabou de adicionar um." Mas não é. É um salto em confusão para os outros dois métodos. O método params e o método genérico.
McKay
@McKay: bem, é um salto na confusão ter um dado statede 3 funções, não uma ou duas delas, para ser mais preciso. De fato, basta comentar qualquer um deles, para resolver um problema. A melhor correspondência entre as funções disponíveis é aquela com params, a segunda é aquela com genericsparâmetro, quando adicionamos a terceira cria uma confusão em uma setdas funções. Acho que, provavelmente, não é uma mensagem de erro clara produzida pelo compilador.
Tigran