Monthly Archives: Sierpień 2012

Jawna implementacja interfejsów jako rozwiązanie konfliktu nazw

2012-08-25

Implementując w danej klasie kilka interfejsów możemy spotkać się z sytuacją, w której różne interfejsy będą posiadały składowe o tych samych nazwach:

interface IFirstInterface
{
    void SomeMethod();
}

interface ISecondInterface
{
    void SomeMethod();
}

class SomeClass : IFirstInterface, ISecondInterface
{
    public void SomeMethod()
    {
        Console.WriteLine("Call some method...");
    }
}

Powyższy kod prezentuje klasę implementującą dwa interfejsy zawierające metody o tej samej nazwie. W klasie metoda zaimplementowana jest raz i działa bez względu na to, za pośrednictwem którego interfejsu zostanie wywołana:

static void Main(string[] args)
{
    SomeClass someClass = new SomeClass();

    IFirstInterface iFirst = someClass;
    ISecondInterface iSecond = someClass;

    iFirst.SomeMethod(); //Call some method...
    iSecond.SomeMethod(); //Call some method...

    Console.ReadLine();
}

A co jeżeli metoda pierwszego interfejsu ma inne przeznaczenie niż metoda drugiego i ich kod powinien być różny? Rozwiązaniem tego problemu jest jawna implementacja interfejsów:

class SomeClass : IFirstInterface, ISecondInterface
{
    void IFirstInterface.SomeMethod()
    {
        Console.WriteLine("Call some method from first interface...");
    }

    void ISecondInterface.SomeMethod()
    {
        Console.WriteLine("Call some method from second interface...");
    }
}

W przypadku jawnej implementacji nazwę składowej poprzedzamy nazwą danego interfejsu. Drugą ważną rzeczą jest pominięcie modyfikatora dostępu, ponieważ jawnie implementowana składowa automatycznie otrzymuje modyfikator private i nie można tego zmienić. Fakt ten oznacza, że taka składowa nie będzie dostępna przy odwołaniu z poziomu obiektu a jedynie z poziomu danego interfejsu. Oczywiście w powyższym kodzie można na przykład jawnie zaimplementować jedynie drugi interfejs, dzięki temu pierwsza wersja metody będzie dostępna z poziomu obiektu oraz interfejsu IFirstInterface, natomiast druga wersja jedynie z poziomu interfejsu ISecondInterface. Oto działanie jawnej implementacji:

static void Main(string[] args)
{
    SomeClass someClass = new SomeClass();

    IFirstInterface iFirst = someClass;
    ISecondInterface iSecond = someClass;

    iFirst.SomeMethod(); //Call some method from first interface...
    iSecond.SomeMethod(); //Call some method from second interface...

    Console.ReadLine();
}

Jak widać, wywołując metodę za pośrednictwem wybranego interfejsu, obiekt jest w stanie określić jej odpowiednią wersję. Oprócz rozwiązywania konfliktów nazw, jawną implementację interfejsów można wykorzystać także do ukrywania pewnych elementów klasy przy dostępie z poziomu obiektu. Elementy te będą jednocześnie dostępne przy odwołaniu poprzez interfejs.
Na zakończenie warto dodać, że powinno unikać się sytuacji, w których składowe o tych samych nazwach różnią się przeznaczeniem lub sposobem działania. Jeżeli jednak nie mamy na to wpływu (na przykład korzystamy z bibliotek zewnętrznych) jawna implementacja interfejsów pozwoli nam na rozwiązanie konfliktu nazw.

Reklamy

Instrukcja yield return – tworzenie leniwych kolekcji danych

2012-08-18

W tym wpisie zajmę się omówieniem polecenia yield, udostępnionego w wersji 2.0 języka C#. Do czego służy ta instrukcja? Dzięki niej możemy tworzyć tzw. leniwe kolekcje, do których poszczególne elementy dodawane są dopiero w momencie zgłoszenia na nie zapotrzebowania. Żeby zaprezentować działanie polecenia yield, najpierw zobaczmy standardowy sposób tworzenia kolekcji. Poniższa metoda zwraca kolekcję pięciu elementów typu int:

static IEnumerable<int> GetData()
{
    Console.WriteLine("Rozpoczęcie metody GetData.");

    List<int> list = new List<int>();

    for (int i = 1; i <= 5; i++)
    {
        Console.WriteLine("Przygotowanie wartości: " + i.ToString());
        list.Add(i);
    }

    Console.WriteLine("Zakończenie metody GetData.");

    return list;
}

Oto zastosowanie utworzonej metody:

static void Main(string[] args)
{
    Console.WriteLine("Pobranie danych.");
    IEnumerable<int> data = GetData();

    Console.WriteLine("Rozpoczęcie przetwarzania.");
    foreach (int i in data)
    {
        Console.WriteLine("Odczyt wartości: " + i.ToString());
        if (i == 3)
            break;
    }

    Console.WriteLine("Zakończenie przetwarzania.");
    Console.ReadLine();
}

Po uruchomieniu powyższego kodu otrzymamy następujące wyniki:

Pobranie danych.
Rozpoczęcie metody GetData.
Przygotowanie wartości: 1
Przygotowanie wartości: 2
Przygotowanie wartości: 3
Przygotowanie wartości: 4
Przygotowanie wartości: 5
Zakończenie metody GetData.
Rozpoczęcie przetwarzania.
Odczyt wartości: 1
Odczyt wartości: 2
Odczyt wartości: 3
Zakończenie przetwarzania.

Jak widać wszystko działa zgodnie z oczekiwaniami. Można jednak zauważyć, iż mimo że metoda GetData zwróciła pięć elementów, my wykorzystaliśmy tylko trzy z nich. Oznacza to, że niepotrzebnie straciliśmy czas i zasoby na przygotowanie dwóch ostatnich elementów kolekcji. Ale skąd metoda GetData może wiedzieć, z których elementów będzie korzystał dalszy kod programu? Jej działanie kończy się przed rozpoczęciem przetwarzania danych, co z kolei wymusza na niej utworzenie i zapisanie w pamięci wszystkich elementów wynikowej kolekcji. Takie działanie można określić jako zachłanne. Jego przeciwieństwem jest działanie leniwe, i właśnie do tego celu służy polecenie yield. Poniżej znajduje się nowa, „leniwa” wersja metody GetData:

static IEnumerable<int> GetData()
{
    Console.WriteLine("Rozpoczęcie metody GetData.");

    for (int i = 1; i <= 5; i++)
    {
        Console.WriteLine("Przygotowanie wartości: " + i.ToString());
        yield return i;
    }

    Console.WriteLine("Zakończenie metody GetData.");
}

W obecnej wersji nie ma już potrzeby tworzenia lokalnej zmiennej dla wynikowej kolekcji. Poszczególne elementy zwracane są z metody poprzez instrukcję yield return, ale jedynie w przypadku gdy nastąpi do nich odwołanie.

Poniżej znajduje się wynik działania programu z nową wersją metody GetData:

Pobranie danych.
Rozpoczęcie przetwarzania.
Rozpoczęcie metody GetData.
Przygotowanie wartości: 1
Odczyt wartości: 1
Przygotowanie wartości: 2
Odczyt wartości: 2
Przygotowanie wartości: 3
Odczyt wartości: 3
Zakończenie przetwarzania.

Jak widać, przebieg programu jest teraz zupełnie inny. Metoda GetData wykonywana jest dopiero po rozpoczęciu przetwarzania i zwraca poszczególne elementy tylko w momencie ich odczytu (de facto po poleceniu yield return sterowanie przekazywane jest z metody do kodu, który ją wywołał i powraca do niej w momencie odczytu kolejnego elementu). W tym miejscu należy zwrócić uwagę na jedną rzecz – brak komunikatu „Zakończenie metody GetData”. Jest to spowodowane przerwaniem pętli foreach po odczycie trzech elementów, co z kolei wymusiło także przerwanie działania metody GetData. Aby komunikat pojawił się, w metodzie należy zastosować blok try finally, a jego wyświetlenie umieścić w sekcji finally. Z powyższym faktem wiąże się jeszcze jedna cecha polecenia yield – nie może ono znajdować się w sekcji try jeżeli po niej występuje sekcja catch. A co dzieje się z enumeratorem zawierającym pobrane elementy w przypadku przerwania metody? Na szczęście pętla foreach o to dba, a więc po jej przerwaniu zostanie on prawidłowo zwolniony.
Dzięki zastosowaniu instrukcji yield pozbyliśmy się dwóch problemów jednocześnie: nie musimy tworzyć elementów, z których nie skorzystamy, nie musimy także buforować całej kolekcji w pamięci.
W celu sprawdzenia w jakim stopniu zastosowanie yield wpływa na poprawę wydajności, utworzyłem dwie wersje metody odczytującej plik tekstowy i zwracającej jego ponumerowane linie w postaci kolekcji string-ów:

Wersja 1 (bez użycia yield):

static IEnumerable<string> GetDataFromFile()
{
    List<string> data = new List<string>();
    string fileLine;
    int lineNumber = 0;

    using (StreamReader sr = File.OpenText(@"d:\Dane.txt"))
    {
        while ((fileLine = sr.ReadLine()) != null)
        {
            data.Add((++lineNumber).ToString() + ' ' + fileLine);
        }
    }
    return data;
}

Wersja 2 (z użyciem yield):

static IEnumerable<string> GetDataFromFile()
{
    string fileLine;
    int lineNumber = 0;

    using (StreamReader sr = File.OpenText(@"d:\Dane.txt"))
    {
        while ((fileLine = sr.ReadLine()) != null)
        {
            yield return (++lineNumber).ToString() + ' ' + fileLine;
        }
    }
}

Poniżej znajduje się kod zapisujący zwrócone przez utworzoną metodę dane w nowym pliku tekstowym, dodatkowo mierzony jest czas wykonania całej operacji:

static void Main(string[] args)
{
    Stopwatch sw = new Stopwatch();
    sw.Start();

    File.WriteAllLines(@"d:\Dane1.txt", GetDataFromFile());

    sw.Stop();
    Console.WriteLine(sw.ElapsedMilliseconds.ToString());
    Console.ReadLine();
}

Plik, na którym przeprowadziłem test miał rozmiar ok. 190MB i nieco ponad milion linii. W przypadku pierwszej wersji metody program wykonywał się 4 sekundy i zajął 430MB pamięci. Przy drugiej wersji metody czas wyniósł 2,9 sekundy, natomiast program w pamięci zajął jedynie 6MB. Jak widać zastosowanie instrukcji yield znacznie poprawiło wydajność rozwiązania, szczególnie w zakresie zapotrzebowania na pamięć. Efekt ten został osiągnięty dzięki temu, że nie było potrzeby generowania i umieszczania w pamięci całej kolekcji przed rozpoczęciem jej przetwarzania.

Na zakończenie kilka uwag odnośnie stosowania polecenia yield:

  • może być używane jedynie w metodach zwracających IEnumerable lub IEnumerator;
  • nie może znajdować się w sekcji try jeżeli po niej występuje sekcja catch;
  • jeżeli chcemy przerwać zwracanie kolejnych elementów kolekcji używamy polecenia yield break;

Więcej informacji na ten temat: MSDN.

Indeksatory a kolekcje obiektów typu wartościowego

2012-08-10

W dzisiejszym temacie poruszę pewien problem na jaki można natknąć się podczas używania kolekcji obiektów typu wartościowego. Aby pokazać o co dokładnie chodzi zacznijmy od zdefiniowania przykładowej klasy opisującej prostokąt:

class Rect
{
    public int Height { get; set; }
    public int Width { get; set; }
}

Teraz tworzymy listę obiektów typu Rect zawierającą dwa elementy, po czym modyfikujemy właściwość Height pierwszego z nich:

List<Rect> rectangles = new List<Rect>
{
    new Rect {Height = 2, Width = 3},
    new Rect {Height = 5, Width = 6}
};

rectangles[0].Height = 3;

Powyższy kod jest jak najbardziej poprawny i działa bez problemów. Co się jednak stanie gdy zmienimy naszą klasę Rect na strukturę?:

struct Rect
{
    public int Height { get; set; }
    public int Width { get; set; }
}

Okazuje się, że przy próbie kompilacji otrzymamy błąd w linii rectangles[0].Height = 3:

Cannot modify the return value of ‚System.Collections.Generic.List<Rect>.this[int]’ because it is not a variable

Co ciekawe, jeżeli zamiast listy użyjemy tablicy wszystko będzie w porządku. Poniższy kod nie generuje już błędu niezależnie czy użyjemy klasy czy struktury jako elementów tablicy:

Rect[] rectangles =
{
    new Rect {Height = 2, Width = 3},
    new Rect {Height = 5, Width = 6}
};

rectangles[0].Height = 3;

Aby wyjaśnić przyczynę takiego zachowania stwórzmy następującą klasę:

class MyCollection<T>
{
    public T[] Elements;

    public T this[int index]
    {
        get
        {
            return Elements[index];
        }
        set
        {
            Elements[index] = value;
        }
    }

    public MyCollection(int count)
    {
        Elements = new T[count];
    }
}

Składa się ona z trzech elementów: tablicy, indeksatora oraz konstruktora przyjmującego parametr określający rozmiar tablicy. Indeksator poprzez akcesory get i set zwraca lub ustawia określony element tablicy. Oto przykład zastosowania powyższej klasy:

MyCollection<Rect> rectangles = new MyCollection<Rect>(2);
rectangles.Elements[0] = new Rect { Height = 2, Width = 3 };
rectangles[1] = new Rect { Height = 5, Width = 6 };

rectangles.Elements[0].Height = 3;
rectangles[1].Height = 3;

Utworzony został obiekt klasy MyCollection operujący na typie Rect. Do naszego obiektu dodaliśmy dwa prostokąty, jak widać można to zrobić zarówno poprzez bezpośrednie odwołanie do tablicy (rectangles.Elements[0]) jak i poprzez indeksator (rectangles[1]). W kolejnych dwóch liniach następuje próba modyfikacji właściwości Height obu elementów, pierwszego przy użyciu tablicy, drugiego przy użyciu indeksatora. I teraz najważniejszy test, jeżeli typ Rect będzie klasą, cały kod skompiluje się i będzie działał poprawnie. Jeżeli natomiast typ Rect będzie strukturą, przy próbie kompilacji zobaczymy znajomy błąd (linia rectangles[1].Height = 3):

Cannot modify the return value of ‚MyCollection<Rect>.this[int]’ because it is not a variable

Jak widać w przypadku typów wartościowych (struktura), problemem jest użycie indeksatora do pobrania elementu, który chcemy zmodyfikować. Dotyczy to zarówno naszej klasy, jak i klasy List<T> gdzie do odczytu elementów także wykorzystywane są indeksatory. Dlaczego tak się dzieje? Jeżeli pobieramy element poprzez indeksator (akcesor get), zgodnie z zachowaniem typów wartościowych dostajemy tak naprawdę jego kopię, a więc modyfikacja oryginalnego elementu w ten sposób nie jest możliwa. Inaczej zachowują się tablice, w ich przypadku poprzez indeks jesteśmy w stanie operować bezpośrednio na danym obiekcie. Jeżeli stosujemy obiekty typu referencyjnego (klasa), nie ma znaczenia czy odwołujemy się do nich poprzez indeks tablicy czy indeksator listy. W obu przypadkach otrzymamy po prostu referencję do danego obiektu, poprzez którą nastąpi jego modyfikacja.
Jak więc zmodyfikować wybrany element listy zawierającej obiekty typu wartościowego? Musimy pobrać dany element do zmiennej (wykonywana jest jego kopia), następnie dokonać modyfikacji, po czym przypisać zmodyfikowany obiekt do listy (wykonywana jest kolejna kopia):

List<Rect> rectangles = new List<Rect>
{
    new Rect {Height = 2, Width = 3},
    new Rect {Height = 5, Width = 6}
};

Rect tmp = rectangles[0];
tmp.Height = 3;
rectangles[0] = tmp;

SSIS Script Component Destination – własny zapis danych

2012-08-04

W dwóch poprzednich tematach poświęconych Integration Services przedstawiłem wykorzystanie Script Component do tworzenia własnego źródła danych oraz ich transformacji. W dzisiejszym wpisie omówię jego ostatnie zastosowanie – implementację własnego zapisu danych. Po dodaniu komponentu do elementu Data Flow oraz podpięciu danych wejściowych, przechodzimy do jego konfiguracji:

1. Skrypt ustawiamy jako miejsce docelowe danych:

2. W sekcji Script poprzez właściwości ReadOnlyVariables i ReadWriteVariables udostępniamy dla skryptu zmienne:

3. W sekcji Input Columns wybieramy dostępne kolumny wejściowe:

4. W sekcji Inputs and Outputs określamy nazwę wejściowego zbioru danych.

5. W sekcji Connection Managers udostępniamy dla skryptu istniejące połączenia (oczywiście jeżeli chcemy z nich skorzystać).

Po skonfigurowaniu komponentu przechodzimy do edycji kodu C# (sekcja Script -> Edit Script). Skrypt składa się z klasy ScriptMain, w której implementujemy wybrane metody w celu zapisania danych wejściowych. Są to metody:

  • AcquireConnections – metoda, w której możemy nawiązać połączenie z zewnętrznym źródłem (możemy zdefiniować nowe połączenie lub wykorzystać udostępnione, w drugim przypadku używamy obiektu Connections zawierającego listę połączeń oraz metody AcquireConnection udostępniającej konkretne połączenie);
  • PreExecute – metoda wykonywana jest przed rozpoczęciem procesu przetwarzania danych wejściowych;
  • <nazwa_wejścia>_ProcessInputRow – metoda wykonywana dla każdego rekordu wejściowego, tutaj implementujemy zapis danych;
  • PostExecute – metoda wykonywana po zakończeniu procesu przetwarzania danych;
  • ReleaseConnections – w tej metodzie należy zamknąć połączenia do zewnętrznych źródeł;

Wszystkie dostępne metody: ScriptComponent Class

Poniższy skrypt zapisuje dane wejściowe do pliku XML, którego lokalizację przekazujemy w zmiennej XmlFilePath (aby skorzystać z typu XDocument należy dodać referencję do System.Xml.Linq):

using System;
using System.Data;
using Microsoft.SqlServer.Dts.Pipeline.Wrapper;
using Microsoft.SqlServer.Dts.Runtime.Wrapper;
using System.Xml.Linq;

[Microsoft.SqlServer.Dts.Pipeline.SSISScriptComponentEntryPointAttribute]
public class ScriptMain : UserComponent
{
    XDocument xmlDocument;

    public override void PreExecute()
    {
        base.PreExecute();

        xmlDocument = new XDocument(
            new XDeclaration("1.0", "utf-8", "yes"),
            new XComment("SSIS Script Component Destination"),
            new XElement("rows"));
    }

    public override void MyInput_ProcessInputRow(MyInputBuffer Row)
    {
        XElement xmlElement = new XElement("row");
        xmlElement.Add(new XElement("Id", Row.Id.ToString()));
        xmlElement.Add(new XElement("Name", Row.Name));

        xmlDocument.Root.Add(xmlElement);
    }

    public override void PostExecute()
    {
        base.PostExecute();

        xmlDocument.Save(Variables.XmlFilePath);
    }
}

Użycie przygotowanego komponentu:

Dane wejściowe:

Wynikowy plik XML:

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<!--SSIS Script Component Destination-->
<rows>
  <row>
    <Id>1</Id>
    <Name>AAA</Name>
  </row>
  <row>
    <Id>2</Id>
    <Name>BBB</Name>
  </row>
  <row>
    <Id>3</Id>
    <Name>CCC</Name>
  </row>
</rows>

Więcej informacji na ten temat: MSDN.