궁금한 null 병합 연산자 사용자 지정 암시 적 변환 동작
참고 : 이것은 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
그리고 C
C와 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()
두 번 불린다 는 사실 은 놀랍습니다. 나는 그 표현이 두 번 평가 되어야 할 이유를 알 수 없습니다 .
이 문제를 분석하는 데 기여한 모든 사람에게 감사합니다. 분명히 컴파일러 버그입니다. 통합 연산자의 왼쪽에 두 개의 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/
이것은 가장 확실한 버그입니다.
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)
이것은 표현식이 두 개의 널 입력 가능 유형 간의 변환이 필요한 경우에만 발생합니다. 측면 중 하나가 문자열 인 다양한 순열을 시도했지만 아무도이 동작을 유발하지 않았습니다.
왼쪽 그룹의 경우 생성 된 코드를 보면 실제로 다음과 같은 작업이 수행됩니다 ( 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
하고 b
null이 반환됩니다 c
. 그러나 경우 a
또는 b
그것은 비 - 널 측정합니다 재 a
에 암시 적 변환의 일부를 B
어떤 리턴하기 전에 a
또는 b
비 - 널이다.
C # 4.0 사양 §6.1.4에서 :
- nullable 변환이에서
S?
로 변환되는 경우T?
:
- 소스 값이
null
(HasValue
property isfalse
)이면 결과는null
type 값입니다T?
.- 그렇지 않으면 변환은에서
S?
로 래핑 해제S
후 기본 변환에서S
로 변환T
, 래핑 (§4.1.10)에서T
로 변환으로 평가됩니다T?
.
이것은 두 번째 언 랩핑 랩핑 조합을 설명하는 것으로 보입니다.
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;
이것이 타입 추론 시스템에 주어진 추가 마법 때문이 아닌지 궁금합니다 .
사실, 나는 이것을 더 명확한 예제와 함께 지금 버그라고 부를 것입니다. 이것은 여전히 유효하지만 이중 평가는 확실히 좋지 않습니다.
마치 A ?? B
으로 구현 된 것처럼 보입니다 A.HasValue ? A : B
. 이 경우에도 많은 캐스팅이 있습니다 (삼항 ?:
연산자 의 일반 캐스팅에 따름 ). 그러나 모든 것을 무시하면 구현 방법에 따라 의미가 있습니다.
A ?? B
~로 확장A.HasValue ? A : B
A
우리x ?? y
입니다. 로 확장x.HasValue : x ? y
- 모든 발생을 대체하십시오->
(x.HasValue : x ? y).HasValue ? (x.HasValue : x ? y) : B
여기에서 x.HasValue
두 번 확인되었으며 x ?? y
캐스팅 이 필요한 경우 x
두 번 캐스팅됩니다.
테이크 아웃 : 부작용이있는 암시 적 캐스팅 연산자를 만들지 마십시오.??
컴파일러 버그가 아닌 구현
방식의 인공물로 간단히 내려 놓았습니다
.
??
구현 방법을 중심으로하는 컴파일러 버그 인 것 같습니다 . 테이크 아웃 : 부작용을 포함하는 통합 표현식을 중첩하지 마십시오.
내 질문 기록에서 볼 수 있듯이 나는 C # 전문가가 아니지만 이것을 시도해보고 버그라고 생각합니다 ...하지만 초보자로서 나는 모든 것을 이해하지 못한다고 말해야합니다 내가 여기에 있으면 답변을 삭제하겠습니다.
나는이에 온 bug
같은 시나리오를하는 거래 프로그램의 다른 버전을 만들어 결론,하지만 훨씬 덜 복잡합니다.
백업 저장소와 함께 세 개의 null 정수 속성을 사용하고 있습니다. 나는 각각 4로 설정 한 다음 실행int? something2 = (A ?? B) ?? C;
( 여기에서 전체 코드 )
이것은 단지 A와 다른 것을 읽지 않습니다.
나에게이 진술은 다음과 같아야한다.
- 대괄호로 시작하여 A를보고 A를 리턴 한 후 A가 널이 아닌 경우 완료하십시오.
- A가 널인 경우 B를 평가하고 B가 널이 아닌 경우 완료
- A와 B가 널이면 C를 평가하십시오.
따라서 A가 널이 아니므로 A 만보고 완료됩니다.
귀하의 예에서 첫 번째 사례에 중단 점을 넣으면 x, y 및 z가 모두 null이 아니므로 덜 복잡한 예와 동일하게 취급 될 것으로 기대합니다 ....하지만 너무 많이 두려워합니다. C # 초보자 의이 질문의 요점을 완전히 놓쳤다!
'Programming' 카테고리의 다른 글
참조로 전달하는 것과 값으로 전달하는 것의 차이점은 무엇입니까? (0) | 2020.02.10 |
---|---|
Xamarin C # 및 Java로 작성된 Android 앱의 성능을 비교하는 벤치 마크 (코드 및 결과)가 있습니까? (0) | 2020.02.10 |
문자열을 목록으로 나누는 방법? (0) | 2020.02.10 |
Visual Studio 2012 Release Preview를 설치 한 후 오류 'LINK : 치명적인 오류 LNK1123 : COFF로 변환하는 동안 실패 : 파일이 잘못되었거나 손상되었습니다'오류 (0) | 2020.02.10 |
Android 애플리케이션의 아이콘 설정 (0) | 2020.02.10 |