Nowości w C# 7.3

Już po konferencji Connect 2018 a została jeszcze ostania mała wersja C# do omówienia. Na ten moment najnowsza. Zmiany w tej wersji są chyba najmniej popularne ze względu na skupienie się na poprawieniu efektywności w pisaniu kodu nie zarządzanego czyli skupieniu się na wydajności. Poniżej zapraszam na analizę zmian w C# 7.3.

Spis treści

 

Możliwość dostępu do zmiennych z wyrażeniem fixed bez przypinania

Wyrażenie fixed umożliwia zabezpieczenie w kodzie nie zarządzanym aby dany fragment pamięci nie był przenoszony przez GC.  Aby użyć tej składni należało wykonać następujący kod:

    unsafe struct S
    {
        public fixed int myFixedField[10];
    }

    public class C73Features
    {
        private S s = new S();

        private unsafe void FixedPoint()
        {
            // before C# 7.3
            fixed (int* ptr = s.myFixedField)
            {
                int p_old= ptr[5];
            }
                      
        }
    }

Od wersji C# 7.3 możemy skrócić ten zapis do czegoś takiego:

        private unsafe void FixedPoint()
        {
           int p_new = s.myFixedField[5];
        }

Efekt jest taki sam a pisania jest znacznie mniej. Nie skomentuje czy to jest dobra zmiana, bo bardzo mało używam kodu nie zarządzanego.

Możliwość przypisania nowej wartości do lokalnych zmiennych ref

Kontynuacja zmian związanych z usprawnieniem używania wyrażenia ref. Od wersji C# 7.3 można już przypisywać do zmiennej lokalnej typu ref inna zmienną typu ref.  Jak poniżej:

 ref VeryLargeStruct refLocal = ref veryLargeStruct; // initialization
 refLocal = ref anotherVeryLargeStruct; // reassigned, refLocal refers to different storage.

Instrukcja stackalloc umożliwia inicjalizację

W C# można skorzystać z inicjalizacji tablicy podczas jej deklaracji jak w przykładzie poniżej:

        private void StackallocArrya()
        {
            var arr = new int[3] { 1, 2, 3 };
            var arr2 = new int[] { 1, 2, 3 };
        }

Od wersji C# 7.3 można taki sam zapis zastosować używając instrukcji stackalloc, przykład poniżej:

        private unsafe void StackallocArrya()
        {
            int* pArr = stackalloc int[3] {1, 2, 3};
            int* pArr2 = stackalloc int[] {1, 2, 3};
            Span<int> arr = stackalloc [] {1, 2, 3};
        }

Wyrażenie fixed wspiera więcej typów

W wersji C# 7.3 każdy obiekt, który posiada metodę GetPinnableReference(), która zwraca typ ref lub ref readolny może być użyty razem z instrukcją fixed. Poniżej przykład jak to wygląda w praktyce:

    public unsafe class FixedType
    {
        public Point[] Points { get; set; } 

        public ref Point GetPinnableReference()
        {
            return ref Points[1];
        }
    }


    private unsafe void FixedAnyType(FixedType ft)
    {
        fixed (Point* d = ft )
        {
                
        }
    }

Informacji o tym gdzie i kto to używa jest dość mało. Choć jeśli chodzi o większą elastyczność w pisaniu wydajnego kodu, taka zmiana możne być pomocna. Sam nie używam wiele kodu nie zarządzanego, jednak wyobrażam sobie, że takie podejście może być bardzo pomocne.

Wsparcie typu TUPLE dla opratorów == i !=

Najbardziej praktyczna zmiana ze wszystkich w C# 7.3. Od teraz typy Tuple można porównywać używając operatorów == i !=. Poniżej lista wszystkich możliwych porównań. Generalnie działa to dość intuicyjnie. Nazwy pól nie mają znaczenia, typy można porównać jeśli można zrobić rzutowanie. Polecam prześledzić dokładnie tą listę raz i porządnie a na pewno zostanie ona w pamięci:

private void AssertTuple()
        {
            var left = (a: 5, b: 10);
            var right = (a: 5, b: 10);

            Console.WriteLine(left == right); // 'true'


            var left1 = (a: 5, b: 10);
            var right1 = (a: 5, b: 10);

            (int a, int b)? nullableTuple = right1;
            Console.WriteLine(left1 == nullableTuple); // 'true'

            // lifted conversions
            var left2 = (a: 5, b: 10);
            (int? a, int? b) nullableMembers = (5, 10);
            Console.WriteLine(left2 == nullableMembers); // 'true'

            // converted type of left is (long, long)
            (long a, long b) longTuple = (5, 10);
            Console.WriteLine(left2 == longTuple); // 'true'

            // comparisons performed on (long, long) tuples
            (long a, int b) longFirst = (5, 10);
            (int a, long b) longSecond = (5, 10);
            Console.WriteLine(longFirst == longSecond); // 'true'


            (int a, string b) pair = (1, "Hello");
            (int z, string y) another = (1, "Hello");
            Console.WriteLine(pair == another); // 'true'
            Console.WriteLine(pair == (z: 1, y: "Hello")); // warning: literal contains different member names


            (int, (int, int)) nestedTuple = (1, (2, 3));
            Console.WriteLine(nestedTuple == (1, (2, 3)) );
        }

Atrybut field dla get-ów i set-ów

W C# mamy atrybut field, który stosuje się do pół i ma on tylko tam zastosowanie. Od teraz można stosować ten atrybut też do właściwości czyli do  get-ów i set-tów. Działa to tak, że dla właściwości i tak powstaje pole w IL-u więc dla tego pola ten atrybut będzie miał zastosowanie. Poniżej, krótkie wyjaśnienie:

[field: SomeThingAboutFieldAttribute]
public int SomeProperty { get; set; }

W przykładzie widać, że atrybut SomeThingAboutFieldAttribute będzie zastosowany do pola, które będzie wygenerowane dla właściwości SomeProperty .

Naprawa błędu z przeciążeniem paramentu  in

Tutaj sprawa jest dość prosta. Jest to nie tyle zmiana ile po prostu naprawa pewnego błędu. Błąd polegał na tym, że zastosowanie w metodzie operatora in powodowana dwuznaczność w przeciążeniach. To znaczny, kompilator nie wiedział, którą metodę ma wybrać w takim przypadku jak niżej:

static void M(S arg);
static void M(in S arg);

W takim wypadku lepszym kandydatem od wersji C# 7.3 do przeciążenia będzie metoda 1 (bez in), jeśli będziemy chcieli wybrać drugą metodę musimy użyć słowa in.

Zmienne typu out w konstruktorach

C# 7.3 to dalsze usprawnienia co do typów ref i out. Tym razem mamy już możliwość używania słowa kluczowego out w konstruktorach:

public class B
{
   public B(int i, out int j)
   {
      j = i;
   }
}

public class D : B
{
   public D(int i) : base(i, out var j)
   {
      Console.WriteLine($"The value of 'j' is {j}");
   }
}

Nic dodać, nic ująć ; )

Nowe opcje kompilatora

Nowe opcje postały aby wspierać różne scenariusze DevOps.

Pierwszym nowym parametrem jest -publicsign. Umożliwia no podpisanie biblioteki używając publicznego klucza. Przykładem może być popisanie biblioteki w projektach open-source. Aby użyć tego przełącznika należy tez podać przełączniki  -keyfile lub -keycontainer aby podać właściwy klucz.

Drugim nowym parametrem jest -pathmap, który mapuje ścieżki do plików z tych ze środowiska, gdzie biblioteka została skompilowana na takie jakie byśmy chcieli. Ścieżki te są zapisane w plikach PDB. Dzięki temu na produkcji jeśli mamy pliki PDB możemy w błędach widzieć ścieżki z góry ustalone a nie te z produkcji, które często są dość długie i nic nie mówiące. Użycie przekładowe może wyglądać w taki sposób:

 
csc -pathmap:C:\work\tests=\publish t.cs 

Ścieżka C:\work\tests  jest mapowana na \publish w pliku t.cs.

Podsumowanie

Z C# 7.3 widać wyraźnie, że twórcy C# chcą wydusić z języka jak najwięcej. Chcą ułatwić wydajne programowanie i może też trochę zachęcić do refaktoryzacji kodu pod tym względem. Szczególnie jeśli chodzi o kod nie zarządzany. Najbardziej chyba użytkową zmiana jest dodanie obsługi operatorów ==!= dla tuple-i oraz możliwość mapowania ścieżek na inne niż  domyślne. Te dwie zmiany często wykorzystuje.

Następny wpis o nowością będzie już z wersji C# 8.0 ale to jeszcze musimy poczekać.