sobota, 26 października 2019

Generowanie danych testowych

Dane testowe to szeroki temat. Dane takie potrzebne są do wstępnych testów aplikacji, testów obciążeniowych (load test) itp.
Dane takie można wprowadzić manualnie za pomocą skryptów SQL bądź UI aplikacji. Minusem takiego rozwiązania jest czas potrzebny na stworzenie odpowiedniej ilości rekordów.
Z pomocą przychodzą różnego rodzaju biblioteki, które pozwalają wygenerować dowolną liczbę danych testowych.
Jedną z takich bibliotek, które miałem ostatnio możliwość używać jest Bogus, który można dodać jako paczkę nugeta: https://www.nuget.org/packages/Bogus/
Bogus jest portem popularnej biblioteki faker.js. Autor zadbał aby korzystanie z niego było wygodne i pozwalało korzystać z smaczków C# jak fluent syntax.

Prosty przykład wykorzystania biblioteki:

Tworzymy prosty obiekt przechowujący dane np. osoby:

    public class Person
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Email { get; set; }

        public override string ToString()
        {
            return $"{nameof(FirstName)}: {FirstName}, {nameof(LastName)}: {LastName}, {nameof(Email)}: {Email}";
        }
    }

Następnie ściągamy paczkę nugeta z Bogusem:

PM> Install-Package Bogus

Kolejnym krokiem jest stworzenie obiektu klasy Faker, który pozwala na wypełnianie poszczególnych właściwości klasy danymi:

            var faker = new Faker<Person>()
                .RuleFor(p => p.FirstName, f => f.Person.FirstName)
                .RuleFor(p => p.LastName, f => f.Person.LastName)
                .RuleFor(p => p.Email, f => f.Person.Email);
            var personList = faker.Generate(10);
            personList.ForEach(Console.WriteLine);

Za pomocą metody RuleFor definiujemy jakimi danymi poszczególne właściwości klasy mają zostać wypełnione. W przykładzie powyżej zdefiniowaliśmy, że faker ma wypełnić imię, nazwisko oraz email. Po uruchomieniu aplikacji zobaczymy następujący efekt:



Dane takie możemy wykorzystać w dowolny sposób.

Bardzo fajnym rozwiązaniem jest także stworzenie klasy która dziedziczy po klasie FakerBase. Rozwiązanie to pozwala opakować kod tworzący dane testowe w różne scenariusze (możemy np. dla jednych testów generować więcej danych, dla innych mniej, stosować inne algorytmy czy też korzystać z wsparcia wielu języków - lokalizacja). Przykładowa klasa implementująca FakerBase:

    public class CustomerFaker : FakerBase
    {
        public static IList<Customer> GenerateCustomers(int count)
        {
            var customerFaker = new Faker<Customer>()
                .RuleFor(c => c.FirstName, f => f.Person.FirstName)
                .RuleFor(c => c.LastName, f => f.Person.LastName)
                .RuleFor(c => c.BirthDate, f => f.Person.DateOfBirth.ToString("d"))
                .RuleFor(c => c.Id, f => f.Random.Guid())
                .RuleFor(c => c.Chain, f => 5136)
                .RuleFor(c => c.Addresses, f => AddressFaker.GenerateAddresses())
                .RuleFor(c => c.Emails, f => EmailFaker.GenerateEmails())
                .RuleFor(c => c.Loyalties, f => LoyaltyFaker.GenerateLoyalties())
                .RuleFor(c => c.Phones, f => PhoneFaker.GeneratePhones())
                .RuleFor(c => c.Prefix, f => f.Name.Prefix())
                .RuleFor(c => c.Suffix, f => f.Name.Suffix())
                .RuleFor(c => c.MiddleName, f => f.Person.UserName);
            return customerFaker.Generate(count);
        }
    }

Czy warto korzystać z tej biblioteki? Myślę, że tak. Pisanie własnej od zera może i wydaje się fajnym wyzwaniem, jednak zajmie nam niepotrzebnie czas, który możemy poświęcić na inne aspekty rozwoju aplikacji. Lepszym pomysłem wydaje się fork rozwiązania (projekt jest na githubie) i dodanie potrzebnych nam rzeczy.

Biblioteka posiada także wersję premium. Wersja premium dodaje większą ilość słowników oraz plugin do VS, który ułatwia pisanie kodu przypisującego odpowiednie generatory danych dla naszych klas - kolejna oszczędność czasu :)

piątek, 25 października 2019

Parametry opcjonalne i nazwane

Jak każdy wie, nie jest to żadna nowość gdyż parametry opcjonalne i nazwane zostały dodane do języka C# już w wersji 4.0 
W tym poście chciałbym wspomnieć o jednej pułapce, którą łatwo można przeoczyć. 

Parametry opcjonalne pozwalają nam pominąć podanie wartości dla argumentu oznaczonego jako opcjonalny. Parametry nazwana łączą parametry metody z wartością za pomocą nazwy (w tradycyjnym modelu możemy przyjąć, że argumenty metody łączone są z wartościami kolejnością przesłanych parametrów).

    class Program
    {
        static void Main(string[] args)
        {
            var customer = CreateCustomer(); //all parameters will be 
            var customer1 = CreateCustomer("Patryk"); //assign only firstName, rest of parameters (optional) will be assigned with empty value
            var customer2 = CreateCustomer(email: "patryk.osowski@someemail.com"); //assign only email
            var customer3 = CreateCustomer(address: "Krakow, Wadowicka 4", firstName: "Sebastian", lastName: "Kowalski"); //assign address, firstName and lastName
        }

        public static Customer CreateCustomer(string firstName = "", string lastName = "", string address = "", string email = "")
        {
            return new Customer {FirstName = firstName, LastName = lastName, Address = address, Email = email};
        }
    }

    public class Customer
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Address { get; set; }
        public string Email { get; set; }
    }

Powyżej przykład wywołania który zapewne każdy z was widział i pewnie stosuje. 

Gdzie można stosować parametry nazwane i/lub opcjonalne? W metodach, indekserach, konstruktorach oraz delegatach. 

Jakie wartości można przypisywać w deklaracji typu wartościowego? Dozwolone wartości:
  • stałe wyrażenie, czyli wartość która może zostać wyliczona podczas kompilacji
  • element typu wyliczeniowego
  • wartość domyślna (string firstName = default)
Jeżeli zostanie użyte wyrażenie, które wymaga uruchomienia programu w celu jego obliczenie - kompilator zgłosi błąd podczas budowania projektu. 

Parametry opcjonalne nie mogą mieszać się z parametrami wymaganymi. W deklaracji sygnatury metody muszą występować na końcu. 

Można zadać pytanie - czy opcjonalne / nazwane parametry mają jakikolwiek wpływ na wydajność aplikacji? Nie mają. Kompilator podczas budowania kodu niejako "wrzuci" wartości w wywołanie metody. Z powyższego wynika jedna pułapka na, którą warto uważać. Najlepiej przedstawi to poniższy przykład:


Tworzymy dwa projekty - pierwszy to zwykła aplikacja konsolowa, druga to biblioteka z jedną klasą np. Sample jak w tym przypadku. Klasa Sample posiada jedną metodę z opcjonalnym parametrem:

using System;

namespace OptionalParameterAssembly
{
    public class Sample
    {
        public void PrintDefaultValue(int value = 5)
        {
            Console.WriteLine(value);
        }
    }
}

Aplikacja konsolowa posiada referencję do biblioteki i wywołuje metodę PrintDefaultValue:

            var sample = new Sample();
            sample.PrintDefaultValue();

Po uruchomieniu aplikacji nie zobaczymy nic nadzwyczajnego - zostanie wyświetlona wartość 5:


Przejdźmy do sedna: Zmieńmy wartość domyślną w naszej bibliotece i skompilujmy tylko ją. Następnie skopiujmy ją do aplikacji konsolowej:

        public void PrintDefaultValue(int value = 10)
        {
            Console.WriteLine(value);
        }







Jak myślicie co zostanie wyświetlone na ekranie po uruchomieniu OptionalNamedParameters.exe?


Dokładnie 5! Opcjonalne parametry łączone są z metodą w momencie kompilacji programu. Jeżeli więc używamy biblioteki i zawiera ona opcjonalne metody, należy zawsze pamiętać o skompilowaniu głównej aplikacji po zmianach w bibliotece. 

niedziela, 20 października 2019

Exception - kompendium wiedzy

Kolejnym ciekawym tematem, który chciałbym omówić są wyjątki w .NET. Ostatnie wersje języka C# przyniosły zmiany na które warto zwrócić uwagę.

Anomalie w kodzie można podzielić na trzy kategorie:

  1. Bugs -  błędy wprowadzone przez programistę (np. wyjście poza zakres tablicy)
  2. User Errors - błędy związane z danymi wprowadzanym przez użytkownika. Np. w pole numer pesel użytkownik wpisuje "Jan Kowalski"
  3. Exceptions - błędy, które trudno przewidzieć podczas pisania aplikacji. Przykładami takich błędów może być brak połączenia z bazą danych, usunięty plik z dysku, nieodpowiadający serwis itp.
C# od wersji 7.0 pozwala rzucać wyjątki zarówno w przypadku instrukcji (statement) jak i wyrażeń (expression). Czym się różnią? Najlepiej ilustruje to poniższy schemat:


W skrócie:
  • instrukcja - nie zwraca wartości
  • wyrażenie - po wykonaniu musi zwrócić jednoznaczną wartość
Wszystkie wyjątki w .NET dziedziczą po klasie System.Exception. Dodatkowo istnieje dodatkowa konwencja podziału wyjątków (o niej trochę więcej później w części poświęconej tworzeniu własnych wyjątków):
  • System level, dziedziczą po klasie System.SystemException. Wyjątki te rzucane są przez platformę .NET np. IndexOutOfRangeException
  • Application level, dziedziczą po klasie System.ApplicationException. Wyjątki zdefiniowane przez aplikację. Trzeba nadmienić, że większość programistów tworząc własne wyjątki dziedziczy z klasy System.Exception i jest to jak najbardziej poprawne technicznie.

Przykładowe rzucenie wyjątku i przechwycenie go celem wyświetlenia w konsoli:


            try
            {
                throw new Exception("My exception");
            }
            catch(Exception ex)
            {
                Console.WriteLine(ex);
            }



Klasa bazowa wyjątku System.Exception udostępnia kilka ciekawych właściwości z których możemy skorzystać. Zobaczmy je w akcji:


            try
            {
                var exception = new Exception("My exception")
                {
                    HelpLink = "http://OurPage.com/help"
                };
                exception.Data.Add("ExceptionTime", DateTime.UtcNow);
                throw exception;
            }
            catch(Exception ex)
            {
                Console.WriteLine(ex);
                foreach (var key in ex.Data.Keys)
                {
                    Console.WriteLine($"{key} : {ex.Data[key]}");
                }
                Console.WriteLine($"Look to documentation at : {ex.HelpLink}");
            }


Mamy tutaj do czynienia z dodatkowymi polami jak:
  • HelpLink - możemy przekazać użytkownikowi gdzie może szukać informacji jak rozwiązać dany problem
  • Data - kolekcja typu IDictionary przechowująca dane w postaci klucz wartość

Powyższe kawałki kodu prezentują podstawowy blok obsługi wyjątków. Zobaczmy na pełną wersję możliwości tego bloku:


            try
            {
                //Block that could throw exception
            }
            catch (Exception ex) when (1 == 1) //filter which can catch exception
            {
                //block that is executed only if error is thrown
            }
            finally
            {
                //block that will always execute - when exception is thrown and when not
            }


Słowa kluczowe

Słowa kluczowe które mamy do dyspozycji:
  • try - rozpoczyna blok kodu, w którym może zostać rzucony wyjątek
  • catch - bloku kodu który zostanie wywołany w momencie rzucenia wyjątku. Uwaga C# pozwala definiować catch bez zmiennej przyjmującej exception (try {} catch {}), nie otrzymujemy żadnych informacji o wyjątku jednak jest to jak najbardziej poprawna konstrukcja. 
  • when - nowość od C# 6.0 pozwala filtrować wyjątki. Blok ten może być zarówno prostą instrukcją warunkową bądź też metodą zwracającą wartość typu prawda / fałsz
  • finally - blok który zostanie wykonany niezależnie od tego czy wyjątek został zgłoszony czy nie. Służy przede wszystkim zamknięciu uchwytów do plików, połączeń do bazy danych i zwalnianiu innych zasobów które implementują interfejs IDispose 

Definiowanie własnych typów wyjątków

Definiując własne klasy wyjątków powinniśmy pamiętać o kilku podstawowych aspektach:
  • klasa wyjątku powinna mieć nazwę kończącą się na Exception np. MySuperException
  • klasa powinna być oznaczona modyfikatorem public, gdyż często zdarza się, że wyjątek będzie przesłany poza assembly w którym został wywołany
  • klasa powinna dziedziczyć po Exception/ApplicationException
  • klasa może posiadać dowolną ilość dodatkowych składowych (metod, pól, właściwości itp.)
  • klasa powinna być oznaczona atrybutem [System.Serializable]
  • klasa powinna posiadać domyślny konstruktor
  • powinien istnieć konstruktor przyjmujący parametr message i ustawiający ją w klasie bazowej
  • powinien istnieć konstruktor obsługujący serializacje
  • powinien istnieć konstruktor przyjmujący wewnętrzny wyjątek
Sporo zasad i dobra wiadomość dla użytkowników Visual Studio, który definiuje pomocny szablon tworzenia klas wyjątku. Wystarczy wpisać Exception i wcisnąć dwukrotnie przycisk tabulatora aby został stworzony szablon wyjątku:




Może nasuwać się pytanie po co tworzyć własne klasy wyjątków? Bardzo często służy to nie dodawaniu nowych pól czy właściwości. Chodzi o stworzenie silnie typowanego typu, który jednoznacznie identyfikuje dany rodzaj błędu dzięki czemu programista w kodzie może lepiej i łatwiej obsłużyć dany wyjątek.  

Kolejność przetwarzania wyjątków

Wyjątki przetwarzane są w kolejności którą zdefiniujemy. W przypadku gdy mamy wiele bloków catch wyjątki powinny być uszeregowane od najbardziej szczegółowego do najbardziej generycznego. Przykładowo:


Powyższy kod nie będzie kompilował się poprawnie. Wyjątek ArgumentNullException jest bardziej szczegółowy. Kolejność przechwytywania powinna zatem zostać zmieniona. 

Ponowne rzucenie wyjątkiem

Czasami zdarza się, że po przechwyceniu wyjątku chcemy go mimo wszystko rzucić ponownie. Przykładowo tworzymy bibliotekę, która ma za zadanie pobrać dane z serwisu pogody. Może zdarzyć się problem z połączeniem do serwera (np. jest chwilowo niedostępny). Łapiemy więc wyjątek np. TimeoutException, logujemy jego dane do pliku i mimo wszystko chcemy dać programiście informacje, że z serwerem jest coś nie tak. Przykład:


            try
            {
                //Check weather service
            }
            catch(TimeoutException ex)
            {
                System.Diagnostics.Debug.WriteLine("Weather servie is unavailable");
                throw;
            }


W tym miejscu ważna uwaga. Słówko kluczowe throw występuje samo w tym kontekście. Warto wiedzieć że możemy w kodzie znaleźć także drugą wersję throw ex; Czym one się różnią?
  • throw - rzuca wyjątek i zachowuje oryginalny stack trace wywołania
  • throw ex - zawija stack trace i tworzy go od nowa (od aktualnie wykonywanej linijki)
Co to oznacza w praktyce? Używając konstrukcji throw ex tracimy informację o oryginalnym miejscu gdzie został rzucony wyjątek. Warto wiedzieć o tym, gdyż jest to bardzo częste pytanie na rekrutacjach. Dodatkowo może powodować wiele niejasności i wprowadzać deweloperów w błąd co do miejsca gdzie błąd występuje. 

Przekazywanie wewnętrznych wyjątków

W niektórych sytuacja dochodzi do sytuacji, w której pomimo dobrych chęci w bloku catch zostanie rzucony wyjątek. Co w takim przypadku powinniśmy zrobić? Powinniśmy ten wyjątek zwrócić jako wewnętrzny wyjątek pierwotnego. Jak możemy to zrobić? Za pomocą konstruktora:


            try
            {
                //Some code throwin MySuperException
            }
            catch (MySuperException ex)
            {
                try
                {
                    //Do something that throws exception
                }
                catch (ArgumentNullException ex2)
                {
                    throw new MySuperException(ex.Message, ex2);
                }
            }


Błędy których nie jesteśmy w stanie przechwycić

Są to specjalne typy błędów, które mimo najlepszych bloków catch nie jesteśmy w stanie przechwycić. Na nasze szczęście nie jest takich wyjątków zbyt dużo:
  • StackOverflowException - błąd przepełnienia stosu. Rzucenie tego wyjątku powoduje automatyczne wykrzaczenie aplikacji. Nie ma w tym nic dziwnego, jeżeli skończyło się miejsce na stosie to nie ma nawet pamięci aby obsłużyć błąd.
  • ThreadAbortException- w tym przypadku mamy możliwość przechwycenia wyjątku, jednak pod koniec bloku błąd zostanie automatycznie rzucony ponownie czy tego chcemy czy nie. Istnieje jedna możliwość przechwycenia tego wyjątku - użycie metody Thread.ResetAbort() w bloku catch. Nie jest to zalecane i powinno się raczej przemyśleć architekturę aplikacji.
  • AccessViolationException - błąd zgłaszany gdy zostanie nadpisana pamięć podczas pracy nad zasobami niezarządzanymi. Błąd ten mimo wszystko można przechwycić, korzystając z atrybutu [HandleProcessCorruptedStateExceptionsAttribute] który musi być zadeklarowany w metodzie rzucającej ten typ wyjątku

Blok using

W tym miejscu trudno pominąć bloku using. Blok prezentuje się następująco:

            using(var file = new StreamWriter(@"c:\Pliki\plik.txt"))
            {
                file.WriteLine("Some text");
            }


Jak wiadomo blok using zapewnia nam wyczyszczenie zasobów niezarządzanych w przypadku problemów z kodem. W tym przypadku jeżeli nie udałoby się zapisać pliku, na zmiennej file zostanie wywołana metoda Dispose.

Dekompilując powyższy kod możemy łatwo zobaczyć że jest on kompilowany do postaci try { } finally {} zapewniając wywołanie metody Dispose. Ot cała magia :)



środa, 16 października 2019

Struktury kompendium wiedzy

Bardzo często podczas rekrutacji na programistę .NET można usłyszeć pytanie "czym różni się struktura od klasy" lub "proszę powiedzieć coś o strukturach w .NET". Odpowiedź na to pytanie najczęściej sprowadza się do zdania typu "struktura jest typem wartościowym a klasa referencyjnym".

Powyższe stwierdzenie jest oczywiście jak najbardziej prawdziwe, jednak struktura posiada znacznie więcej ciekawy cech które warto sobie przyswoić. Celem tego postu jest stworzenie małego kompendium na temat struktur, coś w stylu "wszystko co byś chciał wiedzieć o strukturze".


1. Strukturę deklarujemy za pomocą słówka kluczowego strcut


    public struct HouseNumber
    {
        public int Number { get; }

        public HouseNumber(int number)
        {
            Number = number;
        }
    }

2. Struktury należą do typów wartościowych, dziedziczą po ValueType, a ten po Object



3. Stworzone do reprezentowania "lekkich obiektów" w .NET przykładami struktur może być Point, Rectangle, Color


4. Wielkość nie powinna przekraczać 16 bajtów


5. Struktury przekazywane są przez wartość. Co to oznacza? Jeżeli prześlemy strukturę do metody to automatycznie tworzona jest jej kopia. Podczas operowania na strukturze nie zmodyfikujemy oryginalnej struktury. Ukazuje to dobrze poniższy przykład:


Po opuszczeniu metody, która w zamyśle ma zmodyfikować wartość struktury wartość wyświetlona pozostaje nadal 50. Stało się tak dlatego iż w metodzie ModifyStructure operowaliśmy na kopi a nie na oryginale.


5. Dobrą praktyką jest tworzenie struktur które inicjujemy i nie możemy zmienić ich wartości - immutable. Dlaczego? Łatwo wpaść w pułapkę, która wynika z poprzedniego punktu. Wyobraźmy sobie sytuację, że refaktorujemy nasz kod i wydzielamy kawałek modyfikujący wartość struktury do osobnej metody. Bum! Modyfikacja nie będzie mieć miejsca.
Kolejnym argumentem jest łatwiejsza praca z kodem używającym wielowątkowości. Typy immutable są wręcz stworzone do pracy z wieloma wątkami.


6. Nie powinny podlegać częstemu boxingowi / unboxingowi. Jak wiadomo z innych typów boxing/unboxing jest kosztowny i dlatego zamiast typu ArrayList (o którym pewnie dzisiaj już tylko w muzeum kodu można usłyszeć) używamy typów generycznych np. List<T>


7. Nie mogą posiadać domyślnego konstruktora



    public struct HouseNumber
    {
        public int Number { get; set; }

        public HouseNumber() //Error CS0568  Structs cannot contain explicit parameterless constructors

        {
            Number = 10;
        }

        public HouseNumber(int number)
        {
            Number = number;
        }
    }



8. Nie mogą dziedziczyć po sobie, ani po żadnej klasie. Klasa także nie może dziedziczyć po strukturze.


9. Mogą implementować interfejsy



    public struct HouseNumber : IEquatable<HouseNumber>
    {
        public int Number { get; set; }

        public HouseNumber(int number)
        {
            Number = number;
        }

        public bool Equals(HouseNumber other)
        {
            return Number == other.Number;
        }

        public override bool Equals(object obj)
        {
            return obj is HouseNumber other && Equals(other);
        }

        public override int GetHashCode()
        {
            return Number;
        }
    }



10. W przypadku klas zmienna przechowuje referencję do obiektu czyli zmienna z adresem znajduje się na stosie a dane na stercie. Dane struktury przechowywane są bezpośrednio w zmiennej gdzie są zadeklarowane.


11. Przypisują zmienną typu struct do innej zmiennej typu struct kopiujemy wartość. Tworzymy tym samym dwa niezależne byty.


12. Struktura nie może posiadać metod abstrakcyjnych ani wirtualnych. Może nadpisywać tyko metody znajdujące się w typie ValueType/Object.


13. Może zostać użyta jako typ Nullable<T>


14. Deklarując strukturę można to zrobić na dwa sposoby - ze słówkiem new i bez niego. Przykład:


15. Porównywanie struktur. Temat bardzo ważny i pomijany. Domyślnie struktury nie można porównywać za pomocą operatora "==". Można oczywiście dodać jego implementację.
W jaki sposób porównujemy struktury? Używamy do tego metody Equals.
Jest tu jednak mały haczyk. Jeżeli nie nadpiszemy Equals własną implementacją, zostanie użyta implementacja z klasy bazowej. Implementacja ta bazuje na refleksji. Oznacza to dla nas duży spadek wydajności. W drugim przypadku, który testowałem przyspieszenie było praktycznie 13 krotne! Dobrą radą jest nadpisywanie Equals dla każdej struktury którą tworzymy.


    public class CompareStructures
    {
        public static void Compare()
        {
            Address a1 = new Address(10, 15);
            Address a2 = new Address(10, 15);

            Console.WriteLine("Compare simple structure without reference members.");
            Console.WriteLine($"a1 == a2 ? {a1.Equals(a2)}");
            var stopwatch = Stopwatch.StartNew();
            for (int i = 0; i < 1000000; i++)
            {
                bool areEqual = a1.Equals(a2);
            }
            stopwatch.Stop();
            Console.WriteLine($"1 mln structure comparision for simple structure msc: {stopwatch.ElapsedMilliseconds}");
            Console.WriteLine();


            AddressWithEquals awe1 = new AddressWithEquals(10, 15);
            AddressWithEquals awe2 = new AddressWithEquals(10, 15);

            Console.WriteLine("Compare simple structure without reference members.");
            Console.WriteLine($"a1 == a2 ? {awe1.Equals(awe2)}");
            stopwatch = Stopwatch.StartNew();
            for (int i = 0; i < 1000000; i++)
            {
                bool areEqual = awe1.Equals(awe2);
            }
            stopwatch.Stop();
            Console.WriteLine($"1 mln structure comparision for simple structure with equals msc: {stopwatch.ElapsedMilliseconds}");
            Console.WriteLine();


            BigAddress ba1 = new BigAddress(10, 20, "Andrychow");
            BigAddress ba2 = new BigAddress(10, 20, "Andrychow");
            Console.WriteLine("Compare simple structure with reference members.");
            Console.WriteLine($"ba1 == ba2 ? {ba1.Equals(ba2)}");

            stopwatch = Stopwatch.StartNew();
            for (int i = 0; i < 1000000; i++)
            {
                bool areEqual = ba1.Equals(ba2);
            }
            stopwatch.Stop();
            Console.WriteLine($"1 mln structure comparision for complex structure msc: {stopwatch.ElapsedMilliseconds}");

            BigAddressWithEquals bawe1 = new BigAddressWithEquals(10, 20, "Andrychow");
            BigAddressWithEquals bawe2 = new BigAddressWithEquals(10, 20, "Andrychow");
            Console.WriteLine("Compare complex structure with equal override.");
            Console.WriteLine($"ba1 == ba2 ? {bawe1.Equals(bawe2)}");

            stopwatch = Stopwatch.StartNew();
            for (int i = 0; i < 1000000; i++)
            {
                bool areEqual = bawe1.Equals(bawe2);
            }
            stopwatch.Stop();
            Console.WriteLine($"1 mln structure comparision for structure with equals: {stopwatch.ElapsedMilliseconds}");
        }
    }

    public struct Address
    {
        public int HouseNumber { get; }
        public int FlatNumber { get; }

        public Address(int houseNumber, int flatNumber)
        {
            HouseNumber = houseNumber;
            FlatNumber = flatNumber;
        }
    }

    public struct AddressWithEquals
    {
        public int HouseNumber { get; }
        public int FlatNumber { get; }

        public AddressWithEquals(int houseNumber, int flatNumber)
        {
            HouseNumber = houseNumber;
            FlatNumber = flatNumber;
        }

        public bool Equals(AddressWithEquals other)
        {
            return HouseNumber == other.HouseNumber && FlatNumber == other.FlatNumber;
        }

        public override bool Equals(object obj)
        {
            return obj is AddressWithEquals other && Equals(other);
        }

        public override int GetHashCode()
        {
            unchecked
            {
                return (HouseNumber * 397) ^ FlatNumber;
            }
        }
    }

    public struct BigAddress
    {
        public int HouseNumber { get; }
        public int FlatNumber { get; }
        public string FirstLineOfAddress { get; }

        public BigAddress(int houseNumber, int flatNumber, string firstLineOfAddress)
        {
            HouseNumber = houseNumber;
            FlatNumber = flatNumber;
            FirstLineOfAddress = firstLineOfAddress;
        }
    }

    public struct BigAddressWithEquals
    {
        public int HouseNumber { get; }
        public int FlatNumber { get; }
        public string FirstLineOfAddress { get; }

        public BigAddressWithEquals(int houseNumber, int flatNumber, string firstLineOfAddress)
        {
            HouseNumber = houseNumber;
            FlatNumber = flatNumber;
            FirstLineOfAddress = firstLineOfAddress;
        }

        public bool Equals(BigAddress other)
        {
            return HouseNumber == other.HouseNumber && FlatNumber == other.FlatNumber && FirstLineOfAddress == other.FirstLineOfAddress;
        }

        public override bool Equals(object obj)
        {
            return obj is BigAddress other && Equals(other);
        }

        public override int GetHashCode()
        {
            unchecked
            {
                var hashCode = HouseNumber;
                hashCode = (hashCode * 397) ^ FlatNumber;
                hashCode = (hashCode * 397) ^ (FirstLineOfAddress != null ? FirstLineOfAddress.GetHashCode() : 0);
                return hashCode;
            }
        }
    }




poniedziałek, 14 października 2019

Atrybuty Caller.* oraz DebuggerTypeProxy

Dzisiaj kilka przydatnych atrybutów.

Zacznijmy od atrybutów które pozwalają sprawdzić kto i skąd wywołuje naszą metodę. Oczywiście ktoś może zapytać: po co nam to skoro IDE jest w stanie znaleźć wszystkie użycia danej metody. Dla większości przypadków będzie to prawda. Zdarza się jednak, że piszemy bibliotekę, która następnie dodawana jest przez różne aplikacje. W takim przypadku może się przydać nam informacja kto i skąd używa naszej metody bibliotecznej.
Do dyspozycji mamy 3 atrybuty: CallerMeemberName, CallerFilePath, CallerLineNumber. Przykładowe użycie najlepiej prezentuje ich możliwości:


    public class UsageDetails
    {
        public static void Method([CallerMemberName] string callerMemeberName = null, [CallerFilePath] string callerFilePath = null, [CallerLineNumber] int callerLineNumber = 0)
        {
            Console.WriteLine(callerMemeberName);
            Console.WriteLine(callerFilePath);
            Console.WriteLine(callerLineNumber);
        }
    }


Po odpaleniu apki na ekranie ukaże się taki wynik:


Mamy więc informację z jakiego pliku (dokładna ścieżka) został wywołany kod, w jakiej metodzie (Main) oraz numer linii kodu (9).

Należy przy tym zwrócić uwagę, że parametry oznaczone atrybutem Caller- są opcjonalnymi parametrami.

Poza diagnostyką atrybut CallerMemberName może przydać się w aplikacjach używających interfejsu INotifyPropertyChanged (np. WPF). W takim przypadku nie musimy podawać nazwy właściwości jako łańcucha znaków.


    public class Customer : NotifyPropertyChanged
    {
        private string firstName;
        private string lastName;

        public string FirstName
        {
            get
            {
                return firstName;
            }
            set
            {
                firstName = value;
                OnPropertyChanged();
            }
        }

        public string LastName
        {
            get { return lastName; }
            set
            {
                lastName = value;
                OnPropertyChanged();
            }
        }
    }

    public abstract class NotifyPropertyChanged : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        protected void OnPropertyChanged([CallerMemberName]string name = null)
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
        }
    }



Nie musimy w takim przypadku za każdym razem podawać właściwości jako string, co też zwiększa bezpieczeństwo kodu. Robiąc refactoring nie musimy się obawiać że pominiemy któryś ze stringów.


Kolejnym ciekawym atrybutem jest DebuggerTypeProxy. Atrybut ten pozwala zmienić sposób w jaki debugger Visual Studio wyświetla obiekt. Wykorzystanie atrybutu obrazuje prosty przykład:

    public class Person
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public int Age { get; set; }
        public IList<Phone> Phones { get; set; } = new List<Phone>(2);
    }

    public class Phone
    {
        public string Value { get; set; }
        public string Type { get; set; }
    }


Utwórzmy nowy obiekt i zobaczmy jak wygląda obiekt w debugu:





Wygląda normalnie - tak jakbyśmy tego oczekiwali. Atrybut DebuggerTypeProxy pozwala zmienić sposób wyświetlania. Dla przykładu scalmy telefony do jednej linii, a imię i nazwisko wyświetlmy jako FullName:


    public class PersonDebuggerProxy
    {
        private readonly Person obj;

        public PersonDebuggerProxy(Person obj)
        {
            this.obj = obj;
        }

        public string FullName { get => $"{obj.FirstName} {obj.LastName}";  }

        public int Age { get => obj.Age; }

        public string Phones
        {
            get
            {
                return string.Join("; ", obj.Phones?.Select(phone => $"{phone.Type} : {phone.Value}"));
            }
        }
    }

Następnie do klasy Person dodajemy deklarację atrybutu:

    [DebuggerTypeProxy(typeof(PersonDebuggerProxy))]
    public class Person


Jak teraz możecie się domyślić, obiekt zostanie wyświetlony w sposób zdefiniowany przez klasę Proxy:



Formatowanie zostało uwzględnione. Stosując atrybut DebuggerTypeProxy konstruktor klasy proxy musi przyjąć obiekt typu który chcemy formatować. Pozostałe operacje to kwestia wyobraźni i potrzeb.