SplashScreen

W Visual Studio bardzo łatwo stworzyć Splash Screen- czyli okno powitalne, wyświetlające się w momencie ładowania aplikacji. Każda z większych aplikacji posiada taki gadget (Visual Studio, Office itp.). Zamiast wyświetlać pusty ekran, dużo lepszym rozwiązaniem jest wyświetlenie SplashScreen’u z logiem firmy\aplikacji. W VS można tego dokonać za pomocą kilku kliknięć:

  1. Dodajemy obrazek do solucji (Add->Add Existing Item).
  2. W oknie właściwości wybieramy BuildAction –> Splash Screen.
  3. Gotowe! Można uruchomić aplikację aby przekonać się , że to naprawdę działa.

Code review: Pułapka z LINQ

Rozważmy następujący kod:

IEnumerable<Employee> employees=GetAllEmployees();
if (employees.Count() > 0)
{
    // jakis kod
}

Załóżmy, że chcemy wykonać jakiś kod jeśli istnieją w bazie pracownicy. Ponadto nie mamy pojęcia co dokładnie zwraca GetAllEmployees – wiemy, że jest to IEnumerable<Employee>. Jeśli jest to np. LINQ – SQL, rozwiązanie jest bardzo niewydajne. Lepiej zastąpić to metodą Any():

IEnumerable<Employee> employees=GetAllEmployees();
if (employees.Any())
{
    // jakis kod
}

Count musi pobrać liczbę wszystkich wierszy w bazie lub wykonać foreach po każdym obiekcie. Any wykonuje po prostu MoveNext i jeśli operacja udała się to znaczy, że istnieje przynajmniej jeden obiekt. W przypadku LINQ to Objects, nie ma to znaczenia ponieważ jest tam optymalizacja która zwraca .Length przy wywołaniu Count(). W praktyce jednak nigdy nie wiemy co może zwrócić nam metoda więc lepiej pisać od razu optymalny kod.

Code review: słowo kluczowe using

W postach oznaczonych “Code review” zamierzam przedstawiam dobre i złe praktyki pisania kodu. Posty przeważnie będą składały się z fragmentu kodu i opisu co jest w nim źle. Dziś spójrzmy na:

StreamReader streamReader = new StreamReader(File.Open(path, FileMode.Open));
string header = streamReader.ReadLine();
string dataPoints = streamReader.ReadLine();
string[] dataPointArray = dataPoints.Split(new char[]{';’});
streamReader.Close();

Co w tym jest jest nie tak? W przypadku gdy np. ReadLine wyrzuci wyjątek, nie będziemy mieli opcji zamknięcia pliku. Dużo lepiej wszelkie operacje IO przechowywać w wyrażeniu using:

using(StreamReader streamReader = new StreamReader(File.Open(path, FileMode.Open)))
{
    string header = streamReader.ReadLine();
    string dataPoints = streamReader.ReadLine();
    string[] dataPointArray = dataPoints.Split(new char[]{';’});
}

Obsługa wyjątków–kilka najczęściej popełnianych błędów

Nieprawidłowa obsługa wyjątków może przynieść więcej problemów niż pożytku.  O obsłudze błędów można byłoby napisać artykuł, jednak w poście chciałbym skupić się wyłącznie na kilku aspektach.

1. Pierwszym problemem jest fakt, że część programistów używa wyjątków do sytuacji po prostu niewyjątkowych. Jak sama nazwa mówi, wyjątek powinien być zastosowany gdzie może zdarzyć się coś niespodziewanego. Spójrzmy więc na poniższy fragment:

int number;
try
{
    number = int.Parse(text);
}
catch
{
    number = -1;
}

Załóżmy, że mamy aplikację w której użytkownik może wpisywać w pole jakiś tekst a następnie musimy to zamienić na liczbę. To, że użytkownik wpiszę tekst zamiast liczby nie jest wyjątkowe i stanowi normalny workflow. Powinniśmy zatem skorzystać z alternatywy np.:

int number;
if(int.TryParse(text, out number) == false)
    number = -1;

Innym często spotykanym scenariuszem jest metoda przeszukująca bazę danych. Jeśli nie ma danego wiersza, nie powinniśmy wyrzucać wyjątków a po prostu NULL lub Null object pattern. W końcu jeśli implementujemy wyszukiwarkę, może się zdarzyć że szukana fraza nie istnieje w bazie danych:

Article FindArticle(string text)
{
    Article article = ...
    // jakis SELECT * FROM
    if(article == null)
        throw new Exception("Article not found"); // ZLE! Powinnismy zwrocic po prostu null    
}

2. Nie należy również umieszczać w klauzuli Try\Catch zbyt dużej ilości kodu. Po pierwsze nie jest to optymalne rozwiązanie. Ważniejszym jednak powodem jest fakt, że wyrzucony i obsłużony wyjątek powinien dotyczyć prostych (pojedynczych) operacji. Zbyt ogólna informacja nie daje żadnych korzyści podczas debuggowania lub przeglądania wygenerowanych logów.

3. Rozważmy następujący fragment aplikacji:

try
{
    // jakas logika
}
catch(Exception ex)
{
   // obsluga wyjatku
}

Typ Exception jest najbardziej ogólnym wyjątkiem. Powinniśmy raczej łapać poszczególne typy wyjątków i uzależnić od nich obsługę np:

try
{
 //
}
catch(ConnectionException ce)
{
 // nie mozna nawiazac polaczenie, moze warto zaimplementowac mechanizm recovery?
}
catch(Exception e)
{
// Nie wiadomo do konca co sie stalo - wykonujemy odpowiedni wpis
// w pliku logow i np. zamykamy aplikacje
}

Drugi Catch lapie wszystko, pierwszy tylko szczególny typ wyjątku.

4. Łapanie wyjątku i ignorowanie go:

try
{
 // logika
}
catch(Exception e}
{
 // brak jakiegokolwiek kodu
}

Powyższy kod jest skrajnie zły – ignorując wyjątek pozwalamy aplikacji nadal funkcjonować i zapominamy po prostu, że to co chciał wykonać użytkownik tak naprawdę nie zostało wykonane. Skoro łapiemy wyjątek powinniśmy przynajmniej wykonać log.

6. Pomijanie stacktrace:

try
{
 // logika
}
catch(Exception e)
{
 // jakas logika, wykonanie logow itp.
 throw e;// ZLE!, throw new Exception("msg") rowniez jest niepoprawne

}

Co się stanie z stacktrace? W momencie ponownego wyrzucenia, nowy zostanie wygenerowany z klauzuli catch. Stary zostanie po prostu usunięty i nie będziemy w stanie zobaczyć, w której metodzie tak naprawdę został wyrzucony wyjątek. Prawidłowe podejście, przechowujące prawidłowy stacktrace:

try
{
 // logika
}
catch(Exception e)
{
 // jakas logika, wykonanie logow itp.
 throw;
}

Ewentualnie:

try
{
 // logika
}
catch(Exception e)
{
 // jakas logika, wykonanie logow itp.
 throw new WrappedException("custom par 1",e);

}

W pewnych sytuacjach ukrywanie stacktrace ma sens – np. gdy nie chcemy dostarczyć hacker’owi jakiś informacji. Jednak myślę, że jest to rzadki przypadek dla większości z nas ( tak jak napisałem  kiedyś, dostarczając bibliotekę DLL już dajemy potencjalnemu włamywaczowi sporo informacji).

To kilka pospolitych błędów. Jeśli macie jeszcze jakieś inne “kwiatki” to zachęcam do komentowania!

Rendezvous w C#–klasa Barrier

Klasa Barrier umożliwia synchronizacje kilku wątków. Mechanizm bardzo znany np. z Ady pozwala dopuścić wykonanie kodu wyłącznie jeśli inne wątki na to się zgadzają. Załóżmy, że mamy 4 wątki robiące równoległe jakieś obliczenia. Po ich zakończeniu chcemy scalić wynik ale musimy poczekać aż wszystkie zadania zostaną ukończone. W C# 4.0 można wykorzystać tzw. taski, ale w tym artykule skupimy się na Barrier, która służy do dużo bardziej skomplikowanych zadań.

Przypuśćmy, że nasz algorytm składa się z kilku faz – z tym że faza następna nie może być  rozpoczęta dopóki wszystkie składowe fazy A nie zostały skończone. Zatem w fazie A, 4 wątki wykonują operacje, po ich zakończeniu rozpoczynamy fazę B itp. Nie możemy jednak rozpocząć fazy B dopóki faza A nie zostanie skończona przez wszystkie wątki.

Definiujemy więc barierę dla 4 uczestników:

     Barrier barrier = new Barrier(4, (b) =>
                                                 {
                                                     MessageBox.Show(string.Format("Wynik={0}, faza={1}", magicNumber,
                                                                                   b.CurrentPhaseNumber));
                                                 });

 

Pierwszy parametr to liczba uczestników (wątków) a drugi to callback wyświetlający numer właśnie zakończonej fazy oraz wyliczoną wartość w algorytmie. Każdy wątek zawiera taką samą logikę liczenia liczby:

  Action calculations = () =>
                                      {
                                          Interlocked.Increment(ref magicNumber);
                                          barrier.SignalAndWait(); // faza A zakończona                               

                                          Interlocked.Increment(ref magicNumber);
                                          barrier.SignalAndWait(); // faza B zakończona

                                          Interlocked.Increment(ref magicNumber);
                                          barrier.SignalAndWait(); // faza C zakończona

                                          Interlocked.Increment(ref magicNumber);
                                          barrier.SignalAndWait(); // faza D zakończona
                                      };

SignalAndWait wysyła sygnał do innych wątków oraz czeka (blokuje) aż inne wątki wyślą również sygnał. Jeśli zostanie uruchomionych więcej wątków niż liczba uczestników, Barrier wyrzuci wyjątek:

The number of threads using the barrier exceeded the total number of registered participants.

Następnie wystarczy uruchomić 4 wątki:

Parallel.Invoke(calculations, calculations, calculations, calculations);

Dobrym zwyczajem jest również wywołanie Dispose na Barrier:

barrier.Dispose();

Istnieje również możliwość zmieniania liczby uczestników już po inicjalizacji bariery:

barrier.AddParticipants(2);
barrier.RemoveParticipant();

Przykładowy kod kolejno powinien wyświetlić liczby 4,8,12,16. Mamy tego pewność ponieważ następna faza nie zostanie rozpoczęta dopóki poprzednia się nie zakończy a na koniec każdej fazy wynik zwiększa się o 4.

Kiedy używać plików PDB?

Wiele osób z pewnością kojarzy pliki PDB, przynajmniej z nazwy. Myślę, jednak, że w praktyce mało kto zdaje sobie sprawę co one zawierają i kiedy mogą się przydać programiście. W poście zatem chciałbym przybliżyć praktyczne korzyści z używania Program Database Files.

Z pewnością PDB kojarzy się z debuggowaniem i niestety z czymś niepotrzebnym w środowisku produkcyjnym – co jak się okaże jest nieprawdą! Zacznijmy od wprowadzenia teoretycznego, PDB dla .NET  zawiera:

  1. Nazwy plików z kodem źródłowym.
  2. Nazwy lokalnych zmiennych.
  3. Numery linii kodu.

Wszystkie inne dane  są zawarte już w plikach binarnych (DLL, EXE itp) i metadanych. W przypadku aplikacji CPP, PDB przechowują dużo więcej danych ale o C++ nie będę pisał tutaj.

Kiedy Windows ładuje bibliotekę do pamięci poszukuje również odpowiedniego pliku PDB. Na przykład, jeśli uruchamiamy HelloWorld.exe, Windows również będzie próbował znaleźć HelloWorld.pdb. Każda plik binarny oraz odpowiadający mu PDB posiadają wspólny unikalny ID (GUID). Podczas każdej kompilacji generowany jest nowy GUID. Jeśli PDB ma inny GUID niż biblioteka, niemożliwe będzie wykorzystanie ich podczas debuggowania (id muszą być jednakowe).

Myślę, że na początek tyle informacji wystarczy. Nie chce zanudzać więcej o rzeczach takich jak lokalizacje PDB czy DUMPBIN (narzędzie służące do odczytywania GUID). Sprawdźmy w praktyce, jak nam może pomóc PDB. Zmieńmy najpierw typ kompilacji na RELEASE i w jakieś metodzie umieśćmy następujący kod, wywołujący wyjątek (dzielenie przez zero):

try
{
 int localVar = 54;
 int val = 0;
 int result = localVar/val;
}
catch (Exception e)
{
 MessageBox.Show(e.ToString());
 throw;
}  

Po kompilacji zobaczymy, że plik PDB został wygenerowany:

image

Odpalmy aplikacje i zobaczmy jakie informacje dostaniemy po wyrzuceniu wyjątku:

image

Zgodnie z tym co zawiera PDB, dostaliśmy bardzo ważną wskazówkę: numer linii kodu oraz nazwę pliku źródłowego. Z pewnością za pomocą takiej informacji bardzo łatwo zlokalizować błąd. Usuńmy teraz plik pdb aby przekonać się, że faktycznie bez niego nie dostaniemy tej informacji:

image

Na maszynie developerskiej zawsze mamy pod ręką VS. Problemy zaczynają się na innych komputerach (środowisko produkcyjne, komputery testerów itp.). Sam wyjątek w pliku logów niewiele mówi programiście. Nazwa pliku i numer zawęża maksymalnie pole do poszukiwań. Dlatego warto pliki PDB deployować wraz z plikami binarnymi. Niektórzy programiści mogą być niechętni deployować PDB ze względów bezpieczeństwa . Jednak dla większości aplikacji nie ma to znaczenia – jeśli deployujemy pliki binarne to i za pomocą czystego Reflect’ora wiele informacji o strukturze aplikacji można uzyskać. Oczywiście PDB ułatwiają trochę zadanie potencjalnemu hackerowi ale jeśli istnieje jakaś luka w systemie wcześniej czy później zostanie ona wykorzystana. Każdy sam musi oszacować ryzyko ale szczególnie dla klientów korporacyjnych, myślę, że PDB dostarcza dużo więcej korzyści niż wad – w końcu w razie awarii możemy bardzo szybko zareagować

Inicjalizacja właściwości obiektu

W C# 3.0 wprowadzono nowy typ inicjalizacji pól klas, przykład:

class SampleClass
{
    public int X { get; set; }
    public int Y { get; set; }
}
SampleClass sampleClass = null;
try
{
 sampleClass = new SampleClass {X = 5, Y = 10};
}
catch (Exception)
{
}
int y = sampleClass.Y;

Przed c# 3.0 programiści zwykle używali następującej konstrukcji:

SampleClass sampleClass = null;
try
{
 sampleClass=new SampleClass();
 sampleClass.X = 5;
 sampleClass.Y = 10;
}
catch (Exception)
{
}
int y = sampleClass.Y;

Czy obydwie konstrukcje pełnią taką samą rolę? W końcu zarówno pierwszy jak i drugi sposób służy do inicjalizacji pól. Jeśli zajrzymy do zdekompilowanewgo kodu (np. za pomocą .NET Reflector) to zobaczymy, że w pewnych przypadkach konstrukcje znaczącą będą różnić się  zachowaniem. Przyjrzyjmy się najpierw klasycznemu podejściu (druga konstrukcja, kod po dekompilacji):

// kod po dekompilacji, zwykle przypisanie wlasciwosci
SampleClass sampleClass;
int y;
sampleClass = null;
Label_0018:
try
{
   sampleClass = new SampleClass();
   sampleClass.X = 5;
   sampleClass.Y = 10;
   goto Label_0038;
}
catch (Exception)
{
Label_0033:
   goto Label_0038;
}
Label_0038:
y = sampleClass.Y;

Nie ma chyba tutaj zaskoczenia. Wywołujemy konstruktor a następnie ustawiamy właściwości X oraz Y (za pomocą wygenerowanych setterów). Sprawdźmy co zostanie wygenerowane jeśli użyjemy inicjalizatora obiektu (pierwsza konstrukcja):

// kod po dekompilacji, inicjalizator obiektow
    SampleClass sampleClass;
    SampleClass <>g__initLocal0;
    int y;
    sampleClass = null;
Label_0018:
    try
    {
        <>g__initLocal0 = new SampleClass();
        <>g__initLocal0.X = 5;
        <>g__initLocal0.Y = 10;
        sampleClass = <>g__initLocal0;
        goto Label_003A;
    }
    catch (Exception)
    {
    Label_0035:
        goto Label_003A;
    }
Label_003A:
    y = sampleClass.Y;

Jak widać po dekompilacji zwykłe przypisania wygenerowały inny kod niż inicjalizator obiektu. Jaki to ma skutek w praktyce? Przypuśćmy, że przypisanie wartości do Y, spowoduje wyjątek. W przypadku inicjalizatora obiektu sampleClass będzie miał wartość NULL ponieważ nigdy nie dojdzie do przypisania <>g_initLocal0!

Dla konstrukcji ze zwykłym przypisaniem właściwości, sampleClass będzie różny od NULL, a właściwość X będzie zainicjalizowana (Y będzie pusty, ponieważ wywołał wyjątek).

Decydując się na któreś z rozwiązań warto zwrócić na to uwagę, ponieważ wyjątki typu NULL Reference, są zwykle trudne w znalezieniu. Aby się przekonać, ,że sampleClass naprawdę będzie NULL’em  wystarczy zasymulować wyrzucenie wyjątku:

class SampleClass
{
   public int X { get; set; }
   
   public int Y
   {
       get { return 0; }
       set{throw new NotImplementedException();}
   }
}

Obsługa zdarzeń – dwa podejścia

Dzisiaj prosta i częsta sprawa – obsługa zdarzeń.  Zdefiniujmy fikcyjne zdarzenie:

class SampleClass
{
   public event EventHandler EventHappened;
}

Następnie mamy jakąś metodę, która po wykonaniu własnej logiki, wywołuje zdarzenie. Jak powinno to zostać prawidłowo wykonane? Zacznijmy od niepoprawnego podejścia:

class SampleClass
{
   public event EventHandler EventHappened;
   private void Method()
   {
       EventHappened(this,new EventArgs());
   }
 }

Metoda spowoduje wyjątek jeśli żadne zdarzenie nie zostało doczepione. Rozwiązaniem jest klasyczne podejście, zgodne z guidelines:

class SampleClass
{
   public event EventHandler EventHappened;
   private void Method()
   {
       OnEventHappened(new EventArgs());
   }
   virtual protected void OnEventHappened(EventArgs e)
   {
       if (EventHappened != null)
           EventHappened(this, e);
   }
}

Stworzyliśmy więc wirtualną metodę protected zaczynająca się od przedrostka On, przyjmującą jako parametr wejściowy argumenty zdarzenia. W ciele metody sprawdzamy, czy są jakieś subskrypcje podpięte i następnie wywołujemy zdarzenie.

Rozwiązanie poprawne, zgodne z guidelines i większości przypadków właśnie w taki sposób obsługuję zdarzenia. Chciałbym jednak pokazać inne podejście,  które przede wszystkim pomaga w dwóch kwestiach:

  1. Powyższa implementacja nie jest thread-safe. Jeśli dwa wątki niefortunnie wywołają OnEventHappened może dojść do problemu z synchronizacją warunku IF.
  2. Niektórzy programiści są bardzo przeczuleni na punkcie IF w programowaniu obiektowym. Zwykle możemy się ich pozbyć poprzez polimorfizm lub Null Object pattern.

Rozwiązaniem jest przypisanie po prostu domyślnej wartości:

 public event EventHandler EventHappened = delegate { };  

Całość:

class SampleClass
{
   public event EventHandler EventHappened = delegate { };  
   private void Method()
   {
       OnEventHappened(new EventArgs());
   }
   virtual protected void OnEventHappened(EventArgs e)
   {
       EventHappened(this, e);
   }
}

Rozwiązanie polega na wykorzystaniu Null Object Pattern. Nie potrzebujemy już instrukcji IF – zawsze jest podpięte przynajmniej jedno zdarzenie.