Przeczytałem specyfikację języka C # dotyczącą warunkowych operatorów logicznych ||
i &&
, znanych również jako zwierające operatory logiczne. Wydawało mi się niejasne, czy istniały one dla wartości logicznych dopuszczających wartość null, tj. Typu operandu Nullable<bool>
(również napisanego bool?
), więc wypróbowałem to z typowaniem niedynamicznym:
bool a = true;
bool? b = null;
bool? xxxx = b || a; // compile-time error, || can't be applied to these types
To wydawało się rozstrzygać kwestię (nie mogłem jasno zrozumieć specyfikacji, ale zakładając, że implementacja kompilatora Visual C # była poprawna, teraz wiedziałem).
Jednak chciałem też spróbować z dynamic
wiązaniem. Więc zamiast tego spróbowałem:
static class Program
{
static dynamic A
{
get
{
Console.WriteLine("'A' evaluated");
return true;
}
}
static dynamic B
{
get
{
Console.WriteLine("'B' evaluated");
return null;
}
}
static void Main()
{
dynamic x = A | B;
Console.WriteLine((object)x);
dynamic y = A & B;
Console.WriteLine((object)y);
dynamic xx = A || B;
Console.WriteLine((object)xx);
dynamic yy = A && B;
Console.WriteLine((object)yy);
}
}
Zaskakującym wynikiem jest to, że działa to bez wyjątku.
Cóż, x
i y
nie jest to zaskakujące, ich deklaracje prowadzą do pobrania obu właściwości, a wynikowe wartości są zgodne z oczekiwaniami, x
jest true
i y
jest null
.
Ale ewaluacja xx
z A || B
ołowiu do wiązania bez wyjątku czasu, a tylko nieruchomość A
została przeczytana, nie B
. Dlaczego to się dzieje? Jak widać, moglibyśmy zmienić B
metodę pobierającą, aby zwracała szalony obiekt, taki jak "Hello world"
i xx
nadal oceniałby true
bez problemów z wiązaniem ...
Ocenianie A && B
(dla yy
) również nie prowadzi do błędu w czasie wiązania. I tutaj oczywiście pobierane są obie właściwości. Dlaczego jest to dozwolone przez spinacz czasu wykonywania? Jeśli zwracany obiekt z B
zostanie zmieniony na „zły” obiekt (taki jak a string
), występuje wyjątek wiązania.
Czy to prawidłowe zachowanie? (Jak możesz to wywnioskować ze specyfikacji?)
Jeśli spróbujesz B
jako pierwszy operand, oba B || A
i B && A
daj wyjątek spinacza czasu wykonywania ( B | A
i B & A
działa dobrze, ponieważ wszystko jest normalne z operatorami nie powodującymi zwarcia |
i &
).
(Wypróbowano z kompilatorem C # programu Visual Studio 2013 i wersją środowiska wykonawczego .NET 4.5.2).
źródło
Nullable<Boolean>
zaangażowania, tylko wartości logiczne w pudełku są traktowane jakodynamic
- Twój test zbool?
jest nieistotny. (Oczywiście nie jest to pełna odpowiedź, tylko zalążek jednego.)A || B
to pewien sens, ponieważ nie chcesz oceniać,B
chyba żeA
jest fałszywe, a tak nie jest. Więc tak naprawdę nigdy nie znasz typu wyrażenia.A && B
Wersja jest bardziej zaskakujące - Zobaczę, co mogę znaleźć w spec.A
isbool
i wartośćB
isnull
, tobool && bool?
może być zaangażowany operator.&&
rozmów o rozwiązywaniu go tak, jakby była&
zamiast tego, a konkretnie obejmuje przypadek, w którym oba operandy sąbool?
- ale następnie następna sekcja, do której się odwołuje, nie obsługuje wielkości dopuszczalnej. Mógłbym dodać coś w rodzaju odpowiedzi, bardziej szczegółowo na ten temat, ale nie wyjaśniłoby to w pełni.Odpowiedzi:
Przede wszystkim dziękuję za wskazanie, że specyfikacja nie jest jasna w przypadku niedynamicznego przypadku nullable-bool. Naprawię to w przyszłej wersji. Zachowanie kompilatora jest zamierzonym zachowaniem;
&&
i||
nie powinny działać na wartościach zerowych.Jednak dynamiczne spoiwo nie wydaje się implementować tego ograniczenia. Zamiast tego wiąże operacje komponentu osobno:
&
/|
i?:
. W ten sposób jest w stanie sobie poradzić, jeśli zdarzy się, że pierwszy operand totrue
lubfalse
(które są wartościami logicznymi i dlatego są dozwolone jako pierwszy operand?:
), ale jeśli podasznull
jako pierwszy operand (np. Jeśli spróbujeszB && A
w powyższym przykładzie), pobierz wyjątek powiązania środowiska uruchomieniowego.Jeśli się nad tym zastanowisz, możesz zobaczyć, dlaczego zaimplementowaliśmy dynamiczną
&&
i w||
ten sposób zamiast jako jedną dużą dynamiczną operację: operacje dynamiczne są wiązane w czasie wykonywania po ocenie ich operandów , dzięki czemu powiązanie może być oparte na typach środowiska wykonawczego wyników tych ocen. Ale taka gorliwa ocena jest sprzeczna z celem zwarcia operatorów! Zamiast tego wygenerowany kod dla dynamiki&&
i||
dzieli ocenę na części i będzie postępować w następujący sposób:x
)bool
niejawną konwersjętrue
lubfalse
operatory lub (niepowodzenie, jeśli nie można)x
jako warunku w?:
operacjix
jako wynikuy
)&
lub|
na podstawie typu środowiska wykonawczegox
iy
(niepowodzenie, jeśli nie jest możliwe)Jest to zachowanie, które przepuszcza pewne „niedozwolone” kombinacje operandów:
?:
operator pomyślnie traktuje pierwszy operand jako wartość logiczną nieprzekraczającą wartości null , operator&
lub z|
powodzeniem traktuje go jako wartość logiczną dopuszczającą wartość null , a dwa nigdy nie koordynują, aby sprawdzić, czy się zgadzają .Więc to nie jest tak dynamiczne && i || praca na wartości null. Chodzi o to, że zostały zaimplementowane w sposób nieco zbyt łagodny w porównaniu z przypadkiem statycznym. Powinno to prawdopodobnie zostać uznane za błąd, ale nigdy tego nie naprawimy, ponieważ byłaby to przełomowa zmiana. Nie pomogłoby też nikomu zaostrzyć zachowania.
Mamy nadzieję, że to wyjaśnia, co się dzieje i dlaczego! To intrygujący obszar i często jestem zdumiony konsekwencjami decyzji, które podjęliśmy, wdrażając dynamikę. To pytanie było pyszne - dziękuję za poruszenie!
Mads
źródło
dynamic
jest zapakowane, nie możemy powiedzieć, że różnica międzybool?
któreHasValue
i „Simple”bool
.Tak, jestem całkiem pewien, że tak.
Sekcja 7.12 specyfikacji języka C # w wersji 5.0 zawiera informacje dotyczące operatorów warunkowych
&&
i||
powiązania z nimi dynamicznego wiązania. Odpowiednia sekcja:Myślę, że to jest kluczowy punkt, który odpowiada na twoje pytanie. Jaka jest rozdzielczość występująca w czasie wykonywania? Sekcja 7.12.2, Warunkowe operatory logiczne zdefiniowane przez użytkownika, wyjaśnia:
W obu przypadkach pierwszy operand x zostanie przekonwertowany na bool przy użyciu operatorów
false
lubtrue
. Następnie wywoływany jest odpowiedni operator logiczny. Mając to na uwadze, mamy wystarczająco dużo informacji, aby odpowiedzieć na pozostałe pytania.Dla
||
operatora wiemy, że to następujetrue(A) ? A : |(A, B)
. Mamy zwarcie, więc nie dostaniemy wyjątku czasu wiązania. Nawet gdyby takA
byłofalse
, nadal nie otrzymalibyśmy wyjątku powiązania środowiska uruchomieniowego z powodu określonych kroków rozwiązania. JeśliA
takfalse
, wykonujemy|
operator, który z powodzeniem obsługuje wartości null, zgodnie z sekcją 7.11.4.Z podobnych powodów ten również działa.
&&
jest oceniany jakofalse(x) ? x : &(x, y)
.A
można pomyślnie przekonwertować na plikbool
, więc nie ma problemu. PonieważB
jest null,&
operator jest przenoszony (sekcja 7.3.7) z tego, który przyjmuje a,bool
do tego, który przyjmujebool?
parametry, a zatem nie ma wyjątku w czasie wykonywania.W przypadku obu operatorów warunkowych, jeśli
B
jest czymś innym niż bool (lub null dynamic), powiązanie środowiska uruchomieniowego kończy się niepowodzeniem, ponieważ nie może znaleźć przeciążenia, które przyjmuje jako parametry bool i non-bool. Dzieje się tak jednak tylko wtedy, gdyA
nie spełnia pierwszego warunku operatora (true
for||
,false
for&&
). Dzieje się tak, ponieważ dynamiczne wiązanie jest dość leniwe. Nie będzie próbował powiązać operatora logicznego, chyba żeA
jest fałszywy i musi przejść tą ścieżką, aby ocenić operator logiczny. GdyA
nie spełni pierwszego warunku dla operatora, zakończy się niepowodzeniem z wiążącym wyjątkiem.Miejmy nadzieję, że już wiesz, dlaczego tak się dzieje (lub źle się spisałem, wyjaśniając). Pierwszym krokiem do rozwiązania tego operatora warunkowego jest pobranie pierwszego operandu
B
i użycie jednego z operatorów konwersji bool (false(B)
lubtrue(B)
) przed wykonaniem operacji logicznej. OczywiścieB
, bycianull
nie można przekonwertować na jeden ztrue
lubfalse
, więc zdarza się wyjątek powiązania środowiska uruchomieniowego.źródło
dynamic
powiązanie odbywa się w czasie wykonywania przy użyciu rzeczywistych typów instancji, a nie typów z czasu kompilacji (pierwszy cytat). Twój drugi cytat jest nieistotny, ponieważ żaden typ tutaj nie przeciążaoperator true
ioperator false
.explicit operator
Powrociebool
jest coś innego niżoperator true
afalse
. Trudno czytać spec w jakikolwiek sposób, który pozwalaA && B
(w moim przykładzie), nie pozwalając równieża && b
, gdziea
ib
są statycznie wpisywanych zerowalne wartości logicznych, czylibool? a
abool? b
, z obowiązującymi w czasie kompilacji. Jednak to jest niedozwolone.Typ dopuszczający wartość null nie definiuje warunkowych operatorów logicznych || i &&. Proponuję następujący kod:
bool a = true; bool? b = null; bool? xxxxOR = (b.HasValue == true) ? (b.Value || a) : a; bool? xxxxAND = (b.HasValue == true) ? (b.Value && a) : false;
źródło