Zagadnienia egzaminu 70-483 opisane w tej notatce:

  • Tworzenie i implementacja zdarzeń i wywołań zwrotnych.
  • Użycie wyrażeń lambda.
  • Użycie metod anonimowych.
  • Implementacja obsługi wyjątków.
  • Blok try-catch-finally.
  • Wyrzucanie wyjątków.

Praca z delegatami

Poniższy kod pokazuje jak tworzymy delegat:

[accessibility] delegate returnType DelegateName([parameters]);

Elementy tworzenia delegat:

  • accessibility - modyfikator dostępu do delegat np. private lub public.
  • delegate - wymagane słowo kluczowe.
  • returnType - typ danych zwracany przez metody tego delegaty np int lub string.
  • DelegateName - nazwa typu delegaty.
  • parameters - lista parametrów które metody tego typu delegaty powinny przyjmować na wejściu. 
    Przykładowy delegat:
privatedelegatefloatFunctionDelegate(float x);

Ten typ reprezentuje metody które przyjmują liczbę typu float jako parametr i zwracają typ int jako wynik. 
Po zdefiniowaniu typu delegaty można utworzyć zmienną tego typu. Poniższy kod deklaruje zmienną TheFunction typu FunctionDelegate:

private FunctionDelegate TheFunction;

Później można ustawić zmienną na metodę która ma odpowiednie parametry i zwraca odpowiedni typ. Poniższy kod definiuje metodę Function1 i ustawia zmienną TheFunction na zadeklarowaną metodę:

// y = 12 * Sin(3 * x) / (1 + |x|)
privatestaticfloatFunction1(float x)
{
return (float)(12 * Math.Sin(3 * x) / (1 + Math.Abs(x)));
}
// Initialize TheFunction.
privatevoidForm1_Load(object sender, EventArgs e)
{
TheFunction = Function1;
}

Po inicjalizacji zmiennej TheFunction program może użyć jej jakby to była sama metoda. Przykładowo poniższy kod ustawia zmienną y równą wartości zwracanej przez TheFunction z parametrem o wartości 1.23.

float y = TheFunction(1.23f);

W tym momencie nie wiadomo do jakiej metody odnosi się zmienna TheFunction. Zmienna może odnosić się do metody Function1 lub innej metody która posiada sygnaturę pasującą do typu FunctionDelegate.

Zaleca się aby delegaty przechowywać w zmiennych. Delegaty można przechowywać jak każdy inny typ wartości w strukturze, tablicy czy liście. Przykładowo poniższy kod tworzy listę z która może zawierać referencje do metod które pasują do typu FunctionDelegate:

List<FunctionDelegate> functions = newList<FunctionDelegate>();

Szczegóły delegaty

Użycie delegat jest podobne do użycia innych typów danych. Wartościami delegat są referencje do metod. Delegaty mają również zdefiniowane operacje takie jak dodawanie czy odejmowanie. Załóżmy, że metody Method1 i Method2 nie pobierają żadnych parametrów i zwracają typ void to poniższy kod jest poprawny:

Action del1 = Method1;
Action del2 = Method2;
Action del3 = del1 + del2 + del1;

Delegata del3 zawiera serię zmiennych. Jeżeli ją wywołamy to program wykona po kolei Method1Method2 i Method1
Istnieje również kilka cech które są unikalne dla delegat.

Metody statyczne i metody instancji

Jeżeli ustawimy zmienną delegaty do statycznej metody to jest jasne co stanie się po wywołaniu tej metody. Istnieje tylko jedna taka metoda współdzielona przez wszystkie instancje klasy która ją definiuje, więc to jest zawsze ta metoda która jest wywoływana. 
Jeżeli ustawimy wartość zmiennej delegaty do metody instancji to wynik może być bardziej zagmatwany. Kiedy wykonamy metodę przypisaną do zmiennej to wykonana zostanie metoda tej instancji klasy która została przypisana do zmiennej. 
Przykład kodu z metodą statyczną i metodą instancji:

class Person
{
public stringName;
// A method that returns a string.
public delegate stringGetStringDelegate();
// A staticmethod.
public staticstringStaticName()
{
return"Static";
}
// Return this instance's Name.
public stringGetName()
{
returnName;
}
// Variables to hold GetStringDelegates.
public GetStringDelegateStaticMethod;
public GetStringDelegateInstanceMethod;
}
//...
private voidForm1_Load(object sender, EventArgs e)
{
// Make some Persons.
Person alice = new Person() { Name = "Alice" };
Person bob = new Person() { Name = "Bob" };
// MakeAlice's InstanceMethod variable refer to her own GetNamemethod.
alice.InstanceMethod = alice.GetName;
alice.StaticMethod = Person.StaticName;
// MakeBob's InstanceMethod variable refer to Alice's GetNamemethod.
bob.InstanceMethod = alice.GetName;
bob.StaticMethod = Person.StaticName;
// Demonstrate the methods.
stringresult = "";
result += "Alice's InstanceMethod returns: " + alice.InstanceMethod() +
Environment.NewLine;
result += "Bob's InstanceMethod returns: " + bob.InstanceMethod() +
Environment.NewLine;
result += "Alice's StaticMethod returns: " + alice.StaticMethod() +
Environment.NewLine;
result += "Bob's StaticMethod returns: " + bob.StaticMethod();
resultsTextBox.Text = result;
resultsTextBox.Select(0, 0);
}

Kowariancja i kontrawariancja

Kowariancja i kontrawariancja dają pewną elastyczność podczas przypisywania metod do zmiennych delegat. Przede wszystkim pozwalają traktować zwracany typ i parametry delegaty polimorficznie. 
Kowariancja pozwala metodzie zwracać wartości z podklas oczekiwanego przez delegatę wyniku. Przykładowo jeżeli klasa Employee dziedziczy z klasy Person i delegata typu ReturnPersonDelegate reprezentuje metodę która zwraca obiekt typu Person to można do zmiennejReturnPersonDelegate przypisać metodę która zwraca typ Employee ponieważ jest on podtypem klasy Person
Kontrawariancja pozwala metodom pobierać parametry które są typu klasy nadrzędnej do oczekiwanego typu. Przykładowo załużmy, że delegat typu EmployeeParameterDelegate reprezentuje metodę która oczekuje obiektu klasy Employee jako parametru to można do zmiennej typuEmployeeParameterDelegate przypisać metodę która przyjmuje parametr typu Person
Przykład kodu wykorzystującego kowariancję i kontrawariancję:

// A delegate that returns a Person.
privatedelegate Person ReturnPersonDelegate();
private ReturnPersonDelegate ReturnPersonMethod;
// A method that returns an Employee.
private Employee ReturnEmployee()
{
returnnew Employee();
}
// A delegate that takes an Employee as a parameter.
privatedelegatevoidEmployeeParameterDelegate(Employee employee);
private EmployeeParameterDelegate EmployeeParameterMethod;
// A method that takes a Person as a parameter.
privatevoidPersonParameter(Person person)
{
}
// Initialize delegate variables.
privatevoidForm1_Load(object sender, EventArgs e)
{
// Use covariance to set ReturnPersonMethod = ReturnEmployee.
ReturnPersonMethod = ReturnEmployee;
// Use contravariance to set EmployeeParameterMethod = PersonParameter.
EmployeeParameterMethod = PersonParameter;
}

Wbudowane delegaty

Delegata Action

Generyczna delegata Action reprezentuje metodę zwracającą typ void. Różne wersje delegaty Action pobieraja od 1 do 18 parametrów wejściowych. Poniższy kod pokazuje definicję delegaty Action pobierającej dwa parametry wejściowe:

publicdelegatevoid Action<in T1, in T2>(T1 arg1, T2 arg2)

Słowo kluczowe in z parametrami generycznymi oznacza, że parametry T1 i T2 są kontrawariantami. 
jeżeli potrzebujemy zdefiniować delegat który pobiera mniej niż 18 parametrów wejściowych to możemy użyć Action zamiast tworzyć swój własny typ. Przykładowe porównanie tworzenia:

// Method 1.
privatedelegatevoidEmployeeParameterDelegate(Employee employee);
private EmployeeParameterDelegate EmployeeParameterMethod1;
// Method 2.
private Action<Employee> EmployeeParameterMethod2;

Delegata Func

Generyczna delegata Func reprezentuje metodę zwracającą wartość i pobiera od 1 do 18 parametrów wejściowych. Przykład delegaty Func pobierającej dwa parametry wejściowe:

publicdelegate TResult Func<in T1, in T2, out TResult>(T1 arg1, T2 arg2)

Trzy typy zdefiniowane przez generyczną delegatę to dwa typy parametrów wejściowych i typ zwracany. Przykładowe porównanie tworzenia:

// Method 1.
privatedelegate Person ReturnPersonDelegate();
private ReturnPersonDelegate ReturnPersonMethod1;
// Method 2.
private Func<Person> ReturnPersonMethod2;

Metody anonimowe

Metody anonimowe to metody które nie posiadają nazwy. Zamiast tworzyć metodę w normalny sposób tworzy się delegatę która odnosi się do metody anonimowej. Składnia tworzenia delegaty z metodą anonimową:

delegate([parameters]) { code... }

Przykład metody anonimowej:

private Func<float, float> Function = delegate(float x) { return x * x; };

Nie można wywołać takiej metody po nazwie ale można użyć zmiennej Function aby wywołać metodę. 
Zwykle używa się metod anonimowych kiedy program kiedy program potrzebuje relatywnie prostego fragmentu kodu wywoływanego w jednym miejscu. 
Dwa inne miejsca gdzie często wykorzystuje się metody anonimowe to definiowanie prostej obsługi zdarzeń i wykonywanie prostych zadań w osobnych wątkach. 
Poniższy kod dodaje obsługę zdarzenia Paint:

privatevoidForm1_Load(object sender, EventArgs e)
{
this.Paint += delegate(object obj, PaintEventArgs args)
{
args.Graphics.DrawEllipse(Pens.Red, 10, 10, 200, 100);
};
}

Poniższy kod wykonuje metodę anonimową w osobnym wątku:

privatevoid Form1_Load(object sender, EventArgs e)
{
System.Threading.Threadthread=new System.Threading.Thread(
delegate() { MessageBox.Show("Hello World"); }
);
thread.Start();
}

Wyrażenia Lambda

Metody anonimowe pozwalaja na tworzenie krótkich metod wykonywanych tylko w jednym miejscu. Wyrażenia lambda są skróconą notacją tworzącą metody anonimowe. 
Wyrażenia lambda mogą być zapisywane w kilku formatach i kilku wariantach:

Expression Lambdas

Składnia wyrażenia lambda:

() => expression;

Wyrażenie expression w kodzie jest pojedynczą deklaracją języka C# która powinna być wykonana przez delegatę. Fakt, że to wyrażenie lambda posiada pojedyncze wyrażenie po prawej stronie sprawia, że jest to wyrażenie lambda. 
Pusty nawias reprezentuje pustą listę parametrów metody anonimowej. Znaki => oznaczają, że jest to deklaracja lambda. 
Przykład użycia:

Action note = () => MessageBox.Show("Hi");

Przykład użycia z parametrami:

Action<string> note = (message) => MessageBox.Show(message);

Kiedy wyrażenie lambda posiada jeden parametr to można ominąć nawias np:

Action<string> note = message => MessageBox.Show(message);

Zwykle Visual Studio potrafi wywnioskować typy danych parametrów wyrażenia lambda. Jeżeli nie lub jeżeli chcemy zapisać kod bardziej jawnie to można zawrzeć typ danych parametrów np:

Action<string> note = (stringmessage) => MessageBox.Show(message);

Przykład z czterema parametrami wejściowymi:

Action<string, string, MessageBoxButtons, MessageBoxIcon> note;
note = (message, caption, buttons, icon) =>
MessageBox.Show(message, caption, buttons, icon);
note("Invalid input", "Alert", MessageBoxButtons.OK,
MessageBoxIcon.Asterisk);

Wyrażenie lambda może również zwracać wartość np:

Func<float, float> square = (float x) => x * x;
float y = square(13);

Statement Lambdas

Deklaracja lambda różni się tym, że jej kod zawarty jest w nawiasie klamrowym oraz musi posiadać deklarację return zwracającą jakiś wynik. Pozwala to na zapis bardziej skomplikowanych wyrażeń składających się z więcej niż jednego wyrażenia. 
Przykład deklaracji lambda:

TheFunction = (float x) =>
{
constfloat A = -0.0003f;
constfloat B = -0.0024f;
constfloat C = 0.02f;
constfloat D = 0.09f;
constfloat E = -0.5f;
constfloat F = 0.3f;
constfloat G = 3f;
return (((((A * x + B) * x + C) * x + D) * x + E) * x + F) * x + G;
};

Async Lambdas

Asynchroniczne wyrażenia lambda pozwalają na użycie słowa kluczowego async sprawiającego, że metoda może być wykonywana asynchronicznie. Można wtedy użyć słowa kluczowego await aby fragment kodu wywołał asynchroniczną metodę i poczekał na zwracaną wartość. Przykład kodu wykorzystującego asynchroniczne wyrażenie lambda:

// The number of times we have run DoSomethingAsync.
privateint Trials = 0;
// Create an event handler for the button.
privatevoidForm1_Load(object sender, EventArgs e)
{
runAsyncButton.Click += async (button, buttonArgs) =>
{
int trial = ++Trials;
statusLabel.Text = "Running trial " + trial.ToString() + "...";
await DoSomethingAsync();
statusLabel.Text = "Done with trial " + trial.ToString();
};
}
// Do something time consuming.
async Task DoSomethingAsync()
{
// In this example, just waste some time.
await Task.Delay(3000);
}

Praca ze zdarzeniami

Zdarzenia pozwalają obiektom na komunikowanie programowi, że coś ciekawego zaszło. Przykładowo obiekt skrzynki email może wywołać zdarzenie aby powiedzieć programowi, że otrzymał nową wiadomość, guzik może wywołać zdarzenie mówiące programowi, że użytkownik nacisnął reprezentację graficzną guzika na ekranie. Obiekt, który wywołuje zdarzenie nazywa wydawca tego zdarzenia (event’s publisher). Klasa która przechwytuje zdarzenia nazywana jest subskrybentem (subscriber).

Publikowanie zdarzeń

Zanim obiekt zgłosi zdarzenie, musi najpierw je zadeklarować aby subskrybenci wiedzieli jakie zdarzenie jest zgłaszane i jakie parametry zawiera. Składnia deklaracji zdarzenia:

accessibility eventdelegate EventName;

Przykładowo klasa BankAccount może używać poniższego kodu do definiowania zdarzenia Overdrawn:

publicdelegatevoidOverdrawnEventHandler();
publicevent OverdrawnEventHandler Overdrawn;

Pierwsza linijka deklaruje delegatę OverdrawnEventHandler, która reprezentuje metodę nie pobierającą żadnych parametrów wejściowych i zwracającą void
Druga linijka deklaruje zdarzenie o nazwie Overdrawn typu OverdrawnEventHandler. Oznacza to, że subskrybenci muszą użyć metod pasujących do delegaty OverdrawnEventHandler aby obsłużyć to zdarzenie. 
Później obiekt BankAccount może zgłosić to zdarzenie w razie potrzeby. Przykład klasy BankAccount:

classBankAccount
{
publicdelegatevoidOverdrawnEventHandler();
publicevent OverdrawnEventHandler Overdrawn;
// The account balance.
publicdecimal Balance { get; set; }
// Add money to the account.
publicvoidCredit(decimal amount)
{
Balance += amount;
}
// Remove money from the account.
publicvoidDebit(decimal amount)
{
// See if there is this much money in the account.
if (Balance >= amount)
{
// Remove the money.
Balance -= amount;
}
else
{
// Raise the Overdrawn event.
if (Overdrawn != null) Overdrawn();
}
}
}

Predefiniowane typy zdarzeń

Zdarzenie zdefiniowane powyżej, które nie pobiera parametrów i nic nie zwraca można uprościć za pomocą predefiniowanej delegaty Action:

publicevent Action Overdrawn;

Najlepsze praktyki używania zdarzeń

Microsoft zaleca aby wszystkie zdarzenia posiadały dwa parametry: obiekt zgłaszający zdarzenie i obiekt z argumentami które są wymagane do obsługi zdarzenia. Drugi obiekt powinien dziedziczyć z klasy EventArgs
Przykładowo obiekt klasy BankAccount opisany w poprzedniej sekcji powinien być pierwszym argumentem zdarzenia tak aby można było stwierdzić które konto zgłosiło zdarzenie Overdrawn
Fakt, że zdarzenie zostało zgłoszone mówi nam o tym, że konto nie posiada wystarczającej ilości środków ale nie mówi nam jak duży jest debet. Można zmodyfikować program tak aby jako drugi parametr przesyłał nam tę informację. 
Klasa z informacjami powinna dziedziczyć z klasy EventArgs i wg konwencji jej nazwa powinna składać się z nawy zdarzenia i przyrostka EventArgs np:

classOverdrawnEventArgs : EventArgs
{
publicdecimal CurrentBalance, DebitAmount;
publicOverdrawnEventArgs(decimal currentBalance, decimal debitAmount)
{
CurrentBalance = currentBalance;
DebitAmount = debitAmount;
}
}

Ponieważ obsługa zdarzenia wymaga teraz dwóch parametrów, to trzeba zmienić deklarację zdarzenia. Można stworzyć nową delegatę ale .NET Framework posiada już predefiniowaną delegatę generyczną o nazwie EventHandler. Deklaracja zdarzenia po zamianie:

publicevent EventHandler<OverdrawnEventArgs> Overdrawn;

Metoda Debit wywołująca nowy typ zdarzenia:

// Remove money from the account.
publicvoidDebit(decimal amount)
{
// See if there is this much money in the account.
if (Balance >= amount)
{
// Remove the money.
Balance -= amount;
}
else
{
// Raise the Overdrawn event.
if (Overdrawn != null)
Overdrawn(this, new OverdrawnEventArgs(Balance, amount));
}
}

Dziedziczenie zdarzeń

Zdarzenie może być wywoływane jedynie z klasy która je deklaruje więc podklasy nie mogą zgłaszać zdarzeń zadeklarowanych w klasie bazowej. 
Rozwiązaniem zaproponowanym przez Microsoft jest dodanie do klasy bazowej metody chronionej która wywołuje zdarzenie. Klasa która dziedziczy może wywołać tę metodę aby zgłosić zdarzenie. Wg konwencji metody takie zaczynają się przedrostkiem On i kończą nazwą zdarzenia npOnOverdrawn
Wg wzorca obsługi metoda wywołująca zdarzenie Overdrawn klasy BankAccount powinna przenieść wywołanie zdarzenia do nowej metody OnOverdrawn, np:

// Raise the Overdrawn event.
protectedvirtualvoidOnOverdrawn(OverdrawnEventArgs args)
{
if (Overdrawn != null) Overdrawn(this, args);
}
// Remove money from the account.
publicvoidDebit(decimal amount)
{
// See if there is this much money in the account.
if (Balance >= amount)
{
// Remove the money.
Balance -= amount;
}
else
{
// Raise the Overdrawn event.
OnOverdrawn(new OverdrawnEventArgs(Balance, amount));
}
}

Przypuśćmy, że chcemy dodać klasę MoneyMarketAccount dziedziczącą z klasy BankAccount. Kiedy chcemy zgłosić zdarzenie Overdrawn to wywołujemy metodę rodzica OnOverdrawn:

classMoneyMarketAccount : BankAccount
{
publicvoidDebitFee(decimal amount)
{
// See if there is this much money in the account.
if (Balance >= amount)
{
// Remove the money.
Balance -= amount;
}
else
{
// Raise the Overdrawn event.
OnOverdrawn(new OverdrawnEventArgs(Balance, amount));
}
}
}

Subskrybowanie i anulowanie subskrypcji do zdarzeń

Aby dodać subskrypcję zdarzenia można użyć kodu podobnego do tego poniżej:

processOrderButton.Click += processOrderButton_Click;

Kod ten dodaje metodę processOrderButton_Click jako obsługę zdarzenia Click guzika processOrderButton
Alternatywnie ten sam kod można napisać tak:

processOrderButton.Click += new System.EventHandler(processOrderButton_Click);

Przykładowa pusta metoda obsługująca zdarzenie processOrderButton_Click:

voidprocessOrderButton_Click(object sender, EventArgs e)
{
thrownew NotImplementedException();
}

Anulowanie subskrypcji dokonywane jest za pomocą poniższego kodu:

processOrderButton.Click -= processOrderButton_Click;

Jeżeli dokonamy subskrypcji zdarzenia więcej niż raz to obsługa tego zdarzenia będzie wywołana więcej niż raz. 
Za każdym razem jak anulujemy subskrypcję zdarzenia to usuwana jest jedna subskrypcja z listy subskrybentów. 
Jeżeli anulujemy subskrypcję obsługi zdarzenia które nie jest subskrybowane do zdarzenia to nic się nie wydarzy i nie będzie zgłoszony żaden błąd ani wyjątek. 
Subskrybowanie zdarzeń może się również odbyć z okna Designer w Visual Studio.

Obsługa wyjątków

Bez względu na to, jak dobrze zaprojektujemy aplikację, pewne problemy są nieuniknione. Użytkownicy wpiszą nieprawidłowe wartości, niezbędne pliki zostaną usunięte a krytyczne połączenia sieciowe nie powiodą się. Aby uniknąć tego rodzaju problemów, program musi obsługiwać sprawdzanie błędów i obsługę wyjątków.

Sprawdzanie błędów i obsługa wyjątków

Sprawdzanie błędów to proces przewidywania błędów, sprawdzając, czy będą występować, i pracy wokół nich. Na przykład, jeśli użytkownik musi wprowadzić liczbę całkowitą w polu tekstowym to w końcu ktoś wprowadzi wartość nienumeryczną. Jeśli program próbuje analizować wprowadzone wartości, jak gdyby były liczbą całkowitą, to ulegnie awarii. 
Zamiast awarii, program powinien walidować wprowadzany tekst aby sprawdzić czy ma on sens przed konwertowaniem jego wartości. Przykładowo metoda int.TryParse wykonuje obie te czynności. 
W przeciwieństwie do sprawdzania błędów, obsługa wyjątków jest procesem zabezpieczania aplikacji, gdy wystąpi nieoczekiwany błąd. Przykładowo kiedy program zacznie pobierać plik z sieci i w pewnym momencie utracone zostanie połączenie sieciowe. Nie ma możliwości aby program przewidział taki problem ponieważ sieć była dostępna kiedy program rozpoczynał pobieranie. 
Nawet jeśli sprawdzimy każdą wartość wprowadzoną przez użytkownika i sprawdzimy każdą możliwą sytuację, mogą pojawić się nieoczekiwane wyjątki. Pliki mogą zostać uszkodzone, połączenie sieciowe, które było obecne może zostać zerwane, w systemie może zabraknąć pamięci lub biblioteki, których używasz i nad którymi nie masz kontroli mogą wyrzucić wyjątek.

Blok try-catch-finally

Blok try-catch-finally pozwala programowi na przechwycenie niepożądanych błędów i ich obsługę. Zawiera on trzy sekcje: jeden blok try, jeden lub wiele bloków catch i blok finally. Sekcja try jest wymagana i musi posiadać przynajmniej jedną sekcję catch lub finally
Sekcja try zawiera kod który może zgłosić wyjątek. W tej sekcji można zamieścić dowolną ilość deklaracji. Można również zagnieżdżać bloki try-catch-finally. Składnia sekcji try i catch:

try
{
// Statements that might throw an exception.
//...
}
catch [(ExceptionType [variable])]
{
//Statements to execute...
}

Jeżeli w sekcji try wystąpi wyjątek to program przegląda jego wszystkie sekcje catch w kolejności w jakiej zostały napisane w programie w poszukiwaniu obsługi typu wyjątku zgodnego ze zgłoszonym wyjątkiem. typ wyjątku jest zgodny jeżeli jest jednym z typów klasy typu wyjątkuExceptionType. Przykładowo klasa DivideByZeroException dziedziczy z klasy ArithmeticException która dziedziczy z klasy SystemException a at z kolei dziedzczy z klasy Exception. Jeżeli program wyrzuci wyjątek DivideByZeroException to obsłużyć go można w bloku catchobsługującym którykolwiek z tych typów wyjątków. Każdy wyjątek dziedziczy z klasy Exception, więc blok catch z typem wyjątku Exception przechwyci każdy zgłoszony wyjątek. 
Kiedy program znajdzie dopasowanie typu wyjątku w sekcji catch to wykona kod znajdujący się w odnalezionej sekcji i przeskoczy wszystkie pozostałe sekcje catch
Ponieważ program przeszukuje sekcje catch w kolejności w jakiej zostały napisane w kodzie to należy tak pisać kod aby lista sekcji catch rozpoczynała się od najbardziej szczegółowych wyjątków a kończyła się najbardziej ogólnymi. Przykładowo klasa FormatException dziedziczy zSystemException a ta dziedziczy z Exception więc blok try-catch powinien wyglądać tak:

try
{
//Statements...
}
catch (SystemException ex)
{
//Statements...
}
catch (FormatException ex)
{
//Statements...
}
catch (Exception ex)
{
//Statements...
}

Jeżeli w sekcji catch nie podamy typu wyjątku to blok ten przechwyci wszystkie rodzaje wyjątków np:

int quantity;
try
{
quantity = int.Parse(quantityTextBox.Text);
}
catch
{
MessageBox.Show("The quantity must be an integer.");
}

Jeżeli blok catch zawiera typ wyjątku i zmienną to zmienna jest typu tego wyjątku i posiada informacje na temat zgłaszanego wyjątku. Wszystkie klasy dziedziczące z Exception posiadają właściwość Message która zawiera informację tekstową o zgłaszanym wyjątku. 
Jeżeli blok catch zawiera typ wyjątku ale nie zawiera zmiennej to sekcja przechwyci wyjątek który pasuje do danego typu ale nie posiada szczegółowych informacji które byłyby zawarte w zmiennej. 
Sekcja finally jest wykonywana po zakończeniu wykonywania sekcji try i catch. jest ona wykonywana zawsze, nawet jeżeli program opuści sekcje try i catch z niżej wymienionych powodów:

  • Sekcja try zakończyła się sukcesem i żadna sekcja catch nie została wykonana.
  • Sekcja try wyrzuciła wyjątek i sekcja catch go obsłużyła.
  • Sekcja try wyrzuciła wyjątek i żadna sekcja catch go nie obsłużyła.
  • Sekcja try użyła wyrażenia return służącego do wyjścia z metody.
  • Sekcja catch użyła wyrażenia return służącego do wyjścia z metody.
  • Kod w sekcji catch wyrzucił wyjątek. 
    Jedynie kod w sekcji try jest chroniony przez blok try-catch-finally. jeżeli wyjątek zostanie zgłoszony w sekcji catch lub sekcji finally to nie zostanie obsłużony w tym bloku try-catch-finally. Dlatego bloki try-catch-finally mogą być zagnieżdżone.

Użycie wyrażenia using

Wyrażenie using zachowuje się jak sekwencja try-finally wywołująca metodę Dispose obiektu w sekcji finally np:

using (Pen pen = new Pen(Color.Red, 10))
{
// Use the pen to draw...
}

Jest ekwiwalentem kodu:

Pen pen;
try
{
pen = new Pen(Color.Red, 10);
// Use the pen to draw...
}
finally
{
if (pen != null) pen.Dispose();
}

Wyjątki nieobsłużone

Wyjątki nieobsłużone występują wtedy gdy program wyrzuci wyjątek i nie jest on obsłużony w żadnym bloku try-catch-finally. Może się to wydarzyć na dwa sposoby. Pierwszym jest gdy wyjątek zostaje wyrzucony we fragmencie kodu który nie znajduje się w bloku try-catch-finally. Drugi przypadek jest wtedy gdy wyjątek zostaje wyrzucony w sekcji try ale żadna sekcja catch nie obsłuży tego wyjątku. 
Kiedy program napotka na nieobsłużony wyjątek to kontrola przechodzi do góry stosu wywołań do metody która wywołała kod który wywołał wyjątek. Jeżeli metoda ta znajduje się w sekcji try bloku try-catch-finally to jego sekcja catch spróbuje obsłużyć wyjątek. W przeciwnym wypadku kontrola przechodzi o poziom wyżej w stosie wywołań. Kontrola przechodzi wyżej aż znajdzie sekcję catch która obsłuży zgłoszony wyjątek. W tym wypadku kod zostaje wywoływany dalej od tego momentu w którym wyjątek został obsłużony. Jeżeli wyjątek nie zostanie obsłużony w żadnym bloku try-catch-finally to program ulegnie awarii i pojawi się okno mówiące o awarii a w tym o wyjątku który został zgłoszony i stosie wywołań w programie prowadzącym do tego wyjątku. Okno posiada guziki umożliwiające zakończenie programu lub podjęcie próby kontynuacji działania programu. Przykład okna zgłaszającego nieobsłużony wyjątek. 
Alt text
Aby chronić program przed możliwymi wyjątkami, należy cały kod zawrzeć w bloku try-catch-finally.

Powszechnie stosowane wyjątki

.NET Framework definiuje setki klas wyjątków reprezentujących różne błędy. Poniżej rysunek przedstawiający najczęściej wykorzystywane wyjątki zdefiniowane w przestrzeni nazw System
Alt text
Użyteczne klasy wyjątków:

  • System.Exception - Klasa bazowa wszystkich innych klas wyjątków. Reprezentuje wyjątki na wysokim poziomie.
  • SystemException - Klasa bazowa wyjątków zdefiniowanych w przestrzeni nazw System.
  • ArgumentException - Jeden z argumentów metody jest niepoprawny.
  • ArgumentNullException - Jeden z argumentów ma niedozwolona wartość null.
  • ArgumentOutOfRangeException - Argument jest poza dozwolonym zakresem.
  • ArithmeticException - Błąd arytmetyczny lub błąd przy konwertowaniu lub rzutowaniu typów.
  • DivideByZeroException - Błąd dzielenia przez zero.
  • OverflowException - Operacja arytmetyczna, konwersja lub rzutowanie wykonane w bloku checked zwraca błąd przepełnienia.
  • NotFiniteNumberException - Operacja zmiennoprzecinkowa zwraca błąd, że wynik jest nieskończonością lub NaN.
  • ArrayTypeMismatchException - Program próbuje wstawić zły typ do tablicy.
  • FormatException - Argument ma niepoprawny format.
  • IndexOutOfRangeException - indeks tablicy z poza jej zakresu.
  • InvalidCastException - Niepoprawne rzutowanie lub konwersja.
  • InvalidOperationException - Wywołanie metody jest nieprawidłowe dla obecnego stanu obiektu.
  • IO.IOException - Błąd wejścia/wyjścia.
  • IO.DirectoryNotFoundException - Cześć ścieżki nie została odnaleziona.
  • IO.DriveNotFoundException - Dysk lub zasób sieciowy niedostępny.
  • IO.EndOfStreamException - Program próbował odczytać poza końcem strumienia.
  • IO.FileLoadException - Program próbował załadować plik który istnieje ale jest niedostępny.
  • IO.FileNotFoundException - Plik nie został odnaleziony.
  • IO.PathTooLongException - Ścieżka lub nazwa pliku jest za długa.
  • NotImplementedException - Funkcja niezaimplementowana.
  • NotSupportedException - Program próbował wywołać metodę, która nie jest wspierana. Możesz wyrzucić ten wyjątek, aby wskazać metodę, która została usunięta w ostatnich wersjach biblioteki.
  • NullReferenceException - Program próbował dostać się do obiektu który ma wartość null.
  • OutOfMemoryException - Za mało pamięci aby program mógł kontynuować.
  • RankException - Program wstawił do metody tablicę z niewłaściwą liczbą wymiarów.
  • Security.SecurityException - Błąd bezpieczeństwa.
  • Security.VerificationException - Polityka bezpieczeństwa wymaga kodu by używał typów bezpiecznych, a kod nie może być zweryfikowany jako bezpiecznego typu.
  • UnauthorizedAccessException - Dostęp zabroniony z powodu błędu wejścia/wyjścia lub bezpieczeństwa.

Wyjątki SQL

SQL Server używa klasy System.Data.SqlClient.SqlException reprezentującej wszyskie błędy i wyjątki. Można użyć właściwości tej klasy aby dowiedzieć się co dokładnie się wydarzyło. Najbardziej użyteczne właściwości klasy SqlException:

  • Class - Numer od 0 do 25 oznaczający typ błędu. Błędy powyżej 20 są fatalne i oznaczają zamknięcie połączenia do bazy danych. Błędy poniżej 10 są klasy informacyjnej. Błędy z zakresu 11-16 mogą być naprawione przez użytkownika. Błąd 17 oznacza, że serwer korzysta z zasobów które nie są konfigurowalne i błąd może rozwiązać administrator. 18 to błąd niefatalny wewnątrz oprogramowania. 19 oznacza, że SQL Server przekroczył limit zasobów. 20: problem w wyrażeniu wywołanym przez aktualny proces. 21: problem dotyczący wszystkich procesów bazy danych. 22: indeks lub tabela są uszkodzone. 23: baza danych jest uznana za uszkodzoną. 24: problem sprzętowy. 25: błąd systemowy.
  • LineNumber - numer błędnej linii w komendzie T-SQL lub procedurze składowanej.
  • Message - Informacja opisująca problem.
  • Number - Numer błędu.
  • Procedure - Nazwa procedury wywołującej błąd. 
    Klasa System.Data.Common.DbException jest klasa bazową klasy SqlException i jeszcze trzech innych klas reprezentujących błędy dla innych typów baz danych, są to:
  • System.Data.Odbc.OdbcException - Klasa błędów baz ODBC.
  • System.Data.OleDb.OleDbException - Klasa błędów baz OleDb.
  • System.Data.OracleClient.OracleException - Klasa błędów baz danych Oracle
    Wszystkie te klasy zawierają właściwość message która zawiera informacja opisująca problem.

Overflow Exception

Domyślnie program nie wyrzuca wyjątku jeżeli operacja arytmetyczna powoduje przepełnienie zakresu liczby całkowitej. Jeżeli operandy są całkowite lub dziesiętne to program obcina wartość wynikową. 
Aby program zwrócił wyjątek OverflowException należy użyć bloku checked lub ustawić zaawansowaną opcję budowania. 
Program nie wyrzuca wyjątku również dla operacji zmiennoprzecinkowej która powoduje przepełnienie lub zwraca specialną wartość NaN (not a number). 
Typy zmiennoprzecinkowe posiadają właściwości statyczne PositiveInfinityNegativeInfinity i NaN. Można porównać wartość z PositiveInfinity lub NegativeInfinity ale nie można porównać z NaN
Zamiast porównywania można użyć specjalnych metod sprawdzających:

  • IsInfinity - zwraca true jeżeli wartość jest PositiveInfinity lub NegativeInfinity.
  • IsNegativeInfinity - zwraca true jeżeli wartość jest NegativeInfinity.
  • IsPositiveInfinity - zwraca true jeżeli wartość jest PositiveInfinity.
  • IsNaN - zwraca true jeżeli wartość jest NaN.

Właściwości wyjątków

Klasa System.Exception jest przodkiem wszystkich klas wyjątków i definiuje kilka właściwości mówiących programowi o tym co się wydarzyło i jaki problem wystąpił, są to właściwości:

  • Data - Kolekcja par klucz/wartość dająca dodatkowe informacje o wyjątku.
  • HelpLink - Łącze do pliku pomocy powiązanego z wyjątkiem.
  • HResult - Numeryczny kod wyjątku.
  • InnerException - Wyjątek wewnętrzny posiadający więcej informacji o błędzie który wystąpił.
  • Message - Wiadomość opisująca wyjątek.
  • Source - Nazwa aplikacji lub obiektu zgłaszającego wyjątek.
  • StackTrace - Tekstowa reprezentacja stosu wywołań prowadzącego do wystąpienia wyjątku.
  • TargetSite - Informacje o metodzie zgłaszającej wyjątek. 
    Właściwość Message klasy Exception nie zawiera wystarczająco dokładnych informacji aby wyświetlić je użytkownikowi lecz czasem jest wystarczająca dla programisty w czasie usuwania błędów. Metoda ToString klasy Exception posiada więcej użytecznych dla programisty informacji. Zawiera nazwę klasy wyjątku, właściwość Message oraz stos wywołań. Przykładowo metoda ToString klasy OverflowException może zwrócić następujący tekst:
System.OverflowException: Arithmetic operation resulted in an overflow.
at OrderProcessor.OrderForm.CalculateSubtotal() in
d:\Support\Apps\OrderProcessor\OrderForm.cs:line 166

Wyrzucanie i ponowne wyrzucanie wyjątków

Metoda może używać bloku try-catch-finally do przechwytywania wyjątków. Jeżeli ta metoda posiada interakcję z użytkownikiem to prawdopodobnie wyświetli wiadomość o wystąpieniu błędu. 
Jednakże bardzo często metody nie powinny mieć interakcji z użytkownikiem. Zamiast tego powinny wyrzucać wyjątki do ich właścicieli aby powiedzieć wywołującemu kodowi, że wydarzył się błąd. Ten kod może wyświetlić informację użytkownikowi lub może poradzić sobie z problemem, nie przeszkadzając użytkownikowi.

Użycie wyjątków i zwracanie wartości

Metoda może wykonywać jakąś akcję po czym zwrócić informację do wywołującego ją kodu poprzez wartość zwracaną lub parametr wyjściowy. Wyjątek daje metodzie kolejną możliwość komunikacji z wywołującym ją kodem. Wyjątek mówi programowi, że coś wyjątkowego się wydarzyło i, że metoda mogła nie zakończyć powierzonego jej zadania. 
Zdania na temat tego, kiedy metody powinny zwracać wartość za pomocą wartości lub parametru oraz kiedy powinny zwracać informacje za pomocą wyjątków, są podzielone. Większość programistów zgadza się z tym, że informacje o normalnym statusie powinny być zwracane za pomocą wartości a wyjątki powinny być używane dla błędów. 
Najlepszą metodą aby zdecydować czy użyć wyjątku jest zadanie pytania czy wywołujący kod może pozwolić na ignorowanie zwracanego przez metodę statusu. Jeżeli metoda zwraca informację za pomocą wartości to wywołujący kod może ją zignorować. Jeżeli metoda zwraca wyjątek to wywołujący kod musi zawierać blok try-catch aby obsłużyć jawnie wyjątek. Na przykład, rozważmy następującą metodę, która zwraca silnię liczby:

// Calculate a number's factorial.
private long Factorial(long n)
{
// Make sure n >= 0.
if (n < 0) return0;
checked
{
try
{
long result = 1;
for (long i = 2; i <= n; i++) result *= i;
returnresult;
}
catch
{
return0;
}
}
}

Jeśli parametr jest mniejszy niż zero lub jeśli obliczenie powoduje przepełnienie liczby całkowitej to metoda zwraca wartość 0. 
Istnieją dwa problemy z takim podejściem. Po pierwsze, kod wywołujący może zignorować błąd i traktować wartość 0 jako wartość silni dając nieprawidłowe wyniki. Jeżeli wartość jest używana w złożonych obliczeniach to błąd może wystąpić w trakcie obliczeń co może być trudne do zlokalizowania i naprawienia. Drugim problemem jest to, że kod wywołujący nie może powiedzieć, co poszło nie tak. Można zwracać różne wartości, np 0 dla przepełnienia zakresu liczby całkowitej i -1 dla wartości parametru mniejszego od 0, lecz nawet używając różnych kodów zwracających status nie możemy sprawdzić czy kod nie został zignorowany. Lepszym rozwiązaniem jest wyrzucenie odpowiedniego wyjątku. poniższa wersja metody jest lepszym rozwiązaniem:

// Calculate a number's factorial.
private long Factorial(long n)
{
// Make sure n >= 0.
if (n < 0) throw new ArgumentOutOfRangeException(
"n", "The number n must be at least 0 to calculate n!");
checked
{
long result = 1;
for (long i = 2; i <= n; i++) result *= i;
returnresult;
}
}

Jeżeli parametr jest mniejszy niż zero to kod wyrzuci wyjątek ArgumentOutOfRangeException. Ponieważ obliczenia są zagnieżdżone w bloku checked to jeżeli nastąpi przepełnienie zakresu liczby całkowitej to zostanie wyrzucony wyjątek OverflowException..

Przechwytywanie, wyrzucanie i ponowne wyrzucanie wyjątków

Jeżeli metoda wyjaśnia dlaczego wyjątek wystąpił a nie raportuje, że wystąpił lub jeżeli dodaje dodatkowe informacje sprawiając, że wyjątek jest bardziej specyficzny to metoda powinna przechwycić wyjątek i wyrzucić nowy zawierający nowe informacje. 
Jeżeli wyrzucamy wyjątek w ten sposób to dobrą praktyką jest zawarcie oryginalnego wyjątku we właściwości InnerException wyrzucanego wyjątku. 
Jeżeli metoda dodaje nowe informacje do wyjątku to zwykle nie powinna go przechwytywać lecz pozwolić na przejście wyżej w drzewie wywołań. Czasami chcemy jednak przechwycić taki wyjątek w celu wykonania pewnej “prywatnej” akcji jak np logowanie. W takim przypadku można wykonać prywatny kod obsługujący wyjątek i ponownie go wyrzucić tak aby mógł zostać przechwycony wyżej w drzewie wywołań. 
Ponowne wyrzucenie aktualnego wyjątku polega na użyciu deklaracji throw bez podania wyjątku. Poniższy kod demonstruje tą technikę:

try
{
// Do something dangerous.
//...
}
catch (Exception)
{
// Log the error.
//...
// Re-throw the exception.
throw;
}

Można też wyrzucić ponownie wyjątek z jawą jego deklaracją lecz powoduje to zresetowanie drzewa wywołań do aktualnej lokalizacji co może zmylić programistę który próbuje naprawić błąd. Poniższy kod demonstruje tą technikę:

try
{
// Do something dangerous.
//...
}
catch (Exception ex)
{
// Log the error.
//...
// Re-throw the exception.
throw ex;
}

Istnieją jednak powody dla których chcemy ponownie wyrzucać wyjątki i resetować drzewo wywołań, np kiedy nie chcemy pokazywać detali zamkniętej biblioteki i jej prywatnych metod. W takim wypadku powinniśmy obsłużyć wyjątek w metodzie publicznej i wtedy wyrzucić go ponownie tak aby drzewo wywołań zaczynało się w tej metodzie publicznej.

Tworzenie niestandardowych wyjątków

Czasami nie możemy znaleźć w .NET Framework odpowiedniej klasy wyjątku która odpowiada naszym potrzebom. W takim wypadku możemy utworzyć własną klasę wyjątku. 
Klasa wyjątku powinna dziedziczyć z klasy Exception i jej nazwa powinna być zakończona słowem Exception
Aby klasa była jak najbardziej użyteczna powinniśmy nadać jej konstruktor pasujący do zdefiniowanego w klasie Exception. Poniższy kod prezentuje klasę InvalidException napisaną wg powyższych zaleceń:

[Serializable]
class InvalidProjectionException : Exception
{
public InvalidProjectionException()
: base() { }
public InvalidProjectionException(string message)
: base(message) { }
public InvalidProjectionException(string message,
Exception innerException)

: base(message, innerException) { }
protected InvalidProjectionException(SerializationInfo info,
StreamingContext context)

: base(info, context) { }
}

Każdy z konstruktorów przekazuje parametry do konstruktora klasy bazowej. Typy SerializationInfo i StreamingContext są zdefiniowane w przestrzeni nazw System.Runtime.Serialization
Aby wyjątki mogły być przekazywane poza granice domeny aplikacji należy umożliwić im serializację poprzez dodanie atrybutu Serialize.

Użycie Debug.Assert

Klasa System.Diagnostics.Debug wprowadza metodę Assert która jest często używana do sprawdzania poprawności danych wstawianych do metody. Metoda Assert pobiera wartość bool jako pierwszy parametr i wyrzuca wyjątek jeżeli ta wartość to false. Inne parametry pozwalaja na określenie i wyświetlenie informacji gdzie metoda zawiodła. 
W kompilacji debug metoda Assert zatrzymuje wykonywanie i wyświetla drzewo wywołań. W kompilacji release program pomija wywołanie tej metody. 
Metody Assert można użyć do zweryfikowania czy wprowadzone dane mają sens. Przykładowo metoda PrintInvoice pobiera na wejściu tablicę obiektów klasy OrderItem nazwaną items i wyświetla faktury dla tych obiektów. Metoda ta może zaczynać się od użycia metody Assertsprawdzającej czy tablica wejściowa jest mniejsza niż 100:

Debug.Assert(items.Length <= 100)

Podsumowanie

Delegaty

  • Delegata jest tpem reprezentującym rodzaj metody. Definiuje parametry metody i typ zwracany.
  • Zazwyczaj nazwa delegaty nazywana jest z końcówką Delegate lub Callback.
  • Można użyć operatorów + i - do kombinowania zmiennych delegat np del3 = del1 + del2 to delegata del3 wykona metody z obu delegat.
  • Jeżeli zmienna delegaty odwołuje się do metody instancji obiektu to wykonuje się na obiekcie do którego została przypisana.
  • Kowariancja pozwala metodzie zwrócić wartość z podklasy typu który został zadeklarowany jako zwracany przez delegatę.
  • Kontrawariancja pozwala metodzie przyjąć parametr z klasy bazowej typu który był oczekiwany przez delegatę.
  • .NET Framework definiuje dwa wbudowane typy delegat Action i Func np:
publicdelegatevoid Action<in T1, in T2>(T1 arg1, T2 arg2)
publicdelegate TResult Func<in T1, in T2, out TResult>
(T1 arg1, T2 arg2)
  • Metoda anonimowa to metoda bez nazwy np:
Func<float, float> function = delegate(float x) { return x * x; };
  • Wyrażenia lambda pozwalają na tworzenie metod anonimowych w zwięzły sposób np:
Action note1 = () => MessageBox.Show("Hi");
Action<string> note2 = message => MessageBox.Show(message);
Action<string> note3 = (message) => MessageBox.Show(message);
Action<string> note4 = (string message) => MessageBox.Show(message);
Func<float, float> square = (float x) => x * x;

Zdarzenia

  • Zdarzenia posiadają wydawców i subskrybentów.
  • Do definicji zdarzeń używamy delegat w następujący sposób:
publicdelegatevoidOverdrawnEventHandler();
publicevent OverdrawnEventHandler Overdrawn;
  • Można użyć zdefiniowanej delegaty Action do utworzenia zdarzenia np:
publicevent Action Overdrawn;
  • Zaleca się aby pierwszym parametrem zdarzenia był obiekt nadawcy a następne parametry dające więcej informacji o zdarzeniu. Obiekt nadawcy powinien dziedziczyć z klasy EventArgs i mieć nazwę zakończona Args.
  • Można użyć zdefiniowanej delegaty EventHandler aby zdefiniować zdarzenie pobierające obiekt nazwany sender jako pierwszy argument i obiekt danych zdarzenia jako drugi np:
publicevent EventHandler<OverdrawnEventArgs> Overdrawn;
  • Zgłaszaj zdarzenia jak w poniższym kodzie:
if (EventName != null) EventName(arguments...);
  • Klasy nie mogą dziedziczyć zdarzen. Aby klasy mogły zgłaszać zdarzenia klas bazowych używa się metod nazywanych w stylu OnEventName zgłaszających zdarzenie.
  • Program używa operatorów += i -= do zapisywania się i wypisywania z subskrypcji zdarzeń.
  • Jeżeli program zapiszę się na subskrypcję zdarzenia więcej niż raz to obsługa zdarzenia zostanie wykonana wiele razy.
  • Jeżeli program wypisze się z subskrypcji zdarzenia nawet jeżeli nie był na nią wcześniej zapisany to nic się nie wydarzy.

Wyjątki

  • Obsługa wyjątków jest procesem ochrony programu od nieoczekiwanych błędów.
  • Blok try-catch-finally musi mieć co najmniej jedną sekcję catch lub finally.
  • Sekcja finally jest zawsze wykonywana.
  • Najbardziej specyficzna sekcja catch powinna pojawić się jako pierwsza w bloku try-catch-finally.
  • Jeżeli nie chcemy robić nic ze zgłoszonym wyjątkiem to możemy ominąć zmienną wyjątku pozostawiając tylko jego typ w sekcji catch.
  • Klasa Exception jest klasą bazową wszystkich wyjątków i przechwytuje wszystkie rodzaje wyjątków.
  • Sekcja catch bez zdefiniowanego typu wyjątku przechwytuje wszystkie wyjątki.
  • Wyrażenie using jest równoważne z blokiem try-catch-finally w którym w sekcji finally obiekt jest zbyty.
  • Wyjątek przechodzi w górę drzewa wywołan aż napotka blok catch który go obsłuży.
  • Właściwość Message wyjątku posiada informacje o wyjatku. Metoda ToString zawiera właściwość Message oraz dodatkowe opcję wraz z drzewem wywołań.
  • Klasa SqlException reprezentuje wyjątki serwera SQL Server.
  • Ponowne wyrzucenie wyjątku polega na użyciu instrukcji throw bez podania obiektu wyjątku. Można również wyrzucić ponownie wyjątek z obiektem lecz powoduje to zresetowania drzewa wywołań.