궁금한 null 병합 연산자 사용자 지정 암시 적 변환 동작


542

참고 : 이것은 수정 된 것으로 보입니다 Roslyn

내 대답 작성할 때 문제는 발생 이 하나 의 연관성에 대해 이야기, 널 병합 연산자 .

알림과 마찬가지로, null-coalescing 연산자의 아이디어는 폼의 표현입니다.

x ?? y

먼저 평가 x 한 후 다음을 수행하십시오.

  • 값이 x 이 null 인 y경우 평가되고 표현식의 최종 결과입니다
  • 값이 경우 x비 - 널이, y되어 있지 평가와의 값 x의 컴파일 시간 형으로 전환 된 후, 다음의 식의 최종 결과 y필요하다면

이제 일반적으로 변환이 필요하지 않거나 널 입력 가능 유형에서 널 입력 불가능 유형으로 변환됩니다. 일반적으로 유형은 동일하거나 (예 :에서)로 변경 int?됩니다 int. 그러나, 당신은 할 수 있습니다 자신의 암시 적 변환 연산자를 만들고, 사람들은 필요한 경우 사용된다.

의 간단한 경우에는 x ?? y이상한 행동을 보지 못했습니다. 그러나 (x ?? y) ?? z나는 혼란스러운 행동을 보았습니다.

짧지 만 완전한 테스트 프로그램은 다음과 같습니다. 결과는 주석으로 표시됩니다.

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

우리는 세 개의 사용자 정의 값 유형, 그래서 A, B그리고 CC와 B, C에 A, 및 A로부터 B 로의 전환이를,

두 번째 경우와 세 번째 경우를 모두 이해할 수 있지만 첫 번째 경우에 추가 A에서 B 로의 전환이 필요한 이유 는 무엇입니까? 특히, 나는 첫 번째 사례와 두 번째 사례는 실제로 같은 것으로 예상 입니다. 결국 표현식을 로컬 변수로 추출하는 것입니다.

무슨 일이 일어나고 있습니까? C # 컴파일러와 관련하여 "버그"를 울리는 것은 매우 주저하지만, 무슨 일이 일어나고 있는지에 대해 충격을 받았습니다 ...

편집 : 좋아, 여기에 구성 자의 답변 덕분에 무슨 일이 일어나고 있는지에 대한 초기 예가 있습니다. 편집 : 이제 샘플에는 두 개의 null 병합 연산자가 필요하지 않습니다 ...

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

이것의 결과는 다음과 같습니다.

Foo() called
Foo() called
A to int

Foo()여기에서 두 번 호출 되는 사실은 나에게 놀랍습니다. 표현이 두 번 평가 되는 이유를 알 수 없습니다 .


32
나는 그들이 :) "아무도 그런 식으로 그것을 사용하지 않습니다"생각 내기
cyberzed

57
더 나쁜 것을보고 싶습니까? 모든 암시 적 변환과 함께이 행을 사용해보십시오 C? first = ((B?)(((B?)x) ?? ((B?)y))) ?? ((C?)z);. 당신은 얻을 것이다 :Internal Compiler Error: likely culprit is 'CODEGEN'
피규

5
또한 Linq 표현식을 사용하여 동일한 코드를 컴파일 할 때는 이런 일이 발생하지 않습니다.
구성자

8
@Peter는 거의 패턴이 없지만 그럴듯하다(("working value" ?? "user default") ?? "system default")
Factor Mystic

23
@ yes123 : 변환 만 처리 할 때 전적으로 확신하지 못했습니다. 메소드를 두 번 실행하면 이것이 버그라는 것이 분명해졌습니다. 당신 부정확 해 보이지만 실제로는 완전히 올바른 행동에 놀랄 것 입니다. C # 팀은 나보다 똑똑합니다. 무언가 잘못되었다는 것을 증명할 때까지 내가 바보라고 생각하는 경향이 있습니다.
Jon Skeet 2016 년

답변:


418

이 문제를 분석하는 데 기여한 모든 사람에게 감사합니다. 분명히 컴파일러 버그입니다. 통합 연산자의 왼쪽에 두 개의 nullable 유형이 포함 된 해제 변환이있는 경우에만 발생합니다.

나는 정확하게 일이 잘못되는 곳을 아직 식별하지 못했지만, 어떤 시점에서 컴파일의 "널링 가능 하강"단계 (초기 분석 후 코드 생성 이전) 동안 우리는 표현을 줄입니다.

result = Foo() ?? y;

위의 예에서 도덕적 등가에 이르기까지 :

A? temp = Foo();
result = temp.HasValue ? 
    new int?(A.op_implicit(Foo().Value)) : 
    y;

분명히 그것은 틀렸다; 올바른 낮추는 것입니다

result = temp.HasValue ? 
    new int?(A.op_implicit(temp.Value)) : 
    y;

지금까지의 분석에 근거한 가장 좋은 추측은 nullable 최적화 프로그램이 여기에서 벗어나는 것입니다. 널 입력 가능 유형의 특정 표현식이 널이 될 수없는 상황을 찾는 널 입력 가능 옵티마이 저가 있습니다. 다음과 같은 순진한 분석을 고려하십시오.

result = Foo() ?? y;

와 같다

A? temp = Foo();
result = temp.HasValue ? 
    (int?) temp : 
    y;

그리고 우리는 말할 수 있습니다

conversionResult = (int?) temp 

와 같다

A? temp2 = temp;
conversionResult = temp2.HasValue ? 
    new int?(op_Implicit(temp2.Value)) : 
    (int?) null

그러나 옵티마이 저가 들어 와서 "아, 잠깐만 요, 우리는 이미 temp가 null이 아닌지 확인했습니다. 즉, 해제 된 변환 연산자를 호출하기 때문에 null을 두 번째로 확인할 필요가 없습니다"라고 말합니다. 우리는 그것들을

new int?(op_Implicit(temp2.Value)) 

내 생각에 우리는 어딘가에 최적화 된 형태 (int?)Foo()new int?(op_implicit(Foo().Value))있지만 실제로는 우리가 원하는 최적화 된 형태가 아니라는 사실을 캐싱 하고있다. 우리는 Foo ()의 최적화 된 형식을 임시로 변환 한 후 변환하기를 원합니다.

C # 컴파일러의 많은 버그는 잘못된 캐싱 결정의 결과입니다. 현명한 말 : 나중에 사용하기 위해 사실을 캐시 할 때마다 관련 변경 사항이있을 경우 불일치가 발생할 수 있습니다. . 이 경우 초기 분석 후 변경된 관련 사항은 Foo () 호출은 항상 임시 페치로 실현되어야한다는 것입니다.

C # 3.0에서 nullable rewriting pass를 많이 재구성했습니다. 이 버그는 C # 3.0 및 4.0에서는 재현되지만 C # 2.0에서는 재현되지 않습니다. 즉, 버그가 아마도 제 잘못 일 수 있습니다. 죄송합니다!

데이터베이스에 버그를 입력하고 향후 버전의 언어로이 문제를 해결할 수 있는지 확인할 것입니다. 분석해 주셔서 감사합니다. 매우 도움이되었습니다!

업데이트 : Roslyn에 대해 nullable 최적화 프로그램을 처음부터 다시 작성했습니다. 이제 더 나은 작업을 수행하고 이러한 종류의 이상한 오류를 피합니다. Roslyn의 옵티 마이저 작동 방식에 대한 일부 아이디어는 여기에서 시작하는 일련의 기사를 참조 하십시오. https://ericlippert.com/2012/12/20/nullable-micro-optimizations-part-one/


1
@Eric I 경이이 또한 설명 할 경우 : connect.microsoft.com/VisualStudio/feedback/details/642227
MarkPflug

12
Roslyn의 최종 사용자 미리보기가 있으므로 수정되었음을 확인할 수 있습니다. (이것은 여전히 ​​네이티브 C # 5 컴파일러에 있습니다.)
Jon Skeet

84

이것은 가장 확실한 버그입니다.

public class Program {
    static A? X() {
        Console.WriteLine("X()");
        return new A();
    }
    static B? Y() {
        Console.WriteLine("Y()");
        return new B();
    }
    static C? Z() {
        Console.WriteLine("Z()");
        return new C();
    }

    public static void Main() {
        C? test = (X() ?? Y()) ?? Z();
    }
}

이 코드는 다음을 출력합니다 :

X()
X()
A to B (0)
X()
X()
A to B (0)
B to C (0)

이를 통해 각 ??통합 표현 의 첫 부분이 두 번 평가 된다고 생각했습니다 . 이 코드는 그것을 증명했다 :

B? test= (X() ?? Y());

출력 :

X()
X()
A to B (0)

이것은 표현식이 두 개의 널 입력 가능 유형 간의 변환이 필요한 경우에만 발생합니다. 측면 중 하나를 문자열로 사용하여 다양한 순열을 시도했지만 아무도이 동작을 유발하지 않았습니다.


11
와우-표현을 두 번 평가하는 것은 실제로 잘못된 것 같습니다. 잘 발견되었습니다.
Jon Skeet 2016

소스에 하나의 메소드 호출 만 있는지 확인하는 것이 약간 더 간단 하지만 여전히 매우 명확하게 보여줍니다.
Jon Skeet 2016

2
내 질문에이 "이중 평가"의 약간 더 간단한 예를 추가했습니다.
Jon Skeet

8
모든 메소드가 "X ()"를 출력해야합니까? 어떤 방법이 실제로 콘솔에 출력되는지 알기가 다소 어려워집니다.
jeffora

2
X() ?? Y()내부적으로 확장 된 것처럼 보이 X() != null ? X() : Y()므로 두 번 평가되는 이유는 무엇입니까?
Cole Johnson

54

왼쪽 그룹의 경우 생성 된 코드를 보면 실제로 다음과 같은 작업이 수행됩니다 ( csc /optimize-).

C? first;
A? atemp = a;
B? btemp = (atemp.HasValue ? new B?(a.Value) : b);
if (btemp.HasValue)
{
    first = new C?((atemp.HasValue ? new B?(a.Value) : b).Value);
}

또 다른 발견은, 당신이 경우에 사용 first 이 모두있는 경우 바로 가기를 생성 a하고 bnull이 반환됩니다 c. 그러나 경우 a또는 b그것은 비 - 널 측정합니다 재 a에 암시 적 변환의 일부를 B어떤 리턴하기 전에 a또는 b비 - 널이다.

C # 4.0 사양 §6.1.4에서 :

  • nullable 변환이에서 S?로 변환되는 경우 T?:
    • 소스 값이 null( HasValueproperty is false)이면 결과는 nulltype 값입니다 T?.
    • 그렇지 않으면 변환은에서 S?로 래핑 해제 S후 기본 변환에서 S로 변환 T, 래핑 (§4.1.10)에서 T로 변환으로 평가됩니다 T?.

이것은 두 번째 unwrapping-wrapping 조합을 설명하는 것으로 보입니다.


C # 2008 및 2010 컴파일러는 매우 유사한 코드를 생성하지만 위 코드는 다음 코드를 생성하는 C # 2005 컴파일러 (8.00.50727.4927)의 회귀처럼 보입니다.

A? a = x;
B? b = a.HasValue ? new B?(a.GetValueOrDefault()) : y;
C? first = b.HasValue ? new C?(b.GetValueOrDefault()) : z;

이것이 형식 유추 시스템에 주어진 추가 마법 때문이 아닌지 궁금합니다 .


+1이지만 전환이 두 번 수행되는 이유를 실제로 설명하지는 않습니다. 식을 한 번만 평가해야합니다 (IMO).
Jon Skeet 2016

@ Jon : 나는 놀고 있었고 (@configurator가했던 것처럼) Expression Tree에서 끝나면 예상대로 작동한다는 것을 알았습니다. 표현식을 정리하여 내 게시물에 추가합니다. 나는 이것이 "버그"라는 것을 포지셔닝해야 할 것이다.
user7116

@ Jon : OK Expression Tree를 사용할 때 (x ?? y) ?? z중첩 된 람다로 바뀌어 이중 평가없이 순서대로 평가할 수 있습니다. 이것은 분명히 C # 4.0 컴파일러가 취하는 접근법이 아닙니다. 내가 알 수있는 것에서, 6.1.4 섹션은이 특정 코드 경로에서 매우 엄격한 방식으로 접근되며 임시는 제거되지 않으므로 이중 평가가 발생합니다.
user7116 2016 년

16

사실, 나는 이것을 더 명확한 예제와 함께 지금 버그라고 부를 것입니다. 이것은 여전히 ​​유효하지만 이중 평가는 확실히 좋지 않습니다.

마치 A ?? B로 구현 된 것처럼 보입니다 A.HasValue ? A : B. 이 경우에도 많은 캐스팅이 있습니다 (삼항 ?:연산자 의 일반 캐스팅에 따름 ). 그러나 모든 것을 무시하면 구현 방법에 따라 의미가 있습니다.

  1. A ?? B ~로 확장 A.HasValue ? A : B
  2. A우리 x ?? y입니다. 로 확장x.HasValue : x ? y
  3. 모든 발생을 대체하십시오-> (x.HasValue : x ? y).HasValue ? (x.HasValue : x ? y) : B

여기에서 x.HasValue두 번 확인되었으며 x ?? y캐스팅 이 필요한 경우 x두 번 캐스팅됩니다.

??컴파일러 버그가 아닌 구현 방식의 인공물로 간단히 내려 놓았습니다 . 테이크 아웃 : 부작용이있는 암시 적 캐스팅 연산자를 만들지 마십시오.

??구현 방법을 중심으로하는 컴파일러 버그 인 것 같습니다 . 테이크 아웃 : 부작용을 포함하는 통합 표현식을 중첩하지 마십시오.


오, 나는 확실히 이와 같은 코드를 사용하고 싶지는 않지만, 첫 번째 확장에는 "하지만 A와 B를 한 번만 평가"해야한다는 점에서 여전히 컴파일러 버그로 분류 될 있다고 생각합니다 . (메소드 호출이라고 상상해보십시오.)
Jon Skeet

@Jon 나는 그것이 또한 가능하다는 것에 동의하지만 분명하게 부르지 않을 것입니다. 글쎄, 실제로, 나는 그것이 두 번 A() ? A() : B()평가 A()되지만 A() ?? B()그렇게 많이 평가 되지는 않을 것임을 알 수 있습니다 . 그리고 그것은 캐스팅에서만 발생하기 때문에 ... 흠 .. 나는 그것이 정확하게 올바르게 작동하지 않는다고 생각하면서 나 자신에게 이야기했습니다.
Philip Rieck

10

내 질문 기록에서 볼 수 있듯이 나는 C # 전문가가 아니지만 이것을 시도해보고 버그라고 생각합니다 ....하지만 초보자로서, 나는 모든 것을 이해하지 못한다고 말해야합니다 내가 여기에 있으면 답변을 삭제하겠습니다.

나는이에 온 bug같은 시나리오를하는 거래 프로그램의 다른 버전을 만들어 결론,하지만 훨씬 덜 복잡합니다.

백업 저장소와 함께 세 개의 null 정수 속성을 사용하고 있습니다. 나는 각각 4로 설정 한 다음 실행int? something2 = (A ?? B) ?? C;

( 여기에서 전체 코드 )

이것은 단지 A와 다른 것을 읽지 않습니다.

나에게이 진술은 다음과 같아야한다.

  1. 대괄호로 시작하여 A를보고 A를 리턴 한 후 A가 널이 아닌 경우 완료하십시오.
  2. A가 널인 경우 B를 평가하고 B가 널이 아닌 경우 완료
  3. A와 B가 널이면 C를 평가하십시오.

따라서 A가 널이 아니므로 A 만보고 완료됩니다.

귀하의 예에서 첫 번째 사례에 중단 점을 넣으면 x, y 및 z가 모두 null이 아니므로 덜 복잡한 예와 동일하게 취급 될 것으로 기대합니다 ....하지만 너무 많이 두려워합니다. C # 초보자 의이 질문의 요점을 완전히 놓쳤다!


5
Jon의 예는 nullable 구조체 ( int) 와 같은 내장 유형과 "유사한 값 유형"을 사용한다는 점에서 모호한 경우입니다 . 그는 여러 암시 적 유형 변환을 제공하여 사건을 더 모호한 구석으로 밀어 넣습니다. 이를 확인하려면 컴파일러가 데이터 유형 을 변경해야합니다 null. 이러한 암시 적 유형 변환으로 인해 그의 예제가 귀하의 것과 다릅니다.
user7116
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.