Nota: isso parece ter sido corrigido em Roslyn
Essa pergunta surgiu ao escrever minha resposta a esta , que fala sobre a associatividade do operador de coalescência nula .
Apenas como lembrete, a ideia do operador de coalescência nula é que uma expressão da forma
x ?? y
primeiro avalia x
e depois:
- Se o valor de
x
for nulo,y
for avaliado e esse for o resultado final da expressão - Se o valor de
x
for não nulo, nãoy
for avaliado e o valor de for o resultado final da expressão, após uma conversão para o tipo de tempo de compilação, se necessáriox
y
Agora, normalmente, não há necessidade de conversão, ou é apenas de um tipo anulável para um não anulável - geralmente os tipos são os mesmos ou apenas de (digamos) int?
para int
. No entanto, você pode criar seus próprios operadores implícitos de conversão, e esses são usados sempre que necessário.
Para o simples caso de x ?? y
, eu não vi nenhum comportamento estranho. No entanto, com (x ?? y) ?? z
eu vejo algum comportamento confuso.
Aqui está um programa de teste curto, mas completo - os resultados estão nos comentários:
using System;
public struct A
{
public static implicit operator B(A input)
{
Console.WriteLine("A to B");
return new B();
}
public static implicit operator C(A input)
{
Console.WriteLine("A to C");
return new C();
}
}
public struct B
{
public static implicit operator C(B input)
{
Console.WriteLine("B to C");
return new C();
}
}
public struct C {}
class Test
{
static void Main()
{
A? x = new A();
B? y = new B();
C? z = new C();
C zNotNull = new C();
Console.WriteLine("First case");
// This prints
// A to B
// A to B
// B to C
C? first = (x ?? y) ?? z;
Console.WriteLine("Second case");
// This prints
// A to B
// B to C
var tmp = x ?? y;
C? second = tmp ?? z;
Console.WriteLine("Third case");
// This prints
// A to B
// B to C
C? third = (x ?? y) ?? zNotNull;
}
}
Portanto, temos três tipos de valores personalizados A
, B
e C
, com conversões de A para B, A para C e B para C.
Eu posso entender o segundo e o terceiro casos ... mas por que há uma conversão extra de A para B no primeiro caso? Em particular, eu realmente esperava que o primeiro e o segundo casos fossem a mesma coisa - afinal, é apenas extrair uma expressão em uma variável local.
Algum comprador sobre o que está acontecendo? Estou extremamente hesitante em chorar "bug" quando se trata do compilador C #, mas estou perplexo com o que está acontecendo ...
EDIT: Ok, aqui está um exemplo mais desagradável do que está acontecendo, graças à resposta do configurador, o que me dá mais motivos para pensar que é um bug. EDIT: A amostra nem precisa de dois operadores coalescentes nulos agora ...
using System;
public struct A
{
public static implicit operator int(A input)
{
Console.WriteLine("A to int");
return 10;
}
}
class Test
{
static A? Foo()
{
Console.WriteLine("Foo() called");
return new A();
}
static void Main()
{
int? y = 10;
int? result = Foo() ?? y;
}
}
A saída disso é:
Foo() called
Foo() called
A to int
O fato de Foo()
ser chamado duas vezes aqui é extremamente surpreendente para mim - não vejo motivo para a expressão ser avaliada duas vezes.
fonte
C? first = ((B?)(((B?)x) ?? ((B?)y))) ?? ((C?)z);
. Você obterá:Internal Compiler Error: likely culprit is 'CODEGEN'
(("working value" ?? "user default") ?? "system default")
Respostas:
Obrigado a todos que contribuíram para analisar esse problema. É claramente um bug do compilador. Parece ocorrer apenas quando há uma conversão levantada envolvendo dois tipos anuláveis no lado esquerdo do operador coalescente.
Ainda não identifiquei onde exatamente as coisas dão errado, mas em algum momento durante a fase de compilação "anulável" - após a análise inicial, mas antes da geração do código - reduzimos a expressão
do exemplo acima ao equivalente moral de:
Claramente isso está incorreto; a descida correta é
Meu melhor palpite, com base em minha análise até agora, é que o otimizador anulável está saindo dos trilhos aqui. Temos um otimizador anulável que procura situações em que sabemos que uma expressão específica do tipo anulável não pode ser nula. Considere a seguinte análise ingênua: podemos dizer primeiro que
é o mesmo que
e então podemos dizer que
é o mesmo que
Mas o otimizador pode intervir e dizer "uau, espere um minuto, já verificamos que temp não é nulo; não há necessidade de checar nulos uma segunda vez só porque estamos chamando um operador de conversão levantado". Nós os otimizamos para apenas
Meu palpite é que estamos em algum lugar escondendo o fato de que a forma otimizada de
(int?)Foo()
é,new int?(op_implicit(Foo().Value))
mas essa não é realmente a forma otimizada que queremos; queremos a forma otimizada de Foo () - substituída por temporária e depois convertida.Muitos erros no compilador C # são resultado de decisões incorretas de armazenamento em cache. Uma palavra para o sábio: toda vez que você armazena em cache um fato para uso posterior, você potencialmente cria uma inconsistência, caso algo relevante mude . Nesse caso, o importante que mudou após a análise inicial é que a chamada para Foo () deve sempre ser realizada como uma busca temporária.
Reorganizamos bastante o passe de reescrita anulável no C # 3.0. O bug é reproduzido no C # 3.0 e 4.0, mas não no C # 2.0, o que significa que o bug provavelmente foi o meu mal. Desculpa!
Vou inserir um bug no banco de dados e veremos se podemos consertar isso para uma versão futura do idioma. Obrigado novamente a todos pela sua análise; foi muito útil!
UPDATE: reescrevi o otimizador anulável do zero para Roslyn; agora faz um trabalho melhor e evita esses tipos de erros estranhos. Para algumas reflexões sobre como o otimizador em Roslyn funciona, consulte minha série de artigos que começa aqui: https://ericlippert.com/2012/12/20/nullable-micro-optimizations-part-one/
fonte
Definitivamente, isso é um bug.
Este código produzirá:
Isso me fez pensar que a primeira parte de cada
??
expressão de coalescência é avaliada duas vezes. Este código provou:saídas:
Isso parece acontecer apenas quando a expressão requer uma conversão entre dois tipos anuláveis; Eu tentei várias permutações com um dos lados sendo uma string e nenhum deles causou esse comportamento.
fonte
X() ?? Y()
expandir-se internamente paraX() != null ? X() : Y()
, portanto, por que seria avaliado duas vezes.Se você der uma olhada no código gerado para o caso agrupado à esquerda, ele realmente fará algo como isto (
csc /optimize-
):Outra descoberta, se você o usar
first
, irá gerar um atalho se ambosa
eb
forem nulos e retornaremc
. No entanto, sea
oub
não for nulo, ele reavaliaa
como parte da conversão implícita paraB
antes de retornar quala
oub
não é nulo.Na especificação do C # 4.0, §6.1.4:
Isso parece explicar a segunda combinação de desembrulhar-embrulhar.
O compilador C # 2008 e 2010 produz um código muito semelhante, no entanto, isso parece uma regressão do compilador C # 2005 (8.00.50727.4927) que gera o seguinte código para o acima:
Gostaria de saber se isso não é devido à magia adicional dada ao sistema de inferência de tipos?
fonte
(x ?? y) ?? z
em lambdas aninhadas, o que garante uma avaliação em ordem sem avaliação dupla. Obviamente, essa não é a abordagem adotada pelo compilador C # 4.0. Pelo que sei, a seção 6.1.4 é abordada de maneira muito rigorosa nesse caminho de código específico e os temporários não são eliminados, resultando na dupla avaliação.Na verdade, chamarei isso de bug agora, com o exemplo mais claro. Isso ainda é válido, mas a dupla avaliação certamente não é boa.
Parece que
A ?? B
é implementado comoA.HasValue ? A : B
. Nesse caso, também existem muitas transmissões (após a transmissão regular para o ternário?:
operador ). Mas se você ignorar tudo isso, isso faz sentido com base em como é implementado:A ?? B
expande paraA.HasValue ? A : B
A
é nossox ?? y
. Expandir parax.HasValue : x ? y
(x.HasValue : x ? y).HasValue ? (x.HasValue : x ? y) : B
Aqui você pode ver que
x.HasValue
é verificado duas vezes e, sex ?? y
requer transmissão,x
será lançado duas vezes.Eu colocaria isso simplesmente como um artefato de comoTake-Away: não crie operadores de elenco implícitos com efeitos colaterais.??
é implementado, em vez de um bug do compilador.Parece ser um bug do compilador que gira em torno de como
??
é implementado. Take-away: não aninhe expressões coalescentes com efeitos colaterais.fonte
A() ? A() : B()
possivelmente será avaliadoA()
duas vezes, masA() ?? B()
não tanto. E já que isso só acontece no elenco ... Humm ... acabei de me convencer a pensar que certamente não está se comportando corretamente.Eu não sou um especialista em C #, como você pode ver no meu histórico de perguntas, mas tentei isso e acho que é um bug ... mas, como novato, devo dizer que não entendo tudo o que está acontecendo aqui, então vou excluir minha resposta se estiver longe.
Cheguei a essa
bug
conclusão criando uma versão diferente do seu programa que lida com o mesmo cenário, mas muito menos complicado.Estou usando três propriedades de número inteiro nulo com lojas de backup. Defino cada um como 4 e depois corro
int? something2 = (A ?? B) ?? C;
( Código completo aqui )
Isso apenas lê o A e nada mais.
Esta declaração para mim parece-me que deveria:
Portanto, como A não é nulo, ele apenas olha para A e termina.
No seu exemplo, colocar um ponto de interrupção no Primeiro Caso mostra que x, ye z não são nulos e, portanto, eu esperaria que eles fossem tratados da mesma forma que meu exemplo menos complexo .... mas eu tenho medo de ser demais. de um novato em C # e perdeu completamente o objetivo desta pergunta!
fonte
int
). Ele empurra o caso ainda mais para um canto obscuro, fornecendo várias conversões implícitas de tipo. Isso requer que o compilador altere o tipo de dados durante a verificaçãonull
. É por causa dessas conversões implícitas de tipo que o exemplo dele é diferente do seu.