Любопытный нуль-коалесцирующий оператор нестандартного поведения неявного преобразования
Примечание: кажется, это было исправлено в Roslyn
Этот вопрос возник при написании моего ответа на этот, который говорит об ассоциативности нуль-сливающегося оператора.
Как напоминание, идея оператора слияния нулей состоит в том, что выражение вида
x ?? y
сначала оценивает x
, затем:
- Если значение
x
нулевой,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#, но я в тупике о том, что происходит...
РЕДАКТИРОВАТЬ: Хорошо, вот более противный пример того, что происходит, благодаря ответу конфигуратора, который дает мне еще одну причину думать, что это ошибка. РЕДАКТИРОВАТЬ: образец даже не нуждается в двух нуль-объединяющих операторов сейчас...
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()
Меня дважды вызывают здесь, это удивляет меня - я не вижу причин для того, чтобы выражение было оценено дважды.
5 ответов
Спасибо всем, кто внес вклад в анализ этой проблемы. Это явно ошибка компилятора. Похоже, это происходит только тогда, когда есть преобразование отмены, включающее два обнуляемых типа в левой части оператора объединения.
Я еще не определил, где именно что-то пошло не так, но в какой-то момент во время фазы компиляции "обнуляемое понижение" - после первоначального анализа, но до генерации кода - мы уменьшаем выражение
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;
Насколько я могу судить, исходя из моего анализа на данный момент, то, что обнуляемый оптимизатор сходит с рельсов. У нас есть обнуляемый оптимизатор, который ищет ситуации, когда мы знаем, что определенное выражение типа обнуляемого не может быть нулевым. Рассмотрим следующий наивный анализ: мы могли бы сначала сказать, что
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 не нуль; нет необходимости проверять его на ноль во второй раз только потому, что мы вызываем поднятый оператор преобразования". Мы бы их оптимизировали, чтобы просто
new int?(op_Implicit(temp2.Value))
Я предполагаю, что мы где-то кешируем тот факт, что оптимизированная форма (int?)Foo()
является new int?(op_implicit(Foo().Value))
но на самом деле это не оптимизированная форма, которую мы хотим; мы хотим, чтобы оптимизированная форма Foo() была заменена на временную и затем преобразованную.
Многие ошибки в компиляторе C# являются результатом неправильных решений кэширования. Слово мудрому: каждый раз, когда вы кешируете факт для последующего использования, вы потенциально создаете несоответствие, если что-то уместно изменится. В этом случае важная вещь, которая изменилась после первоначального анализа, заключается в том, что вызов Foo() всегда должен быть реализован как выборка временного.
Мы провели большую реорганизацию проходного переписывающего кода в C# 3.0. Ошибка воспроизводится в C# 3.0 и 4.0, но не в C# 2.0, что означает, что ошибка, вероятно, была моей. Сожалею!
Я внесу ошибку в базу данных, и мы посмотрим, сможем ли мы исправить это для будущей версии языка. Еще раз спасибо всем за ваш анализ; это было очень полезно!
ОБНОВЛЕНИЕ: я переписал обнуляемый оптимизатор с нуля для Roslyn; теперь он делает лучше и избегает таких странных ошибок. Некоторые мысли о том, как работает оптимизатор в 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
равны нулю и возвращаются c
, Еще если a
или же b
ненулевой, он переоценивает a
как часть неявного преобразования в B
перед возвратом какой из a
или же b
не является нулевым.
Из спецификации C# 4.0, §6.1.4:
- Если обнуляемое преобразование из
S?
вT?
:
- Если исходное значение
null
(HasValue
свойствоfalse
), результатом являетсяnull
значение типа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
- заменить все вхождения A ->
(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 был нулевым, оцените B, закончите, если B не нулевой
- Если A и B были нулевыми, оцените C.
Таким образом, поскольку A не нуль, он только смотрит на A и завершает.
В вашем примере размещение точки останова в первом случае показывает, что x, y и z не равны нулю, и поэтому я ожидаю, что с ними будут обращаться так же, как в моем менее сложном примере... но я боюсь, что я слишком много новичка в C# и полностью упустили суть этого вопроса!