Resolução de Sobrecarga de Método

O enigma desta edição é-nos trazido por Jon Skeet.

Dado o seguinte código:

class X
{
  static int M(Func<int?, byte> x, object y) { return 1; }
  static int M(Func<X, byte> x, string y) { return 2; }

  const int Value = 1000;

  static void Main()
  {
    var a = M(X => (byte)X.Value, null);

    unchecked
    {
      Console.WriteLine(a);
      Console.WriteLine(M(X => (byte)X.Value, null));
    }
  }
}

Qual é o resultado da sua execução?

Resultado

O resultado da execução é:

1
2

O que se passa aqui? Porque é que simplesmente passar a expressão para um bloco unchecked causa um comportamento diferente?

Explicação

A expressão em que nos devemos concentrar é esta:

M(X => (byte)X.Value, null)

Trata-se apenas de uma chamada a um método usando uma expressão lambda, usando resolução de sobrecarga de método (method overload resolution) para determinar qual a sobrecarga a chamar e o tipo de inferência para determinar o tipo de argumentos para o método.

Simplificando a descrição da resolução de sobrecarga de método, seguem-se os seguintes passos:

  • Determina-se que sobrecargas (overloads) são aplicáveis (isto é, quais fazem sentido em termos dos argumentos fornecidos e os correspondentes parâmetros)
  • Compara-se as sobrecargas aplicáveis entre si em termos de qual “a melhor”
  • Se uma sobrecarga é “melhor” que todas as outras, usar essa
  • Se não há sobrecargas aplicáveis, ou nenhuma é melhor que as outras, então a chamada é inválida e leva a um erro de compilação

Pode haver a tentação de saltar diretamente para a segunda opção, assumindo que ambas as sobrecargas são válidas em ambos os casos.

Sobrecarga 1 – um parâmetro simples

Primeiro olhemos para a primeira sobrecarga: aquela onde o primeiro parâmetro é do tipo Func<int?, byte>. O que significa a expressão lambda X => (byte)X.Value quando convertida para aquele tipo de delegate? É sempre válida?

A parte manhosa é perceber o que o nome‑simples X significa como parte de X.Value dentro da expressão lambda. Aqui a parte importante da especificação é o começo da seção §7.6.2 (nomes simples):

Um nome‑simples está na forma I ou na forma I<A1, …, AK>, onde I é um único identificador e <A1, …, AK> é uma lista opcional de tipos argumento. Quando não for especificada uma lista de tipos argumento, considera-se que K é zero. O nome‑simples é avaliado e classificado do seguinte modo:

  • Se K é zero e o nome‑simples aparece dentro de um bloco e se o espaço de declaração de variáveis do bloco (ou blocos incluídos) (§3.3) contem uma variável local, parâmetro ou constante e classificado como variável ou valor.

(A especificação continua com outros casos.) Então X refere-se ao parâmetro da expressão lambda, que é do tipo int? – portanto X.Value refere-se ao valor do parâmetro subjacente.

Sobrecarga 2 – um pouco mais de complexidade

E então a segunda sobrecarga? Nesta o tipo do primeiro parâmetro de M é Func<X, byte>, portanto está-se a tentar converter a mesma expressão lambda para esse tipo. Aqui a mesma parte da seção §7.6.2 é usada, mas também a seção §7.6.4.1 é envolvida para determinar o significado da expressão de acesso a membro X.Value:

No acesso a um membro da forma E.I, se E é um único identificador, e o significado de E como nome-simples (§7.6.2) é uma constante, campo, propriedade, variável local ou parâmetro com o mesmo tipo que o significado de E como nome‑de‑tipo (§3.8), então ambos os significados possíveis de E são permitidos. Os dois significados possíveis de E.I nunca são ambíguos, uma vez que I deve necessariamente ser um membro do tipo E em ambos os casos. Por outras palavras, a regra apenas permite acesso aos membros estáticos de E onde, caso contrário, um erro de compilação teria ocorrido.

Não é bem explícito aqui, mas a razão de que não pode ser ambíguo é porque não podem existir dois membros com o mesmo nome no mesmo tipo (para além da sobrecarga de métodos). Pode existir sobrecarga de métodos que são ambos estáticos e de instância, mas então as regras normais de sobrecarga de métodos são aplicadas.

Sendo assim, X.Value neste caso, não envolve em nada a utilização do parâmetro chamado X. Em vez disso, é a constante chamada Value dentro da classe X. Note-se que isto acontece apenas porque o tipo do parâmetro é o mesmo que se o tipo a que o nome do parâmetro se refere. (Não é bem o mesmo que os nome serem os mesmos. Se uma diretiva using introduzir um alias, como using Y = X; então a condição pode ser satisfeita com nomes diferentes.)

Mas então que diferença faz unchecked?

Func<int?, byte> foo = X => (byte)X.Value;
Func<X, byte> bar = X => (byte)X.Value;

De volta a sobreposições

var a = M(X => (byte)X.Value, null);

Recursos

Publicado na edição 44 (PDF) da Revista PROGRAMAR.