Obiekty nigdy nie wychodzą poza zakres w języku C #, podobnie jak w języku C ++. Zajmuje się nimi Garbage Collector automatycznie, gdy nie są już używane. Jest to bardziej skomplikowane podejście niż C ++, w którym zakres zmiennej jest całkowicie deterministyczny. Moduł śmieciowy CLR aktywnie przechodzi przez wszystkie utworzone obiekty i sprawdza, czy są one używane.
Obiekt może wyjść „poza zakres” w jednej funkcji, ale jeśli jego wartość zostanie zwrócona, GC sprawdzi, czy funkcja wywołująca zachowuje wartość zwracaną.
Ustawienie odwołań do obiektu na null
jest konieczne, ponieważ czyszczenie pamięci działa, sprawdzając, do których obiektów odwołują się inne obiekty.
W praktyce nie musisz się martwić zniszczeniem, po prostu działa i jest świetne :)
Dispose
należy wywoływać we wszystkich obiektach, które implementują IDisposable
po zakończeniu pracy z nimi. Zwykle używałbyś using
bloku z takimi obiektami:
using (var ms = new MemoryStream()) {
//...
}
EDYTOWAĆ W zakresie zmiennym. Craig zapytał, czy zakres zmiennej ma jakikolwiek wpływ na czas życia obiektu. Aby poprawnie wyjaśnić ten aspekt CLR, muszę wyjaśnić kilka pojęć z C ++ i C #.
Rzeczywisty zakres zmiennej
W obu językach zmienna może być używana tylko w tym samym zakresie, w jakim została zdefiniowana - klasa, funkcja lub blok instrukcji ujęty w nawiasy klamrowe. Subtelna różnica polega jednak na tym, że w języku C # zmienne nie mogą być ponownie zdefiniowane w zagnieżdżonym bloku.
W C ++ jest to całkowicie legalne:
int iVal = 8;
//iVal == 8
if (iVal == 8){
int iVal = 5;
//iVal == 5
}
//iVal == 8
W języku C # pojawia się błąd kompilatora:
int iVal = 8;
if(iVal == 8) {
int iVal = 5; //error CS0136: A local variable named 'iVal' cannot be declared in this scope because it would give a different meaning to 'iVal', which is already used in a 'parent or current' scope to denote something else
}
Ma to sens, jeśli spojrzysz na wygenerowany MSIL - wszystkie zmienne używane przez funkcję są zdefiniowane na początku funkcji. Spójrz na tę funkcję:
public static void Scope() {
int iVal = 8;
if(iVal == 8) {
int iVal2 = 5;
}
}
Poniżej znajduje się wygenerowana IL. Zauważ, że iVal2, który jest zdefiniowany wewnątrz bloku if, jest faktycznie zdefiniowany na poziomie funkcji. W efekcie oznacza to, że C # ma zasięg klasy i funkcji tylko w zakresie zmiennego czasu życia.
.method public hidebysig static void Scope() cil managed
{
// Code size 19 (0x13)
.maxstack 2
.locals init ([0] int32 iVal,
[1] int32 iVal2,
[2] bool CS$4$0000)
//Function IL - omitted
} // end of method Test2::Scope
Zakres C ++ i czas życia obiektu
Ilekroć zmienna C ++ przydzielona na stosie wykracza poza zakres, zostaje zniszczona. Pamiętaj, że w C ++ możesz tworzyć obiekty na stosie lub na stercie. Kiedy tworzysz je na stosie, gdy wykonanie opuści zakres, są one wyskakiwane ze stosu i niszczone.
if (true) {
MyClass stackObj; //created on the stack
MyClass heapObj = new MyClass(); //created on the heap
obj.doSomething();
} //<-- stackObj is destroyed
//heapObj still lives
Kiedy obiekty C ++ są tworzone na stercie, muszą zostać jawnie zniszczone, w przeciwnym razie jest to wyciek pamięci. Jednak nie ma takiego problemu ze zmiennymi stosu.
C # Żywotność obiektu
W CLR obiekty (tj. Typy referencyjne) są zawsze tworzone na zarządzanej stercie. Jest to dodatkowo wzmocnione przez składnię tworzenia obiektów. Rozważ ten fragment kodu.
MyClass stackObj;
W C ++ spowoduje to utworzenie instancji na MyClass
stosie i wywołanie jej domyślnego konstruktora. W języku C # tworzyłoby odwołanie do klasy, MyClass
które niczego nie wskazuje. Jedynym sposobem utworzenia instancji klasy jest użycie new
operatora:
MyClass stackObj = new MyClass();
W pewien sposób obiekty C # są bardzo podobne do obiektów tworzonych przy użyciu new
składni w C ++ - są tworzone na stercie, ale w przeciwieństwie do obiektów C ++, są zarządzane przez środowisko wykonawcze, więc nie musisz się martwić o ich zniszczenie.
Ponieważ obiekty są zawsze na stercie, fakt, że odwołania do obiektów (tj. Wskaźniki) wykraczają poza zakres, staje się dyskusyjny. Przy ustalaniu, czy obiekt ma zostać zebrany, bierze się więcej czynników niż po prostu obecność odniesień do obiektu.
C # Odwołania do obiektów
Jon Skeet porównał odwołania do obiektów w Javie do kawałków łańcucha przymocowanych do balonu, którym jest obiekt. Ta sama analogia dotyczy odwołań do obiektów C #. Wskazują po prostu lokalizację stosu zawierającego obiekt. Zatem ustawienie wartości null nie ma bezpośredniego wpływu na czas życia obiektu, balon nadal istnieje, dopóki GC go nie „wyskoczy”.
Kontynuując analogię balonu, wydaje się logiczne, że gdy balon nie będzie do niego przymocowany, może zostać zniszczony. W rzeczywistości dokładnie tak działają obiekty zliczane referencjami w językach niezarządzanych. Tyle że to podejście nie działa zbyt dobrze w przypadku odwołań cyklicznych. Wyobraź sobie dwa balony, które są połączone sznurkiem, ale żaden balon nie ma sznurka do niczego innego. Zgodnie z prostymi regułami liczenia referencji oba nadal istnieją, mimo że cała grupa balonów jest „osierocona”.
Obiekty .NET przypominają balony helowe pod dachem. Gdy dach się otworzy (biegnie GC) - nieużywane balony odpływają, nawet jeśli grupy balonów są ze sobą powiązane.
.NET GC wykorzystuje kombinację generacyjnego GC oraz mark i sweep. Podejście generacyjne obejmuje środowisko wykonawcze faworyzujące obiekty, które zostały ostatnio przydzielone, ponieważ są one bardziej nieużywane, a oznaczanie i przeglądanie obejmuje środowisko wykonawcze przechodzące przez cały wykres obiektów i sprawdzanie, czy istnieją grupy obiektów, które nie są używane. To odpowiednio rozwiązuje problem zależności cyklicznej.
Ponadto .NET GC działa na innym wątku (tak zwanym wątku finalizatora), ponieważ ma sporo do zrobienia, a wykonanie tego w głównym wątku zakłóciłoby działanie programu.