Saturday 20 May 2006

Chwila refleksji

Nie, nie zmieniłem pomysłu na prowadzenie bloga, znów będzie o C#. Przestrzenie nazw System.Reflection i System.Reflection.Emit udostępniają zestaw bardzo interesujących klas, służących do dynamicznego analizowania i generowania kodu. Pozwala to np. stworzyć uniwersalne narzędzia, służące do serializowania dowolnych, nieznanych wcześniej typów, czy też pisanie własnych kompilatorów i analizatorów kodu.


Mechanizm refleksji, chociaż potężny, jest niestety dość powolny, w związku z tym wskazane jest uzupełnianie go dynamicznym generowaniem kodu. W ten sposób wystarczy raz tylko wykonać kosztowną analizę obiektu, a potem można korzystać z zebranych wcześniej informacji, wywołując wygenerowane za pierwszym razem metody i klasy. Oczywiście, wyprodukowany w ten sposób kod można zapisać na dysku w formie zwyczajnych assembly, do późniejszego wykonania.


Korzystanie z przestrzeni nazw System.Reflection.Emit wymaga niestety zapoznania się, chociaż pobieżnego, z CIL. Na szczęście SDK od Microsoftu zawiera dwa bardzo przydatne narzędzia. peverify.exe analizuje pliki assembly, zgłaszając wszelkie nieprawidłowości w ich strukturze, jak np. niewłaściwe nazwy obiektów, niezgodności typów, brak inicjalizacji instancji obiektów itp. Jest to istotne, ponieważ CLR bywa nadmiernie wyrozumiały dla licznych błędów, które można popełnić generując kod z pominięciem kompilatora. ildasm.exe oraz jego odpowiednik w mono, czyli monodis, pozwalają podejrzeć kod znajdujący się w plikach assembly, co z kolei umożliwia generowanie własnego kodu per analogiam. Wystarczy napisać kod w C#, skompilować, i sprawdzić, jak efekt wygląda w CIL.


Warto też pamiętać o rozszerzeniach mechanizmu refleksji, które powstała w trakcie rozwoju mono. Są to Cecil, pozwalający na inspekcję kodu bez ładowania go do środowiska CLR oraz CodeGeneration, który generuje CIL używając składni opartej na C#.


Na koniec parę ciekawostek odnośnie CLR. Na początek: interfejsy nie istnieją. CIL modeluje je za pomocą klas abstrakcyjnych. To pociąga za sobą (sic!) wielokrotne dziedziczenie. Dalej - nie istnieją też klasy. Są reprezentowane jako powiązane ze sobą struktury danych i funkcje. Każda metoda oczywiście potrzebuje wskaźnika this, który jest przekazywany do niej jako parametr numer 0 (kojarzy się z C++ ?). Pozwala to na takie operacje, jak wywołanie na obiekcie metod, których on nie posiada (poprzez wrzucenie go na stos parametrów metody innego obiektu - na szczęście, jeśli odpowiednie rzutowanie nie byłoby możliwe, wykonanie zakończy się wyjątkiem) lub uruchamianie metod instancji obiektu bez uprzedniego stworzenia go (wywołanie metody, kiedy stos nie zawiera odpowiedniego wskaźnika do obiektu - CLR zachowuje się wtedy, jakby obiekt istniał, ale wszelkie pola ustawione są na wartości domyślne dla swojego typu). Użyteczna dla wielu zastosowań może być możliwość dostępu do metod i pól prywatnych.

Friday 12 May 2006

Zwalnianie zasobów (C#)

Jeśli dopiero co zaczynasz korzystać z platformy .Net, a wcześniej nie pisałeś w językach z automatycznym zarządzaniem pamięcią, to na pewno wcześniej czy później popełnisz ten sam błąd co ja. Wyobraź sobie, że pisana przez Ciebie klasa wykorzystuje jakieś zasoby systemowe niezarządzane przez Garbage Collector,. Jak je zwolnić? Naturalne wydaje się zrobienie tego w destruktorze, prawda? Przecież w C# ma on nawet tą samą znaną składnię, ~DummyClass(), co z tego, że nazywa się finalizer?


Haczyki są trzy. Pierwszy polega na tym, że finalizer zostanie wywołany w momencie sprzątania obiektu przez Garbage Collector, a to może stać się długo po usunięciu ostatniego do niego odwołania. Drugi - finalizer nie zostanie wywołany w ogóle, jeśli cała aplikacja jest właśnie wyłączana. Trzeci - obiekty posiadające finalizer śledzone odrębnie od nie posiadających go i usuwane z pamięci później. Całe to zachowanie wynika z faktu, że w środowisku z zarządzaną pamięcią klasyczna implementacja destruktora jest bardzo niewydajna, jeśli nie w ogóle niemożliwa.


Jak więc zwolnić takie zasoby? Należy zaimplementować interfejs IDisposable i pamiętać, by go używać, korzystając z implementujących go klas. Służy do tego specjalne słowo kluczowe using, gwarantujące, że na "opakowanym" w nie obiekcie zostanie wywołane Dispose(), niezależnie od tego,czy kod w klauzuli zakończy się normalnie czy też wyjątkiem. Używa się tego konstruktu tak:


using(DummyObject something = new DummyObject())
{
//jakiś kod, korzystający z obiektu something
}

Gdy korzystamy z obiektów implementujących IDisposable, a niemożliwe jest ujęcie ich w ramy using, należy je zwalniać w metodzie Dispose() korzystającego z nich obiektu.


I jeszcze przykładowe zastosowanie IDisposable, bo w nim też łatwo narobić błędów:


/// <summary></summary>
public class DisposableObject : IDisposable
{
#region implementacja IDisposable
/// <summary>
/// Finalizer, wywołuje Dispose,
/// <strong>nie nalezy na nim polegać!</strong>.
/// </summary>
~DisposableObject()
{
Dispose(false);
}

/// <summary>
/// Zwalnia używane zasoby. Wyrejestrowuje finalizer,
/// by niepotrzebnie nie opóźniać zwalniania pamięci.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}

/// <summary>
/// Zwalnia używane zasoby.
/// </summary>
/// <param name="disposing">czy mamy czas na dokładne sprzątanie?</param>
protected virtual void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
//tutaj trafia to, co _powinniśmy_ posprzątać
}
//a tutaj to, co _musimy_ posprzątać
}
disposed = true;
}

/// <summary>Aby tylko raz zwalniac zasoby.
/// (Dispose może zostać wywołane wielokrotnie.)</summary>
protected bool disposed = false;
#endregion
}

Jeśli nasza klasa dziedziczy z innej, już implementującej IDisposable, to wystarczy ograniczyć się do pokrycia metody protected virtual void Dispose(bool disposing), pamiętając, by wywołać w niej base:Dispose(disposing).


Na koniec ważna uwaga odnośnie kooperacji C++ i .Net. W zarządzanym C++ za pomocą składni ~DummyObject() definiuje się metodę Dispose()! Analogicznie, delete someObject oznacza wywołanie na nim metody Dispose(). Finalizer można zaimplementować poprzez !DummyObject().

Thursday 11 May 2006

Globalna obsługa wyjątków w .Net

Mały,ale użyteczny kawałek kodu:


AppDomain.CurrentDomain.UnhandledException +=
delegate(object o, UnhandledExceptionEventArgs args)
{Console.WriteLine(args.ExceptionObject.ToString());};

Do czego to służy? Otóż zdarzenie UnhandledException wywoływane jest przy każdym nieobsłużonym przez aplikację wyjątku. Można to wykorzystać by zapisać błąd w dzienniku systemowym czy automatycznie zgłosić go obsłudze klienta / developerom. Nie można w ten sposób usunąć wyjątku - zdarzenie jest wywoływane już po 'wysypaniu' się wątku, w którym wystąpił wyjątek. Jeśli wolisz gotowe rozwiązania, polecam Unhandled Exception Manager z codeproject.