AKTUALIZACJA : Użyłem tego pytania jako podstawy do artykułu, który można znaleźć tutaj ; zobacz to, aby uzyskać dodatkowe omówienie tego problemu. Dzięki za dobre pytanie!
Chociaż odpowiedź Schabse jest oczywiście poprawna i odpowiada na zadane pytanie, istnieje ważny wariant twojego pytania, którego nie zadałeś:
Co się stanie, jeśli font4 = new Font()
wyrzuca po tym, jak niezarządzany zasób został przydzielony przez konstruktora, ale zanim ctor zwróci i wypełni font4
odwołanie?
Pozwólcie, że wyjaśnię to trochę. Załóżmy, że mamy:
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;
}
}
}
Teraz mamy
using(Foo foo = new Foo())
Whatever(foo);
To jest to samo co
{
Foo foo = new Foo();
try
{
Whatever(foo);
}
finally
{
IDisposable d = foo as IDisposable;
if (d != null)
d.Dispose();
}
}
DOBRZE. Załóżmy, że Whatever
rzuca. Następnie finally
blok jest uruchamiany i zasób jest zwalniany. Nie ma problemu.
Załóżmy, że Blah1()
rzuca. Następnie rzut ma miejsce, zanim zasób zostanie przydzielony. Obiekt został przydzielony, ale ctor nigdy nie wraca, więc foo
nigdy nie jest wypełniany. Nigdy nie try
weszliśmy do pola, więc nigdy nie wchodzimy do finally
żadnego. Odniesienie do obiektu zostało osierocone. W końcu GC to wykryje i umieści to w kolejce finalizatora. handle
nadal wynosi zero, więc finalizator nic nie robi. Zwróć uwagę, że finalizator musi być niezawodny w obliczu finalizowanego obiektu, którego konstruktor nigdy nie został ukończony . Jesteś zobowiązany napisać finalizatory że są to silne. To kolejny powód, dla którego powinieneś pozostawić pisanie finalizatorów ekspertom i nie próbować robić tego samodzielnie.
Załóżmy, że Blah3()
rzuca. Rzut następuje po przydzieleniu zasobu. Ale znowu, foo
nigdy nie jest wypełniony, nigdy nie wchodzimy do finally
, a obiekt jest czyszczony przez wątek finalizatora. Tym razem uchwyt jest niezerowy, a finalizator czyści go. Ponownie finalizator działa na obiekcie, którego konstruktor nigdy się nie powiódł, ale finalizator i tak działa. Oczywiście musiało, bo tym razem miało do wykonania pracę.
Teraz załóżmy, że Blah2()
rzuca. Rzut następuje po przydzieleniu zasobu, ale przed handle
wypełnieniem! Ponownie finalizator będzie działał, ale teraz handle
nadal wynosi zero i wyciekamy uchwyt!
Musisz napisać niezwykle sprytny kod, aby zapobiec wystąpieniu tego wycieku. Teraz, w przypadku twojego Font
zasobu, kogo to obchodzi? Wyciekamy uchwyt czcionki, wielka sprawa. Ale jeśli absolutnie stanowczo wymagasz, aby każdy niezarządzany zasób został wyczyszczony, bez względu na czas wyjątków , masz bardzo trudny problem.
CLR musi rozwiązać ten problem za pomocą zamków. Od czasu C # 4 blokady, które używają lock
instrukcji, zostały zaimplementowane w następujący sposób:
bool lockEntered = false;
object lockObject = whatever;
try
{
Monitor.Enter(lockObject, ref lockEntered);
lock body here
}
finally
{
if (lockEntered) Monitor.Exit(lockObject);
}
Enter
został bardzo starannie napisany, aby bez względu na to, jakie wyjątki zostały wyrzucone , lockEntered
jest ustawiony na wartość true wtedy i tylko wtedy, gdy blokada została faktycznie podjęta. Jeśli masz podobne wymagania, to tak naprawdę musisz napisać:
public Foo()
{
Blah1();
AllocateResource(ref handle);
Blah2();
Blah3();
}
i AllocateResource
sprytnie pisz Monitor.Enter
tak, że bez względu na to, co dzieje się w środku AllocateResource
, pole handle
jest wypełnione wtedy i tylko wtedy, gdy trzeba je cofnąć.
Opis technik służących do tego wykracza poza zakres tej odpowiedzi. Skonsultuj się z ekspertem, jeśli masz takie wymaganie.