Führt C # eine Kurzschlussbewertung von if-Anweisungen mit Wartezeit durch?

73

Ich glaube, dass C # die Auswertung einer if-Anweisungsbedingung beendet, sobald es in der Lage ist, das Ergebnis zu ermitteln. Also zum Beispiel:

if ( (1 < 0) && check_something_else() )
    // this will not be called

Da die Bedingung (1 < 0)als ausgewertet wird false, kann die &&Bedingung nicht erfüllt werden und check_something_else()wird nicht aufgerufen.

Wie wertet C # eine if-Anweisung mit asynchronen Funktionen aus? Wartet es, bis beide zurückkehren? Also zum Beispiel:

if( await first_check() && await second_check() )
    // ???

Wird dies jemals kurzgeschlossen werden?

Aidan
quelle
13
Weder ifnoch awaitKurzschluss beeinflussen. asyncBeeinflussen Sie nicht das Verhalten der Sprache, außer dass Sie await die Verwendung zulassen und darauf warten, dass bereits asynchrone Vorgänge ausgeführt werden, ohne sie zu blockieren.
Panagiotis Kanavos
14
Welcher Teil der Operator &&Dokumentation hat Sie zu der Annahme gebracht, dass ein Kurzschluss jemals übersprungen werden kann?
Alexei Levenkov
8
@ IanKemp: Ich denke, Sie müssen noch einmal lesen, was Alexei gesagt hat ...
Musefan
1
Kurzschluss und boolesche Logik sind jedoch nicht dasselbe. (<anyThing> && false) kann auch nur als false ausgewertet werden, aber C, C #, C ++ haben entschieden, dass das Kurzschließen nur auf dem ersten Argument basiert.
Hans Olsson
2
Kurzschluss hat nichts damit zu tun if. &&und ||führen Sie einen Kurzschluss durch, egal wo sie verwendet werden, z. B.some_var = <expression1> && <expression2>
Barmar

Antworten:

66

Dies ist super einfach zu überprüfen.

Versuchen Sie diesen Code:

async Task Main()
{
    if (await first_check() && await second_check())
    {
        Console.WriteLine("Here?");
    }
    Console.WriteLine("Tested");
}

Task<bool> first_check() => Task.FromResult(false);
Task<bool> second_check() { Console.WriteLine("second_check"); return Task.FromResult(true); }

Es gibt "Getestet" und sonst nichts aus.

Rätselhaftigkeit
quelle
29
Der Test zeigt, dass dieser Kurzschluss durch C # zulässig ist (vorausgesetzt, der verwendete Compiler ist kompatibel). Es wird nicht angezeigt, ob der Kurzschluss erforderlich ist, auf den sich der Programmierer verlassen kann.
Nanoman
Ich denke, ein Teil des Kontextes hier ist, dass zwei awaitAnweisungen, die direkt nacheinander platziert werden, im Wesentlichen in der richtigen Reihenfolge ausgeführt werden. Obwohl sie für die asynchrone Programmierung verwendet werden, awaitändert sich die Reihenfolge, in der die s selbst ausgeführt werden, nie. (Dies gilt offensichtlich nicht, wenn sie unterschiedliche Funktionen haben oder so.) Daher wird immer das Ergebnis von " first_check()Zurück" angezeigt, bevor überhaupt über einen Anruf nachgedacht wird second_check(). Die Kurzschlussauswertung wird also immer in derselben Reihenfolge durchgeführt, ohne jemals zuvor auszuwerten second_check().
Panzercrisis
1
@Panzercrisis Die andere Art, darüber nachzudenken, ist, dass der springende Punkt darin awaitbesteht, dass asynchrone Funktionen synchron erscheinen. Es gibt keinen Grund, warum dies anders sein sollte, wenn sie in booleschen Ausdrücken verwendet werden.
Barmar
91

Ja, es wird kurzgeschlossen. Ihr Code entspricht:

bool first = await first_check();
if (first)
{
    bool second = await second_check();
    if (second)
    {
        ...
    }
}

Beachten Sie, wie es selbst dann nicht nennen second_check , bis die awaitable zurück von first_checkabgeschlossen. Beachten Sie daher, dass die beiden Prüfungen nicht parallel ausgeführt werden. Wenn Sie das tun wollten, könnten Sie verwenden:

var t1 = first_check();
var t2 = second_check();

if (await t1 && await t2)
{
}

An diesem Punkt:

  • Die beiden Prüfungen werden parallel ausgeführt (vorausgesetzt, sie sind wirklich asynchron).
  • Es wird gewartet, bis die erste Prüfung abgeschlossen ist, und dann nur, bis die zweite Prüfung abgeschlossen ist, wenn die erste true zurückgibt
  • Wenn die erste Prüfung false zurückgibt, die zweite Prüfung jedoch mit einer Ausnahme fehlschlägt, wird die Ausnahme effektiv verschluckt
  • Wenn die zweite Prüfung sehr schnell false zurückgibt, die erste Prüfung jedoch lange dauert, dauert der Gesamtvorgang lange, da darauf gewartet wird, dass die erste Prüfung zuerst abgeschlossen wird

Wenn Sie Prüfungen parallel ausführen und beenden möchten, sobald eine von ihnen false zurückgibt, möchten Sie wahrscheinlich einen Allzweckcode dafür schreiben, die Aufgaben zunächst sammeln und dann Task.WhenAnywiederholt verwenden. (Sie sollten auch überlegen, was mit Ausnahmen geschehen soll, die von Aufgaben ausgelöst werden, die für das Endergebnis aufgrund einer anderen Aufgabe, die false zurückgibt, praktisch irrelevant sind.)

Jon Skeet
quelle
14
Tanks für die explizite Aussage hier, dass die Ausnahme effektiv verschluckt wird, wenn der erste Aufruf zurückkehrt false. Dieser Punkt wird oft übersehen, wenn jemand verwendet Task.WhenAny.
Sebastian Schumann
Danke, das war super nützlich. Ich möchte sie eigentlich nicht parallel bewerten, ich war nur neugierig, wie das awaitfunktioniert.
Aidan
Beachten Sie, dass , wenn Sie alle Auswertung erreichen wollen, kann es leichter erreicht wird durch die Verwendung &statt &&: if (await first_check() & await second_check()) { ... }Das ist , weil der &Bediener nicht Bypass etwas, während &&stoppt , wenn das Ergebnis klar ist , und ändert nicht durch nachfolgende Operanden (dh , wenn der erste Operanden schon falsedann macht es keinen Sinn, den zweiten Operanden zu überprüfen). Das gleiche gilt für |und ||(logisches ODER gegen Verknüpfungs-ODER), aber hier bedeutet die Verknüpfung, dass die Auswertung stoppt, wenn der erste Operand ist true.
Matt
12

Ja tut es. Sie können dies selbst mit sharplab.io überprüfen :

public async Task M() {
    if(await Task.FromResult(true) && await Task.FromResult(false))
        Console.WriteLine();
}

Wird vom Compiler effektiv in Folgendes umgewandelt:

TaskAwaiter<bool> awaiter;

... compiler-generated state machine for first task...

bool result = awaiter.GetResult();

// second operation started and awaited only if first one returned true    
if (result)
{
     awaiter = Task.FromResult(false).GetAwaiter();
...

Oder als einfaches Programm:

Task<bool> first_check() => Task.FromResult(false);
Task<bool> second_check() => throw new Exception("Will Not Happen");

if (await first_check() && await second_check()) {}

Zweites Beispiel auf sharplab.io .

Guru Stron
quelle
3

Da ich selbst Compiler geschrieben habe, fühle ich mich qualifiziert, eine logischere Meinung abzugeben, die nicht nur auf einigen Tests basiert.

Heutzutage verwandeln die meisten Compiler den Quellcode in einen AST (Abstract Syntax Tree), mit dem der Quellcode sprachunabhängig dargestellt wird.
AST besteht normalerweise aus Syntaxknoten. Ein Syntaxknoten, der einen Wert erzeugt, wird als Ausdruck bezeichnet, während einer, der nichts erzeugt, eine Anweisung ist.

Angesichts des Codes in der Frage,

if (await first_check() && await second_check())

Betrachten wir also den Testbedingungsausdruck

await first_check() && await second_check()

Der für einen solchen Code erstellte AST lautet wie folgt:

AndExpression:
    firstOperand = (
        AwaitExpression:
            operand = (
                MethodInvocationExpression:
                    name = "first_check"
                    parameterTypes = []
                    arguments = []
            )
    )
    secondOperand = (
        AwaitExpression:
            operand = (
                MethodInvocationExpression:
                    name = "second_check"
                    parameterTypes = []
                    arguments = []
            )
    )

Der AST selbst und die Syntax, mit der ich ihn dargestellt habe, wurden im laufenden Betrieb vollständig erfunden. Ich hoffe, es ist klar. Es sieht so aus, als würde es der StackOverflow-Markup-Engine gefallen, da sie gut aussieht! :) :)

An dieser Stelle muss herausgefunden werden, wie interpretiert wird. Nun, ich kann den meisten Dolmetschern sagen, dass sie Ausdrücke nur hierarchisch bewerten. Daher wird es so ziemlich so gemacht:

  1. Bewerten Sie den Ausdruck await first_check() && await second_check()

    1. Bewerten Sie den Ausdruck await first_check()

      1. Bewerten Sie den Ausdruck first_check()

        1. Löse das Symbol auf first_check

          1. Ist es eine Referenz? Nein (andernfalls prüfen Sie, ob es auf einen Delegierten verweist.)
          2. Ist es ein Methodenname? Ja (Ich schließe Dinge wie das Auflösen verschachtelter Bereiche, das Überprüfen, ob sie statisch sind oder nicht, usw. nicht ein, da sie nicht zum Thema gehören und in der Frage nicht genügend Informationen enthalten sind, um diese Details genauer zu untersuchen.)
        2. Argumente auswerten. Da ist niemand. Es soll also eine parameterlose Methode mit dem Namen first_checkaufgerufen werden.

        3. Rufen Sie eine parameterlose Methode mit dem Namen auf, first_checkderen Ergebnis der Wert des Ausdrucks ist first_check().

      2. Es wird erwartet, dass der Wert ein Task<T>oder ist ValueTask<T>, da dies ein wartender Ausdruck ist.

      3. Auf den Ausdruck "Warten" wird gewartet, um den Wert zu erhalten, den er schließlich erzeugen wird.

    2. Produziert der erste Operand des Ausdrucks und false? Ja. Der zweite Operand muss nicht ausgewertet werden.

    3. An diesem Punkt wissen wir, dass der Wert von await first_check() && await second_check()notwendigerweise auch falseso sein wird.

Einige der Überprüfungen, die ich aufgenommen habe, werden statisch durchgeführt (dh zur Kompilierungszeit). Sie dienen jedoch dazu, die Dinge klarer zu machen - es ist unnötig, über die Kompilierung zu sprechen, da wir uns nur mit der Art und Weise befassen, wie Ausdrücke ausgewertet werden.

Das Wesentliche an dieser ganzen Sache ist, dass es C # egal ist, ob der Ausdruck erwartet wird oder nicht - es ist immer noch der erste Operand eines und-Ausdrucks und wird als solcher zuerst ausgewertet. Dann wird nur trueausgewertet , wenn der zweite Operand erzeugt wird. Andernfalls wird angenommen, dass das Ganze und der Ausdruck so sind false, wie es nicht anders sein kann.

Dies ist meistens die Art und Weise, wie die überwiegende Mehrheit der Compiler, einschließlich Roslyn (der eigentliche C # -Compiler, der vollständig mit C # geschrieben wurde) und Interpreter funktionieren, obwohl ich einige Implementierungsdetails versteckt habe, die keine Rolle spielen, wie die Art und Weise, wie auf den Ausdruck gewartet wird Ich habe wirklich darauf gewartet, was Sie selbst verstehen können, wenn Sie sich den generierten Bytecode ansehen (Sie können eine Website wie diese verwenden . Ich bin sowieso nicht mit dieser Website verbunden - ich schlage sie nur vor, weil sie Roslyn verwendet und ich denke, es ist eine schöne Werkzeug zu beachten .)

Zur Verdeutlichung ist die Art und Weise, wie Ausdrücke abwarten, ziemlich kompliziert und passt nicht zum Thema dieser Frage. Es würde eine vollständige, getrennte Antwort verdienen, um richtig erklärt zu werden, aber ich halte es nicht für wichtig, da es sich lediglich um ein Implementierungsdetail handelt und der erwartete Ausdruck sich ohnehin nicht anders verhält als normale Ausdrücke.

Davide Cannizzo
quelle
Ich bin sehr gespannt auf die Abwertung, die ich gerade erhalten habe. Lieber Downvoter, würde es Ihnen etwas ausmachen, meine Antwort zu kommentieren, um einen Grund für die Downvote anzugeben?
Davide Cannizzo