값 유형을 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.