Wstęp
Wraz z nadejściem Visual Studio 2022 (o moich testach tego środowiska możesz przeczytać tutaj) przyszła nowa wersja C#. To już dziesiąta odsłona. Co tym razem przygotowali ludziska z Microsoftu?
Namespace na cały plik
W C# wszystko musi być w jakimś namespace (przestrzeni nazw):
namespace Test
{
class Class1
{
}
}
Teraz wprowadzono możliwość zdefiniowania namespace w obrębie całego pliku:
namespace Test;
class Class1
{
}
Wszystko, co znajdzie się w tym pliku, trafi do przestrzeni Test. Jednak w tak skonstruowanym pliku może być tylko jedna przestrzeń robocza. Jeśli chcesz mieć ich więcej, musisz to zrobić po staremu.
Jakimś plusem tego jest mniej wcięć. Tzn. wcięcia klasy zaczynają się od początku (od pierwszego znaku). Jednak…
Moje wrażenia? Jak dla mnie – zupełnie zbędne, zwłaszcza że domyślny template tworzy pliki po staremu. Póki co nie skorzystałem z tego i wątpię, że skorzystam.
Globalne usingi
Brzmi jak mniej kodu w Twoich plikach cs. Co prawda, zawsze można było usingi ukryć w dyrektywie region, np:
#region "Usingi"
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
#endregion
jednak i tak w każdym pliku trzeba było je dodawać. Do tej pory działały tylko lokalnie – w zakresie pliku, w którym zostały „użyte”. Teraz możemy posłużyć się globalnym usingiem, np:
global using System.Linq;
Jeśli taką linijkę wpiszemy w JAKIMKOLWIEK pliku cs, będziemy mogli korzystać z LINQ we wszystkich plikach w tym projekcie.
Usingi statyczne
Ale to nie wszystko… Petarda nadchodzi powoli… Możemy korzystać ze wszystkich form using, jakie znaliśmy wcześniej, np:
global using static System.Console
Jeśli nie znałeś wcześniej znaczenia using static System.Console
, to już tłumaczę.
Żeby wypisać coś na konsoli, normalnie odwołujemy się do metody przez klasę:
using System;
public void Main()
{
Console.WriteLine("Siemka");
}
Dzięki statycznemu usingowi możemy pominąć klasę, do której należy STATYCZNA metoda i wywołać ją bezpośrednio, jak gdyby była standardowa:
using static System.Console;
public void Main()
{
WriteLine("Siemka");
}
Możemy to zrobić z dowolną statyczną publiczną metodą.
Globalny statyczny using daje taką możliwość w obrębie całego projektu. Czyli raz wywołany:
global using static System.Console;
umożliwia nam operacje na konsoli w każdym pliku cs w danym projekcie bez bezpośredniego odwołania się do klasy Console.
Typedef w C#
global using
zadziała też do aliasów, np:
global using Generics = System.Collections.Generic;
Ale co jest dla mnie petardą… możemy wreszcie zrobić coś na zasadzie typedef z C++. BAAAARDZO mi tego brakowało w C#:
global using IntList = System.Collections.Generic.List<int>;
i IntList
będzie widoczny jako typ w całym projekcie! Niesamowite. Do tej pory jedyne, co mogliśmy zrobić, żeby „zasymulować” coś takiego to utworzenie nowej klasy dziedziczącej po List<int>
, co nie było ani zbyt sensowne, ani miłe.
Dyrektywy global using
możesz umieścić w dowolnym pliku cs w danym projekcie. Ale najbardziej radosnym miejscem do tego wydaje się plik Program.cs (App.cs), chociaż można też utworzyć zupełnie nowy plik o nazwie np. "usings.cs"
i tam umieścić wszystkie globalne usingi.
Moje wrażenia?
Pomocne, zwłaszcza jak masz duże grupy takich samych usingów w kilku plikach. Można stworzyć alias na konkretny typ (typedef), co jest niesamowite i bardzo mi tego brakowało.
Czasami jednak w głowie pojawia mi się pytanie: „Czy ten using może/powinien być globalny?” i to działa na minus, bo zaczynam się zastanawiać nad pierdołami. Ale myślę, że to kwestia bardziej zachłyśnięcia się tą opcją i po pewnym czasie to minie.
Stałe w interpolowanych stringach
Spójrz na taki kod:
[Obsolete("Zamiast metody Foo, użyj Bar")]
static void Foo()
{
}
Wcześniej nie można było użyć interpolowanych stringów w takich przypadkach. Teraz już można:
[Obsolete($"Zamiast metody {nameof(Foo)}, użyj {nameof(Bar)}")]
void Foo()
{
}
void Bar()
{
}
Mała zmiana, ale cieszy. Czasami tego brakowało. Oczywiście powyższy przykład jest mało użyteczny, ale łapiesz kontekst. Jest jeszcze więcej zmian w interpolowanych stringach, które po prostu zoptymalizowały ich działanie.
Wygodniejsze lambdy
Lambda jako var
Do tej pory, żeby zdefiniować lambdę, trzeba było posłużyć się wyrażeniem lub zadeklarować lambdę jako Action<>
lub Func<>
, np:
Func<string, int> l = (string s) => { return s.Length; };
int len = l("abc");
teraz można lambdę zadeklarować jako var
:
var l = (string s) => { return s.Length; };
int len = l("abc");
Konkretny typ lambdy zostanie za nas „odgadnięty”. Dokładnie tak jakbyśmy się tego spodziewali przy słowie var
.
Lambdy można też przypisać do typu object
lub Delegate
a także Expression
:
var l = (string s) => { return s.Length; }; //Func<string, int>?
object obj = (string s) => { return s.Length; };
Delegate del = (string s) => { return s.Length; };
Expression ex = (string s) => s.Length;
LambdaExpression lex = (string s) => s.Length;
A co z przeciążeniami?
Weźmy takie metody:
void Foo(string s)
{
}
void Foo(int i)
{
}
void Bar(double d)
{
}
Możemy zadeklarować:
var b = Bar;
Ale nie zrobimy już:
var f = Foo;
ponieważ metoda Foo
może przyjąć różne argumenty (jest przeciążona). W tym przypadku zmienna f nie będzie reprezentowała wszystkich przeciążeń. Po prostu kompilator nie jest w stanie wykminić, którą wersję chcemy użyć. W takim przypadku trzeba posłużyć się starym kodem:
Action<string> fs = Foo;
Action<int> fi = Foo;
Różne typy zwracane
Czasem jest tak, że nie wiadomo, co powinna zwrócić lambda. W takim wypadku zawsze stosowaliśmy typ object
, jako typ zwracany, np:
Func<bool, object> f = (bool b) => b ? "string" : 0;
To był jedyny możliwy zapis. Niestety kompilator nie potrafi odgadnąć typu zwracanego z takiego kodu:
var f = (bool b) => b ? "string" : 0;
chociaż nie wiem dlaczego z automatu nie uznaje tego za object
. Ale można mu to teraz podpowiedzieć:
var f = object (bool b) => b ? "string" : 0;
Bardzo mi to przypomina definiowanie wskaźnika na funkcję w C++
Atrybuty
W wersji 10 dołożono też możliwość oznaczania lambdy atrybutem! Np:
var l = [MyAttribute](bool b) => { return !b; };
Moje wrażenia. Super, że można definiować lambdy z użyciem var
. To naprawdę przydatne. Jeśli chodzi o atrybuty przy lambdach… nigdy nie miałem potrzeby, żeby to stosować, więc nie potrafię znaleźć teraz użytecznego przykładu. A szukałem i kombinowałem. Największe nadzieje pokładałem w atrybucie Conditional
, ale z jakiegoś powodu nie chce on działać z lambdami, chociaż powinien działać każdy atrybut, który targetuje w metody.
Struktury i rekordy
W nowej odsłonie C# znajdziemy też kilka drobnych zmian związanych mocniej lub słabiej ze strukturami. Wszystkie zdecydowanie na PLUS. Zacznijmy od:
Konstruktor bez parametrów!
Do tej pory każda struktura posiadała domyślny konstruktor bezparametrowy. Nie można było stworzyć takiego samemu. Taki kod po prostu się nie kompilował:
struct Point
{
public int X { get; init; }
public int Y { get; init; }
public Point()
{
X = 0;
Y = 0;
}
}
A domyślny niejawny konstruktor wszystkie pola inicjalizował domyślnymi wartościami. Czyli np. stringa nullem. Powiem szczerze, nie wiem czemu tak to działało. Na szczęście w wersji 10 można już utworzyć samemu konstruktor bezparametrowy tak jak powyżej.
Record Struct
W C#9 zawitały typy rekordowe. Jeśli nie wiesz, czym są… Rekordy to w skrócie coś łączące ze sobą struktury i klasy. Rekordy są traktowane jako typ referencyjny, natomiast porównywane są wartościowo. Mają wbudowane operatory porównań. Więcej do poczytania tutaj: https://docs.microsoft.com/en-us/dotnet/csharp/whats-new/tutorials/records
W C#10 doszły record struct.
Moje testy tego tworu były dość krótkie, natomiast widzę, że mogą świetnie zająć miejsce tupli. Można je zdefiniować np. w taki sposób:
readonly record struct Point(int X, int Y);
Tak! Tylko jedna linijka! A co mamy?
Mamy dwie właściwości – X i Y. Mamy możliwość porównywania takich rekordów (porównywane są WARTOŚCI poszczególnych składników, nawet jeśli mamy stringa – porównana będzie wartość).
Słówko readonly
oznacza, że taki rekord jest immutable – niezmienny.
Kopiowanie struktur i rekordów
Kod powie więcej niż 1024 słowa:
readonly record struct Point(int X, int Y);
...
void Foo()
{
Point pt = new(0, 0);
Point pt2 = new Point();
var pt3 = pt2 with { Y = 5 };
}
Spójrz, co się stało.
Najpierw utworzyliśmy jeden rekord (5) – pt
. Potem drugi – pt2
z wartościami (0, 0).
Następnie utworzyliśmy trzeci – pt3
. A właściwie słowo with
spowodowało skopiowanie rekordu pt2
ze zmienioną jedną wartością.
Rozszerzony wzorzec właściwości
Czyli Extended property pattern. To takie nowe cudo, które może nieco skrócić kod. Ciężko mi w tym momencie określić, czy to dobre rozwiązanie, na pewno trzeba się do niego przyzwyczaić. Już tłumaczę o co chodzi.
Załóżmy, że mamy takie struktury:
readonly record struct Point(int X, int Y);
public struct Address
{
public string City { get; init; } = "<unknown>";
}
public record struct Person
{
public string FirstName { get; init; }
public string LastName { get; init; }
public Address Address { get; init; }
}
Teraz utwórzmy w jakiś sposób obiekt Person:
object obj = new Person
{
FirstName = "Adam",
LastName = "Jachocki",
Address = new Address { City = "Łódź" }
};
I sprawdźmy, czy taki obiekt jest osobą o imieniu Adam. Najpierw po staremu:
if (obj is Person && ((Person)obj).FirstName == "Adam")
Console.WriteLine("Tak, to Adam");
A po nowemu (używając extended property pattern) możemy zrobić to milej:
if (obj is Person { FirstName: "Adam"})
Console.WriteLine("Tak, to Adam");
Ale co ciekawe, możemy łączyć warunki. Łączenie to koniunkcja, czyli „and”:
if (obj is Person { FirstName: "Adam", Address: { City: "Łódź" }})
Console.WriteLine("Tak, to Adam");
To jest taki cukier składniowy. Z przyzwyczajenia nie miałem okazji go jeszcze użyć, ale często takie sprawdzenia robię w kodzie GUI i dla mnie wydaje się naprawdę fajną sprawą. Chociaż osobiście muszę się do tego trochę przekonać.
To tyle nowości jeśli chodzi o C#10. Moimi ulubieńcami są globalne usingi, możliwość definiowania lambdy jako var no i jednolinijkowe rekordy. A co Wam się najbardziej z tego podoba? Dajcie znać w komentarzu.