Примечание: похоже, это было исправлено в 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
, с преобразованиями из A в B, A в C и B в C.
Я могу понять и второй случай, и третий случай ... но почему в первом случае происходит дополнительное преобразование A в B? В частности, я действительно ожидал, что первый и второй регистры будут одинаковыми - в конце концов, это всего лишь извлечение выражения в локальную переменную.
Есть ли кто-нибудь о том, что происходит? Я крайне неохотно плачу «ошибку», когда дело доходит до компилятора 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()
здесь дважды вызывается, удивляет меня - я не вижу причин для того, чтобы выражение было оценено дважды.
источник
C? first = ((B?)(((B?)x) ?? ((B?)y))) ?? ((C?)z);
. Вы получите:Internal Compiler Error: likely culprit is 'CODEGEN'
(("working value" ?? "user default") ?? "system default")
Ответы:
Спасибо всем, кто внес вклад в анализ этой проблемы. Это явно ошибка компилятора. Похоже, что это происходит только тогда, когда есть преобразование отмены, включающее два обнуляемых типа в левой части оператора объединения.
Я еще не определил, где именно что-то пошло не так, но в какой-то момент на этапе компиляции «обнуляемое понижение» - после первоначального анализа, но до генерации кода - мы уменьшаем выражение
из приведенного выше примера в моральном эквиваленте:
Очевидно, что это неправильно; правильное опускание
Насколько я могу судить, исходя из моего анализа на данный момент, то, что обнуляемый оптимизатор сходит с рельсов. У нас есть обнуляемый оптимизатор, который ищет ситуации, когда мы знаем, что определенное выражение типа обнуляемого не может быть нулевым. Рассмотрим следующий наивный анализ: мы могли бы сначала сказать, что
такой же как
и тогда мы могли бы сказать, что
такой же как
Но оптимизатор может вмешаться и сказать: «Вау, подожди минутку, мы уже проверили, что temp не нуль; нет необходимости проверять его на ноль во второй раз только потому, что мы вызываем поднятый оператор преобразования». Мы бы их оптимизировали, чтобы просто
Я предполагаю, что мы где-то кешируем тот факт, что оптимизированная форма
(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/
источник
Это определенно ошибка.
Этот код выведет:
Это заставило меня подумать, что первая часть каждого
??
выражения coalesce оценивается дважды. Этот код доказал это:выходы:
Кажется, это происходит только тогда, когда выражение требует преобразования между двумя обнуляемыми типами; Я пробовал различные перестановки с одной из сторон, являющейся строкой, и ни одна из них не вызывала такого поведения.
источник
X() ?? Y()
расширяется изнутриX() != null ? X() : Y()
, поэтому и будет оцениваться дважды.Если вы посмотрите на сгенерированный код для левого сгруппированного случая, он на самом деле делает что-то вроде этого (
csc /optimize-
):Еще одна находка, если вы используете
first
ее, сгенерирует ярлык, если обаa
иb
равны null и возвращаютсяc
. Тем не менее, еслиa
или неb
равно NULL, оно переоцениваетсяa
как часть неявного преобразования в,B
прежде чем возвращать, какой изa
или неb
равен NULL.Из спецификации C # 4.0, §6.1.4:
Похоже, это объясняет вторую комбинацию распаковки и обертывания.
Компилятор C # 2008 и 2010 производит очень похожий код, однако это выглядит как регрессия из компилятора C # 2005 (8.00.50727.4927), который генерирует следующий код для вышеупомянутого:
Интересно, это не из-за дополнительной магии, данной системе вывода типов?
источник
(x ?? y) ?? z
во вложенные лямбды, что обеспечивает оценку по порядку без двойной оценки. Это явно не тот подход, который используется компилятором C # 4.0. Из того, что я могу сказать, к разделу 6.1.4 очень строго подходят в этом конкретном пути кода, и временные значения не исключаются, что приводит к двойной оценке.На самом деле, я сейчас назову это ошибкой, с более ясным примером. Это все еще имеет место, но двойная оценка, конечно, не хорошо.
Вроде как
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
будет разыгрываться дважды.Я бы описал это просто как артефактВывод: не создавайте неявных операторов приведения с побочными эффектами.??
реализации, а не как ошибку компилятора.Кажется, это ошибка компилятора, связанная с
??
его реализацией. Вывод: не вкладывайте коалесцирующие выражения с побочными эффектами.источник
A() ? A() : B()
, возможно , оценятA()
дважды, ноA() ?? B()
не так сильно. И так как это происходит только на кастинге ... Хм ... Я только что заставил себя подумать, что это, конечно, ведет себя неправильно.Я совсем не эксперт по C #, как вы можете видеть из моей истории вопросов, но я попробовал это, и я думаю, что это ошибка .... но как новичок, я должен сказать, что я не понимаю все происходящее здесь, поэтому я удалю свой ответ, если я далеко.
Я пришел к такому
bug
выводу, сделав другую версию вашей программы, которая работает по тому же сценарию, но гораздо менее сложна.Я использую три целочисленных свойства с нулевым целым с резервными хранилищами. Я установил каждый на 4, а затем запустить
int? something2 = (A ?? B) ?? C;
( Полный код здесь )
Это просто читает А и ничего больше.
Это утверждение для меня выглядит так:
Таким образом, поскольку A не нуль, он только смотрит на A и завершает.
В вашем примере размещение точки останова в первом случае показывает, что x, y и z не равны нулю, и поэтому я ожидаю, что с ними будут обращаться так же, как в моем менее сложном примере ... но я боюсь, что я слишком много новичка C # и упустили суть этого вопроса полностью!
источник
int
). Он толкает дело дальше в неясный угол, предоставляя несколько неявных преобразований типов. Это требует, чтобы компилятор изменил тип данных при проверкеnull
. Именно из-за этих неявных преобразований типов его пример отличается от вашего.