C # me permet de faire ce qui suit (exemple de MSDN):
using (Font font3 = new Font("Arial", 10.0f),
font4 = new Font("Arial", 10.0f))
{
// Use font3 and font4.
}
Que se passe-t-il si font4 = new Font
Lance? D'après ce que je comprends, font3 entraînera une fuite de ressources et ne sera pas éliminé.
using(... , ...)
devrait être complètement évité en faveur de l'utilisation imbriquée?Non.
Le compilateur générera un bloc finally
distinct pour chaque variable.
Le spec (§8.13) dit:
Lorsqu'une acquisition de ressource prend la forme d'une déclaration de variable locale, il est possible d'acquérir plusieurs ressources d'un type donné. Une instruction
using
du formulaireusing (ResourceType r1 = e1, r2 = e2, ..., rN = eN) statement
est précisément équivalent à une séquence d'instructions imbriquées:
using (ResourceType r1 = e1) using (ResourceType r2 = e2) ... using (ResourceType rN = eN) statement
[~ # ~] mise à jour [~ # ~] : J'ai utilisé cette question comme base pour un article qui peut être trouvé ici ; consultez-le pour plus de détails sur cette question. Merci pour la bonne question!
Bien que la réponse de Schabse soit bien sûr correcte et réponde à la question qui a été posée, il y a une variante importante de votre question que vous n'avez pas posée:
Que se passe-t-il si
font4 = new Font()
jette après la ressource non managée a été allouée par le constructeur mais avant le ctor retourne et remplitfont4
avec la référence?
Permettez-moi de clarifier cela un peu. Supposons que nous ayons:
public sealed class Foo : IDisposable
{
private int handle = 0;
private bool disposed = false;
public Foo()
{
Blah1();
int x = AllocateResource();
Blah2();
this.handle = x;
Blah3();
}
~Foo()
{
Dispose(false);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
private void Dispose(bool disposing)
{
if (!this.disposed)
{
if (this.handle != 0)
DeallocateResource(this.handle);
this.handle = 0;
this.disposed = true;
}
}
}
Maintenant nous avons
using(Foo foo = new Foo())
Whatever(foo);
C'est la même chose que
{
Foo foo = new Foo();
try
{
Whatever(foo);
}
finally
{
IDisposable d = foo as IDisposable;
if (d != null)
d.Dispose();
}
}
D'ACCORD. Supposons que Whatever
lance. Ensuite, le bloc finally
s'exécute et la ressource est désallouée. Aucun problème.
Supposons que Blah1()
lance. Ensuite, le lancer se produit avant que la ressource ne soit allouée. L'objet a été alloué mais le ctor ne revient jamais, donc foo
n'est jamais rempli. Nous n'avons jamais entré le try
donc nous n'entrons jamais non plus le finally
. La référence d'objet est devenue orpheline. Finalement, le GC le découvrira et le mettra dans la file d'attente du finaliseur. handle
est toujours nul, donc le finaliseur ne fait rien. Notez que le finaliseur doit être robuste face à un objet en cours de finalisation dont le constructeur n'a jamais terminé . Vous êtes tenu d'écrire des finaliseurs aussi solides. C'est une autre raison pour laquelle vous devriez laisser la rédaction des finaliseurs à des experts et ne pas essayer de le faire vous-même.
Supposons que Blah3()
lance. Le lancer se produit après l'allocation de la ressource. Mais encore une fois, foo
n'est jamais rempli, nous n'entrons jamais dans le finally
, et l'objet est nettoyé par le thread du finaliseur. Cette fois, la poignée est différente de zéro et le finaliseur la nettoie. Encore une fois, le finaliseur s'exécute sur un objet dont le constructeur n'a jamais réussi, mais le finaliseur fonctionne quand même. Evidemment ça doit parce que cette fois, ça avait du travail à faire.
Supposons maintenant que Blah2()
lance. Le lancer se produit après l'allocation de la ressource mais avanthandle
est rempli! Encore une fois, le finaliseur s'exécutera mais maintenant handle
est toujours nul et nous perdons la poignée!
Vous devez écrire extrêmement un code intelligent afin d'éviter que cette fuite ne se produise. Maintenant, dans le cas de votre ressource Font
, qui s'en soucie? Nous avons une fuite de poignée de police, gros problème. Mais si vous avez absolument besoin que chaque ressource non gérée soit nettoyée quel que soit le moment des exceptions alors vous avez un problème très difficile à résoudre.
Le CLR doit résoudre ce problème avec des verrous. Depuis C # 4, les verrous qui utilisent l'instruction lock
ont été implémentés comme ceci:
bool lockEntered = false;
object lockObject = whatever;
try
{
Monitor.Enter(lockObject, ref lockEntered);
lock body here
}
finally
{
if (lockEntered) Monitor.Exit(lockObject);
}
Enter
a été très soigneusement écrit afin que quelles que soient les exceptions levées, lockEntered
est défini sur true si et uniquement si le verrou a bien été pris. Si vous avez des exigences similaires, vous devez en fait écrire:
public Foo()
{
Blah1();
AllocateResource(ref handle);
Blah2();
Blah3();
}
et écrivez AllocateResource
astucieusement comme Monitor.Enter
afin que, quoi qu'il se passe à l'intérieur de AllocateResource
, le handle
est rempli si et seulement si il doit être désalloué.
Décrire les techniques pour le faire dépasse le cadre de cette réponse. Consultez un expert si vous avez cette exigence.
En complément de la réponse @SLaks, voici l'IL de votre code:
.method private hidebysig static
void Main (
string[] args
) cil managed
{
// Method begins at RVA 0x2050
// Code size 74 (0x4a)
.maxstack 2
.entrypoint
.locals init (
[0] class [System.Drawing]System.Drawing.Font font3,
[1] class [System.Drawing]System.Drawing.Font font4,
[2] bool CS$4$0000
)
IL_0000: nop
IL_0001: ldstr "Arial"
IL_0006: ldc.r4 10
IL_000b: newobj instance void [System.Drawing]System.Drawing.Font::.ctor(string, float32)
IL_0010: stloc.0
.try
{
IL_0011: ldstr "Arial"
IL_0016: ldc.r4 10
IL_001b: newobj instance void [System.Drawing]System.Drawing.Font::.ctor(string, float32)
IL_0020: stloc.1
.try
{
IL_0021: nop
IL_0022: nop
IL_0023: leave.s IL_0035
} // end .try
finally
{
IL_0025: ldloc.1
IL_0026: ldnull
IL_0027: ceq
IL_0029: stloc.2
IL_002a: ldloc.2
IL_002b: brtrue.s IL_0034
IL_002d: ldloc.1
IL_002e: callvirt instance void [mscorlib]System.IDisposable::Dispose()
IL_0033: nop
IL_0034: endfinally
} // end handler
IL_0035: nop
IL_0036: leave.s IL_0048
} // end .try
finally
{
IL_0038: ldloc.0
IL_0039: ldnull
IL_003a: ceq
IL_003c: stloc.2
IL_003d: ldloc.2
IL_003e: brtrue.s IL_0047
IL_0040: ldloc.0
IL_0041: callvirt instance void [mscorlib]System.IDisposable::Dispose()
IL_0046: nop
IL_0047: endfinally
} // end handler
IL_0048: nop
IL_0049: ret
} // end of method Program::Main
Notez les blocs try/finally imbriqués.
Ce code (basé sur l'exemple d'origine):
using System.Drawing;
public class Class1
{
public Class1()
{
using (Font font3 = new Font("Arial", 10.0f),
font4 = new Font("Arial", 10.0f))
{
// Use font3 and font4.
}
}
}
Il produit ce qui suit CIL (dans Visual Studio 201 , ciblant . NET 4.5.1):
.method public hidebysig specialname rtspecialname
instance void .ctor() cil managed
{
// Code size 82 (0x52)
.maxstack 2
.locals init ([0] class [System.Drawing]System.Drawing.Font font3,
[1] class [System.Drawing]System.Drawing.Font font4,
[2] bool CS$4$0000)
IL_0000: ldarg.0
IL_0001: call instance void [mscorlib]System.Object::.ctor()
IL_0006: nop
IL_0007: nop
IL_0008: ldstr "Arial"
IL_000d: ldc.r4 10.
IL_0012: newobj instance void [System.Drawing]System.Drawing.Font::.ctor(string,
float32)
IL_0017: stloc.0
.try
{
IL_0018: ldstr "Arial"
IL_001d: ldc.r4 10.
IL_0022: newobj instance void [System.Drawing]System.Drawing.Font::.ctor(string,
float32)
IL_0027: stloc.1
.try
{
IL_0028: nop
IL_0029: nop
IL_002a: leave.s IL_003c
} // end .try
finally
{
IL_002c: ldloc.1
IL_002d: ldnull
IL_002e: ceq
IL_0030: stloc.2
IL_0031: ldloc.2
IL_0032: brtrue.s IL_003b
IL_0034: ldloc.1
IL_0035: callvirt instance void [mscorlib]System.IDisposable::Dispose()
IL_003a: nop
IL_003b: endfinally
} // end handler
IL_003c: nop
IL_003d: leave.s IL_004f
} // end .try
finally
{
IL_003f: ldloc.0
IL_0040: ldnull
IL_0041: ceq
IL_0043: stloc.2
IL_0044: ldloc.2
IL_0045: brtrue.s IL_004e
IL_0047: ldloc.0
IL_0048: callvirt instance void [mscorlib]System.IDisposable::Dispose()
IL_004d: nop
IL_004e: endfinally
} // end handler
IL_004f: nop
IL_0050: nop
IL_0051: ret
} // end of method Class1::.ctor
Comme vous pouvez le voir, le bloc try {}
Ne démarre qu'après la première allocation, qui a lieu à IL_0012
. À première vue, cela apparaît pour allouer le premier élément dans le code non protégé. Cependant, notez que le résultat est stocké à l'emplacement 0. Si la deuxième allocation échoue, le bloc externe finally {}
S'exécute, et ceci récupère l'objet depuis l'emplacement 0, c'est-à-dire la première allocation de font3
, et appelle sa méthode Dispose()
.
Fait intéressant, la décompilation de cet assembly avec dotPeek produit la source reconstituée suivante:
using System.Drawing;
public class Class1
{
public Class1()
{
using (new Font("Arial", 10f))
{
using (new Font("Arial", 10f))
;
}
}
}
Le code décompilé confirme que tout est correct et que le using
est essentiellement développé en using
s imbriqués. Le code CIL est un peu déroutant à regarder, et j'ai dû le regarder pendant quelques minutes avant de bien comprendre ce qui se passait, donc je ne suis pas surpris que certains "vieux contes d'épouses" aient commencé à germer cette. Cependant, le code généré est la vérité inattaquable.
Voici un exemple de code pour prouver la réponse de @SLaks:
void Main()
{
try
{
using (TestUsing t1 = new TestUsing("t1"), t2 = new TestUsing("t2"))
{
}
}
catch(Exception ex)
{
Console.WriteLine("catch");
}
finally
{
Console.WriteLine("done");
}
/* outputs
Construct: t1
Construct: t2
Dispose: t1
catch
done
*/
}
public class TestUsing : IDisposable
{
public string Name {get; set;}
public TestUsing(string name)
{
Name = name;
Console.WriteLine("Construct: " + Name);
if (Name == "t2") throw new Exception();
}
public void Dispose()
{
Console.WriteLine("Dispose: " + Name);
}
}