0% found this document useful (0 votes)
49 views212 pages

Platformy Technologiczne .NET 2020

Uploaded by

Bak Alak
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
49 views212 pages

Platformy Technologiczne .NET 2020

Uploaded by

Bak Alak
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

Platformy technologiczne - .

NET C#
Materiały 2020
Zasady zaliczenia
Na wynik z przedmiotu składa się średnia arytmetyczna wyników procentowych ze
wszystkich trzech składowych przedmiotu (przy czym każda ze składowych musi zostać
zaliczona na co najmniej 50%):

● Wykładu
● Laboratorium
● Projektu

Mapowanie wyniku na ocenę jest następujące:

● 50% - 3
● 60% - 3,5
● 70% - 4
● 80% - 4,5
● 90+% - 5
Zasady zaliczenia 1

Platforma .NET i jej składowe 9


Technologie wchodzące w skład .NET 9
Architektura platformy .NET 10

Podstawy języka C# 11
Budowa programu 11
Komentarze 12
Zmienne 13
Podstawowe typy danych 14
Textowe 14
Liczby całkowite 14
Liczby zmiennoprzecinkowe 15
Logiczne 15
Operator default 16
Stałe 17
Stałe czasu kompilacji 17
Stałe czasu wykonania (runtime) 18
Ciągi znaków (typ string) 19
Konstrukcja w pamięci 19
Ciąg pusty 19
Znaki specjalne 19
Porównywanie 20
Konkatenacja (łączenie) 21
Formatowanie 22
Przeszukiwanie stringów 24
Modyfikowanie zawartości ciągu 26
Dzielenie i łączenie kolekcji ciągów 27
Weryfikacja niepustości 27
Normalizacja wielkości liter 28
Przycinanie ciągu (usuwanie białych znaków) 28
StringBuilder 29
Konwersje typów (implicit, explicit, TryParse) 30
Implicit conversion 30
Explicit conversion 31
Boxing i Unboxing 31
AS i IS 32
TryParse i ToString 33
UserDefined conversions 35
Klasa Console i interakcja z użytkownikiem 36
Wyświetlanie i pobieranie danych z użyciem konsoli 36
Manipulacja położeniem kursora 37
Modyfikacja koloru tła i tekstu 38
Przekierowywanie strumieni danych 38
Czyszczenie konsoli 41
Petle 42
While 42
do-while 42
For 43
Foreach 43
Rekurencja 45
Intrukcje warunkowe 47
if-else 48
krótki if (ternary-if) 49
Switch 50
Instrukcje skoku bezwarunkowego 51
break 51
continue 51
return 52
goto 54
Typy wyliczeniowe (enum) 55
Przypisywanie wartości liczbowych 56
Konwersja int ⇔ enum 56
Iterowanie po możliwych wartościach 57

Metody 58
Konstrukcja 58
Słowo kluczowe “params” 59
Modyfikatory parametrów 60
Ref 60
In 60
Wartości domyślne parametrów 62
Non-trailing named arguments 63

Tworzenie bibliotek 65
Referencjonowanie projektów w Visual Studio 65
Czym jest nuget? 66
Repozytorium paczek nuget 67
Wyjątki 68
Obsługa wyjątków 69
Samodzielne wyrzucenie wyjątku 70
Tworzenie własnych wyjątków 71

Niezarządzalne zasoby danych 73


Strumienie 73
Interface IDisposable i słowo kluczowe using 74
Słowo kluczowe “using” 75

Programowanie obiektowe 76
Klasy i struktury 77
Typy referencyjne i wartościowe 78
Konstruktor i destruktor 80
Pola (fields) 82
Anemiczny i bogaty model domenowy 83
Bogaty Model Domenowy 83
Anemiczny Model Domenowy 85
Własności (properties) 87
Null propagator 89
Operator '??' 91
Typy i metody generyczne 91
Klasa Nullable 93

Konwencje nazewnicze 94

Paradygmaty programowania obiektowego: 95


Polimorfizm 95
Hermetyzacja 96
Abstrakcja 97
Klasy abstrakcyjne 97
Interface’y 99
Dziedziczenie 101
Blokowanie możliwości dziedziczenia 103
Nadpisywanie i przesłanianie 103
Słowo kluczowe this i base 106
Kolekcje 109
Definicja 109
Podstawowe rodzaje kolekcji 109
Tablica 109
Lista 110
Dictionary 111
HashSet 112
Stack 112
Queue 112
LINQ 113
Wybrane funkcje - Where 113
Wybrane funkcje - First, FirstOrDefault, Last, LastOrDefault 114
Wybrane funkcje - All, Any 115
Wybrane funkcje - ForEach 115
Wybrane funkcje - Average, Sum, Count, Max, Min 115
Wybrane funkcje - Select, SelectMany 115
Wybrane funkcje - Distinct 116
Wybrane funkcje - Exept 116
Wybrane funkcje - GroupBy 117
Wybrane funkcje - Intersect 117
Wybrane funkcje - Take, Skip, OrderBy 117
Statement Lambda 117
Różnice między interfejsami IList, ICollection i IEnumerable 118
Słowo kluczowe “yield” 118

Serializacja 119
Przygotowanie danych 119
Serializacja Binarna 120
Serializacja Xmlowa 120
Serializacja Jsonowa 121

Dodatkowe słowa kluczowe 121


typeof 121
checked 121
unchecked 122
nameof 122
sizeof 122

Obiekty statyczne 122


Klasy Statyczne 122
Metody Statyczne 123
Konstruktor Statyczny i Pola Statyczne 123
Property Statyczne 123

Rzutowania 124
Rzutowanie niejawne 124
Rzutowanie jawne 124
Is 124
As 125

Zaawansowane Aspekty programowania .NET 125


Tworzenie obiektów 125
Extension Methods 125
Typy Anonimowe 125
Dynamic 126
Klasy Lokalne 126
Metody Lokalne 126
Refleksja - Activator 127

REST API i Entity Framework 127


Migracje 127
Interfejs IQueryable - Implementacja Lazy Loadingu 127
Metody Include, ThenInclude 128

Autoryzacja OAuth2 128


Rejestracja za pomocą Google’a 128
Wejdź na [Link] 128
Zdefiniowanie autentykacji w kontenerze DI 128
Atrybut Authorize 129
Stworzenie konkretnych polityk dostępu 130

Dobre praktyki programowania 131


SOLID 131
Single Responsibility Principle 131
Open/Close principle 132
Liskov Substitution Principle 132
Interface Segregation Principle 136
Dependency Inversion Principle 141
KISS 145
YAGNI 145
Boy Scout Rule 146

Dependency Injection Containers 146

Wzorce projektowe 156

Testy jednostkowe z .NET 156


Biblioteka testowa NUnit 157
Testowanie w izolacji - Moq 167
Izolowanie kodu od zależności statycznych 175

Programowanie równoległe i współbieżne 177


Wątki (Thread) 178
Synchronizacja wątków 181
Taski 188
async/await 195
Klasa Process 205

Usługi systemowe (Windows) 207


Zarządzanie usługami systemowymi 207
TopShelf 209
1. Platforma .NET i jej składowe
Platforma .NET to framework dostarczany przez firmę Microsoft. Framework to duży zbiór
narzędzi i bibliotek, którego uwarunkowania znacząco wpływają na sposób wytwarzania
oprogramowania. Najbliższym rozwiązaniem konkurencyjnym .NET’a jest Java.

1.1. Technologie wchodzące w skład .NET


Platforma .NET dzieli się obecnie na trzy podstawowe technologie:

● .NET Framework - rozwiązanie przeznaczone na systemy operacyjne Windows,


● .NET Core - rozwiązanie przeznaczone do pisania rozwiązań wieloplatformowych
(Windows, Linux, MacOS)
● Xamarin - rozwiązanie przeznaczone na pisanie mobilnych rozwiązań
wieloplatformowych (Android, IOS, …)

Źródło: [Link]

Dodatkowo, rozróżnić można następujące podtechnologie wchodzące w skład .NET Core i


.NET Framework:

● WindowsForm (.NET Framework) - tworzenie aplikacji okienkowych (legacy)


● WPF (.NET Framework) - Windows Presentation Foundation. Następca Windows
Forms, pozwala na tworzenie rozbudowanych widoków w oparku o język znaczników
XAML.
● [Link] WebForms (.NET Framework) - tworzenie aplikacji webowych (legacy)
● [Link] MVC (.NET Framework) - tworzenie aplikacji webowych w oparciu o
wbudowany wzorzec projektowym MVC (Model-View-Controller) z możliwością
wykorzystywania języka C# w dokumencie HTML.
● [Link] Core MVC (.NET Core) - następna [Link] MVC oparty na .NET Core.
● WCF (.NET Framework) - Windows Communication Foundation. Technologia
pozwalająca na tworzenie usług sieciowych opartych o protokół SOAP.
● [Link] WebApi - technologia pozwalająca na tworzenie usług webowych opartych o
protokół HTTP.
● [Link] Core WebApi - następca [Link] WebApi oparty na .NET Core.

1.2. Architektura platformy .NET


Platforma .NET poza technologiami wspomagającymi wytworzenie oprogramowania
wybranego typu dostarcza pełną infrastrukturę służącą do kompilacji i uruchamiania aplikacji.
Źródło: [Link]

2. Podstawy języka C#
2.1. Budowa programu
W C#, jako że jest on językiem obiektowym, wszystkie operacje i dane muszą być
przechowywane w obiektach. Nie ma możliwości wykonywania żadnych operacji poza
granicami obiektów. Obiekty reprezentowany jest [Link]. przez klasy.
Po utworzeniu programu z szablonu Console App otrzymujemy klasę Program, a w niej
funkcję Main(). Funkcja Main() jest miejscem, od którego zawsze zaczyna się wykonywanie
naszego programu i to w niej umieszczamy pierwsze instrukcje programu.

Klasy w programie powinny (ale nie muszą) być organizowane w tzw. namespaces -
przestrzenie nazw. Więcej informacji na ich można znaleźć w materiałach o Programowaniu
Obiektowym.

Na samej górze programu znajduje się sekcja z referencjami. Referencje pozwalają nam na
korzystanie z klas i funkcji, które znajdują się już w .NET Framework lub które dodaliśmy z
zewnątrz w formie bibliotek. Aby poprawnie dodać referencje, należy podać słowo kluczowe
using a następnie wpisać za nim prawidłową nazwę przestrzeni nazw, w której znajdują się
klasy, których chcemy użyć.

//Using'i - lista zaimportowanych przestrzeni nazw


using System;

//definicja przestrzeni nazw, w której znajduje się kod


//napisany wewnątrz klamer
namespace HelloWorld
{
//klasa program - zwyczajowo jest to klasa zawierająca funkcję mail
class Program
{
//statyczna funkcja main - zwyczajowo punkt startowy programu
static void Main(string[] args)
{
//Hello world
[Link]("Hello World!");
}
}
}

2.2. Komentarze
Komentarze, to tekst w kodzie programu, który pomijany jest przez kompilator podczas
procesu budowania aplikacji. Komentarze mają kilka zastosowań:

● Wyjaśnienie zawiłych lub pozornie dziwnych fragmentów kodu innym developerom,


● Chwilowe wyłączenie jakiegoś fragmentu logiki programu przed wykonaniem (np.:
podczas szukania błędów),
● Można postać użycie komentarzy w celu dokumentacji kodu.

Zgodnie z dzisiejszymi trendami w programowaniu, komentarze powinny być używane


tymczasowo lub w celu wyjaśnienia kodu pozornie podejrzanego ale niezbędnego (aby ktoś inny
go nie usunął lub nie zastanawiał się długo nad celem jego istnienia). Powodem jest tendencja
do nieutrzymywania komentarzy w zgodzie ze stanem kodu podczas jego rozwoju. Mogą one
wprowadzać innych developerów w błąd. Dodatkowo unikanie pisania komentarzy
automatycznie wspiera pisanie przez developerów kodu łatwiejszego do zrozumienia.

Komentarze celem wyjaśnienia tego co robi kod zastępuje prawidłowy podział kodu na
metody i klasy (zgodnie z SOLID - omawiany później). Dokumentację kodu najczęściej
zastępują UnitTesty.

Komentarze występują w dwóch wariantach:

● Komentarz jednolinijkowy

//Komentarz jednolinijkowy

● Komentarz wielolinijkowy:

/*
Komentarz wielolinijkowy
*/

W MS VisualStudio komentować kod można poprzez zaznaczenie tekstu i użycie skrótu


klawiszowego Ctrl+K, Ctrl+C. Odkomentowywanie wykonywane jest z kolei komendą Ctrl+K,
Ctrl+U.

W przypadku tworzenia np.: biblioteki przeznaczonej do szerszego użytku (udostępnianej na


repozytorium NuGet lub w innej formie innym developerom) klasy i metody publiczne można
dokumentować za pomocą tzw. summary. Summary jest dość rozbudowanym mechanizmem
dokumentowania w C# .NET, jednak częstotliwość jego wykorzystywania jest dość niska. Z tego
też powodu nie będzie ono szeroko omawiane. Informacje o możliwościach i sposobie pisania
dokumentacji przy użyciu summary można znaleźć tutaj.

2.3. Zmienne
Zmienna to obszar w pamięci programu, w którym możemy przechowywać jakąś wartość.
Zmienna posiada 4 podstawowe cechy:

● Typ - mówi o tym, jakiego rodzaju dane przechowujemy w zmiennej, np.: liczbę lub tekst,
● Nazwa - po nazwie możemy odwołać się do naszych danych w innym miejscu w
programie,
● Wartość - dane przechowywane w pamięci, do której odnosi się zmienna,
● Miejsce w pamięci - każda zmienna rezerwuje przestrzeń w pamięci pozwalającą na
zapisaniem w nim danych o wskazanym Typie.
2.4. Podstawowe typy danych
Typy podstawowe to proste typy danych, z których można budować większe, bardziej
złożone koncepty. Podstawowe typy danych w .NET dzielimy na 4 kategorie:

2.4.1. Textowe

Nazwa Opis

char Reprezentuje pojedynczy znak (UTF-16)

string Reprezentuje ciąg znaków

2.4.2. Liczby całkowite

Wszystkie typy Liczbowe całkowite reprezentują liczby bez części ułamkowej.

Nazwa Min. wartość Max. wartość Rozmiar [B]

byte 0 255 1

sbyte -127 128 1

short -32 768 32 767 2

ushort 0 65 535 2

int -2 147 483 648 2 147 483 647 4

uint 0 4 294 967 295 4

long -9 223 372 036 854 775 808 9 223 372 036 854 775 807 8

ulong 0 18 446 744 073 709 551 615 8

Domyślne liczby całkowite w .NET traktowane są jako typ int. Aby zmienna była innego typu,
należy:

● Wprost podać typ danych (nie używając słowa kluczowego var),


● Dokonać konwersji danych podczas przypisania, np.:

var x = (long)42;
var y = (uint)42;
Do liczb całkowitych możliwe jest przypisanie wartości zarówno w postaci liczby w systemie
dziesiętnym, szesnastkowym jak i binarnym:

var dec = 42;


var hex = 0x2A;
var bin = 0b_0010_1010;

2.4.3. Liczby zmiennoprzecinkowe

Nazwa Min. wartość Max. wartość Precyzja Rozmiar [B]

float ± 1,5 × 10− 45 ± 3.4 × 1038 Od 10-6 do 10-9 4

double ± 5.0 × 10-324 ± 1.7 × 10308 Od 10-15 do 10-17 8

decimal 1.0 × 10-28 7.9228 × 1028 Od 10-28 do 10-29 16

Domyślnie liczby zmiennoprzecinkowe traktowane są jako typ double. Aby zmienna była
innego typu, należy:

● Wprost podać typ danych (nie używając słowa kluczowego var),


● Dokonać konwersji danych podczas przypisania, np.:

var x = (float)42.01;
var y = (double)42.01;
var z = (decimal)42.01;

● Dodać oznaczenie typu po podaniu wartości:

var x = 42.01f; //float


var y = 42.01d; //double
var z = 42.01m; //decimal

2.4.4. Logiczne

Nazwa Dopuszczalne wartości Rozmiar [B]

bool true; false 1


2.5. Operator default
Dokumentacja - click i click.

Operator default pozwala na uzyskanie domyślnej wartości danej wybranego typu. Z definicji
są to wartości 0 lub jej odpowiedniki dla nieliczbowych typów danych:

● wartość 0 dla wszystkich zmiennych typu wartościowego:


○ liczb całkowitych,
○ liczb zmiennoprzecinkowych,
○ typu logicznego bool (w tym wypadku 0 to false),
○ dowolnego typu wyliczeniowego enum (nawet, jeżeli żadna z prawidłowych
wartości nie posiada przypisanej wartości 0).
● wartość null dla zmiennych typu referencyjnego, w tym:
○ typu string,
○ dowolnych klas.

var defaultInt = default(int); //0


var defaultDouble = default(double); //0.0d
var defaultBool = default(bool); //false
var defaultString = default(string); //null
var defaultMyClass = default(myClass); //null

W przypadku zastosowania operatora default na strukturze, wszystkie pola struktury


inicjalizowane są przy użyciu ich wartości domyślnych, np.:

public struct GeoCoords


{
public double Longitude;
public double Latitude;
}

public struct City


{
public GeoCoords Coordinates;
public string Name;
public int Population;
}

public City CreateEmptyCity()


{
var defaultCity = default(City);
/*
[Link] = 0.0d
[Link] = 0.0d
[Link] = null
[Link] = 0
*/
return default(City);
}

2.6. Stałe
Stałe to obiekty, których wartości nie można zmienić po przypisaniu do nich wartości. W .NET
występują dwa rodzaje stałych:

2.6.1. Stałe czasu kompilacji

Stałe czasu kompilacji (Compile-time constants) to stałe, których wartość musi być
jednoznacznie określona podczas kompilacji programu. Z tego powodu stałymi mogą być
jedynie typy proste. Stała czasu kompilacji może być stałą lokalną funkcji lub stałym polem klasy.

Stałe czasu kompilacji charakteryzują się:

● wysoką wydajnością - kompilator podstawia wartość stałej w miejsca jej wywołania,


● małą elastycznością - stała ma taką samą wartość z obrębie całego programu.

Stałe czasu kompilacji poprzedzamy słowem kluczowym const. Próba przypisania wartości
do stałej poza miejscem jej deklaracji wywoła błąd kompilacji.

using System;

namespace ConstsExample
{
public class Calculator
{
private const double _pi = 3.14;

public double GetCircleArea(double r)


{
const int powerExponent = 2;
return _pi * [Link](r, powerExponent);
}

public double GetSphereVolume(double r)


{
const int powerExponent = 3;
const double sphereFactor = 4.0 / 3.0;
return sphereFactor * _pi * [Link](r, powerExponent);
}
}
}

2.6.2. Stałe czasu wykonania (runtime)

Stałe czasu wykonania (runtime constants) to stałe, których wartość musi zostać przypisana
w momencie tworzenia obiektu, w którym są zdefiniowane. Stała czasu wykonania może być
dowolnego typu, jednak może nią być tylko i wyłącznie pole klasy. Zmienna czasu wykonania
musi zostać ustawiona podczas deklaracji pola lub w konstruktorze.

Stałe czasu kompilacji charakteryzują się (w porównaniu do stałych czasu kompilacji):

● niższą wydajnością - tworzone są dynamicznie w czasie pracy aplikacji,


● wyższą elastycznością - każdy obiekt może mieć swoją wartość stałych czasu
wykonania.

Stałe czasu wykonania poprzedzamy słowem kluczowym readonly. Próba przypisania


wartości do stałej poza deklaracją pola lub konstruktorem wywoła błąd kompilacji.

namespace ConstsExample
{
public class LinearFuncion
{
private readonly double _a = 1;
private readonly double _b = 1;

public LinearFuncion()
{
}

public LinearFuncion(double a, double b)


{
_a = a;
_b = b;
}

public double CalculateY(double x)


{
return (_a * x) + _b;
}
}
}
2.7. Ciągi znaków (typ string)
Dokumentacja (.net framework 4.8) - click.
Dokumentacja (.net core 3.1) - click.

String to klasa reprezentująca ciąg znaków. String, pomimo że jest klasą (typem
referencyjnym) jest immutable (niezmienny). Oznacza to, że raz stworzony ciąg znaków nie
może zostać zmieniony. Każda operacja mająca na celu modyfikację istniejącego ciągu znaków
powoduje utworzenie zupełnie nowego ciągu w pamięci programu.

String posiada wiele metod i operatorów ułatwiających przetwarzanie jego wartości. Poniżej
wymieniono najczęściej używane:

2.7.1. Konstrukcja w pamięci

Obiekt typu string jest obiektem referencyjnym. Oznacza to, że nazwa obiektu w programie
przechowuje adres do lokalizacji na stercie, w której znajdują się właściwe dane, w tym
wypadku kolejne znaki ciągu.

Podczas tworzenia stringa w pamięci programu jednorazowo rezerwowana jest ciągła


przestrzeń pozwalająca na zapisanie wszystkich znaków ciągu jeden po drugim. Po stworzeniu
przestrzeni tej nie można zmodyfikować aż do jej zwolnienia. Takie typy danych nazywamy
immutable (ang. niezmienne). Każda próba stworzenia nowego ciągu (poprzez jego modyfikację
czy rozbudowę) wiąże się z alokacją nowej przestrzeni w pamięci i zapisaniu w niej kompletu
danych (a co za tym idzie - powieleniu tych, które znajdowału się w ciągu przed modyfikacją)..

2.7.2. Ciąg pusty


Najprostszym ciągiem znaków jest ciąg pusty, czyli taki który nie zawiera ani jednego znaku
za wyjątkiem znaku ciągu znaków (który w .NET nie jest jawny). Pusty ciąg można stworzyć
samodzielnie lub z wykorzystaniem pola statycznego [Link].

var emptyString1 = [Link];


var emptyString2 = "";

2.7.3. Znaki specjalne

Typ string w .NET umożliwia wprowadzanie znaków specjalnych, takich jak znak nowej linii
(\n) czy tabulację (\t). W celu pominięcia (escapowania) szczególnego znaczenia tych znaków,
można wykorzystać standardowy mechanizm podwójnego backslash’a (\\). W celu ułatwienia
zapisu ciągów posiadających wiele backslash’y (jak np ścieżki do plików) powstał znak @.
Poprzedzienie nim ciągu znaków prowadzi do automatycznego escapowania wszystkich
znaków specjalnych (każdy znak traktowany jest dosłownie).
[Link]("\tab"); // ' ab'
[Link]("\\tab"); // '\tab'
[Link](@"\tab"); // '\tab'

2.7.4. Porównywanie

Zmienne typu string można porównywać ze sobą na dwa sposoby. Jednym z nich jest
standardowy operator porównania. Pomimo, że instancje string są typu referencyjnego, operator
porównania == w implementacji typu string został przeciążony i porównuje wartości zamiast
referencji.

public void StringCompareExample()


{
var stringA = "SOME_TEXT";
var stringB = "some_text";

[Link](stringA == stringB); //false


}

Dodatkowo istnieje również zestaw statycznych przeciążeń funkcji Compare. Każde z nich
służy do porównania całości lub fragmentów dwóch stringów. Jako wynik funkcje te zwracają
odpowiednio jedną z następujących wartości:

● < 0 - string podany jako pierwszy parametr poprzedzał by po sortowaniu string podany
jako parametr drugi,
● > 0 - string podany jako drugi parametr poprzedzał by po sortowaniu string podany jako
parametr pierwszy,
● == 0 - wartości obu parametrów są takie same.

Poniżej zostały przytoczone dwa najprostsze, resztę można odnaleźć w dokumentacji.


Pierwszy działa analogicznie do operatora ==. Drugi z kolei jako trzeci parametr przyjmuje flagę
ignoreCase - jeżeli ustawimy ją na true, podczas porównywania znaków w wybranych ciągach
wielkość liter nie będzie brana pod uwagę.

public void StringCompareExample()


{
var stringA = "SOME_TEXT";
var stringB = "some_text";

[Link]([Link](stringA, stringB) == 0); //false


[Link]([Link](stringA, stringB, true) == 0); //true
}
2.7.5. Konkatenacja (łączenie)

Łączenie dwóch lub więcej ciągów znaków w jeden można dokonać na dwa podstawowe
sposoby. Pierwszym z nich jest wykorzystanie operatora +.

namespace StringExample
{
public class Person
{
public enum SexType { Male, Female }

public string Name { get; set; }


public string Surname { get; set; }
public SexType Sex { get; set; }
public bool IsMarried { get; set; }
}

public class StringComparer


{
public string GetFullTitle(Person person)
{
string title;

if ([Link] == [Link])
{
title = "Mr. ";
}
else
{
title = [Link] ? "Mrs. " : "Ms. ";
}

return title + [Link] + " " + [Link];


}
}
}

Niestety łączenie stringów za pomocą operatora + nie jest najbardziej wydajnym


rozwiązaniem. Każde jego użycie powoduje tworzenie się w pamięci nowego, dłuższego
łańcucha.

Alternatywną jest wykorzystanie metody Concat. Posiada ona kilka przeciążeń, z których
część przyjmuje dwa, trzy lub cztery ciągi i zwraca w odpowiedzi jeden połączony.
namespace StringExample
{
public class Person
{
public enum SexType { Male, Female }

public string Name { get; set; }


public string Surname { get; set; }
public SexType Sex { get; set; }
public bool IsMarried { get; set; }
}

public class StringComparer


{
public string GetFullTitle(Person person)
{
string title;

if ([Link] == [Link])
{
title = "Mr. ";
}
else
{
title = [Link] ? "Mrs. " : "Ms. ";
}

return [Link](title, [Link], " ", [Link]);


}
}
}

2.7.6. Formatowanie

Formatowanie to nieco bardziej rozbudowana forma konkatenacji. Pozwala na osadzenie


wartości zmiennych w stałym szablonie tekstowym. Formatowanie możliwe jest na dwa
sposoby.

Pierwszym z nich jest wykorzystanie funkcji statycznej Format. Funkcja ta przyjmuje szablon
tekstu z zaznaczonymi miejscami do uzupełnienia oraz parametry, którymi ma zostać
wypełniony. Miejsca te oznaczone są indexem parametru, który ma znaleźć się w określonym
miejsca.

namespace StringExample
{
public class Person
{
public enum SexType { Male, Female }

public string Name { get; set; }


public string Surname { get; set; }
public SexType Sex { get; set; }
public bool IsMarried { get; set; }
}

public class StringComparer


{
public string GetFullTitle(Person person)
{
string title;

if ([Link] == [Link])
{
title = "Mr";
}
else
{
title = [Link] ? "Mrs" : "Ms";
}

return [Link]("{0}. {1} {2}", title, [Link], [Link]);


}
}
}

Drugim sposobem jest tzw. string interpolation. Ten sposób konstrukcji stringa pozwala na
utworzenie całego łańcucha przy wykonaniu pojedynczej alokacji pamięci.

Aktualnie w nowo pisanym oprogramowaniu jest to najpopularniejsza forma konkatenacji i


formatowania łańcuchów znaków ze względu na prostotę zapisu jak i dobrą wydajność
pamięciową. Tworzenie stringa z wykorzystaniem string interpolation oznacza się poprzez
wpisanie znaku $ przed otwierającym znakiem cudzysłowu. Wartości do szablonu sprowadza
się poprzez wpisanie nazwy zmiennej w nawiasach klamrowych.

namespace StringExample
{
public class Person
{
public enum SexType { Male, Female }
public string Name { get; set; }
public string Surname { get; set; }
public SexType Sex { get; set; }
public bool IsMarried { get; set; }
}

public class StringComparer


{
public string GetFullTitle(Person person)
{
string title;

if ([Link] == [Link])
{
title = "Mr";
}
else
{
title = [Link] ? "Mrs" : "Ms";
}

return $"{title}. {[Link]} {[Link]}";


}
}
}

2.7.7. Przeszukiwanie stringów

Jako, że typ string wewnętrznie jest ciągiem znaków (char) możliwe jest jego przeszukiwania
pod kątem posiadania konkretnych podciągów czy elementów.

Jedną z metod realizujących ten cel są metody IndexOf. Pozwala ona na odnalezienie indexu
elementu w ciągu znaków, pod którym znajduje się konkretny znak lub rozpoczyna się konkretny
inny ciąg znaków. Jeżeli taki element nie zostanie odnaleziony, zwracana jest wartość -1.

var someText = "some_text";

[Link]([Link]("some")); //0
[Link]([Link]("text")); //5
[Link]([Link]('_')); //4

[Link]([Link]("qwerty")); //-1
Podobne, nieco bardziej rozbudowane, działanie posiada metoda IndexOfAny. Przyjmuje ona
jako parametr tablicę szukanych znaków lub stringów. Metoda ta przeszuka ciąg pod kątem
posiadania któregokolwiek z elementów tablicy przekazanej w parametrze i zwróci index
pierwszego trafienia.

var advert1 = "The phone is pretty good and reasonably priced";


var advert2 = "This car is rubbish and it’s cost is ridiculously high";
var advert3 = "This laptop is awesome. Simply buy it";

var priceVerbs = ["price","cost","money","cash"];

[Link]("Does advert contain price info:");


[Link]($"Adv1: {[Link](priceVerbs) > -1}"); //true
[Link]($"Adv2: {[Link](priceVerbs) > -1}"); //true
[Link]($"Adv3: {[Link](priceVerbs) > -1}"); //false

Jeżeli interesuje nas jedynie informacja o istnieniu danego elementu w ciągu (niepotrzebna
jest znajomość miejsca jego położenia w ciągu), można w tym celu użyć metody Contains.
Metoda ta działa analogicznie do sprawdzenia [Link](x) > -1.

var someText = "some_text";

[Link]([Link]("some")); //true
[Link]([Link]("text")); //ture
[Link]([Link]('_')); //true

[Link]([Link]("qwerty")); //false

Możliwe jest również łatwe sprawdzenie zawartości początku i końca ciągu przy użyciu
odpowiednio metod StartsWith i EndsWith.

public bool GenerateDataReport(string filename)


{
const string fileFormatExpected = ".xlsx";
const string testFilePrefix = "TEST_";

if (![Link](fileFormatExpected))
{
[Link]($"Only {fileFormatExpected} is supported. Aborting
analysis.");
return false;
}
var analysisResults = AnalyzeDataFromFile(filename);
GenerateDataReport(analysisResults);

if([Link](testFilePrefix))
{
[Link]("Test file detected. Data not saved in
database.");
}
else
{
SaveResultsInDb(analysisResults);
}
}

2.7.8. Modyfikowanie zawartości ciągu

Kolejne trzy metody pozwalają na utworzenie nowego ciągu poprzez modyfikację już
istniejącego.

Pierwsza z nich to Remove, który pozwala na usunięcie wybranego fragmentu stringa.


Istnieją dwa przeciążenia metody Remove. Pierwsza - jednoparametrowa - pozwala na
usunięcie wszystkich znaków do samego końca ciągu zaczynając od wskazanego indexu.
Druga - dwuparametrowa - pozwala na usunięcie określonej liczby znaków od wskazanego
indexu.

Druga z nich to Insert, który pozwala na wstawienie dowolnego ciągu znaków w wybrane
miejsce ciągu już istniejącego.

var originalText = "Seven cars took part in the accident. Five cars were
totaled.";

var temp = [Link](6, 4); //Seven took part in the accident.


Five cars were totaled.
var finalText = [Link](6, "vehicles"); //Seven vehicles took part in
the accident. Five cars were totaled.

Trzecia to Replace, który wyszukuje wszystkie wystąpienia wskazanego ciągu i wymienia go


na inny.

var originalText = "Seven cars took part in the accident. Five cars were totaled.";

var finalText = [Link](“cars”, "vehicles"); //Seven vehicles took part in the accident.
Five vehicles were totaled.
2.7.9. Dzielenie i łączenie kolekcji ciągów
String posiada zaimplementowaną funkcję pozwalającą na dzielenie jednego ciągu na wiele
mniejszych elementów po wybranym separatorze. Taką funkcjonalność realizuje funkcja Split.
Jej odwrotnością jest funkcja Join, która pozwala na połączenie wszystkich ciągów znaków ze
wskazanej kolekcji w jeden, wstawiając pomiędzy elementy kolekcji wybrany separator.

var csvData = "Jan,Kowalski,10-10-1985,Gdansk";


var separatedData = [Link](new[] { ',' });
var joinedData = [Link](";", separatedData);
//Jan;Kowalski;10-10-1985;Gdansk

2.7.10. Weryfikacja niepustości

Często zdarza się, że przed wykorzystaniem wartości jakiego stringa, najpierw należy
sprawdzić czy ta zawartość faktycznie niesie pewne informacje. Zazwyczaj sprowadza się to do
sprawdzenia, czy string nie jest nullem i czy zawiera jakieś znaki. Typ string posiada
wbudowane dwie metody na tę ewentualność - IsNullOrEmpty oraz IsNullOrWhiteSpace.

IsNullOrEmpty zwróci prawdę, jeżeli podany string jest nullem lub nie zawiera żadnych
znaków ([Link]). IsNullOrWhiteSpace dodatkowo zwróci prawdę, jeżeli wewnątrz ciągu
znajdują się tylko białe znaki (spacje, taby, znaki nowej linii, itp).

var nullStr = (string)null;


var emptyStr = [Link];
var whitespaceStr = " ";
var someStr = "some_text";

[Link](IsNullOrEmpty(nullStr)); //true
[Link](IsNullOrEmpty(emptyStr)); //true
[Link](IsNullOrEmpty(whitespaceStr)); //false
[Link](IsNullOrEmpty(someStr)); //false

[Link](IsNullOrWhiteSpace(nullStr)); //true
[Link](IsNullOrWhiteSpace(emptyStr)); //true
[Link](IsNullOrWhiteSpace(whitespaceStr)); //true
[Link](IsNullOrWhiteSpace(someStr)); //false

2.7.11. Normalizacja wielkości liter


Aby wielokrotnie nie porównywać danych z wykorzystaniem opcji IgnoreCase czy też
podczas przetwarzania, wyświetlania lub raportowania przedstawiać je w spójnej formie,
wielkość liter poddaje się normalizacji (uwspólnieniu). Służą do tego dwie metody
zaimplementowane w typie string. ToUpper zwraca nowy string, który zawiera te same znaki co
string wejściowy, jednak wszystkie litery są duże. Przeciwieństwem jest metoda ToLower, która
zachowuje się identycznie, jednak tekst wyjściowy posiada wyłącznie małe litery.

var pokemon = "PoKeMoN!";


var lowText = [Link](); //pokemon!
var upperText = [Link](); //POKEMON!

2.7.12. Przycinanie ciągu (usuwanie białych znaków)

Funkcjonalność szczególnie przydatna w przypadku parsowania dokumentów czy


wprowadzania do systemu danych pobranych od użytkownika. Czasami dane poprzedzają lub
kończą niechciane białe znaki. Uniemożliwia to choćby efektywne porównywanie ciągów, gdyż
ciąg poprzedzony spacją będzie różny od nieporzedzonego, nawet jeżeli sam tekst jest taki
sam.

W tej sytuacji pomocne są metody Trim. Metodra TrimStart usuwa wszystkie białe znaki
poprzedzające tekst w ciągu. Metoda TrimEnd usuwa wszystkie białe znaki znajdujące się po
tekście w ciągu znaków. Metoda Trim to połączenie funkcjonalności metod TrimStart i TrimEnd.

using System;

namespace StringExample
{
public class UsersService
{
private string _loggedUser = "Bulczi";

public void LoggedUserCheckExample()


{
var userName = " Bulczi ";

[Link](_loggedUser == userName); //false


[Link](_loggedUser == [Link]()); //true
}
}
}

2.8. StringBuilder
Dokumentacja (.net framework 4.8) - click.
Dokumentacja (.net core 3.1) - click.

Ze względu na swoją konstrukcję w pamięci nie zaleca się konstruowania długich stringów w
pętlach przy użyciu konkatenacji. Powoduje to tworzenie dużej ilości obiektów o rosnącym
rozmiarze, co w efekcie zaśmieca pamięć, wymusza częste uruchamianie Garbage Collectora i
negatywnie wpływa na wydajność pamięciową programu.

W przypadku konieczności konstruowania ciągu w taki sposób należy wykorzystać obiekt


klasy StringBuilder. StringBuilder przechowuje wszystkie elementy ciągu, które chcemy
połączyć całość osobno aż do momentu, w którym poprosimy go o przekazanie całości w
formie jednego ciągu. Dopiero wtedy alokuje on pamięć niezbędną do przechowania wszystkich
znaków w ciągu i jednorazowo umieszcza je wszystkie w pamięci programu.

using System;
using [Link];

namespace StringExample
{
public class User
{
public string Name { get; set; }
public string Surname { get; set; }
public string Email { get; set; }
public DateTime BirthDate { get; set; }
}

public class UsersService


{
public void ExportAllUsersToFile(User[] users, string filepath)
{
var stringBuilder = new StringBuilder();

foreach(var user in users)


{
var userData =
$"{[Link]},{[Link]},{[Link]},{[Link]}";
[Link](userData);
}

var data = [Link]();


SaveToFile(data, filepath);
}
}
}

2.9. Konwersje typów (implicit, explicit, TryParse)


Podczas pisania kodu czasami konieczna jest zamiana jednego typu danych w inny. .NET
dopuszcza kilka rodzajów konwersji typów.
2.9.1. Implicit conversion

Konwersja niejawna (implicit) polega na zamianie jednego typu w drugie bez konieczności
jawnego jej zaznaczenia w kodzie. Ten rodzaj konwersji dotyczy głównie danych numerycznych
(zmiany między typami liczb całkowitych i typami zmiennoprzecinkowymi czy też danych o
różnych zakresach i rozmiarach.

int a = 10;
double b = a;

Konwersje niejawną można wykorzystać również podczas przypisywania obiektu pewnej


klasy do zmiennej typu, z którego ta klasa dziedziczy.

namespace StringExample
{
public class Car
{
public string Name { get; set; }
public void Drive() { }
}

public class SportCar : Car


{
public void DriveFast() { }
}

public class CarsService


{
public void InheritanceImpConvertionExample()
{
Car car = new SportCar();
}
}
}

Próba wykonania nieprawidłowej konwersji zostanie zasygnalizowana błędem kompilacji.

Pełna lista liczbowych konwersji ukrytych w .NET C# można znaleźć tutaj.

2.9.2. Explicit conversion

Konwersja jawna (explicit) polega na zamianie jednego typu w drugi z jawnym jej
zaznaczenia w kodzie. Ten rodzaj konwersji dotyczy głównie danych numerycznych (zmiany
między typami liczb całkowitych i typami zmiennoprzecinkowymi czy też danych o różnych
zakresach i rozmiarach, ale występuje również choćby podczas konwersji wartości zmiennej
typu wyliczeniowego (enum) na zmienną typu int.

Aby dokonać jawnej konwersji typu, należy przed nazwą zmiennej, która ma zostać poddana
konwersji, napisać w nawiasach typ, na który ma zostać skonwertowana.

double a = 10.0d;
int b = (int)a;

Próba wykonania nieprawidłowej konwersji zostanie zasygnalizowana wyrzuceniem wyjątku


podczas działania programu.

Pełna lista liczbowych konwersji ukrytych w .NET C# można znaleźć tutaj.

2.9.3. Boxing i Unboxing

Boxing i unboxing to techniki wykorzystujące explicit conversion oraz fakt, że każdy obiekt w
.NET dziedziczy po bazowej klasie object. Boxing to rzutowanie dowolnego obiektu do klasy
object, podczas gdy unboxing to ponowne jego rzutowanie do pierwotnej reprezentacji.

namespace ConversionsExample
{
public class Person
{
public string Name { get; set; }
public string Surname { get; set; }
}

public class UsersService


{
public void BoxingAndUnboxingExample()
{
var person = new Person
{
Name = "Jan",
Surname = "Nowak"
};

var boxedPerson = (object)person;


var unboxedPerson = (Person)boxedPerson;
}
}
}
Mechanizm boxingu i unboxingu jest dość powszechnie używany za code smell, gdyż
zaciemnia kod oraz może powodować wyrzucanie niepotrzebnych wyjątków wymagających
obsługi. Należy go używać wyłącznie w sytuacjach, w którym jest on absolutnie niezbędny.

2.9.4. AS i IS

Słowa kluczowe AS oraz IS pozwalają na bezpieczniejsze korzystanie z mechanizmu jawnej


konwersji między typami dla typów referencyjnych.

Słowo kluczowe IS pozwala na sprawdzenie, czy dany obiekt może zostać rzutowany na
wybrany typ danych (poprzez przeszukanie jego drzewa dziedziczenia).

Słowo kluczowe AS dokonuje rzutowania na wybrany typ, jednak w przeciwieństwie do


explicit conversion jeżeli konwersji nie da się przeprowadzić, w odpowiedzi zamiast wyrzucenia
wyjątku zwracany jest null.

AS oraz IS w celu uzyskania wyniku przeszukują drzewo dziedziczenia danego obiektu.


Innymi słowy konwersji typów słowami kluczowymi AS oraz IS nie definiuje się jawnie w
implementacji klasy - ich zachowanie definiuje to, w jaki sposób dziedziczą po sobie na wzajem.

using System;

namespace ConversionsExample
{
public class Car
{
public string Name { get; set; }
public void Drive() { }
}

public class SportCar : Car


{
public void DriveFast() { }
}

public class Truck : Car


{
public void DeliverCargo() { }
}

public class CarsService


{
public void ConversionsExample()
{
Car car = new SportCar { Name = "DB11" };
var isSportCar = car is SportCar ? "is" : "is not";
[Link]($"{[Link]} {isSportCar} a sport car");
//DB11 is a sport car
SportCar asSportCar = car as SportCar; //an object

var isTruck = car is Truck ? "is" : "is not";


[Link]($"{[Link]} {isTruck} a truck"); //DB11 is
not a truck
Truck asTruck = car as Truck; //null
}
}
}

2.9.5. TryParse i ToString

Każdy typ w .NET posiada metodę ToString. W przypadku typów podstawowych ich
implementacja pozwala na uzyskanie reprezentacji tekstowej wartości danej zmiennej.

var x = 42.01f;
var y = 42.01d;
var z = 42.01m;

string xStr = [Link]();


string yStr = [Link]();
string zStr = [Link]();

Dodatkowo każdy typ liczbowy oraz typ wyliczeniowy posiada metodę Parse oraz TryParse.
Obie z nich służą do próby zamiany stringa na prawidłową wartość tego typu. Różnica między
nimi polega na zachowaniu w przypadku niepowodzenia. Parse w takiej sytuacji rzuci wyjątek
wymagający obsłużenia. TryParse z kolei zwraca wartość typu bool określającą, czy
parsowanie się powiodło.

public void ParsingExample()


{
var text1 = "12";
var text2 = "someText";

TryParseInt(text1); // '12' successfully parsed into int = 12


TryParseInt(text2); // 'someText' is not an int

ParseInt(text1); // '12' successfully parsed into int = 12


ParseInt(text2); // Exception will be thrown - invalid data format
}
private void TryParseInt(string text)
{
int result;

if ([Link](text, out result))


{
[Link]($"'{text}' successfully parsed into int =
{result}");
}
else
{
[Link]($"'{text}' is not an int");
}
}

private void ParseInt(string text)


{
var result = [Link](text);
[Link]($"'{text}' successfully parsed into int = {result}");
}

Nieco ciekawszy i bardziej złożony jest przypadek parsowania daty. Data i godzina może być
reprezentowana w formie tekstowej na wiele sposobów. Domyślne parsowanie czy rzutowanie
do stringa (metodami Parse, TryParse oraz ToString) odbywa się z wykorzystaniem ustawień
regionalnych systemu, na którym działa system.

Jeżeli system ma obsługiwać inne formaty dat, można wykorzystać przeciążenie metody
ToString przyjmującej format daty, jaki chcemy uzyskać w efekcie rzutowania. .NET posiada
kilka predefiniowanych formatów oraz możliwość zdefiniowania własnego. String może zostać
zamieniony z powrotem w obiekt daty przy użyciu metody ParseExact lub TryParseExact, które
również przyjmują informację o formacie, w jakim data zapisana jest w stringu.

Dokumentacja - standardowe formaty daty - click.


Dokumentacja - nietypowe formaty daty - click.

2.9.6. UserDefined conversions

Dokumentacja - click.

Podczas tworzenia własnych klas i struktur, możliwe jest zdefiniowanie własnej logiki
konwersji jawnej i niejawnej. Dokonuje się tego z użyciem słów kluczowych operator oraz
odpowiednio implicit lub explicit. Sama definicja logiki to funkcja opisująca jak dokonać zamiany
jednego typu w drugi.
Poniżej znajduje się przykład zaczerpnięty wprost z dokumentacji MSDocs. Przykład zawiera
definicje struktury Digit, która implementuje konwersję jawną z typu byte na Digit, oraz niejawną
z typu Digit na byte.

using System;

namespace UserDefinedConversionsExample
{
public struct Digit
{
private readonly byte _digit;

public Digit(byte digit)


{
if (digit > 9)
{
throw new ArgumentOutOfRangeException(nameof(digit), "Digit
cannot be greater than nine.");
}

_digit = digit;
}

public static implicit operator byte(Digit d) => d._digit;


public static explicit operator Digit(byte b) => new Digit(b);

public override string ToString() => $"{_digit}";


}

public class DigitConverter


{
public void Test()
{
var digit1 = new Digit(7);
byte y = digit1; //y eq 7

byte x = 2;
Digit digit2 = (Digit) x; //Digit2 eq. 2
}
}
}

2.10. Klasa Console i interakcja z użytkownikiem


Dokumentacja (.NET Framework) - click.
Dokumentacja (.NET Core) - click.

Klasa Console pozwala na prowadzenie interakcji z użytkownikiem za pomocą konsoli


systemowej. W dzisiejszych systemach komunikacja z użyciem konsoli naturalnie nie jest
częstym sposobem komunikacji z użytkownikiem końcowym. Często jest jednak
wykorzystywana podczas pisania oprogramowania w celach:

● diagnostycznych,
● pisania api pozwalającego na zautomatyzowane wykonywanie operacji dostępnych w
systemie,
● pisania narzędzi dla osób biegłych w obsłudze komputera (administratorów,
programistów, itd).

2.10.1. Wyświetlanie i pobieranie danych z użyciem konsoli

W celu wyświetlenia na ekranie wybranego tekstu można użyć metod statycznych Write lub
WriteLine. Metoda Write wyświetla na ekranie dokładnie tekst, który został podany jej jako
parametr. Metoda WriteLine dodatkowo stawia znak nowej linii na końcu wyświetlonego wiersza.

W celu pobrania danych od użytkownika możliwe jest użycie metody ReadLine. Zwraca ona
wszystkie wpisane przez użytkownika znaki aż do momentu wpisania znaku nowej linii
(wciśnięcia klawisza enter).

Klasa Console posiada również metodę Read. Różnica między metodą ReadLine i Read jest
jednak znacznie większa niż ma to miejsce w przypadku Write i WriteLine. Metoda Read
odczytuje dane wprowadzane przez użytkownika po jednym znaku. Logika zakończenia
pobierania danych musi zostać napisana samodzielnie przez programistę.

using System;

namespace ConsoleExamples
{
public class User
{
public string Login;
public string Password;
}

public class UsersHelper


{
public User GetUserData()
{
[Link]("Please enter user following data:");

[Link]("Login: ");
var login = [Link]();

[Link]("Password: ");
var password = [Link]();

var user = new User


{
Login = login,
Password = password
};

[Link]("Thank you for providing all the data!");

return user;
}
}
}

2.10.2. Manipulacja położeniem kursora

Konsola systemowa to w gruncie rzeczy bufor znaków reprezentowany w postaci macierzy o


wybranym rozmiarze. Kursor, czyli miejsce w którym pojawi się następnie wypisany tekst
można swobodnie przesuwać w obrębie tej macierzy za pomocą współrzędnych (X,Y). Typowo
dla systemów komputerowych, komórka o współrzędnych (0,0) znajduje się w lewym górnym
rogu, a wartość współrzędnych Y rośnie wraz z przesuwaniem się w dół konsoli (do kolejnych
wierszy).

Położeniem kursora można manipulować za pomocą własności [Link] oraz


[Link], które odpowiednio reprezentują współrzędna X i Y. Jeżeli istnieje potrzeba
ustawienia obu wartości jednocześnie, można również skorzystać z metody
[Link].

[Link] = 2;
[Link] = 5;
[Link]($"X={[Link]};Y={[Link]}");
//X=2;Y=4

[Link](4, 10);
[Link]($"X={[Link]};Y={[Link]}");
//X=4;Y=10
2.10.3. Modyfikacja koloru tła i tekstu

Klasa Console pozwala również na modyfikację koloru tła i czcionki wyświetlanej na ekranie.
Można tego dokonać przy użyciu własności BackgroundColor i ForegroundColor. Dodatkowo,
aby przywrócić domyślną kolorystykę, utworzona została metoda ResetColor.

[Link]("Text in standard colors");

[Link] = [Link];
[Link] = [Link];

[Link]("White text on red bg");

[Link] = [Link];
[Link] = [Link];

[Link]("Red text on white bg");

[Link]();

[Link]("Text in standard colors");

2.10.4. Przekierowywanie strumieni danych

Artykuły - click i click.

Konsola systemowa posiada trzy rodzaje strumieni danych, które są na niej wyświetlane:

● StandardInput - wyświetlanie standardowych komunikatów,


● StandardOutput - pobieranie danych,
● StandardError - wyświetlanie komunikatów o błędach (np.: nieobsłużonych wyjątkach).

Każdy z tych strumieni posiada trzy elementy pozwalające na:

● sprawdzenie, czy strumień jest przekierowany,


● zamianę standardowego strumienia na inny,
● pobranie aktualnego strumienia.

Typ danych Czy jest Pobranie aktualnego Ustawienie nowego


przekierowany strumienia strumienia

Input IsInputRedirected OpenStandardInput() SetIn()

Output IsOutputRedirected OpenStandardOutput() SetOut()

Error IsErrorRedirected OpenStandardError() SetError()


using System;
using [Link];

namespace ConsoleExamples
{
public class ConsoleIoRedirector : IDisposable
{
private StreamReader _inputStream;
private StreamWriter _outputStream;
private StreamWriter _errorStream;

public void RedirectStandardInput(string filePath)


{
if ([Link])
{
throw new Exception("Input already redirected!");
}

if(_inputStream != null)
{
CloseStreamReader(_inputStream);
}

_inputStream = CreateStreamReader(filePath);
[Link](_inputStream);
}

public void RedirectStandardOutput(string filePath)


{
if ([Link])
{
throw new Exception("Output already redirected!");
}

if (_outputStream != null)
{
CloseStreamWriter(_outputStream);
}

_outputStream = CreateStreamWriter(filePath);
[Link](_outputStream);
}

public void RedirectStandardError(string filePath)


{
if ([Link])
{
throw new Exception("Output already redirected!");
}

if (_errorStream != null)
{
CloseStreamWriter(_errorStream);
}

_errorStream = CreateStreamWriter(filePath);
[Link](_errorStream);
}

private StreamReader CreateStreamReader(string filePath)


{
if (![Link](filePath))
{
throw new Exception($"File {filePath} dont exist!");
}

return new StreamReader(filePath);


}

private StreamWriter CreateStreamWriter(string filePath)


{
if ([Link](filePath))
{
throw new Exception($"File {filePath} already exist!");
}

return new StreamWriter(filePath);


}

public Stream GetCurrentInputStream()


{
return [Link]();
}

public Stream GetCurrentOutputStream()


{
return [Link]();
}

public Stream GetCurrentErrorStream()


{
return [Link]();
}

private void CloseStreamReader(StreamReader stream)


{
[Link]();
[Link]();
}

private void CloseStreamWriter(StreamWriter stream)


{
[Link]();
[Link]();
}

public void Dispose()


{
CloseStreamReader(_inputStream);
_inputStream = null;

CloseStreamWriter(_outputStream);
_outputStream = null;

CloseStreamWriter(_errorStream);
_errorStream = null;
}
}
}

2.10.5. Czyszczenie konsoli

W celu wyczyszczenia okna konsoli (usunięcia z niej całego widocznego tekstu) należy użyć
metody statycznej [Link]().

2.11. Petle
Pętle pozwalają na wielokrotne wykonanie pewnego bloku kodu programu aż do momentu
osiągnięcia warunku definiującego koniec jej wykonania.

2.11.1. While

Dokumentacja - click.

Pętla while wykonuje kolejne obroty tak długo, jak warunek do niej przekazany jest prawdziwy.
Jeżeli warunek jest nieprawdziwy przed pierwszym obrotem, kod wewnątrz pętli nie zostanie
wykonany ani razu.
public void RepeatUntiExit()
{
bool exit = false;

while(!exit)
{
[Link]("Type sth: ");
var input = [Link]();
[Link](input);

exit = [Link]() == "exit";


}
}

2.11.2. do-while

Dokumentacja - click.

Pętla do-while działa analogicznie do pętli while. Ma ona jednak nieco inną konstrukcję, dzięki
której gwarantowany jest co najmniej jeden obrót pętli.

public void RepeatUntiExit()


{
bool exit;

do
{
[Link]("Type sth: ");
var input = [Link]();
[Link](input);

exit = [Link]() == "exit";


}
while (!exit);
}

2.11.3. For

Dokumentacja - click.

Pętla for pozwala na wykonywanie obrotów funkcji aż do momentu osiągnięcia warunku


końcowego (w przeciwieństwie do pętli while). Pętla for przyjmuje 3 elementów, których każdy
jest opcjonalny:
● Inicjalizator - deklaracja zmiennej na potrzeby pętli, widoczna tylko w jej obrębie,
● Warunek końca pętli - po jego osiągnięciu następne obroty nie będą wykonywane,
● Iterator - zachowanie wykonywane po każdym obrocie pętli.

namespace LoopsExamples
{
public class LinearFuncion
{
private readonly double _a;
private readonly double _b;

public LinearFuncion(double a, double b)


{
_a = a;
_b = b;
}

public void PrintPointsInRange(int minX, int maxX)


{
for(var x = minX; x <= maxX; x++)
{
var y = (_a * (double)x) + _b;
[Link]($"(x={x};y={y})");
}
}
}
}

2.11.4. Foreach

Dokumentacja - click.

Częstym wykorzystaniem pętli for było iterowanie po kolejnych elementach kolekcji danych w
celu wykonania na nich pewnych operacji. W celu uproszczenia tworzenia tego zachowania
powstała pętla foreach. Jej celem jest wykonanie obrotu kodu pętli dla każdego elementu w
kolekcji przekazanej do pętli.

using System;
using [Link];

namespace LoopsExamples
{
public class Point
{
public double X;
public double Y;
}

public class LinearFuncion


{
private readonly double _a;
private readonly double _b;

public LinearFuncion(double a, double b)


{
_a = a;
_b = b;
}

public void PrintPointsInRange(int minX, int maxX)


{
var points = CalculatePointsInRange(minX, maxX);

foreach(var point in points)


{
[Link]($"(x={point.X};y={point.Y})");
}
}

private List<Point> CalculatePointsInRange(int minX, int maxX)


{
var points = new List<Point>();

for (var x = minX; x <= maxX; x++)


{
var y = (_a * (double)x) + _b;
[Link](new Point { X = x, Y = y });
}

return points;
}
}
}

Pętla foreach nie pozwala na modyfikację kolekcji w trakcie jej działania. Próba zmiany
rozmiaru zbioru, na którym iteruje pętla, spowoduje wyrzucenie wyjątku. Modyfikacja elementów
pętli jest jak najbardziej możliwa.

public List<Point> GetPositivePointsInRange(int minX, int maxX)


{
var points = CalculatePointsInRange(minX, maxX);

foreach(var point in points)


{
if(point.Y < 0)
{
[Link](point);
}
}

return points;
}

2.12. Rekurencja
Artykuł - click.

Rekurencja to technika, w której metoda modyfikuje odpowiednio swoje parametry wejściowe


po czym wywołuje ponownie samą siebie przy ich użyciu. Funkcja rekurencyjna musi posiadać
warunek końca, czyli fragment przebiegu, który dla pewnych parametrów wejściowych zwraca
konkretną wartość.

Przykładem obliczeń, które charakteryzują się taką cechą, są np.: ciągi liczbowe. Poniższy
przykład przedstawia dwa sposoby wypisywania na ekranie ciągu liczb Fibonacciego: iteracyjny
(przy użyciu pętli) oraz rekurencyjny.

using System;

namespace RecursionExample
{
public class Program
{
static void Main()
{
const int length = 10;

PrintFibonacciSequenceRecursive(0, 1, 1, length);
[Link]();

PrintFibonacciSequenceIterative(length);
[Link]();
}

private static void PrintFibonacciSequenceRecursive(int a, int b,


int countedElements, int length)
{
if(length <= 0)
{
throw new ArgumentException("Length must be more than 0");
}

if(length == 1)
{
[Link]("0");
return;
}

if (length == 2)
{
[Link]("0 1");
return;
}

if (countedElements <= length)


{
[Link]("{0} ", a);
PrintFibonacciSequenceRecursive(b, a + b, countedElements +
1, length);
}
}

private static void PrintFibonacciSequenceIterative(int length)


{
int a = 0;
int b = 1;
int c = 0;

if (length <= 0)
{
throw new ArgumentException("Length must be more than 0");
}

if (length == 1)
{
[Link](a);
return;
}

[Link]($"{a} {b}");

if (length == 2)
{
return;
}

for (int i = 2; i < length; i++)


{
c = a + b;
a = b;
b = c;

[Link]($" {c}");
}
}
}
}

Rekurencje w każdym przypadku można zastąpić przy użyciu użyciu pętli. Najważniejsze
różnice między użyciem rekurencji a pętl, to:

● Rekurencja jest mniej wydajna - każde wywołanie funkcji to pewien dodatkowy narzut
związany z samym jej wywołaniem (jak odłożenie kodu powrotu czy argumentów na
stosie). Obciąża ona bardziej zarówno pamięć jak i procesor.
● Nieskończona lub zbyt długa rekurencja prowadzi do wyczerpania pamięci na stosie,
wyrzucenia wyjątku StackOverflowException i wyłączenie się aplikacji. Pętla pochłaniać
będzie kolejne cykle procesora, jednak aplikacja w dalszym ciągu będzie działać.

2.13. Intrukcje warunkowe


Instrukcje warunkowe pozwalają na zmianę kolejności wykonywanych operacje w zależności
od zdefiniowanych predykatów i aktualnych danych.

2.13.1. if-else

Dokumentacja - click.

Instrukcja warunkowa, która determinuje wykonanie wykonanie bloku kodu w przypadku


spełnienia danego warunku. If-else składa się z dwóch elementów:

● Bloku kodu poprzedzonym słowem kluczowym if i predykatem, którego spełnienie


pozwala na wejście programowi do tego bloku. If jest elementem wymaganym.

public string GetInputFromUser(string message)


{
if([Link](message))
{
message = "Please enter text";
}

[Link]($"{message}: ");
var text = [Link]();

return text;
}

● Blok kodu poprzedzony słowem kluczowym else, który występuje po bloku z if’em,. Blok
ten wykonywany jest, jeżeli predykat przed słowem kluczowym if nie został spełniony.

public void PrintNumberEvenParity(int number)


{
if(number % 2 == 0)
{
[Link]($"Number {number} is even");
}
else
{
[Link]($"Number {number} is odd");
}
}

Dodatkowo, możliwe jest łączenie słów else oraz if, jeżeli jest więcej niż dwie możliwości
rozgałęzienia logiki kodu. Celem takiego zabiegu jest podniesienie wydajności w stosunku do
zapisania kilku niezależnych bloków z instrukcjami if. Konstrukcja else if zapewnia, że
sprawdzonych zostanie tylko tyle predykatów, ile jest niezbędnych do dotarcia do właściwego
rozgałęzienia.

public enum TimeOfDay { Morning, Noon, Afternoon, Evening, Night, Midnight }

public TimeOfDay GetTimeOfDay(DateTime date)


{
var hour = [Link];
var minutes = [Link];

TimeOfDay result;

if(hour >= 5 && hour < 12)


{
result = [Link];
}
else if(hour == 12 && minutes == 0)
{
result = [Link];
}
else if(hour >= 12 && hour < 17)
{
result = [Link];
}
else if(hour >= 17 && hour < 22)
{
result = [Link];
}
else if(hour == 0 && minutes == 0)
{
result = [Link];
}
else
{
result = [Link];
}

return result;
}

2.13.2. krótki if (ternary-if)

Dokumentacja - click.

Krótki if to konstrukcja, która pozwala na warunkowe przypisanie wartości w zależności od


prawdziwości danego predykatu. Pozwala to zastąpić w niektórych przypadkach konstrukcję if
else.

public void PrintResultsSummary(int[] results)


{
var message = [Link]()
? "No results found"
: $"{[Link]} results found";

[Link](message);
}

2.13.3. Switch

Dokumentacja - click.
Instrukcja warunkowa switch pozwala wybranie odpowiedniego przebiegu programu dla
różnych wartości tej samej zmiennej. Pozwala ona zastąpić i skrócić niektóre wykorzystania
konstrukcji else-if.

Konstrukcja rozpoczynana jest słowem kluczowych switch(), do którego w nawiasach


przekazujemy zmienną, której wartość będzie podlegała sprawdzeniu. Następnie dla każdej
wartości, na którą chcemy zareagować, tworzymy przypadek case i definiujemy w nim żądane
zachowanie. Każdy case wymaga zakończenia słowem kluczowym break lub return (w
zależności od potrzeb).

Aby obsłużyć przypadki, które nie są realizowane przez żaden z case’ów, należy na końcu
całej konstrukcji utworzyć sekcję default. Zostanie ona wykonana dla każdego przypadku, który
nie został wyłapany przez żaden a case’ów.

public enum ClientType { Baby, Child, Adult, Senior }

public double GetCinemaTickerDiscountFactor(ClientType clientType)


{
double discountFactor;

switch (clientType)
{
case [Link]:
discountFactor = 0.0d;
break;
case [Link]:
discountFactor = 0.5d;
break;
case [Link]:
discountFactor = 1.0d;
break;
case [Link]:
discountFactor = 0.5d;
break;
default:
discountFactor = 1.0d;
break;
}

return discountFactor;
}
2.14. Instrukcje skoku bezwarunkowego
Instrukcje te pozwalają na wykonanie zmiany w przebiegu programu bezwarunkowo.
Najczęściej wykorzystywane są w połączeniu z pętlami, niektóre jednak są wymagane przez
niektóre z konstrukcji, np.: switch wymaga użycia słów break lub return.

2.14.1. break

Dokumentacja - click.

Słowo kluczowe break przerywa pętlę lub instrukcję warunkową switch, w której jest użyte.
Jednym z typowych użyć break jest przerwanie pętli po uzyskaniu interesującego nas wyniku
bez konieczności wykonywania obrotów niezbędnych do spełnienia warunku końcowego.

public bool Contains(int[] collection, int elementToFind)


{
var contains = false;

foreach(var element in collection)


{
if(element == elementToFind)
{
contains = true;
break;
}
}

return contains;
}

2.14.2. continue

Dokumentacja - click.

Słowo kluczowe continue pozwala na przejście do kolejnego obrotu pętli bez konieczności
wykonania wszystkich zawartych w pętli instrukcji. Jeżeli w trakcie wykonywania pętli, że dalsze
przetwarzanie aktualnych danych jest niepotrzebne, możemy pominąć wszystkie następne kroki
i przejść do kolejnego obrotu pętli.

using System;
using [Link];
using [Link];
namespace ContinueExample
{
public class Shop
{
public string Name;
public List<double> InvoiceValues;
}

public class IncomeCalculator


{
public void PrintIncomes(List<Shop> shops)
{
foreach(var shop in shops)
{
if([Link]() == 0)
{
continue;
}

var incomeSum = [Link]();


[Link]($"{[Link]} income is {incomeSum}");
}
}
}
}

2.14.3. return

Dokumentacja - click.

Słowo kluczowe return kończy wykonanie funkcji.

W przypadku funkcji void, słowo return występuje samodzielnie. Nie jest ono również
niezbędne na końcu funkcji.

W przypadku funkcji zwracającej w wyniku działania jakąś wartość, po słowie return należy
podać zwracaną wartość. W tym wypadku słowo kluczowe return występuje w funkcji co
najmniej raz, najczęściej na samym jej końcu (choć zależy to od tego, jak funkcja jest
skonstruowana).

using [Link];
using [Link];

namespace ContinueExample
{
public class Shop
{
public string Name;
public List<double> InvoiceValues;
}

public class IncomeCalculator


{

public double GetTotalIncome(List<Shop> shops)


{
if (shops == null || [Link]() == 0)
{
return 0.0d;
}

var totalIncome = 0.0d;

foreach(var shop in shops)


{
if([Link]() == 0)
{
continue;
}

totalIncome += [Link]();
}

return totalIncome;
}
}
}

2.14.4. goto

Dokumentacja - click.

Słowo kluczowe goto pozwala na wykonanie skoku do etykiety zdefiniowanej w innym miejscu
w kodzie. Aby wykonać skok, należy wpisać słowo kluczowe goto i po nim podać nazwę etykiety,
do której chcemy się przenieść. Słowa goto można użyć w dwóch przypadkach:

● Wewnątrz metody - pozwala ono na przeniesienie się w dowolne inne miejsce w


metodzie.

public enum ClientType { Baby, Child, Adult, Senior }


public double GetCinemaTickerDiscountFactor(ClientType clientType)
{
double discountFactor;

switch (clientType)
{
case [Link]:
discountFactor = 0.0d;
break;
case [Link]:
goto case [Link];
case [Link]:
discountFactor = 1.0d;
break;
case [Link]:
discountFactor = 0.5d;
break;
default:
discountFactor = 1.0d;
break;
}

return discountFactor;
}

● Wewnątrz struktury instrukcji warunkowej switch - pozwala ono na przenoszenie się


między case’ami.

public bool ContainsAnyDuplicates(int[] tableA, int[] tableB)


{
foreach(var elementA in tableA)
{
foreach(var elementB in tableB)
{
if(elementA == elementB)
{
goto success;
}
}
}

return false;

success:
return true;
}

Używanie słowa kluczowego goto uważane jest za code smell i należy go unikać. Powodem
jest nieustrukturalizowana konstrukcja mechanizmu stojącego za goto. Swoboda
przeskakiwania między etykietami wprowadza możliwość łatwego wprowadzenia błędu w
postaci nieskończonych pętli, odwołania do niezainicjalizowanych zmiennych czy obiektów w
nieprawidłowym stanie. Wyłapanie błędów spowodowanych nieprawidłowym użyciem
mechanizmu goto poprzez statyczną analizę kodu (np.: podczas code review) może być trudne.
Sam mechanizm z kolei najczęściej znajduje zastosowanie w kodzie o nieprawidłowej
strukturze - potrzeba jego użycia najczęściej świadczy o złym designie i konieczności
przeprowadzenia refactoringu.

2.15. Typy wyliczeniowe (enum)


Typy wyliczeniowe, tzw. enumy, to typy pozwalające na stworzenie nazwanego zbioru
dozwolonych wartości. Przykładem zastosowania może być definicja typów biletów np.: w kinie.
Zdefiniowanie zbioru kilku typów biletów przy użyciu standardowych typów prostych byłby dość
karkołomny. Enum pozwala zdefiniować zbiór, w którym każdy jego element jest nazwany.
Enumy są wspierane przez Intellisense, dzięki czemu nie sposób w kodzie przypisać enumowi
nieprawidłową wartość.

Każdy zdefiniowany typ wyliczeniowy rozumiany jest przez kompilator jako odrębny typ
danych. Enum jest typem wartościowym.

public enum HttpCodes


{
Success,
SuccessNoContent,
NotFound,
InternalServerError
}

2.15.1. Przypisywanie wartości liczbowych

Wewnętrznie enum bazuje na typie int. Każdy element zbioru posiada przypisaną domyślną
wartość liczbową. Dla pierwszego elementu jest to 0, a każdy kolejny otrzymuje liczbę o jeden
większą niż poprzedni. Możliwe jest jednak przypisanie elementom zbiorów swoich własnych
wartości. Można przypisać wartość liczbową każdej wartości z osobna:

public enum HttpCodes


{
Success = 200,
SuccessNoContent = 204,
NotFound = 404,
InternalServerError = 500
}

, lub tylko elementom, których wartość powinna być inna, niż o jeden wyższa niż poprzednia:

public enum HttpCodes


{
Success = 200,
SuccessNoContent = 204,
BadRequest = 400,
Unauthorized, //400 + 1 = 401
PaymentRequired, //401 + 1 = 402
Forbidden, //402 + 1 = 403
NotFound, //403 + 1 = 404
InternalServerError = 500
}

2.15.2. Konwersja int ⇔ enum

Poniższy kod pokazuje, jak prawidłowo wykonać konwersję z enuma na liczbę i tekst oraz w
drugą stronę.

using System;

namespace EnumExample
{
public class HttpCodesConverter
{
public enum HttpCodes
{
Success,
SuccessNoContent,
NotFound,
InternalServerError
}

public string ConvertToText(HttpCodes httpCode)


{
return [Link]();
}
public int ConvertToInt(HttpCodes httpCode)
{
return (int)httpCode;
}

public HttpCodes ConvertFromText(string httpCodeText)


{
if (![Link](httpCodeText, out HttpCodes result))
{
throw new ArgumentException($"HttpCodes enum don't contain
value corresponding to '{httpCodeText}' text");
}

return result;
}

public HttpCodes ConvertFromNumber(int httpCodeInt)


{
try
{
return (HttpCodes)httpCodeInt;
}
catch(Exception e)
{
throw new ArgumentException($"HttpCodes enum don't contain
value corresponding to '{httpCodeInt}' number", e);
}
}
}
}

2.15.3. Iterowanie po możliwych wartościach

Możliwe jest również wygenerowanie kolekcji zawierającej wszystkie prawidłowe wartości


danego typu wyliczeniowego.

public HttpCodes[] GetAvailableValues()


{
return (HttpCodes[])[Link](typeof(HttpCodes));
}

3. Metody
Dokumentacja - click.
Metody (czy też funkcje - w nomenklaturze .NET jest to jedno i to samo) to nazwane
fragmenty kodu odpowiedzialne za wykonanie określonych operacji. Do metody można odwołać
się przy użyciu jej nazwy z innych miejsca w kodzie, dzięki czemu te same operacje nie muszą
być definiowanie kilkakrotnie Prawidłowy podział na metody ułatwia to utrzymanie i testowanie
kodu, zmniejsza ryzyko pomyłki i podnosi czytelność.

Metoda powinna posiadać pojedynczą odpowiedzialność a jej nazwa powinna jasno


wskazywać, jakiego zachowania spodziewamy się ją wywołując.

3.1. Konstrukcja
Metoda składa się z dwóch nadrzędnych elementów:

● Sygnatury - opisuje metodę,


● Ciała - zawiera logikę metody.

Sygnatura dodatkowo składa się z kolejnych elementów:

● Modyfikatora dostępu - opisuje zakres widoczności metody,


● Typu zwracanego - określa jakiego typu obiekt zostanie zwrócony w wyniku działania
metody (lub void, jeżeli nie zostanie zwrócone nic).
● Nazwy - ciąg znaków, po którym metoda może zostać zidentyfikowana w innym miejscu
w kodzie.
● Parametry wejściowe - opis danych niezbędnych funkcji do prawidłowego
przeprowadzenia swoich operacji.

//sygnatura
//mod. costepu: public
//typ zwracany: double
//nazwa: Divide
//parametry: int a, int b
public double Divide(int a, int b)
{
//cialo metody
var result = (double)a / (double)b;
return result;
}

Ciało metody dodatkowo może wykorzystać słowo kluczowe return. Powoduje ono wyjście z
metody i powrót do instrukcji, które dokonały jej wywołania. Każda ścieżka wykonania metody
musi zawierać definicje wyjścia z metody.

public bool IsNumberOdd(int number)


{
if(number % 2 == 1)
{
return true;
}
else
{
//return false; //dopóki nie odkomentujemy 'return false', otrzymamy
błąd kompilacji
}
}

3.2. Słowo kluczowe “params”


Dokumentacja - click.

Słowo kluczowe params pozwala na przyjęcie przez metodę zmiennej ilości argumentów
wybranego typu. W sygnaturze metody może występować co najwyżej jedno słowo kluczowe
param i musi ono być ostatnim argumentem metody.

namespace ParamsExample
{
public class Calculator
{
public void ParamsExample()
{
var sum = GetSum(-3, -1, 0, 2, 4); //2
}

private int GetSum(params int[] args)


{
var result = 0;

foreach(int arg in args)


{
result += arg;
}

return result;
}
}
}
3.3. Modyfikatory parametrów
Artykuł - click.

Modyfikatory parametrów (słowa kluczowe ref, in oraz out) pozwalają na referencyjne


przekazywanie argumentów typu wartościowego do i z metody.

3.3.1. Ref
Dokumentacja - click.

Słowo kluczowe ref pozwala na przekazanie przez referencje obiektu typu wartościowego.
Obiekt taki może być modyfikowany wewnątrz metody a zmodyfikowana wartość będzie
dostępna w metodzie nadrzędnej. Zmienna przekazywana do metody jako parametr ref musi
zostać wcześniej zainicjalizowana.

private void RefExample()


{
int number = 2;
ModifyNumber(ref number);
[Link](number); //3
}

private void ModifyNumber(ref int number)


{
number++;
}

3.3.2. In
Dokumentacja - click.

Słowo kluczowe in pozwala na przekazanie obiektu do metody zabraniając jego modyfikacji.


Oznacza to, że w przypadku struktur nie będzie możliwe zmodyfikowanie ich wartości ani
wartości żadnego z ich pól.

public struct Point


{
public int X;
public int Y;
}

private void InExample()


{
var a = new Point
{
X = 2,
Y = 4
};

ModifyPoint(in a);
}

private void ModifyPoint(in Point point)


{
point.X = 10; //<-- spowoduje błąd kompilacji
}
public struct Point
{
public int X;
public int Y;
}

private void InExample()


{
var a = new Point
{
X = 2,
Y = 4
};

ModifyPoint(in a);
}

private void ModifyPoint(in Point point)


{
point.X = 10; //<-- spowoduje błąd kompilacji
}

W przypadku zmiennych typu referencyjnego, nie możliwe będzie przypisanie zmiennej


oznaczonej słowem in referencji do innego obiektu, jednak modyfikacja wartości jego pół będzie
dozwolona.

3.4. Wartości domyślne parametrów


Możliwe jest przypisanie parametrom metody domyślnych wartości. Są one wykorzystywane,
gdy podczas wywołania metody nie zostanie danemu parametrowi przypisana wartość.
Domyślne wartości można przypisywać dowolnej liczbie parametrów, ale parametry z
wartościami domyślnymi muszą występować w nieprzerwanym ciągu i znajdować się na końcu
zbioru parametrów.

namespace DefaultParamValueExample
{
public class User
{
public int Name;
public int Surname;
public bool IsActive;
}

public class UsersService


{
private User[] _users;

public User[] GetUser(int count, bool includeInactive = false)


{
var usersFound = new User[count];
var usersAdded = 0;

foreach(var user in _users)


{
if([Link] || includeInactive)
{
usersFound[usersAdded] = user;
usersAdded++;
}

if(usersAdded >= count)


{
break;
}
}

return usersFound;
}
}
}

3.5. Non-trailing named arguments


Non-trailing named arguments pozwala na przekazywanie wartości do parametrów metody
bez zachowania kolejności ich definicji w sygnaturze metody. Dokonuje się tego poprzez
poprzedzenie wartości nazwą argumentu, do którego ma zostać przypisana.
namespace DefaultParamValueExample
{
public class User
{
public int Name;
public int Surname;
public bool IsActive;
}

public class UsersService


{
private User[] _users;

public void Example()


{
var usersIncludingInactive = GetUser(includeInactive: true,
count: 10);
}

public User[] GetUser(int count, bool includeInactive)


{
var usersFound = new User[count];
var usersAdded = 0;

foreach(var user in _users)


{
if(([Link] || includeInactive))
{
usersFound[usersAdded] = user;
usersAdded++;
}

if(usersAdded >= count)


{
break;
}
}

return usersFound;
}
}
}
Non-trailing named arguments w połączeniu z domyślnymi wartościami pozwala na
uniknięcie problemu ustalania kolejności wartości domyślnych tak, aby ta zmieniana najrzadziej
znajdowała się na końcu.

namespace DefaultParamValueExample
{
public class User
{
public int Name;
public int Surname;
public bool IsActive;
public bool IsAdmin;
}

public class UsersService


{
private User[] _users;

public void Example()


{
var usersIncludingInactive = GetUser(10, includeAdmins: true)
}

public User[] GetUser(int count, bool includeInactive = false, bool


includeAdmins = false)
{
var usersFound = new User[count];
var usersAdded = 0;

foreach(var user in _users)


{
if(([Link] || includeInactive)
&& (![Link] || includeAdmins))
{
usersFound[usersAdded] = user;
usersAdded++;
}

if(usersAdded >= count)


{
break;
}
}

return usersFound;
}
}
}

4. Tworzenie bibliotek
Biblioteka to zbiór obiektów i zawartej w nich logiki w formie osobnego, nie uruchamialnego
binarium, którego celem jest współdzielenie kodu na potrzeby wykorzystania w innych
projektach.

Biblioteki wykorzystywane są również w celu oddzielania od siebie różnych warstw i


komponentów aplikacji. Ułatwia to skuteczne przeciwdziałanie mieszaniu elementów systemu,
które nie powinny być od siebie uzależnione (jak np.: warstwa dostępu do bazy danych i
warstwa prezentacyjna aplikacji powinny był rozdzielone). Uniezależnienie ich od siebie daje
możliwość łatwiejszej rozbudowy aplikacji i jej adaptacji do zmian w przyszłości.

4.1. Referencjonowanie projektów w Visual Studio


Aby powiązać projekt wykonywalny (np.: aplikację konsolową) z biblioteką realizowaną w
ramach solucji, należy kliknąć prawym przyciskiem na Dependencies w projekcie, do którego
chcemy dodać referencje i wybrać Add references… .

Następnie należy wybrać zakładkę Projects -> Solution. Tam znajduje się lista wszystkich
projektów, które można zarerefencjonować. Po zaznaczeniu wybranych, należy kliknąć ok.
Aby sprawdzić referencje projektu, należy w Solution Explorer rozwinąć gałąź Delendencies.

4.2. Czym jest nuget?


Nuget to paczka zawierająca zestaw bibliotek i narzędzi powstała w celu ich redystrybucji na
potrzeby współdzielenia ich funkcjonalności w wielu projektach. Nuget posiadają informacje
[Link]. o ich wersji czy kompatybilności z konkretnymi wersjami technologii wchodzących w skład
platformy .NET.

Biblioteki i narzędzia udostępnione w postaci paczek nuget można łatwo pobrać z


repozytorium paczek.
4.3. Repozytorium paczek nuget
Repozytorium paczek nuget to serwer zawierający zbiór paczek udostępnianych
developerom rozwijającym róznego rodzaju aplikację w .NET. Repozytoria można przeglądać z
poziomu programu Visual Studio i dodawać paczki w nich zawarte jako zależności tworzonych
projektów. Ich dostępność na serwerach zewnętrznych pozwala na uniknięcie przechowywania
bibliotek zewnętrznych w repozytorium razem z kodem aplikacji, co znacząco zmniejsza jego
rozmiar. Ułatwiona jest również kwestia szukania bibliotek, ich aktualizacja, weryfikacja
warunków licencyjnych, dostęp do strony projektu i kodu źródłowego itd itd.

Z poziomu programu Visual Studio repozytorium paczek dla wybranego projektu można
przeszukiwać poprzez kliknięcie prawym przyciskiem myszy na projekt w Solution Explorer i
wybranie opcji Manage Nuget Packages… .

Po wybraniu tej opcji pojawi się widok managera paczek nuget. Zawiera on trzy zakładki:

● Browse - pozwala na przeszukiwanie repozytoriów uwzględnionych w konfiguracji


repozytoriów (1),
● Installed - pozwala na przeszukiwanie paczek zainstalowanych w danym projekcie (2),
● Updated - posiada informacje o wszystkich paczkach nuget, które posiadają wersję
nowszą od zainstalowanej i pozwala na ich instalację (3).
Każda z zakładek posiada pole wyszukiwania, które pozwala na efektywne przeszukiwanie
paczek w danej zakładce (4). Po wybraniu pozycji na liście, w panelu po prawej stronie (5),
znaleźć można informację o paczce, jej zależnościach, licencji itd.

Konfiguracja repozytoriów możliwa jest po naciśnięciu przycisku Settings (6). Domyślne


Visual Studio posiada skonfigurowany dostęp do publicznego repozytorium firmy Microsoft -
[Link]. Dodatkowo, repozytorium [Link] można przeglądać z wykorzystaniem strony
internetowej - click.

W przypadku dużych firm informatycznych, często posiadają one własne serwery paczek
nuget z bibliotekami wykorzystywanymi na wewnętrzne potrzeby firmy. Takie repozytoria można
dodać do konfiguracji Visual Studio właśnie w tym oknie.

5. Wyjątki
Wyjątki to obiekty reprezentujące zdarzenia nieprawidłowego zachowania aplikacji lub
wystąpienia scenariusza, w którym logika aplikacji nie stwierdza jednoznacznie jak rozwiązać
dany problem. Przykładem może być np.: odniesienie się do nieistniejącej pozycji w tablicy.

Wyjątki, jeżeli nie zostaną obsłużone przez logikę aplikacji, powodują zamknięcie aplikacji z
błędem.
5.1. Obsługa wyjątków
W celu obsłużenia wyjątku wykorzystuje się konstrukcję try-catch-finally. Jak nazwa
wskazuje, składa się ona z 3 sekcji. Nie wszystkie są wymagane:

● try (obowiązkowa) - zawiera kod, który spodziewamy się, że może wyrzucić wyjątek,
● catch (opcjonalna, jeżeli występuje finally) - zawiera kod, który zostanie wykonany w
momencie, gdy kod z sekcji try wyrzuci wyjątek,
● finally (opcjonalna, jeżeli występuje co najmniej jeden catch) - zawiera kod, który zostanie
wykonany zawsze, niezależnie

public string ReadFileChunk(string filePath, int startPosition, int


charsCount)
{
var streamReader = new StreamReader(filePath);
var buffer = new char[charsCount];

try
{
[Link](buffer, startPosition, [Link]);
}
catch (IOException e)
{
[Link]($"An error occurred when reading file {filePath}:
{[Link]}");
return null;
}
finally
{
if (streamReader != null)
{
[Link]();
}
}

return new string(buffer);


}

W konstrukcji try-catch-finally może występować więcej niż jedna sekcja catch. Każda z nich
musi odnosić się do innego typu wyjątków. Dana sekcja catch obsłuży wyjątek, jeżeli jego typ
jest dokładnie taki sam jak podany w nawiasach lub po nim dziedziczy. W przypadku
wystąpienia wyjątki sekcje catch są dopasowywane w kolejności, w jakiej zostały
zadeklarowane. Jeżeli żadna z sekcji nie obsługuje wyjątku, ten zostanie wysłany dalej w stosie
wywołań (CallStack), aż natrafi na catch go obsługujący lub spowoduje błąd aplikacji.
W .NET każdy wyjątek dziedziczy po klasie Exception. Oznacza to, że catch(Exception e) to
sekcja, która obsłuży dowolny wyjątek rzucony przez kod w sekcji try.

public string ReadFileChunk(string filePath, int startPosition, int


charsCount)
{
var streamReader = new StreamReader(filePath);
var buffer = new char[charsCount];

try
{
[Link](buffer, startPosition, [Link]);
}
catch (IOException e)
{
[Link]($"An IO error occured when reading file
{filePath}: {[Link]}");
return null;
}
catch(Exception e)
{
[Link]($"Some error occured when reading file chunk:
{[Link]}");
return null;
}
finally
{
if (streamReader != null)
{
[Link]();
}
}

return new string(buffer);


}

5.2. Samodzielne wyrzucenie wyjątku


W celu samodzielnego stworzenia i rzucenia wyjątku wykorzystuje się słowo kluczowe throw.
Zaraz po nim powinno pojawić się odwołanie do obiektu, którego typ dziedziczy po klasie
Exception.

Dodatkowo, jeżeli sekcja catch ma na celu jedynie np.: zakomunikowanie błędu, nie jego
pełną obsługę, można wyjątek złapany przez tę sekcję wyrzucić ponownie skróconym zapisem
throw; .
public string ReadFileChunk(string filePath, int startPosition, int
charsCount)
{
var streamReader = new StreamReader(filePath);
var buffer = new char[charsCount];

if(![Link](filePath))
{
throw new ArgumentException($"File {filePath} don't exists! Data
cannot be read.");
}

try
{
[Link](buffer, startPosition, [Link]);
}
catch (Exception e)
{
[Link]($"An IO error occured when reading file
{filePath}: {[Link]}");
throw;
}
finally
{
if (streamReader != null)
{
[Link]();
}
}

return new string(buffer);


}

5.3. Tworzenie własnych wyjątków


W celu napisania własnego wyjątku, wystarczy stworzyć własną klasę dziedziczącą po klasie
Exception. Każdy obiekt klasy dziedziczącej po Exception może być użyty po słowie throw.

using System;

namespace CustomExceptionExamples
{
public struct GeoCoords
{
public double Longitude;
public double Latitude;
}

public class Wgs84CoordinatesException : Exception


{
public GeoCoords GeoCoords { get; }

public Wgs84CoordinatesException(
string message,
GeoCoords geoCoords)
: base(message)
{
GeoCoords = geoCoords;
}
}

public class LocalizationService


{
private const int LatitudeMax = 90;
private const int LongitudeMax = 180;

public GeoCoords GetWgs84GeoCoordinatesFromUser()


{
[Link]("Enter latitude: ");
var latitude = [Link]([Link]());

[Link]("Enter longitude: ");


var longitude = [Link]([Link]());

var geoCoords = new GeoCoords


{
Latitude = latitude,
Longitude = longitude
};

if([Link](latitude) > LatitudeMax)


{
throw new Wgs84CoordinatesException(
$"WGS84 GeoCoords latitude must be in range
<{-LatitudeMax}.{LatitudeMax}>",
geoCoords);
}

if ([Link](longitude) > LongitudeMax)


{
throw new Wgs84CoordinatesException(
$"WGS84 GeoCoords longitude must be in range
<{-LongitudeMax}.{LongitudeMax}>",
geoCoords);
}

return geoCoords;
}
}
}

6. Niezarządzalne zasoby danych


Niezarządzalne zasoby danych to źródła danych będące poza kontrolą zarządzania pamięcią
programu przez platformę .NET. W ich skład wchodzą [Link]. pliki, bazy danych czy pamięć.

6.1. Strumienie
Dokumentacja (.NET Framework) - click.
Dokumentacja (.NET Core) - click.
Artykuł - click i click.

Jednym ze sposobów dostępu do niezarządzalnych zasobów są strumienie. Strumienie


pozwalają na traktowanie zasobu danych jako ciągu bajtów do przeczytania. Przykładem
strumienia może być strumień odczytu danych z pliku tekstowego.

using System;
using [Link];
using [Link];

namespace FileStreamExample
{
public class FileService
{
public void Example()
{
string filePath = @".\[Link]";

if([Link](filePath))
{
[Link](filePath);
}

CreateFile(filePath);
PrintFileContent(filePath);
}

private void PrintFileContent(string filePath)


{
var fs = [Link](filePath);

var buffer = new byte[1024];


var utf8Encoder = new UTF8Encoding(true);

while ([Link](buffer, 0, [Link]) > 0)


{
[Link]([Link](buffer));
}

[Link]();
}

private void CreateFile(string filePath)


{
var fs = [Link](filePath);

AddText(fs, "This is some text");


AddText(fs, "This is some more text,");
AddText(fs, "\r\nand this is on a new line");

for (int i = 1; i < 10; i++)


{
AddText(fs, $"{i}\r\n");
}

[Link]();
}

private void AddText(FileStream fs, string value)


{
byte[] info = new UTF8Encoding(true).GetBytes(value);
[Link](info, 0, [Link]);
}
}
}

6.2. Interface IDisposable i słowo kluczowe using


Ponieważ zasoby niezarządzalne nie mogą zostać zwolnione automatycznie przez platformę
.NET, muszą one zostać zwolnione ręcznie. W tym celu powstał interface IDisposable. Narzuca
on na klasie konieczność implementacji metody Dispose (widoczna w poprzednim przykładzie).
Metoda dispose jest odpowiedzialna za zwolnienie zasobu, w przypadku pliku FileStream
wewnętrznie zwalnia uchwyt do pliku, który przechowuje.

6.3. Słowo kluczowe “using”


Podczas pracy z niezarzączalnymi zasobami danych częstym błędem w programach było
nieprawidłowe zwalnianie zasobu czy też niepotrzebne przetrzymywanie otwartego uchwytu do
niego.

W celu ułatwienia zwalniania niezarządzalnych zasobów powstało słowo kluczowe using.


Słowo kluczowe using pozwala na zainicjalizowanie zmiennej typu implementującego interface
IDisposable, która widoczna jest jedynie wewnątrz bloku kodu poniżej jego wystąpienia. Na
końcu bloku zasób (poprzez niejawne wywołanie metody Dispose) zwalniany jest
auotmatycznie.

using System;
using [Link];
using [Link];

namespace FileStreamExample
{
public class FileService
{
public void Example()
{
string filePath = @".\[Link]";

if([Link](filePath))
{
[Link](filePath);
}

CreateFile(filePath);
PrintFileContent(filePath);
}

private void PrintFileContent(string filePath)


{
using (FileStream fs = [Link](filePath))
{
var buffer = new byte[1024];
var utf8Encoder = new UTF8Encoding(true);

while ([Link](buffer, 0, [Link]) > 0)


{
[Link]([Link](buffer));
}
}
}

private void CreateFile(string filePath)


{
using (FileStream fs = [Link](filePath))
{
AddText(fs, "This is some text");
AddText(fs, "This is some more text,");
AddText(fs, "\r\nand this is on a new line");

for (int i = 1; i < 10; i++)


{
AddText(fs, $"{i}\r\n");
}
}
}

private void AddText(FileStream fs, string value)


{
byte[] info = new UTF8Encoding(true).GetBytes(value);
[Link](info, 0, [Link]);
}
}
}

Używanie konstrukcji ze słowem kluczowym using pozwala na łatwe zwalnianie zasobów i


blokowanie ich tylko w czasie, w którym są wykorzystywane. Poza wyjątkowymi sytuacjami
ręcznego zwalniania zasobów niezarządzalnych nie stosuje się.

7. Programowanie obiektowe
Programowanie obiektowe to jeden z paradygmatów programowania. Stanowi on o
modelowaniu systemu w oparciu o tworzenie klas obiektów i interakcji między nimi w celu
rozwiązania zadanego problemu.
7.1. Klasy i struktury
Klasy i struktury w .NET służą do definiowania opisu obiektów. Każdy obiekt danej klasy lub
typu struktury będzie posiadał możliwość przypisania mo określonych wartości i wykonywaniu z
jego udziałem określonych zachowań.

Z punktu widzenia definicji klasa i struktura wyglądają identycznie - różnią się jedynie
zastosowanym przed ich nazwą słowem kluczowym. Dla klasy jest to słowo class, a dla
struktury - struct.

namespace OopExamples
{
public class Car
{
public string Make;
public string Model;
public string LicensePlates;
public int TraveledRange;
}

public class CarsRepository


{
public Car[] GetExemplaryCars()
{
var cars = new Car[2];

var subaru = new Car


{
Make = "Subaru",
Model = "Impreza",
LicensePlates = "GD123CW",
TraveledRange = 213424
};

var honda = new Car


{
Make = "Honda",
Model = "Civic",
LicensePlates = "GD645JS",
TraveledRange = 153564
};

cars[0] = subaru;
cars[1] = honda;
return cars;
}
}
}

7.2. Typy referencyjne i wartościowe


Podstawowe dwa typy obiektów z .NET to typy wartościowe i referencyjne. Różnica między
nimi to sposób ich przechowywania w pamięci.

W przypadku typu referencyjnego zmienna, która się do nich odnosi stanowi wskaźnik do
danych w pamięci. Oznacza to, że przekazywanie tej zmiennej do różnych metod i pół obiektów
różnych klas to przekazanie wskaźnika na dokładnie ten sam obiekt na stercie. Klasy to obiekty
typu referencyjnego. Wartość domyślna obiektów typu referencyjnego jest null.

using System;

namespace OopExamples
{
public class Car
{
public string Make;
public string Model;
public string LicensePlates;
public int TraveledRange;
public bool IsRented;
}

public class Program


{
public static void Main()
{
new Program().Run();
}

private void Run()


{
var subaru = new Car
{
Make = "Subaru",
Model = "Impreza",
LicensePlates = "GD123CW",
TraveledRange = 213424,
IsRented = false
};

RentACar(subaru);

[Link]([Link]); //true
}

private void RentACar(Car car)


{
[Link] = true;
}
}
}

W przypadku obiektu typu wartościowego, każde jego przypisanie do zmiennej czy


przekazanie jako parametr metody spowoduje zduplikowanie całego obiektu. Oznacza to, że
każda metoda czy klasa otrzyma swój własny, niezależny obiekt na którym będzie mogła
pracować. Struktury to obiekty typu wartościowego.

Wartością domyślną obiektów typu wartościowego jest 0 lub jego reprezentacja dla danego
typu. W przypadku złożonej struktury, jest to obiekt którego wszystkiego pola mają przypisane
wartości domyślne.

using System;

namespace OopExamples
{
public struct Car
{
public string Make;
public string Model;
public string LicensePlates;
public int TraveledRange;
public bool IsRented;
}

public class Program


{
public static void Main()
{
new Program().Run();
}

private void Run()


{
var subaru = new Car
{
Make = "Subaru",
Model = "Impreza",
LicensePlates = "GD123CW",
TraveledRange = 213424,
IsRented = false
};

RentACar(subaru);

[Link]([Link]); //false
}

private void RentACar(Car car)


{
[Link] = true;
}
}
}

Każda definicja klasy lub struktury to defacto definicja nowego typu, którego można użyć
pisząc system.

7.3. Konstruktor i destruktor


Każdy typ w .NET posiada swój destruktor lub konstruktor. Jeżeli te nie zostaną jawnie
zdefiniowane, automatycznie wykorzystywane są tzw. konstruktory i destruktory domyślne.
Charakteryzują się one brakiem parametrów i logiki.

Konstruktor w .NET wywoływany jest po zainicjalizowaniu obiektu w pamięci oraz wszystkich


jego pół, których inicjalizacja została zaimplementowana na poziomie definicji pola.

Destruktor wywoływany jest zanim Garbage Collector zwolni miejsce w pamięci, które
aktualnie zajmuje dany obiekt.

using System;

namespace OopExamples
{
public class Car
{
public int Id;
public string Make;
public string Model;
public string LicensePlates;
public int TraveledRange;
public bool IsRented;
}

public class Program


{
public static void Main()
{
new Program().Run();
}

private void Run()


{
var carsRepo = new CarsRepository(true);

var car = [Link](1);

[Link]($"{[Link]} {[Link]}"); //Subaru Impreza


}
}

public class CarsRepository


{
//Inicjalizacja pola zostanie wykonana przed wywołaniem
konstruktora.
//Zostanie ona wykonana w momencie rezerwacji pamięci dla obiektu
tej klasy.
private Car[] _cars = new Car[100];

//Konstruktor
public CarsRepository(bool addExemplatoryCar)
{
if(!addExemplatoryCar)
{
var subaru = new Car
{
Id = 1,
Make = "Subaru",
Model = "Impreza",
LicensePlates = "GD123CW",
TraveledRange = 213424,
IsRented = false
};

_cars[0] = subaru;
}
[Link]("CarsRepository object initialized");
}

//Destruktor
~CarsRepository()
{
[Link]("CarsRepository object deinitialized");
}

public Car GetCar(int id)


{
foreach(var car in _cars)
{
if([Link] == id)
{
return car;
}
}

return null;
}
}
}

7.4. Pola (fields)


Pole reprezentuje daną, która można przechowywać w obiekcie danej klasy. Klasa Car
składała się tylko z pół, które pozwalają na opisanie pojazdu, który reprezentował konkretny
obiekt tej klasy.

public class Car


{
public int Id;
public string Make;
public string Model;
public string LicensePlates;
public int TraveledRange;
public bool IsRented;
}
7.5. Anemiczny i bogaty model domenowy
Modelem danych nazywamy klasy, które reprezentują dane na których operuje program. W
przypadku pisania aplikacji np.: traktującej o wynajmie samochodów, modelem danych będzie
[Link]. samochód, ale i dane klientów, odbyte przejazdy, faktury, rachunki itd.

Zgodnie z paradygmatem programowania obiektowego, klasy reprezentują pewne elementy


systemu, które mogą zawierać dane oraz logikę pozwalającą na nich operować. Z tej definicji
powstały dwa popularne modele struktury logiki wewnątrz aplikacji - Anemiczny Model
Domenowy (Anemic Domain Model) oraz Bogaty Model Domenowy (Rich Domain Model).

7.5.1. Bogaty Model Domenowy

Model bogaty charakteryzuje się tworzeniem klas obiektów jednocześnie zawierających


model danych, jak i definicje logiki operacji na tych danych i interakcji z innymi obiektami. Poniżej
reprezentacja funkcjonalności wynajmu pojazdu.

using System;

namespace OopExamples
{
public class User
{
public int Id;
public string Name;
public string Surname;
public Car RentedCar;
}

public class Car


{
public int Id;
public string Make;
public string Model;
public string LicensePlates;
public int TraveledRange;
private bool _isRented;
private User _rentingUser;

public bool Rent(User user)


{
if(_isRented)
{
[Link]($"Car {Id} already rented!");
return false;
}
if (user == null)
{
[Link]($"Cannot rent to undefined user");
return false;
}

[Link] = this;
_rentingUser = user;
_isRented = true;

return true;
}
}

public class Program


{
public static void Main()
{
new Program().Run();
}

private void Run()


{
var carsRepo = new CarsRepository(true);
var usersRepo = new UsersRepository(true);

var car = [Link](1);


var user = [Link](1);

[Link](user);
}
}
}

Podstawowymi zaletami modelu bogatego jest zdefiniowane logiki i danych w jednym


miejscu, co ułatwia przeszukiwanie kodu oraz umożliwia hermetyzację danych, udostępniając ją
jedynie logice wewnętrznej obiektu danej klasy.

Do wad należy niejasna odpowiedzialność klas (powinniśmy napisać metodę [Link](User)


czy może [Link](Car)?), powstawanie klas o bardzo dużej złożoności oraz, w przypadku
dużych projektów, powstawanie kodu, który wiąże się ze sobą w nieintuicyjny sposób. Powoduje
to łatwe wprowadzanie błędów w oprogramowaniu oraz podnosi próg wejścia dla nowych osób
w projekcie.
Dodatkowo, model bogaty im jest bardziej rozbudowany, tym jest mniej elastyczny na zmiany.

7.5.2. Anemiczny Model Domenowy

W modelu anemicznym odpowiedzialność za dane programu oraz jego logikę rozdziela się.
Typy odpowiedzialne za przechowywanie danych posiadają jedynie pola lub własności
pozwalające na przechowywanie danych na których operuje program. Same operacje wykonują
arbitralne obiekty, często bezstanowe, zwane zwyczajowo serwisami.

using System;

namespace OopExamples
{
public class User
{
public int Id;
public string Name;
public string Surname;
public Car RentedCar;
}

public class Car


{
public int Id;
public string Make;
public string Model;
public string LicensePlates;
public int TraveledRange;
public bool IsRented;
public User RentingUser;
}

public class Program


{
public static void Main()
{
new Program().Run();
}

private void Run()


{
var carsRepo = new CarsRepository(true);
var usersRepo = new UsersRepository(true);
var rentalService = new RentalService();

var car = [Link](1);


var user = [Link](1);

[Link](user, car);
}
}

public class RentalService


{
public bool Rent(User user, Car car)
{
if(user == null || car == null)
{
[Link]("User and car needs to be defined in order
to rent");
return false;
}

if([Link])
{
[Link]($"Car is already rented");
return false;
}

[Link] = car;
[Link] = user;
[Link] = true;

return true;
}
}
}

Do zalet modelu anemicznego należy zaliczyć łatwą możliwość tworzenia małych,


wyspecjalizowanych typów, mniej zawiły kod oraz mocno ograniczoną stanowość logiki aplikacji
- ułatwia ona testowanie i zrozumienie kodu.

Błędy wprowadzanie w modelu anemicznym często są dużo mniej skomplikowane. Wynika


to z faktu mniej złożonej zależności między obiektami różnych klas.

Wadą jest rozproszenie logiki w wielu klasach, których w przypadku dużych systemów może
być wiele.
7.6. Własności (properties)
W celu hermetyzacji danych w .NET w okresie szerokiego wykorzystywania Bogatego
Modelu Domenowego, programiści często sprowadzali w kodzie metody Get i Set. Ich celem
było zareagowanie na próbę ustawienia pewnych pół dodatkową logiką:

public class Car


{
public int Id;
public string Make;
public string Model;
public string LicensePlates;
public int TraveledRange;
private bool _isRented;
private User _rentingUser;

public void SetUser(User user)


{
_isRented = user == null;
_rentingUser = user;
}

public bool GetIsRented()


{
return _isRented;
}

public User GetRentingUser()


{
return _rentingUser;
}
}

W celu uproszczenia zapisu takich zachowań wprowadzono własności. Własność to


element obiektu widoczny jak pole, który tak naprawde jest definicją dwóch metod - get i set.

Metoda get wywoływana jest, gdy odwołanie do własności znajduje się po prawej stronie
znaku przypisania. Metoda set, gdy odwołanie do własności znajduje się po lewej strone znaku
przypisania. Wartość wpisywana do własności w przypadku wywołania metody set dostępna
jest pod słowem kluczowym value.

Dodatkowo, metody get i set mogą posiadać różne modyfikatory dostępu. Domyślne ich
widoczność jest taka, jak widoczność własności. Można im jednak ustawić modyfikator
indywidualnie - musi być on tak samo lub bardziej restrykcyjny niż modyfikator własności.
public class Car
{
public int Id;
public string Make;
public string Model;
public string LicensePlates;
public int TraveledRange;

private bool _isRentedBackingField;


public bool IsRented
{
get
{
return _isRentedBackingField;
}
private set
{
_isRentedBackingField = value;
}
}

private User _rentingUserBackingField;


public User RentingUser
{
get { return _rentingUserBackingField; }
set
{
IsRented = value == null;
_rentingUserBackingField = value ;
}
}
}

W momencie wzrostu popularności Anemicznego Modelu Domenowego, w wielu systemach


zaczęto rezygnować z rozbudowanych metod get i set. Jednak w oczym czasie powstało wiele
bibliotek, które podczas szukania danych wykorzystują właśnie własności (np.: serializatory, jak
[Link] czy ORM’y jak EntityFramework).

W celu uproszczenia zapisu wprowadzono składnię AutoProperties - pozwala ona na


skrócenie własności odnoszących się do jednego konkretnego pola bez specjalnej dodatkowej
logiki do jednej linijki.

public class Car


{
public int Id;
public string Make;
public string Model;
public string LicensePlates;
public int TraveledRange;

public bool IsRented { get; private set; }

private User _rentingUserBackingField;


public User RentingUser
{
get { return _rentingUserBackingField; }
set
{
IsRented = value == null;
_rentingUserBackingField = value ;
}
}
}

Ze względu na implementację serializatorów i ORM’ów wszystkie dane, które mają zostać


przez nie uwzględnione muszą być własnościami. Stąd modele danych często przybierają
następującą postać:

public class User


{
public int Id { get; set; }
public string Name { get; set; }
public string Surname { get; set; }
public Car RentedCar { get; set; }
}

7.7. Null propagator


Null propagator to operator pozwalający na przeszukiwanie pól obiektu w sposób chroniący
przed wyjątkiem NullReferenceException. Propagator przechodzi do kolejnych wartości tak
długo aż:

● natrafi na null, wtedy zwrócona wartość całego wyrażenia to null,


● dotrze do ostatniego elementu, wtedy to jego wartość zostanie zwrócona.

Null propagator pozwala zauważalnie zmniejszyć ilość wystąpień instrukcji warunkowych


sprawdzających, czy dany obiekt jest lub nie jest null’em. Null propagatora używa się poprzez
wstawienie znaku zapytania przed odwołaniem się do pola w obiekcie typu referencyjnego.
public class Program
{
public static void Main()
{
new Program().Run();
}

private void Run()


{
var car1 = (Car)null;

var car2 = new Car


{
RentingUser = null
};

var car3 = new Car


{
RentingUser = new User
{
Surname = null
}
};

var car4 = new Car


{
RentingUser = new User
{
Surname = "Nowak"
}
};

PrintRentersSurname(car1); //Renters surname: unknown


PrintRentersSurname(car2); //Renters surname: unknown
PrintRentersSurname(car3); //Renters surname: unknown
PrintRentersSurname(car4); //Renters surname: Nowak
}

private void PrintRentersSurname(Car car)


{
var surname = car?.RentingUser?.Surname;

var surnameToPrint = surname == null


? "unknown"
: surname;

[Link]($"Renters surname: {surnameToPrint}");


}
}

7.8. Operator '??'


Operator '??' to skrócony zapis instrukcji warunkowej ternary-if z warunkiem sprawdzającym,
czy dany obiekt jest null’em.

Operator '??' zwróci wartość wskazaną po lewej stronie wyrażenia, o ile ta nie jest null. Jeżeli
jest, zwrócona zostanie wartość po prawej stronie wyrażenia.

private void PrintRentersSurname(Car car)


{
var surname = car?.RentingUser?.Surname;

//var surnameToPrint = surname == null


// ? "unknown"
// : surname;

var surnameToPrint = surname ?? "unknown";

[Link]($"Renters surname: {surnameToPrint}");


}

7.9. Typy i metody generyczne


Dokumentacja - click.

Typy generyczne to typy, które pozwalają na operowanie na typach, które nie są znane do
momentu ich deklaracji. Poniżej bardzo prosty przykład reprezentujący mocno podstawowy
przykład generycznego repozytorium.

Możliwe jest również definiowanie metod generycznych, które przyjmują typ, na którym ma
się wykonywać operacja.

Możliwe jest ograniczanie typów za pomocą słowa kluczowego where. Możliwości słowa
where pozwalają na ograniczenie typu danych przekazywanego do klasy lub metody
generycznej do typów wartościowych (where T : struct), referencyjnych (where T : class) oraz
do obiektów posiadających wybrany typ w swoim drzewie dziedziczenia.

using System;

namespace OopExamples
{
public class Entity
{
public int Id;
}

public class User : Entity


{
public string Name;
public string Surname;
public Car RentedCar;
}

public class Car : Entity


{
public string Make;
public string Model;
public string LicensePlates;
public int TraveledRange;
public bool IsRented;
public User RentingUser;
}

public class Program


{
public static void Main()
{
new Program().Run();
}

private void Run()


{
var janush = new User
{
Id = 1,
Name = "Jan",
Surname = "Ush"
};

var subaru = new Car


{
Id = 1,
Make = "Subaru",
Model = "Impreza"
};

var carsRepo = new EntityRepository<Car>();


var usersRepo = new EntityRepository<User>();

[Link](subaru);
[Link](janush);

PrintId<Car>([Link](1));
PrintId<User>([Link](1));
}

private void PrintId<T>(T data) where T : Entity


{
[Link]($"Entity id is {[Link]}");
}
}

public class EntityRepository<T> where T : Entity


{
private int firstFreeIndex = 0;
private T[] _data = new T[100];

public void Add(T data)


{
_data[firstFreeIndex] = data;
firstFreeIndex++;
}

public T GetById(int id)


{
foreach (var data in _data)
{
if ([Link] == id)
{
return data;
}
}

return null;
}
}
}

7.10. Klasa Nullable


Klasa Nullable pozwala na obudowanie dowolnej struktury w klasę, dzięki czemu będzie
zachowywała się jak typ referencyjny i możliwe będzie jej przypisanie wartości null.
Typowym przykładem wykorzystania klasy Nullable jest choćby integracja aplikacji z bazą
danych. Wiele typów, które w .NET są typami wartościowymi w bazach danych mogą
przyjmować wartości null, jak np.: wartości liczbowe w MSSQL.

Aby utworzyć obiekt klasy nullable można zadeklarować go jawnie poprzez Nullable<T>,
gdzie T to nazwa typu wartościowego, który chcemy w niej zawrzeć, lub poprzez zapis
skrócony T? Podczas deklaracji typu.

Aby sprawdzić, czy dany obiekt posiada wartość, można wykorzystać pole HasValue.

int? nullableInt1 = 1;
Nullable<int> nullableInt2 = null;

[Link]([Link]); //true
[Link]([Link]); //false

8. Konwencje nazewnicze
Artykuł - click.

Kod piszemy w pełni po angielsku. Nie stosujemy notacji węgierskiej i unikamy skrótów
(chyba, że są to skróty powszechnie znane w danej dziedzinie problemowej, jak HTTP, GPS,
PESEL itd).

//Nazwa firmy - [Link] (skróty 2-znakowe piszemy dużymi, 3 i więcej -


PascalCase
//Nazwa departamentu - [Link]
//Nazwa rozwiązywanego problemu - CarSharing
//Nazwa binarium - BusinessLogic
//<nazwa_firmy>.<nazwa_departamentu>.<nazwa_rozwiązywanego_problemu>.<nazwa_
binarium>
namespace [Link]
{
//Interface’y PascalCase poprzedzony duzym i
public interface ISomeInterface
{
void SomeMethod();
}

//Nazwa klasy PascalCase


public class NamingConvetionsExample
{
//Nazwy typów wyliczeniowych i ich wartości - PascalCase
public enum SomeEnum
{
SomeEnumValue
}

//pola i własności prywatne - podkreślenie i camelCase (rowniez


readonly).
private int _someVariable;
private int _someProperty { get; set; }

//pola i własności publiczne - PascalCase (rowniez readonly)


public int SomeVariable;
public int SomeProperty { get; set; }

//nazwy metod (niezależnie od mod. dostępu - PascalCase)


public void SomeMethod(int param1, int param2)
{
//nazwy argumentów i zmiennych lokalnych (również readonly i
const) - camelCase
const int localConst = 1;
int localVar1;
}
}
}

9. Paradygmaty programowania obiektowego:


Paradygmaty programowania obiektowego definiują cechy, które musi posiadać język
programowania, aby można go uznać za obiektowy.

9.1. Polimorfizm
O polimorfizmie mówimy, gdy dwie metody o jednakowej nazwie, ale należące do obiektów
różnych typów posiadają własną logikę.

using System;
using [Link];

public class FileDataAccess


{
public void Save(string fileName, string data)
{
[Link](fileName, data);
[Link]($"File {fileName} save successfully");
}
}
public class FtpDataAccess
{
public void Save(string fileName, string data)
{
var ftpPath = [Link]("[Link]
fileName;
//Some FTP connection and saving data to file logic to put here
[Link]($"Data saved to file {ftpPath} on FTP server");
}
}

public class Program


{
public static void Main()
{
new Program().Run();
}

private void Run()


{
var fileName = "[Link]";
var myData = "my data oto be saved";

var fileDataAccess = new FileDataAccess();


var ftpDataAccess = new FtpDataAccess();

[Link](fileName, myData);
[Link](fileName, myData);
}
}

9.2. Hermetyzacja
Celem hermetyzacji jest możliwość zarządzania dostępem do danych i typów. W tym celu
.NET implementuje zestaw czterech modyfikatorów dostępu. Modyfikatory te mogą być
ustawione zarówno dla pól, własności i metod. Trzy z nich mogą być nadane typom (klasom i
strukturom). Są one następujące:

● private - element jest widoczny tylko wewnątrz typu, w którym jest zdefiniowany,
● protected - (nie dotyczy klas) element jest widoczny tylko wewnątrz typu, w którym został
zdefiniowany oraz w typach po nim dziedziczących.
● internal - element widoczny jest dla wszystkich obiektów, ale tylko wewnątrz binarium, w
ramach którego został zdefiniowany (najczęściej wykorzystywane w bibliotekach).
● public - element jest widoczny dla wszystkich innych obiektów w danym binarium, jak i w
binariach je wykorzystujących.

9.3. Abstrakcja
Abstrakcja mówi o tworzeniu abstrakcyjnego modelu problemu, który zostanie wykorzystany.
Bazuje on na szablonach, a nie wiernej reprezentacji obiektów występujących w prawdziwym
świecie.

W .NET możliwe jest również zapewnienie abstrakcji wewnętrznie, pomiędzy obiektami.


Pozwalają one na uniezależnienie od siebie konkretnych implementacji obiektów na potrzeby
wykorzystania ich w logice innych typów. Istnieją dwa mechanizmy, które pozwalają na
zdefiniowanie takiego zachowania.

9.3.1. Klasy abstrakcyjne

Klasa abstrakcyjna to klasa posiadająca zaimplementowany szablon zachowań, pól i


własności do nich niezbędnych, ale nie posiadająca pełnej implementacji. Nie można utworzyć
instancji klasy abstrakcyjnej - można jedynie z niej dziedziczyć. Klasa dziedzicząca musi
zaimplementować wszystkie brakujące metody klasy lub również zostać oznaczona jako
abstrakcyjna.

using System;

namespace AbstractionExample
{
public abstract class Car
{
public string Make;
public string Model;

public abstract void Drive();


public abstract void Crash();
}

public class DailyCar : Car


{
public override void Crash()
{
[Link]("I'm dead");
}

public override void Drive()


{
[Link]("I got to the destination quickly.");
}
}

public class Truck : Car


{
public override void Crash()
{
[Link]("I have crashed, but I do not have even a
scratch");
}

public override void Drive()


{
[Link]("I got to the destination but it took me years
to reach it");
}
}

public class Program


{
public static void Main()
{
new Program().Run();
}

private void Run()


{
var subaru = new DailyCar
{
Make = "Subaru",
Model = "Impreza"
};

var peterbilt = new Truck


{
Make = "Peterbilt",
Model = "389"
};

//Starting a journey in Subaru. I got to the destination


quickly.
GetToTheDestination(subaru, true);

//Starting a journey in Peterbilt. I have crashed, but I do not


have even a scratch
GetToTheDestination(peterbilt, false);
}
private void GetToTheDestination(Car car, bool areYouLucky)
{
[Link]($"Starting a journey in {[Link]}");

if(areYouLucky)
{
[Link]();
}
else
{
[Link]();
}
}
}
}

9.3.2. Interface’y

Interface pozwala na zdefiniowanie sposobu komunikacji z obiektem. Jego możliwości są o


wiele bardziej ograniczone, pozwala bowiem na zdefiniowanie wymaganych metod (a co za tym
idzie, również własności).

Tak jak klasa abstrakcyjna stanowi szablon na potrzeby implementacji innych klas, tak
interface ma za zadanie zagwarantować możliwość komunikowania się z obiektem który go
implementuje. Pozwala na obsługę obiektu bez konieczności znajomości jakiegokolwiek typu, z
którym jest związany.

W .NET typ może implementować wiele interface'ów.

using System;

namespace AbstractionExample
{
public interface IDrivable
{
public abstract void Drive();
public abstract void Crash();
}

public class Car : IDrivable


{
public string Make;
public string Model;
public void Crash()
{
[Link]("I'm dead");
}

public void Drive()


{
[Link]($"I got to the destination quickly in my
{Make}.");
}
}

public class Presentation : IDrivable


{
public string Topic;

public void Crash()


{
[Link]("No electricity - I'm done!");
}

public void Drive()


{
[Link]($"I am presenting some cool stuff about
{Topic} to the audience");
}
}

public class Program


{
public static void Main()
{
new Program().Run();
}

private void Run()


{
var subaru = new Car
{
Make = "Subaru",
Model = "Impreza"
};

var peterbilt = new Presentation


{
Topic = "Pizzas all over the world"
};
//Starting to drive my thing. I got to the destination quickly
in my Subaru.
StartDriving(subaru, true);

//Starting to drive my thing. No electricity - I'm done!


StartDriving(peterbilt, false);
}

private void StartDriving(IDrivable drivable, bool areYouLucky)


{
[Link]($"Starting to drive my thing");

if(areYouLucky)
{
[Link]();
}
else
{
[Link]();
}
}
}
}

9.4. Dziedziczenie
Dziedziczenie stanowi, że w programowaniu obiektowym typy obiektów mogą być
dziedziczone, a co za tym idzie rozbudowywane z postaci bardziej ogólnej do bardziej
szczegółowej. W .NET typ może dziedziczyć po maksymalnie jednym typie. Domyślnie
wszystkie typy w .NET dziedziczą po typie bazowym object.

using System;

namespace InheritanceExample
{
public class Car
{
public string Make { get; set; }
public string Model { get; set; }

public void Drive()


{
[Link]($"I got to the destination quickly in my
{Make}.");
}
}

public class Truck : Car


{
public string Cargo { get; protected set; }
}

public class PlowTruck : Truck


{
public PlowTruck()
{
Cargo = "Sand & Salt";
}

public void Plow()


{
[Link]($"Getting rid of a snow on a road and
spreading {Cargo}");
}
}

public class Program


{
public static void Main()
{
new Program().Run();
}

private void Run()


{
var plowTruck = new PlowTruck
{
Make = "Volvo",
Model = "SnowPlower 3000"
};

[Link]();
[Link]();
}
}
}
9.4.1. Blokowanie możliwości dziedziczenia

Jeżeli dany typ ma nie pozwalać na dalsze jego rozbudowywanie, należy jego definicję
opatrzyć słowem kluczowym sealed. Próba dziedziczenia po takim typie spowoduje błąd
kompilacji.

public sealed class PlowTruck : Truck


{
public PlowTruck()
{
Cargo = "Sand & Salt";
}

public void Plow()


{
[Link]($"Getting rid of a snow on a road and spreading
{Cargo}");
}
}

//Dziedziczenie spowoduje podniesienie błędu kompilacji


public class JetEnginedPlowTruck : PlowTruck
{
//...
}

9.4.2. Nadpisywanie i przesłanianie

Tworząc typ bazowy możemy pozwolić na modyfikację dziedzicznej z niego funkcjonalności.


Można tego dokonać oznaczając metodę słowem kluczowym virtual.

Modyfikacji zachowania można dokonać na dwa sposoby. Poprzez przesłanianie lub


nadpisywanie.

Przesłanianie polega na stworzeniu metody o identycznej nazwie jak w typie bazowym.


Jednak wywołanie obiektu jako obiektu typu bazowego spowoduje wywołanie starej
funkcjonalności.

using System;

namespace InheritanceExample
{
public class Car
{
public string Make { get; set; }
public string Model { get; set; }

public virtual void Drive()


{
[Link]($"I got to the destination quickly in my
{Make}.");
}
}

public class Truck : Car


{
public string Cargo { get; protected set; }

public void Drive()


{
[Link]("I got to the destination but it took me years
to reach it");
}
}

public class Program


{
public static void Main()
{
new Program().Run();
}

private void Run()


{
var truck = new Truck
{
Make = "Peterbilt",
Model = "389"
};

DriveCar(truck); //I got to the destination quickly in my


Peterbilt.
DriveTruck(truck); //I got to the destination but it took me
years to reach it
}

private void DriveCar(Car car)


{
[Link]();
}
private void DriveTruck(Truck truck)
{
[Link]();
}
}
}

Aby ujednolicić zachowanie, należy poprzedzić definicję metody w klasie dziedziczącej


słowem override. Sprawi ono, że zachowanie zostanie nadpisane również dla typu bazowego.

using System;

namespace InheritanceExample
{
public class Car
{
public string Make { get; set; }
public string Model { get; set; }

public virtual void Drive()


{
[Link]($"I got to the destination quickly in my
{Make}.");
}
}

public class Truck : Car


{
public string Cargo { get; protected set; }

public override void Drive()


{
[Link]("I got to the destination but it took me years
to reach it");
}
}

public class Program


{
public static void Main()
{
new Program().Run();
}

private void Run()


{
var truck = new Truck
{
Make = "Peterbilt",
Model = "389"
};

DriveCar(truck); //I got to the destination quickly in my


Peterbilt.
DriveTruck(truck); //I got to the destination but it took me
years to reach it
}

private void DriveCar(Car car)


{
[Link]();
}

private void DriveTruck(Truck truck)


{
[Link]();
}
}
}

9.4.3. Słowo kluczowe this i base

Słowa kluczowe this i base odnoszą się odpowiednio:

● this - do obiektu, który je wywołuje,


● base - do klasy bazowej obiektu, który je wywołuje.

Słowo kluczowe base może być wykorzystywane do odwoływania się do przesłoniętych


metod typu bazowego oraz do jego konstruktora podczas inicjalizacji.

using System;

namespace InheritanceExample
{
public class Car
{
public string Make { get; private set; }
public string Model { get; private set; }

public Car(string make, string model)


{
[Link] = make;
[Link] = model;
}

public void Drive()


{
[Link]($"I got to the destination in my {Make}.");
}
}

public class VolvoTruck : Car


{
public string Cargo { get; private set; }

public VolvoTruck(string model, string cargo)


: base("Volvo", model)
{
[Link] = cargo;
}

public void Drive()


{
[Link]();
[Link]("It took me years to reach it");
}
}

public class Program


{
public static void Main()
{
new Program().Run();
}

private void Run()


{
var truck = new VolvoTruck("FH I-Save", "Electronics");

//I got to the destination in my Volvo.


//It took me years to reach it.
[Link]();
}
}
}
10. Kolekcje
10.1. Definicja
Dokumentacja - click

10.2. Podstawowe rodzaje kolekcji


10.2.1. Tablica
Przykład tablicy jednowymiarowej?
var lastYearTemperatures = new int[365];
//... wypełnienie tablicy danymi ...
[Link]("1szego stycznia temperatura wynosiła " +
lastYearTemperatures[0]);
Przykład tablicy tablic:
var lastYearTemperatures = new int[12][];
lastYearTemperatures[0] = new int[31];
lastYearTemperatures[1] = new int[28];
lastYearTemperatures[2] = new int[31];
lastYearTemperatures[3] = new int[30];
lastYearTemperatures[4] = new int[31];
lastYearTemperatures[5] = new int[30];
lastYearTemperatures[6] = new int[31];
lastYearTemperatures[7] = new int[31];
lastYearTemperatures[8] = new int[30];
lastYearTemperatures[9] = new int[31];
lastYearTemperatures[10] = new int[30];
lastYearTemperatures[11] = new int[31];
//... wypełnienie tablic danymi ...
[Link]("17tego czerwca temperatura wynosiła " +
lastYearTemperatures[5][16]);

Przykład tablicy wielowymiarowej

var chessBoard = new int[8, 8];


//Fill the board
//Move Horse
chessBoard[1, 0] = 0;
chessBoard[2, 2] = 2;

10.2.2. Lista

var lista = new List<string>();


[Link]("first");
[Link]("collection");
[Link]("trial");

for (int i = 0; i < [Link]; i++)


lista[i] = [Link](lista[i][0]) +
lista[i].Substring(1);

foreach (var slowo in lista)


[Link](slowo);
Pytanie: Co by się stało gdybyśmy podali typ “object” zamiast “string” do listy?

Przykład konwersji między tablicą i listą:

var lastYearTemperaturesArray = new int[365];


//... wypełnienie tablicy danymi ...
var lastYearTemperaturesList =
[Link]();
lastYearTemperaturesArray = [Link]();

Przykład listy list

var lastYearTemperatures = new List<List<int>>();

for(int i=0; i<12; i++)


[Link](new List<int>());

//... wypełnienie list danymi ...

[Link]("17tego czerwca temperatura wynosiła " +


lastYearTemperatures[5][16]);

10.2.3. Dictionary
Przykład słownika
var telephoneNumbers = new Dictionary<string, List<int>>();

var jansTelephoneNumbers = new List<int>


{
502503572,
781781781
};

[Link]("JanNowak", piotrsTelephoneNumbers);
var jansFirstNumber = telephoneNumbers["JanNowak"][0];

Pytanie: Co się stanie jak podkreśloną linijkę zduplikujemy i wykonamy jeszcze raz?
Pytanie: Jaka jest różnica między Dictionary<int, List<string>> a List<List<string>>?

10.2.4. HashSet

var employeeNames = new HashSet<string>


{
"Michał",
"Janusz",
"Lech",
"Karolina",
"Marta",
"Piotr",
"Lech",
"Paweł"
};

[Link]([Link]);

10.2.5. Stack

Stack<UrgentTask> tasks = new Stack<UrgentTask>();


[Link](new UrgentTask(id: 14523));
[Link](new UrgentTask(id: 334));
[Link](new UrgentTask(id: 894942));
var resolvedTask = [Link]();
//Konsola wyświetli 894942
[Link]($"ResolvedTask is {[Link]}");

10.2.6. Queue

Queue<UrgentTask> tasks = new Queue<UrgentTask>();


[Link](new UrgentTask(id: 14523));
[Link](new UrgentTask(id: 334));
[Link](new UrgentTask(id: 894942));
var resolvedTask = [Link]();
//Konsola wyświetli 14523
[Link]($"ResolvedTask is {[Link]}");

10.3. LINQ
Dokumentacja - click

10.3.1. Wybrane funkcje - Where


Zadanie: Podnieś wszystkim pracownikom ilość lat doświadczenia o 1. Potem zapisz do
zmiennej “allSeniors” wszystkich pracowników którzy mają conajmniej 5 lat doświadczenia.
private class Employee
{
public string Name { get; set; }
public string Surname { get; set; }
public int YearsOfExperience { get; set; }
}

static void Main(string[] args)


{
const int SeniorYearsOfExperience = 5;
var employees = LoadEmployeesDataFromDb();
...
}

Przykład rozwiązania bez użycia LINQ


var allSeniors = new List<Employee>();
foreach (var employee in employees)
if (++[Link] >=
SeniorYearsOfExperience)
[Link](employee);

Przykłąd rozwiązania z użyciem LINQ

allSeniors = [Link](employee =>


++[Link] >=
SeniorYearsOfExperience).ToList();

10.3.2. Wybrane funkcje - First, FirstOrDefault, Last, LastOrDefault

var janKowalski = employees


.FirstOrDefault(employee => [Link] == "Jan" &&
[Link] == "Kowalski");
if (janKowalski == null)
[Link]("There is no Jan Kowalski in the
employee list");
//Jeżeli nie ma Jana Nowaka w zbiorze pracowników to poniższa
//linijka rzuci wyjątek
var janNowak = [Link](employee => [Link] ==
"Jan" && [Link] == "Nowak");

10.3.3. Wybrane funkcje - All, Any

//Poniższe warunki są równoważne


var areAllEmployeesSeniors = [Link](employee =>
[Link] >= 5);
var isAnyEmployeeNotSenior = [Link](employee =>
[Link] < 5);

10.3.4. Wybrane funkcje - ForEach

static void PresentAllEmployees(List<Employee> employees)


{
[Link](employee => [Link]($"Employee
{[Link]} {[Link]} has
{[Link]} years of experience."));
}

10.3.5. Wybrane funkcje - Average, Sum, Count, Max, Min

var whatIsAverageEmployeeExperience = [Link](employee => [Link]);


var howManySeniorsDoWeHave = [Link](employee => [Link] >= 5);
var whatIsAggregatedEmployeeExperience = [Link](employee => [Link]);
var whatIsMinimalEmployeeExperience = [Link](employee => [Link]);
var whatIsMaximalEmployeeExperience = [Link](employee => [Link]);

10.3.6. Wybrane funkcje - Select, SelectMany

private class Employee


{
public string Name { get; set; }
public string Surname { get; set; }
public int YearsOfExperience { get; set; }
}

private class Company


{
public List<Employee> Employees { get; set; }
}

static void Main(string[] args)


{
var companies = LoadECompaniesDataFromDb();

var firstCompany = [Link]();


var employeeFullNamesFromFirstCompany =
[Link](employee =>
$"{[Link]}
{[Link]}");
var employeeFullNamesFromAllCompanies =
[Link](company =>
[Link](employee => $"{[Link]}
{[Link]}"));
}

10.3.7. Wybrane funkcje - Distinct

var allSurnames = employees


.Select(employee => [Link])
.Distinct();

10.3.8. Wybrane funkcje - Exept

var maxExperienceInYears = [Link](employee =>


[Link]);
var employeesWithBiggestExperience =
[Link](employee => [Link]
== maxExperienceInYears);
var restOfEmployees =
[Link](employeesWithBiggestExperience));

10.3.9. Wybrane funkcje - GroupBy

var employeesGroupedByExperience = [Link](employee


=> [Link]);
foreach(var employeesWithTheSameExperience in employeesGroupedByExperience)
{
[Link]().ForEach(employee =>
[Link]($"{[Link]} {[Link]} has
{[Link]} years of expierence"));
}

10.3.10. Wybrane funkcje - Intersect

var firstCompany = [Link](0);


var secondCompany = [Link](1);

var sharedEmployees =
[Link]([Link]);

10.3.11. Wybrane funkcje - Take, Skip, OrderBy

var orderedEmployees = [Link](employee =>


[Link]);
var sixthToTenthEmployee = [Link](5).Take(5);

10.3.12. Statement Lambda

[Link](employee =>
{
[Link]($"Analysing employee {[Link]}
{[Link]}");
if ([Link] <= 2)
return "JUNIOR";
else if ([Link] < 5)
return "MIDDLE";
else
return "SENIOR";
});

10.4. Różnice między interfejsami IList, ICollection i IEnumerable

Bardzo przyjemne wytłumaczenie: click

Pytanie: Co wyświetli się na consoli?

var numbers = new List<int> { 1, 2, 3 };


var incrementedNumbers = [Link](number =>
{
[Link]($"I am incrementing {++number}");
return number;
});

[Link]([Link]());
[Link]([Link]());

10.5. Słowo kluczowe “yield”

Przykład metody która zwraca obiekt typu IEnumerable bez użycia słowa kluczowego yield:

public static IEnumerable<Employee> AddGroupOfEmployees()


{
var newEmployees = new List<Employee>();

while (true)
{
[Link]("Add employee first name and surname.");
[Link]("You can also put \"S\" to save or \"X\" to cancel");
var firstName = [Link]();
if ([Link]("X"))
{
newEmployees = null;
break;
}
else if ([Link]("S"))
break;

var surname = [Link]();


[Link](new Employee { Name = firstName, Surname = surname });
}

return newEmployees;
}

Ten sam przykład z użyciem słowa kluczowego “yield”

public static IEnumerable<Employee> AddGroupOfEmployees()


{
while (true)
{
[Link]("Add employee first name and surname.");
[Link]("You can also put \"S\" to save or \"X\" to cancel");
var firstName = [Link]();
if ([Link]("X"))
yield break;
else if ([Link]("S"))
break;
var surname = [Link]();
yield return new Employee { Name = firstName, Surname = surname };
}
}

11. Serializacja
11.1. Przygotowanie danych

[Serializable]
public class Employee
{
public string Name { get; set; }
public string Surname { get; set; }
}

[Serializable]
public class Company
{
public IEnumerable<Employee> Employees { get; set; }
public string Name { get; set; }
}
public static void Main(string[] args)
{
var companies = new List<Company>();

[Link](new Company
{
Name = "KGHM",
Employees = new List<Employee>
{
new Employee { Name = "Jan", Surname = "Kowalski" },
new Employee { Name = "Jan", Surname = "Nowak" },
new Employee { Name = "Edyta", Surname = "Rabenda" }
}
});

[Link](new Company
{
Name = "JSW",
Employees = new List<Employee>
{
new Employee {Name = "Zenon", Surname = "Górecki"}
}
});
}

11.2. Serializacja Binarna

var stream = new FileStream("[Link]",


[Link]);
var binaryFormatter = new BinaryFormatter();
[Link](stream, companies);
[Link]();

11.3. Serializacja Xmlowa


//Możnaby alternatywnie stworzyć “using var xmlStream”. Wtedy
//nie trzeba by było wołać metody “Close”
var xmlStream = new FileStream("[Link]",
[Link]);
var xmlSerializer = new XmlSerializer(typeof(List<Company>));
[Link](xmlStream, companies);
[Link]();

Trzeba pamiętać o zamienieniu wszystkich interfejsów w modelu na konkretne klasy czyli w


klasie “Company” zamienić typ właściwości “employees” na “List<Employee>”

11.4. Serializacja Jsonowa

var jsonStream = new


FileStream("[Link]", [Link]);
var serializationTask =
[Link](jsonStream, companies);
[Link]();
[Link]();

12. Dodatkowe słowa kluczowe


12.1. typeof
Pozwala na uzyskanie zmiennej typu “Type” (czyli zmiennej w której mamy zapisany typ innego
obiektu) który jest znany podczas kompilacji.

public static void Main(string[] args)


{
var type = typeof(Company);
[Link](
$"Type {[Link]} has
{[Link]().[Link]()} properties ");
}

12.2. checked

public class Company


{
public List<Employee> Employees { get; set; }
public string Name { get; set; }
public int WorkingAssets { get; set; }
}
public static void Main(string[] args)
{
var companies = LoadCompaniesFromDb();
var sharedCompaniesAssets = 0;
[Link](
_ => sharedCompaniesAssets =
checked(sharedCompaniesAssets + _.WorkingAssets));
}

12.3. unchecked
Domyślnie cały kod jest ustawiony w tryb “unchecked” - czyli przepełnienie zmiennej nie
skutkuje wyjątkiem. Można zmienić domyślny tryb poprzez parametr podany do budowania. Jest
to jeden z głównych przypadków gdy używa się słowa unchecked. Można go również użyć w
większym bloku oznaczonym jako checked
checked
{
var previousIteration = LoadIterationFromDb();
var commonCompaniesAssets = 0;
[Link](_ => {
commonCompaniesAssets += _.WorkingAssets;
[Link]($"There was a {unchecked(++previousIteration)}");
});
}

12.4. nameof
Uzyskanie nazwy konkretnego elementu bez podawania całego namespace’a
[Link](
_ =>
[Link](
$"The value of {nameof(_.WorkingAssets)} for {_.Name} is {_.WorkingAssets}"));

12.5. sizeof
SizeOf działa tylko dla typów value (bo jest znana ich wielkość). Może być przydatne podczas
tworzenia hiperwydajnych aplikacji gdzie się nadpisuje standardowe .NETowe alokacje obiektów
przy użyciu słów kluczowych “stackalloc” i “fixed”.
var sizeOfDouble = sizeof(double);

13. Obiekty statyczne


13.1. Klasy Statyczne
Klasy statycznej nie można stworzyć za pomocą słowa kluczowego “new”. Do klasy statycznej
odwołuje się poprzez nazwę klasy a nie nazwę konkretnego obiektu tej klasy. Typowym
przykładem klasy statycznej jest klasa Console - nie tworzymy nowego obiektu klasy Console,
ale bezpośrednio wołamy metodzie na klasie.

Wszystkie Elementy klasy statycznej muszą być również statyczne.


13.2. Metody Statyczne
● Metody statyczne mogą znajdować się w niestatycznych klasach.
● Do metod statycznych zawsze odwołujemy się poprzez klasę a nie obiekt klasy
static class Logger
{
const string LogFile = "[Link]";
public static void Log(string dataToLog)
{
using var streamWriter = new StreamWriter(LogFile);
[Link]("Log Started");
[Link](dataToLog);
[Link]("Log Ended");
}
}

Klasy statyczne są niebezpieczne - co się stanie jak powyższego loggera uruchomi wiele
wątków?

13.3. Konstruktor Statyczny i Pola Statyczne


● Konstruktor statyczny może być jeden.
● Konstruktor statyczny nie może mieć parametrów.
● Konstruktor statyczny nie może posiadać modyfikatora dostępu.

static class Logger


{
const string LogFile = "[Link]";

private static StreamWriter streamWriter;

static Logger()
{
streamWriter = new StreamWriter(LogFile);
}

public static void Log(string dataToLog)


{
[Link]("Log Started");
[Link](dataToLog);
[Link]("Log Ended");
}
}

Pytanie: Czy nowe podejście rozwiązuje nam wszystkie problemy przy uruchomieniu na wielu
wątkach? Odpowiedź brzmi - nie wszystkie :)

13.4. Property Statyczne

public class Company


{
public static string ComapniesQuestionare {
get
{
var companiesQuestionare = LoadQuestionareFromDb();
return $"Every company needs to feel the following questionare:
{companiesQuestionare}";
}
}
public List<Employee> Employees { get; set; }
public string Name { get; set; }
public int WorkingAssets { get; set; }
}

14. Rzutowania
14.1. Rzutowanie niejawne
Rzutowanie niejawne wykonuje się wtedy gdy poprzez rzutowanie nie tracimy żadnej informacji.
Rzutowanie niejawne wykonuje się również podczas rzutowania obiektu na podklasę którą
obiekt dziedziczy.

long biggerBoxForNumber = [Link]; //9223372036854775807


int smallerBoxForNumber = [Link]; //2147483647

int number = 2147483647;

biggerBoxForNumber = number; //2147483647

14.2. Rzutowanie jawne


Rzutowanie jawne trzeba wykonać gdy poprzez rzutowanie tracimy dostęp do informacji. Innym
przykładem użycia rzutowania jawnego jest gdy wcześniej dokonaliśmy rzutowania niejawnego
do podklasy obiektu, a potem chcemy odwrócić proces
long biggerBoxForNumber = [Link]; //9223372036854775807
int smallerBoxForNumber = [Link]; //2147483647

long number = 9223372036854775807;


long number2 = 2147483647;

// Poniższa linijka nie zadziała dlatego musiała zostać zakomentowana


// smallerBoxForNumber = number;

smallerBoxForNumber = (int) number; // -1


smallerBoxForNumber = (int) number2; // 2147483647

14.3. Is
public class Orlen : Company { }

public static void Main(string[] args)


{
var company = new Orlen();
var employee = new Employee();
int integer = [Link];
long longInteger = [Link];
var someTests = new bool[]{
company is Employee, //false
employee is Employee, //true
company is Company, //true
employee is Company, //false
company is Orlen, //true
employee is Orlen, //false
integer is int, //true
integer is long, //WAŻNE! false
longInteger is int, //false
longInteger is long }; //true
}

14.4. As
//Poniższe dwie instrukcje dają taki sam rezultat niezależnie od wartości “company"
var orlenCompany = company is Orlen ? (Orlen)company : null;
var orlenCompany2 = company as Orlen;

15. Zaawansowane Aspekty programowania .NET


15.1. Tworzenie obiektów
Ustawmy breakpointa na pierwszej z poniższych operacji (tak by obie instrukcje nie były jeszcze
wykonane). Pytanie: Jaką wartość pod debuggerem będzie miała zmienna “word” a jaką
“dateTime”?
var word = "aWord";
var dateTime = [Link];

15.2. Extension Methods


public static class StringExtensions
{
public static Company ToCompany(this string companyString, int assets = 0)
{
return new Company
{
Name = companyString,
Employees = new List<Employee>(),
WorkingAssets = assets
};
}
}

public static void Main(string[] args)


{
string orlenString = "Orlen";
Company orlenCompany = [Link]();
}

15.3. Typy Anonimowe


var aComplexObject = new
{
Actually = "It",
Can = "Be",
Anything = "You",
Would = new
{
Like = "To",
Have = '.'
},
AnInteger = 1,
ALong = 2L,
Or = "even",
ACompany = new Company()
};

15.4. Dynamic
dynamic aDynamicVariable = "This one";
aDynamicVariable = "Is Quite similar";
aDynamicVariable = "To some scrypting languages";
aDynamicVariable = "You can change it value to anything, like";
aDynamicVariable = 1;
aDynamicVariable = 1m;
aDynamicVariable = 0.15423;
aDynamicVariable = "But almost every time it is bad practise to use this";
aDynamicVariable = "Because it results in runtime exceptions";
aDynamicVariable = "For instance when you add to an int something like Company!";
aDynamicVariable = 2;
aDynamicVariable += new Company();
aDynamicVariable += "Yes! You are right! Aformentioned code compiles!!!";

15.5. Klasy Lokalne


Klasy Lokalne zakłada się w celu zapewnienia jeszcze większego poziomu hermetyzacji - na
przykład jak nasza klasa pozwala oszukać mechanizm autoryzacji (którą przygotowaliśmy na
przykład na potrzeby testów) nie powinna być eksponowana w całym assembly.
[TestFixture]
class CompanyControllerTest
{
private class TestingDbContext : DbContext
{

[Test]
public void CompanyControllerTest()
{
var dbContext = new TestingDbContext();
var companyController = new CompanyController(dbContext);
[Link]();
}
}

15.6. Metody Lokalne

public static void Main(string[] args)


{

Employee SelectEmployeeWithBiggestExperience(Company company)


{
return [Link]?.First(
_ => _.YearsOfExperience ==
[Link](_ => _.YearsOfExperience));
}

var mostExpieriencedEmployees =
[Link](SelectEmployeeWithBiggestExperience);
}

15.7. Refleksja - Activator

public static void Main(string[] args)


{
[Link]("Which class should I create?");
var className = [Link]();
Type classType =
[Link]().GetTypes()
.FirstOrDefault(_ => _.Name == className);
var anObject = [Link](classType);
if (anObject is Company)
[Link]("It was a company!!!");
else
[Link]("It was not a company :(");
}
}

16. REST API i Entity Framework


Dokumentacja - click.

16.1. Migracje
Dokumentacja - click.

16.2. Interfejs IQueryable - Implementacja Lazy Loadingu


Podczas używania Entity Frameworka operujemy na obiekcie reprezentującym dane w bazie
danych. Dopóki nie zadeklarujemy jawnie że chcemy wyczytać dane z tego obiektu dopóty
zapytanie do bazy się nie wykona. Aby zadeklarować jawnie chcęć wyczytania danych można
przeiterować po naszym obiekcie (np. za pomocą foreach) lub można wykonać konwersję do
obiektów które nie implementują IQueryable (czyli np. za pomocą metod ToList lub ToArray)
public class EmployeesService : IEmployeesService
{
private readonly DatabaseContext context;

public async Task<IEnumerable<Employee>> GetEmployees(string companyName)


{
return context //To jest kontekst bazy danych, samo wywołanie go nie ściąga danych
.Companies //Odwołanie się do DbSeta, nie ściąga danych
.Where(_ => _.Name == name) //Zwraca obiekt implementujący IQueryable - nie
// ściąga danych
.Select(_ => _.Employees) //Zwraca obiekt implementujący IQueryable - nie
// ściąga danych
.ToList() // Zwraca listę - tutaj wykonuje się SQL który ściąga dane
}
}
16.3. Metody Include, ThenInclude
public class EmployeesService : IEmployeesService
{
private readonly DatabaseContext context;

public async Task<IEnumerable<Employee>> GetEmployees(string companyName)


{
var company = [Link](_ => _.Name == name).ToList();
return [Link](_ => _.Employees);
}
}

Pytanie - Co zostanie zwrócone z funkcji GetEmployees? Niezależnie co znajdowałoby się w


bazie danych to zostanie zwrócony null. Dzieję się tak ponieważ Entity Framework optymalizuje
zapytanie SQL i nie ściąga danych które uznaje za zbędne. Domyślnie wszystkie elementy z
innych tabel uważa za zbędne. By jawnie określić dane z których relacji mają być ściągnięte
używamy funkcji Include oraz ThenInclude

public class EmployeesService : IEmployeesService


{
private readonly DatabaseContext context;

public async Task<IEnumerable<Employee>> GetEmployees(string companyName)


{
var company = [Link](_ => _.Name == name)
.Include(company => [Link])
.ThenInclude(employee => [Link]).ToList();
return [Link](_ => _.Employees);
}
}

17. Autoryzacja OAuth2


17.1. Rejestracja za pomocą Google’a
17.1.1. Wejdź na [Link]
17.1.2. Kliknij “Utwórz dane logowania” -> “Identyfikator klienta oauth”
17.1.3. Wybierz “Aplikacja Internetowa”. Wpisz nazwę aplikacji.
17.1.4. Wpisz adres URI do przekierowania. Powinien być zbieżny z adresem do
naszego API.
17.1.5. Skopiuj ClientId i ClientSecret
17.2. Zdefiniowanie autentykacji w kontenerze DI
W poniższym przykładzie użyte zostały Cookiesy do przekazywania AccessTokena a Google do
uzyskania Access Tokena
public class Startup{

public Startup(IConfiguration configuration)


{
Configuration = configuration;
}

public IConfiguration Configuration { get; }

public void ConfigureServices(IServiceCollection services)


{
[Link]();

[Link](options =>
{
[Link] =
[Link];
[Link] = [Link];
[Link] = [Link];
[Link] = [Link];
}).AddGoogle(options =>
{
[Link] = "CLIENT_ID_FROM_GOOGLE";
[Link] = "CLIENT_SECRET_FROM_GOOGLE";
})
.AddCookie(options =>
{

[Link] = false;
[Link] = [Link];
});

[Link]();
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)


{
if ([Link]())
{
[Link]();
using (var ss = [Link]())
{
var context = [Link]<PgDbContext>();
[Link](new COmpanty { Name = "JSw", Surname = "Kak" });
[Link]();
}
}

[Link]();

[Link]();

[Link]();

[Link]();
[Link](endpoints =>
{
[Link]();
});
}

17.3. Atrybut Authorize


Powyższy kod powinien się kompilować jednak nie zmieni nic w naszym dostępie do
kontrollerów. Aby API wymagało Googlowej atoryzacji musimy dodać atrybut Authorize nad
naszym kontrolerem.

[Route("api/[controller]")]
[Authorize]
[ApiController]
public class CmpaniesController : ControllerBase
{
...
...
...

17.4. Stworzenie konkretnych polityk dostępu

public void ConfigureServices(IServiceCollection services)


{
...
...
...

[Link](options =>
[Link]("User", policy =>
[Link](new UserPolicyRequirement())));
[Link](options =>
[Link]("Admin" policy =>
[Link](new AdminPolicyRequirement())));
[Link]<IAuthorizationHandler, UserPolicyHandler>();
[Link]<IAuthorizationHandler, AdminPolicyHandler>();
[Link]();
}

public class AdminPolicyHandler : AuthorizationHandler<AdminPolicyRequirement>


{
const string GoogleEmailAddressSchema =
"[Link]

protected override Task HandleRequirementAsync(


AuthorizationHandlerContext context,
AdminPolicyRequirement requirement)
{
var email = [Link](p =>
[Link]("Google") &&
[Link](GoogleEmailAddressSchema));

if (email != null && [Link]("[Link]@[Link]"))


[Link](requirement);
return [Link];
}
}

public class AdminPolicyRequirement : IAuthorizationRequirement { }

public class UserPolicyHandler : AuthorizationHandler<UserPolicyRequirement>


{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
UserPolicyRequirement requirement)
{
[Link](requirement);
return [Link];
}
}

public class UserPolicyRequirement : IAuthorizationRequirement { }


Polityk dostępu używa się poprzez podanie konkretnego parametru do kontrollera.

[Route("api/[controller]")]
[Authorize(Policy = "Admin")]
[ApiController]
public class CmpaniesController : ControllerBase
{
...
...
...

18. Dobre praktyki programowania


Niniejszy rozdział zawiera zbiór popularnych dobrych praktyk programowania (nie tylko z
.NET), które są powszechnie wykorzystywane podczas codziennej pracy programisty. Część z
nich jest trywialna w treści, ale trudna w zastosowaniu. Inne wręcz odwrotnie - pomimo
skomplikowanej treści są proste i ich użycie przychodzi wręcz naturalnie.

18.1. SOLID
Artykuł - click.

SOLID to najważniejszy zbiór zasad, do którego powinien stosować się programista języka
obiektowego. Zasady SOLID ułatwiają tworzenie kodu podatnego na dalszy rozwój w bliżej
nieprzewidzianym oraz ułatwiają jego zrozumienie, co przyspiesza adaptację nowych, nawet
mniej doświadczonych członków zespołu programistycznego.

Skrót SOLID to pierwsze litery każdej z 5 zasad, które się pod nim kryją:

● Single Responsibility Principle,


● Open/Close Principle,
● Liskov Substitution Principle,
● Interface Segregation Principle,
● Dependency Inversion Principle.

18.1.1. Single Responsibility Principle

Zasada pojedynczej odpowiedzialności stanowi, iż klasa obiektów powinna być


odpowiedzialna za konkretny i spójny wycinek funkcjonalności.

Za przykład jej zastosowania niech posłuży klasa File, przychodząca wraz z bibliotekami
platformy .NET. Klasa file odpowiedzialna jest za takie operacje jak tworzenie tworzenie pliku,
sprawdzanie jego istnienia, prosty odczyt czy zapis zawartości jak i manipulowanie nazwą i
rozszerzeniem. Gdy jednak chcemy utworzyć plik we wskazanym katalogu, aby sprawdzić czy
katalog istnieje i go stworzyć, nie możemy skorzystać z klasy File. Jest ona odpowiedzialna
jedynie za pliki. W celu zarządzania katalogami musimy skorzystać z klasy Directory.

Pisanie kodu zgodnie z Zasada Pojedynczej Odpowiedzialności prowadzi do tworzenia klas


obiektów, które łatwo można wyciągać do osobnych bibliotek i używać w innych projektach.
Ułatwia również zarządzanie strukturą kodu, upraszcza jego zrozumienie i zapobiega
powstawaniu odniesień i powiązań między typami, które później utrudniałyby rozwój systemu.

18.1.2. Open/Close principle

Zasada Open/Close w swojej pełen treści mówi, że “klasa powinna być otwarta na
rozszerzenia, ale zamknięta na modyfikację”. Oznacza to, że projektując nasze klasy i testując
je powinniśmy starać się pisać je tak, aby klasa raz stworzona i przetestowania nie wymagała
modyfikacji zawartej w niej logiki, ale aby łatwo można było rozszerzać ją o nowe
funkcjonalności. Pozwala to zapobiec regresji.

18.1.3. Liskov Substitution Principle

Zasada podstawiania Liskov mówi, że każdy obiekt powinien być móc użyty jako obiekt swojej
klasy bazowej i jego zachowanie powinno być zgodne z oczekiwaniami. Popularnym
przykładem zasady podstawiania Liskov jest modelowanie kwadratu i prostokąta.

Załóżmy, że w ramach realizowanej funkcjonalności wymagane jest stworzenie logiki


obliczającej pole prostokąta. W wyniku realizacji zadania powstaje następująca klasa
reprezentująca prostokąt:

public class Rectangle


{
public int A { get; private set; }
public int B { get; private set; }

public void SetSideA(int a)


{
A = a;
}

Public void SetSideB(int b)


{
B = b;
}

public int GetArea()


{
return A * B;
}

public int GetPerimeter()


{
return 2 * (A + B);
}
}

Wraz z rozwojem aplikacji pojawia się konieczność rozszerzenia obecnej funkcjonalności o


kwadrat. Zgodnie z wiedzą matematyczną, wg. której kwadrat to szczególny przypadek
prostokąta, wykorzystamy go jako bazę pod stworzenie klasy reprezentującej kwadrat. Aby to
umożliwić i odziedziczyć wspólną funkcjonalność kwadratu i prostokąta, klasa reprezentująca
prostokąt wymaga drobnych modyfikacji.

public class Rectangle


{
public int A { get; protected set; }
public int B { get; protected set; }

public virtual void SetSideA(int a)


{
A = a;
}

public virtual void SetSideB(int b)


{
B = b;
}

public int GetArea()


{
return A * B;
}

public int GetPerimeter()


{
return 2 * (A + B);
}
}

public class Square : Rectangle


{
public override void SetSideA(int a)
{
A = a;
B = a;
}

public override void SetSideB(int a)


{
A = a;
B = a;
}
}

Taka implementacja wydaje się zgodna z powszechną wiedzą matematyczną, niestety nie
sprawdza się ona w przypadku programowania obiektowego. Powodem jest zupełnie inne
oczekiwane zachowanie od obiektów obu klas. Jeżeli spróbujemy użyć kwadratu tak jak
prostokąta (którym jest), ten zachowa się w sposób niezgodny z naszymi oczekiwaniami.

public class Program


{
static void Main()
{
var square = new Square();

[Link](2);
[Link]([Link]()); //4 - as expected

InitializeRextangleByUser(square);
[Link]([Link]()); //9 - 6 expected
}

private static void InitializeRextangleByUser(Rectangle rec)


{
[Link]([Link]([Link]())); //User types 2
[Link]([Link]([Link]())); //User types 3
}
}
Prawidłowa implementacja przedstawionego przykładu w zgodzie z Zasadą Podstawiania
Liskov może polegać na przykład na stworzeniu abstrakcyjnej klasy bazowej dla figury bazującej
na prostokącie i dwóch dziedziczących z niej klas, kolejno reprezentujących kwadrat i prostokąt.
Nie pozwoli to na nieprawidłowe wykorzystanie którejkolwiek z tych klas poprzez mechanizm
dziedziczenia.

using System;
namespace SolidExamples
{
public class Program
{
static void Main()
{
var square = new Square();

[Link](2);
[Link]([Link]()); //4 - as expected

InitializeRextangleByUser(square); //<-- Compilation error


[Link]([Link]()); //9 - as expected
}

private static void InitializeRextangleByUser(Rectangle rec)


{
[Link](
[Link]([Link]()), //User types 2
[Link]([Link]())); //User types 3
}

private static void InitializeRextangleByUser(Square rec)


{
[Link](
[Link]([Link]())); //User types 3
}
}

public abstract class RectangularShape


{
protected int GetArea(int a, int b)
{
return a * b;
}

protected int GetPerimeter(int a, int b)


{
return 2 * (a + b);
}
}

public class Rectangle : RectangularShape


{
public int A { get; private set; }
public int B { get; private set; }
public void SetSides(int a, int b)
{
A = a;
B = b;
}

public int GetArea()


{
return GetArea(A, B);
}

public int GetPerimeter()


{
return GetPerimeter(A, B);
}
}

public class Square : RectangularShape


{
public int A { get; private set; }

public void SetSide(int a)


{
A = a;
}

public int GetArea()


{
return GetArea(A, A);
}

public int GetPerimeter()


{
return GetPerimeter(A, A);
}
}
}

18.1.4. Interface Segregation Principle

Zasada segregacji interfaceów mówi, aby zamiast tworzenia dużych, ogólnych interfaców dla
obiektów, tworzyć wiele małych, specyficznych i implementować wiele z nich w jednej klasie.
Przykładowo, tworząc aplikację związaną z logistyką posiadamy logikę zarządzającą pojazdami
transportowymi. Załóżmy, że chcemy posiadać interface, który gwarantuje implementację
funkcjonalności wymaganej od pojazdu transportowego - pozwala on na załadowanie pojazdu,
dołączenie do niego naczepy i wysłanie w trasę:

public interface IDeliveryVehicle


{
void SetDestination(City destination);
void AttachTrailer(Trailer trailer);
void Load(Good[] goods);
}

public class City


{
public string Name { get; set; }
}

public class Trailer


{
public int WeightCapacity { get; set; }
}

public class Good


{
public string Name { get; set; }
public int Weight { get; set; }
}

Na podstawie interfaceu modelujemy klasę reprezentującą ciężarówkę:

public class Truck : IDeliveryVehicle


{
public string Brand { get; set; }
public string Model { get; set; }
public string LicensePlates { get; set; }
public int WeightCapacity { get; set; }

private City _destination { get; set; } = null;


private Trailer _trailerAttached { get; set; } = null;
private List<Good> _loadedGoods { get; set; } = new List<Good>();

public void AttachTrailer(Trailer trailer)


{
if(_trailerAttached != null)
{
[Link]($"Truck {LicensePlates} already have a
trailer!");
return;
}

_trailerAttached = trailer;
}

public void Load(Good[] goods)


{
var totalCapacity = WeightCapacity +
(_trailerAttached?.WeightCapacity ?? 0);
var totalLoad = _loadedGoods.Sum(g => [Link]);
var totalNewLoad = [Link](g => [Link]);

if(totalCapacity < totalLoad + totalNewLoad)


{
[Link]($"Truck {LicensePlates} overweight! Cannot add
required goods.");
return;
}

_loadedGoods.AddRange(goods);
}

public void SetDestination(City destination)


{
if (_destination != null)
{
[Link]($"Truck {LicensePlates} already have a
destination {_destination.Name}!");
return;
}

_destination = destination;
}
}
Załóżmy, że wraz z rozwojem konieczne jest dodanie nowego, mniejszego rodzaju pojazdu
ciężarowego, do którego nie można zamontować naczepy. Użycie istniejącego interface’u
oznaczałoby konieczność niepełnej jego implementacji:

public class Van : IDeliveryVehicle


{
public string Brand { get; set; }
public string Model { get; set; }
public string LicensePlates { get; set; }
public int WeightCapacity { get; set; }

private City _destination { get; set; } = null;


private List<Good> _loadedGoods { get; set; } = new List<Good>();

public void AttachTrailer(Trailer trailer)


{
throw new Exception("Vans cannot get trailer attached");
}

//...
}

Jest to jeden z przykładów naruszenia Zasady Segregacji Interfaceów. W celu zniwelowania


tego naruszenia, należałoby wyciągnąć metodę AttachTrailer do nowego, osobnego interfaceu i
odpowiednio zmodyfikować klasy Truck i Van:

public interface IDeliveryVehicle


{
void SetDestination(City destination);
void Load(Good[] goods);
}

public interface ITrailerExtendibleVehicle


{
void AttachTrailer(Trailer trailer);
}

public class Van : IDeliveryVehicle


{
public string Brand { get; set; }
public string Model { get; set; }
public string LicensePlates { get; set; }
public int WeightCapacity { get; set; }

private City _destination { get; set; } = null;


private List<Good> _loadedGoods { get; set; } = new List<Good>();

public void Load(Good[] goods)


{
var totalLoad = _loadedGoods.Sum(g => [Link]);
var totalNewLoad = [Link](g => [Link]);
if (WeightCapacity < totalLoad + totalNewLoad)
{
[Link]($"Van {LicensePlates} overweight! Cannot add
required goods.");
return;
}

_loadedGoods.AddRange(goods);
}

public void SetDestination(City destination)


{
if (_destination != null)
{
[Link]($"Van {LicensePlates} already have a
destination {_destination.Name}!");
return;
}

_destination = destination;
}
}

public class Truck : IDeliveryVehicle, ITrailerExtendibleVehicle


{
public string Brand { get; set; }
public string Model { get; set; }
public string LicensePlates { get; set; }
public int WeightCapacity { get; set; }

private City _destination { get; set; } = null;


private Trailer _trailerAttached { get; set; } = null;
private List<Good> _loadedGoods { get; set; } = new List<Good>();

public void AttachTrailer(Trailer trailer)


{
if(_trailerAttached != null)
{
[Link]($"Truck {LicensePlates} already have a
trailer!");
return;
}

_trailerAttached = trailer;
}

public void Load(Good[] goods)


{
var totalCapacity = WeightCapacity +
(_trailerAttached?.WeightCapacity ?? 0);
var totalLoad = _loadedGoods.Sum(g => [Link]);
var totalNewLoad = [Link](g => [Link]);

if(totalCapacity < totalLoad + totalNewLoad)


{
[Link]($"Truck {LicensePlates} overweight! Cannot add
required goods.");
return;
}

_loadedGoods.AddRange(goods);
}

public void SetDestination(City destination)


{
if (_destination != null)
{
[Link]($"Truck {LicensePlates} already have a
destination {_destination.Name}!");
return;
}

_destination = destination;
}
}

18.1.5. Dependency Inversion Principle

Zasada Odwróconej Zależności stanowi, aby obiekty klas bardziej ogólnych nie zależały
bezpośrednio od obiektów klas niższego poziomu, lecz od abstrakcji tych obiektów.

W praktyce oznacza to, że gdy funkcjonalność obiektu jednej klasy zależy od funkcjonalności
obiektów innych klas, nie należy tworzyć sztywnego powiązania między nimi poprzez tworzenie
instancji zależności wewnątrz obiektu klasy bardziej ogólnej. Pozwala to na umożliwienie
wyizolowania powiązania między obiektami, testowania w izolacji i podniesienia elastyczności i
podatności na zmiany tworzonego rozwiązania.

Załóżmy, że posiadamy klasę odpowiedzialną za eksport historii transakcji bankowych do


pliku.
public class BankTransfer
{
public DateTime Date { get; set; }
public string RecipientAccountNumber { get; set; }
public string SenderAccountNumber { get; set; }
public decimal Amount { get; set; }
}

public class BankTransferService


{
public void ExportTransfersHistory(DateTime dateFrom, DateTime dateTo,
string accountNumber, string destination)
{
List<BankTransfer> history;

using(var dbContext = new BankTransferDbContext())


{
history = [Link]
.Where(bt =>
[Link] = accountNumber
&& [Link] >= dateFrom
&& [Link] <= dateTo)
.ToList();
}

var data = [Link](transfers);


[Link](data, destination);
}
}

Wyżej przedstawiona klasa jest ściśle zależna od źródła danych, jakim jest baza danych
reprezentowana przez BankTransfersDbContext, od serializacji przy użyciu formatu JSON oraz
pozwala na export jedynie do plików tekstowych. Funkcja ExportTransfersHistory jest z tego
powodu nietestowalna a kod sztywny i niezdolny do rozszerzeń czy podmiany funkcjonalności
(np.: przełączenia źródła danych czy też zmiany formatu serializacji).

W celu zmiany kodu do zgodnego z Zasadą Odwracania Zależności niezbędne jest


wydzielenie reprezentacji wszystkich trzech wyżej wymienionych elementów do osobnych klas,
dodanie warstwy abstrakcji w postaci interfaceu (o ile jeszcze takowy nie istnieje) oraz
wstrzykiwanie zależności z zewnątrz podczas konstrukcji obiektu:

using [Link];
using System;
using [Link];
using [Link];
using [Link];
using [Link];

namespace SolidExamples
{
public class Program
{
static void Main()
{
var bankTransfersService = new BankTransferService(
() => new BankTransfersDataContext(),
new JsonSerializer(),
new FileWrapper());
}
}

public class BankTransfer


{
public DateTime Date { get; set; }
public string RecipientAccountNumber { get; set; }
public string SenderAccountNumber { get; set; }
public decimal Amount { get; set; }
}

public interface IDataExportSource


{
void SaveData(string data, string destination);
}

public class FileWrapper : IDataExportSource


{
public void SaveData(string data, string destination)
{
[Link](data, destination);
}
}

public interface IDataSerializer


{
public string Serialize<T>(T data);
}

public class JsonSerializer : IDataSerializer


{
public string Serialize<T>(T data)
{
return [Link](data);
}
}

public interface IBankTransfersDataContext : IDisposable


{
DbSet<BankTransfer> BankTransfers { get; set; }
}

public class BankTransfersDataContext : DbContext,


IBankTransfersDataContext
{
public BankTransfersDataContext()
: base("ConnectionStringName")
{ }

public DbSet<BankTransfer> BankTransfers { get; set; }


}

public class BankTransferService


{
private Func<IBankTransfersDataContext> _dataSourceFactoryMethod;
private IDataSerializer _dataSerializer;
private IDataExportSource _dataExportSource;

public BankTransferService(
Func<IBankTransfersDataContext> dataSourceFactoryMethod,
IDataSerializer dataSerializer,
IDataExportSource dataExportSource
)
{
_dataSourceFactoryMethod = dataSourceFactoryMethod;
_dataSerializer = dataSerializer;
_dataExportSource = dataExportSource;
}

public void ExportTransfersHistory(DateTime dateFrom, DateTime


dateTo, string accountNumber, string destination)
{
List<BankTransfer> history;

using (var dataSource = _dataSourceFactoryMethod())


{
history = [Link]
.Where(bt =>
[Link] == accountNumber
&& [Link] >= dateFrom
&& [Link] <= dateTo)
.ToList();
}

var data = _dataSerializer.Serialize(history);


_dataExportSource.SaveData(data, destination);
}
}
}

Powyższa implementacja pozwala na podmianę źródła danych, serializatora oraz logiki


zapisu historii transakcji na dowolny spełniający wykorzystane interfacey. Mogą one zostać
również podmienione na mocki w UnitTestach, co pozwoli na testowanie logiki w izolacji.

Ręczne zarządzanie zależnościami przy zachowaniu zgodności z zasadą odwróconej


zależności w przypadku rozrastających się projektów jest trudne i nieefektywne. Z pomocą
przychodzą kontenery Dependency Injection.

18.2. KISS
Keep It Stupid Simple - zasada mówiąca, aby starać się implementować kod w sposób
najprostszy z możliwych. Podczas pracy nad kodem, po napisaniu i sprawdzeniu stworzonej
logiki należy ja przeanalizować pod kątem złożoności kognitywnej (łatwości zrozumienia). Jeżeli
utworzona logika może zostać uproszczona lub zmodyfikowana w sposób podnoszący
zrozumienie, należy takie modyfikacje przeprowadzić przed wprowadzeniem zmian do głównej
gałęzi kodu.

Częstymi sygnałami niezgodności z tą zasadą są:

● Komentarze opisujące sposób działania danego fragmentu logiki,


● Długie metody,
● Długie klasy.

18.3. YAGNI
You Ain’t Gonna Need It - zasada mówiąca, że podczas implementacji funkcjonalności należy
unikać dodawania logiki, która nie jest wymagana od programu w danym momencie. Każdy
niewykorzystywany w logice systemu kod powinien zostać z niego usunięty. Kod taki, nawet
jeżeli nie jest w wykorzystaniu, dalej powinien być przetestowany i utrzymywany. Oznacza to
zbędny koszt podczas pracy z rozwojem i utrzymaniem systemu. Nie testowanie i brak
utrzymywania tego fragmentu z kolei oznacza potencjalny punkt generujący błędy w systemie w
przypadku jego wykorzystania.
Sygnałem niezgodności z tą zasadą jest pisanie funkcjonalności “na zapas”, gdyż wydaje
nam się, że prawdopodobnie przyda się ona w przyszłości. Wymagania, architektura systemu
czy też kod i modele wykorzystywane przez “potencjalnie przydatną w przyszłości”
funkcjonalność może ulec zmianie. Z tego powodu powinna ona zostać utworzona dopiero w
momencie, gdy będzie niezbędna do realizacji dodawanej do systemy funkcjonalności.

18.4. Boy Scout Rule


Jeżeli podczas realizacji naszego zadania widzimy fragment kodu, który wydaje nam się
dziwny, nieprawidłowy, można go uprościć lub nie spełnia konwencji projektowych, zasad SOLID
lub innych uzgodnionych w zespole reguł wytwarzania programowania, a sama zmiana nie jest
skomplikowania i czasochłonna, należy takiej zmiany dokonać.

Takie podejście pozwala na utrzymanie kodu w dobrym stanie poprzez częste niewielkie
zmiany i poprawy jego jakości. To z kolei chroni projekt przed zboczeniem w nieprawidłowe
rozwiązania projektowe i architektoniczne wraz z jego rozwojem, degradacji jakości i
kosztownych, czasochłonnych refactoringów kodu w przyszłości.

19. Dependency Injection Containers


Dependency Injection to jeden ze wzorców projektowych pozwalających na efektywne
zarządzanie zależnościami w zgodnie z Zasadą Odwróconych Zależności z SOLID. Istnieje
wiele bibliotek realizujących zarządzanie wstrzykiwaniem zależności z wykorzystaniem
kontenerów. Idea polega na powiązaniu ze sobą interfaceów z ich konkretnymi implementacjami
i umieszczenie tych powiązań w kontenerze. Następnie, w celu utworzenia instancji obiektu
implementującego wybrany interface, prosimy kontener o jego utworzenie poprzez wskazanie
interfaceu, który nas interesuje. Kontener, korzystając z przekazanych mu definicji, postara się
utworzyć obiekt przypisany do danego interfaceu wraz ze wszystkimi jego zależnościami.

Pełen zbiór zależności wszystkich obiektów podrzędnych tworzonego obiektu nazywamy


drzewem zależności.

W ramach przykładu zaprezentowanie zostanie wykorzystanie funkcjonalności biblioteki


NInject - jednej z kilku popularnych implementacji kontenera Dependency Injection. Za bazę
wykorzystany zostanie kod z sekcji o Dependency Inversion Principle w nieco bardziej
rozbudowanej formie:

using [Link];
using System;
using [Link];
using [Link];
using [Link];
using [Link];

namespace SolidExamples
{
public class Program
{
static void Main()
{
var program = new Program(
new BankDataExportService(
new BankTransfersService(
() => new BankTransfersDataContext()),
new JsonSerializer(),
new FileWrapper())
);

[Link]();
}

private IBankDataExportService _bankDataExportService;

public Program(IBankDataExportService bankDataExportService)


{
_bankDataExportService = bankDataExportService;
}

public void Run()


{
//TODO Do stuff
}
}

public class BankTransfer


{
public DateTime Date { get; set; }
public string RecipientAccountNumber { get; set; }
public string SenderAccountNumber { get; set; }
public decimal Amount { get; set; }
}

public interface IDataExportSource


{
void SaveData(string data, string destination);
}

public class FileWrapper : IDataExportSource


{
public void SaveData(string data, string destination)
{
[Link](data, destination);
}
}

public interface IDataSerializer


{
public string Serialize<T>(T data);
}

public class JsonSerializer : IDataSerializer


{
public string Serialize<T>(T data)
{
return [Link](data);
}
}

public interface IBankTransfersDataContext : IDisposable


{
DbSet<BankTransfer> BankTransfers { get; set; }
}

public class BankTransfersDataContext : DbContext,


IBankTransfersDataContext
{
public BankTransfersDataContext()
: base("ConnectionStringName")
{ }

public DbSet<BankTransfer> BankTransfers { get; set; }


}

public interface IBankTransfersService


{
List<BankTransfer> GetTransactionsHistory(DateTime dateFrom,
DateTime dateTo, string accountNumber);
}

public class BankTransfersService : IBankTransfersService


{
private Func<IBankTransfersDataContext> _dataSourceFactoryMethod;

public BankTransfersService(Func<IBankTransfersDataContext>
dataSourceFactoryMethod)
{
_dataSourceFactoryMethod = dataSourceFactoryMethod;
}

public List<BankTransfer> GetTransactionsHistory(DateTime dateFrom,


DateTime dateTo, string accountNumber)
{
List<BankTransfer> history;

using (var dataSource = _dataSourceFactoryMethod())


{
history = [Link]
.Where(bt =>
[Link] == accountNumber
&& [Link] >= dateFrom
&& [Link] <= dateTo)
.ToList();
}

return history;
}
}

public interface IBankDataExportService


{
void ExportTransfersHistory(DateTime dateFrom, DateTime dateTo,
string accountNumber, string destination);
}

public class BankDataExportService : IBankDataExportService


{
private IBankTransfersService _dataSourceFactoryMethod;
private IDataSerializer _dataSerializer;
private IDataExportSource _dataExportSource;

public BankDataExportService(
IBankTransfersService dataSourceFactoryMethod,
IDataSerializer dataSerializer,
IDataExportSource dataExportSource)
{
_dataSourceFactoryMethod = dataSourceFactoryMethod;
_dataSerializer = dataSerializer;
_dataExportSource = dataExportSource;
}

public void ExportTransfersHistory(DateTime dateFrom, DateTime


dateTo, string accountNumber, string destination)
{
var history =
_dataSourceFactoryMethod.GetTransactionsHistory(dateFrom, dateTo,
accountNumber);
var data = _dataSerializer.Serialize(history);
_dataExportSource.SaveData(data, destination);
}
}
}

Analizując konstruktory klas z powyższego kodu, drzewo zależności wygląda w nim


następująco:

Podstawowa zauważalna wada tego kodu do utworzenia obiektu klasy Program. Jego
konstruktor wymaga ręcznego definiowania wszystkich zależności całego drzewa. Z pomocą
przychodzi NInjec (click). Pozwala on na tworzenie obiektu Kernela. Obiekt ten zawiera definicję
wszystkich powiązań między interfaceami a konkretnymi ich implementacjami.

Dodatkowo, w naszym przypadku kontekst bazy danych podawany jest zgodnie ze wzorcem
FactoryMethod. Aby dodać bibliotece NInject wsparcie dla takiego podejścia, należy ściągnąć
paczkę rozszerzeń o nazwie [Link] (click).

W celu zdefiniowana Kernela przeważnie tworzy się osobną klasę, w której tworzy się
definicje powiązań między interface’ami oraz przechowuje obiekt Kernela. Definicję tworzy się
przy użyciu metod generycznych Bind().To() . Metoda Bind służy do wskazania interface’u, a
metoda To wskazuje na konkretną jego implementację, która zostanie do niego powiązana.

public class DiConfig


{
private IKernel _kernel;

public IKernel GetKernel()


{
if(_kernel != null)
{
return _kernel;
}

_kernel = new StandardKernel();

_kernel.Bind<IProgram>().To<Program>();
_kernel.Bind<IBankDataExportService>().To<BankDataExportService>();
_kernel.Bind<IBankTransfersService>().To<BankTransfersService>();
_kernel.Bind<IDataSerializer>().To<JsonSerializer>();
_kernel.Bind<IDataExportSource>().To<FileWrapper>();
_kernel.Bind<IBankTransfersDbContext>().ToMethod(x => new
BankTransfersDbContext());

return _kernel;
}
}

Warto zwrócić uwagę na powiązanie dla kontekstu bazy danych - wykorzystuje on metodę
rozszerzającą ToMethod() pochodzącą z biblioteki [Link].

W celu wykorzystania utworzonych definicji należy utworzyć obiekt kernela poprzez


wywołanie metody GetKernel, a następnie wywołać na nim metodę Get. Metoda Get jest metodą
generyczną. Podajemy w niej interface, którego implementację chcemy utworzyć. INinject,
tworząc wskazaną implementację, sprawdzi zależności przez nią wymagane. Następnie
poszuka definicji każdej z nich. Gdy definicja zostanie znaleziona, NInject sprawdzi jej
zależności i ponownie przeszuka definicje. W ten sposób rozwijane jest całe drzewo
zależności.

static void Main()


{
var kernel = new DiConfig().GetKernel();
var program = [Link]<IProgram>();
[Link]();
}

W momencie rozwiązywania zależności, każdy obiekt wykorzystujący daną zależność


otrzyma własną jego instancję. Aby utworzyć dokładnie jeden obiekt danego typu (singleton),
należy po metodzie To() należy użyć metody InSingletonScope(), np.:

_kernel.Bind<IDataSerializer>().To<JsonSerializer>().InSingletonScope();

Pełen kod rozwiązania po dodaniu kontenera NInject:

using [Link];
using Ninject;
using System;
using [Link];
using [Link];
using [Link];
using [Link];

namespace SolidExamples
{
public interface IProgram
{
void Run();
}

public class Program : IProgram


{
static void Main()
{
var kernel = new DiConfig().GetKernel();
var program = [Link]<IProgram>();

[Link]();
}

private IBankDataExportService _bankDataExportService;

public Program(IBankDataExportService bankDataExportService)


{
_bankDataExportService = bankDataExportService;
}
public void Run()
{
//TODO Do stuff
}
}

public class DiConfig


{
private IKernel _kernel;

public IKernel GetKernel()


{
if (_kernel != null)
{
return _kernel;
}

_kernel = new StandardKernel();

_kernel.Bind<IProgram>().To<Program>();

_kernel.Bind<IBankDataExportService>().To<BankDataExportService>();

_kernel.Bind<IBankTransfersService>().To<BankTransfersService>();
_kernel.Bind<IDataSerializer>().To<JsonSerializer>();
_kernel.Bind<IDataExportSource>().To<FileWrapper>();
_kernel.Bind<IBankTransfersDbContext>().ToMethod(x => new
BankTransfersDbContext());

return _kernel;
}
}

public class BankTransfer


{
public DateTime Date { get; set; }
public string RecipientAccountNumber { get; set; }
public string SenderAccountNumber { get; set; }
public decimal Amount { get; set; }
}

public interface IDataExportSource


{
void SaveData(string data, string destination);
}

public class FileWrapper : IDataExportSource


{
public void SaveData(string data, string destination)
{
[Link](data, destination);
}
}

public interface IDataSerializer


{
public string Serialize<T>(T data);
}

public class JsonSerializer : IDataSerializer


{
public string Serialize<T>(T data)
{
return [Link](data);
}
}

public interface IBankTransfersDbContext : IDisposable


{
DbSet<BankTransfer> BankTransfers { get; set; }
}

public class BankTransfersDbContext : DbContext, IBankTransfersDbContext


{
public BankTransfersDbContext()
: base("ConnectionStringName")
{ }

public DbSet<BankTransfer> BankTransfers { get; set; }


}

public interface IBankTransfersService


{
List<BankTransfer> GetTransactionsHistory(DateTime dateFrom,
DateTime dateTo, string accountNumber);
}

public class BankTransfersService : IBankTransfersService


{
private Func<IBankTransfersDbContext> _dataSourceFactoryMethod;

public BankTransfersService(Func<IBankTransfersDbContext>
dataSourceFactoryMethod)
{
_dataSourceFactoryMethod = dataSourceFactoryMethod;
}

public List<BankTransfer> GetTransactionsHistory(DateTime dateFrom,


DateTime dateTo, string accountNumber)
{
List<BankTransfer> history;

using (var dataSource = _dataSourceFactoryMethod())


{
history = [Link]
.Where(bt =>
[Link] == accountNumber
&& [Link] >= dateFrom
&& [Link] <= dateTo)
.ToList();
}

return history;
}
}

public interface IBankDataExportService


{
void ExportTransfersHistory(DateTime dateFrom, DateTime dateTo,
string accountNumber, string destination);
}

public class BankDataExportService : IBankDataExportService


{
private IBankTransfersService _dataSourceFactoryMethod;
private IDataSerializer _dataSerializer;
private IDataExportSource _dataExportSource;

public BankDataExportService(
IBankTransfersService dataSourceFactoryMethod,
IDataSerializer dataSerializer,
IDataExportSource dataExportSource)
{
_dataSourceFactoryMethod = dataSourceFactoryMethod;
_dataSerializer = dataSerializer;
_dataExportSource = dataExportSource;
}

public void ExportTransfersHistory(DateTime dateFrom, DateTime


dateTo, string accountNumber, string destination)
{
var history =
_dataSourceFactoryMethod.GetTransactionsHistory(dateFrom, dateTo,
accountNumber);
var data = _dataSerializer.Serialize(history);
_dataExportSource.SaveData(data, destination);
}
}
}

20. Wzorce projektowe


Wzorce projektowe to opisy rozwiązań popularnych problemów architektonicznych (w różnej
skali), na które natrafia się podczas tworzenia, rozwoju i utrzymania systemów komputerowych.
Sama idea wzorców zapożyczona została z branży budowlanej.

Wzorzec sam w sobie nie stanowi rozwiązania. Jego implementacje mogą być (a nawet
powinny) do spełnienia wymagań stawianych przed nim w konkretnym projekcie i przy
konkretnym problemie. Jest on ogólnym opisem mówiącym, w jaki sposób podejść do realizacji i
jakie relacje między obiektami różnych klas należy utworzyć, aby rozwiązać dany problem.

Najbardziej podstawowym zbiorem wzorców jest tak zwany zbiór Wzorców projektowych
Gangu Czworga (Gang of Four Design Patterns). Nazwa wzięła się od twórców tego zbioru -
grupy czterech programistów, w skład której wchodzą: Erich Gamma, Richard Helm, Ralph
Johnson and John Vlissides. Zbiór składa się z 23 wzorców podzielonych na 3 kategorie: .
Informacje o każdej z kategorii oraz o każdym ze wzorców wchodzących w skład zbioru można
znaleźć tutaj - click.

21. Testy jednostkowe z .NET


Testy jednostkowe to najniższy poziomowo rodzaj testów przeprowadzanych na systemach
komputerowych. Testy te testują zachowania pojedynczych funkcjonalności obiektów klas.
Zadaniem testów jednostkowych jest wykrywanie zmian zachować logiki zdefiniowanej w
klasach. Prawidłowy test jednostkowy jednoznacznie wskazuje przypadek testowy oraz
funkcjonalność, której logika nie spełnia założonych wymagań. Aby było to możliwe, klasa musi
być testowana w izolacji. Innymi słowy, błędy zawarte w definicjach jednych klas nie powinny
wpływać na wyniki testów innych.

Dodatkowo, czas wykonania Unit Testów powinien być możliwie jak najkrótszy. Dzięki temu
Unit Testy można uruchomić często i udoskonalać je na bieżąco.

Podstawową zaletą UnitTestów jest wczesne wykrywanie błędów (do znacząco redukuje
koszt ich naprawy) oraz podnoszą pewność siebie zespołu podczas wprowadzania zmian w
istniejących funkcjonalnościach. Każda zmiana zachowania testowanej klasy zostanie
wykazana nieprawidłowym zakończeniem testu, a developer samodzielnie przeanalizuje sprawę
i stwierdzi, czy to test jest już nieaktualny i wymaga modyfikacji, czy może przypadkiem
wprowadził niepożądaną zmianę, która może wpłynąć negatywnie na inne funkcjonalności
systemu. UnitTesty zwyczajowo uruchamia się w Continuous Integration (CI), takich jak
TeamCity czy Bamboo, od razu po zbudowaniu kodu, a ich niepowodzenie jest tożsame z
nieudanym buildem systemu.

Testy jednostkowe, ze względu na ich zadanie, nie wykryją nieprawidłowych relacji między
obiektami czy ich nieprawidłowej współpracy. Takie aspekty pokrywane są przy pomocy testów
integracyjnych (które również można pisać przy użyciu omawianych w tym rozdziale narzędzi).

Testy integracyjne z kolei nie sprawdzają zgodności oprogramowania z wymaganiami. Za to


odpowiedzialne są testy akceptacyjne (inaczej również testy końcowe, End-to-End lub E2E).

W tym rozdziale poruszane będzie jedynie tworzenie testów jednostkowych.

Są dwa podejścia do pisania testów jednostkowych:

● Pisanie testów po stworzeniu funkcjonalności,


● Pisanie testów przed stworzeniem funkcjonalności (TDD - Test Driven Development).

Ze względu na prowadzenie dydaktyki testowanie zostanie omówione na podstawie już


istniejącego kodu, jednak próby wdrażania TDD w projektach jest bardzo modne i zachęcam do
zapoznania się z tą techniką (click).

21.1. Biblioteka testowa NUnit


Biblioteka NUnit to jedna z bibliotek wspierających implementację testów jednstkowych.
Załóżmy, że posiadamy następującą klasę do przetestowania:

using System;

namespace [Link]
{
public class Person
{
public string Name { get; set; }
public string Surname { get; set; }
public string PersonalNo { get; set; }
public bool CanGetLoan { get; set; }
}

public class LoanCalculationResult


{
public bool CanBeTaken { get; set; }
public decimal TotalCost { get; set; }
}

public class LoanService


{
public LoanCalculationResult CalculateLoanCosts(Person requestor,
decimal requestedAmount)
{
if (requestor == null || requestedAmount < 0)
{
throw new ArgumentException("Invalid argument provided. Loan
cannot be calculated.");
}

if (![Link])
{
return new LoanCalculationResult
{
CanBeTaken = false,
TotalCost = 0.0m
};
}

return new LoanCalculationResult


{
CanBeTaken = true,
TotalCost = requestedAmount * 1.15m
};
}
}
}

Klasa LoanService posiada jedną metodę - CalculateLoanCosts. To ona zostanie poddana


testowaniu. Klasy LoanCalculationResult oraz Person to modele danych - jako, że nie posiadają
żadnej logiki, nie podlegają testowaniu.

Analizując metodę CalculateLoanCosts widzimy w niej trzy przypadki testowe:

1. Gdy podana wartość pożyczki jest zbyt duża lub zbyt mała albo użytkownik jest nullem -
funkcja powinna wyrzucić wyjątek,
2. Gdy wnioskodawca nie może uzyskać kredytu - funkcja powinna zwrócić odmowę,
3. Gdy wnioskodawca może uzyskać kredyt - funkcja powinna zwrócić zgodę i koszt
kredytu w wysokości 115% wnioskowanej kwoty.
UnitTesty pisze się w oddzielnym projekcie. Visual Studio 2019 posiada nawet template o
nazwie NUnit Test Projekt (.NET Core):

Zasadniczo jest to zwyczajny projekt biblioteki klas, posiada jednak od razu dodane referencje
do wymaganych bibliotek NuGet:

● NUnit - biblioteka pozwalająca na zdefiniowanie UnitTestów,


● NUnit3TestAdapter - biblioteka pozwalająca na uruchamianie testów NUnit z poziomu
Visual Studio.

Zwyczajowo projekt testowy nazywa się tak samo jak projekt testowany, dodaje się jednak na
końcu postfix .Tests. W naszym przypadku będzie to więc projekt [Link] .

Technicznie, uruchamianie testów poprzez NUnit polega na zbudowaniu biblioteki z


definicjami testów. Biblioteka ta następnie konsumowana jest przez NUnit3TestAdapter (z
poziomu Visual Studio, w sposób niewidoczny dla developera). Adapter wyszukuje w bibliotece
definicje testów i uruchamia je. Wyniki testów wyświetlane są w specjalnie przeznaczonym do
tego oknie w Visual Studio - TestExplorer. Okno można otworzyć poprzez menu Test:
NOTE: Testy NUnit można uruchamiać również poprzez aplikację konsolową NUnit Console
Runner - wykorzystuje się to choćby w BuildAutomation w systemach CI.

Aby NUnit3TestAdaper czy NUnit Console Runner były w stanie odnaleźć klasy i przypadki
testowe, należy opatrzyć je odpowiednimi atrybutami.

Klasę z testami oznacza się atrybutem [TestFixture]. Zwyczajowo klasa testowa nazywa się
tak samo jak klasa testowana i posiada na końcu dopisek Tests. W zaprezentowanym
przykładzie będzie to więc LoanServiceTests.

W najprostszym wydaniu, każda metoda testowa oznaczana jest atrybutem [Test] i jest ona
publiczną metodą typu void bez parametrów. Nazwa metody zwyczajowo składa się z 3
segmentów oddzielonych podkreślnikiem. Pierwszy informuje o testowanej funkcjonalności,
drugi o przypadku testowym, a trzeci o spodziewanym wyniku. Poniżej przykład klasy testowej z
pustą metodą testową:

using [Link];

namespace [Link]
{
[TestFixture]
public class LoanServiceTests
{
[Test]
public void SomeFeature_SomeCase_ExpectedResult()
{
//TODO Test stuff
}
}
}
Na początek spróbujmy zaimplementować przypadki testowe 2 i 3.

Przypadek drugi to sytuacja w której użytkownik nie posiada zdolności kredytowej. Aby
przeprowadzić test tego przypadku, musimy:

● zdefiniować takiego użytkownika,


● wysłać zapytanie o kalkulację do testowanej metody,
● sprawdzić uzyskany wynik.

UnitTesty piszę się zgodnie z zasadą AAA:

● Arrange - tworzenie obiektów i definiowane danych niezbędnych do przeprowadzenia


testów,
● Act - przeprowadzenie testu (wywołanie testowanej metody),
● Asset - sprawdzenie zgodności wyników z oczekiwaniami.

Istotnym jest, aby wszelkie dane były zapisane “na sztywno” - wprowadzanie kalkulacji do
testów wprowadza ryzyko błędów implementacji a to oznacza, że musielibyśmy testować
własne testy. A tego na pewno nie chcemy :)

Zgodnie z powyższą analizą, test dla przypadku dwa będzie wyglądał następująco:

[Test]
public void LoanCalculation_LoanNotAllowedForUser_Rejection()
{
//Arrange
var user = new Person
{
CanGetLoan = false
};

var sut = new LoanService();

//Act
var result = [Link](user, 1000);

//Assert
[Link]([Link]);
[Link](0.0m, [Link]);
}

Jak widać po załączonym kodzie, do testu przekazujemy tylko minimalną, niezbędna do jego
przeprowadzenia ilość informacji.
W celu sprawdzenia zgodności wyników z oczekiwaniami używamy statycznej klasy Assert.
Posiada ona zdefiniowany zestaw funkcji pozwalających na sprawdzanie danych w różnych
wariantach, a niezgodności sygnalizowane są w błędach w stosunkowo jasnej formie.

Po przebudowaniu projektu, test pojawi się w oknie TestExplorer:

Test można uruchomić przy pomocy strzałek znajdujących się w lewym górnym rogu okna.
Po ich uruchomieniu test powinien zostać oznaczony na zielono.

Analogicznie realizujemy test dla przypadku trzeciego. Tym razem intencjonalnie


wprowadzimy jednak błąd:

[Test]
public void LoanCalculation_LoanAllowedForUser_Acceptation()
{
//Arrange
var user = new Person
{
CanGetLoan = true
};

var sut = new LoanService();

//Act
var result = [Link](user, 1000);

//Assert
[Link]([Link]);
[Link](1100.0m, [Link]); //Should be 1150
}

Po uruchomieniu test zostanie oznaczony jako nieudany. W NUnit wiadomości w wynikach


testów ujrzymy informację o niezgodności:

W tym konkretnym przypadku błąd jest oczywisty, jednak czasami wymagane jest
przedebuggowanie błędu. W tym celu należy postawi breakpoint w obrębie implementacji testu i
prawym przyciskiem myszy wybrać na teście opcję Debug. W ten sposób po kodzie testu
można poruszać się w sposób identyczny jak po kodzie rozwijanej aplikacji.

Do przetestowania został jeszcze przypadek pierwszy. Występuje on w dwóch przypadkach:

● Gdy wartość kredytu jest mniejsza od zera lub większa od miliona,


● Gdy dane użytkownika są null.

W celu efektywnego zaimplementowania tego przypadku skorzystamy z atrybutu [TestCase].


Pozwala on na zdefiniowanie różnych danych wejściowych dla jednej implementacji testu.
Podaje się je wewnątrz atrybutu w nawiasach. Te powinny znaleźć swoje odzwierciedlenie w
parametrach metody definiującej test. Niestety - atrybut [TestCase] akceptuje jedynie stałe czasu
kompilacji. Oznacza to, że testowanie przekazywania null jako użytkownika będzie musiało
zostać przekazane do osobnego testu.
NOTE: W celu uwspólnienia ciała testu również dla zmiennych danych typu referencyjnego,
można wykorzystać atrybut [TestCaseSource] (click). Nie będzie on jednak tutaj opisywany.

Poniżej przykład implementacji z użyciem tego atrybutu dla pierwszego przypadku


testowego:

[TestCase(-1)]
[TestCase(1000001)]
public void LoanCalculation_InvalidLoanValue_Exception(decimal loanSize)
{
//Arrange
var user = new Person
{
CanGetLoan = true
};

var sut = new LoanService();

//Act
var result = [Link](user, loanSize);

//Assert
//??
}

Na drzewie testów w TestExplorer każdy TestCase widoczny jest jako osobny test:

Pytaniem pozostaje jak sprawdzić, czy test wyrzuca wyjątek. Możnaby w tym celu
wykorzystać konstrukcję Try-Catch oraz metody [Link]() i [Link](), NUnit posiada
jednak wbudowany mechanizm sprawdzania wystąpienia spodziewanego wyjątku. Jest nim
generyczna metoda [Link]<>(). Pozwala ona sprawdzić, czy kod wyrzucił dokładnie
taki typ wyjątku, jakiego od niego oczekujemy:

[TestCase(-1)]
[TestCase(1000001)]
public void LoanCalculation_InvalidLoanValue_Exception(decimal loanSize)
{
//Arrange
var user = new Person
{
CanGetLoan = true
};

var sut = new LoanService();

//Act + Assert
[Link]<ArgumentException>(() => [Link](user,
loanSize));
}

Podczas implementacji testów dla jednej klasy często sekcja Assert jest taka sama lub
bardzo podobna dla każdego z nich. Aby nie powielać jej wielokrotnie, można stworzyć metodę
inicjalizującą oznaczoną atrybutem [SetUp]. Taka metoda zostanie uruchomiona przez każdym
z testów i używa się jej do inicjalizacji części wspólnej przypadków testowych. W przypadku
testów dla klasy LoanService mogłaby ona przyjąć następujący kształt:

[TestFixture]
public class LoanServiceTests
{
private Person _userNotAllowedToTakeLoan;
private Person _userAllowedToTakeLoan;
private LoanService _sut;

[SetUp]
public void SetUp()
{
_userNotAllowedToTakeLoan = new Person
{
CanGetLoan = false
};

_userAllowedToTakeLoan = new Person


{
CanGetLoan = true
};

_sut = new LoanService();


}

//Tests
}
W przypadku, gdy chcemy uruchomić jakieś zachowanie po każdym teście (nie jest to częste
w UnitTestach, ale w testach integracyjnych już tak), można stworzyć odpowiednią metodę i
oznaczyć ją atrybutem [TearDown]. Wtedy zostanie ona wywołana po zakończeniu każdego z
testów.

Poniżej implementacja wszystkich omawianych przypadków testowych dla metody


CalculateLoanCosts omawiane w tym rozdziale:

using [Link];
using System;
using [Link];

namespace [Link]
{
[TestFixture]
public class LoanServiceTests
{
private Person _userNotAllowedToTakeLoan;
private Person _userAllowedToTakeLoan;
private LoanService _sut;

[SetUp]
public void SetUp()
{
_userNotAllowedToTakeLoan = new Person
{
CanGetLoan = false
};

_userAllowedToTakeLoan = new Person


{
CanGetLoan = true
};

_sut = new LoanService();


}

[Test]
public void LoanCalculation_LoanNotAllowedForUser_Rejection()
{
//Act
var result = _sut.CalculateLoanCosts(_userNotAllowedToTakeLoan,
1000);

//Assert
[Link]([Link]);
[Link](0.0m, [Link]);
}

[Test]
public void LoanCalculation_LoanAllowedForUser_Acceptation()
{
//Act
var result = _sut.CalculateLoanCosts(_userAllowedToTakeLoan,
1000);

//Assert
[Link]([Link]);
[Link](1150.0m, [Link]);
}

[TestCase(-1)]
[TestCase(1000001)]
public void LoanCalculation_InvalidLoanValue_Exception(decimal
loanSize)
{
//Act + Assert
[Link]<ArgumentException>(() =>
_sut.CalculateLoanCosts(_userAllowedToTakeLoan, loanSize));
}

[Test]
public void LoanCalculation_UserIsNull_Exception()
{
//Act + Assert
[Link]<ArgumentException>(() =>
_sut.CalculateLoanCosts(null, 1000));
}
}
}

21.2. Testowanie w izolacji - Moq


Na potrzeby tego rozdziału zmodyfikujemy nieco kod klasy LoanService oraz Person. Od
teraz to klasa LoanService będzie odpowiedzialna za sprawdzenie, czy dany użytkownik
posiada zdolność kredytową. W tym celu będzie komunikował się z nową klasą -
LoanAllowanceService. Będzie ona odpowiedzialna za oszacowanie zdolności kredytowej
każdego z przekazanych jej do analizy użytkowników. Klasa LoanAllowanceService będzie
jednak posiadała błąd - dla każdego możliwego zapytania będzie wyrzucała wyjątek.
public class Person
{
public string Name { get; set; }
public string Surname { get; set; }
public string PersonalNo { get; set; }
}

public class LoanCalculationResult


{
public bool CanBeTaken { get; set; }
public decimal TotalCost { get; set; }
}

public class LoanService


{
private LoanAllowanceService _loanAllowanceService;

public LoanService()
{
_loanAllowanceService = new LoanAllowanceService();
}

public LoanCalculationResult CalculateLoanCosts(Person requestor,


decimal requestedAmount)
{
if (requestor == null || requestedAmount < 0 || requestedAmount >
1000000)
{
throw new ArgumentException("Invalid argument provided. Loan
cannot be calculated.");
}

var canGetLoan =
_loanAllowanceService.IsPersonAllowedToGetLoan(requestor);

return new LoanCalculationResult


{
CanBeTaken = canGetLoan,
TotalCost = canGetLoan
? requestedAmount * 1.15m
: 0.0m
};
}
}

public class LoanAllowanceService


{
public bool IsPersonAllowedToGetLoan(Person requestor)
{
throw new Exception("Some random issue");
}
}

Jako, że funkcjonalność LoanService została bez zmian (uległ jedynie sposób jej realizacji),
zestaw przypadków testowych pozostał bez zmian. Jako przykład zaprezentowany zostanie
przypadek trzeci - gdy użytkownik jest uprawniony do otrzymania kredytu. Gdy jednak
spróbujemy zaadaptować poprzednio napisany test natrafimy na dwie przeszkody:

● Skoro klasa Person nie posiada już pola CanGetLoan, w jaki sposób pokierować testem,
aby zachował się tak, jak w przypadku użytkownika uprawnionego do wzięcia kredytu?
● Każde wywołanie metody CalculateLoanCosts() zakończy się wyrzuceniem wyjątku,
gdyż metoda ta jest zależna od [Link]();

Pokonać oba problemy pomoże nam biblioteka Moq. Moq to biblioteka pozwalająca na
tworzenie tzw. Mocków - obiektów implementujących dowolny wybrany interface, którego
zachowanie dostosowujemy do własnych potrzeb. Aby móc skorzystać z Moq musimy dodać
do naszego projektu testowego bibliotekę Moq z NuGet (click) . Dodatkowo niezbędne będą
modyfikacje kodu klas LoanService oraz LoanAllowanceService. W tym momencie są one ze
sobą ściśle powiązane. Należy dodać pomiędzy nimi warstwę abstrakcji w postaci interface’u:

public class LoanService


{
private ILoanAllowanceService _loanAllowanceService;

public LoanService(ILoanAllowanceService loanAllowanceService)


{
_loanAllowanceService = loanAllowanceService;
}

public LoanCalculationResult CalculateLoanCosts(Person requestor,


decimal requestedAmount)
{
if (requestor == null || requestedAmount < 0 || requestedAmount >
1000000)
{
throw new ArgumentException("Invalid argument provided. Loan
cannot be calculated.");
}

var canGetLoan =
_loanAllowanceService.IsPersonAllowedToGetLoan(requestor);
return new LoanCalculationResult
{
CanBeTaken = canGetLoan,
TotalCost = canGetLoan
? requestedAmount * 1.15m
: 0.0m
};
}
}

public interface ILoanAllowanceService


{
bool IsPersonAllowedToGetLoan(Person requestor);
}

public class LoanAllowanceService : ILoanAllowanceService


{
public bool IsPersonAllowedToGetLoan(Person requestor)
{
throw new Exception("Some random issue");
}
}

Dzięki takiej modyfikacji klasa LoanService zaakceptuje jako swoją zależność dowolny obiekt
implementujący interface ILoanAllowanceService. W przypadku samej aplikacji będzie to
instancja klasy LoanAllowanceService, wstrzykiwana np.: poprzez NInject. W przypadku projektu
z UnitTestami, będzie to mock stworzony z użyciem biblioteki Moq.

W celu utworzenia Mocka należy skorzystać z klasy o identycznej nazwie z przestrzeni


biblioteki Moq. Jest to klasa generyczna, która jako typ przyjmuje interface obiektu, który będzie
symulować.

private Mock<ILoanAllowanceService> _loanAllowanceServiceMock;


private LoanService _sut;

[SetUp]
public void SetUp()
{
_loanAllowanceServiceMock = new Mock<ILoanAllowanceService>();

_sut = new LoanService(_loanAllowanceServiceMock.Object);


}
NOTE: Należy pamiętać, że Mock to nie od razu obiekt, którego spodziewa się testowana klasa.
Znajduje się on dopiero we własności Object obiektu typu Mock.

W następnej kolejności musimy zdefiniować spodziewane zachowanie mockowanego


obiektu. Można to zrobić na dowolnym poziomie - przed wstrzyknięciem obiektu do instancji
testowanej klasy jak i po. Można również modyfikować zachowanie raz zdefiniowanej metody.

W naszym przypadku do zamockowania jest tylko jedna metoda interface’u


ILoanAllowanceService. Tak jak w przypadku danych testowych - mockujemy tylko te metody,
których użycia spodziewamy się podczas testu. W celu zamockowania metody używamy
metod Setup i Returns (jeżeli metoda nie jest typu void):

[Test]
public void LoanCalculation_LoanAllowedForUser_Acceptation()
{
//Arrange
var person = new Person();

_loanAllowanceServiceMock
.Setup(x => [Link](person))
.Returns(true);

//Act
var result = _sut.CalculateLoanCosts(person, 1000);

//Assert
[Link]([Link]);
[Link](1150.0m, [Link]);
}

Powyższa definicja mówi, że dla osoby person metoda IsPersonAllowedToGetLoan wywołana


na mocku stworzonym przez _loanAllowanceServiceMock zwróci true.

Warto zaznaczyć, że Mock dopasowywuje definicje metody poprzez porównanie argumentów


(poprzez standardową metodę Equals, którą każdy obiekt dziedziczy z klasy object). Dzięki
temu można mieć wiele definicji tej samej metody, a właściwa zostanie wybrana bazująć na
przekazanych do niej argumentach. Potencjalne wykorzystanie tej możliwości może wyglądać w
przypadku naszego kodu następująco:

private Person _personNotAllowedToGetLoan;


private Person _personAllowedToGetLoan;
private Mock<ILoanAllowanceService> _loanAllowanceServiceMock;
private LoanService _sut;

[SetUp]
public void SetUp()
{
_personNotAllowedToGetLoan = new Person();
_personAllowedToGetLoan = new Person();

_loanAllowanceServiceMock = new Mock<ILoanAllowanceService>();

_loanAllowanceServiceMock
.Setup(x => [Link](_personNotAllowedToGetLoan))
.Returns(true);

_loanAllowanceServiceMock
.Setup(x => [Link](_personNotAllowedToGetLoan ))
.Returns(false);

_sut = new LoanService(_loanAllowanceServiceMock.Object);


}

[Test]
public void LoanCalculation_LoanNotAllowedForUser_Rejection()
{
//Arrange
var result = _sut.CalculateLoanCosts(_personNotAllowedToGetLoan, 1000);

//Assert
[Link]([Link]);
[Link](0.0m, [Link]);
}

[Test]
public void LoanCalculation_LoanAllowedForUser_Acceptation()
{
//Act
var result = _sut.CalculateLoanCosts(_personAllowedToGetLoan, 1000);

//Assert
[Link]([Link]);
[Link](1150.0m, [Link]);
}

Nie zawsze jednak dokładne sprawdzanie wartości podczas wyboru definicji metody jest
pożądane. Możemy testować np.: metodę, która sama wewnętrznie tworzy obiekt, który
przekazuje do zależności i nie możemy wcześniej poznać jego referencji czy wartości. W takiej
sytuacji pomocna jest klasa statyczna It, która pozwala na bardziej ogólne klasyfikowanie
parametrów. W przypadku naszych testów można zdefiniować metody odpowiednio dla
każdego z przypadków, a zwracany wynik uniezależnić od konkretnego wpadającego jako
parametr obiektu.

[Test]
public void LoanCalculation_LoanNotAllowedForUser_Rejection()
{
//Arrange
_loanAllowanceServiceMock
.Setup(x => [Link]([Link]<Person>()))
.Returns(true);

//Arrange
var result = _sut.CalculateLoanCosts(new Person(), 1000);

//Assert
[Link]([Link]);
[Link](0.0m, [Link]);
}

Klasa It posiada wiele przydatnych funkcji, takich jak sprawdzanie nulli, wartości w
wyznaczonym zakresie czy wyrażeń regularnych. Więcej z nich można znaleźć tutaj (click).

Przykład pełnych testów LoanService z wykorzystaniem Moq:

using System;
using Moq;
using [Link];
using [Link];

namespace [Link]
{
[TestFixture]
public class LoanServiceTests
{
private Mock<ILoanAllowanceService> _loanAllowanceServiceMock;
private LoanService _sut;

[SetUp]
public void SetUp()
{
_loanAllowanceServiceMock = new Mock<ILoanAllowanceService>();
_loanAllowanceServiceMock
.Setup(x => [Link]([Link]<Person>()))
.Returns(true);

_sut = new LoanService(_loanAllowanceServiceMock.Object);


}

[Test]
public void LoanCalculation_LoanNotAllowedForUser_Rejection()
{
_loanAllowanceServiceMock
.Setup(x => [Link]([Link]<Person>()))
.Returns(false);

var result = _sut.CalculateLoanCosts(new Person(), 1000);

[Link]([Link]);
[Link](0.0m, [Link]);
}

[Test]
public void LoanCalculation_LoanAllowedForUser_Acceptation()
{
_loanAllowanceServiceMock
.Setup(x => [Link]([Link]<Person>()))
.Returns(true);

var result = _sut.CalculateLoanCosts(new Person(), 1000);

[Link]([Link]);
[Link](1150.0m, [Link]);
}

[TestCase(-1)]
[TestCase(1000001)]
public void LoanCalculation_InvalidLoanValue_Exception(decimal
loanSize)
{
[Link]<ArgumentException>(() =>
_sut.CalculateLoanCosts(new Person(), loanSize));
}

[Test]
public void LoanCalculation_UserIsNull_Exception()
{
[Link]<ArgumentException>(() =>
_sut.CalculateLoanCosts(null, 1000));
}
}
}

21.3. Izolowanie kodu od zależności statycznych


Często w kodzie wykorzystywane są zależności statyczne. Do najczęstszych z nich należy
klasa Console albo DateTime. Załóżmy, że posiadamy klasę posiadającą jedną metodę, która
wykrywa, czy mamy dzisiaj piątek:

using System;

namespace [Link]
{
public class FridayDetectionService
{
public bool IsFridayToday()
{
return [Link] == [Link];
}
}
}

Przetestowanie jej w powyższej formie jest niemożliwe. Nie jesteśmy w stanie w żaden
sposób wpłynąć na klasę DateTime, aby zwróciła nam datę dla wybranego przez nas dnia
tygodnia. Z definicji klasy posiadające zależności statyczne są nietestowalne z punktu widzenia
testów jednostkowych. W celu umożliwienia testowania tworzy się adaptery - klasy niestatyczne
udostępniające statyczną funkcjonalność niezbędnych nam do działania klas. Dla
zaprezentowanego wyżej przypadku klasa taka może przyjąć następującą formę:

public interface IDateProvider


{
DateTime GetDateTimeNow();
}

public class DateProvider : IDateProvider


{
public DateTime GetDateTimeNow()
{
return [Link];
}
}

Dzięki takiemu zabiegowi klasę adaptera DateProvider możemy wstrzyknąć do klasy


FridayDetectionService w kodzie naszej aplikacji, podczas gdy w testach będziemy mogli ją
zamockować i zdefiniować jaką dokładnie datę ma ona zwracać.

Implementacja klasy FridayDetectionService wykorzystująca adapter przyjmie następującą


formę:

public class FridayDetectionService


{
private readonly IDateProvider _dateProvider;

public FridayDetectionService(
IDateProvider dateProvider)
{
_dateProvider = dateProvider;
}

public bool IsFridayToday()


{
return _dateProvider.GetDateTimeNow().DayOfWeek == [Link];
}
}

Testy metody IsFridayToday() z kolei będą wyglądały następująco:

using System;
using Moq;
using [Link];

namespace [Link]
{
[TestFixture]
public class FridayDetectionServiceTests
{
private Mock<IDateProvider> _dateProviderMock;
private FridayDetectionService _sut;

[SetUp]
public void Setup()
{
_dateProviderMock = new Mock<IDateProvider>();

_sut = new FridayDetectionService(_dateProviderMock.Object);


}

[Test]
public void FridayDetection_Friday_True()
{
_dateProviderMock
.Setup(x => [Link]())
.Returns(new DateTime(2020, 4, 3));

var result = _sut.IsFridayToday();

[Link](result);
}

[Test]
public void FridayDetection_NotFriday_False()
{
_dateProviderMock
.Setup(x => [Link]())
.Returns(new DateTime(2020, 4, 4));

var result = _sut.IsFridayToday();

[Link](result);
}
}
}

22. Programowanie równoległe i współbieżne


Celem niniejszego rozdziału jest przedstawienie klas i mechanizmów zaimplementowanych
z platformie .NET wspierających programowanie równoległe i asynchroniczne/współbieżne.
Sama tematyka złożoności i problematyki programowania równoległego i współbieżnego nie
będzie tutaj poruszana. W celu jej zgłębienia tematyki zapraszamy na KASK na studia
magisterskie ;)

Wszelkie opisane mechanizmy jak i wiele więcej z nich można znaleźć w artykule Joseptha
Ablahari poświęconego w całości mechanizmom programowania równoległego i współbieżnego
w platformie .NET. Materiał jest bardzo dokładny i wyczerpujący i stanowi (w moim uznaniu)
pełne i rzetelne źródło wiedzy w tym temacie (click).
22.1. Wątki (Thread)
Klasa Thread jest najniższą poziomowo klasą umożliwiającą zrównoleglenie pracy aplikacji.
Utworzenie i uruchomienie obiektu klasy Thread wiąże się fizycznie z zarezerwowaniem
pamięci i powołaniem do życia wątku, który chodząc jako element procesu naszej aplikacji
będzie miał przydzielany czas pracy procesora i zajmował jego zasoby na potrzebę realizacji
swoich zadań. Klasa Thread przyjmuje jako parametr startowy wskaźnik na funkcję (delegat),
która będzie stanowiła jego punkt startowy. Naturalnie nowo utworzony wątek będzie posiadał
własny stos, ale będzie współdzielił stertę wraz z innymi procesami chodzącymi w ramach
procesu naszej aplikacji.

Każdy proces posiada minimalnie jeden wątek główny. W przypadku aplikacji w .NET jest to
wątek, który wraz z uruchomieniem aplikacji rozpoczyna swoją pracę na początku funkcji main.

Poniższy przykład tworzy nowy wątek i uruchamia go. Poniżej kodu wklejono również
generowany przez niego wynik:

using System;
using [Link];

namespace ThreadingExamples
{
class Program
{
static void Main()
{
const int threadId = 1;

var someThread = new Thread(DoSomeStuff);


[Link]();

for (var i = 0; i < 5; i++)


{
[Link](250);
SayHello(threadId);
}

[Link]();
}

private static void DoSomeStuff()


{
const int threadId = 2;
for (var i = 0; i < 10; i++)
{
[Link](200);
SayHello(threadId);
}
}

private static void SayHello(int threadId)


{
[Link]($"Thread {threadId} here!");
}
}
}

Thread 1 here!
Thread 2 here!
Thread 2 here!
Thread 1 here!
Thread 2 here!
Thread 1 here!
Thread 2 here!
Thread 1 here!
Thread 2 here!
Thread 1 here!
Thread 2 here!
Thread 2 here!
Thread 2 here!
Thread 2 here!
Thread 2 here!

Jak widać wątki równolegle wypisywały tekst powitalny z przypisanym im identyfikatorem.


Tym sposobem każdemu z wątków można przyporządkować jego zadanie i każdy z wątków
wykona je niezależnie od siebie. Czasami zachodzi konieczność, aby do wątku przekazać
parametry wejściowe. Możliwe jest to przy pomocy ParametrizedThreadStart (przyjmowanego
przez jedno z przeciążeń konstruktora klasy Thread), jednak o wiele wygodniejsze jest użycie
lambdy. Poniższy przykład tworzy cztery wątki i każdemu z nich przekazuje jego identyfikator w
parametrze:

using System;
using [Link];

namespace ThreadingExamples
{
class Program
{
static void Main()
{
const int threadId = 1;

for (var i = threadId; i <= 4; i++)


{
var newThreadId = i;
var someThread = new Thread(() => DoSomeStuff(newThreadId));
[Link]();
}

[Link]();
}

private static void DoSomeStuff(int threadId)


{
for (var i = 0; i < 5; i++)
{
[Link](200);
[Link]($"Thread {threadId} here!");
}
}
}
}

Thread 2 here!
Thread 1 here!
Thread 3 here!
Thread 4 here!
Thread 2 here!
Thread 4 here!
Thread 1 here!
Thread 3 here!
Thread 2 here!
Thread 4 here!
Thread 1 here!
Thread 3 here!
Thread 2 here!
Thread 1 here!
Thread 4 here!
Thread 3 here!
Thread 2 here!
Thread 1 here!
Thread 4 here!
Thread 3 here!

NOTE: Podczas przekazywania parametrów do wątku należy pamiętać, że jego utworzenie nie
jest zerowe w czasie. Zobaczyć to można poprzez usunięcie zmiennej lokalnej newThreadId.
Przejście do kolejnej pętli będzie prawdopodobnie szybsze niż utworzenie i wystartowanie
wątku, co spowoduje brak wątku o ID 1 a pojawienie się np.: wątku o ID 5.

22.2. Synchronizacja wątków


Czasami od naszych wątków chcemy zebrać również wyniki działania. W tym celu również
pomogą nam wyrażenia lambda. Należy pamiętać, aby każdy wątek otrzymał własną przestrzeń
na wskazanie odpowiedzi (np.: miejsce w słowniku czy osobną zmienną lub pole jakiegoś
obiektu. Poniższy przykład oblicza 3 różne wartości dla listy losowych liczb - wartość
maksymalną, minimalną i średnią. Każdy z wyników wpisywany jest do innego pola tego
samego obiektu:

using System;
using [Link];
using [Link];
using [Link];

namespace ThreadingExamples
{
public class NumericCollectionStatistics
{
public double MaxValue { get; set; }
public double MinValue { get; set; }
public double Average { get; set; }
}

public class Program


{
static void Main()
{
var data = GenerateData(
minValue: 0,
maxValue: 1000,
count: 100000000);

var results = new NumericCollectionStatistics();

var threads = new Thread[2];

threads[0] = new Thread(() => [Link] =


FindMinValue(data));
threads[1] = new Thread(() => [Link] =
FindMaxValue(data));

foreach(var thread in threads)


{
[Link]();
}

[Link] = CalculateAvgValue(data);

[Link]();

[Link]("Results:");
[Link]($"MinValue: {[Link]}");
[Link]($"MaxValue: {[Link]}");
[Link]($"AvgValue: {[Link]}");
}

private static List<double> GenerateData(


int minValue,
int maxValue,
int count)
{
var random = new Random();
var data = new List<double>(count);

for (var i = 0; i < count; i++)


{
[Link](minValue + ([Link]() * maxValue));
}

return data;
}

private static double FindMinValue(List<double> data)


{
var result = [Link]();
[Link]("MinValue calculated");
return result;
}

private static double FindMaxValue(List<double> data)


{
var result = [Link]();
[Link]("MaxValue calculated");
return result;
}

private static double CalculateAvgValue(List<double> data)


{
var result = [Link]();
[Link]("AvgValue calculated");
return result;
}
}
}

MaxValue calculated
AvgValue calculated
MinValue calculated

Results:
MinValue: 1.0244548325540753E-05
MaxValue: 999.9999781139195
AvgValue: 499.96769447083517

W powyższym przykładzie widać (po jego uruchomieniu), że kalkulację różnych wartości


zajmują różny czas. Czas ten zależny jest od mocy i obciążenia procesora i jest z góry
nieprzewidywalny. Użytkownik był zobowiązany zaczekać i wcisnąć klawisz enter, aby
wyświetlić dane dopiero, gdy wszystkie żądane wartości zostały obliczone. Jednak najczęściej
nie mamy tego komfortu, aby zrzucić odpowiedzialność za synchronizację wątków na
użytkownika i musimy zadbać o nią z poziomu kodu. W tym celu możemy użyć metody Join().
Blokuje ona działanie wątku z którego została wywołana aż do zakończenia się wątku, na którym
została wywołana. Poniżej zmodyfikowany przykład, w którym [Link]() został
zastąpiony Joinem:

using System;
using [Link];
using [Link];
using [Link];

namespace ThreadingExamples
{
public class NumericCollectionStatistics
{
public double MaxValue { get; set; }
public double MinValue { get; set; }
public double Average { get; set; }
}
public class Program
{
static void Main()
{
var data = GenerateData(
minValue: 0,
maxValue: 1000,
count: 100000000);

var results = new NumericCollectionStatistics();

var threads = new Thread[2];

threads[0] = new Thread(() => [Link] =


FindMinValue(data));
threads[1] = new Thread(() => [Link] =
FindMaxValue(data));

foreach(var thread in threads)


{
[Link]();
}

[Link] = CalculateAvgValue(data);

foreach (var thread in threads)


{
[Link]();
}

[Link]("Results:");
[Link]($"MinValue: {[Link]}");
[Link]($"MaxValue: {[Link]}");
[Link]($"AvgValue: {[Link]}");
}

private static List<double> GenerateData(


int minValue,
int maxValue,
int count)
{
var random = new Random();
var data = new List<double>(count);

for (var i = 0; i < count; i++)


{
[Link](minValue + ([Link]() * maxValue));
}

return data;
}

private static double FindMinValue(List<double> data)


{
return [Link](); ;
}

private static double FindMaxValue(List<double> data)


{
return [Link]();
}

private static double CalculateAvgValue(List<double> data)


{
return [Link](); ;
}
}
}

Results:
MinValue: 2.793967725147478E-06
MaxValue: 999.9999953433871
AvgValue: 499.97750278331705

Może również zaistnieć sytuacja, w której dwa wątki próbują dobrać się wspólne do jednego i
tego samego zasobu. Wyobraźmy sobie, że to nowy wątek zajmuje się generowaniem listy
liczb, a pozostałe muszą poczekać na zakończenie generowania. Istnieje kilka mechanizmów
synchronizacji wątków, najpopularniejszym i najprostszym z nich jest lock.

Lock jest mechanizmem pozwalającym na kontrolowanie dostępu wątków do tzw. sekcji


krytycznej. W sekcji krytycznej może działać jednocześnie tylko jeden wątek. Lock jako
parametr przyjmuje obiekt (dowolny) i na poziomie CLR oznacza go jako zajęty. Nie ma to
żadnego wpływu na korzystanie z tego obiektu przez inne klasy, ale lock wykorzystuje to
oznaczenie jako informację o obecności jakiegoś wątku w sekcji krytycznej. Dopóki wątek jej nie
opuści, inny nie będzie mógł do niej wejść. W przykładzie poniżej zbiór liczb, na których
wykonywane są obliczenia został wyniesiony do osobnej klasy. Za odpowiedzialna jest za jego
wygenerowanie. Dopóki zbiór jest generowany, niemożliwe jest pobranie zbioru przez inne wątki.

using System;
using [Link];
using [Link];
using [Link];

namespace ThreadingExamples
{
public class DataGenerationService
{
private object _lockObject = new object();
private List<double> _data;

public void GenerateData(


int minValue,
int maxValue,
int count)
{
[Link]("Waiting to get to critical section to
generate data.");
lock (_lockObject)
{
[Link]("In! Generating data.");

var random = new Random();


_data = new List<double>(count);

for (var i = 0; i < count; i++)


{
_data.Add(minValue + ([Link]() * maxValue));
}
}
}

public List<double> GetDataSet()


{
[Link]("Waiting to get to critical section to get
data.");
lock(_lockObject)
{
[Link]("In! Getting data.");
return _data;
}
}
}

public class NumericCollectionStatistics


{
public double MaxValue { get; set; }
public double MinValue { get; set; }
public double Average { get; set; }
}

public class Program


{
static void Main()
{
var dataGenerationService = new DataGenerationService();

var results = new NumericCollectionStatistics();

var dataGenerationThread = new Thread(


() => [Link](
minValue: 0,
maxValue: 1000,
count: 100000000));

[Link]();

[Link](100); //To give dataGenerationThread time to start

var calculationThreads = new Thread[2];

calculationThreads[0] = new Thread(() => [Link] =


FindMinValue([Link]()));
calculationThreads[1] = new Thread(() => [Link] =
FindMaxValue([Link]()));

foreach(var thread in calculationThreads)


{
[Link]();
}

[Link] =
CalculateAvgValue([Link]());

foreach (var thread in calculationThreads)


{
[Link]();
}

[Link]("Results:");
[Link]($"MinValue: {[Link]}");
[Link]($"MaxValue: {[Link]}");
[Link]($"AvgValue: {[Link]}");
}
private static double FindMinValue(List<double> data)
{
return [Link](); ;
}

private static double FindMaxValue(List<double> data)


{
return [Link]();
}

private static double CalculateAvgValue(List<double> data)


{
return [Link]();
}
}
}

Waiting to get to critical section to generate data.


In! Generating data.
Waiting to get to critical section to get data.
Waiting to get to critical section to get data.
Waiting to get to critical section to get data.
In! Getting data.
In! Getting data.
In! Getting data.
Results:
MinValue: 2.0023435363556927E-05
MaxValue: 999.9999958090484
AvgValue: 500.00211302930285

22.3. Taski
Wraz z rozwojem platformy .NET pojawiła się biblioteka TPL - Task Parallel Library. Dodała
ona kilka znaczących ułatwień w pracy z wielowątkowością i asynchronicznością w .NET.

Biblioteka ta bazuje na ThreadPoolu. Jest to zbiór wątków zarządzanych wewnętrznie przez


TPL. Logika wewnętrzna biblioteki decyduje o rozmiarze tej puli (na bazie zapotrzebowania
programu jak i dostępnych zasobów). Koncepcja polega na jednokrotnym tworzeniu wątków i
używaniu ich wielokrotnie dla potrzeb różnych zadań. Pozwala to zmniejszyć ilość czasu
potrzebnego na realizację zleconej pracy. Zadania zlecane ThreadPoolowi do wykonania
reprezentuje klasa Task.
Z punktu widzenia programisty Task zachowuje się jak uproszczony Thread. Spróbujmy
rozpatrzyć jego użycie w kontekście problemu przedstawionego w rozdziale z wątkami.

Task, w przeciwieństwie to Thread, występuje w dwóch odmianach:

● Niegenerycznej - reprezentującej zadanie które nie zwraca żadnego wyniku,


● Generycznej - reprezentującej zdanie zwracające jakiś wynik.

W pierwszej kolejności modyfikacji poddane zostanie wywoływanie generowania danych. Jest


to zadanie nie zwracające żadnego wyniku - lista wygenerowanych liczb przechowywana jest
jako prywatne pole klasy DataGenerationService. Jako, że zadanie uruchamiamy tutaj od razu
po jego stworzeniu, do jego utworzenia wykorzystamy statyczną metodę klasy
[Link](). Metoda ta tworzy nowy Task i od razu go uruchamia, a referencje do
obiektu zadania przekazuje jako wynik:

var dataGenerationTask = [Link](() =>


[Link](
minValue: 0,
maxValue: 1000,
count: 100000000));

Następnie zmodyfikować należy kolejny fragment kodu - uruchamianie zadań


odpowiedzialnych za obliczanie maksymalnej, minimalnej i średniej wartości wszystkich
elementów. Tak jak w przypadku wątków - można podejść do tej sprawy na kilka sposobów. Na
potrzeby przykładu wykorzystamy zdolność Task’a do zwracania w odpowiedzi wyniku swojego
działania - w tym wypadku liczb typu double.

Zadanie zwracające wynik uruchomić można tak samo jak zadanie nie zwracające wyniku.
Zostanie ono uruchomione na ThreadPoolu, podczas gdy wątek główny będzie mógł
przeprowadzić własne zadania. Aby pobrać wynik obliczony przez Task, należy pobrać wartość
z jego własności Result. Klasa Task wewnętrznie sprawdzi, czy operacje wchodzące w zakres
zadania zostały wykonane. Jeżeli nie - zablokuje wątek główny aż do momentu, gdy tak się
stanie. Gdy obliczenia będą zakończone, wynik zostanie wpisany do zmiennej. Aby ręcznie
zaczekać na Task, należy skorzystać z jego metody Wait().

var minValueCalculationTask = new Task<double>(


() => FindMinValue([Link]()));
var maxValueCalculationTask = new Task<double>(
() => FindMaxValue([Link]()));

[Link]();
[Link]();
[Link] = CalculateAvgValue([Link]());

[Link] = [Link];
[Link] = [Link];

Cały zmodyfikowany kod wygląda następująco (z biznesowego punktu widzenia jego sposób
działania i wynik pozostają niezmienne):

using System;
using [Link];
using [Link];
using [Link];
using [Link];

namespace ThreadingExamples
{
public class DataGenerationService
{
private object _lockObject = new object();
private List<double> _data;

public void GenerateData(


int minValue,
int maxValue,
int count)
{
[Link]("Waiting to get to critical section to
generate data.");
lock (_lockObject)
{
[Link]("In! Generating data.");

var random = new Random();


_data = new List<double>(count);

for (var i = 0; i < count; i++)


{
_data.Add(minValue + ([Link]() * maxValue));
}
}
}

public List<double> GetDataSet()


{
[Link]("Waiting to get to critical section to get
data.");
lock(_lockObject)
{
[Link]("In! Getting data.");
return _data;
}
}
}

public class NumericCollectionStatistics


{
public double MaxValue { get; set; }
public double MinValue { get; set; }
public double Average { get; set; }
}

public class Program


{
static void Main()
{
var dataGenerationService = new DataGenerationService();

var results = new NumericCollectionStatistics();

var dataGenerationTask = [Link](() =>


[Link](
minValue: 0,
maxValue: 1000,
count: 100000000));

[Link](100);

var minValueCalculationTask = new Task<double>(


() => FindMinValue([Link]()));
var maxValueCalculationTask = new Task<double>(
() => FindMaxValue([Link]()));

[Link]();
[Link]();

[Link] =
CalculateAvgValue([Link]());

[Link] = [Link];
[Link] = [Link];

[Link]("Results:");
[Link]($"MinValue: {[Link]}");
[Link]($"MaxValue: {[Link]}");
[Link]($"AvgValue: {[Link]}");
}

private static double FindMinValue(List<double> data)


{
return [Link](); ;
}

private static double FindMaxValue(List<double> data)


{
return [Link]();
}

private static double CalculateAvgValue(List<double> data)


{
return [Link]();
}
}
}

Dodatkowo, przydatnymi metodami do synchronizacji Tasków są WaitAll i WaitAny -


pozwalają na oczekiwanie aż wszystkie lub którykolwiek z Tasków w kolekcji zakończy swoje
działanie. Modyfikując nieco kod, umieszczamy wszystkie taski w tablicy (tak jak pierwotnie
uczyniliśmy z wątkami) i podamy tę kolekcję jako parametr metody WaitAll:

using System;
using [Link];
using [Link];
using [Link];
using [Link];

namespace ThreadingExamples
{
public class DataGenerationService
{
private object _lockObject = new object();
private List<double> _data;

public void GenerateData(


int minValue,
int maxValue,
int count)
{
[Link]("Waiting to get to critical section to
generate data.");
lock (_lockObject)
{
[Link]("In! Generating data.");

var random = new Random();


_data = new List<double>(count);

for (var i = 0; i < count; i++)


{
_data.Add(minValue + ([Link]() * maxValue));
}
}
}

public List<double> GetDataSet()


{
[Link]("Waiting to get to critical section to get
data.");
lock(_lockObject)
{
[Link]("In! Getting data.");
return _data;
}
}
}

public class NumericCollectionStatistics


{
public double MaxValue { get; set; }
public double MinValue { get; set; }
public double Average { get; set; }
}

public class Program


{
static void Main()
{
var dataGenerationService = new DataGenerationService();

var results = new NumericCollectionStatistics();

var dataGenerationTask = [Link](() =>


[Link](
minValue: 0,
maxValue: 1000,
count: 100000000));

[Link](100);

var calculationTasks = new Task[2];

calculationTasks[0] = new Task(


() => [Link] =
FindMinValue([Link]()));
calculationTasks[1] = new Task(
() => [Link] =
FindMaxValue([Link]()));

foreach(var task in calculationTasks)


{
[Link]();
}

[Link] =
CalculateAvgValue([Link]());

[Link](calculationTasks);

[Link]("Results:");
[Link]($"MinValue: {[Link]}");
[Link]($"MaxValue: {[Link]}");
[Link]($"AvgValue: {[Link]}");
}

private static double FindMinValue(List<double> data)


{
return [Link](); ;
}

private static double FindMaxValue(List<double> data)


{
return [Link]();
}

private static double CalculateAvgValue(List<double> data)


{
return [Link]();
}
}
}
22.4. async/await
Async/await to dodatkowy mechanizm umożliwiający łatwą synchronizację kody
synchronicznego w oparciu o taski. Jest on w szczególności wykorzystywany podczas
tworzenia aplikacji. Pozwala on na asynchroniczne uruchamianie funkcji złożonych obliczeniowo
oddając na czas obliczeń sterowanie do wątku głównego. Załóżmy, że kod wyjściowy jest
synchroniczny i wygląda następująco:

using System;
using [Link];
using [Link];

namespace ThreadingExamples
{
public class DataGenerationService
{
private List<double> _data;

public void GenerateData(


int minValue,
int maxValue,
int count)
{
var random = new Random();
_data = new List<double>(count);

for (var i = 0; i < count; i++)


{
_data.Add(minValue + ([Link]() * maxValue));
}
}

public double FindMinValue()


{
return _data.Min();
}

public double FindMaxValue()


{
return _data.Max();
}

public double CalculateAverageValue()


{
return _data.Average();
}
}

public class Program


{
private static DataGenerationService _dataGenerationService = new
DataGenerationService();

public static void Main()


{
[Link]("Menu:");
[Link]("1. Generate data");
[Link]("2. Find min value");
[Link]("3. Find max value");
[Link]("4. Calculate avg value");

while (true)
{
[Link]("Choose option:");
var option = [Link]([Link]());

switch(option)
{
case 1:
GenerateData();
break;
case 2:
FindMinValue();
break;
case 3:
FindMaxValue();
break;
case 4:
CalculateAvgValue();
break;
default:
[Link]("Unknown option");
break;
}
}
}

private static void GenerateData()


{
_dataGenerationService.GenerateData(
minValue: 0,
maxValue: 1000,
count: 100000000);

[Link]();
[Link]("Data generated");
}

private static void FindMinValue()


{
var minData = _dataGenerationService.FindMinValue();
[Link]();
[Link](minData);
}

private static void FindMaxValue()


{
var maxData = _dataGenerationService.FindMaxValue();
[Link]();
[Link]($"Max value: {maxData}");
}

private static void CalculateAvgValue()


{
var avgData = _dataGenerationService.CalculateAverageValue();
[Link]();
[Link]($"Average value: {avgData}");
}
}
}

Menu:
1. Generate data
2. Find min value
3. Find max value
4. Calculate avg value
Choose option:1

Data generated
Choose option:2

2.3283064376228985E-06
Choose option:3

Max value: 999.9999920837581


Choose option:4
Average value: 500.0418791278575
Choose option:

Program pozwala na wywołanie jednej z 4 opcji wymienionych w menu. Każda z nich


powoduje synchroniczne wywołanie metod w klasie DataGenerationService. Zadaniem jest
zmiana zachowania na asynchroniczne wywoływanie każdej z nich.

Przy użyciu async/await w Task zamykamy samo czasochłonne obliczenie. Za przykład


weźmy metodę generującą dane. Całą zawartość metody zamkniemy w Task’u i dodamy lock,
aby w późniejszej fazie nie zacząć obliczeń podczas generacji danych.

public Task GenerateData(


int minValue,
int maxValue,
int count)
{
return [Link](() =>
{
[Link]("Waiting to get to critical section to generate
data.");
lock (_lockObject)
{
[Link]("In! Generating data.");

var random = new Random();


_data = new List<double>(count);

for (var i = 0; i < count; i++)


{
_data.Add(minValue + ([Link]() * maxValue));
}
}
});
}

Taka implementacja pozwala nam zwrócić uruchomiony już obiekt Task i zająć się jego
monitorowaniem w dalszej części aplikacji. Problem pojawia się niestety już krok dalej - w
metodzie GenerateData klasy Program. Po wygenerowaniu danych wyświetla ona
użytkownikowi informację o wykonaniu zadania. Aby informacja została wyświetlona zgodnie z
prawdą musielibyśmy albo zaczekać na zakończenie się otrzymanego obiektu typu Task albo
zamknąć całą zawartość metody GenerateData w kolejnym Tasku i przekazać wyżej.

Tu z pomocą przychodzi async/await. Każdą metodę, która zwraca obiekt typu Task,
Task<T> lub jest void można oznaczyć słowem kluczowym async. Wewnątrz takiej metody
można użyć słowa kluczowego await. Słowo await może pojawić się przed każdym obiektem
typu Task lub inną metodą oznaczoną słowem kluczym async. Powoduje ono utworzenie
maszyny stanów monitorującej asynchronicznie stan Task’a przed którym zostało postawione
oraz natychmiastowe zwrócenie sterowania metodzie wywołującej. Gdy Task zakończy swoje
działanie, maszyna rozpocznie dalsze wykonywanie kodu od miejsca, gdzie pojawiło się słowo
kluczowe await. Więcej informacji tutaj (click).

Po dodaniu mechanizmu async/await kolejno metody GenerateData z klas Program i


DataGeneratonService wygladaja następująco:

public class DataGenerationService


{
private object _lockObject = new object();
private List<double> _data;

public async Task GenerateData(


int minValue,
int maxValue,
int count)
{
await [Link](() =>
{
[Link]("Waiting to get to critical section to
generate data.");
lock (_lockObject)
{
[Link]("In! Generating data.");

var random = new Random();


_data = new List<double>(count);

for (var i = 0; i < count; i++)


{
_data.Add(minValue + ([Link]() * maxValue));
}
}
});
}

//Some more code


}

public class Program


{
private static DataGenerationService _dataGenerationService = new
DataGenerationService();

public static void Main()


{
[Link]("Menu:");
[Link]("1. Generate data");
[Link]("2. Find min value");
[Link]("3. Find max value");
[Link]("4. Calculate avg value");

while (true)
{
[Link]("Choose option:");
var option = [Link]([Link]());

switch(option)
{
case 1:
GenerateData();
break;
case 2:
FindMinValue();
break;
case 3:
FindMaxValue();
break;
case 4:
CalculateAvgValue();
break;
default:
[Link]("Unknown option");
break;
}
}
}

private static async void GenerateData()


{
await _dataGenerationService.GenerateData(
minValue: 0,
maxValue: 1000,
count: 100000000);

[Link]();
[Link]("Data generated");
}
//Some more code
}

Po dodatkowym obudowaniu pozostałych metod klasy DataGenerationService lockami, wynik


działania ulega widocznej zmianie:

Menu:
1. Generate data
2. Find min value
3. Find max value
4. Calculate avg value
Choose option:1
Choose option:Waiting to get to critical section to generate data.
In! Generating data.
2
Waiting to get to critical section to find min value.
In! Calculating...

Data generated

Min value: 4.656612875245797E-07


Choose option:

Jak widzimy menu stało się dla nas dostępne jeszcze zanim dane zostały wygenerowane. Po
uruchomieniu opcji 2 (jako, że jest ona synchroniczna) aplikacja zatrzymała się i nie pozwoliła
nam wybrać kolejnej dopóki dane nie zostały wygenerowane i minimalna wartość nie została
znaleziona.

Warto zauważyć, że wraz z pojawieniem się mechanizmu async/await w metodzie


GenerateData klasy DataGenerationService, zniknęło z niej słowo kluczowe return. Dzieje się
tak dlatego, że mechanizm że metoda asynchroniczna jako wynik swojego działania zwraca typ,
jaki przypisany jest Task’owi. W tym wypadku było to Task bez typu, czyli metoda nie zwraca
wyniku. W przypadku metod FindMinValue, FindMaxValue i CalculateAvgValue będą one typu
async Task<double>, ale return będzie zwracał liczbę typu double - ponieważ to ona jest
wynikiem obliczeń zadania.

W sposób analogiczny zrealizujemy asynchroniczne uruchamianie pozostałych metod. Pełny


kod wraz z jego zachowaniem można znaleźć poniżej:

using System;
using [Link];
using [Link];
using [Link];

namespace ThreadingExamples
{
public class DataGenerationService
{
private object _lockObject = new object();
private List<double> _data;

public async Task GenerateData(


int minValue,
int maxValue,
int count)
{
await [Link](() =>
{
[Link]("Waiting to get to critical section to
generate data.");
lock (_lockObject)
{
[Link]("In! Generating data.");

var random = new Random();


_data = new List<double>(count);

for (var i = 0; i < count; i++)


{
_data.Add(minValue + ([Link]() *
maxValue));
}
}
});
}

public async Task<double> FindMinValue()


{
return await Task<double>.[Link](() =>
{
[Link]("Waiting to get to critical section to
find min value.");
lock (_lockObject)
{
[Link]("In! Calculating...");
return _data.Min();
}
});
}
public async Task<double> FindMaxValue()
{
return await Task<double>.[Link](() =>
{
[Link]("Waiting to get to critical section to
find max value.");
lock (_lockObject)
{
[Link]("In! Calculating...");
return _data.Max();
}
});
}

public async Task<double> CalculateAverageValue()


{
return await Task<double>.[Link](() =>
{
[Link]("Waiting to get to critical section to
calculate avg value.");
lock (_lockObject)
{
[Link]("In! Calculating...");
return _data.Average();
}
});
}
}

public class Program


{
private static DataGenerationService _dataGenerationService = new
DataGenerationService();

public static void Main()


{
[Link]("Menu:");
[Link]("1. Generate data");
[Link]("2. Find min value");
[Link]("3. Find max value");
[Link]("4. Calculate avg value");

while (true)
{
[Link]("Choose option:");
var option = [Link]([Link]());
switch(option)
{
case 1:
GenerateData();
break;
case 2:
FindMinValue();
break;
case 3:
FindMaxValue();
break;
case 4:
CalculateAvgValue();
break;
default:
[Link]("Unknown option");
break;
}
}
}

private static async void GenerateData()


{
await _dataGenerationService.GenerateData(
minValue: 0,
maxValue: 1000,
count: 100000000);

[Link]();
[Link]("Data generated");
}

private static async void FindMinValue()


{
var minData = await _dataGenerationService.FindMinValue();
[Link]();
[Link]($"Min value: {minData}");
}

private static async void FindMaxValue()


{
var maxData = await _dataGenerationService.FindMaxValue();
[Link]();
[Link]($"Max value: {maxData}");
}
private static async void CalculateAvgValue()
{
var avgData = await
_dataGenerationService.CalculateAverageValue();
[Link]();
[Link]($"Average value: {avgData}");
}
}
}

Menu:
1. Generate data
2. Find min value
3. Find max value
4. Calculate avg value
Choose option:1
Choose option:Waiting to get to critical section to generate data.
In! Generating data.
2
Choose option:Waiting to get to critical section to find min value.
3
Choose option:Waiting to get to critical section to find max value.
4
Choose option:Waiting to get to critical section to calculate avg value.
In! Calculating...

Data generated
In! Calculating...

Min value: 3.7252903001966375E-06


In! Calculating...

Max value: 999.9999734573066

Average value: 499.9706995619267

22.5. Klasa Process


Klasa Process to klasa pozwalająca na diagnostykę jak i uruchamianie i zarządzanie
Procesami w systemie. Posiada kilka prostych przydatnych funkcjonalności.
Jedną z nich jest wypisanie wszystkich procesów aktualnie działających w systemie
operacyjnym. Można je później przeszukiwać, analizować i pobierać informacje o np.: zużyciu
pamięci czy priorytecie (click).

using System;
using [Link];

namespace ThreadingExamples
{
public class Program
{
public static void Main()
{
var processes = [Link]();

foreach (var process in processes)


{
[Link]($"Process: {[Link]} ID:
{[Link]}");
}
}
}
}

Możliwe jest również pobranie informacji o aktualnym procesie lub o dowolnym procesie o
znanym PID:

using [Link];

namespace ThreadingExamples
{
public class Program
{
public static void Main()
{
var currentProcess = [Link]();
var process = [Link](1234);
}
}
}

Dodatkowo klasa proces pozwala na stworzenie nowego procesu w systemie. Można za jego
pomocą uruchomić dowolną aplikację poprzez podanie ścieżki do jej pliku *.exe, nazwy pod
która jest widoczny (jeżeli jest dodany do zmiennej środowiskowej PATH) oraz przekazywać do
niego argumenty. Jest to możliwe dzięki różnym przeciążeniom metody statycznej
[Link](). Zwraca ona uchwyt do procesu identyczny z uchwytami zwracanymi w
poprzednich przykładach. Stan procesu można monitorować i reagować odpowiednio na jego
poczynania.

Proces można również zakończyć metodą Kill na uchwycie do procesu:

using [Link];
using [Link];

namespace ThreadingExamples
{
public class Program
{
public static void Main()
{
var process = [Link]("notepad");

[Link](1000);

[Link]();
}
}
}

23. Usługi systemowe (Windows)


Usługa systemowa to aplikacja uruchomiona w tle, której cyklem życia zarządza system
operacyjny. W zależności od konfiguracji, usługa systemowa może zostać (i przeważnie jest)
uruchomiona automatycznie zaraz po uruchomieniu systemu operacyjnego. Usługi
uruchamiane są jeszcze przed zalogowaniem użytkownika.

Popularnymi usługami systemowymi są np.: antywirusy, firewall, serwery (np.: SQL) czy
klienty serwerów paczek aktualizacyjnych.

23.1. Zarządzanie usługami systemowymi


Listę usług systemowych zainstalowanych w systemie operacyjnym można sprawdzić w
aplikacji Services:
Usługa systemowa posiada następujące charakterystyki:

● Name - Nazwę,
● Description - Opis,
● Status:
○ Running,
○ Paused,
○ Starting,
○ Stopping,
○ Puste (Stopped).
● Startup Type - tryb uruchamiania:
○ Automatic - uruchamianie automatyczne po uruchomieniu systemu
operacyjnego,
○ Automatic (Delayed start) - uruchamianie automatyczne z opóźnieniem,
○ Automatic (Trigger start) - uruchamianie automatyczne po uruchomieniu systemu
operacyjnego lub po wystąpieniu zdefiniowanego zdarzenia systemowego,
○ Manual - uruchamianie ręczne przez użytkownika lub wywołanie komendy,
○ Manual (Triggered start) - uruchamianie automatyczne bo wystąpieniu
zdefiniowanego zdarzenia systemowego,
● Log On As - użytkownik, z uprawnieniami którego uruchomiony jest serwis. Najczęściej
są to tzw. Faceless / Service Accounts. Są to konta utworzone specjalnie na potrzeby
uruchamiania danego serwisu. Dobrą praktyka jest posiadanie oddzielnego konta dla
każdego serwisu i nadanie im najniższych możliwych uprawnień pozwalających na
prawidłowe działanie serwisu.
Aby uruchomić lub zatrzymać usługę systemową należy na liście w aplikacji Services kliknąć
na nią prawym klawiszem myszy i wybrać odpowiednią akcję. Możliwe jest również zarządzanie
uprawnieniami i konfiguracją - wystarczy wybrać z menu kontekstowego opcję Properties.

Usługami można manipulować również z poziomu linii komend czy PowerShella. Jednym ze
sposobów jest wykorzystanie wbudowanej aplikacji Service Control (click) dostępnej w linii
komend pod komendą sc. Pozwala ona między innymi na instalację i deinstalację usług, czy też
uruchamianie i zatrzymywanie.

Domyślne usługi systemowe uruchamiane są z uprawnieniami lokalnymi systemu


operacyjnego (Local System).

23.2. TopShelf
Platforma .NET posiada wbudowane biblioteki pozwalające na tworzenie usług systemowych.
Visual Studio posiada również template pozwalający na szybkie ich tworzenie. Podstawowym
problemem z wykorzystaniem domyślnego podejścia jest jednak monitorowanie działania
aplikacji jak i jej debuggowanie. Usługi systemowe, jako systemy chodzące w tle i zarządzane
przez system operacyjny nie posiadają żadnego interfaceu użytkownika. Wymagana jest więc
analiza logów aplikacji lub podpinanie się do istniejącego procesu systemowego.

Z pomocą przychodzi bardzo popularna biblioteka TopShelf (click i click). Jest to biblioteka,
która pozwala na stworzenie usługi systemowej z aplikacji konsolowej. Oznacza to, że podczas
pracy z aplikacją z poziomu Visual Studio czy też ręcznie, poprzez uruchomienie pliku exe,
projekt zachowuje się jak aplikacja konsolowa. TopShelf dodaje jednak kilka komend, które
można podać podczas uruchamiania aplikacji z linii komend. Pozwalają one na instalację
aplikacji jako usługi systemowej czy zarządzanie jej trybem życia po zainstalowaniu (click).
Aby dodać tę funkcjonalność konieczne jest dodanie niespełna kilkunastu linijek kodu w klasie
Program (click). Pozwalają one na zdefiniowane zachowania aplikacji dla wybranej akcji oraz
dodanie podstawowych elementów, takich jak opis czy nazwa.

Poniżej przykład wykorzystania TopShelf wraz z rozwiązaniem zależności przy użyciu


NInject:

using Ninject;
using System;
using Topshelf;

namespace TopShelfExample
{
public class DiCofig
{
private IKernel _kernel;

public IKernel GetKernel()


{
if(_kernel != null)
{
return _kernel;
}

_kernel = new StandardKernel();


_kernel.Bind<IApplicationHost>().To<ApplicationHost>();

return _kernel;
}
}

public interface IApplicationHost


{
void Start();
void Stop();
}

public class ApplicationHost : IApplicationHost


{
public void Start()
{
[Link]("Service started");
}

public void Stop()


{
[Link]("Service stopped.");
}
}

class Program
{
public static void Main()
{
var kernel = new DiCofig().GetKernel();

var rc = [Link](x =>


{
[Link]<IApplicationHost>(s =>
{
[Link](name =>
[Link]<IApplicationHost>());
[Link](tc => [Link]());
[Link](tc => [Link]());
});
[Link]();

[Link]("Test TopShelf Service");


[Link]("Test Service [Link]");
[Link]("Test Service [Link]");
});

var exitCode = (int)[Link](rc, [Link]());


[Link] = exitCode;
}
}
}

What to add next year


● Protobuf
● - XUnit
● - Async main
● - Parallel loop
● - More about Process class
● - Typical problems to consider when introducing parallel and async processing
(deadlocks, performance drop and so on)
● - WPF Basics
● - MVC Basics
● - Azure Basics
● - Own DesignPattern examples and explanations
● - Popular software architectures

You might also like