Paweł Łukasiewicz
2018-03-08
Paweł Łukasiewicz
2018-03-08
C# 7.0 – wprowdzenie do nowych funkcji języka
Po długiej nieobecności na blogu pora odswieżyć swoją wiedzę związaną z językiem. Microsoft wprowadził nową wersję języka C#. Nauczymy się 7 nowych (wybranych) funkcjonalności. Datą premiery tej wersji języka jest marzec 2017 roku.
Wszystkie przykłady zostały przygotowane w środowisku Visual Studio 2017. Po odpowiednich aktualizacjach poniższe przykłady zadziałają również w wersji 2015 ale wymaga to odpowiednich aktulizacji i specjalnej konfiguracji.
Funkcjonalność nr 1 – literały binarne
Nie istniały do wersji 6.0. Poprzednio używaliśmy dosłownych, stałych wartości dla typów podstawowych w języku. Poniżej fragment kodu ze starszej wersji języka, który pokazuję deklarację kilku stałych wartości dla niektórych typów podstawowych:
private static void CurrentPrimitiveTypeLiterals()
{
int employeeNumber = 34;
float passPercentage = 34.0f;
int employeeAge = 0x22;
}
Do wersji C# 6.0 nie było możliwości binarnego zaprezentowania wartości literału w kodzie. Obecna wersja rozwiązauje ten problem dzięki wprowadzeniu nowego prefiksu: "0b" lub "0B". Podstawowym typem danych takiej zmiennej jest wciąż liczba całkowita tak jak to miało miejsce w przypadku liczb szesnastkowych. Bazą będą 2 liczby, a dopuszczalną reprezentacją w ciągu jest 0 lub 1. Poniżej przykładowa binarna definicja literałów w kodzie:
private static void BinaryLiteralsFeature()
{
var employeeNumber = 0b00100010; // binarny odpowiednik liczby 34
Console.WriteLine(employeeNumber); // wypisanie na konsoli wartości tej zmiennej
long empNumberWithLongBackingType = 0b00100010; // w tym przypadku typem zastępnczym dla reprezentacji binarnej jest long
Console.WriteLine(empNumberWithLongBackingType); // wypisanie na konsoli wartości tej zmiennej
int employeeNumber_WithCapitalPrefix = 0B00100010; // prefixy 0b oraz 0B są sobie równoważne
Console.WriteLine(employeeNumber_WithCapitalPrefix); // wypisanie na konsoli wartości tej zmiennej
}
Funkcjonalność nr 2 – separator dziesiętny
Nie istniał do wersji 6.0. Teraz możemy poprawić czytelność liczb poprzez użycie podkreślnika jako separatora dziesiętnego. Możemy używać ich dowolnie – w dowolnym miejscu i dowolną liczbę razy z godnie z naszymi upodobaniami. Będą szczególnie użyteczne w literałach binarnych ponieważ zwykle mają one długą reprezentację. Kompilator ignoruje te podkreślenia podczas kompilacji kodu. Poniżej przykład kodu pokazujący sposób użycia:
private static void DigitSeparatorsFeature()
{
// podkreślenia pomiędzy liczami nie mają wpływu na to jak są przechowywane
// w taki sposób można poprawić czytelność długich reprezentacji liczb
int largeNaturalNumber = 345_2_45;
int largeNaturalNumber_oldFormat = 345245; // dokładnie ta sama liczba jak powyżej
long largeWholeNumber = 1_2_3_4___5_6_7_8_0_10;
long largeWholeNumber_oldFormat = 12345678010; // dokładnie ta sama liczba jak powyżej
int employeeNumber = 0b0010_0010; // bardzo przydatne przy reprezentacji binarnej
}
Należy jednak pamietać o kilku ograniczeniach:
- Podkreślenie nie może pojawić się na początku liczby;
- Podkreślenie nie może pojawić się przed przecinkiem dziesiętnym;
- Podkreślenie nie może pojawić się po znaku wykładniczym;
- Podkreślenie nie może pojawić się przed przyrostkiem specyfikacji.
Poniżej przykłady umówione powyżej:
private static void DigitSeparatorsFeature()
{
int underscoreAtStarting = _23_34; // niedozwolone, nie dojdzie do kompilacji kodu!
int underscoreBeforeDecimalPoint = 10_.0; // niedozwolone, nie dojdzie do kompilacji kodu!
double underscoreAfterExponentialSign = 1.1e_1; // niedozwolone, nie dojdzie do kompilacji kodu!
float underscoreBeforeTypeSpecifier = 34.0_f; // niedozwolone, nie dojdzie do kompilacji kodu!
}
Funkcjonalność nr 3 – krotka dostępna jako wartościowy typ danych
Krotka została przedstawiona w C# 4.0. Do wersji 6.0 dostępna była jako referencyjny typ danych dostępy w przestrzeni nazw System. Poniżej deklaracja krotki w C# 6.0:
private static void TupleReferenceTypeLiterals()
{
Tuple<int, string, bool> tuple = new Tuple<int, string, bool>(1, "cat", true);
Console.WriteLine(tuple.Item1); // kompilacja tworzy publiczną właściwość zgodnie z nazewnictwem "Item..."
Console.WriteLine(tuple.Item2); // Item1, Item2, Item3 - właściwości te są widoczne przez Intelisense
Console.WriteLine(tuple.Item3);
}
Zwrócenie wielu wartości z krotki było ich podstawowym użyciem, aż do pojawienia się wersji 7.0 języka C#.
Używanie krotek wraz z nowszą wersją stało się jeszcze łatwiejsze. Krotki są teraz dostępne jako wartościowy typ danych. Zdefiniowane są one w typie bazowym: System.ValueTuple, który jest strukturą. Poniżej przykład użycia krotek w wersji C# 7.0:
private static void TupleValueTypeLiterals()
{
// typy składowych struktry są automatycznie wnioskowane przez kompilator jako typ: string, string
var genders = ("Male", "Female");
Console.WriteLine("Possible genders in human race are : {0} and {1} ", genders.Item1, genders.Item2);
Console.WriteLine($"Possible genders in human race are {genders.Item1}, {genders.Item2}.");
// przykład zastąpienia domyślnych nazw Item1, Item2, ...
var geoLocation = (latitude: 124, longitude: 23);
Console.WriteLine("Geographical location is : {0} , {1} ", geoLocation.longitude, geoLocation.latitude);
// heterogoniczny(złożony) typ danych jest również dopuszczalny
var employeeDetail = ("Pawel", 33, true); //(string,int,bool)
Console.WriteLine("Details of employee are Name: {0}, Age : {1}, IsPermanent: {2} ", employeeDetail.Item1, employeeDetail.Item2, employeeDetail.Item3);
// referencyjny typ danych również może wystąpić w krotce
Employee emp;
var employeeRecord = ( emp: new Employee { FirstName = "Foo", LastName = "Bar" }, Id: 1 );
Console.WriteLine("Employee details are - Id: {0}, First Name: {1}, Last Name: {2}", employeeRecord.Id, employeeRecord.emp.FirstName, employeeRecord.emp.LastName);
// Komentarz dotyczący błędu: Predefined type 'System.ValueTuple´2´ is not defined or imported
// Dla .NET 4.6.2 lub niższej wersji, .NET Core 1.x, oraz .NET Standard 1.x trzeba doinstalować paczkę NuGet: System.ValueTuple
}
Krotki pozwalają na zwracanie wielu wartości z metod. W języku mieliśmy podobne funkcjonalności, ale żadna z nich nie była taka przejrzysta jak nowe krotki. Dotyczas mogliśmy zwracać wiele wartości z metod używając poniższych sposobów:
- zwracanie tablicy;
- zdefiniowanie struktury i zwrócenie jej;
- zdefiniowanie klasy z właściwościami publicznymi dla każdego zwracanego elementu;
- parametry wyjściowe;
- zwrócenie krotki: Tuple
Teraz będzie to prostrze niż dotychczas:
static void Main(string[] args)
{
var geographicalCoordinates = ReturnMultipleValuesFromAFunction("Warsaw");
Console.WriteLine(geographicalCoordinates.longitude);
Console.WriteLine(geographicalCoordinates.Item1); // ten sam wynik jak w przypadku długości geograficznej
Console.WriteLine(geographicalCoordinates.latitude);
Console.WriteLine(geographicalCoordinates.Item2); // ten sam wynik jak w przypadku szerokości geograficznej
Console.ReadKey();
}
private static (double longitude, double latitude) ReturnMultipleValuesFromAFunction(string nameOfPlace)
{
var geoLocation = (0D, 0D);
switch (nameOfPlace)
{
case "Warsaw":
geoLocation = (52.2297, 21.0122);
break;
default:
break;
}
return geoLocation;
}
Poniżej lista kiku zalet nowego typu krotek:
- Nie musisz tworzyć zdefiniowanego typu. Typy są tworzone w locie jako anonimowe przez kompilator;
- Teraz możesz nadawać nazwy członków publicznych krotki. W przestrzeni nazw Sytem.Tuple kompilator używa nazw generycznych, tj. Item1, Item2, Item3, etc. dla publicznych właściwości krotki;
- Wewnętrzenie krotka jest strukturą, która została przydzielona do obszaru pamięci stosu.
Funkcjonalność nr 4 – parametry wyjściowe
Jeżeli używałeś parametru wyjściowego do uzyskania wartości zwracanej z metody to pamiętasz brzydki sposób deklarowania zmiennej przed przekazaniem jej do metody:
private static void OutParameterOldUsage()
{
var number = "20";
int parsedValue; // pre-definiowana zmienna przekazywana jako argument
int.TryParse(number, out parsedValue);
}
Teraz zostało to uproszczone. Zmienna, która ma zostać przekazana jako parametr wyjściowy wywołania metody może zostać zdeklarowana w tej samej linii co wywołanie metody. Zostało to pokazanie poniżej:
private static void OutParameterNewUsage()
{
var number = "20";
// nie musimy pre-definiować parametru przekazywanego jako argument
// int parsedValue;
// deklaracja parametru
if (int.TryParse(number, out int parsedValue))
Console.WriteLine(parsedValue);
else
Console.WriteLine("The input number was not in correct format!");
// można nawet użyć słowa kluczowego var
if (int.TryParse(number, out var parsedValue2))
Console.WriteLine(parsedValue2);
else
Console.WriteLine("The input number was not in correct format!");
}
Funkcjonalność nr 5 – metody lokalne
Nie istniały do wersji 6.0. Często piszemy metody wykonujące złożoną i długą implementację algorytmiczną. Zwykle takie metody dzielimy na kilka mniejszych, aby ułatwić ich zrozumienie i zarządzenie w przyszłości. W większości jednak przypadków taki podział spowoduje, że mniejsze metody nie będą mogłby być użyte przez inne części Twojego programu ponieważ nie są wystarczająco ogólne i zwykle są specyficznym krokiem problemu algorytmicznego nad którym obecenie pracujesz.
Takie metody, które są używane tylko w jednym miejscu, mogą zostać zdefiniowane lokalnie tak, aby nie wprowadzać niepotrzebnego zamieszania. Są one zagnieżdzone wewnątrz metody w której mają być wywołane:
private static void LocalFunctionsFeature()
{
// metoda ta nie jest widoczna dla innych metod w tej klasie
string GetAgeGroup(int age)
{
if (age < 10)
return "Child";
else if (age < 50)
return "Adult";
else
return "Old";
}
Console.WriteLine("My age group is {0}", GetAgeGroup(33));
}
Funkcjonalność nr 6 – nowy sposób rzucania wyjątków
Każdy z nas używał rzucania wyjątków, aby zapobieć nagłemu i nieoczekiwanemu zamknięciu aplikacji. W największym skrócie kod prezentował się w następujący sposób:
private static void SimpleThrowImplementation()
{
int zero = 0;
try
{
var result = 1 / zero;
}
catch (Exception ex)
{
// powinniśmy dodać wyjątek do log'u tak, aby wiedzieć dokładnie co się stało
//....
//....
// następnie rzucamy wyjątek do metody wywołującej, aby złapać i odpowiednio obsłużyć
throw new ArithmeticException("Divide by zero exception occured in SimpleThrowApplication method");
}
}
Do wersji C# 6.0 instrukcja throw jest samodzielną instrukcją. Nie można było jej połączyć z innymi wyrażeniami czy klauzulami.
W wersji 7.0 możemy użyć instrukcji throw wewnątrz, np. wyrażenia warunkowego jak pokazano na poniższym przykładzie:
private static int ThrowUsageInAnExpression(int value = 40)
{
return value < 20 ? value : throw new ArgumentOutOfRangeException("Argument value must be less than 20");
}
Funkcjonalność nr 7 – nowe możliwości użycia wyrażeń lambda
C# 6.0 pozwalał na zdefiniowanie ciała metody przy użyciu wyrażeń lambda tak jak zostało to pokazane na poniższym przykładzie:
public class Employee
{
private string _firstName;
private string _lastName;
public string FirstName
{
get => _firstName; // getters
set => _firstName = value; // setters
}
}
Jednakże takie użycie miało wiele ograniczeń ponieważ nie było dozwolone podczas definiowania właściwości, konstruktorów, finalizatorów, etc.
C# 7.0 pozwala na użycie podobnej konstrukcji dla właściwości, konstruktorów, etc.:
public class Employee
{
private string _firstName;
private string _lastName;
//Not working in Visual Studio 15 Preview 5 currently.
public string FirstName
{
get => _firstName; // getters
set => _firstName = value; // setters
}
}
Podsumowanie
W powyższym artykule zostało omówionych 7 nowych funkcjonalności języka C# 7.0. Oczywiście nie jest to kompletna lista ponieważ nie sposób opisać wszystkiego w obrębie jego artykułu, wprowadzenia.
W kolejnych artykułach postaram się opisać kolejne aktualizacje związane z:
- C# 7.1: async main, aktualizacja związana z właściwościami krotek, domyślne wyrażenia, dopasowanie wzorca z typami generycznami;
- C# 7.2:Span<T> - Pozwala na pracę z DOWOLNĄ pamięcią w bezpieczny i bardzo skuteczny sposób. Dzięki niemu można wykorzystać w pełni niezarządzaną pamięć.