Was passiert wirklich bei einem Versuch? {Return x; } endlich {x = null; } Aussage?

259

Ich habe diesen Tipp in einer anderen Frage gesehen und mich gefragt, ob mir jemand erklären könnte, wie um alles in der Welt das funktioniert.

try { return x; } finally { x = null; }

Ich meine, wird die finallyKlausel wirklich nach der returnAnweisung ausgeführt? Wie thread-unsicher ist dieser Code? Können Sie sich zusätzliche Hackerangriffe vorstellen, die mit diesem try-finallyHack durchgeführt werden können?

Dmitri Nesteruk
quelle

Antworten:

235

Nein - auf IL-Ebene können Sie nicht aus einem Block mit Ausnahmebehandlung zurückkehren. Es speichert es im Wesentlichen in einer Variablen und kehrt danach zurück

dh ähnlich wie:

int tmp;
try {
  tmp = ...
} finally {
  ...
}
return tmp;

Zum Beispiel (mit Reflektor):

static int Test() {
    try {
        return SomeNumber();
    } finally {
        Foo();
    }
}

kompiliert zu:

.method private hidebysig static int32 Test() cil managed
{
    .maxstack 1
    .locals init (
        [0] int32 CS$1$0000)
    L_0000: call int32 Program::SomeNumber()
    L_0005: stloc.0 
    L_0006: leave.s L_000e
    L_0008: call void Program::Foo()
    L_000d: endfinally 
    L_000e: ldloc.0 
    L_000f: ret 
    .try L_0000 to L_0008 finally handler L_0008 to L_000e
}

Dies deklariert im Grunde eine lokale Variable ( CS$1$0000), platziert den Wert in der Variablen (innerhalb des behandelten Blocks) und lädt nach dem Verlassen des Blocks die Variable und gibt sie dann zurück. Reflektor macht dies wie folgt:

private static int Test()
{
    int CS$1$0000;
    try
    {
        CS$1$0000 = SomeNumber();
    }
    finally
    {
        Foo();
    }
    return CS$1$0000;
}
Marc Gravell
quelle
10
Ist das nicht genau das, was ocdedio gesagt hat: Das wird schließlich nach der Berechnung des Rückgabewerts und bevor es wirklich von der Funktion zurückkehrt ausgeführt?
mmmmmmmm
"Block mit Ausnahmebehandlung" Ich denke, dieses Szenario hat nichts mit Ausnahmen und Ausnahmebehandlung zu tun. Hier geht es darum, wie .NET das endgültige Resource Guard-Konstrukt implementiert .
g.pickardou
361

Die finally-Anweisung wird ausgeführt, der Rückgabewert wird jedoch nicht beeinflusst. Die Ausführungsreihenfolge lautet:

  1. Code vor der Ausführung der return-Anweisung
  2. Der Ausdruck in der return-Anweisung wird ausgewertet
  3. Schließlich wird der Block ausgeführt
  4. Das in Schritt 2 ausgewertete Ergebnis wird zurückgegeben

Hier ist ein kurzes Programm zur Demonstration:

using System;

class Test
{
    static string x;

    static void Main()
    {
        Console.WriteLine(Method());
        Console.WriteLine(x);
    }

    static string Method()
    {
        try
        {
            x = "try";
            return x;
        }
        finally
        {
            x = "finally";
        }
    }
}

Dies gibt "try" aus (weil das zurückgegeben wird) und dann "finally", weil dies der neue Wert von x ist.

Wenn wir einen Verweis auf ein veränderbares Objekt (z. B. einen StringBuilder) zurückgeben, werden alle Änderungen, die am Objekt im finally-Block vorgenommen wurden, bei der Rückgabe sichtbar - dies hat keinen Einfluss auf den Rückgabewert selbst (der nur ein Wert ist) Referenz).

Jon Skeet
quelle
Ich möchte fragen, ob es in Visual Studio eine Option gibt, um die generierte Intermediate Language (IL) für geschriebenen C # -Code zum Zeitpunkt der Ausführung
Enigma State
Die Ausnahme von "Wenn wir einen Verweis auf ein veränderbares Objekt (z. B. einen StringBuilder) zurückgeben, werden alle am Objekt im finally-Block vorgenommenen Änderungen bei der Rückgabe sichtbar" ist, wenn das StringBuilder-Objekt im finally-Block auf null gesetzt ist. In diesem Fall wird ein Nicht-Null-Objekt zurückgegeben.
Nick
4
@Nick: Das ist keine Änderung am Objekt - das ist eine Änderung an der Variablen . Es hat keinen Einfluss auf das Objekt, auf das sich der vorherige Wert der Variablen überhaupt bezieht. Also nein, das ist keine Ausnahme.
Jon Skeet
3
@Skeet Bedeutet das "return-Anweisung gibt eine Kopie zurück"?
Prabhakaran
4
@prabhakaran: Nun, es wertet den Ausdruck am Punkt der returnAnweisung aus und dieser Wert wird zurückgegeben. Der Ausdruck wird nicht ausgewertet, da die Kontrolle die Methode verlässt.
Jon Skeet
19

Die finally-Klausel wird nach der return-Anweisung ausgeführt, jedoch bevor sie tatsächlich von der Funktion zurückkehrt. Es hat wenig mit Thread-Sicherheit zu tun, denke ich. Es ist kein Hack - das wird garantiert immer ausgeführt, egal was Sie in Ihrem Try-Block oder Ihrem Catch-Block tun.

Otávio Décio
quelle
13

Zusätzlich zu den Antworten von Marc Gravell und Jon Skeet ist es wichtig zu beachten, dass sich Objekte und andere Referenztypen bei der Rückgabe ähnlich verhalten, jedoch einige Unterschiede aufweisen.

Das zurückgegebene "Was" folgt der gleichen Logik wie einfache Typen:

class Test {
    public static Exception AnException() {
        Exception ex = new Exception("Me");
        try {
            return ex;
        } finally {
            // Reference unchanged, Local variable changed
            ex = new Exception("Not Me");
        }
    }
}

Die zurückgegebene Referenz wurde bereits ausgewertet, bevor der lokalen Variablen im finally-Block eine neue Referenz zugewiesen wurde.

Die Ausführung ist im Wesentlichen:

class Test {
    public static Exception AnException() {
        Exception ex = new Exception("Me");
        Exception CS$1$0000 = null;
        try {
            CS$1$0000 = ex;
        } finally {
            // Reference unchanged, Local variable changed
            ex = new Exception("Not Me");
        }
        return CS$1$0000;
    }
}

Der Unterschied besteht darin, dass es weiterhin möglich ist, veränderbare Typen mithilfe der Eigenschaften / Methoden des Objekts zu ändern, was zu unerwartetem Verhalten führen kann, wenn Sie nicht vorsichtig sind.

class Test2 {
    public static System.IO.MemoryStream BadStream(byte[] buffer) {
        System.IO.MemoryStream ms = new System.IO.MemoryStream(buffer);
        try {
            return ms;
        } finally {
            // Reference unchanged, Referenced Object changed
            ms.Dispose();
        }
    }
}

Eine zweite Sache, die bei try-return-finally zu beachten ist, ist, dass Parameter, die "als Referenz" übergeben werden, nach der Rückgabe noch geändert werden können. Nur der Rückgabewert wurde ausgewertet und in einer temporären Variablen gespeichert, die darauf wartet, zurückgegeben zu werden. Alle anderen Variablen werden weiterhin auf normale Weise geändert. Der Vertrag eines out-Parameters kann sogar bis zur endgültigen Blockierung auf diese Weise unerfüllt bleiben.

class ByRefTests {
    public static int One(out int i) {
        try {
            i = 1;
            return i;
        } finally {
            // Return value unchanged, Store new value referenced variable
            i = 1000;
        }
    }

    public static int Two(ref int i) {
        try {
            i = 2;
            return i;
        } finally {
            // Return value unchanged, Store new value referenced variable
            i = 2000;
        }
    }

    public static int Three(out int i) {
        try {
            return 3;
        } finally {
            // This is not a compile error!
            // Return value unchanged, Store new value referenced variable
            i = 3000;
        }
    }
}

Wie jedes andere Flusskonstrukt hat "try-return-finally" seinen Platz und kann saubereren Code ermöglichen, als die Struktur zu schreiben, in die es tatsächlich kompiliert wird. Aber es muss vorsichtig verwendet werden, um Gotchas zu vermeiden.

Arkaine55
quelle
4

Wenn xes sich um eine lokale Variable handelt, sehe ich den Punkt nicht, da er xbeim Beenden der Methode ohnehin effektiv auf null gesetzt wird und der Wert des Rückgabewerts nicht null ist (da er vor dem Aufruf zum Setzen in das Register gestellt wurde xzu null).

Ich kann dies nur sehen, wenn Sie die Änderung des Wertes eines Feldes bei der Rückgabe (und nach der Bestimmung des Rückgabewerts) garantieren möchten.

casperOne
quelle
Es sei denn, die lokale Variable wird auch von einem Delegierten erfasst :)
Jon Skeet
Dann gibt es eine Schließung, und das Objekt kann nicht durch Müll gesammelt werden, da noch eine Referenz vorhanden ist.
rekursiv
Aber ich verstehe immer noch nicht, warum Sie eine lokale Variable in einem Delegaten verwenden würden, es sei denn, Sie wollten ihren Wert verwenden.
rekursiv