Kawa z rana

Kojarzycie znak drogowy z filiżanką informujący o „bufecie lub kawiarni”? Ja też nie. Ale to nie znaczy ze taki nie istnieje (dla dociekliwych D-27). Nie kojarzymy go bo nie jest zbyt często używany, szczególnie w czasach gdy każda stacja benzynowa oferuje kawę. Podobnie mają się te mniej znane rozwiązania w C#.

Mało popularnych znaków jest sporo chociażby takie jak informujące o bocznym wietrze (A-19) czy spadających kamieniach (A-25), ale nie o nich jest ten wpis. Wpis jest o C#, w którym analogicznie mamy rozwiązania które istnieją ale są zapomniane, lub niedoceniane. Większości używamy na co dzień tak jak znaków stop lub ograniczenia prędkości. Są jednak rzeczy których używamy rzadko lub wcale, ale warto sobie o nich przypomnieć od czasu do czasu. Często to właśnie takie detale wyznaczają jakość kodu.

1. Lazy<T>

Pierwszy z nich to Lazy<T>. Warto zaznaczyć obecność na pierwszym miejscu jest zupełnie przypadkowa, tak jak cała lista. Kto z nas nie używa propertiesów z podejściem „backing field”? Używamy go codziennie, m.in. do tego aby zaimplementować lazy loading szczególnie gdy dane wyciągane są z bazy lub ich konstrukcja jest kosztowna. Standardowa aplikacja konsolowa w podejściu backing field mogła by wyglądać tak:

   class Program
    {
        static void Main(string[] args)
        {
            var test = new Test();
            Console.WriteLine(test.Data);
            Console.WriteLine(test.Data);
            Console.WriteLine(test.Data);
            Console.WriteLine(test.Data);
            Console.ReadKey();
        }

        internal class Test
        {
            private string _data;
            internal string Data
            {
                get
                {
                    if (_data == null)
                    {
                        _data = LoadValue();
                    }
                    return _data;
                }
            }

            private string LoadValue()
            {
                Console.WriteLine("Load Data");
                return "Some example string";
            }
        }
    }

Efekt działania aplikacji wygląda następująco:

Wynik Lazy<t>

Przykład jest uproszczony do granic możliwości: podczas pobierania wartości Data sprawdzamy czy backing field jest uzupełniony, jeżeli nie to go uzupełniamy a następnie zwracamy wartość. Pobieranie następuje tylko raz. Jest też inne, mniej popularne rozwiązanie. Możemy zmienić naszą klasę Test używając Lazy<T>. na przykład w ten sposób:

        internal class Test
        {
            private Lazy<string> _data;
            internal string Data => _data.Value;

            internal Test()
            {
                _data = new Lazy<string>(() => LoadValue());
            }

            private string LoadValue()
            {
                Console.WriteLine("Load Data");
                return "Some example string";
            }
        }

Co tutaj się stało? Praktycznie to samo. Zapytacie więc po co tego używać? To był najprostszy możliwy przykład, abym mógł pokazać jak zaimplementować to rozwiązanie. Ale tak naprawdę Lazy<T> służy do inicjalizacji dużych kolekcji, wtedy można wykorzystać potencjał klasy poprzez np. wielowątkową inicjalizację (konstruktor: Lazy<T>(Func, Boolean). Można też wybrać tryb zarządzania z enuma LazyThreadSafetyMode. Więcej o klasie Lazy mozna poczytać tutaj. Klasa nie powstała wczoraj ale mam wrażenie że cały czas jest mało znana.

2. Caller information attributes

Jest to bardzo proste rozwiązanie ale niezwykle pomocne przy wszelkiego rodzaju loggerach. Ja osobiście bardzo często opakowuje metody logujące popularnych bibliotek swoją klasą aby w razie takiej potrzeby mieć możliwość globalnej zmiany w logach. Podczas zapisywania informacji do pliku/maila/bazy nieoceniona jest informacja z jakiego pliku metody i linii pochodzi komunikat. Tutaj z pomocą przychodzą właśnie caller information attributes. Zobaczmy to na przykładzie naszej konsolówki:

   class Program
    {
        static void Main(string[] args)
        {
            LogInfo();
            SomeMethod();
            Console.ReadKey();
        }

        static void SomeMethod()
        {
            LogInfo();
        }

        static void LogInfo([CallerMemberName] string methodName = "", [CallerFilePath] string filepath = "", [CallerLineNumber] int lineNumber = 0)
        {
            Console.WriteLine($"Something happened in method: {methodName}, line: {lineNumber} and file: {methodName}");
        }
    }

Wywołujemy a samą metodę dwa razy, jednak do potencjalnego loga zapisalibyśmy różne informacje:

Wynik caller information attributes

Prosta rzecz a cieszy 🙂

3. Task Parallel Library (TPL)

Któż z nas nie używa foreach’a? To moim zdaniem najczęściej wykorzystywana pętla ever! Typowe jej zastosowanie to uruchomienie jakiegoś powtarzalnego kodu dla jakiegoś elementu co zajmuje określony czas. A co gdyby ten czas skrócić kilkukrotnie zamiast katować jeden wątek? Spójrzmy:

   var items = Enumerable.Range(0, 300).ToList();
            var sw = Stopwatch.StartNew();
            foreach (var item in items)
            {
                Thread.Sleep(50);
            }
            Console.WriteLine($"Took {sw.ElapsedMilliseconds} milliseconds...");
            Console.ReadKey();

To kod symulujący przetwarzanie 50ms dla każdego z 300 elementów. Aby sprawdzić czas działania użyliśmy Stopwatch. Wynik programu: „Took 17687 milliseconds…”. Całkiem logiczne w końcu 300×50 = 15000 dodatkowe milisekundy resztę logiki. Spróbujmy tego samego używając TPL:

 var items = Enumerable.Range(0, 300).ToList();
            var sw = Stopwatch.StartNew();
            Parallel.ForEach(items, (item) =>
            {
                Thread.Sleep(50);
            });
            Console.WriteLine($"Took {sw.ElapsedMilliseconds} milliseconds...");
            Console.ReadKey();

Jak widać składnia różni się nieznacznie od poprzedniego kodu. Sprawdźmy więc wynik działania: „Took 2202 milliseconds…” czyli niecałe 13% poprzedniego czasu. Wyobrażacie sobie taki skok wydajności na produkcji? 🙂 Warto zaznaczyć że pomiar jest poglądowy i nie można uważać że każda taka zmiana powoduje podobny wzrost. Przykład jest maksymalnie uproszczony na potrzeby artykułu. W realnym zastosowaniu trzeba uważać również na kolekcje które używamy w TPL – muszą być thread safe. W C# są to kolekcje typu Concurrent np:

  • ConcurrentStack
  • ConcurrentQueue
  • BlockingCollection
  • ConcurrentBag
  • ConcurrentDictionary

To oczywiście tylko bardzo mały wycinek możliwości biblioteki, pełną dokumentację znajdziecie TUTAJ.

4. Dekorowanie metod jako Obsolete

Tak jak atrybuty uważam za dobrze znane rozwiązania w C# to Obsolete uważam za często ignorowany. Czasami zdarza się tak że jakieś metody powinny zostać usunięte z biblioteki którą tworzymy, jednak nie możemy tego zrobić w sposób drastyczny ponieważ inni korzystają ze starej metody i muszą świadomie przejść na nowe rozwiązanie. Ktokolwiek używał zewnętrznych bibliotek takich jak Telerik/Progress, DevExpress lub podobnych i migrował je na nowsze odpowiedniki zetknął się z tym niejako od strony „klienta”. Jednak jak często zdarzało Wam się dekorować metody jako Obsolete? Jest to bardzo prosty ale niestety trochę zapomniany mechanizm. Spójrzmy do kodu:

        [Obsolete("This method has been superseded by the DoSomethingElse() method")]
        static void DoSomething()

Taka składnia pozwoli nam na poinformowanie użytkownika o tym że metoda jest Obsolete. Mimo wszystko można jej używać (zielone podkreślenie w VS):

Metoda obsolete Warning

Jeżeli jako drugi argument dodamy true:

Metoda obsolete Error

Spowodujemy, że wywołanie metody w kodzie zamiast ostrzeżenia będzie powodowało błąd (czerwone podkreślenie w VS) i uniemożliwi kompilację kodu (nawet jeżeli metoda będzie działa poprawnie). Proste, prawda? Usuwanie metod nie jest zbyt częstym przypadkiem ale jak już musimy to zrobić to polecam to rozwiązanie. Sam stosuje to rozwiązanie w bibliotekach które pisze w .Net standard (więcej o .Net standard poczytasz TUTAJ).

5. Filtry w wyjątkach

Filtry w wyjątkach zostały wprowadzone już w C# w wersji 6 jednak jakoś nie zauważyłem dużego zainteresowania tym tematem. Temat jest z pozoru trywialny ale moim zdaniem bardzo użyteczny. Nie zawsze wystarczy nam filtrowanie rodzajów błędu, czasami potrzebujemy rozróżnić dodatkowe warunki np:

WebClient wc = null;
try
{
   wc = new WebClient();
   var resultData = wc.DownloadString("http://google.com");
}
catch (WebException ex) when (ex.Status == WebExceptionStatus.ProtocolError)
{
   //code specifically for a WebException ProtocolError
}
catch (WebException ex) when ((ex.Response as HttpWebResponse)?.StatusCode == HttpStatusCode.NotFound)
{
   //code specifically for a WebException NotFound
}
catch (WebException ex) when ((ex.Response as HttpWebResponse)?.StatusCode == HttpStatusCode.InternalServerError)
{
   //code specifically for a WebException InternalServerError
}
finally
{
   wc?.Dispose();
}

Jak widać powyżej informacja że błąd jest klasy WebException niewiele nam mówi. Dopiero dodatkowe warunki pozwalają na ocenę i prawidłowe zalogowanie/zwrócenie rezultatu akcji. Aby dodać taki warunek powinniśmy użyć when.

Podsumowanie

Chciałbym wspomnieć że kod użyty w przykładach nie jest kodem produkcyjnym, jest możliwie najbardziej uproszczony dla pokazania implementacji. Nie jest to oczywiście również opis nowości tylko rzeczy które nie są stosowane często (o nowościach szczególnie w C# przeczytasz TUTAJ). Nie wiem czy te rozwiązania C# są w rzeczywistości mało znane, czy po prostu rzadko wykorzystywane. Wydaje mi się że chyba bardziej to drugie, nie wiem dlaczego. Jestem bardzo ciekawy jak to jest u Was? Jeżeli znajdziecie chwilkę bardzo proszę abyście pochwalili się w komentarzu jak to wygląda w waszych repozytoriach?

O autorze

Niepoprawny optymista. 100 pomysłów na sekundę, wielbiciel nowych technologii, nie tylko z rodziny .Net. Często nosi przy sobie jabłko, takie nadgryzione... ;)

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Możesz używać znaczników języka HTML i ich atrybutów: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

Witryna wykorzystuje Akismet, aby ograniczyć spam. Dowiedz się więcej jak przetwarzane są dane komentarzy.

Zamknij