Artykuł

mar 20 2013
0

8 prostych tricków w C# przydatnych w codziennym programowaniu

Można powiedzieć, że temat tego wpisu powstał na poczekaniu, ponieważ na początku miałem zamiar pisać o czym innym. Dzisiejszy tekst miał dotyczyć tworzenia silnych nazw, ale po małym researchu stwierdziłem, że chyba nie jestem ekspertem od tematów powiązanych z szyfrowaniem, dlatego odpuściłem sobie ten wątek (osoby zainteresowane tematem odsyłam do ciekawego posta, którego znalazłem podczas własnego zgłębiania tematu). Ponieważ miałem nieodpartą ochotę napisać jednak dziś coś o programowaniu, naprędce wymyśliłem nowy temat.

W dzisiejszym wpisie chciałbym Wam zatem przedstawić zestaw użytecznych sztuczek i wskazówek w C#, które z pozoru są błahe, ale często nie do końca znane, szczególnie na wczesnych etapach kariery programistycznej:-)

1. Dziel stringi szybko

Kiedy ktoś chce rozłożyć określony string na czynniki pierwsze z reguły wykorzystuje metodę Split, w której należy podać odpowiedni separator. W sytuacji w której zależy nam na prostym podziale stringa na kolejne znaki, można te sprawę załatwić jeszcze prościej:

string blog = "AltControlDelete";
foreach (char c in blog)
{
    // zrób coś
}

Powyższa konstrukcja jest również tożsama z poniższym zapisem:

string blog = "AltControlDelete";
foreach (char c in blog.ToCharArray())
{
    // zrób coś
}

Metoda ToCharArray daje jednak więcej możliwości, ponieważ umożliwia określenie miejsca z którego rozpoczniemy wycinanie tablicy oraz pozwala nam zdefiniować jej długość.

Z powyższych zapisów wynika, że string jest tablicą znaków, dlatego też prawidłowa jest poniższa konstrukcja:

Console.WriteLine(blog[0]);

Która w praktyce wyświetli pierwszy znak wskazanego stringu.

2. Nie bój się formatować tekstu

Jedną z częstszą przypadłości wielu programistów jest mozolne sklejanie stringów, tymczasem w C# możemy je sobie bardzo ładnie sformatować za pomocą odpowiednich metod i tablicy argumentów. Spójrzmy na pewien zły przykład:

string name = "Ala";
int age = 23;
DateTime birthdayDate = new DateTime(1990, 01, 01);
Console.WriteLine(name + " ma " + age + " lat i urodziła się " +
    birthdayDate + ".");

Czy nie wygląda to brzydko? Czy taka konkatenacja nie jest męcząca? W moim odczuciu jest i to bardzo. Na szczęście wiele metod zapisu w C#, w tym używany przez nas WriteLine wspierają mechanizmy formatowania tekstu. Powyższy przykład można zapisać zatem inaczej:

string name = "Ala";
int age = 23;
DateTime birthdayDate = new DateTime(1990, 01, 01);
Console.WriteLine("{0} ma {1:D} lat i urodziła się {2:yyyy.MM.dd}.",
    name, age, birthdayDate);

Czyż taki kod nie wygląda ładniej i nie zapewnia lepszej czytelności? W moim odczuciu różnica jest diametralna. Używając klamrowych nawiasów przekazujemy pozycje kolejnych argumentów z dynamicznej tablicy która pojawia się po sformatowanym łańcuchu. Formaty umieszczone po dwukropku pozwalają odpowiednio zapisać typ. W tym przypadku formatowaliśmy wiek oraz datę urodzenia.

Jeśli interesująca nas metoda nie obsługuje tego eleganckiego sposobu formatowania tekstu, zawsze możemy zrobić to sami za pomocą metody Format z klasy string. W tym przypadku warto również określić odpowiednie ustawienia regionalne za pomocą klasy CultureInfo:

Console.WriteLine(string.Format(CultureInfo.CurrentCulture, 
    "{0} ma {1:D} lat i urodziła się {2:yyyy.MM.dd}.",
    name, age, birthdayDate));

3. Datę parsuj prosto z łańcucha

Datę zapisaną w stringu nie trzeba wcale dzielić na poszczególne elementy i na ich podstawie tworzyć nowy obiekt DateTime. Całą operację można uprościć wykorzystując metodę Parse:

string date = "1988-01-18";
DateTime parsedDate = DateTime.Parse(date, CultureInfo.CurrentCulture);
Console.WriteLine("{0:yyyy.MM.dd}", parsedDate);

W tym przypadku również pomocny będzie obiekt CultureInfo - szczególnie jeśli data została zapisana ze specyficznymi ustawieniami regionalnymi.

4. Zapomnij o zamykaniu obiektów w finally

Często tworząc obiekty musimy pamiętać o ich należytym zamknięciu i to niezależnie czy operacja na nich realizowana się powiedzie, czy też nie. Tak postępujemy bardzo często w przypadku różnorakich strumieni. Dlatego też często można spotkać taki kod:

StreamReader reader = null;
try
{
    reader = new StreamReader("C:/plik.txt");
    // zrób coś ze strumieniem
}
catch (Exception ex)
{
    // obsłuż wyjątek
}
finally
{
    if (reader != null)
    {
        reader.Dispose();
    }
}

Taki kod oczywiście jest poprawny i bardzo ładnie zadziała. Warto jednak wiedzieć, że można go uprościć nie tracąc przy tym na jego stabilności:

try
{
    using (StreamReader reader = new StreamReader("C:/plik.txt"))
    {
        // zrób coś ze strumieniem
    }
}
catch (Exception ex)
{
    // obsłuż wyjątek
}

Pozbyliśmy się sekcji finally i 6 linijek kodu, a napisane przez nas operacje wykonują dokładnie to samo. Magia? Nie, to po prostu interfejs IDisposable. Taka sztuczka zadziała na wszystkich obiektach, które go implementują:-)

5. Mądrze rzucaj wyjątki

Wyjątki od zawsze były kłopotliwym tematem. W tej materii powstały różne szkoły. Niektórzy mają setki różnych konstrukcji try..catch zagłębionych na różnych poziomach, inni z kolei wolą chwytać wyjątek możliwie na samej górze sterty wywołań. Osobiście jestem zwolennikiem tej drugiej metody, jednak czasem pojawia się konieczność wykonania odpowiedniej akcji w miejscu w którym znajdujemy się obecnie. W takim przypadku stosuję blok try..catch, wykonuję odpowiednią operację na wskazanym poziomie i następnie rzucam wyjątek. Tu właśnie pojawia się problem, który popełnia wiele osób:

try
{
    // kod tworzący zło
}
catch (Exception ex)
{
    // zrób coś na tym poziomie
    throw new Exception(ex.Message);
}

Co jest złego w tym przypadku? Najogólniej mówiąc wszystko. Przy takim zastosowaniu konstrukcji throw, przekazaliśmy tylko komunikat błędu. Straciliśmy dwie często bardzo istotne rzeczy, czyli StackTrace oraz InnerException. Jeśli właściwie logowanie błędu znajduje się wyżej, to tych informacji już nie uzyskamy, a sam komunikat błędu w stylu Object reference not set to an instance of an object nie powie nam w tym przypadku z reguły nic.

Jak zatem należy postępować? Wystarczy powyższą konstrukcję zastąpić po prostu słowem kluczowym throw:

try
{
    // kod tworzący zło
}
catch (Exception ex)
{
    // zrób coś na tym poziomie
    throw;
}

6. Operuj na interfejsach a nie na konkretnych klasach

Jeśli tworzysz własne klasy danych, lub operujesz na klasach systemowych staraj się w miarę możliwości operować na interfejsach. Poniższy kod:

class InterfaceExample
{
    private Collection<string> _namesCollection = null;

    public InterfaceExample(Collection<string> namesCollection)
    {
        _namesCollection = namesCollection;
    }

    public Collection<string> NamesCollection
    {
        get { return _namesCollection; }
    }
}

Możemy z powodzeniem zastąpić rozwiązaniem opartym o interfejsy:

class InterfaceExample
{
    private ICollection<string> _namesCollection = null;

    public InterfaceExample(ICollection<string> namesCollection)
    {
        _namesCollection = namesCollection;
    }

    public ICollection<string> NamesCollection
    {
        get { return _namesCollection; }
    }
}

Dzięki temu zyskaliśmy ogromną elastyczność. Nie jesteśmy już ograniczeni do klasy Collection (ewentualnie klas po niej dziedziczących), ale możemy korzystać z dowolnej klasy, która implementuje wskazany interfejs. Jak zapewne pamiętacie z podstaw programowania, na tej zasadzie opiera się cały polimorfizm oraz dziedziczenie.

7. Wyliczaj enumem a nie intem

Częstym błędem programistów jest użytkowanie inta jako zmiennej wyliczeniowej. Załóżmy że opisujemy stan jakiegoś procesu, który przyjmuje kolejne wartości. W teorii można zrobić to z intem...:

/// <summary>
/// Wartości 
/// 0 - zarejestrowany
/// 1 - start
/// 2 - przetwarzanie
/// 3 - koniec
/// 1000 - błąd
/// </summary>
private int _processState = 0;

W praktyce takie rozwiązanie jest koszmarne. Co z tego, że wszystko mamy w komentarzu, kiedy w dowolnym innym miejscu aplikacji te liczby nic nikomu nie powiedzą? No właśnie - nic. Int co najwyżej nadaje się na przełącznik 0/1 - choć i od tego mamy zmienną typu bool.

Dużo lepszym rozwiązaniem w tym przypadku będzie enum, który możemy utworzyć jako prywatny jeśli ma zastosowanie tylko wewnątrz określonej klasy, bądź też umieścić bezpośrednio w przestrzeni wskazanego zestawu:

private enum ProcessingState
{
    registred = 0,
    start,
    processing,
    end,
    error = 1000
}

private ProcessingState _processState = ProcessingState.registred;

Czy rozwiązanie w którym sama wartość enumeracji wyraźnie mówi o statusie nie jest bardziej przejrzyste? Myślę, że jest to klasyczny przykład pytania retorycznego;-)

Enumeracje można również bardzo fajnie wykorzystać do stworzenia flag bitowych.

8. Nie bój się tworzyć własne klasy wyjątków

Wcześniej w tekście wspominałem o dziedziczeniu. Nie bój się go używać we własnych projektach, w końcu jest to esencja programowania obiektowego. Dziedziczyć można praktycznie wszystko (chyba że autor określonego kodu, zablokuje taką możliwość). Dlaczego by więc na potrzeby własnego projektu nie stworzyć użytecznej klasy wyjątku, która będzie zawierać nasz kod błędu? W praktyce jest to bardzo proste:

public enum MyErrors
{
    initerror = 0,
    processingerror,
    unknown = 1000
}

public class MyException : Exception
{
    private MyErrors _errorCode = MyErrors.unknown;

    public MyException()
        : base()
    {
    }

    public MyException(string errorMessage)
        : base(errorMessage)
    {
    }

    public MyException(MyErrors errorCode, string errorMessage)
        : base(errorMessage)
    {
        _errorCode = errorCode;
    }

    public MyErrors ErrorCode
    {
        get { return _errorCode; }
    }
}

Pamiętając o uwagach z punktu 7, całość zaczynamy od utworzenia enumeracji, w której zapiszemy kody błędów (1-6). Następnie przygotowujemy właściwą klasę wyjątku która rozszerza klasę bazową i dodając do niej kilka standardowych konstruktorów (12-20).

Nowością w tym przypadku jest pole klasy, które definiuje rodzaj błędu (10), specjalny konstruktor w którym oprócz opisu błędu podajemy również rzeczony kod (22-26) oraz właściwość ErrorCode (28-31), która umożliwia nam jego zwrócenie.

Oczywiście możliwości na tym polu są znacznie większe, a powyższe rozwiązanie to tylko prosty przykład jak można uprościć sobie życie;-)

Data ostatniej modyfikacji: 21.03.2013, 15:15.

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

Send to Kindle

Komentarze

blog comments powered by Disqus