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 font4odwoł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 Whateverrzuca. Następnie finallyblok 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 foonigdy nie jest wypełniany. Nigdy nie tryweszliś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. handlenadal 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, foonigdy 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 handlenadal wynosi zero i wyciekamy uchwyt!
Musisz napisać niezwykle sprytny kod, aby zapobiec wystąpieniu tego wycieku. Teraz, w przypadku twojego Fontzasobu, 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ą lockinstrukcji, 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);
}
Enterzostał bardzo starannie napisany, aby bez względu na to, jakie wyjątki zostały wyrzucone , lockEnteredjest 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 AllocateResourcesprytnie pisz Monitor.Entertak, że bez względu na to, co dzieje się w środku AllocateResource, pole handlejest 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.