Wykonuj operatory zwarcia || i && istnieją dla wartości logicznych dopuszczających wartość null? Czasami tak sądzi RuntimeBinder

84

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 dynamicwią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óż, xi ynie jest to zaskakujące, ich deklaracje prowadzą do pobrania obu właściwości, a wynikowe wartości są zgodne z oczekiwaniami, xjest truei yjest null.

Ale ewaluacja xxz A || Bołowiu do wiązania bez wyjątku czasu, a tylko nieruchomość Azostała przeczytana, nie B. Dlaczego to się dzieje? Jak widać, moglibyśmy zmienić Bmetodę pobierającą, aby zwracała szalony obiekt, taki jak "Hello world"i xxnadal oceniałby truebez 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 Bzostanie 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 Bjako pierwszy operand, oba B || Ai B && Adaj wyjątek spinacza czasu wykonywania ( B | Ai B & Adział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).

Jeppe Stig Nielsen
źródło
4
W ogóle nie ma żadnych przypadków Nullable<Boolean>zaangażowania, tylko wartości logiczne w pudełku są traktowane jako dynamic- Twój test z bool?jest nieistotny. (Oczywiście nie jest to pełna odpowiedź, tylko zalążek jednego.)
Jeroen Mostert,
3
Ma A || Bto pewien sens, ponieważ nie chcesz oceniać, Bchyba że Ajest fałszywe, a tak nie jest. Więc tak naprawdę nigdy nie znasz typu wyrażenia. A && BWersja jest bardziej zaskakujące - Zobaczę, co mogę znaleźć w spec.
Jon Skeet
2
@JeroenMostert: No chyba, że ​​kompilator zdecydowałby, że jeśli typ Ais booli wartość Bis null, to bool && bool?może być zaangażowany operator.
Jon Skeet,
4
Co ciekawe, wygląda na to, że ujawniło to błąd kompilatora lub specyfikacji. Specyfikacja C # 5.0 dla &&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.
Jon Skeet
14
Wysłałem Madsowi e-mail o problemie ze specyfikacją, aby sprawdzić, czy to tylko problem w tym, jak to czytam ...
Jon Skeet,

Odpowiedzi:

67

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 to truelub false(które są wartościami logicznymi i dlatego są dozwolone jako pierwszy operand ?:), ale jeśli podasz nulljako pierwszy operand (np. Jeśli spróbujesz B && Aw 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:

  • Oceń lewy operand (nazwijmy wynik x)
  • Spróbuj przekształcić go w boolniejawną konwersję truelub falseoperatory lub (niepowodzenie, jeśli nie można)
  • Użyj xjako warunku w ?:operacji
  • W prawdziwej gałęzi użyj xjako wyniku
  • W fałszywej gałęzi oblicz teraz drugi operand (nazwijmy wynik y)
  • Spróbuj powiązać operator &lub |na podstawie typu środowiska wykonawczego xi y(niepowodzenie, jeśli nie jest możliwe)
  • Zastosuj wybrany operator

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

Mads Torgersen - MSFT
źródło
Widzę, że te operatory zwarciowe są szczególne, ponieważ przy dynamicznym wiązaniu tak naprawdę nie możemy znać typu drugiego operandu w przypadku, gdy dokonujemy zwarcia. Może specyfikacja powinna o tym wspomnieć? Oczywiście, ponieważ wszystko Wewnątrz dynamicjest zapakowane, nie możemy powiedzieć, że różnica między bool?które HasValuei „Simple” bool.
Jeppe Stig Nielsen
6

Czy to prawidłowe zachowanie?

Tak, jestem całkiem pewien, że tak.

Jak możesz to wywnioskować ze specyfikacji?

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:

Jeśli operand warunkowego operatora logicznego ma typ dynamiczny w czasie kompilacji, wówczas wyrażenie jest dynamicznie wiązane (§ 7.2.2). W tym przypadku typ wyrażenia w czasie kompilacji jest dynamiczny, a rozwiązanie opisane poniżej będzie miało miejsce w czasie wykonywania przy użyciu typu czasu wykonywania tych operandów, które mają typ dynamiczny w czasie kompilacji.

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:

  • Operacja x && y jest oceniana jako T.false (x)? x: T. & (x, y), gdzie T.false (x) to wywołanie operatora false zadeklarowanego w T, a T. & (x, y) to wywołanie wybranego operatora &
  • Operacja x || y jest oceniane jako T.true (x)? x: T. | (x, y), gdzie T.true (x) jest wywołaniem operatora true zadeklarowanego w T, a T. | (x, y) jest wywołaniem wybranego operatora |.

W obu przypadkach pierwszy operand x zostanie przekonwertowany na bool przy użyciu operatorów falselub true. 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.

Ale ocena dla xx A || B nie prowadzi do żadnego wyjątku w czasie wiązania i odczytano tylko właściwość A, a nie B. Dlaczego tak się dzieje?

Dla ||operatora wiemy, że to następuje true(A) ? A : |(A, B). Mamy zwarcie, więc nie dostaniemy wyjątku czasu wiązania. Nawet gdyby tak Abyło false, nadal nie otrzymalibyśmy wyjątku powiązania środowiska uruchomieniowego z powodu określonych kroków rozwiązania. Jeśli Atak false, wykonujemy |operator, który z powodzeniem obsługuje wartości null, zgodnie z sekcją 7.11.4.

Ocena A && B (dla yy) również nie prowadzi do błędu czasu wiązania. I tutaj oczywiście pobierane są obie właściwości. Dlaczego jest to dozwolone przez spinacz czasu wykonywania? Jeśli zwrócony obiekt z B zostanie zmieniony na „zły” obiekt (taki jak łańcuch), wystąpi wyjątek wiązania.

Z podobnych powodów ten również działa. &&jest oceniany jako false(x) ? x : &(x, y). Amożna pomyślnie przekonwertować na plik bool, więc nie ma problemu. Ponieważ Bjest null, &operator jest przenoszony (sekcja 7.3.7) z tego, który przyjmuje a, booldo tego, który przyjmuje bool?parametry, a zatem nie ma wyjątku w czasie wykonywania.

W przypadku obu operatorów warunkowych, jeśli Bjest 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, gdy Anie spełnia pierwszego warunku operatora ( truefor ||, falsefor &&). Dzieje się tak, ponieważ dynamiczne wiązanie jest dość leniwe. Nie będzie próbował powiązać operatora logicznego, chyba że Ajest fałszywy i musi przejść tą ścieżką, aby ocenić operator logiczny. Gdy Anie spełni pierwszego warunku dla operatora, zakończy się niepowodzeniem z wiążącym wyjątkiem.

Jeśli spróbujesz B jako pierwszego operandu, oba B || A i B && A dają wyjątek spinacza czasu wykonywania.

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 Bi użycie jednego z operatorów konwersji bool ( false(B)lub true(B)) przed wykonaniem operacji logicznej. Oczywiście B, bycia nullnie można przekonwertować na jeden z truelub false, więc zdarza się wyjątek powiązania środowiska uruchomieniowego.

Christopher Currens
źródło
Nic dziwnego, że dynamicpowią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ąża operator truei operator false. explicit operatorPowrocie booljest coś innego niż operator truea false. Trudno czytać spec w jakikolwiek sposób, który pozwala A && B(w moim przykładzie), nie pozwalając również a && b, gdzie ai bsą statycznie wpisywanych zerowalne wartości logicznych, czyli bool? aa bool? b, z obowiązującymi w czasie kompilacji. Jednak to jest niedozwolone.
Jeppe Stig Nielsen,
-1

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;
Thomas Papamihos
źródło