ś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;
            }
        }
    }




Brak komentarzy:

Prześlij komentarz