C # ok com a comparação de tipos de valor com nulo

85

Corri para isso hoje e não tenho ideia de por que o compilador C # não está gerando um erro.

Int32 x = 1;
if (x == null)
{
    Console.WriteLine("What the?");
}

Estou confuso sobre como x poderia ser nulo. Especialmente porque esta atribuição definitivamente lança um erro do compilador:

Int32 x = null;

É possível que x possa se tornar nulo, a Microsoft simplesmente decidiu não colocar essa verificação no compilador ou ela foi perdida completamente?

Atualização: depois de mexer no código para escrever este artigo, de repente o compilador apareceu com um aviso de que a expressão nunca seria verdadeira. Agora estou realmente perdido. Coloquei o objeto em uma classe e agora o aviso foi embora, mas deixei a questão, um tipo de valor pode acabar sendo nulo.

public class Test
{
    public DateTime ADate = DateTime.Now;

    public Test ()
    {
        Test test = new Test();
        if (test.ADate == null)
        {
            Console.WriteLine("What the?");
        }
    }
}
Joshua Belden
fonte
9
Você também pode escrever if (1 == 2). Não é função do compilador realizar a análise do caminho do código; é para isso que servem as ferramentas de análise estática e os testes de unidade.
Aaronaught
Por que o aviso foi embora, veja minha resposta; e não - não pode ser nulo.
Marc Gravell
1
Concordo em (1 == 2), eu estava mais pensando sobre a situação (1 == null)
Joshua Belden
Obrigado a todos que responderam. Tudo fazendo sentido agora.
Joshua Belden
Em relação ao aviso ou nenhum problema de aviso: Se a estrutura em questão for um "tipo simples", como int, o compilador gera avisos legais. Para os tipos simples, o ==operador é definido pela especificação da linguagem C #. Para outras estruturas (não do tipo simples), o compilador se esquece de emitir um aviso. Consulte o aviso do compilador errado ao comparar struct com nulo para obter detalhes Para estruturas que não são tipos simples, o ==operador deve ser sobrecarregado por um opeartor ==método que é membro da estrutura (caso contrário, não ==é permitido).
Jeppe Stig Nielsen

Respostas:

119

Isso é legal porque a resolução de sobrecarga do operador tem um único melhor operador para escolher. Existe um operador == que leva dois ints anuláveis. O int local é conversível em um int anulável. O literal nulo é conversível em um int anulável. Portanto, este é um uso legal do operador == e sempre resultará em falso.

Da mesma forma, também permitimos que você diga "if (x == 12.6)", que também será sempre falso. O int local é conversível em duplo, o literal é conversível em duplo e, obviamente, eles nunca serão iguais.

Eric Lippert
fonte
4
Re seu comentário: connect.microsoft.com/VisualStudio/feedback/…
Marc Gravell
5
@James: (retiro meu comentário errôneo anterior, que excluí.) Tipos de valor definidos pelo usuário que têm um operador de igualdade definido pelo usuário definido também por padrão têm um operador de igualdade definido pelo usuário elevado gerado para eles. O operador de igualdade definido pelo usuário suspenso é aplicável pelo motivo que você afirma: todos os tipos de valor são implicitamente conversíveis em seu tipo anulável correspondente, assim como o literal nulo. É não o caso de que um tipo de valor definido pelo utilizador que carece de um operador de comparação definido pelo utilizador é comparável ao literal nulo.
Eric Lippert
3
@James: Claro, você pode implementar seu próprio operador == e operador! = Que recebem estruturas anuláveis. Se eles existirem, o compilador os usará em vez de gerá-los para você automaticamente. (E, aliás, lamento que o aviso para o operador levantado sem sentido em operandos não anuláveis ​​não produza um aviso; isso é um erro no compilador que não conseguimos corrigir.)
Eric Lippert
2
Queremos nosso aviso! Nos merecemos isso.
Jeppe Stig Nielsen
3
@JamesDunne: Que tal definir um static bool operator == (SomeID a, String b)e marcá- lo com Obsolete? Se o segundo operando for um literal sem tipo null, seria uma combinação melhor do que qualquer forma que exigisse o uso de operadores elevados, mas se for um SomeID?que fosse igual null, o operador suspenso venceria.
supercat de
17

Não é um erro, pois há uma int?conversão ( ); ele gera um aviso no exemplo dado:

O resultado da expressão é sempre 'falso', pois um valor do tipo 'int' nunca é igual a 'nulo' do tipo 'int?'

Se você verificar o IL, verá que está completamente remove o branch inacessível - ele não existe em uma versão de lançamento.

Observe, entretanto, que ele não gera esse aviso para estruturas personalizadas com operadores de igualdade. Costumava no 2.0, mas não no compilador 3.0. O código ainda é removido (para que saiba que o código está inacessível), mas nenhum aviso é gerado:

using System;

struct MyValue
{
    private readonly int value;
    public MyValue(int value) { this.value = value; }
    public static bool operator ==(MyValue x, MyValue y) {
        return x.value == y.value;
    }
    public static bool operator !=(MyValue x, MyValue y) {
        return x.value != y.value;
    }
}
class Program
{
    static void Main()
    {
        int i = 1;
        MyValue v = new MyValue(1);
        if (i == null) { Console.WriteLine("a"); } // warning
        if (v == null) { Console.WriteLine("a"); } // no warning
    }
}

Com o IL (para Main) - observe que tudo, exceto MyValue(1)(que pode ter efeitos colaterais) foi removido:

.method private hidebysig static void Main() cil managed
{
    .entrypoint
    .maxstack 2
    .locals init (
        [0] int32 i,
        [1] valuetype MyValue v)
    L_0000: ldc.i4.1 
    L_0001: stloc.0 
    L_0002: ldloca.s v
    L_0004: ldc.i4.1 
    L_0005: call instance void MyValue::.ctor(int32)
    L_000a: ret 
}

isto é basicamente:

private static void Main()
{
    MyValue v = new MyValue(1);
}
Marc Gravell
fonte
1
Alguém também me relatou isso internamente recentemente. Não sei por que paramos de produzir esse aviso. Inserimos isso como um bug.
Eric Lippert
5

O fato de que uma comparação nunca pode ser verdadeira não significa que seja ilegal. No entanto, não, um tipo de valor sempre pode ser null.

Adam Robinson
fonte
1
Mas um tipo de valor pode ser igual a null. Considere int?, que é o açúcar sintático para Nullable<Int32>, que é um tipo de valor. Uma variável de tipo int?certamente poderia ser igual a null.
Greg
1
@Greg: Sim, pode ser igual a nulo, supondo que o "igual" ao qual você está se referindo seja o resultado do ==operador. É importante observar que a instância não é realmente nula.
Adam Robinson
1

Um tipo de valor não pode ser null, embora possa ser igual a null(considere Nullable<>). No seu caso, as intvariáveis ​​e nullsão implicitamente convertidas Nullable<Int32>e comparadas.

Greg
fonte
0

Suspeito que seu teste específico está apenas sendo otimizado pelo compilador quando ele gera o IL, já que o teste nunca será falso.

Nota lateral: é possível ter um Int32 anulável usando Int32? x em vez disso.

GrayWizardx
fonte
0

Eu acho que isso ocorre porque "==" é um açúcar de sintaxe que realmente representa a chamada para o System.Object.Equalsmétodo que aceita System.Objectparâmetro. Nulo pela especificação ECMA é um tipo especial do qual é derivado System.Object.

É por isso que há apenas um aviso.

Vitaly
fonte
Isso não é correto por dois motivos. Primeiro, == não tem a mesma semântica que Object.Equals quando um de seus argumentos é um tipo de referência. Em segundo lugar, null não é um tipo. Consulte a seção 7.9.6 da especificação se quiser entender como funciona o operador de igualdade de referência.
Eric Lippert
"O literal nulo (§9.4.4.6) avalia o valor nulo, que é usado para denotar uma referência que não aponta para nenhum objeto ou array, ou a ausência de um valor. O tipo nulo tem um único valor, que é o nulo valor. Portanto, uma expressão cujo tipo é o tipo nulo pode avaliar apenas o valor nulo. Não há maneira de escrever explicitamente o tipo nulo e, portanto, não há maneira de usá-lo em um tipo declarado. " - esta é uma citação da ECMA. Do que você está falando? Além disso, qual versão do ECMA você usa? Não vejo 7.9.6 no meu.
Vitaly
0

[EDITADO: transformou avisos em erros e tornou os operadores explícitos sobre anuláveis ​​em vez de hackear a string.]

De acordo com a sugestão inteligente de @supercat em um comentário acima, as seguintes sobrecargas de operador permitem gerar um erro sobre as comparações do seu tipo de valor personalizado com nulo.

Ao implementar operadores que se comparam a versões anuláveis ​​de seu tipo, o uso de nulo em uma comparação corresponde à versão anulável do operador, o que permite gerar o erro por meio do atributo Obsolete.

Até que a Microsoft nos devolva nosso aviso do compilador, continuarei com esta solução alternativa, obrigado @supercat!

public struct Foo
{
    private readonly int x;
    public Foo(int x)
    {
        this.x = x;
    }

    public override string ToString()
    {
        return string.Format("Foo {{x={0}}}", x);
    }

    public override int GetHashCode()
    {
        return x.GetHashCode();
    }

    public override bool Equals(Object obj)
    {
        return x.Equals(obj);
    }

    public static bool operator ==(Foo a, Foo b)
    {
        return a.x == b.x;
    }

    public static bool operator !=(Foo a, Foo b)
    {
        return a.x != b.x;
    }

    [Obsolete("The result of the expression is always 'false' since a value of type 'Foo' is never equal to 'null'", true)]
    public static bool operator ==(Foo a, Foo? b)
    {
        return false;
    }
    [Obsolete("The result of the expression is always 'true' since a value of type 'Foo' is never equal to 'null'", true)]
    public static bool operator !=(Foo a, Foo? b)
    {
        return true;
    }
    [Obsolete("The result of the expression is always 'false' since a value of type 'Foo' is never equal to 'null'", true)]
    public static bool operator ==(Foo? a, Foo b)
    {
        return false;
    }
    [Obsolete("The result of the expression is always 'true' since a value of type 'Foo' is never equal to 'null'", true)]
    public static bool operator !=(Foo? a, Foo b)
    {
        return true;
    }
}
yoyo
fonte
A menos que eu esteja perdendo algo, sua abordagem fará o compilador reclamar Foo a; Foo? b; ... if (a == b)..., mesmo que tal comparação seja perfeitamente legítima. A razão pela qual sugeri o "string hack" é que permitiria a comparação acima, mas seria desagradável if (a == null). Em vez de usar string, pode-se substituir qualquer tipo de referência diferente de Objectou ValueType; se desejado, pode-se definir uma classe fictícia com um construtor privado que nunca poderia ser chamado e intitulá-la ReferenceThatCanOnlyBeNull.
supercat
Você está absolutamente correto. Eu deveria ter esclarecido que minha sugestão quebra o uso de anuláveis ​​... que na base de código que estou trabalhando são considerados pecaminosos de qualquer maneira (boxe indesejado etc.). ;)
yoyo
0

Acho que a melhor resposta de por que o compilador aceita isso é para classes genéricas. Considere a seguinte classe ...

public class NullTester<T>
{
    public bool IsNull(T value)
    {
        return (value == null);
    }
}

Se o compilador não aceitar comparações com os nulltipos de valor, ele essencialmente quebraria essa classe, tendo uma restrição implícita anexada ao seu parâmetro de tipo (ou seja, ele só funcionaria com tipos não baseados em valor).

Lee.J.Baxter
fonte
0

O compilador permitirá que você compare qualquer struct que implemente o ==com nulo. Ele ainda permite que você compare um int com um nulo (no entanto, você receberia um aviso).

Mas se você desmontar o código, verá que a comparação está sendo resolvida quando o código é compilado. Então, por exemplo, este código (onde Fooestá uma implementação de estrutura ==):

public static void Main()
{
    Console.WriteLine(new Foo() == new Foo());
    Console.WriteLine(new Foo() == null);
    Console.WriteLine(5 == null);
    Console.WriteLine(new Foo() != null);
}

Gera este IL:

.method public hidebysig static void  Main() cil managed
{
  .entrypoint
  // Code size       45 (0x2d)
  .maxstack  2
  .locals init ([0] valuetype test3.Program/Foo V_0)
  IL_0000:  nop
  IL_0001:  ldloca.s   V_0
  IL_0003:  initobj    test3.Program/Foo
  IL_0009:  ldloc.0
  IL_000a:  ldloca.s   V_0
  IL_000c:  initobj    test3.Program/Foo
  IL_0012:  ldloc.0
  IL_0013:  call       bool test3.Program/Foo::op_Equality(valuetype test3.Program/Foo,
                                                           valuetype test3.Program/Foo)
  IL_0018:  call       void [mscorlib]System.Console::WriteLine(bool)
  IL_001d:  nop
  IL_001e:  ldc.i4.0
  IL_001f:  call       void [mscorlib]System.Console::WriteLine(bool)
  IL_0024:  nop
  IL_0025:  ldc.i4.1
  IL_0026:  call       void [mscorlib]System.Console::WriteLine(bool)
  IL_002b:  nop
  IL_002c:  ret
} // end of method Program::Main

Como você pode ver:

Console.WriteLine(new Foo() == new Foo());

Está traduzido para:

IL_0013:  call       bool test3.Program/Foo::op_Equality(valuetype test3.Program/Foo,
                                                               valuetype test3.Program/Foo)

Enquanto que:

Console.WriteLine(new Foo() == null);

É traduzido para falso:

IL_001e:  ldc.i4.0
hardkoded
fonte