czwartek, 20 sierpnia 2020

Readonly struct w C# 7.2

Począwszy od C# 7.2 możemy deklarować struct który jest readonly:

    public readonly struct Point3D
    {
        public double X { get; }
        public double Y { get; }
        public double Z { get; }

        public Point3D(double x, double y, double z)
        {
            X = x;
            Y = y;
            Z = z;
        }
    }

Jakie zalety powoduje dodanie słówka readonly w przypadku struktur?
  • struktura jest całkowicie niemodyfikowalna z zewnątrz i wewnątrz
  • wszystkie pola muszą być typu readonly
  • właściwości nie mogą posiadać setterów

Oprócz powyższych zalet jest jeszcze jednak o której warto wspomnieć a dotyczy ona - wydajności. Jeżeli w jakimś innym typie zadeklarujemy obiekt struktury tylko do odczytu jako pole / właściwość readonly kompilator może uniknąć kopiowania jej gdy ktoś z niej skorzysta. Przykład:

    class Program
    {
        static void Main(string[] args)
        {
            var rectangle = new Rectangle(new Point3D(1, 2, 3));
            var calculationResult = rectangle.A.DoSomeCalculation();
            Console.WriteLine(calculationResult);
        }
    }

    public class Rectangle
    {
        public Point3D A { get; }

        public Rectangle(Point3D a)
        {
            A = a;
        }
    }

    public readonly struct Point3D
    {
        public double X { get; }
        public double Y { get; }
        public double Z { get; }

        public Point3D(double x, double y, double z)
        {
            X = x;
            Y = y;
            Z = z;
        }

        public double DoSomeCalculation()
        {
            return X * Y * Z;
        }
    }

Normalnie kompilator C# stworzyłby kopię obiektu Point3D podczas dostępu do niego ze zmiennej rectanble. Kompilator nie wie czy struktura może spowodować zmianę w niej np. poprzez wywołanie metody DoSomeCalculation(). Jednak jako że struktura jest oznaczona słowem readonly nie zostanie wykonana jego kopia i kompilator wie, że ma wykonać metodę DoSomeCalculation bezpośrednio na obiekcie składowy rectangle.

środa, 19 sierpnia 2020

Dlaczego niektóre metody przyjmujące tablice obiektów params oferują także przeładowania dla pojedynczych parametrów?

Zastanawialiście się może kiedyś dlaczego string.Format oferuje oprócz przeładowania przyjmującego dowolną ilość argumentów - params - także wersje pozwalające przesłać 1, 2 lub 3 argumenty?
Czy nie wydawało Wam się to dziwne?



Odpowiedź na to pytanie nie jest trudna. Operacje na stringach to dosyć popularne operacje. korzystając za każdym razem z wersji params powodujemy utworzenie tablicy. Tworzenie obiektów na stosie jest mimo wszystkich procesem kosztownym. Dzięki przeładowaniom z poszczególnymi argumentami, nie jest konieczne tworzenie tablicy za każdym razem.  

wtorek, 18 sierpnia 2020

Referencje do zmiennych i zwracanych wartości

Poprzedni artykuł omówił sposoby przekazywania przez referencję argumentów do metod. Teraz zobaczmy w jaki sposób możemy uzyskać referencję do zmiennych oraz wyników metod. 

            string s1 = "Ala ma kota";
            ref string ref1 = ref s1;

            ref1 = "Override value";
            Console.WriteLine(s1);

Jakie są ograniczenia lokalnych referencji?
  • Po pierwsze kompilator nie pozwoli stworzyć referencji do zmiennej, która zostanie usunięta przed referencją (zakres widzialności zmiennej i referencji musi być taki sam).
  • Nie można stosować ref dla metod asynchronicznych (async)
  • nie można używać w iteratorach
  • nie można używać w funkcjach anonimowych

Warto jednak zaznaczyć, że ref można zastosować dla właściwości. Dzieje się tak dlatego, że właściwości są przekształcane do metod get i set. Generalna zasada jest taka, że kompilator musi mieć pewność że referencja nie "przeżyje" obiektu do którego ta referencja się odnosi.

Zobaczmy teraz kilka przykładów:

    public class Sample
    {
        public ref int WillFail()
        {
            int i = 42;
            return ref i;
        }
    }
Kod ten nie skompiluje się gdyż, zmienna i przestanie istnieć po opuszczeniu zasięgu metody WillFail.

    public class Sample
    {
        public ref int GetSameRef(ref int arg) => ref arg;

        public ref int WillFail()
        {
            int i = 42;
            return ref GetSameRef(ref i);
        }
    }

Kod ten także nie zadziała. Próbujemy poprzez dodatkową funkcję zwrócić referencję do zmiennej i która po opuszczeniu zasięgu metody WillFail zostanie usunięta. 

    public class Sample
    {
        public ref int GetSameRef(ref int arg) => ref arg;

        public ref int WillWork(ref int i)
        {
            return ref GetSameRef(ref i);
        }
    }

Ten przypadek zadziała poprawnie. Zwracamy referencję do zmiennej, która zostanie przekazana z zewnątrz a więc "przeżyje" lokalny zasięg. 

    public class Sample
    {
        private int i;
        private int[] tab = new int[5];
        public ref int ReferenceToField => ref i;
        public ref int ReferenceToArrayElement(int index) => ref tab[index];
    }

Powyższy przykład także będzie kompilował się poprawnie - zostanie zwrócona referencja do pola oraz elementu tablicy. Jest to możliwe dlatego, że składowe klasy żyją na stosie i garbage collector jest świadomy, aby ich nie usuwać. 

Jaka jest realna zaleta używania ref dla lokalnych zmiennych i wartości zwracanych? Tak samo jak w przypadku przesyłania dużych struktur danych możemy zaoszczędzić moc, która byłaby marnowana na kopiowanie obiektów. 

poniedziałek, 17 sierpnia 2020

Przesyłanie argumentów przez referencje

Temat dobrze znany, ale wraz z rozwojem C# zwłaszcza siódmej i ósmej wersji postanowiłem ponownie podejść do tematu. 

Przed wprowadzeniem Tupli w siódmej wersji języka, aby zwrócić więcej niż jedną wartość z metody mogliśmy zastosować słowo kluczowe out. Ponieważ słowo kluczowe out istnieje od początku języka C# możemy się z nim spotkać w wielu miejscach - zwłaszcza przy próbach konwersji:

            //1. Parsing string to int
            string someText = "5";
            bool canParse = int.TryParse(someText, out int a);
            if (canParse)
            {
                Console.WriteLine($"The value is {a}");
            }
            else
            {
                Console.WriteLine("Can't parse");
            }

            //2. Try get value from dictionary
            var data = new Dictionary<string, int>
            {
                ["Adam"] = 30,
                ["Sebastian"] = 50,
                ["Marek"] = 35
            };
            string searchKey = "Adam";
            var canGetValueByKey = data.TryGetValue(searchKey, out int age);
            if(canGetValueByKey)
            {
                Console.WriteLine($"{searchKey} is {age}");
            }
            else
            {
                Console.WriteLine("Key not present in dictionary");
            }

Sama deklaracja funkcji Parse dla typu int wygląda następująco:

        public static bool TryParse(String s, out Int32 result) 
        {
            return Number.TryParseInt32(s, NumberStyles.Integer, NumberFormatInfo.CurrentInfo, out result);
        }

Zarówno w funkcji jak i jej wywołaniu musimy jawne podać słówko out. 

Warto w tym miejscu zaznaczyć, że metody oznaczone słówkiem kluczowym async nie mogą posiadać argumentów out. Jest to spowodowane faktem, że metody async nie determinują jednoznacznie kiedy zostaną wykonane w pełni. Metoda może wykonać się w części i zwrócić kontrolę do głównej funkcji, która z kolei także może zwrócić kontrolę do metody wołającej. Tym sposobem zmienne przekazane przez out mogłyby już nie istnieć. Ta sama zależność jest związana z funkcjami anonimowymi, do których także nie możemy przesłać parametru za pomocą słówka out.

Czasami może się zdarzyć, że wartość parametru out nie jest nam potrzebna. Przykładowo chcemy tylko sprawdzić czy wartość można skonwertować czy też nie. Jeżeli nie interesuje nas w takim przypadku wartość możemy zastosować tzw. discard operator:

            string someText = "5";
            bool canParse = int.TryParse(someText, out _);

Przed erą C# 7.0 musielibyśmy zadeklarować zmienną, której wartość byłaby po prostu nie używana. 


Drugą metodą przekazywania zmiennych przez referencję jest użycie słówka ref. Działa podobnie jak słówko out z tą różnicą, że zmienna przesyłana do metody musi zostać zainicjowana przed wysłaniem:

        static async Task Main(string[] args)
        {
            int value = 20;
            DoSomething(ref value);
            Console.WriteLine(value);
        }

        public static void DoSomething(ref int value)
        {
            value = 10;
        }


Trzecią opcją jest nowe słówko (począwszy od C# 7.2) in. Słówko to pozwala na przesłanie obiektu przez referencję ale tylko do odczytu. Zachodzi tu relacja out - wysyłamy zmienną aby ją nadpisać, in - zmienna tylko do odczytu. Można zapytać po co wprowadzono słówko in? Na pewno nie przyda się ono do przesłania obiektów typu int. Obiekty wartościowe (np. duże struktury) przesyłane są przez wartość, czyli krótko mówiąc przesyłamy ich kopię do metody. Kopiowanie dużej ilości obiektów może obniżyć wydajność naszej aplikacji. In pozwala uniknąć kopiowania - wszak prześlemy kopię referencji (32 lub 64 bity):

        static async Task Main(string[] args)
        {
            var point3D = new Point3D(1, 2, 3);
            DoSomething(point3D);
        }

        public static void DoSomething(in Point3D point)
        {
            Console.WriteLine(point);
            
            //point = new Point3D(3, 4, 5); //Error CS8331  Cannot assign to variable 'in Point3D' because it is a readonly variable
        }
    }

    public readonly struct Point3D
    {
        public double X { get; }
        public double Y { get; }
        public double Z { get; }

        public Point3D(double x, double y, double z)
        {
            X = x;
            Y = y;
            Z = z;
        }

        public override string ToString()
        {
            return $"X:{X} Y:{Y} Z:{Z}";
        }
    }

Należy zauważyć, że struktura została oznaczona jako readonly. Jeżeli by nie została, kompilator przyjąłby agresywniejszą metodę i stworzył mimo wszystko kopię struktury Point3d.

W kolejnej części zajmę się referencjami do zmiennych i zwracanych wyników. 

poniedziałek, 20 lipca 2020

Tuple

Temat stosowania tupli ma tylu samo przeciwników ilu zwolenników. Wraz z wejściem C# 7.0 Tuple zyskał nową składnię, która ułatwia pracę z nimi. Na początek zobaczmy co się zmieniło a później zastanówmy się kiedy ich używać.

Najprostsza postać Tupla to ta znana przed C# 7.0:

            var person = new Tuple<string, string, int>("Jacek", "Kowalski", 10);
            Console.WriteLine($"My name is {person.Item1} {person.Item2}. I'm {person.Item3} years old.");

Składnia ta nie pozwala na nazwanie poszczególnych składowych. Do poszczególnych składowych odwołujemy się poprzez składowe ItemX gdzie X to numer pola.
Zastosowanie pól numerowanych nie jest wygodnym rozwiązaniem i z pewnością nie motywowało do częstego używania Tupli. Wszystko zmieniło się wraz z C# 7.0 gdzie wprowadzono nową składnię:

            (string FirstName, string LastName, int Age) person = ("Jacek", "Kowalski", 10);
            Console.WriteLine($"My name is {person.FirstName} {person.LastName}. I'm {person.Age} years old.");

Zapis możemy skrócić korzystając z słówka var:

            var person = (FirstName: "Jacek", LastName: "Kowalski", Age: 10);
            Console.WriteLine($"My name is {person.FirstName} {person.LastName}. I'm {person.Age} years old.");

Zapis możemy jeszcze bardziej skrócić, jednak wtedy wracamy do dawnej konwencji nazywania składowych Tupla Item:

            var person = ("Jacek", "Kowalski", 10);
            Console.WriteLine($"My name is {person.Item1} {person.Item2}. I'm {person.Item3} years old.");

Warto wspomnieć jeszcze o dekonstrukcji Tupla na jego poszczególne składowe. Oczywiście do poszczególnych składowych można odnieść się po nazwie pola / ItemX. Warto jednak wiedzieć, że istnieje alternatywa:

            var (firstName, lastName, age) = person;
            Console.WriteLine($"My name is {firstName} {lastName}. I'm {age} years old.");

Teraz zastanówmy się nad tym kiedy używać Tupli a kiedy nie. Zacznijmy od kilku ich właściwości, które mogą pomóc zdecydować czy je używać czy nie:
  1. Zdefiniowane nazwy składowych Tupla nie są stałe - podczas działania programu znikają.
  2. Nowy Tupl to instancja struktury ValueTuple<T> - typ wartościowy, więc przekazywany jest przez wartość. Problem częstego kopiowania można zaadresować przez zastosowanie ref local / ref return
  3. Tuple pozwalają na porównywanie za pomocą == czy !=

Kiedy więc warto skorzystać z Tupli? Z pewności w metodach prywatnych biblioteki czy klasy. Pozwala to uniknąć tworzenia kolejnych definicji obiektów. W przypadku publicznych API nie jest zalecane używanie Tupli.

sobota, 14 marca 2020

Hierarchia plików konfiguracyjnych w ASP.NET

Obecnie większość z Was pewnie tworzy aplikacje w .NET Core. Konfiguracja w .NET Core znacząco różni się od tej znanej z ASP.NET. Niemniej jednak ostatnio musiałem prześledzić zgłoszenie klienta w jednej ze starszych aplikacji. Ponieważ nie do końca wiedziałem dlaczego jedno z ustawień pobieranych z Web.configa ma taką a nie inną wartość, musiałem prześledzić hierarchię ustawień.
Konfiguracja w ASP.NET ma strukturę hierachiczną:


Na samej górze hierarchii znajduje się Machine.config - jest to globalny plik ustawień dla każdej aplikacji .NET. Kolejnym miejscem gdzie możemy wprowadzić ustawienia jest globalny Web.config. Plik ten nadpisuje część ustawień machine.config jak i dodaje nowe.
Kolejne poziomy to:

  • katalog witryn
  • pojedyncza aplikacja webowa
  • folder w aplikacji webowej
Ostatni poziom hierarchii jest o tyle ciekawy, że w aplikacji webowej dla każdego podfolderu możemy zdefiniować osobny plik ustawień.

Ustawienia są oczywiście cachowane, przez co nie są za każdym razem odczytywane z dysku twardego co przyspiesza aplikację.  

poniedziałek, 9 marca 2020

Rola klienta i serwera

Często podczas rozmowy kwalifikacyjnej pada pytanie o rolę serwera / klienta aplikacji internetowej. Postaram się w kilku słowach streścić prawidłową odpowiedź na to pytanie.
Zacznijmy od tego czym jest klient i serwer:

  • klient - w przypadku aplikacji internetowej jest to przeglądarka, pozwala wyświetlać interfejs aplikacji i wysyłać komendy do serwera w celu ich przetworzenia
  • serwer - zwraca żądane przez klienta strony; odpowiedź z serwera zawiera kod HTML który determinuje sposób wyświetlenia żądania klienta
Zarówno klient jak i serwer komunikują się ze sobą za pomocą protokołu HTTP. Protokół ten używa portu 80, lub jeżeli serwer używa certyfikatu SSL - HTTPS na porcie TCP 443.

Proces uzyskiwania połączenia:
  1. Użytkownik wprowadza w przeglądarce adres żądanej strony
  2. Przeglądarka za pomocą protokołu HTTP wysyła żądanie GET do serwera
  3. Serwer WWW otrzymuje zapytanie i rozpoczyna je przetwarzać
  4. Jeżeli zapytanie jest poprawne, serwer zwraca odpowiedź wraz z kodem statusu 200. W przeciwnym przypadku, np. jeżeli adres strony jest błędny, zwraca status błędu 404.
  5. Przeglądarka internetowa użytkownika otrzymuje odpowiedź wraz z kodem statusu. Jeżeli kod jest równy 200 wyświetla zwróconą stronę. Jeżeli kod błędu jest inny, np. 404 wyświetla błąd.
Rola serwera:
  1. Walidacja przychodzącego żądania (requestu)
  2. Uwierzytelnienie - jeżeli serwer używa certyfikatu SSL
  3. Uwierzytelnienie użytkownika jeżeli aplikacja wymaga podania użytkownika i hasła
  4. Autoryzacja użytkownika - sprawdzenie czy ma dostęp do żądanych treści
  5. Przetworzenie żądania
  6. Obsługa błędów
  7. Wynik odpowiedzi może zostać zapamiętany w pamięci (cache) co pozwoli przetworzyć kolejne, takie samo zapytanie znacznie szybciej
  8. Kompresja wyniku w celu zredukowania ilości przesyłanych danych
  9. Logowanie danych zapytania, statystyk
Rola klienta:
  1. Przesyłanie żądań do serwera
  2. Uwierzytelnianie serwera gdy ten dysponuje certyfikatem SSL
  3. Przetworzenie odpowiedzi z serwera i wyświetlenie odpowiedzi
  4. Uruchamianie skryptów po stronie przeglądarki (JavaScript)
Niektóre komendy HTTP:
  • GET - pobieranie danych
  • POST - wysyłanie danych do serwera (np. pola formularza)
  • PUT - pozwala tworzyć dane bezpośrednio na serwerze (o ile użytkownik ma do tego prawo)
  • DELETE - usuwa dane po stronie serwera (o ile użytkownik ma do tego prawo)
  • HEAD - zwraca metadane strony bez jej pobierania; jeżeli strona została zapisane w pamięci (cache) po stornie przeglądarki, można wykorzystać HEAD w celu weryfikacji czy dane po stronie serwera się zmieniły
  • OPTIONS - zwraca listę obsługiwanych komend przez serwer
  • TRACE - diagnostyka i śledzenie wysłanego żądania
  • CONNECT - tworzy tunel, np. gdy chcemy użyć serwera proxy
Kody statusów podzielone są na grupy:

  • 1xx - informacje - żądanie zostało dostarczone do serwera i wciąż jest przetwarzane
  • 2xx - żądanie zostało poprawnie przetworzone
  • 3xx - żądanie przekierowania - klient musi użyć innego adresu aby otrzymać żądane dane
  • 4xx - błąd klienta - w żądaniu (requescie) albo serwer nie wie jak przetworzyć zapytanie
  • 5xx - błędy serwera; serwer nie jest w stanie przetworzyć zapytania
Pełną listę kodów odpowiedzi można sprawdzić w Wikipedii.