web-dev-qa-db-fra.com

Qu'est-ce qui se passe réellement dans un essai {return x; } finally {x = null; } déclaration?

J'ai vu cette astuce dans une autre question et je me demandais si quelqu'un pourrait m'expliquer comment cela fonctionne?

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

Je veux dire, la clause finally exécute-t-elle vraiment après l'instruction return? À quel point ce code est-il dangereux pour les threads? Pouvez-vous penser à un hackery supplémentaire qui pourrait être fait avec w.r.t. cette try-finally pirater?

246
Dmitri Nesteruk

Non - au niveau IL, vous ne pouvez pas revenir de l'intérieur d'un bloc géré par une exception. Il stocke essentiellement dans une variable et retourne ensuite

c'est-à-dire semblable à:

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

par exemple (avec réflecteur):

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

compile pour:

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

Cela déclare essentiellement une variable locale (CS$1$0000), place la valeur dans la variable (à l'intérieur du bloc géré), puis après la sortie du bloc charge la variable, puis la renvoie. Le réflecteur traduit ceci en:

private static int Test()
{
    int CS$1$0000;
    try
    {
        CS$1$0000 = SomeNumber();
    }
    finally
    {
        Foo();
    }
    return CS$1$0000;
}
229
Marc Gravell

L'instruction finally est exécutée, mais la valeur de retour n'est pas affectée. L'ordre d'exécution est:

  1. Code avant que l'instruction return soit exécutée
  2. L'expression dans l'instruction de retour est évaluée
  3. enfin le bloc est exécuté
  4. Le résultat évalué à l'étape 2 est renvoyé.

Voici un programme court pour démontrer:

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

Ceci affiche "try" (parce que c'est ce qui est retourné) puis "finalement" parce que c'est la nouvelle valeur de x.

Bien sûr, si nous retournons une référence à un objet mutable (par exemple un StringBuilder), toutes les modifications apportées à l'objet dans le bloc finally seront visibles au retour - cela n'a pas affecté la valeur de retour elle-même (qui est simplement référence).

345
Jon Skeet

La clause finally s'exécute après l'instruction return mais avant le retour de la fonction. Cela a peu à voir avec la sécurité du fil, je pense. Ce n’est pas un hack, c’est enfin garanti de pouvoir toujours courir, peu importe ce que vous ferez dans votre bloc try ou catch block.

19
Otávio Décio

Pour compléter les réponses données par Marc Gravell et Jon Skeet, il est important de noter que les objets et les autres types de référence se comportent de la même manière quand ils sont renvoyés, à quelques différences près.

Le "Quoi" renvoyé suit la même logique que les types simples:

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

La référence renvoyée a déjà été évaluée avant que la variable locale ne se voit attribuer une nouvelle référence dans le bloc finally.

L'exécution est essentiellement:

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

La différence est qu'il serait toujours possible de modifier les types mutables à l'aide des propriétés/méthodes de l'objet, ce qui peut entraîner des comportements inattendus si vous ne faites pas attention.

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

Une autre chose à considérer à propos de try-return-finally est que les paramètres passés "par référence" peuvent toujours être modifiés après le retour. Seule la valeur renvoyée a été évaluée et est stockée dans une variable temporaire en attente de renvoi. Toutes les autres variables sont toujours modifiées normalement. Le contrat d’un paramètre out peut même ne pas être rempli jusqu’au blocage final de cette manière.

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

Comme toute autre construction de flux, "try-return-finally" a sa place et peut permettre un code plus net que l'écriture de la structure avec laquelle il compile . Mais il faut l'utiliser avec précaution pour éviter les tracas.

13
Arkaine55

Si x est une variable locale, je ne vois pas le problème, car x sera effectivement mis à null de toute façon lorsque la méthode sera sortie et que la valeur de la valeur de retour n'est pas null ( car il a été placé dans le registre avant l'appel pour définir x sur null).

Je ne peux voir cela se produire que si vous voulez garantir le changement de la valeur d'un champ lors du retour (et après la détermination de la valeur de retour).

4
casperOne