Неявно преобразуване с оператор за нулево обединяване

Открих странно поведение на моята програма и след допълнителен анализ успях да открия, че вероятно нещо не е наред или в познанията ми за C#, или някъде другаде. Вярвам, че грешката е моя, но никъде не мога да намеря отговор...

public class B
{
    public static implicit operator B(A values) 
    {
        return null; 
    }
}
public class A { }

public class Program
{
    static void Main(string[] args)
    {
        A a = new A();
        B b = a ?? new B();
        //b = null ... is it wrong that I expect b to be B() ?
    }
}

The variable "b" in this code is evaluated to null. I don't get why is it null.

Търсих в Google и намерих отговор в този въпрос - Имплицитно кастинг на резултат от оператор Null-Coalescing - с официалната спецификация.

Но следвайки тази спецификация, не мога да намеря причината, поради която "b" е нула :( Може би го чета погрешно, в който случай се извинявам за спама.

Ако A съществува и не е nullable тип или референтен тип, възниква грешка по време на компилиране.

...това не е така.

Ако b е динамичен израз, типът резултат е динамичен. По време на изпълнение първо се оценява a. Ако a не е нула, a се преобразува в динамично и това става резултатът. В противен случай b се оценява и това става резултатът.

...това също не е така.

В противен случай, ако A съществува и е nullable тип и съществува неявно преобразуване от b към A0, типът резултат е A0. По време на изпълнение първо се оценява a. Ако a не е null, a се разопакова до тип A0 и това става резултатът. В противен случай b се оценява и преобразува в тип A0 и това става резултатът.

...A съществува, неявното преобразуване от b в A0 не съществува.

В противен случай, ако A съществува и съществува неявно преобразуване от b към A, типът резултат е A. По време на изпълнение първо се оценява a. Ако a не е нула, a става резултат. В противен случай b се оценява и преобразува в тип A и това става резултатът.

...A съществува, неявното преобразуване от b в A не съществува.

В противен случай, ако b има тип B и съществува неявно преобразуване от a към B, типът резултат е B. По време на изпълнение първо се оценява a. Ако a не е null, a се разопакова до тип A0 (ако A съществува и е nullable) и се преобразува в тип B, и това става резултатът. В противен случай b се оценява и става резултат.

...b има тип B и съществува имплицитно преобразуване от a към B. a се оценява като нула. Следователно b трябва да бъде оценено и b трябва да бъде резултатът.

В противен случай a и b са несъвместими и възниква грешка по време на компилиране. Не става

Пропускам ли нещо, моля?


person Mirek    schedule 27.01.2014    source източник
comment
Не трябва ли операторът да бъде пренастроен, за да работи?   -  person Rand Random    schedule 27.01.2014
comment
Много интересно. Имайте предвид, че ако промените последния ред на B b = (B)a ?? new B();, т.е. напишете имплицитното предаване изрично в кода, това има значение.   -  person Jeppe Stig Nielsen    schedule 27.01.2014
comment
@JeppeStigNielsen Вие принуждавате преобразуването да се извърши преди нулевата проверка, а не след това, като правите това.   -  person Servy    schedule 27.01.2014
comment
@Servy Точно така. Разбрах и написах в отговора си.   -  person Jeppe Stig Nielsen    schedule 27.01.2014


Отговори (4)


Е, спецификацията казва (сменям на x и y за по-малко объркване тук):

• В противен случай, ако y има тип Y и съществува неявно преобразуване от x към Y, типът на резултата е Y. По време на изпълнение първо се оценява x. Ако x не е null, x се разопакова до тип X0 (ако X съществува и е nullable) и се преобразува в тип Y, и това става резултатът. В противен случай y се оценява и става резултат.

Това се случва. Първо, лявата страна x, която е само a, се проверява за null. Но това не е null само по себе си. След това трябва да се използва лявата страна. След това се изпълнява неявното преобразуване. Неговият резултат от тип B е ... null.

Имайте предвид, че това е различно от:

    A a = new A();
    B b = (B)a ?? new B();

В този случай левият операнд е израз (x), който е null сам по себе си, а резултатът става дясната страна (y).

Може би неявните преобразувания между референтни типове трябва да връщат null (ако и) само ако оригиналът е null, като добра практика?


Предполагам, че момчетата, които са написали спецификацията можеха да го направят по следния начин (но не го направиха):

• В противен случай, ако y има тип Y и съществува неявно преобразуване от x в Y, типът на резултата е Y. По време на изпълнение x първо се оценява и преобразува в тип Y. Ако резултатът от това преобразуване не е нула, този изход става резултат. В противен случай y се оценява и става резултат.

Може би това щеше да е по-интуитивно? Това би принудило времето за изпълнение да извика вашето имплицитно преобразуване, без значение дали входът за преобразуването е null или не. Това не би трябвало да е твърде скъпо, ако типичните реализации бързо определят, че null → null.

person Jeppe Stig Nielsen    schedule 27.01.2014

Защо очаквахте операторът за нулево обединяване да върне new B()? a не е null, така че a ?? new B() се оценява на a.

След като вече знаем, че a ще бъде върнато, трябва да определим типа на резултата (T) и дали трябва да преобразуваме a към T.

• В противен случай, ако b има тип B и съществува неявно преобразуване от a към B, типът на резултата е B. По време на изпълнение първо се оценява a. Ако a не е null, a се разопакова до тип A0 (ако A съществува и е null) и преобразува в тип B, и това става резултат. В противен случай b се оценява и става резултат.

Съществува неявно преобразуване от A в B, така че B е типът резултат на израза. Което означава, че a ще бъде имплицитно прехвърлено към B. И вашият скрит оператор връща null.

Всъщност, ако напишете var b = a ?? new B(); (забележете var), ще видите, че компилаторът извежда B като тип, върнат от израза.

person dcastro    schedule 27.01.2014
comment
Това не обяснява защо промяната на декларацията на object b все още води до нула. - person Kendall Frey; 27.01.2014
comment
@KendallFrey Достатъчно близо до него. ?? работи само ако първият и вторият операнд разрешават един и същи тип (или nullable и nonnullable версии на тип). Първият операнд, a, се сравнява с нула и след това преобразува да бъде от тип B, в който момент неявното преобразуване го превръща в null. Изобщо не е необходимо да се присвоява на нещо, за да се случи това. - person Servy; 27.01.2014
comment
Правилата, цитирани в самия въпрос, за оператора ??, са по-подходящи (и по-сложни) от правилата, които цитирате (чрез блога на Lipperts) за троичния оператор ?:. Например предвид променливите bool b = false; short? s = 10; int i = 10; изразът s ?? i е разрешен, докато b ? s : i не се компилира. Този отговор беше първи, но според мен другите отговори са по-точни. - person Jeppe Stig Nielsen; 27.01.2014
comment
@JeppeStigNielsen добър улов! Обикновено се позовавам на тази публикация в блога, когато обсъждам както троичния, така и нулевия оператор - имам склонност да забравям, че тези правила се прилагат само за троичния оператор (въпреки че и двата набора от правила са много сходни) . Актуализирах отговора си. - person dcastro; 27.01.2014

В противен случай, ако b има тип B и съществува неявно преобразуване от a към B, типът резултат е B. По време на изпълнение първо се оценява a. Ако a не е null, a се разопакова до тип A0 (ако A съществува и е nullable) и се преобразува в тип B, и това става резултатът. В противен случай b се оценява и става резултат.

...b има тип B и съществува имплицитно преобразуване от a към B. a се оценява като нула. Следователно b трябва да бъде оценено и b трябва да бъде резултатът.

Тълкувате това погрешно. Нищо не казва, че преобразуването от a в B е извършено преди да бъде извършена проверката от null. Там се посочва, че null проверката се извършва преди преобразуването!

Вашият случай отговаря на това:

Ако a не е null, a се разопакова до тип A0 (ако A съществува и е null) и преобразува към тип B и това става резултат силен>.

person MarcinJuraszek    schedule 27.01.2014

Частта, която трябва да разгледаме, е типът по време на компилиране на нулевия коалесцентен израз.

В противен случай, ако b има тип B и съществува неявно преобразуване от a към B, типът резултат е B. По време на изпълнение първо се оценява a. Ако a не е null, a се разопакова до тип A0 (ако A съществува и е nullable) и се преобразува в тип B, и това става резултатът. В противен случай b се оценява и става резултат.

За да поставите това в псевдокод:

public Tuple<Type, object> NullCoalesce<TA, TB>(TA a, TB b)
{
    ...
    else if (a is TB) // pseudocode alert, this won't work in actual C#
    {
        Type type = typeof(TB);
        object result;
        if (a != null)
        {
            result = (TB)a; // in your example, this resolves to null
        }
        else
        {
            result = b;
        }
        return new Tuple<Type, object>(type, result);
    }
    ...
}
person Kendall Frey    schedule 27.01.2014