값 유형을 null과 비교해도 괜찮은 C #


85

나는 오늘 이것을 만났고 C # 컴파일러가 오류를 던지지 않는 이유를 모릅니다.

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

x가 어떻게 null이 될 수 있는지 혼란 스럽습니다. 특히이 할당은 확실히 컴파일러 오류를 던지기 때문에 :

Int32 x = null;

x가 null이 될 수 있습니까? Microsoft가이 검사를 컴파일러에 넣지 않기로 결정 했습니까, 아니면 완전히 놓쳤습니까?

업데이트 :이 기사를 작성하기 위해 코드를 엉망으로 만든 후 갑자기 컴파일러가 표현식이 사실이 될 수 없다는 경고를 내보냈습니다. 이제 나는 정말 길을 잃었습니다. 나는 객체를 클래스에 넣었고 이제 경고는 사라졌지 만 질문이 남았습니다. 값 유형이 null이 될 수 있습니까?

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

    public Test ()
    {
        Test test = new Test();
        if (test.ADate == null)
        {
            Console.WriteLine("What the?");
        }
    }
}

9
당신도 쓸 수 있습니다 if (1 == 2). 코드 경로 분석을 수행하는 것은 컴파일러의 일이 아닙니다. 그것이 바로 정적 분석 도구와 단위 테스트의 목적입니다.
Aaronaught

경고가 사라진 이유는 내 대답을 참조하십시오. 그리고 아니요-null이 될 수 없습니다.
Marc Gravell

1
(1 == 2)에 동의했고 상황에 대해 더 궁금해했습니다 (1 == null)
Joshua Belden

응답 해주신 모든 분들께 감사드립니다. 이제 모든 것이 이해됩니다.
Joshua Belden

경고 또는 경고 없음 문제 관련 : 문제의 구조체가와 같이 소위 "단순 유형" int인 경우 컴파일러는 멋진 경고를 생성합니다. 단순 유형의 경우 ==연산자는 C # 언어 사양에 의해 정의됩니다. 다른 (단순 유형이 아닌) 구조체의 경우 컴파일러 는 경고를 내보내는 것을 잊 습니다. 자세한 내용은 구조체를 null과 비교할 때 잘못된 컴파일러 경고 를 참조하세요. 단순 유형이 아닌 구조체의 경우 ==연산자는 opeartor ==구조체의 구성원 인 메서드에 의해 오버로드되어야합니다 (그렇지 않으면 ==허용 되지 않음 ).
Jeppe Stig Nielsen

답변:


119

연산자 과부하 해결에는 선택할 수있는 고유 한 최상의 연산자가 있기 때문에 이는 합법적입니다. 두 개의 nullable int를 취하는 == 연산자가 있습니다. int local은 nullable int로 변환 할 수 있습니다. null 리터럴은 nullable int로 변환 할 수 있습니다. 따라서 이것은 == 연산자의 합법적 인 사용이며 항상 거짓이됩니다.

마찬가지로, "if (x == 12.6)"이라고 말하면 항상 거짓이됩니다. int local은 double로 변환 가능하고 리터럴은 double로 변환 가능하며 분명히 같지 않을 것입니다.



5
@James : (이전에 삭제 한 잘못된 주석을 철회합니다.) 기본적으로 정의 된 사용자 정의 같음 연산자가있는 사용자 정의 값 유형 에는 해제 된 사용자 정의 같음 연산자가 생성됩니다. 해제 된 사용자 정의 같음 연산자는 다음과 같은 이유로 적용 할 수 있습니다. 모든 값 형식은 null 리터럴과 마찬가지로 해당하는 nullable 형식으로 암시 적으로 변환 될 수 있습니다. 사용자 정의 비교 연산자가 없는 사용자 정의 값 유형이 널 리터럴과 비교할 수있는 경우 는 아닙니다 .
Eric Lippert

3
@James : 물론, nullable 구조체를 사용하는 연산자 == 및 연산자! =를 구현할 수 있습니다. 이들이 존재하는 경우 컴파일러는 자동으로 생성하지 않고 사용합니다. (그리고 우연히 nullable이 아닌 피연산자에 대한 무의미한 리프트 연산자에 대한 경고가 경고를 생성하지 않는다는 사실을 후회합니다. 이는 컴파일러에서 수정하지 않은 오류입니다.)
Eric Lippert

2
우리는 경고를 원합니다! 우리는 그럴 자격이 있습니다.
Jeppe Stig Nielsen

3
@JamesDunne : a를 정의 static bool operator == (SomeID a, String b)하고 태그를 지정하는 것은 Obsolete어떻습니까? 두 번째 피연산자가 유형이 지정되지 않은 리터럴 null이면 해제 된 연산자를 사용해야하는 어떤 형식보다 더 나은 일치가 될 수 있지만 SomeID?같으면 null해제 된 연산자가 이깁니다.
supercat 2013-06-10

17

( int?) 변환 이 있기 때문에 오류가 아닙니다 . 주어진 예에서 경고를 생성합니다.

'int'유형의 값이 'int?'유형의 'null'과 같지 않기 때문에 표현식의 결과는 항상 'false'입니다.

IL을 확인하면 도달 할 수없는 분기가 완전히 제거 되었음을 알 수 있습니다. 릴리스 빌드에는 존재하지 않습니다.

그러나 같음 연산자가있는 사용자 지정 구조체에 대해서는이 경고를 생성 하지 않습니다 . 2.0에서는 사용되었지만 3.0 컴파일러에서는 사용되지 않았습니다. 코드는 여전히 제거되지만 (코드에 도달 할 수 없음을 알고 있음) 경고가 생성되지 않습니다.

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
    }
}

IL (for Main)- (부작용을 가질 수 있음)을 제외한 모든MyValue(1) 것이 제거되었습니다.

.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 
}

이것은 기본적으로 :

private static void Main()
{
    MyValue v = new MyValue(1);
}

1
최근에 누군가 내부적으로이 사실을보고했습니다. 우리가 그 경고를 왜 그만두 었는지 모르겠습니다. 버그로 입력했습니다.
에릭 리퍼 트


5

비교가 결코 사실 일 수 없다는 사실이 그것이 불법임을 의미하지는 않습니다. 그럼에도 불구하고 값 유형은 null.


1
그러나 값 형식이 될 수 동일null. 값 유형 int?인의 구문 설탕 인을 고려하십시오 Nullable<Int32>. 유형의 변수는 int?확실히 null.
Greg

1
@Greg : 예, "equal"이 ==연산자 의 결과라고 가정하면 null과 같을 수 있습니다 . 하지만 인스턴스가 실제로 null 이 아니라는 점에 유의하는 것이 중요합니다 .
Adam Robinson


1

값 유형은 null같을 수 있지만 null(고려 Nullable<>) 일 수 없습니다 . 귀하의 경우 int변수 및 null암시 적으로 캐스팅 Nullable<Int32>되고 비교됩니다.


0

테스트가 거짓이 아니기 때문에 IL을 생성 할 때 특정 테스트가 컴파일러에 의해 최적화되고 있다고 생각합니다.

참고 : nullable Int32가 Int32를 사용하도록 할 수 있습니까? x 대신.


0

나는 이것이 "=="가 실제로 매개 변수 System.Object.Equals를 받아들이는 메소드에 대한 호출을 나타내는 구문 설탕이기 때문이라고 생각한다 System.Object. ECMA 사양에 의한 Null은 물론에서 파생 된 특수 유형입니다 System.Object.

그것이 경고 만있는 이유입니다.


이것은 두 가지 이유로 올바르지 않습니다. 첫째, ==는 인수 중 하나가 참조 유형일 때 Object.Equals와 동일한 의미를 갖지 않습니다. 둘째, null은 유형이 아닙니다. 참조 같음 연산자의 작동 방식을 이해하려면 사양의 섹션 7.9.6을 참조하십시오.
Eric Lippert

"null 리터럴 (§9.4.4.6)은 개체 나 배열을 가리 키지 않는 참조 또는 값이 없음을 나타내는 데 사용되는 null 값으로 평가됩니다. null 형식에는 null 인 단일 값이 있습니다. 따라서 유형이 널 유형 인 표현식은 널 값으로 만 평가 될 수 있습니다. 널 유형을 명시 적으로 작성할 수있는 방법이 없으므로 선언 된 유형에서 사용할 방법이 없습니다. " -이것은 ECMA의 인용문입니다. 무슨 소리 야? 또한 어떤 버전의 ECMA를 사용하십니까? 내 7.9.6이 보이지 않습니다.
Vitaly

0

[편집 됨 : 경고를 오류로 만들고 연산자를 문자열 해킹이 아닌 nullable에 대해 명시 적으로 만들었습니다.]

위의 주석에서 @supercat의 영리한 제안에 따라 다음 연산자 오버로드를 사용하면 사용자 정의 값 유형을 null로 비교하는 데 대한 오류를 생성 할 수 있습니다.

유형의 nullable 버전과 비교하는 연산자를 구현하면 비교에서 null을 사용하면 연산자의 nullable 버전과 일치하므로 Obsolete 속성을 통해 오류를 생성 할 수 있습니다.

Microsoft가 컴파일러 경고를 반환 할 때까지이 해결 방법을 사용하겠습니다. @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;
    }
}

내가 뭔가를 놓치지 않는 한, 당신의 접근 방식은 Foo a; Foo? b; ... if (a == b)...그러한 비교가 완벽하게 합법적이어야하더라도 컴파일러가에서 삐걱 거리는 원인이 될 것 입니다. 내가 "string hack"을 제안한 이유는 위의 비교를 허용하지만 if (a == null). 를 사용하는 대신 또는 string이외의 참조 유형을 대체 할 수 있습니다 . 원하는 경우 호출 할 수없는 개인 생성자로 더미 클래스를 정의하고 권한을 부여 할 수 있습니다 . ObjectValueTypeReferenceThatCanOnlyBeNull
supercat 2015-07-24

당신은 절대적으로 정확합니다. 나는 내 제안이 nullables의 사용을 깨뜨린다는 것을 분명히해야했다. 내가 일하고있는 코드베이스에서 어쨌든 (원치 않는 권투 등) 죄악으로 간주되는 nullables의 사용. ;)
yoyo

0

컴파일러가 이것을 받아들이는 이유에 대한 가장 좋은 대답 은 제네릭 클래스에 대한 것입니다. 다음 클래스를 고려하십시오 ...

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

컴파일러가 null값 유형 에 대한 비교를 허용하지 않으면 본질적으로이 클래스를 깨뜨려 해당 유형 매개 변수에 암시 적 제약이 첨부됩니다 (즉, 값 기반이 아닌 유형에서만 작동 함).


0

컴파일러를 사용하면 다음을 구현하는 모든 구조체를 비교할 수 있습니다. == 를 null . int를 null과 비교할 수도 있습니다 (그래도 경고가 표시됩니다).

그러나 코드를 분해하면 코드가 컴파일 될 때 비교가 해결되는 것을 볼 수 있습니다. 예를 들어,이 코드 (여기서는 Foo구조체 구현 ==) :

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

이 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

보시다시피 :

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

번역 :

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

이므로:

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

거짓으로 번역됨 :

IL_001e:  ldc.i4.0
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.