Wymagania formalne:
Z wszystkich zadań należy przygotować sprawozdanie zawierające: stronę tytułową, pełną treść zadania, czytelnie skomentowany kod źródłowy (w postaci tekstu do skopiowania), zrzuty ekranu prezentujące poprawne działanie programu dla różnych danych wejściowych oraz krótkie wnioski podsumowujące nabyte umiejętności. Każde zadanie domowe powinno być rozwiązane w osobnym projekcie konsolowym C#.
01
Zadanie wprowadzające: System rejestracji i monitoringu floty
Cel

Student demonstruje umiejętność projektowania klasy zawierającej różnorodne składowe, takie jak pola prywatne, publiczne właściwości oraz składowe statyczne. Zadanie pokazuje praktyczne zastosowanie konstruktorów instancyjnych i statycznych do zarządzania stanem obiektów oraz globalnymi licznikami systemowymi. Student rozumie różnicę między zachowaniem obiektowym a statycznym w kontekście platformy .NET.

Scenariusz

Pracujesz nad systemem dla firmy logistycznej, który ma za zadanie monitorować bazę pojazdów dostawczych. Każdy pojazd musi posiadać unikalny numer rejestracyjny, markę oraz informację o aktualnym stanie licznika kilometrów. System musi automatycznie zliczać, ile pojazdów zostało zarejestrowanych w trakcie działania aplikacji, korzystając z mechanizmów statycznych. Dodatkowo wymagane jest zdefiniowanie stałej określającej maksymalną dopuszczalną ładowność dla standardowej floty. Konstruktor statyczny powinien zostać użyty do zainicjalizowania globalnych parametrów konfiguracyjnych systemu, które są wspólne dla wszystkich obiektów. Każdy nowy pojazd jest tworzony z zerowym przebiegiem, chyba że podczas rejestracji zostanie podana inna wartość początkowa. Program musi uniemożliwić ustawienie ujemnego przebiegu poprzez odpowiednią logikę we właściwościach klasy. Na koniec aplikacja generuje raport wyświetlający dane konkretnego pojazdu oraz globalną liczbę wszystkich aut w bazie. Całość projektu musi być czytelna i stanowić solidny fundament pod dalszy rozwój modułu logistycznego. Jest to krytyczny element architektury pozwalający na zachowanie spójności danych o zasobach firmy.

Sugerowane kroki do wykonania
  • Zdefiniuj klasę o nazwie Pojazd w nowym pliku lub bezpośrednio w głównej przestrzeni nazw.
  • Dodaj prywatne pole do przechowywania marki pojazdu oraz publiczną właściwość typu string.
  • Zadeklaruj statyczne pole typu int, które będzie pełniło rolę licznika wszystkich utworzonych obiektów.
  • Dodaj stałą (const) określającą domyślną wersję oprogramowania sterownika pojazdu.
  • Zaimplementuj właściwość Przebieg z jawnym polem bazowym i walidacją uniemożliwiającą ustawienie wartości mniejszej od zera.
  • Stwórz konstruktor statyczny, który wypisze w konsoli komunikat o inicjalizacji systemu floty.
  • Dodaj konstruktor instancyjny przyjmujący markę i numer rejestracyjny, który inkrementuje statyczny licznik.
  • Uzupełnij kod o konstruktor przeciążony, pozwalający ustawić również początkowy przebieg przy tworzeniu obiektu.
  • W metodzie Main utwórz co najmniej dwa obiekty klasy Pojazd z różnymi danymi wejściowymi.
  • Wyświetl stan licznika globalnego oraz szczegółowe informacje o każdym pojeździe, korzystając z dostępu do składowych.
Kod rozwiązania
using System;

class Pojazd
{
    // Pola i stałe
    public const string WersjaSystemu = "2.0.4-LTS";
    private static int licznikPojazdow;
    private decimal przebieg;

    // Auto-właściwości
    public string Marka { get; set; }
    public string Rejestracja { get; set; }

    // Właściwość z logiką walidacji
    public decimal Przebieg
    {
        get => przebieg;
        set
        {
            if (value >= 0) przebieg = value;
            else Console.WriteLine("Błąd: Przebieg nie może być ujemny!");
        }
    }

    // Składowa statyczna - dostępna dla całej klasy
    public static int Licznik => licznikPojazdow;

    // Konstruktor statyczny - wywoływany raz przy pierwszym użyciu klasy
    static Pojazd()
    {
        licznikPojazdow = 0;
        Console.WriteLine("System floty: zainicjalizowano składowe statyczne.");
    }

    // Konstruktor instancyjny
    public Pojazd(string marka, string rejestracja)
    {
        Marka = marka;
        Rejestracja = rejestracja;
        licznikPojazdow++;
    }

    // Przeciążony konstruktor
    public Pojazd(string marka, string rejestracja, decimal przebiegStart) : this(marka, rejestracja)
    {
        Przebieg = przebiegStart;
    }
}

class Program
{
    static void Main()
    {
        Console.WriteLine($"Wersja oprogramowania: {Pojazd.WersjaSystemu}");

        Pojazd p1 = new Pojazd("Ford", "KR12345");
        Pojazd p2 = new Pojazd("Iveco", "WA98765", 1500.50m);

        Console.WriteLine($"Zarejestrowano {Pojazd.Licznik} pojazdy.");
        Console.WriteLine($"Pojazd 1: {p1.Marka}, Przebieg: {p1.Przebieg} km");
        Console.WriteLine($"Pojazd 2: {p2.Marka}, Przebieg: {p2.Przebieg} km");
    }
}
                    
2.1
Modelowanie geometryczne: klasa Prostokat
Cel

Zapoznanie studentów z podstawami tworzenia własnych typów danych w C#. Głównym celem jest zrozumienie relacji między polami klasy a jej zachowaniem (metodami) oraz opanowanie operacji na polach prywatnych.

Scenariusz

Zlecono Ci stworzenie modułu do przeliczeń geometrycznych dla aplikacji projektowej. Musisz zdefiniować klasę o nazwie "Prostokat", która będzie w stanie przechowywać długości dwóch boków. Obiekt tej klasy powinien być odpowiedzialny za własne obliczenia, takie jak wyznaczanie pola powierzchni oraz obwodu figury. Pola przechowujące wymiary muszą być ukryte przed bezpośrednim dostępem z zewnątrz, aby zapewnić integralność danych (brak możliwości ustawienia boków o długości zero lub ujemnej bez kontroli). Program główny powinien umożliwiać utworzenie kilku różnych prostokątów o wymiarach podanych przez użytkownika w konsoli. Po wprowadzeniu danych, system ma wyświetlić kompletny zestaw informacji dla każdej z figur w czytelny sposób. Implementacja tej klasy jest wstępem do budowania bardziej złożonych silników rysujących i obliczeniowych w przyszłych modułach. Musisz wykazać, że Twoja klasa poprawnie zarządza swoimi właściwościami wewnętrznymi. Program powinien kończyć się podsumowaniem, w którym porównasz pola powierzchni dwóch utworzonych prostokątów. Jest to klasyczny przykład enkapsulacji logiki i danych w pojedynczym komponencie.

Sugerowane kroki do wykonania
  • Stwórz nową klasę o nazwie Prostokat.
  • Dodaj dwa prywatne pola typu double: bokA i bokB.
  • Zaimplementuj publiczne metody SetBoki(double a, double b) do ustawiania wymiarów.
  • Wewnątrz metody ustawiającej dodaj warunek if, blokujący przypisanie wartości mniejszych lub równych 0.
  • Dodaj publiczną metodę ObliczPole(), która zwraca iloczyn boków.
  • Zaimplementuj metodę ObliczObwod(), zwracającą sumę długości wszystkich boków.
  • W metodzie Main utwórz instancję klasy za pomocą słowa kluczowego new.
  • Pobierz wymiary od użytkownika i przekaż je do obiektu za pomocą metody SetBoki.
  • Wywołaj metody obliczeniowe i wyświetl wyniki na konsoli.
  • Spróbuj celowo ustawić niepoprawne dane, aby sprawdzić odporność Twojej klasy na błędy.
WSKAZÓWKI WYKONANIA
  • Zacznij od zdefiniowania klasy Prostokat i nadaj jej modyfikator dostępu public, aby była dostępna z metody Main.
  • Deklaruj dwa prywatne pola (private) typu double o nazwach bokA i bokB – pola prywatne ukrywają implementację przed światem zewnętrznym zgodnie z zasadą enkapsulacji.
  • Pamiętaj, że nazwy pól powinny być zapisane w notacji camelCase (z małej litery), natomiast właściwości i metody w PascalCase (z wielkiej litery) – to konwencja nazewnictwa w C#.
  • Utwórz metodę public void SetBoki(double a, double b) – metoda ta musi być typu void, ponieważ nie zwraca żadnej wartości, tylko modyfikuje stan obiektu.
  • Warunek walidacji w metodzie SetBoki powinien sprawdzać, czy a > 0 AND b > 0 – użyj operatora && dla obu warunków jednocześnie; jeśli którykolwiek bok jest niepoprawny, nie przypisuj wartości.
  • Dla obliczenia pola powierzchni użyj wzoru: pole = bokA * bokB – wynik zwróć przez słowo kluczowe return.
  • Dla obwodu zastosuj wzór: obwód = 2 * (bokA + bokB) – mnożenie przez 2 jest wykonywane przed dodawaniem zgodnie z kolejnością działań matematycznych.
  • W metodzie Main pobieraj dane od użytkownika za pomocą Console.ReadLine(), a następnie konwertuj je na typ double używając Convert.ToDouble().
  • Zawsze sprawdzaj, czy dane wejściowe są poprawne – użyj decimal.TryParse() lub double.TryParse() jako bezpieczniejszej alternatywy, aby uniknąć wyjątków w przypadku błędnych danych.
  • Tworząc obiekt klasy Prostokat, użyj składni: Prostokat p = new Prostokat(); – słowo kluczowe new inicjuje konstruktor domyślny.
  • Aby wywołać metodę obiektu, użyj notacji z kropką: p.SetBoki(5, 3) – najpierw nazwa zmiennej obiektowej, potem kropka, a następnie nazwa metody.
  • Porównując pola dwóch prostokątów, możesz użyć instrukcji warunkowej if: if (p1.ObliczPole() > p2.ObliczPole()) – w ten sposób określisz, który prostokąt ma większe pole.
Ilustracja do zadania
Ilustracja do zadania 2-1
2.2
Bankowość: enkapsulacja konta bankowego
Cel

Opanowanie praktycznego zastosowania właściwości (properties) zamiast bezpośredniego dostępu do pól. Student uczy się implementować logikę biznesową wewnątrz akcesorów get oraz set, zapewniając bezpieczeństwo transakcyjne.

Scenariusz

Budujesz uproszczony system do obsługi platformy bankowości elektronicznej. Twoim zadaniem jest stworzenie klasy "KontoBankowe", która bezpiecznie przechowuje numer konta (tylko do odczytu po utworzeniu) oraz aktualne saldo (dostępne do odczytu, ale chronione przed bezpośrednią modyfikacją). Wszystkie operacje finansowe, takie jak wpłaty i wypłaty środków, muszą odbywać się za pośrednictwem dedykowanych metod, które sprawdzają poprawność transakcji. Na przykład, system nie może pozwolić na wypłacenie kwoty większej niż aktualnie dostępne środki na koncie. Klasa powinna również posiadać właściwość określającą limit debetowy, który jest stały dla wszystkich kont typu podstawowego. Użytkownik programu w konsoli powinien móc wykonać serię operacji: sprawdzić stan konta, wpłacić pieniądze oraz spróbować wypłacić kwotę przekraczającą stan posiadania, aby przetestować systemy obronne. Program musi generować czytelne komunikaty o sukcesie lub przyczynie odrzucenia transakcji. Dzięki temu podejściu dane finansowe klienta pozostają spójne i chronione przed nieautoryzowaną zmianą. Finalnie system powinien pokazać historię operacji w skróconej formie lub aktualne podsumowanie po serii ruchów finansowych.

Sugerowane kroki do wykonania
  • Zdefiniuj klasę KontoBankowe z prywatnym polem saldo.
  • Dodaj właściwość NumerKonta z modyfikatorem init lub ustawianą tylko w konstruktorze.
  • Zaimplementuj właściwość publiczną Saldo, która posiada tylko akcesor get.
  • Utwórz publiczną metodę Wplata(decimal kwota), która powiększa saldo tylko przy kwotach dodatnich.
  • Wprowadź metodę Wyplata(decimal kwota) z logiką sprawdzającą, czy kwota jest mniejsza lub równa saldu.
  • Dodaj do klasy właściwość Wlasciciel korzystając z auto-właściwości { get; set; }.
  • W Main utwórz obiekt konta i nadaj mu dane właściciela.
  • Wykonaj testową wpłatę 1000 zł i wyświetl saldo.
  • Spróbuj wypłacić 1500 zł i upewnij się, że system zablokował tę operację.
  • Wyświetl komunikat końcowy z danymi konta przy użyciu formatowania walutowego.
WSKAZÓWKI WYKONANIA
  • Zdefiniuj klasę KontoBankowe jako publiczną – musi być dostępna z zewnątrz, aby można było tworzyć jej obiekty w metodzie Main.
  • Prywatne pole saldo powinno być typu decimal, ponieważ typ ten zapewnia precyzję obliczeń finansowych (brak błędów zaokrąglenia typowego dla typu float czy double).
  • Właściwość NumerKonta z modyfikatorem init pozwala na ustawienie numeru konta tylko raz – podczas tworzenia obiektu lub w inicjalizatorze obiektu, a potem staje się tylko do odczytu.
  • Właściwość Saldo powinna mieć tylko akcesor get (tylko do odczytu) – nie wolno używać set, ponieważ saldo nie może być modyfikowane bezpośrednio z zewnątrz klasy.
  • Metoda Wplata(decimal kwota) musi najpierw sprawdzić, czy kwota jest dodatnia – użyj warunku if (kwota > 0), a dopiero wtedy dodaj ją do salda.
  • Metoda Wyplata(decimal kwota) powinna sprawdzać dwa warunki: czy kwota jest dodatnia ORAZ czy saldo jest wystarczające (saldo >= kwota) – użyj operatora && do łączenia warunków.
  • Jeśli warunek w metodzie Wyplata nie jest spełniony, wyświetl komunikat błędu i użyj słowa kluczowego return, aby zakończyć metodę bez wykonania wypłaty.
  • Limit debetowy zdefiniuj jako stałą (const) lub statyczne pole readonly, ponieważ jest wspólny dla wszystkich kont i nie powinien się zmieniać w trakcie działania programu.
  • W metodzie Main testuj różne scenariusze: wpłatę poprawną (np. 1000 zł), wypłatę dozwoloną (np. 500 zł), wypłatę przekraczającą saldo (np. 1500 zł) – sprawdź, czy program reaguje poprawnie w każdym przypadku.
  • Formatowanie walutowe w C# można uzyskać za pomocą specyfikatora formatu C lub.ToString("C2") – na przykład Console.WriteLine($"Saldo: {konto.Saldo:C2}") wyświetli walutę w formacie lokalnym.
  • Unikaj używania typu double do przechowywania kwot pieniężnych – może powodować błędy zaokrąglenia (np. 0.1 + 0.2 nie równa się dokładnie 0.3). Używaj typu decimal.
  • Numer konta możesz przekazać przez konstruktor lub przez inicjalizator obiektu: new KontoBankowe { NumerKonta = "1234567890" } – oba sposoby są poprawne.
Ilustracja do zadania
Ilustracja do zadania 2-2
2.3
Narzędzia matematyczne: klasa statyczna
Cel

Zrozumienie koncepcji klas statycznych w C# oraz sytuacji, w których ich użycie jest uzasadnione. Student uczy się projektować biblioteki metod pomocniczych, które nie wymagają tworzenia instancji obiektu do działania.

Scenariusz

Wyobraź sobie, że piszesz uniwersalną bibliotekę narzędziową dla naukowców, która zawiera przydatne konwertery jednostek miar. Zamiast tworzyć za każdym razem nowy obiekt kalkulatora, chcesz, aby metody były dostępne globalnie w całym programie. Twoim zadaniem jest stworzenie klasy statycznej o nazwie "Konwerter", która będzie posiadać metody do zamiany stopni Celsjusza na Fahrenheita oraz kilometrów na mile morskie. Klasa ta powinna również przechowywać publiczne, statyczne stałe, takie jak wartość liczby PI lub przelicznik mili. Użytkownik programu powinien mieć możliwość szybkiego wywołania tych metod bezpośrednio po nazwie klasy w metodzie Main. Program musi pobrać dane wejściowe od użytkownika, wykonać obliczenia za pomocą Twojej statycznej biblioteki i zwrócić wynik z precyzją naukową. Ważne jest, aby zrozumieć, że klasa statyczna nie może posiadać konstruktora instancyjnego ani zwykłych pól nie-statycznych. Implementacja tego zadania uczy poprawnych wzorców projektowych stosowanych przy budowie tzw. "utility classes" w środowisku .NET. Całość powinna być zorganizowana w prosty i logiczny sposób, ułatwiający reużywalność kodu w innych projektach.

Sugerowane kroki do wykonania
  • Zadeklaruj klasę z modyfikatorem static o nazwie Konwerter.
  • Dodaj publiczne, statyczne pole stałe (const) dla przelicznika mil (np. 0.621).
  • Zaimplementuj statyczną metodę CelsjuszToFahrenheit(double c), zwracającą wynik double.
  • Dodaj analogiczną metodę do konwersji kilometrów na mile.
  • Upewnij się, że wszystkie metody wewnątrz klasy również posiadają modyfikator static.
  • W programie głównym spróbuj zaalokować obiekt tej klasy (sprawdź, czy kompilator na to pozwoli).
  • Wywołaj metody konwersji używając składni Konwerter.Metoda(...).
  • Pobierz od użytkownika wartość w kilometrach i wyświetl wynik przeliczenia na mile.
  • Przetestuj konwersję temperatury dla punktu zamarzania i wrzenia wody.
  • Wyświetl komunikat końcowy informujący o zakończeniu korzystania z narzędzi statycznych.
WSKAZÓWKI WYKONANIA
  • Zdefiniuj klasę ze słowem kluczowym static: public static class Konwerter – Tylko klasa oznaczona jako static może zawierać wyłącznie statyczne składowe i nie można jej tworzyć instancji.
  • Wszystkie składowe klasy statycznej muszą być statyczne – zarówno pola, właściwości, jak i metody muszą mieć modyfikator static, w przeciwnym razie kompilator zgłosi błąd.
  • Przelicznik mil morskich na kilometry to około 1,852 – więc przelicznik kilometrów na mile morskie to 1/1,852 = 0,539957 (około 0,54). Możesz użyć stałej const: public const double PrzelicznikMil = 0.539957;
  • Wzór na konwersję Celsjusza na Fahrenheita: F = C * 9/5 + 32 – w kodzie C#: return celsius * 9.0 / 5.0 + 32.0;
  • Wzór na konwersję kilometrów na mile morskie: mile = kilometry * przelicznik – czyli: return kilometry * PrzelicznikMil;
  • Nie możesz utworzyć obiektu klasy statycznej za pomocą new – kompilator wygeneruje błąd "cannot instantiate static class". Próba ta posłuży Ci do zrozumienia, że klasa statyczna jest współdzielona.
  • Metody wywołujesz przez nazwę klasy, nie przez obiekt: double wynik = Konwerter.CelsjuszToFahrenheit(100); – nie potrzebujesz tworzyć żadnego obiektu.
  • Stała PI jest dostępna w klasie Math jako Math.PI – możesz jej użyć lub zdefiniować własną stałą, jeśli wymaga tego zadanie.
  • Formatowanie precyzji naukowej uzyskasz za pomocą metody ToString() z formatem N2 lub E2 – na przykład: Console.WriteLine($"Wynik: {wynik:N2}") wyświetli wynik z dwoma miejscami po przecinku.
  • Punkt zamarzania wody to 0°C (32°F), a punkt wrzenia to 100°C (212°F) – przetestuj te wartości, aby zweryfikować poprawność swojego konwertera.
  • Używaj typu double do konwersji temperatury i odległości, ponieważ są to typy zmiennoprzecinkowe odpowiednie do obliczeń naukowych.
  • Przy konwersji pamiętaj o priorytetach operatorów – mnożenie i dzielenie wykonywane są przed dodawaniem, więc nawiasy nie są potrzebne we wzorze C * 9/5 + 32.
Ilustracja do zadania
Ilustracja do zadania 2-3
2.4
Kadry: zarządzanie danymi pracownika (konstruktory)
Cel

Opanowanie mechanizmu przeciążania konstruktorów oraz efektywnego wykorzystania słowa kluczowego this do unikania powielania kodu inicjalizacyjnego. Student uczy się poprawnie zarządzać stanem początkowym obiektów klasy.

Scenariusz

System HR nowo założonego startupu wymaga elastycznego sposobu dodawania nowych pracowników do bazy danych. Nie wszyscy pracownicy mają od razu przypisane stanowisko czy określone wynagrodzenie w momencie podpisywania wstępnych dokumentów. Musisz zaprojektować klasę "Pracownik", która pozwala na utworzenie obiektu na trzy różne sposoby. Pierwszy wariant wymaga podania tylko imienia i nazwiska (wynagrodzenie ustawiane jest na płacę minimalną). Drugi wariant pozwala podać imię, nazwisko oraz konkretne wynagrodzenie, jeśli jest ono znane od początku. Trzeci, najbardziej rozbudowany konstruktor, przyjmuje wszystkie dane wraz z nazwą działu, w którym osoba będzie pracować. Twoim celem jest połączenie tych konstruktorów w taki sposób, aby podstawowe przypisania działy się w jednym miejscu, a inne konstruktory jedynie „dopełniały” dane (tzw. constructor chaining). Program w konsoli powinien utworzyć trzech różnych pracowników, korzystając z każdego z dostępnych konstruktorów, a następnie wyświetlić ich dane jako sformatowane karty pracownicze. To zadanie uczy, jak projektować API klas, które jest przyjazne dla innych programistów i odporne na brakujące dane startowe.

Sugerowane kroki do wykonania
  • Zdefiniuj klasę Pracownik z odpowiednimi polami instancyjnymi.
  • Utwórz najbardziej ogólny konstruktor przyjmujący wszystkie parametry (Imię, Nazwisko, Pensja, Dzial).
  • Dodaj konstruktor przyjmujący Imię i Nazwisko, który przekazuje resztę danych do głównego konstruktora za pomocą : this(...).
  • Implementując : this(...), ustaw domyślną pensję na np. 4000 a dział na "Nieprzypisany".
  • Dodaj wersję pośrednią konstruktora przyjmującą Imię, Nazwisko i Pensję.
  • Wewnątrz klasy stwórz metodę PokazDane(), która wypisze czytelny raport o osobie.
  • W Main utwórz obiekt „p1" podając tylko dane osobowe.
  • Utwórz obiekt „p2" z pełnymi danymi (np. Jan Kowalski, 8000, IT).
  • Wywołaj PokazDane dla wszystkich rekordów w bazie.
  • Zwróć uwagę na oszczędność kodu dzięki kaskadowemu wywoływaniu konstruktorów.
WSKAZÓWKI WYKONANIA
  • Zdefiniuj klasę Pracownik z polami: private string imie, private string nazwisko, private decimal pensja, private string dzial – wszystkie pola powinny być prywatne.
  • Utwórz najpełniejszy konstruktor (tzw. konstruktor bazowy) jako pierwszy – powinien przyjmować wszystkie cztery parametry i przypisywać je bezpośrednio do pól klasy.
  • Konstruktor bazowy NIE powinien używać słowa kluczowego this, ponieważ on jest ostatecznym miejscem wykonywania inicjalizacji – inne konstruktory będą go wywoływać.
  • Konstruktor z dwoma parametrami (Imię, Nazwisko) powinien użyć składni : this(...) do wywołania konstruktora bazowego z domyślnymi wartościami.
  • Składnia constructor chaining wygląda tak: public Pracownik(string imie, string nazwisko) : this(imie, nazwisko, 4000, "Nieprzypisany") – użyj this() z listą argumentów dla pełnego konstruktora.
  • Domyślna pensja minimalna w 2024 roku w Polsce to około 4000 PLN brutto – możesz użyć stałej lub wpisać wartość bezpośrednio.
  • Konstruktor pośredni (z trzema parametrami: Imię, Nazwisko, Pensja) również używa : this(imie, nazwisko, pensja, "Nieprzypisany").
  • Słowo kluczowe this w kontekście konstruktora służy do wywołania innego konstruktora w tej samej klasie – jest to tzw. constructor chaining lub delegacja konstruktora.
  • Metoda PokazDane() powinna wyświetlać wszystkie informacje o pracowniku w czytelnym formacie – użyj Console.WriteLine() z interpolacją stringów ($"...").
  • W metodzie Main utwórz trzy obiekty: new Pracownik("Jan", "Kowalski"), new Pracownik("Anna", "Nowak", 5000), new Pracownik("Piotr", "Wiśniewski", 8000, "IT").
  • Sprawdź, czy każdy pracownik otrzymuje domyślne wartości dla brakujących parametrów – powinieneś widzieć, że kod nie jest powielany, a wszystkie przypisania są w jednym miejscu (konstruktorze bazowym).
  • Zaletą tego wzorca jest łatwość modyfikacji – jeśli zmienisz strukturę danych pracownika, update robisz tylko w jednym miejscu (konstruktorze bazowym), a wszystkie przeciążenia automatycznie dziedziczą zmianę.
Ilustracja do zadania
Ilustracja do zadania 2-4
2.5
Konfiguracja systemu: konstruktor statyczny
Cel

Zrozumienie specjalnej roli konstruktora statycznego w inicjalizacji środowiska uruchomieniowego aplikacji. Student uczy się odróżniać inicjalizację danych wspólnych (klasowych) od danych specyficznych dla konkretnych instancji.

Scenariusz

Budujesz zaawansowany "SystemKonfiguracyjny" dla aplikacji serwerowej, która przy starcie musi załadować pewne parametry globalne, takie jak adres serwera bazy danych oraz aktualna strefa czasowa. Te parametry są identyczne dla każdego modułu systemu i nie zmieniają się w trakcie jego działania. Twoim zadaniem jest stworzenie klasy, która przy pierwszej próbie użycia (np. odczytu dowolnego parametru) automatycznie wykona jednorazową konfigurację za pomocą konstruktora statycznego. Konstruktor ten ma symulować odczyt danych z pliku (poprzez wyświetlenie komunikatu w konsoli) i zainicjalizować statyczne pola klasy. Dodatkowo klasa powinna pozwalać na tworzenie obiektów reprezentujących konkretne sesje użytkowników, które posiadają własne, unikalne identyfikatory. Musisz wykazać, że moment wywołania konstruktora statycznego jest niezależny od tworzenia poszczególnych obiektów tej klasy. Wyświetl logi systemowe tak, aby było widać kolejność: najpierw inicjalizacja globalna, potem start konkretnych sesji. To zadanie ma kluczowe znaczenie dla zrozumienia cyklu życia aplikacji w modelu pamięciowym .NET. Program kończy się wyświetleniem parametrów, które zostały poprawnie załadowane tylko jeden raz.

Sugerowane kroki do wykonania
  • Stwórz klasę SystemKonfiguracyjny (nie musi być statyczna jako całość).
  • Dodaj statyczne pole string AdresSerwera oraz statyczną właściwość DataStartu.
  • Zaimplementuj konstruktor statyczny (bez parametrów i modyfikatorów dostępu!).
  • Wewnątrz konstruktora statycznego przypisz adres serwera i zrób Console.WriteLine logu.
  • Dodaj zwykły konstruktor instancyjny, który przypisuje unikalny ID sesji.
  • W Main wypisz najpierw statyczną stałą lub właściwość klasy.
  • Zauważ, że konstruktor statyczny uruchomił się samoistnie przed wypisaniem danych.
  • Utwórz dwa obiekty sesji i zobacz, czy konstruktor statyczny wywoła się ponownie (nie powinien).
  • Wypisz stan wszystkich składowych dla obu sesji.
  • Podsumuj działanie mechanizmu statycznego odpowiednim komunikatem.
WSKAZÓWKI WYKONANIA
  • Zdefiniuj klasę jako zwykłą (nie static), ale z polami i konstruktorem statycznym – klasa nie musi być statyczna, aby posiadać konstruktor statyczny.
  • Konstruktor statyczny deklarujesz przez static SystemKonfiguracyjny() { } – NIE posiada modyfikatora dostępu (ani public, ani private) i NIE przyjmuje parametrów.
  • Konstruktor statyczny wywołuje się AUTOMATYCZNIE dokładnie raz, gdy po raz pierwszy użyjesz klasę lub tworzysz jej instancję – nie można go wywołać ręcznie.
  • Dodaj statyczne pole private static string adresSerwera; – pole statyczne jest wspólne dla wszystkich obiektów klasy i istnieje na poziomie klasy, nie instancji.
  • Również dodaj statyczną właściwość (property) do odczytu adresu serwera: public static string AdresSerwera => adresSerwera;
  • W konstruktorze statycznym umieść logikę inicjalizacji: adresSerwera = "localhost:1433"; i Console.WriteLine("Inicjalizacja systemu konfiguracyjnego...");
  • Konstruktor instancyjny (ten z modyfikatorem public) służy do tworzenia obiektów sesji – musi mieć inną sygnaturę niż konstruktor statyczny.
  • Unikalny ID sesji możesz generować za pomocą statycznego licznika inkrementowanego przy każdym wywołaniu konstruktora instancyjnego.
  • Aby wywołać konstruktor statyczny, wystarczy odwołać się do dowolnego elementu statycznego klasy (np. Console.WriteLine(SystemKonfiguracyjny.AdresSerwera);).
  • Sprawdź kolejność logów w konsoli – konstruktor statyczny powinien uruchomić się PRZED pierwszą sesją, ponieważ jest wywoływany lazy loading (z opóźnieniem) przy pierwszym użyciu klasy.
  • Utwórz dwa obiekty sesji: var sesja1 = new SystemKonfiguracyjny(); var sesja2 = new SystemKonfiguracyjny(); – konstruktor statyczny NIE wywoła się ponownie.
  • Jeśli potrzebujesz jawnego wywołania logiki statycznej, możesz dodać statyczną metodę Inicjalizuj() – ale w tym zadaniu konstruktor statyczny powinien wystarczyć.
  • Przydatną stałą jest DateTime do przechowywania daty i czasu startu systemu – możesz ją zapisać jako private static DateTime dataStartu;
Ilustracja do zadania
Ilustracja do zadania 2-5
2.6
IoT: cyfrowy termometr z historią (właściwości)
Cel

Integracja wiedzy o właściwościach, tablicach oraz logice wewnątrz akcesorów set. Student uczy się projektowania obiektów, które nie tylko przechowują stan, ale również reagują na zmiany tego stanu w sposób inteligentny.

Scenariusz

Jesteś inżynierem oprogramowania projektującym inteligentny termometr domowy (IoT). Urządzenie posiada klasę "Termometr", która przechowuje aktualną temperaturę, ale system wymaga, aby każda zmiana tej wartości była automatycznie zapisywana jako najwyższa zanotowana temperatura (rekord), jeśli nowa wartość jest większa od poprzedniej. Musisz wykorzystać właściwość C# do implementacji tej logiki: przy każdym przypisaniu nowej temperatury, program sprawdza, czy nie pobito rekordu. Dodatkowo termometr powinien informować, jeśli nowa temperatura przekracza bezpieczny zakres (np. powyżej 40 stopni Celsjusza) – informacja taka powinna być wypisana w konsoli jako ostrzeżenie. Użytkownik w pętli symuluje odczyty z sensora w ciągu godziny. Twoim zadaniem jest zapewnienie dodatkowej właściwości o nazwie "Srednia", która jest obliczana na bieżąco (lub po zakończeniu sesji) jako średnia z ostatnich trzech pomiarów zachowanych w małej tablicy wewnętrznej. To zadanie łączy wiedzę o enkapsulacji z prostymi algorytmami monitoringu procesów fizycznych. Finalny raport prezentuje aktualny odczyt, rekord historyczny oraz status bezpieczeństwa urządzenia. Program kończy się, gdy użytkownik wprowadzi specjalny kod wyłączający symulację sensora.

Sugerowane kroki do wykonania
  • Stwórz klasę Termometr z prywatnym polem aktualnaTemp oraz rekord.
  • Zaimplementuj właściwość AktualnaTemperatura z pełnym ciałem akcesora set.
  • W akcesorze set dodaj warunek: jeśli value > rekord, to rekord = value.
  • Dopisz ostrzeżenie w konsoli wewnątrz właściwości, jeśli temperatura przekroczy 40.0.
  • Dodaj publiczną właściwość tylko do odczytu (get) dla pola rekord.
  • W Main utwórz instancję termometru.
  • Uruchom pętlę pobierającą 5 pomiarów od użytkownika w jednej sesji.
  • Po każdym wprowadzeniu danych, przypisz je do właściwości AktualnaTemperatura.
  • Wyświetl bieżącą wartość oraz aktualnie najwyższy wynik (rekord).
  • Upewnij się, że Twoja klasa poprawnie separuje logikę rekordów od zwykłego przechowywania danych.
WSKAZÓWKI WYKONANIA
  • Zdefiniuj klasę Termometr i dodaj dwa prywatne pola: private double aktualnaTemp i private double rekordTemperatury – oba pola przechowują stan termometru.
  • Pole rekordTemperatury powinno być zainicjalizowane na minimalną wartość (np. double.MinValue lub -273.15 dla temperatury absolutnej), aby pierwszy pomiar zawsze był traktowany jako rekord.
  • Zaimplementuj właściwość (property) z pełną definicją get i set, nie auto-właściwością, ponieważ potrzebujesz logiki biznesowej w akcesorze set.
  • Składnia właściwości z logiką: public double AktualnaTemperatura { get { return aktualnaTemp; } set { ... } } – w miejsce kropek wstawiasz logikę walidacji.
  • W akcesorze set sprawdzasz najpierw, czy nowa wartość (zmienna 'value') jest większa od aktualnego rekordu: if (value > rekordTemperatury) rekordTemperatury = value;
  • Następnie przypisujesz wartość do pola: aktualnaTemp = value; – zawsze po sprawdzeniu warunków.
  • Ostrzeżenie o przekroczeniu temperatury bezpiecznej dodajesz po sprawdzeniu rekordu: if (value > 40.0) Console.WriteLine("UWAGA: Przekroczono bezpieczną temperaturę 40°C!");
  • Dla odczytu temperatury maksymalnej dodaj właściwość tylko do odczytu: public double Rekord => rekordTemperatury; – brak set oznacza właściwość tylko do odczytu.
  • W metodzie Main utwórz pętlę for lub while do pobierania pomiarów – na przykład: for (int i = 0; i < 5; i++) { ... }
  • Pobieraj temperaturę od użytkownika: double temp = Convert.ToDouble(Console.ReadLine()); i przypisz do właściwości: termometr.AktualnaTemperatura = temp;
  • Wyświetl informacje po każdym pomiarze: Console.WriteLine($"Aktualna: {termometr.AktualnaTemperatura}, Rekord: {termometr.Rekord}");
  • Aby obliczyć średnią z trzech ostatnich pomiarów, możesz użyć tablicy: private double[] ostatniePomiary = new double[3]; wewnątrz akcesora set dodawaj nową wartość do tablicy i przesuwaj starsze wartości.
  • Wzor na średnią: (pomiary[0] + pomiary[1] + pomiary[2]) / 3.0 – pamiętaj o rzutowaniu na double przed dzieleniem lub użyj 3.0 zamiast 3.
  • Pętla kończy się po wprowadzeniu specjalnego kodu (np. 'q' lub '-1') – sprawdź ten warunek przed próba konwersji na liczbę, aby uniknąć wyjątku FormatException.
Ilustracja do zadania
Ilustracja do zadania 2-6