Artykuł

maj 06 2012
0

C# - Interfejsy które warto znać

W obecnym czasie, z bardzo rozwiniętymi środowiskami programistycznymi, takimi jak choćby Visual Studio, czy też Eclipse, bardzo łatwo jest rozpocząć swoją przygodę z programowaniem, które w tym przypadku czasem bardziej przypomina zabawę z klockami, niż rzeczywiste klepanie kodu.

Jednak nastawiając się na bezmyślne składanie klocków, szybko można obudzić się z przysłowiową ręką w nocniku, w sytuacji gdy zajdzie potrzeba modyfikacji lub rozszerzenia tak radośnie tworzonego kodu, a warto zaznaczyć, że nieumiejętnie sklejony kod może wymagać całkowitej reorganizacji, czy wręcz nawet przepisania w sytuacji gdy zajdzie potrzeba rozszerzenia bieżących funkcjonalności oprogramowania.

Jak sobie zatem z wszystkim poradzić? Przede wszystkim warto sięgnąć po wzorce projektowe, o których na łamach Alt Control Delete już nie raz wspominałem. Pomocne będą również tytułowe interfejsy przygotowane przez twórców języka, które jak dobrze wiadomo leżą u podstaw Polimorfizmu, a odpowiednio zastosowane mają naprawdę spory sens i możliwości - znacznie większe niż by się z pozoru wydawało.

Dziś postaram się zatem przedstawić Wam kilka interfejsów, które mnie osobiście szczególnie okazały się przydatne.

IDisposable - zwalnianie zaalokowanych zasobów

Choć Garbage Collector w C# prędzej czy później zwolni użytkowane przez nas zasoby, czasem warto mieć pewność, że stanie się to w chwili, w której właśnie my tego oczekujemy. Planowane sprzątanie, to również świetny sposób na szybką i optymalną pracę naszej aplikacji, dlatego czasem warto poświęcić chwilę więcej i skupić się również na takim elemencie.

Żeby zrealizować ten cel, należy odpowiednio zaimplementować interfejs IDisposable. Zalecana przez Microsoft implementacja wygląda mniej więcej tak:

class CMyClass : IDisposable
{
    private bool m_bIsDisposed = false;

    public CMyClass()
    {
    }

    // Metody operacyjne klasy...

    public void Dispose()
    {
        Dispose(true);
        GC.SuppressFinalize(this);
    }

    ~CMyClass()
    {
        Dispose(false);
    }

    protected virtual void Dispose(bool bDisposing)
    {
        if (m_bIsDisposed)
        {
            return;
        }
        if (bDisposing)
        {
            // Zwalnianie zasobów zarządzanych
        }
        // Zwalnianie zasobów nierządzanych
        m_bIsDisposed = true;
    }
}

Klasa która ma mieć możliwość ładnego i szybkie sprzątania musi implementować interfejs IDisposable. Dodatkowo dodałem do niej pole klasy przechowujące flagę determinującą czy proces zwalniania zasobów został już wcześniej wywołany (3). Mamy tu również oczywiście konstruktor (5-7) oraz miejsce na kod, który realizować będzie postawione przed tą klasą zadania.

Kluczowym elementem implementacji jest metoda Dispose (11-15) wymuszona przez interfejs. W jej wnętrzu wywołujemy przeciążoną metodę Dispose z flagą która steruje zwalnianiem zasobów zarządzanych oraz blokujemy standardowe działanie GarbageCollectora. Jest on nam w tym momencie niepotrzebny, ponieważ sami ogarniamy kwestię czyszczenia, dlatego ważne jest też, by zrobić to dobrze.

W liniach 17-20, widoczna jest implementacja destruktora, który wywołuje przeciążoną wersję metody Dispose wyłączając jednak zwalnianie zasobów zarządzanych.

Ostatnim etapem jest przeciążona metoda Dispose (22-34), w której wykonywane są kluczowe operacje sprzątające. Na początku (24-27 sprawdzamy czy metoda nie została już wcześniej uruchomiona. Jeśli tak, to przerywamy przetwarzanie.

W kolejnym kroku, zwalniamy zasoby zarządzane (28-31), a następnie pozostawiamy miejsce na zwolnienie tych niezarządzanych (32).

Po całym sprzątaniu, ustawiamy flagę m_bIsDisposed, żeby zapobiec ewentualnej ponownej próbie sprzątania, o której pisałem wyżej.

Metodę Dispose na tak spreparowanej klasie możemy wywołać samodzielnie, bądź też kod takiej klasy użyć w ramach bloku using. Dzięki takiemu podejściu, metoda Dispose zostanie wywołana zawsze, niezależnie czy kod wykona się prawidłowo, czy też będzie błąd.

using (CMyClass oMyClass = new CMyClass())
{
    // operacje na klasie CMyClass...
}

MSDN: link

IEquatable - porównywanie obiektów tej samej klasy

Ostatnimi czasy tworząc aplikację na Windows Phone, stanąłem przed problemem sprawdzenia, czy dodawany obiekt do ObservableCollection nie znajduje się już w tej kolekcji. Generalnie mogę zdradzić, że chodziło o listę o kontaktów. Każdy kontakt cechował się swoją nazwą oraz danymi kontaktowymi. Jako dane kontaktowe mógł posłużyć adres email, bądź też numer telefonu, w zależności od miejsca, w którym klasa była użyta w mojej aplikacji. O unikatowości wybranego obiektu świadczy para pól nazwa i dane kontaktowe.

Do weryfikacji, czy obiekt znajduje się w kolekcji, korzystałem z metody Contains użytej na kolekcji, jednak domyślna implementacja klasy nie radziła sobie z porównywaniem. Lekiem na całe zło okazała się właśnie implementacja tytułowego interfejsu, która wygląda mniej więcej tak jak na poniższym listingu:

public class CContactData : IEquatable<CContactData>
{
    private string m_sContactName = string.Empty;
    private object m_oContactData = null;

    public CContactData(string sContactName, object oContactData)
    {
        m_sContactName = sContactName;
        m_oContactData = oContactData;
    }

    public string ContactName
    {
        get { return m_sContactName; }
    }

    public object ContactData
    {
        get { return m_oContactData; }
    }

    public bool Equals(CContactData oContactData)
    {
        return (oContactData.ContactName == ContactName) &&
            (oContactData.ContactData.ToString() == ContactData.ToString());
    }
}

W przypadku tego interfejsu, musimy podać typ, co też czynimy już w pierwszym wierszu - klasycznie w ostrych nawiasach. W liniach 3-20 widzimy prostą implementację klasy z dwoma polami, właściwościami oraz konstruktorem.

Nas najbardziej interesuje metoda Equals (22-26) wymuszona przez interfejs. W jej wnętrzu porównujemy właściwości lokalnej kopii obiektu, z kopią przekazaną do porównania. Jeśli nazwy kontaktów oraz adresy email/numery telefonu są takie same, to w takim przypadku stwierdzamy, że oba obiekty są identyczne.

MSDN: link

IValueConveter - wyświetlanie danych na kontrolkach w WPF i Silverlight

Nie zawsze jest tak, że dane uporządkowane w postaci logicznej w obiekcie, będą się wyświetlać tak samo dobrze na kontrolce bez żadnej naszej ingerencji. Wyobraźmy sobie, że mamy obiekt w którym przechowujemy informacje o prognozie pogody. Dla wygody oraz skalowalności temperaturę przechowujemy jako liczbę, a za wyświetlanie tej wartości odpowiedzialne będzie GUI. W takim przypadku w warstwie prezentacji konieczne jest wzbogacenie wartości liczbowej o dodatkowe opisowe informacje. Cel ten możemy zrealizować implementując właśnie interfejs IValueConverter, który do naszej temperatury doda np. znacznik stopni Celsjusza. Spójrzmy na przykładowy kod:

class CIntegerToTemperatureValueConverter : IValueConverter
{
	public object Convert(object oValue, Type oTargetType, object 
		oParameter, System.Globalization.CultureInfo oCulture)
	{
		return oValue.ToString() + "°C";
	}

	public object ConvertBack(object oValue, Type oTargetType, object 
		oParameter, System.Globalization.CultureInfo oCulture)
	{
		throw new NotImplementedException();
	}
}

Interfejs ten wymusza implementację dwóch metod. W większości przypadków programiści wypełniają tylko tą pierwszą, ponieważ jest używana ona do prezentacji danych logicznych w warstwie GUI. W sytuacji gdy bindowanie jest obustronne i dane mogą zostać zmienione również w warstwie GUI, konieczna jest również implementacja metody ConvertBack, która będzie w stanie przekształcić wartość widoczną na warstwie prezentacji, do jej postaci logicznej.

MSDN: link

INotifyPropertyChanged - powiadamia klientów o zmianie wartości właściwości

Jeśli kiedykolwiek tworzyłeś aplikację GUI przy użyciu .Net, z pewnością spotkałeś się z terminem bindowania danych. Najogólniej w świecie chodzi w tym momencie o inteligentne podpięcie źródła danych (np. kolekcji obiektów) do odpowiedniej kontrolki, która będzie w stanie te dane wyświetlić.

Istotą bindowania jest również to, że wszystkie elementy, które korzystają z określonych danych powinny reagować na zachodzące w logice zmiany. Aby ten warunek był spełniony, konieczne będzie umieszczenie wszystkich obiektów w kolekcji ObservableColleciton oraz implementacja interfejsu INotifyPropertyChanged dla obiektów które się tam znajdą.

Dla zobrazowaniu przykładu możemy wykorzystać klasę CContactData, dla której przyjmiemy założenie, że jej właściwość ContactData może ulec zmianie w trakcie pracy aplikacji. Spójrzmy zatem na nową wersję klasy implementującą nasz interfejs:

public class CContactData : IEquatable<CContactData>, INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    private string m_sContactName = string.Empty;
    private object m_oContactData = null;

    public CContactData(string sContactName, object oContactData)
    {
        m_sContactName = sContactName;
        m_oContactData = oContactData;
    }

    public string ContactName
    {
        get { return m_sContactName; }
    }

    public object ContactData
    {
        get { return m_oContactData; }
        set
        {
            if (value != ContactData)
            {
                m_oContactData = value;
                NotifyPropertyChanged("ContactData");
            }
        }
    }

    public bool Equals(CContactData oContactData)
    {
        return (oContactData.ContactName == ContactName) &&
            (oContactData.ContactData.ToString() == ContactData.ToString());
    }

    private void NotifyPropertyChanged(string sPropertyName)
    {
        if (null != PropertyChanged)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(sPropertyName));
        }
    }
}

W linii 1 dodajemy nowy interfejs, a w linii 3 (już wewnątrz klasy) zdarzenie obsługujące zmianę wartości właściwości. Następnie do właściwości ContactData dodajemy Seta (22-29), w którym sprawdzamy czy nowa wartość jest różna od obecnej. Jeśli tak, to podmieniamy wartość i za pomocą metody NotifyPropertyChanged (której implementację można znaleźć w liniach 38-44) uruchamiamy zdarzenie PropertyChanged, które informuje wszystkich klientów o zmianie tej wartości.

MSDN: link

Przedstawione wyżej interfejsy to tylko cztery z większej całości, które mnie osobiście przydały się już kilkukrotnie. Pula interfejsów systemowych jest jednak znacznie większa. W razie zainteresowania tematem, być może postaram się o kolejny taki wpis;)

Podoba Ci się ten wpis? Powiedz o tym innym!

Send to Kindle

Komentarze

blog comments powered by Disqus