Platformy Technologiczne .NET 2020
Platformy Technologiczne .NET 2020
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
● 50% - 3
● 60% - 3,5
● 70% - 4
● 80% - 4,5
● 90+% - 5
Zasady zaliczenia 1
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
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
Serializacja 119
Przygotowanie danych 119
Serializacja Binarna 120
Serializacja Xmlowa 120
Serializacja Jsonowa 121
Rzutowania 124
Rzutowanie niejawne 124
Rzutowanie jawne 124
Is 124
As 125
Ź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ć.
2.2. Komentarze
Komentarze, to tekst w kodzie programu, który pomijany jest przez kompilator podczas
procesu budowania aplikacji. Komentarze mają kilka zastosowań:
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.
● Komentarz jednolinijkowy
//Komentarz jednolinijkowy
● Komentarz wielolinijkowy:
/*
Komentarz wielolinijkowy
*/
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
byte 0 255 1
ushort 0 65 535 2
long -9 223 372 036 854 775 808 9 223 372 036 854 775 807 8
Domyślne liczby całkowite w .NET traktowane są jako typ int. Aby zmienna była innego typu,
należy:
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:
Domyślnie liczby zmiennoprzecinkowe traktowane są jako typ double. Aby zmienna była
innego typu, należy:
var x = (float)42.01;
var y = (double)42.01;
var z = (decimal)42.01;
2.4.4. Logiczne
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:
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:
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 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;
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.
namespace ConstsExample
{
public class LinearFuncion
{
private readonly double _a = 1;
private readonly double _b = 1;
public LinearFuncion()
{
}
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:
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.
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.
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.
Łą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 }
if ([Link] == [Link])
{
title = "Mr. ";
}
else
{
title = [Link] ? "Mrs. " : "Ms. ";
}
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 }
if ([Link] == [Link])
{
title = "Mr. ";
}
else
{
title = [Link] ? "Mrs. " : "Ms. ";
}
2.7.6. Formatowanie
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 }
if ([Link] == [Link])
{
title = "Mr";
}
else
{
title = [Link] ? "Mrs" : "Ms";
}
Drugim sposobem jest tzw. string interpolation. Ten sposób konstrukcji stringa pozwala na
utworzenie całego łańcucha przy wykonaniu pojedynczej alokacji pamięci.
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; }
}
if ([Link] == [Link])
{
title = "Mr";
}
else
{
title = [Link] ? "Mrs" : "Ms";
}
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.
[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.
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.
[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.
if ()
{
[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);
}
}
Kolejne trzy metody pozwalają na utworzenie nowego ciągu poprzez modyfikację już
istniejącego.
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 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.
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).
[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
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";
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.
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; }
}
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;
namespace StringExample
{
public class Car
{
public string Name { get; set; }
public void Drive() { }
}
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;
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; }
}
2.9.4. AS i IS
Słowo kluczowe IS pozwala na sprawdzenie, czy dany obiekt może zostać rzutowany na
wybrany typ danych (poprzez przeszukanie jego drzewa dziedziczenia).
using System;
namespace ConversionsExample
{
public class Car
{
public string Name { get; set; }
public void Drive() { }
}
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;
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.
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 - 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;
_digit = digit;
}
byte x = 2;
Digit digit2 = (Digit) x; //Digit2 eq. 2
}
}
}
● 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).
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;
}
[Link]("Login: ");
var login = [Link]();
[Link]("Password: ");
var password = [Link]();
return user;
}
}
}
[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] = [Link];
[Link] = [Link];
[Link] = [Link];
[Link] = [Link];
[Link]();
Konsola systemowa posiada trzy rodzaje strumieni danych, które są na niej wyświetlane:
namespace ConsoleExamples
{
public class ConsoleIoRedirector : IDisposable
{
private StreamReader _inputStream;
private StreamWriter _outputStream;
private StreamWriter _errorStream;
if(_inputStream != null)
{
CloseStreamReader(_inputStream);
}
_inputStream = CreateStreamReader(filePath);
[Link](_inputStream);
}
if (_outputStream != null)
{
CloseStreamWriter(_outputStream);
}
_outputStream = CreateStreamWriter(filePath);
[Link](_outputStream);
}
if (_errorStream != null)
{
CloseStreamWriter(_errorStream);
}
_errorStream = CreateStreamWriter(filePath);
[Link](_errorStream);
}
CloseStreamWriter(_outputStream);
_outputStream = null;
CloseStreamWriter(_errorStream);
_errorStream = null;
}
}
}
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);
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.
do
{
[Link]("Type sth: ");
var input = [Link]();
[Link](input);
2.11.3. For
Dokumentacja - click.
namespace LoopsExamples
{
public class LinearFuncion
{
private readonly double _a;
private readonly double _b;
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;
}
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.
return points;
}
2.12. Rekurencja
Artykuł - click.
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]();
}
if(length == 1)
{
[Link]("0");
return;
}
if (length == 2)
{
[Link]("0 1");
return;
}
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;
}
[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.1. if-else
Dokumentacja - click.
[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.
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.
TimeOfDay result;
return result;
}
Dokumentacja - click.
[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.
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.
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.
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;
}
2.14.3. return
Dokumentacja - click.
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;
}
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:
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;
}
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.
Każdy zdefiniowany typ wyliczeniowy rozumiany jest przez kompilator jako odrębny typ
danych. Enum jest typem wartościowym.
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:
, lub tylko elementom, których wartość powinna być inna, niż o jeden wyższa niż poprzednia:
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
}
return result;
}
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ść.
3.1. Konstrukcja
Metoda składa się z dwóch nadrzędnych elementów:
//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.
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
}
return result;
}
}
}
3.3. Modyfikatory parametrów
Artykuł - click.
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.
3.3.2. In
Dokumentacja - click.
ModifyPoint(in a);
}
ModifyPoint(in a);
}
namespace DefaultParamValueExample
{
public class User
{
public int Name;
public int Surname;
public bool IsActive;
}
return usersFound;
}
}
}
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;
}
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.
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.
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:
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
try
{
[Link](buffer, startPosition, [Link]);
}
catch (IOException e)
{
[Link]($"An error occurred when reading file {filePath}:
{[Link]}");
return null;
}
finally
{
if (streamReader != null)
{
[Link]();
}
}
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.
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]();
}
}
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()
{
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]();
}
}
using System;
namespace CustomExceptionExamples
{
public struct GeoCoords
{
public double Longitude;
public double Latitude;
}
public Wgs84CoordinatesException(
string message,
GeoCoords geoCoords)
: base(message)
{
GeoCoords = geoCoords;
}
}
return geoCoords;
}
}
}
6.1. Strumienie
Dokumentacja (.NET Framework) - click.
Dokumentacja (.NET Core) - click.
Artykuł - click i click.
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);
}
[Link]();
}
[Link]();
}
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);
}
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;
}
cars[0] = subaru;
cars[1] = honda;
return cars;
}
}
}
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;
}
RentACar(subaru);
[Link]([Link]); //true
}
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;
}
RentACar(subaru);
[Link]([Link]); //false
}
Każda definicja klasy lub struktury to defacto definicja nowego typu, którego można użyć
pisząc system.
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;
}
//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");
}
return null;
}
}
}
using System;
namespace OopExamples
{
public class User
{
public int Id;
public string Name;
public string Surname;
public Car RentedCar;
}
[Link] = this;
_rentingUser = user;
_isRented = true;
return true;
}
}
[Link](user);
}
}
}
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;
}
[Link](user, car);
}
}
if([Link])
{
[Link]($"Car is already rented");
return false;
}
[Link] = car;
[Link] = user;
[Link] = true;
return true;
}
}
}
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ą:
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;
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.
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;
}
[Link](subaru);
[Link](janush);
PrintId<Car>([Link](1));
PrintId<User>([Link](1));
}
return null;
}
}
}
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).
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];
[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.
using System;
namespace AbstractionExample
{
public abstract class Car
{
public string Make;
public string Model;
if(areYouLucky)
{
[Link]();
}
else
{
[Link]();
}
}
}
}
9.3.2. Interface’y
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.
using System;
namespace AbstractionExample
{
public interface IDrivable
{
public abstract void Drive();
public abstract void Crash();
}
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; }
[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.
using System;
namespace InheritanceExample
{
public class Car
{
public string Make { get; set; }
public string Model { get; set; }
using System;
namespace InheritanceExample
{
public class Car
{
public string Make { get; set; }
public string Model { get; set; }
using System;
namespace InheritanceExample
{
public class Car
{
public string Make { get; private set; }
public string Model { get; private set; }
10.2.2. Lista
10.2.3. Dictionary
Przykład słownika
var telephoneNumbers = new Dictionary<string, List<int>>();
[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
[Link]([Link]);
10.2.5. Stack
10.2.6. Queue
10.3. LINQ
Dokumentacja - click
var sharedEmployees =
[Link]([Link]);
[Link](employee =>
{
[Link]($"Analysing employee {[Link]}
{[Link]}");
if ([Link] <= 2)
return "JUNIOR";
else if ([Link] < 5)
return "MIDDLE";
else
return "SENIOR";
});
[Link]([Link]());
[Link]([Link]());
Przykład metody która zwraca obiekt typu IEnumerable bez użycia słowa kluczowego yield:
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;
return newEmployees;
}
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"}
}
});
}
12.2. checked
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);
Klasy statyczne są niebezpieczne - co się stanie jak powyższego loggera uruchomi wiele
wątków?
static Logger()
{
streamWriter = new StreamWriter(LogFile);
}
Pytanie: Czy nowe podejście rozwiązuje nam wszystkie problemy przy uruchomieniu na wielu
wątkach? Odpowiedź brzmi - nie wszystkie :)
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.
14.3. Is
public class Orlen : Company { }
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.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!!!";
[Test]
public void CompanyControllerTest()
{
var dbContext = new TestingDbContext();
var companyController = new CompanyController(dbContext);
[Link]();
}
}
var mostExpieriencedEmployees =
[Link](SelectEmployeeWithBiggestExperience);
}
16.1. Migracje
Dokumentacja - click.
[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]();
}
[Link]();
[Link]();
[Link]();
[Link]();
[Link](endpoints =>
{
[Link]();
});
}
[Route("api/[controller]")]
[Authorize]
[ApiController]
public class CmpaniesController : ControllerBase
{
...
...
...
[Link](options =>
[Link]("User", policy =>
[Link](new UserPolicyRequirement())));
[Link](options =>
[Link]("Admin" policy =>
[Link](new AdminPolicyRequirement())));
[Link]<IAuthorizationHandler, UserPolicyHandler>();
[Link]<IAuthorizationHandler, AdminPolicyHandler>();
[Link]();
}
[Route("api/[controller]")]
[Authorize(Policy = "Admin")]
[ApiController]
public class CmpaniesController : ControllerBase
{
...
...
...
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ą:
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.
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.
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.
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.
[Link](2);
[Link]([Link]()); //4 - as expected
InitializeRextangleByUser(square);
[Link]([Link]()); //9 - 6 expected
}
using System;
namespace SolidExamples
{
public class Program
{
static void Main()
{
var square = new Square();
[Link](2);
[Link]([Link]()); //4 - as expected
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ę:
_trailerAttached = trailer;
}
_loadedGoods.AddRange(goods);
}
_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:
//...
}
_loadedGoods.AddRange(goods);
}
_destination = destination;
}
}
_trailerAttached = trailer;
}
_loadedGoods.AddRange(goods);
}
_destination = destination;
}
}
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.
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).
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 BankTransferService(
Func<IBankTransfersDataContext> dataSourceFactoryMethod,
IDataSerializer dataSerializer,
IDataExportSource dataExportSource
)
{
_dataSourceFactoryMethod = dataSourceFactoryMethod;
_dataSerializer = dataSerializer;
_dataExportSource = dataExportSource;
}
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.
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.
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.
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]();
}
public BankTransfersService(Func<IBankTransfersDataContext>
dataSourceFactoryMethod)
{
_dataSourceFactoryMethod = dataSourceFactoryMethod;
}
return history;
}
}
public BankDataExportService(
IBankTransfersService dataSourceFactoryMethod,
IDataSerializer dataSerializer,
IDataExportSource dataExportSource)
{
_dataSourceFactoryMethod = dataSourceFactoryMethod;
_dataSerializer = dataSerializer;
_dataExportSource = dataExportSource;
}
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.
_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].
_kernel.Bind<IDataSerializer>().To<JsonSerializer>().InSingletonScope();
using [Link];
using Ninject;
using System;
using [Link];
using [Link];
using [Link];
using [Link];
namespace SolidExamples
{
public interface IProgram
{
void Run();
}
[Link]();
}
_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 BankTransfersService(Func<IBankTransfersDbContext>
dataSourceFactoryMethod)
{
_dataSourceFactoryMethod = dataSourceFactoryMethod;
}
return history;
}
}
public BankDataExportService(
IBankTransfersService dataSourceFactoryMethod,
IDataSerializer dataSerializer,
IDataExportSource dataExportSource)
{
_dataSourceFactoryMethod = dataSourceFactoryMethod;
_dataSerializer = dataSerializer;
_dataExportSource = dataExportSource;
}
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.
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).
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; }
}
if (![Link])
{
return new LoanCalculationResult
{
CanBeTaken = false,
TotalCost = 0.0m
};
}
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:
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] .
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:
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
};
//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.
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.
[Test]
public void LoanCalculation_LoanAllowedForUser_Acceptation()
{
//Arrange
var user = new Person
{
CanGetLoan = true
};
//Act
var result = [Link](user, 1000);
//Assert
[Link]([Link]);
[Link](1100.0m, [Link]); //Should be 1150
}
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.
[TestCase(-1)]
[TestCase(1000001)]
public void LoanCalculation_InvalidLoanValue_Exception(decimal loanSize)
{
//Arrange
var user = new Person
{
CanGetLoan = true
};
//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
};
//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
};
//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.
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
};
[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));
}
}
}
public LoanService()
{
_loanAllowanceService = new LoanAllowanceService();
}
var canGetLoan =
_loanAllowanceService.IsPersonAllowedToGetLoan(requestor);
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:
var canGetLoan =
_loanAllowanceService.IsPersonAllowedToGetLoan(requestor);
return new LoanCalculationResult
{
CanBeTaken = canGetLoan,
TotalCost = canGetLoan
? requestedAmount * 1.15m
: 0.0m
};
}
}
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.
[SetUp]
public void SetUp()
{
_loanAllowanceServiceMock = new Mock<ILoanAllowanceService>();
[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]);
}
[SetUp]
public void SetUp()
{
_personNotAllowedToGetLoan = new Person();
_personAllowedToGetLoan = new Person();
_loanAllowanceServiceMock
.Setup(x => [Link](_personNotAllowedToGetLoan))
.Returns(true);
_loanAllowanceServiceMock
.Setup(x => [Link](_personNotAllowedToGetLoan ))
.Returns(false);
[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).
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);
[Test]
public void LoanCalculation_LoanNotAllowedForUser_Rejection()
{
_loanAllowanceServiceMock
.Setup(x => [Link]([Link]<Person>()))
.Returns(false);
[Link]([Link]);
[Link](0.0m, [Link]);
}
[Test]
public void LoanCalculation_LoanAllowedForUser_Acceptation()
{
_loanAllowanceServiceMock
.Setup(x => [Link]([Link]<Person>()))
.Returns(true);
[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));
}
}
}
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 FridayDetectionService(
IDateProvider dateProvider)
{
_dateProvider = dateProvider;
}
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>();
[Test]
public void FridayDetection_Friday_True()
{
_dateProviderMock
.Setup(x => [Link]())
.Returns(new DateTime(2020, 4, 3));
[Link](result);
}
[Test]
public void FridayDetection_NotFriday_False()
{
_dateProviderMock
.Setup(x => [Link]())
.Returns(new DateTime(2020, 4, 4));
[Link](result);
}
}
}
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;
[Link]();
}
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!
using System;
using [Link];
namespace ThreadingExamples
{
class Program
{
static void Main()
{
const int threadId = 1;
[Link]();
}
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.
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; }
}
[Link] = CalculateAvgValue(data);
[Link]();
[Link]("Results:");
[Link]($"MinValue: {[Link]}");
[Link]($"MaxValue: {[Link]}");
[Link]($"AvgValue: {[Link]}");
}
return data;
}
MaxValue calculated
AvgValue calculated
MinValue calculated
Results:
MinValue: 1.0244548325540753E-05
MaxValue: 999.9999781139195
AvgValue: 499.96769447083517
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);
[Link] = CalculateAvgValue(data);
[Link]("Results:");
[Link]($"MinValue: {[Link]}");
[Link]($"MaxValue: {[Link]}");
[Link]($"AvgValue: {[Link]}");
}
return data;
}
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.
using System;
using [Link];
using [Link];
using [Link];
namespace ThreadingExamples
{
public class DataGenerationService
{
private object _lockObject = new object();
private List<double> _data;
[Link]();
[Link] =
CalculateAvgValue([Link]());
[Link]("Results:");
[Link]($"MinValue: {[Link]}");
[Link]($"MaxValue: {[Link]}");
[Link]($"AvgValue: {[Link]}");
}
private static double FindMinValue(List<double> data)
{
return [Link](); ;
}
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.
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().
[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;
[Link](100);
[Link]();
[Link]();
[Link] =
CalculateAvgValue([Link]());
[Link] = [Link];
[Link] = [Link];
[Link]("Results:");
[Link]($"MinValue: {[Link]}");
[Link]($"MaxValue: {[Link]}");
[Link]($"AvgValue: {[Link]}");
}
using System;
using [Link];
using [Link];
using [Link];
using [Link];
namespace ThreadingExamples
{
public class DataGenerationService
{
private object _lockObject = new object();
private List<double> _data;
[Link](100);
[Link] =
CalculateAvgValue([Link]());
[Link](calculationTasks);
[Link]("Results:");
[Link]($"MinValue: {[Link]}");
[Link]($"MaxValue: {[Link]}");
[Link]($"AvgValue: {[Link]}");
}
using System;
using [Link];
using [Link];
namespace ThreadingExamples
{
public class DataGenerationService
{
private List<double> _data;
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;
}
}
}
[Link]();
[Link]("Data generated");
}
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
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).
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;
}
}
}
[Link]();
[Link]("Data generated");
}
//Some more code
}
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
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.
using System;
using [Link];
using [Link];
using [Link];
namespace ThreadingExamples
{
public class DataGenerationService
{
private object _lockObject = new object();
private List<double> _data;
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;
}
}
}
[Link]();
[Link]("Data generated");
}
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...
using System;
using [Link];
namespace ThreadingExamples
{
public class Program
{
public static void Main()
{
var processes = [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.
using [Link];
using [Link];
namespace ThreadingExamples
{
public class Program
{
public static void Main()
{
var process = [Link]("notepad");
[Link](1000);
[Link]();
}
}
}
Popularnymi usługami systemowymi są np.: antywirusy, firewall, serwery (np.: SQL) czy
klienty serwerów paczek aktualizacyjnych.
● 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.
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.
using Ninject;
using System;
using Topshelf;
namespace TopShelfExample
{
public class DiCofig
{
private IKernel _kernel;
return _kernel;
}
}
class Program
{
public static void Main()
{
var kernel = new DiCofig().GetKernel();