Любопытно нуль-коалесцирующий оператора пользовательские неявные преобразования поведения
Примечание: это, кажется, было исправлено в Рослин
этот вопрос возник при написании моего ответа на этот, в котором говорится об ассоциативности нуль-коалесцирующий оператор.
как напоминание, идея оператора null-coalescing заключается в том, что выражение вида
x ?? y
сначала оценивает x
, тогда:
- , если значение
x
имеет значение null,y
вычисляется и это конечный результат выражения - , если значение
x
не-null,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# компилятор, но я в тупике относительно того, что происходит...
EDIT: хорошо, вот более неприятный пример того, что происходит, благодаря ответу конфигуратора, который дает мне еще одну причину думать, что это ошибка. Изменить: образец даже не нуждается в двух операторах слияния нулей...
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 ответов:
спасибо всем, кто внес свой вклад в анализ данного вопроса. Это явно ошибка компилятора. По-видимому, это происходит только тогда, когда есть поднятое преобразование, включающее два обнуляемых типа на левой стороне коалесцирующего оператора.
я еще не определил, где именно все идет не так, но в какой-то момент во время фазы "nullable lowering" компиляции - после первоначального анализа, но до генерации кода - мы уменьшаем выражение
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 здесь сходит с рельсов. У нас есть оптимизатор nullable, который ищет ситуации, когда мы знаем, что конкретное выражение типа nullable не может быть null. Рассмотрим следующий наивный анализ: мы могли бы сначала сказать, что
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. Ошибка воспроизводится в C# 3.0 и 4.0, но не в C# 2.0, что означает, что ошибка была наверное моя ошибка. Прости!
я получу ошибку, введенную в базу данных, и мы посмотрим, сможем ли мы исправить это для будущей версии языка. Еще раз спасибо всем за анализ, было очень полезно!
UPDATE: я переписал nullable оптимизатор с нуля для 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)
это заставило меня думать, что первая часть каждого
??
выражение coalesce вычисляется дважды. Этот код доказал это:B? test= (X() ?? Y());
выходы:
X() X() A to B (0)
это, кажется, происходит только тогда, когда выражение требует преобразования между двумя типами nullable; я пробовал различные перестановки с одной из сторон, являющихся строкой, и ни один из них не вызвал этого поведение.
если вы посмотрите на сгенерированный код для левого сгруппированного случая, он на самом деле делает что-то вроде этого (
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
не-null, он переоцениваетa
как часть неявного преобразования вB
перед возвращением какой изa
илиb
не-null.от Спецификации C# 4.0, §6.1.4:
- если преобразование nullable от
S?
доT?
:
- если исходное значение
null
(HasValue
недвижимостьfalse
), результатnull
значение типаT?
.- в противном случае преобразование оценивается как разворачивание из
S?
доS
, а затем базовое преобразование изS
доT
, а затем обертывание (§4.1.10) отT
toT?
.это, по-видимому, объясняет вторую комбинацию разворачивания-обертывания.
компилятор 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
будет брошен дважды.
я бы записал его просто как артефакт howвынос: не создавайте неявные операторы литья с побочными эффектами.??
реализуется, а не ошибка компилятора.похоже, это компилятор ошибка вращается вокруг того, как есть. Вынос: не гнездите коалесцирующие выражения с побочными эффектами.
Я вообще не эксперт по C#, как вы можете видеть из моей истории вопросов, но я попробовал это, и я думаю, что это ошибка.... но как новичок, я должен сказать, что я не понимаю все, что здесь происходит, поэтому я удалю свой ответ, если я уйду.
Я пришел к этому
bug
заключение, сделав другую версию вашей программы, которая имеет дело с тем же сценарием, но гораздо менее сложным.Я использую три свойства null integer с резервными хранилищами. Я установил каждый в 4, а затем запустить
int? something2 = (A ?? B) ?? C;
Это просто читает A и ничего больше.
это заявление для меня выглядит так, как мне следует:
- процесс пуска в скобках, посмотреть, вернуться и закончить, если это не null.
- если был нуль, оценить Б закончить, если б не null
- если A и B были null, оцените C.
Итак, поскольку A не является нулем, он только смотрит на А и заканчивает.
в вашем примере установка точки останова в первом случае показывает, что x, y и z не являются нулевыми, и поэтому я ожидаю, что они будут рассматриваться так же, как и мой менее сложный пример.... но я боюсь, что я слишком новичок в C# и полностью пропустил суть этого вопроса!