Zagadnienia egzaminu 70-483 opisane w tej notatce:

  • Dziedziczenie z klasy bazowej.
  • Wywoływanie konstruktorów klasy potomnej lub tej samej klasy.
  • Tworzenie i implementacja interfejsów.
  • Użycie standardowych interfejsów jak IComparableIEquatable i IEnumerable.
  • Implementacja interfejsu IDisposable i destruktor.
  • Użycie wyrażenia using.

Dziedziczenie z klasy bazowej

Poniższy kod definiuje klasę Person:

publicclassPerson
{
publicstring FirstName { get; set; }
publicstring LastName { get; set; }
publicPerson(string firstName, string lastName)
{
// Validate the first and last names.
if ((firstName == null) || (firstName.Length < 1))
thrownew ArgumentOutOfRangeException(
"firstName", firstName,
"FirstName must not be null or blank.");
if ((lastName == null) || (lastName.Length < 1))
thrownew ArgumentOutOfRangeException(
"lastName", lastName,
"LastName must not be null or blank.");
// Save the first and last names.
FirstName = firstName;
LastName = lastName;
}
}

Klasa Person posiada dwie właściwości typu tekstowego: FirstName i LastName. jej konstruktor pobiera imię i nazwisko jako parametry, wykonuje ich walidację i zapisuje jako właściwości FirstName i LastName
Załóżmy, że chcemy utworzyć klasę Employee posiadającą właściwości FirstNameLastName i DepartmentName. Możemy zbudować tą klasę w całości lecz potrzebuje ona tych samych właściwości co klasa Person i może potrzebować również takiej samej walidacji więc budowanie jej w całości byłoby powtórzeniem pracy. Lepszym rozwiązaniem jest dziedziczenie klasy Employee z klasy Person w taki sposób aby odziedziczyła pola, właściwości, metody i zdarzenia. Logicznie pracownik jest pewnym rodzajem osoby więc sensowne jest stwierdzenie, że klasa Employee jest rodzajem klasy Person
Dziedziczenie klasy z innej jest zapisywane w postaci nazwy klasy, dwukropka i nazwy klasy z której dziedziczy. Poniższy kod przedstawia dziedziczenie klasy Employee z klasy Person:

publicclassEmployee : Person
{
publicstring DepartmentName { get; set; }
}

Klasa Employee dziedziczy z klasy Person wszystkie pola,właściwości, metody i zdarzenia. W ciele klasy dodana jest nowa właściwość DepartmentName
Pomimo dziedziczenia większości kodu ciała klasy rodzica, potomek nie dziedziczy konstruktorów. 
W tym momencie utworzenie obiektu klasy Employee bez inicjalizacji właściwości wygląda tak:

Employee employee = newEmployee();

Ponieważ kod nie inicjuje właściwości to właściwości FirstName i LastName mają wartość null co jest sprzeczne z walidacją w konstruktorze klasy Person. Rozwiązaniem tego problemu jest wywołanie konstruktora rodzica przez konstruktor klasy potomka.

Wywołanie konstruktora klasy rodzica

Aby być pewnym, że konstruktor klasy Person jest wywoływany i sprawdza poprawność imienia i nazwiska, należy nadać klasie potomnej Employee konstruktor zawierający parametry klasy rodzica oraz dodać po dwukropku słowo kluczowe base które sprawi, że zostanie wywołany odpowiedni konstruktor klasy rodzica. Przykład klasy Employee wywołującej konstruktor klasy bazowej Person:

publicclassEmployee : Person
{
publicstring DepartmentName { get; set; }
publicEmployee(string firstName, string lastName,
string departmentName)

: base(firstName, lastName)
{
// Validate the department name.
if ((departmentName == null) || (departmentName.Length < 1))
thrownew ArgumentOutOfRangeException(
"departmentName", departmentName,
"DepartmentName must not be null or blank.");
// Save the department name.
DepartmentName = departmentName;
}
}

Konstruktor klasy bazowej jest wywoływany przed ciałem konstruktora klasy potomka. Jeżeli kasa bazowa ma wiele konstruktorów to można użyć każdego z nich. program decyduje którego konstruktora użyć na podstawie parametrów w nawiasie po słowie kluczowym base
Jeżeli obie klasy bazowa i potomka posiadają konstruktory to konstruktor potomka musi wywoływać jeden z konstruktorów klasy rodzica co oznacza, że użycie klauzuli ze słowem kluczowym base jest wtedy wymagane. W przeciwnym wypadku VisualStudio wygeneruje błąd: “PersonHierarchy.Person Does Not Contain a Constructor That Takes 0 Arguments”
Jeżeli klasa potomka nie posiada konstruktora to nie musi wywoływać konstruktora klasy bazowej tzn, że taka deklaracja jest poprawna:

publicclassEmployee : Person
{
publicstring DepartmentName { get; set; }
}

Wywołanie konstruktora tej samej klasy

bardzo często klasy posiadają wiele konstruktorów wykonujących różne rodzaje inicjalizacji w zależności od wprowadzonych parametrów wejściowych. Czasem wiele konstruktorów musi wykonywać te same zadania. 
Przykładowo klasa Person posiadająca właściwości FirstName i LastName może być inicjowana poprzez podanie imienia lub imienia i nazwiska:

classPerson
{
publicstring FirstName { get; set; }
publicstring LastName { get; set; }
// Constructor with first name.
publicPerson(string firstName)
{
FirstName = firstName;
}
// Constructor with first and last name.
publicPerson(string firstName, string lastName)
{
FirstName = firstName;
LastName = lastName;
}
}

Drugi konstruktor klasy wykonuje tę samą czynność co pierwszy konstruktor czyli inicjuje właściwość FirstName. W tym przykładzie to nie jest duży problem ale w bardziej skomplikowanym przykładzie mogłoby być to wiele powtarzającego się kodu. 
Aby temu zapobiec można pozwolić konstruktorowi wywoływać inny konstruktor. Wykonuje się to tak samo jak wywoływanie konstruktora bazowego tylko, że przy użyciu słowa kluczowego this. Przykład konstruktora wywołującego inny konstruktor:

// Constructor with first and last name.
publicPerson(string firstName, string lastName)
: this(firstName)
{
LastName = lastName;
}

Konstruktor wskazany słowem kluczowym this jest wykonywany przed ciałem konstruktora który go wywołuje.

Interfejsy

Kiedy wiemy już jak dziedziczyć pomiędzy klasami to możemy budować diagramy używające strzałek pokazujących relacje pomiędzy klasami. Język C# pozwala na posiadanie jednej klasy rodzica więc rezultatem takiego diagramu jest drzewiasta hierarchia. 
Hierarchie klasowe są wystarczające dla wielu różnych problemów modelowania, ale od czasu do czasu byłoby wygodne, aby umożliwić klasom dziedziczenie z wielu klas bazowych. Przykładowo pisząc program zarzadzający studentami i pracownikami uniwersytetu. Dwie ważne klasy to klasa Student reprezentująca osobę studiującą i klasa Facultyreprezentująca osobę nauczającą. Problem pojawia się kiedy chcemy dodać klasę TeachingAssistant reprezentującą asystenta wykładowcy który jest jednocześnie studentem. Idealnym rozwiązaniem byłoby użycie wielokrotnego dziedziczenia tak aby klasa mogła dziedziczyc jednocześnie z klas Student i Faculty. Niestety w języku C# jest to niemożliwe. 
Zamiast użycia wielokrotnego dziedziczenia w języku C# można użyć interfejsów aby zasymulować takie dziedziczenie. 
Interfejs wymaga klasy aby zapewnić pewne funkcje tak samo jak klasa bazowa lecz w przeciwieństwie do klasy bazowej interfejs nie posiada implementacji. Ponieważ używanie interfejsu jest podobne do dziedziczenia bez implementacji to nazywane jest dziedziczeniem interfejsu. Każda klasa może dziedziczyć tylko z jednej klasy bazowej ale może implementować dowolną liczbę interfejsów.

Definiowanie interfejsów

Interfejs tak samo jak klasa posiada właściwości, metody i zdarzenia ale nie posiada kodu który je implementuje. Jest formą kontraktu określającego cechy jakie inna klasa powinna implementować. 
Jeżeli klasa implementuje interfejs to musi implementować cechy zdefiniowane w interfejsie. Mówi to programowi, że ta klasa ma te cechy więc kod może je wywoływać. 
Wprowadza to rodzaj polimorfizmu podobny do tego w jaki klasy mogą być traktowane przez program jako obiekty innych klas kompatybilnych. Przykładowo klasa Employee dziedziczy z klasy Person i implementuje interfejs ICloneable. Program może traktować obiekt klasy Employee jako EmployeePerson lub ICloneable
Przykład interfejsu IStudent:

publicinterfaceIStudent
{
// The student's list of current courses.
List<string> Courses { get; set; }
// Print the student's current grades.
voidPrintGrades();
}

Wg konwencji nazwy interfejsów rozpoczynamy od dużej litery I np IStudent.
Klasa Student implementująca interfejs IStudent i dziedzicząca z klasy Person:

publicclassStudent : Person, IStudent
{
// Implement IStudent.Courses.
// The student's list of current courses.
public List<string> Courses { get; set; }
// Implement IStudent.PrintGrades.
// Print the student's current grades.
publicvoid PrintGrades()
{
// Do whatever is necessary...
}
}

Klasa TeachingAssistant dziedzicząca z klasy Faculty i implementująca interfejs IStudent:

publicclassTeachingAssistant : Faculty, IStudent
{
// Implement IStudent.Courses.
// The student's list of current courses.
public List<string> Courses { get; set; }
// Implement IStudent.PrintGrades.
// Print the student's current grades.
publicvoid PrintGrades()
{
// Do whatever is necessary...
}
}

Implementacja interfejsów

Czasami implementacja wszystkich metod zdefiniowanych przez interfejs może być dużą ilością pracy. Na szczęście VisualStudio posiada narzędzia ułatwiające implementację interfejsów. Deklarując interfejs do implementacji w VisualStudio klikamy prawym przyciskiem myszy na nazwie interfejsu i z menu kontekstowego wybieramy opcję Implement Interfacelub Implement Interface Explicitly. Przykład kodu wygenerowanego przez VisualStudio:

// Explicit implementation.
publicclassTeachingAssistant : Faculty, IStudent
{
List<string> IStudent.Courses
{
get
{
thrownew NotImplementedException();
}
set
{
thrownew NotImplementedException();
}
}
void IStudent.PrintGrades()
{
thrownew NotImplementedException();
}
}

Opcja Explicitly powoduje, że przed każdą metodą i właściwością dodane jest cześć z której jest implementowana czyli IStudent.. Wygenerowane części kodu wyrzucają wyjątek NotImplementedException kiedy zostaną wywołane. Tę część kodu należy zamienić na odpowiedni kod logiki aplikacji. 
Oprócz różnic w składni istnieje funkcjonalna różnica pomiędzy niejawną i jawną implementacją interfejsu. Jeśli klasa implementuje interfejs jawnie to program nie może odnieść się do członków interfejsu za pośrednictwem instancji klasy. Zamiast tego musi użyć instancji interfejsu. 
Przykładowo klasa TeachingAssistant implementuje jawnie interfejs IStudent:

TeachingAssistant ta = new TeachingAssistant();
// The following causes a design time error.
ta.PrintGrades();
// The following code works.
IStudent student = ta;
student.PrintGrades();

Jeżeli klasa implementuje interfejs niejawnie to program ma dostęp do członków interfejsu z instancji klasy i z instancji interfejsu.

Delegowanie interfejsów

Obie klasy Student i TeachingAssistant implementuja interfejs IStudent, więc obie zawierają kod zapewniający cechy interfejsu. Kod ten jest zduplikowany. 
Aby uniknąć duplikowania kodu w klasie TeachingAssistant można użyć delegacji do klasy Student w której ten kod jest już zaimplementowany. Delegowanie odbywa się poprzez umieszczenie obiektu klasy Student w ciele klasy TeachingAssistant
Ilekroć klasa TeachingAssistant będzie musiała wykonać pewną czynność wyspecyfikowaną w interfejsie IStudent użyje obiektu klasy Student np:

// Delegate IStudent to a Student object.
publicclassTeachingAssistant : Faculty, IStudent
{
// A Student object to handle IStudent.
private Student MyStudent = new Student();
public List<string> Courses
{
get
{
return MyStudent.Courses;
}
set
{
MyStudent.Courses = value;
}
}
publicvoid PrintGrades()
{
MyStudent.PrintGrades();
}
}

Implementacja popularnych interfejsów

.NET Framework zawiera wiele interfejsów które wspomagają pracę klas. Poniżej opisane zostaną najważniejsze z nich.

IComparable

Interfejs IComparable wprowadza metodę CompareTo która umożliwia programowi porównanie dwóch instancji klas i zdecydowanie który z obiektów jest pierwszy w kolejności sortowania. Przykładowo klasa Car służy do przechowywania modeli samochodów i chcemy aby obiekty tej klasy były sortowane wg nazwy. Możemy stworzyć klasę Car tak aby implementowała interfejs IComparable i użyć metody Array.Sort do sortowania tablicy z obiektami klasy Car
Interfejs IComparable posiada dwie wersje: zwykłą i generyczną. Wersja zwykła interfejsu istnieje dla zgodności z poprzednimi wersjami .NET rameworka. Zaleca się używanie wersji generycznej. 
Jeżeli użyjemy zwykłej wersji to metoda CompareTo pobierze dwa niespecyfikowane obiekty jako parametry po czym w kodzie trzeba je skonwertować na obiekty klasy Car przed porównaniem ich nazw np:

classCar : IComparable
{
publicstring Name { get; set; }
publicint MaxMph { get; set; }
publicint Horsepower { get; set; }
publicdecimal Price { get; set; }
// Compare Cars alphabetically by Name.
publicintCompareTo(object obj)
{
if (!(obj is Car))
thrownew ArgumentException("Object is not a Car");
Car other = obj as Car;
return Name.CompareTo(other.Name);
}
}

Wersja generyczna implementacji interfejsu IComparable dla klasy Car:

class Car : IComparable<Car>
{
public string Name { get; set; }
publicint MaxMph { get; set; }
publicint Horsepower { get; set; }
public decimal Price { get; set; }
// Compare Cars alphabetically by Name.
publicint CompareTo(Car other)
{
returnthis.Name.CompareTo(other.Name);
}
}

IComparer

Interfejs IComparer pozwala na porównywanie obiektów po wielu różnych właściwościach. Klasa implementująca ten interfejs musi posiadać metodę Compare która porównuje obiekty. Przykładowo można utworzyć klasę CarPriceComparer która implementuje interfejs IComparer i posiada metodę Compare która porównuje obiekty klasy Car po właściwościPrice. Można użyć obiektu klasy CarPriceComparer jako parametru metody Array.Sort aby posortować samochody w tablicy wg ceny. 
Interfejs IComparer posiada dwie wersje: zwykłą i generyczną. Wersja zwykła interfejsu istnieje dla zgodności z poprzednimi wersjami .NET rameworka. Zaleca się używanie wersji generycznej. 
Klasa CarPriceComparer pozwala na sortowanie wg ceny ale ciągle nie pozwala na sortowanie wg maksymalnej prędkości czy też innych właściwości samochodu. Można utworzyć kilka różnych klas implementujących interfejs IComparer, ale istnieje prostsze rozwiązanie. jest nim utworzenie jednej klasy implementującej interfejs IComparer z polem które mówi po jakim polu ma odbywać się porównywanie. Poniższy kod przedstawia klasę CarComparer demosntrującą to rozwiązanie:

class CarComparer : IComparer<Car>
{
// The field to compare.
public enum CompareField
{
Name,
MaxMph,
Horsepower,
Price,
}
public CompareField SortBy = CompareField.Name;

publicint Compare(Car x, Car y)
{
switch (SortBy)
{
case CompareField.Name:
return x.Name.CompareTo(y.Name);
case CompareField.MaxMph:
return x.MaxMph.CompareTo(y.MaxMph);
case CompareField.Horsepower:
return x.Horsepower.CompareTo(y.Horsepower);
case CompareField.Price:
return x.Price.CompareTo(y.Price);
}
return x.Name.CompareTo(y.Name);
}
}

IEquatable

Interfejs IEquatable wprowadza metodę Equals która pozwala na sprawdzenie czy obiekty są sobie równe. 
Przykład programu wykorzystującego interfejs:

class Person : IEquatable<Person>
{
publicstring FirstName { get; set; }
publicstring LastName { get; set; }
publicboolEquals(Person other)
{
return ((FirstName == other.FirstName) &&
(LastName == other.LastName));
}
}

Program dodający obiekt klasy Person do listy unikalnych osób:

// The List of Persons.
private List<Person> People = new List<Person>();
// Add a Person to the List.
privatevoidbtnAdd_Click(object sender, EventArgs e)
{
// Make the new Person.
Person person = new Person()
{
FirstName = firstNameTextBox.Text,
LastName = lastNameTextBox.Text
};
if (People.Contains(person))
{
MessageBox.Show("The list already contains this person.");
}
else
{
People.Add(person);
firstNameTextBox.Clear();
lastNameTextBox.Clear();
firstNameTextBox.Focus();
}
}

Metoda Contains listy używa faktu, że klasa Person implementuje interfejs IEquatable do zdecydowania czy obiekt istnieje już w liście. 
Jeżeli klasa Person nie implementowałaby interfejsu IEquatable to metoda Contains traktowałaby każdy inny obiekt jako nowy bez względu na to czy miały by takie same wartości pól. 
Zaleca się aby każda klasa której obiekty mogą być gromadzone w kolekcjach implementowała interfejs IEquatable.

ICloneable

Klasa implementująca interfejs ICloneable musi posiadać metodę Clone która zwraca kopię obiektu dla którego jest wywoływana np:

classPerson : ICloneable
{
publicstring FirstName { get; set; }
publicstring LastName { get; set; }
public Person Manager { get; set; }
// Return a clone of this person.
publicobjectClone()
{
Person person = new Person();
person.FirstName = FirstName;
person.LastName = LastName;
person.Manager = Manager;
return person;
}
}

Metoda Clone zwraca niespecyfikowany obiekt więc kod wywołujący musi rzutować wynik na klasę Person. Przykład użycia:

Person ann = new Person()
{
FirstName ="Ann",
LastName ="Archer",
Manager =null
};
Person bob = new Person()
{
FirstName ="Bob",
LastName ="Baker",
Manager = ann
};
Person bob2 = (Person)bob.Clone();

IEnumerable

Interfejs IEnumerable wprowadza metody pozwalające na wyliczenia elementów które klasa posiada. Metoda GetEnumerator zwraca obiekt implementujący interfejs IEnumerator
Obiekt IEnumerator wprowadza właściwość Current która zwraca aktualny obiekt w wyliczaniu. Wprowadza także metodę MoveNext która przesuwa wyliczenie do następnego elementu i metodę Reset która resetuje enumerator do stanu sprzed wyliczania. Wprowadza także metodę Dispose która czyści wszystkie użyte zasoby kiedy nie są już potrzebne. 
Program poniżej przechowuje informacje o węźle w drzewie:

class TreeNode : IEnumerable<TreeNode>
{
publicint Depth = 0;
publicstring Text = "";
public List<TreeNode> Children = new List<TreeNode>();
publicTreeNode(string text)
{
Text = text;
}
// Add and create children.
public TreeNode AddChild(string text)
{
TreeNode child = new TreeNode(text);
child.Depth = Depth + 1;
Children.Add(child);
return child;
}
// Return the tree's nodes in an preorder traversal.
public List<TreeNode> Preorder()
{
// Make the result list.
List<TreeNode> nodes = new List<TreeNode>();
// Traverse this node's subtree.
TraversePreorder(nodes);
// Return the result.
return nodes;
}
privatevoidTraversePreorder(List<TreeNode> nodes)
{
// Traverse this node.
nodes.Add(this);
// Traverse the children.
foreach (TreeNode child in Children)
child.TraversePreorder(nodes);
}
public IEnumerator<TreeNode> GetEnumerator()
{
returnnew TreeEnumerator(this);
}
IEnumerator IEnumerable.GetEnumerator()
{
returnnew TreeEnumerator(this);
}
}

Klasa TreeEnumerator definiująca obiekty które potrafią enumerować drzewo stworzone z obiektów klasy TreeNode:

class TreeEnumerator : IEnumerator<TreeNode>
{
// The tree's nodes in their proper order.
private List<TreeNode> Nodes;
// The index of the current node.
privateint CurrentIndex;
// Constructor.
publicTreeEnumerator(TreeNode root)
{
Nodes = root.Preorder();
Reset();
}
public TreeNode Current
{
get { return GetCurrent(); }
}
object IEnumerator.Current
{
get { return GetCurrent(); }
}
private TreeNode GetCurrent()
{
if (CurrentIndex < 0)
thrownew InvalidOperationException();
if (CurrentIndex >= Nodes.Count)
thrownew InvalidOperationException();
return Nodes[CurrentIndex];
}
publicboolMoveNext()
{
CurrentIndex++;
return (CurrentIndex < Nodes.Count);
}
publicvoidReset()
{
CurrentIndex = -1;
}
publicvoidDispose()
{
}
}

Ponieważ klasa TreeNode implementuje interfejs IEnumerable a klasa TreeEnumerator implementuje interfejs IEnumerator to program ich używający może enumerować po drzewie złożonym z obiektów TreeNode np:

// Build and display a tree.
privatevoid Form1_Load(object sender, EventArgs e)
{
// Build the tree.
TreeNode president = new TreeNode("President");
TreeNode sales = president.AddChild("VP Sales");
sales.AddChild("Domestic Sales");
sales.AddChild("International Sales");
// Other tree-building code omitted.
...
// Display the tree.
string text = "";
IEnumerator<TreeNode> enumerator = president.GetEnumerator();
while (enumerator.MoveNext())
text += new string(' ', 4 * enumerator.Current.Depth) +
enumerator.Current.Text +
Environment.NewLine;
text = text.Substring(0, text.Length - Environment.NewLine.Length);
treeTextBox.Text = text;
treeTextBox.Select(0, 0);
}

Implementacja interfejsu IEnumerable wymaga dużo pracy w tym zaimplementowanie kilku metod oraz dodanie klasy pomocniczej implementującej interfejs IEnumerator. Jeżeli program wymaga tylko wykonywania pętli na zbiorze obiektów to istnieje łatwiejsza metoda. 
Należy dodać do klasy metodę zwracającą obiekt typu IEnumerable, gdzie class jest klasą z którą pracujemy. Niech metoda znajdzie wszystkie obiekty które mają być wyliczane i niech wywoła dla każdego z nich yield return. Na koniec wyliczania metoda musi wywołać yield break
Poniższy kod pokazuje klasę TreeNode:

// Return an enumerator.
public IEnumerable<TreeNode> GetTraversal()
{
// Get the preorder traversal.
List<TreeNode> traversal = Preorder();
// Yield the nodes in the traversal.
foreach (TreeNode node in traversal) yieldreturn node;
yieldbreak;
}

Kod wywołuje metodę Preorder opisaną wcześniej aby pobrać listę węzłów drzewa. 
Przykładowe użycie enumeracji:

string text = "";
foreach (TreeNode node in president.GetTraversal())
{
text += newstring(' ', 4 * node.Depth) +
node.Text +
Environment.NewLine;
}

Użycie słowa kluczowego yield jest dużo łatwiejsze niż implementacja interfejsu IEnumerable a rezultat programu jest taki sam.

Zarządzanie cyklem życia obiektów

Kiedy program w języku C# tworzy instancję klasy to powstaje obiekt. Program używa tego obiektu przez pewien czas, lecz od pewnego momentu obiekt może już nie byc potrzebny. Kiedy program traci ostatnią referencję do obiektu, albo poprzez ustawienie wszystkich referencji na null albo wyszły z zakresu (scope), to obiekt przestaje być dostępny dla programu i staje się kandydatem do usunięcia (garbage collection). 
W pewnym momencie garbage collector (GC) może zdecydować, że programowi zaczyna brakować pamięci i czas rozpocząć usuwanie śmieci. GC zaznacza całą pamięć, która była używana przez program, jako obecnie nieosiągalną (unreachable). Następnie przechodzi przez wszystkie referencje dostępne dla programu i oznacza je jako osiągalne (reachable). Jeżeli referencja odwołuje się do obiektu to GC podąża za nią dopóki nie odwiedzi wszystkich obiektów do których program może się dostać. 
Kiedy GC sprawdzi referencje to bada wszystkie obiekty oznaczone jako nieosiągalne. Jeżeli obiekt posiada metodę Finalize to GC ją wywołuje w celu wykonania wymaganych porządków. Po wykonaniu metody FinalizeGC czyści pamięć zajętą przez obiekt w celu późniejszego jej użycia. 
Proces wywoływania metody Finalize jest nazywany finalizacją. Ponieważ nie można powiedzieć GC aby uruchomił finalizację obiektu to proces ten nazywa się finalizacją niedeterministyczną. 
Proces ten jest prosty dla prostych obiektów ale może być bardziej skomplikowany dla obiektów która mają dostęp do zasobów które uszą być w jakiś sposób oczyszczone. Przykładowo program tworzy obiekt który blokuje plik do zapisu, przykładowo do rejestrowania zdarzeń. Kiedy obiekt przestaje być w zakresie to staje się kandydatem do czyszczenia dlaGC, lecz nie można powiedzieć GC kiedy ma sfinalizować ten obiekt a tymczasem plik pozostaje zablokowany pomimo, że nie jest używany przez program. 
Można wykonać dwa kroki aby pomóc obiektowi zwolnić jego zasoby: zaimplementować interfejs IDisposable i użyć destruktorów.

Implementacja interfejsu IDisposable

Klasa implementująca interfejs IDisposable musi posiadać metodę Dispose która służy do czyszczenia zasobów używanych przez klasę. Program powinien wywoływać metodę Dispose lub używać deklaracji using kiedy obiekt nie jest już potrzebny i można wykonać czyszczenie jego zasobów. 
Głównym zadaniem metody Dispose jest czyszczenie zasobów niezarządzanych (nie kontrolowanych przez CLR np. pliki), ale może również czyścić zasoby zarządzane. jeżeli obiekt używa referencji do innych obiektów implementujących interfejs IDisposable, to może wywołać ich metodę Dispose
Przykładowo obiekt klasy Shape reprezentuje kształt rysunku i posiada właściwości które są referencjami do obiektów klas Brush i Pen. Klasy Brush i Pen są klasami zarządzanymi implementującymi interfejs IDisposable, więc wywołanie metody Dispose klasy Shape czyści jej zasoby. 
Innym przykładem może być klasa ImageTransformer która używa kodu niezarządzanego do manipulacji bitmapami. Używa wywołań API aby otrzymać uchwyt do bitmapy (HBITMAP), innych wywołań API aby otrzymać kontekst urządzenia (DC) i innych do manipulacji bitmapami. Wywołania API są kodem niezarządzanym. Jeżeli obiekt ImageTransformer jest niszczony bez użycia wywołania API do usunięcia jego uchwytów, to zajmowana przez nie pamięć jest stracona. Metoda Dispose klasy ImageTransformer powinna użyć odpowiednich metod API do zwolnienia tych zasobów. 
Wg konwencji wywołanie metody Dispose więcej niż jeden raz powinno być bezpieczne dlatego stosuje się zmienną binarną sprawdzającą czy metoda ta była już wywołana na obiekcie, jeżeli tak to metoda nie powinna robić nic więcej. 
Microsoft dla obiektów które mogą być używane wielokrotnie zaleca używanie metod Open i Close.

Destruktory

Metoda Dispose zwalnia zasoby jeżeli program ją wywoła, ale jeżeli program jej nie wywoła to zasoby nie zostaną zwolnione. Kiedy GC niszczy niepotrzebne obiekty to oczyszcza zasoby zarządzane ale zasoby niezarządzane nie zostają zwolnione. Aby zapobiec takiej sytuacji można dodać klasie konstruktor który zwalnia zasoby kiedy obiekt jest niszczony. 
Destruktor jest metodą która nie ma typu zwracanego a nazwa jest taka sama jak nazwa klasy z prefiksem ~GC wykonuje konstruktor przed zniszczeniem obiektu. 
Destruktory posiadają pewne zasady które nie są stosowane do innych metod:

  • Destruktory mogą być definiowane tylko w klasach, nie w strukturach.
  • Klasa może posiadać tylko jeden destruktor.
  • Destruktor nie może być odziedziczony lub przeciążony.
  • Destruktor nie może być wywołany bezpośrednio.
  • Destruktor nie może posiadać modyfikatorów ani parametrów. 
    GC wywołuje finalizację obiektu a nie destruktor. Destruktor jest konwertowany na nadpisaną metodę Finalize która wykonuje kod destruktora po czym wywołuje metodę Finalize klasy bazowej. 
    Przykładowo klasa Person posiada taki destruktor:
~Person()
{
// Free unmanaged resources here.
//...
}

Destruktor jest konwertowany na metodę Finalize:

protected override void Finalize()
{
try
{
// Free unmanaged resources here.
//...
}
finally
{
base.Finalize();
}
}

Nie można bezpośrednio nadpisać metody Finalize w kodzie C# oraz nie można jej bezpośrednio wywołać. 
Kiedy wykonywany jest destruktor to GC jest prawdopodobnie w czasie usuwania innych obiektów więc kod destruktora nie może zależeć od istnienia innych obiektów. Przykładowo klasa Person posiada referencję do klasy Company. Destruktor klasy Person nie może zakładać, że obiekt klasy Company nadal istnieje ponieważ mógł być wcześniej zniszczony przez GC. Oznacza to, że destruktor klasy Person nie może wywoływać metody Dispose klasy Company.

Istnieje jeszcze jeden haczyk w zarządzaniu zasobami. jeżeli obiekt posiada destruktor to musi przejść przez kolejkę finalizacji (finalization queue) zanim zostanie zniszczony a to zajmuje dodatkowy czas. Jeżeli metoda Dispose zwolniła zasoby obiektu wcześniej to nie ma potrzeby aby uruchamiać destruktor obiektu. W tym wypadku metoda Dispose może wywołać GC.SuppressFinalize aby GC nie wywoływał finalizacji obiektu i przeskoczył kolejkę finalizacji przy niszczeniu obiektu. 
Podsumowanie zasad i konceptów zarządzania zasobami:

  • Jeżeli klasa nie posiada zasobów zarządzanych i niezarządzanych to nie potrzebuje implementować interfejsu IDisposable ani posiadać destruktora.
  • Jeżeli klasa posiada tylko zasoby zarządzane to powinna implementować IDisposable ale nie potrzebuje destruktora.
  • Jeżeli klasa posiada zasoby niezarządzane to powinna implementować IDisposable oraz destruktor w wypadku gdy metoda Dispose nie zostanie wywołana przez program.
  • Metoda Dispose musi być zabezpieczona przed jej wielokrotnym wywołaniem.
  • Metoda Dispose powinna zwalniać zasoby zarządzane i niezarządzane.
  • Destruktor powinien zwalniać tylko zasoby niezarządzane.
  • Po zwolnieniu zasobów metoda Dispose powinna wywoływać GC.SuppressFinalize aby przeskoczyć kolejkę finalizacji. 
    Przykład programu z implementacją IDisposable i destruktorem:
classDisposableClass : IDisposable
{
// A name to keep track of the object.
publicstring Name = "";
// Free managed and unmanaged resources.
publicvoidDispose()
{
FreeResources(true);
}
// Destructor to clean up unmanaged resources
// but not managed resources.
~DisposableClass()
{
FreeResources(false);
}
// Keep track if whether resources are already freed.
privatebool ResourcesAreFreed = false;
// Free resources.
privatevoidFreeResources(bool freeManagedResources)
{
Console.WriteLine(Name + ": FreeResources");
if (!ResourcesAreFreed)
{
// Dispose of managed resources if appropriate.
if (freeManagedResources)
{
// Dispose of managed resources here.
Console.WriteLine(Name + ": Dispose of managed resources");
}
// Dispose of unmanaged resources here.
Console.WriteLine(Name + ": Dispose of unmanaged resources");
// Remember that we have disposed of resources.
ResourcesAreFreed = true;
// We don't need the destructor because
// our resources are already freed.
GC.SuppressFinalize(this);
}
}
}

GC posiada metodę Collect wymuszająca uruchomienie oczyszczania śmieci przez GC ale używanie jej jest niezalecane ponieważ zaburza normalny cykl algorytmu oczyszczania GC co może mieć wpływ na wydajność programu.

Użycie deklaracji using

Jeżeli obiekt posiada metodę Dispose, to program który go uzywa powinien ją wywołać po wykonaniu przcy obiektu aby zwolnić jego zasoby. Jest to bardzo ważne ale bardzo łatwe do zapomnienia. Aby można było łatwiej to wywoływać w języku C# wprowadzono deklarację using
Deklaracja using rozpoczyna blok kodu który jest związany z obiektem implementującym interfejs IDisposable. Kiedy blok jest zakończony to program automatycznie wywołuje metodę Dispose
Przykład użycia:

using (DisposableClass obj = newDisposableClass())
{
obj.Name = "CreateAndDispose " + ObjectNumber.ToString();
ObjectNumber++;
}

Blok using wywołuje metodę Dispose kiedy jest zakończony, nawet jeżeli kod w środku wyrzuci wyjątek. Poprzedni kod jest ekwiwalentem to poniższego kodu:

{
DisposableClass obj = new DisposableClass();
try
{
obj.Name = "CreateAndDispose " + ObjectNumber.ToString();
ObjectNumber++;
}

finally
{
if (obj != null) obj.Dispose();
}

}

Deklaracja using posiada trzy formy:

// Version 1.
using (DisposableClass obj1 = new DisposableClass())
{
}

// Version 2.
DisposableClass obj2 = new DisposableClass();
using (obj2)
{
}

// Version 3.
DisposableClass obj3;
using (obj3 = new DisposableClass())
{
}

Pierwsza z metod jest zalecana ponieważ obiekt jest inicjowany w nawiasie i jego zasięg ogranicza się do bloku using
W pozostałych metodach pomimo, że obiekt uruchomił już metodę Dispose to jest nadal dostępny w zakresie poza blokiem using.

Podsumowanie

Dziedziczenie

  • Język C# nie zezwala na wielokotne dziedziczenie.
  • Słowo kluczowe base pozwala wywołać konstruktor klasy rodzica w następujący sposób:
public class Employee : Person
{
public Employee(string firstName, string lastName)
: base(firstName, lastName)
{
//...
}
}
  • Słowo kluczowe this pozwala wywołać inny konstruktor tej samej klasy w następujący sposób:
publicclassPerson
{
publicstring FirstName { get; set; }
publicstring LastName { get; set; }
publicPerson(string firstName)
{
FirstName = firstName;
}
publicPerson(string firstName, string lastName)
: this(firstName)
{
LastName = lastName;
}
}
  • Konstruktor może wywołać tylko jeden konstruktor klasy bazowej lub jeden konstruktor tej samej klasy.
  • Jeżeli klasa bazowa ma konstruktory to konstruktory klasy potomka muszą wywoływać jeden z nich pośrednio lub bezpośrednio.

Interfejsy

  • Zgodnie z konwencją nazwy interfejsów rozpoczyna się od dużej litery I np: IComparable.
  • Klasa może implementować wiele interfejsów jednocześnie.
  • Jeżeli klasa implementuje interfejs jawnie to kod nie może dostać się do członków interfejsu poprzez instancję obiektu. Zamiast tego musi użyć instancji interfejsu.
  • jeżeli klasa implementuje interfejs niejawnie to kod może dostać się do członków interfejsu poprzez instancję obiektu jak i interfejsu.
  • Interfejs IComparable wprowadza metodę CompareTo która określa kolejność obiektów.
  • Interfejs IComparer wprowadza metodę Compare która porównuje dwa obiekty i określa ich kolejność.
  • Interfejs IEquatable wprowadza metodę Equals która sprawdza czy obiekty są sobie równe.
  • Interfejs ICloneable wprowadza metodę Clone która tworzy kopię obiektu.
  • Interfejs IEnumerable wprowadza metodę GetEnumerator która zwraca obiekt interfejsu IEnumerator który pozwala na użycie metod MoveNext i Reset które umożliwiają przechodzenie przez listę obiektów.
  • Metoda może użyć deklaracji yield return aby dodac obiekt do wyniku IEnumerator.

Destruktory

  • Destruktory mogą być definiowane jedynie w klasach.
  • Klasa może posiadać tylko jeden destruktor.
  • Destruktor nie może być dziedziczony ani przeciążony.
  • Destruktor nie może być bezpośrednio wywołany.
  • Destruktor nie może posiadać modyfikatorów ani parametrów.
  • Destruktor jest konwertowany na nadpisaną wersję metody Finalize. Metody Finalize nie można nadpisać bezpośrednio ani wywołać.

Zarządzanie zasobami

  • Jeżeli klasa nie posiada zasobów zarządzanych i niezarządzanych to nie potrzebuje implementować interfejsu IDisposable ani posiadać destruktora.
  • Jeżeli klasa posiada tylko zasoby zarządzane to powinna implementować IDisposable ale nie potrzebuje destruktora.
  • Jeżeli klasa posiada zasoby niezarządzane to powinna implementować IDisposable oraz destruktor w wypadku gdy metoda Dispose nie zostanie wywołana przez program.
  • Metoda Dispose musi być zabezpieczona przed jej wielokrotnym wywołaniem.
  • Metoda Dispose powinna zwalniać zasoby zarządzane i niezarządzane.
  • Destruktor powinien zwalniać tylko zasoby niezarządzane.
  • Po zwolnieniu zasobów metoda Dispose powinna wywoływać GC.SuppressFinalize aby przeskoczyć kolejkę finalizacji.
  • Deklaracja using pozwala programowi na automatyczne wywoływanie metody Dispose po wykonaniu bloku kodu.