Que se passe-t-il vraiment lors d'un essai {return x; } enfin {x = null; } déclaration?

259

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

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

Je veux dire, la finallyclause s'exécute-t-elle vraiment après la returndéclaration? Dans quelle mesure ce code est-il dangereux pour les threads? Pouvez-vous penser à un piratage supplémentaire qui pourrait être fait avec ce try-finallypiratage?

Dmitri Nesteruk
la source

Réponses:

235

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

c'est-à-dire similaire à:

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

par exemple (à l'aide d'un réflecteur):

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

se 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 fondamentalement une variable locale ( CS$1$0000), place la valeur dans la variable (à l'intérieur du bloc manipulé), puis après avoir quitté le bloc charge la variable, puis la retourne. Reflector rend ceci comme:

private static int Test()
{
    int CS$1$0000;
    try
    {
        CS$1$0000 = SomeNumber();
    }
    finally
    {
        Foo();
    }
    return CS$1$0000;
}
Marc Gravell
la source
10
N'est-ce pas exactement ce qu'a dit ocdedio: le finalement est exécuté après le calcul de la valeur de retour et avant de vraiment revenir de la fonction ???
mmmmmmmm
"bloc géré par exception" Je pense que ce scénario n'a rien à voir avec les exceptions et la gestion des exceptions. Il s'agit de la façon dont .NET implémente la construction finalement de garde de ressources .
g.pickardou
361

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

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

Voici un petit programme 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 imprime "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 renvoyons 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 juste un référence).

Jon Skeet
la source
Je voudrais demander, Y a-t-il une option dans Visual Studio pour voir le langage intermédiaire généré (IL) pour le code C # écrit au moment de l'exécution ....
Enigma State
Une exception à "si nous renvoyons une référence à un objet mutable (par exemple un StringBuilder), alors toutes les modifications apportées à l'objet dans le bloc finally seront visibles au retour", si l'objet StringBuilder est défini sur null dans le bloc finally, auquel cas un objet non nul est retourné.
Nick
4
@ Nick: Ce n'est pas un changement à l' objet - c'est un changement à la variable . Cela n'affecte pas du tout l'objet auquel la valeur précédente de la variable faisait référence. Donc non, ce n'est pas une exception.
Jon Skeet
3
@Skeet Cela signifie-t-il que "l'instruction de retour renvoie une copie"?
prabhakaran
4
@prabhakaran: Eh bien, il évalue l'expression au point de l' returninstruction, et cette valeur sera retournée. L'expression n'est pas évaluée car le contrôle quitte la méthode.
Jon Skeet
19

La clause finally s'exécute après l'instruction return mais avant de revenir réellement de la fonction. Cela a peu à voir avec la sécurité des fils, je pense. Ce n'est pas un hack - le enfin est garanti pour toujours fonctionner, peu importe ce que vous faites dans votre bloc try ou votre bloc catch.

Otávio Décio
la source
13

En plus des réponses données par Marc Gravell et Jon Skeet, il est important de noter que les objets et autres types de référence se comportent de la même manière lorsqu'ils sont retournés, mais présentent quelques différences.

Le "Quoi" qui est retourné 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 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 en utilisant les 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 deuxième chose à considérer à propos de try-return-enfin est que les paramètres passés "par référence" peuvent toujours être modifiés après le retour. Seule la valeur de retour a été évaluée et est stockée dans une variable temporaire en attente de retour, toutes les autres variables sont toujours modifiées de la manière normale. Le contrat d'un paramètre de sortie peut même rester inachevé jusqu'à ce qu'il soit finalement bloqué de cette façon.

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 propre que d'écrire la structure dans laquelle il se compile. Mais il doit être utilisé avec précaution pour éviter les pièges.

Arkaine55
la source
4

Si xest une variable locale, je ne vois pas le point, car il xsera de toute façon effectivement réglé sur null lorsque la méthode sera quittée et que la valeur de la valeur de retour ne sera pas nulle (puisqu'elle a été placée dans le registre avant l'appel à set xà null).

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

casperOne
la source
Sauf si la variable locale est également capturée par un délégué :)
Jon Skeet
Ensuite, il y a une fermeture, et l'objet ne peut pas être récupéré, car il y a toujours une référence.
récursif le
Mais je ne vois toujours pas pourquoi vous utiliseriez un var local dans un délégué à moins que vous n'ayez l'intention d'utiliser sa valeur.
récursif