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);
	// Komenatrz 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ęć.