Co tak naprawdę dzieje się podczas try {return x; } w końcu {x = null; } oświadczenie?

259

Widziałem tę wskazówkę w innym pytaniu i zastanawiałem się, czy ktoś mógłby mi wyjaśnić, jak to działa?

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

Znaczy, to czy finallyklauzula naprawdę wykonać po tym returnstwierdzeniem? Jak niebezpieczny jest wątek w tym kodzie? Czy możesz pomyśleć o jakimkolwiek dodatkowym hakowaniu, który można by zrobić w przypadku tego try-finallyhacka?

Dmitri Nesteruk
źródło

Odpowiedzi:

235

Nie - na poziomie IL nie można wracać z bloku obsługiwanego wyjątkiem. Zasadniczo przechowuje ją w zmiennej, a następnie zwraca

tj. podobny do:

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

na przykład (za pomocą reflektora):

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

kompiluje się do:

.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
}

Zasadniczo deklaruje zmienną lokalną ( CS$1$0000), umieszcza wartość w zmiennej (wewnątrz obsługiwanego bloku), a następnie po wyjściu z bloku ładuje zmienną, a następnie zwraca ją. Odbłyśnik renderuje to jako:

private static int Test()
{
    int CS$1$0000;
    try
    {
        CS$1$0000 = SomeNumber();
    }
    finally
    {
        Foo();
    }
    return CS$1$0000;
}
Marc Gravell
źródło
10
Czy nie jest to dokładnie to, co powiedział ocdedio: w końcu jest wykonywany po obliczeniu wartości zwracanej i zanim naprawdę powróci z funczionu ???
mmmmmmmm,
„blok obsługiwany przez wyjątek” Myślę, że ten scenariusz nie ma nic wspólnego z wyjątkami i obsługą wyjątków. Chodzi o to, w jaki sposób .NET implementuje konstrukcję wreszcie ochrony zasobów .
g.pickardou
361

Ostatecznie instrukcja jest wykonywana, ale nie ma to wpływu na wartość zwracaną. Kolejność wykonania to:

  1. Kod przed wykonaniem instrukcji return
  2. Wyrażenie w instrukcji return jest oceniane
  3. w końcu blok jest wykonywany
  4. Wynik oceniany w kroku 2 jest zwracany

Oto krótki program pokazujący:

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";
        }
    }
}

Wypisuje „spróbuj” (bo to jest to, co zostało zwrócone), a następnie „w końcu”, ponieważ jest to nowa wartość x.

Oczywiście, jeśli zwracamy odwołanie do zmiennego obiektu (np. StringBuilder), wówczas wszelkie zmiany dokonane w obiekcie w bloku na końcu będą widoczne po powrocie - nie wpłynęło to na samą wartość zwracaną (która jest tylko odniesienie).

Jon Skeet
źródło
Chciałbym zapytać, czy jest jakaś opcja w studiu wizualnym, aby zobaczyć wygenerowany język pośredni (IL) dla napisanego kodu C # w momencie wykonania ....
Stan Enigma
Wyjątek od „jeśli zwracamy odwołanie do obiektu zmiennego (np. StringBuilder), wówczas wszelkie zmiany dokonane w obiekcie w bloku na końcu będą widoczne po powrocie” ma miejsce, gdy obiekt StringBuilder ma wartość null w bloku w końcu, w takim przypadku zwracany jest obiekt inny niż null.
Nick
4
@Nick: To nie jest zmiana obiektu - to jest zmiana zmiennej . Nie wpływa to na obiekt, do którego w ogóle odwoływała się poprzednia wartość zmiennej. Więc nie, to nie jest wyjątek.
Jon Skeet,
3
@Skeet Czy to oznacza „instrukcja return zwraca kopię”?
prabhakaran
4
@prabhakaran: Cóż, ocenia wyrażenie w punkcie returninstrukcji i ta wartość zostanie zwrócona. Wyrażenie nie jest oceniane, ponieważ kontrola opuszcza metodę.
Jon Skeet
19

Klauzula Wreszcie jest wykonywana po instrukcji return, ale przed faktycznym powrotem z funkcji. Myślę, że nie ma to nic wspólnego z bezpieczeństwem wątków. To nie jest hack - w końcu gwarantuje się, że zawsze będzie działał bez względu na to, co robisz w bloku try lub catch.

Otávio Décio
źródło
13

Dodając do odpowiedzi udzielonych przez Marca Gravella i Jona Skeeta, ważne jest, aby pamiętać, że obiekty i inne typy referencji zachowują się podobnie po powrocie, ale mają pewne różnice.

Zwracane „Co” podlega tej samej logice co typy proste:

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");
        }
    }
}

Zwracane odwołanie zostało już ocenione, zanim zmienna lokalna otrzyma nowe odwołanie w bloku na końcu.

Wykonanie jest zasadniczo:

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;
    }
}

Różnica polega na tym, że nadal można modyfikować zmienne typy przy użyciu właściwości / metod obiektu, co może spowodować nieoczekiwane zachowania, jeśli nie będziesz ostrożny.

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();
        }
    }
}

Drugą rzeczą do rozważenia w przypadku try-return-wreszcie jest to, że parametry przekazane „przez referencję” mogą być nadal modyfikowane po powrocie. Tylko zwracana wartość została oszacowana i jest przechowywana w zmiennej tymczasowej oczekującej na zwrot , pozostałe zmienne są nadal modyfikowane w normalny sposób. Kontrakt parametru wyjściowego może nawet nie zostać wypełniony, dopóki ostatecznie nie zablokuje w ten sposób.

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;
        }
    }
}

Jak każda inna konstrukcja przepływu, „try-return-Wreszcie” ma swoje miejsce i może pozwolić na czystszy wygląd kodu niż pisanie struktury, do której faktycznie się kompiluje. Ale należy go używać ostrożnie, aby uniknąć gotcha.

Arkaine55
źródło
4

Jeśli xjest zmienną lokalną, nie widzę sensu, ponieważ i tak xzostanie ona ustawiona na null, gdy metoda zostanie zakończona, a wartość zwracanej wartości nie jest null (ponieważ została ona umieszczona w rejestrze przed wywołaniem set xdo zera).

Widzę, że dzieje się tak tylko wtedy, gdy chcesz zagwarantować zmianę wartości pola po powrocie (i po ustaleniu wartości zwracanej).

casperOne
źródło
Chyba że zmienna lokalna jest również przechwytywana przez delegata :)
Jon Skeet
Następnie następuje zamknięcie, a obiekt nie może zostać wyrzucony, ponieważ nadal istnieje odwołanie.
rekurencyjne
Ale nadal nie rozumiem, dlaczego miałbyś używać lokalnego var w delegacie, chyba że zamierzałeś użyć jego wartości.
rekurencyjne