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);