01
Zadanie wprowadzające: System zarządzania zasobami IT
Cel

Student demonstruje umiejętność implementacji dziedziczenia oraz polimorfizmu poprzez tworzenie hierarchii klas z wykorzystaniem słów kluczowych virtual, override oraz base. Zadanie ma na celu pokazanie praktycznego zastosowania indeksatorów do przeszukiwania zbiorów obiektów oraz wykorzystania modyfikatorów out i params w metodach zarządzających zasobami firmy.

Scenariusz

Zostałeś poproszony o przygotowanie szkieletu systemu do ewidencji sprzętu komputerowego w dużej korporacji. System musi bazować na ogólnej klasie opisującej zasób (np. model, numer seryjny), od której będą dziedziczyły klasy szczegółowe, takie jak Komputer i Serwer. Każdy typ zasobu posiada własny sposób generowania opisu technicznego, co wymaga zastosowania metod wirtualnych i ich nadpisywania w klasach pochodnych. Dodatkowo musisz stworzyć klasę KontrolerZasobow, która będzie pełniła rolę kontenera na obiekty i udostępni wygodny dostęp do nich za pomocą indeksatora (wyszukiwanie po numerze seryjnym lub indeksie). System powinien również oferować metodę statyczną do szybkiego liczenia statystyk, wykorzystując parametr out do zwrócenia wielu wyników naraz (np. liczbę serwerów i stacji roboczych). Kolejnym wymaganiem jest funkcja do masowego dodawania tagów opisowych do konkretnego zasobu, w której wykorzystasz parametr params. Program w konsoli powinien utworzyć kilka zróżnicowanych obiektów, dodać je do kontrolera, a następnie przeprowadzić serię testów: wyszukanie zasobu, zmianę jego statusu oraz wyświetlenie polimorficznego raportu. Całość rozwiązania musi być odporna na typowe błędy i stanowić modelowy przykład zastosowania czystego kodu obiektowego w C#.

Sugerowane kroki do wykonania
  1. Zdefiniuj klasę bazową Zasob z polami: Model i NumerSeryjny oraz wirtualną metodą PokazInfo().
  2. Zaimplementuj klasę pochodną Komputer, która dodaje pole RodzajProcesora i nadpisuje metodę bazową.
  3. Stwórz klasę pochodną Serwer, wywołującą konstruktor bazowy za pomocą base i dodającą specyficzne dane (np. liczba dysków).
  4. Zaprojektuj klasę KontrolerZasobow przechowującą tablicę obiektów klasy Zasob.
  5. Dodaj do Kontrolera indeksator public Zasob this[int index], który zwraca zasób o podanym indeksie.
  6. Zaimplementuj metodę do dodawania tagów (DodajTagi) przyjmującą dowolną liczbę argumentów typu string (parametr params).
  7. Utwórz metodę statyczną ObliczStatystyki, która przez parametry out zwróci liczbę obiektów każdego typu.
  8. W metodzie Main utwórz instancje klas pochodnych i przypisz je do wspólnej tablicy typu Zasob[].
  9. Przetestuj polimorfizm, wywołując metodę PokazInfo() w pętli dla każdego elementu tablicy.
  10. Zademonstruj działanie indeksatora oraz metody z wieloma parametrami wyjściowymi (out).
Kod rozwiązania
using System;

class Zasob
{
    public string Model { get; set; }
    public string SN { get; set; }

    public Zasob(string model, string sn) { Model = model; SN = sn; }

    public virtual void PokazInfo() => Console.WriteLine($"Zasób: {Model}, SN: {SN}");
}

class Komputer : Zasob
{
    public string Procesor { get; set; }
    
    public Komputer(string model, string sn, string cpu) : base(model, sn) { Procesor = cpu; }

    public override void PokazInfo() => Console.WriteLine($"PC: {Model}, CPU: {Procesor}");
}

class Serwer : Zasob
{
    public int LiczbaDyskow { get; set; }

    public Serwer(string model, string sn, int dyski) : base(model, sn) { LiczbaDyskow = dyski; }

    public override void PokazInfo() => Console.WriteLine($"Serwer: {Model}, Dyski: {LiczbaDyskow}");
}

class KontrolerZasobow
{
    private Zasob[] dane = new Zasob[10];
    private int licznik = 0;

    public void Dodaj(Zasob z) { if (licznik < dane.Length) dane[licznik++] = z; }

    // Indeksator
    public Zasob this[int i]
    {
        get {
            if (i < 0 || i >= licznik) {
                Console.WriteLine("Błąd: Pozycja poza zakresem.");
                return null;
            }
            return dane[i];
        }
    }

    // Metoda z params
    public static void DodajTagi(string model, params string[] tagi)
    {
        Console.WriteLine($"Zasób {model} otrzymał tagi: {string.Join(", ", tagi)}");
    }

    // Metoda statyczna z out
    public static void ObliczStatystyki(Zasob[] lista, out int kompy, out int serwery)
    {
        kompy = 0; serwery = 0;
        foreach (var z in lista) {
            if (z is Komputer) kompy++;
            else if (z is Serwer) serwery++;
        }
    }
}

class Program
{
    static void Main()
    {
        KontrolerZasobow kz = new KontrolerZasobow();
        kz.Dodaj(new Komputer("Dell XPS", "SN001", "i9"));
        kz.Dodaj(new Serwer("HP ProLiant", "SN002", 8));
        kz.Dodaj(new Zasob("Monitor LG", "SN003"));

        Console.WriteLine("--- Raport Polimorficzny ---");
        for(int i=0; i < 3; i++) {
            var z = kz[i];
            z?.PokazInfo();
        }

        Console.WriteLine("\n--- Test Params ---");
        KontrolerZasobow.DodajTagi("Dell XPS", "BIURO", "IT-DEPT", "PILNE");

        int c, s;
        Zasob[] testowaTablica = { new Komputer("A","1","i3"), new Serwer("B","2",4) };
        KontrolerZasobow.ObliczStatystyki(testowaTablica, out c, out s);
        Console.WriteLine($"Statystyki: Komputery: {c}, Serwery: {s}");
    }
}
3.1
Matematyczne metody narzędziowe (out i params)
Cel

Zapoznanie studenta z nietypowymi sposobami przekazywania argumentów do metod w C#. Celem zadania jest opanowanie zwracania wielu wyników jednocześnie oraz projektowanie metod akceptujących dowolną liczbę parametrów wejściowych.

Scenariusz

Zlecono Ci przygotowanie zestawu metod pomocniczych dla silnika obliczeniowego, który jest wykorzystywany przez programistów w innym dziale. Pierwsza metoda ma za zadanie podzielić dwie liczby całkowite, ale musi jednocześnie zwrócić iloraz oraz resztę z dzielenia przy użyciu modyfikatora out. Takie podejście pozwala na uniknięcie pisania dwóch oddzielnych metod dla jednej operacji matematycznej. Kolejnym Twoim zadaniem jest stworzenie uniwersalnego sumatora, który przyjmuje dowolną liczbę liczb całkowitych (typ int) jako parametry, wykorzystując słowo kluczowe params. Dzięki temu użytkownik silnika będzie mógł wywołać metodę przekazując jej dwie, pięć lub nawet zero liczb bez konieczności jawnego tworzenia tablicy. Musisz zadbać o poprawną obsługę błędów, na przykład próbę dzielenia przez zero w pierwszej metodzie. Program w konsoli powinien zaprezentować działanie obu metod dla różnych przypadków testowych. Finalne rozwiązanie ma być proste, szybkie i zgodne ze standardami projektowania metod w środowisku .NET.

Sugerowane kroki do wykonania
  1. Stwórz klasę o nazwie KalkulatorZaawansowany.
  2. Dodaj metodę DzielenieZReszta przyjmującą dwie liczby typu int.
  3. W sygnaturze metody zdefiniuj dwa parametry wyjściowe: out int iloraz oraz out int reszta.
  4. Wewnątrz metody wykonaj obliczenia i przypisz wyniki do odpowiednich parametrów out.
  5. Zaimplementuj metodę SumujWiele, która używa modyfikatora params int[] liczby.
  6. Przejdź przez otrzymaną tablicę w pętli i oblicz sumę wszystkich jej elementów.
  7. W metodzie Main wywołaj DzielenieZReszta i przechwyć oba wyniki do nowo utworzonych zmiennych.
  8. Wywołaj metodę SumujWiele przekazując jej bezpośrednio kilka liczb (np. 1, 4, 7, 2).
  9. Wyświetl wszystkie uzyskane wyniki z opisem co oznaczają poszczególne liczby.
  10. Zweryfikuj zachowanie sumatora przy wywołaniu go bez podania żadnych argumentów.
WSKAZÓWKI DO WYKONANIA
  • Nazwij klasę KalkulatorZaawansowany – nazwa powinna odzwierciedlać przeznaczenie klasy, czyli zaawansowane operacje matematyczne.
  • Przed rozpoczęciem kodowania dokładnie przemyśl sygnaturę metody DzielenieZReszta – musi przyjmować dokładnie dwa parametry wejściowe (dzielna i dzielnik typu int) oraz dwa parametry wyjściowe oznaczone słowem kluczowym out.
  • Pamiętaj, że parametry out muszą być przypisane wewnątrz metody przed jej zakończeniem – kompilator wymusi to, więc zawsze przypisz obie wartości (iloraz i resztę) w każdym scenariuszu.
  • Zabezpiecz metodę przed dzieleniem przez zero – sprawdź warunek if (dzielnik == 0) i w takim przypadku ustaw iloraz na 0 oraz poinformuj o błędzie, np. przez Console.WriteLine lub wyjątek.
  • Do obliczania reszty z dzielenia użyj operatora modulo: reszta = dzielna % dzielnik, a ilorazu: iloraz = dzielna / dzielnik.
  • Przy tworzeniu metody SumujWiele zastosuj składnię params int[] liczby jako ostatni parametr metody – params musi być ostatnim parametrem w sygnaturze.
  • Pamiętaj, że parametr params tworzy tablicę automatycznie, więc użytkownik może przekazać argumenty w dowolnej ilości: SumujWiele(1, 2, 3) lub SumujWiele(new int[] {1, 2, 3}).
  • Przejdź przez tablicę uzyskaną z params w pętli foreach lub for i sumuj elementy do zmiennej akumulatorowej.
  • Obsłuż przypadek braku argumentów – gdy tablica params ma długość 0, metoda powinna zwrócić 0 lub odpowiedni komunikat.
  • W metodzie Main przy wywołaniu metody z out musisz zainicjalizować zmienne docelowe przed wywołaniem lub użyć out z nowymi zmiennymi: DzielenieZReszta(10, 3, out int iloraz, out int reszta).
  • Przetestuj metodę SumujWiele z różną liczbą argumentów: zero argumentów, jeden argument, pięć argumentów – upewnij się, że działa poprawnie w każdym przypadku.
  • Wyświetlaj wyniki z jasnymi opisami, np. Console.WriteLine($"Iloraz: {iloraz}, Reszta: {reszta}") aby użytkownik wiedział, co oznaczają poszczególne wartości.
Ilustracja do zadania 3-1
3.2
Elektroniczny magazyn części (indeksatory)
Cel

Zrozumienie mechanizmu indeksowania obiektów jako sposobu na imitowanie zachowania tablicy wewnątrz własnej klasy. Student uczy się pisać akcesory get i set dla indeksatorów oraz zarządzać bezpiecznym dostępem do kolekcji wewnętrznej.

Scenariusz

Zarządzasz bazą danych części zamiennych w warsztacie samochodowym. Zamiast operować na surowej tablicy stringów, chcesz stworzyć klasę "Magazyn", która w sposób inteligentny zarządza zapasami. Twoim zadaniem jest dodanie do tej klasy indeksatora, który pozwoli na pobranie lub zmianę nazwy części za pomocą czytelnej składni: magazyn[3] = "Świeca zapłonowa". Indeksator powinien weryfikować, czy podany indeks mieści się w dopuszczalnym zakresie, i informować o błędzie, jeśli użytkownik spróbuje wyjść poza granice zadeklarowanej pamięci. Możesz również spróbować zaimplementować drugi, przeciążony indeksator, który pozwala wyszukać numer półki na podstawie nazwy części (indeksowanie stringiem). Program w konsoli powinien umożliwić użytkownikowi wypełnienie magazynu kilkoma pozycjami, a następnie ich szybką modyfikację poprzez wspomniany indeksator. To podejście znacznie upraszcza czytanie kodu i sprawia, że Twoje obiekty zachowują się jak natywne kolekcje języka C#. Finalnie system powinien wyświetlić listę wszystkich dostępnych części sformatowaną w kolumnach z odpowiadającymi im numerami indeksów.

Sugerowane kroki do wykonania
  1. Zdefiniuj klasę Magazyn zawierającą prywatną tablicę stringów o rozmiarze 10 elementów.
  2. Dodaj publiczny indeksator korzystający ze składni: public string this[int index].
  3. Wewnątrz akcesora get dodaj warunek sprawdzający granice tablicy (0 do 9).
  4. Wewnątrz akcesora set dodaj analogiczną walidację przed przypisaniem wartości do pola value.
  5. Zapewnij, aby próba dostępu do błędnego indeksu zwracała komunikat tekstowy "Błędna pozycja".
  6. W Main utwórz jeden obiekt klasy Magazyn.
  7. Przypisz wartości do trzech dowolnych indeksów przy użyciu prostej składni tablicowej.
  8. Wyświetl te wartości odczytując je bezpośrednio przez indeksator w pętli.
  9. Spróbuj odczytać wartość z indeksu 15 i zobacz, jak Twój program reaguje na ten błąd.
  10. Dodaj prosty licznik zajętych miejsc w magazynie i wyświetl go na końcu.
WSKAZÓWKI DO WYKONANIA
  • Zadeklaruj wewnątrz klasy Magazyn prywatną tablicę stringów, np. private string[] dostepneCzesci = new string[10] – rozmiar 10 to minimum, ale możesz użyć większego.
  • Nazwij tablicę opisowo, np. dostepneCzesci lub czesci, aby kod był czytelny dla innych programistów.
  • Indeksator definiujesz za pomocą specjalnej składni: public string this[int index] { get {...} set {...} } – słowo kluczowe this oznacza, że to właśnie jest indeksator.
  • W akcesorze get najpierw sprawdź warunek granic: if (index < 0 || index >= dostepneCzesci.Length) – jeśli indeks jest poza zakresem, zwróć null lub komunikat o błędzie.
  • W akcesorze set parametr value reprezentuje wartość przypisywaną – użyj go do przypisania: dostepneCzesci[index] = value, ale najpierw zwaliduj indeks.
  • Unikaj duplikowania kodu walidacji – rozważ wyodrębnienie warunku granic do osobnej metody prywatnej lub obsługę błędu przez wyjątek.
  • Jeśli chcesz zaimplementować drugi indeksator wyszukujący po nazwie, użyj przeciążenia: public int this[string nazwa] – zwróci numer indeksu lub -1 gdy nie znaleziono.
  • Do wyszukiwania nazwy w tablicy użyj pętli for lub metody Array.IndexOf(), która zwraca indeks szukanego elementu.
  • W metodzie Main od razu po utworzeniu obiektu przypisz kilka wartości, np. magazyn[0] = "Olej silnikowy"; – indeksator zachowuje się jak tablica.
  • Do wyświetlania zawartości użyj pętli for z wbudowaną właściwością Length tablicy – nie zakoduj na twardo rozmiaru 10, bo to utrudni późniejsze zmiany.
  • Przetestuj błędne indeksy celowo – wywołaj magazyn[15] aby zobaczyć, jak Twój program reaguje na wartość spoza zakresu.
  • Zadbaj o formatowanie wyjścia – użyj Console.WriteLine($"{i}: {magazyn[i]}") aby wynik był czytelny i łatwy do interpretacji przez użytkownika.
Ilustracja do zadania 3-2
3.3
Pojazdy i silniki: podstawy dziedziczenia
Cel

Opanowanie mechanizmu dziedziczenia oraz poprawnego wywoływania konstruktorów bazowych za pomocą base. Student uczy się hierarchicznego modelowania danych oraz reużywania kodu z klas nadrzędnych.

Scenariusz

Tworzysz system sterowania dla nowoczesnego garażu wielopoziomowego. Musisz przygotować strukturę klas opisującą różne typy pojazdów, zaczynając od ogólnej klasy "Pojazd", która posiada markę oraz typ napędu. Następnie stwórz klasę pochodną "Samochod", która dziedziczy wszystkie te cechy, ale dodaje od siebie unikalne pole, takie jak liczba drzwi czy pojemność bagażnika. Ważnym elementem jest to, aby konstruktor samochodu nie powielał logiki przypisywania marki, lecz przekazywał te dane „do góry" – do klasy nadrzędnej przy użyciu słowa base. Dzięki temu zapewnisz, że każde auto jest przede wszystkim poprawnym obiektem typu pojazd. Twoim zadaniem jest również nadpisanie podstawowej metody wyświetlającej informacje o obiekcie tak, aby samochód prezentował dodatkowe, specyficzne dla niego parametry. Program w konsoli musi utworzyć egzemplarz ogólnego pojazdu oraz egzemplarz konkretnego samochodu i porównać ich zachowanie. Takie podejście pozwala na łatwe dodawanie w przyszłości nowych typów transportu, np. motocykli czy ciężarówek, bez modyfikacji istniejącego silnika systemu. Finalnie wyświetl zestawienie obu obiektów, zwracając uwagę na różnice w ich opisach.

Sugerowane kroki do wykonania
  1. Utwórz klasę Pojazd z polami Marka i TypPaliwa.
  2. Zdefiniuj konstruktor klasy Pojazd, który inicjalizuje te pola.
  3. Zaimplementuj klasę Samochod dziedziczącą po klasie Pojazd (użyj operatora dwukropka).
  4. Dodaj w klasie Samochod dodatkowe pole LiczbaDrzwi.
  5. Stwórz konstruktor dla klasy Samochod przyjmujący trzy parametry.
  6. W wywołaniu konstruktora użyj : base(marka, paliwo), aby zainicjalizować bazę.
  7. Zdefiniuj w klasie bazowej wirtualną metodę PrzedstawSie().
  8. Nadpisz (override) tę metodę w Samochodzie, dodając informację o liczbie drzwi.
  9. W metodzie Main utwórz oba obiekty podając im przykładowe dane.
  10. Wywołaj PrzedstawSie na obu obiektach i zaobserwuj różnice w wyniku.
WSKAZÓWKI DO WYKONANIA
  • Zacznij od klasy bazowej Pojazd – zdefiniuj w niej dwa pola: Marka (string) i TypPaliwa (string) jako właściwości lub pola publiczne.
  • Klasa bazowa powinna mieć konstruktor z parametrami, który inicjalizuje te pola – dzięki temu klasy pochodne będą mogły przekazać wartości przez base.
  • W klasie bazowej zdefiniuj metodę jako virtual, np. public virtual void PrzedstawSie() – bez virtual nie można jej będzie nadpisać w klasach pochodnych.
  • Przy tworzeniu klasy pochodnej użyj składni: class Samochod : Pojazd – dwukropek oznacza dziedziczenie w C#.
  • Dodaj nowe pole LiczbaDrzwi (int) w klasie Samochod – to pole jest specyficzne tylko dla samochodu, a nie dla innych pojazdów.
  • Konstruktor Samochoda musi mieć trzy parametry: markę, typ paliwa i liczbę drzwi – wywołaj : base(marka, typPaliwa) w pierwszej linii konstruktora.
  • Przykład poprawnej składni konstruktora: public Samochod(string marka, string paliwo, int drzwi) : base(marka, paliwo) { LiczbaDrzwi = drzwi; }
  • Nadpisz metodę PrzedstawSie() w klasie Samochod używając override: public override void PrzedstawSie() – dzięki temu wywołanie na obiekcie typu Pojazd uruchomi właściwą wersję.
  • W metodzie PrzedstawSie klasy Samochod rozważ użycie base.PrzedstawSie() na początku, aby najpierw wyświetlić informacje dziedziczone, a potem dopisać specyficzne.
  • W Main możesz tworzyć obiekty na dwa sposoby: Pojazd p = new Pojazd(...) oraz Samochod s = new Samochod(...) – oba są poprawne.
  • Przetestuj polimorfizm: przypisz Samochod do zmiennej typu Pojazd: Pojazd p = new Samochod(...); a następnie wywołaj p.PrzedstawSie() – powinna wyświetlić się wersja z Samochodu.
  • Zachowaj porządek w kodzie – umieszczaj klasy jedna po drugiej, a nie mieszaj definicje z kodem wykonawczym Main.
Ilustracja do zadania 3-3
3.4
System premiowy korporacji (polimorfizm)
Cel

Praktyczne zastosowanie polimorfizmu do rozwiązywania problemów biznesowych. Student uczy się, jak jedna metoda wirtualna zadeklarowana w klasie bazowej może zachowywać się w różny sposób w zależności od rzeczywistego typu obiektu.

Scenariusz

W dziale księgowości dużej firmy system naliczania premii rocznych działa według różnych reguł dla różnych stanowisk. Każdy pracownik jest reprezentowany przez klasę "Pracownik", która posiada metodę wirtualną o nazwie ObliczPremie. Dla przeciętnego pracownika biurowego premia wynosi stałe 10% wynagrodzenia bazowego. Musisz jednak stworzyć dwie klasy pochodne: "Dyrektor" oraz "Sprzedawca". Dla dyrektora premia powinna być powiększona o dodatkowy bonus stały za wyniki pionu, natomiast dla sprzedawcy jest ona uzależniona od obrotu, jaki wygenerował w ciągu roku. Twoim zadaniem jest nadpisanie metody ObliczPremie w tych klasach w taki sposób, aby każdy obiekt „wiedział", jak policzyć własną premię. Kluczowym elementem zadania jest utworzenie tablicy typu Pracownik, w której umieścisz obiekty różnych klas (dyrektora i pracownika biurowego obok siebie), a następnie wywołanie metody obliczeniowej w jednej wspólnej pętli. System powinien automatycznie dopasować algorytm do typu osoby bez konieczności sprawdzania tego w kodzie za pomocą instrukcji if. To zadanie demonstruje potęgę polimorfizmu w zarządzaniu złożonymi procesami biznesowymi.

Sugerowane kroki do wykonania
  1. Zdefiniuj klasę bazową Pracownik z polem Wynagrodzenie.
  2. Dodaj w niej metodę public virtual decimal ObliczPremie() zwracającą 10% pensji.
  3. Stwórz klasę Dyrektor dziedziczącą po Pracownik i dodaj tam pole DodatekDyrektorski.
  4. Nadpisz metodę ObliczPremie w Dyrektorze, dodając dodatek do standardowej premii (możesz użyć base.ObliczPremie()).
  5. Stwórz klasę Sprzedawca i nadpisz w niej metodę, aby liczyła premię uwzględniając obrót (np. 20% pensji).
  6. W Main utwórz tablicę typu Pracownik[] o rozmiarze 3.
  7. Umieść w tablicy po jednym obiekcie każdej klasy.
  8. Uruchom pętlę foreach przechodzącą przez tablicę pracowników.
  9. Dla każdego elementu wywołaj metodę ObliczPremie() i wyświetl wynik.
  10. Zauważ, że mimo że zmienna w pętli jest typu Pracownik, C# wywołuje właściwe metody nadpisane.
WSKAZÓWKI DO WYKONANIA
  • W klasie bazowej Pracownik zdefiniuj pole Wynagrodzenie jako typ decimal (nie double czy int) – decimal jest zalecany do operacji finansowych ze względu na precyzję.
  • Metoda ObliczPremie w klasie bazowej musi być oznaczona jako virtual: public virtual decimal ObliczPremie() – bez tego słowa kluczowego nie będzie można jej nadpisać.
  • Podstawowa premia to 10% wynagrodzenia: return Wynagrodzenie * 0.10m; – dodaj literę 'm' aby kompilator potraktował wartość jako decimal.
  • Klasa Dyrektor powinna dziedziczyć po Pracownik i mieć dodatkowe pole, np. DodatekDyrektorski typu decimal – to stała kwota bonusu.
  • W nadpisanej metodzie Dyrektora rozważ użycie base.ObliczPremie() aby nie powielać obliczeń: return base.ObliczPremie() + DodatekDyrektorski;
  • Klasa Sprzedawca powinna mieć pole ObrotyRoczne (decimal) – premia sprzedawcy może być obliczana procentowo od obrotów lub jako 20% pensji.
  • Przy nadpisywaniu metody używaj dokładnie tej samej sygnatury: public override decimal ObliczPremie() – musi być identyczna jak w klasie bazowej.
  • W Main zadeklaruj tablicę typu bazowego: Pracownik[] pracownicy = new Pracownik[3]; – możesz przechowywać obiekty różnych typów w jednej tablicy.
  • Przypisz obiekty do tablicy: pracownicy[0] = new Pracownik(5000);, pracownicy[1] = new Dyrektor(10000, 2000);, pracownicy[2] = new Sprzedawca(6000, 50000);
  • Użyj pętli foreach do przejścia przez tablicę: foreach (Pracownik p in pracownicy) { Console.WriteLine(p.ObliczPremie()); } – polimorfizm zadziała automatycznie.
  • Nie używaj instrukcji if (p is Dyrektor) ani typeof w pętli – celem zadania jest pokazanie, że polimorfizm eliminuje potrzebę ręcznego rozróżniania typów.
  • Przetestuj, czy premia jest poprawnie obliczana dla każdego typu pracownika – wyniki powinny się różnić mimo wywołania tej samej metody na każdym elemencie tablicy.
Ilustracja do zadania 3-4
3.5
Diagnostyka systemów: new vs override
Cel

Zrozumienie subtelnej, ale kluczowej różnicy między nadpisywaniem metod (override) a ich przesłanianiem (new). Student uczy się świadomego zarządzania nazewnictwem metod w hierarchii oraz dowiaduje się, jak wybór słowa kluczowego wpływa na wywołania polimorficzne.

Scenariusz

Wyobraź sobie, że piszesz system diagnostyczny dla dwóch typów urządzeń: standardowych sensorów oraz nowoczesnych czujników laserowych. Klasa bazowa "Sensor" posiada metodę Testuj(), która jest wirtualna. Klasa pochodna "SensorNowoczesny" nadpisuje tę metodę (override), zmieniając jej zachowanie na bardziej precyzyjne. Z kolei inna klasa, "SensorUkryty", posiada metodę Testuj() oznaczoną słowem kluczowym new, co oznacza, że świadomie przesłania ona wersję z klasy bazowej, nie biorąc udziału w mechanizmie polimorfizmu. Twoim celem jest badanie, co się stanie, gdy przypiszemy oba te obiekty do zmiennych typu bazowego Sensor i wywołamy na nich metodę diagnostyczną. Czy program uruchomi nową wersję testu, czy powróci do starej, bazowej definicji? To zadanie jest bardzo techniczne i ma na celu uniknięcie błędów w dużych frameworkach, gdzie przesłonięcie metody zamiast jej nadpisania może prowadzić do bardzo trudnych do wykrycia błędów logicznych. Program w konsoli powinien wyraźnie pokazać różnice w wywołaniach dla obu przypadków. Dzięki temu zrozumiesz, że słowo kluczowe w sygnaturze metody ma decydujący wpływ na „ścieżkę wywołania" w pamięci komputera. Finalnie opisz w komentarzu zaobserwowane rezultaty eksperymentu.

Sugerowane kroki do wykonania
  1. Utwórz klasę Sensor z metodą public virtual void Testuj() wypisującą "Test bazowy".
  2. Utwórz klasę SensorNowoczesny dziedziczącą po Sensor i nadpisującą metodę przez override.
  3. W SensorNowoczesny wypisz "Test nowoczesny (override)".
  4. Utwórz klasę SensorUkryty dziedziczącą po Sensor i przesłaniającą metodę przez new.
  5. W SensorUkryty wypisz "Test ukryty (new)".
  6. W Main utwórz obiekt Sensor s1 = new SensorNowoczesny().
  7. W Main utwórz obiekt Sensor s2 = new SensorUkryty().
  8. Wywołaj s1.Testuj() oraz s2.Testuj().
  9. Zauważ rzadki przypadek: s2.Testuj() uruchomi wersję bazową, mimo że przypisano SensorUkryty!
  10. Dokonaj konwersji (rzutowania) s2 z powrotem na SensorUkryty i ponownie wywołaj metodę, aby zobaczyć różnicę.
WSKAZÓWKI DO WYKONANIA
  • Zacznij od klasy bazowej Sensor z metodą public virtual void Testuj() – słowo virtual jest obowiązkowe, aby można było nadpisywać lub przesłaniać tę metodę w klasach pochodnych.
  • Metoda Testuj() w klasie bazowej powinna wyświetlać "Test bazowy" – to pozwoli Ci łatwo zaobserwować, która wersja się uruchamia.
  • Klasa SensorNowoczesny dziedziczy po Sensor i nadpisuje metodę: public override void Testuj() – wypisuje "Test nowoczesny (override)".
  • Klasa SensorUkryty dziedziczy po Sensor i przesłania metodę: public new void Testuj() – wypisuje "Test ukryty (new)".
  • Kluczowa różnica: override całkowicie zastępuje metodę bazową w hierarchii dziedziczenia, natomiast new tylko ukrywa metodę bazową dla konkretnej zmiennej.
  • W Main stwórz obiekty przez polimorficzne referencje: Sensor s1 = new SensorNowoczesny(); oraz Sensor s2 = new SensorUkryty();
  • Wywołaj s1.Testuj() – zobaczysz "Test nowoczesny (override)" ponieważ override zachowuje polimorfizm.
  • Wywołaj s2.Testuj() – zobaczysz "Test bazowy" ponieważ new PRZERYWA polimorfizm, metoda jest wybierana na podstawie typu zmiennej, nie obiektu.
  • Aby wywołać ukrytą metodę z SensorUkryty, musisz użyć rzutowania: ((SensorUkryty)s2).Testuj() – teraz zobaczysz "Test ukryty (new)".
  • Używaj słowa kluczowego override gdy chcesz, aby metoda była wywoływana polimorficznie – to jest preferowane podejście w większości scenariuszy.
  • Używaj słowa kluczowego new tylko wtedy, gdy świadomie chcesz ukryć metodę bazową i NIE chcesz polimorfizmu dla tej metody.
  • Dodaj komentarze w kodzie wyjaśniające zaobserwowane zachowanie – to pomoże Ci i innym zrozumieć tę subtelną różnicę w przyszłości.
Ilustracja do zadania 3-5
3.6
Inteligentny agregator przesyłek (kombinacja)
Cel

Integracja wszystkich poznanych mechanizmów: dziedziczenia, metod wirtualnych oraz parametrów params. Student buduje kompleksowy system, który wykazuje korzyści płynące z łączenia różnych zaawansowanych technik programistycznych w C#.

Scenariusz

Jako lider zespołu IT w firmie kurierskiej, przygotowujesz mechanizm do wyliczania kosztów transportu różnych typów paczek. Stwórz główną klasę "Przesylka" z wirtualną metodą ObliczKoszt() oraz właściwością Waga. Następnie zaimplementuj klasy "PaczkaStandardowa" i "PrzesylkaEkspresowa", które w różny sposób nadpisują algorytm cenowy (np. ekspres dodaje stałą opłatę za priorytet). Dodatkowo Twoja klasa bazowa powinna posiadać konstruktor, który przyjmuje wagę i przekazuje ją do pola chronionego (protected). Kolejnym poziomem trudności jest stworzenie klasy statycznej "Logistyka", która zawiera metodę WyslijZbiorczo. Ta metoda powinna przyjmować parametr params Przesylka[] i obliczać całkowity koszt wysłania wszystkich paczek przekazanych w jednym wywołaniu. Dzięki polimorfizmowi, metoda ta będzie działać poprawnie niezależnie od tego, czy wyślemy jej trzy paczki zwykłe, czy pięć ekspresowych. System musi wyświetlić szczegółowy raport z podróży zbiorczej, listując każdą paczkę i jej wyliczony indywidualnie koszt. Taki projekt uczy, jak budować elastyczne interfejsy programistyczne odporne na zmiany wymagań biznesowych. Program kończy się wyświetleniem wielkiego podsumowania dla całej floty wysyłkowej.

Sugerowane kroki do wykonania
  1. Zdefiniuj klasę bazową Przesylka z wirtualną metodą ObliczKoszt.
  2. Dodaj pole chronione protected double waga i zainicjalizuj je w konstruktorze.
  3. Stwórz klasę PaczkaStandardowa, nadpisując koszt jako waga * 2 zł.
  4. Stwórz klasę PrzesylkaEkspresowa, nadpisując koszt jako waga * 5 zł + 20 zł stałej opłaty.
  5. Dodaj klasę statyczną Logistyka, która będzie zbierać obiekty.
  6. Zaimplementuj metodę public static void PodsumujWszystko(params Przesylka[] lista).
  7. Użyj pętli do przejścia przez tablicę params i sumowania wyników metody ObliczKoszt.
  8. W Main utwórz kilka obiektów paczek różnych typów o różnych wagach.
  9. Wywołaj metodę statyczną przekazując jej wszystkie paczki oddzielone przecinkami.
  10. Wyświetl łączny koszt operacji logistycznej w oknie konsoli.
WSKAZÓWKI DO WYKONANIA
  • Zacznij od klasy bazowej Przesylka – zdefiniuj innej pole protected double waga (protected oznacza, że jest dostępne w klasach pochodnych, ale nie na zewnątrz).
  • Zaimplementuj konstruktor klasy Przesylka przyjmujący wagę jako parametr i przypisujący ją do pola: public Przesylka(double waga) { this.waga = waga; }
  • Zdefiniuj wirtualną metodę public virtual double ObliczKoszt() w klasie bazowej – zwraca double, nie decimal (upraszcza obliczenia).
  • W klasie bazowej metoda ObliczKoszt może zwracać 0 lub domyślną wartość – klasy pochodne będą ją nadpisywać.
  • Klasa PaczkaStandardowa dziedziczy po Przesylce i nadpisuje ObliczKoszt: return waga * 2.0; (2 zł za kilogram).
  • Klasa PrzesylkaEkspresowa nadpisuje ObliczKoszt inaczej: return waga * 5.0 + 20.0; (5 zł za kg + 20 zł stałej opłaty).
  • Obie klasy pochodne muszą wywoływać konstruktor bazowy przez : base(waga) – nie powielaj logiki przypisywania wagi.
  • Klasa Logistyka powinna być static: public static class Logistyka – klasy statycznej nie można instancjonować, używa się jej bezpośrednio.
  • Metoda statyczna PodsumujWszystko przyjmuje parametr params Przesylka[] lista – dzięki params możesz przekazać dowolną liczbę paczek bez tworzenia tablicy.
  • W metodzie PodsumujWszystko użyj pętli foreach: foreach (Przesylka p in lista) { Console.WriteLine($"{p.GetType().Name}: {p.ObliczKoszt()} zł"); } – GetType().Name pokazuje nazwę klasy.
  • Sumuj koszty w pętli: double suma = 0; foreach (...) { suma += p.ObliczKoszt(); } – dzięki polimorfizmowi każda paczka obliczy swój koszt sama.
  • W Main wywołaj metodę bez tworzenia tablicy: Logistyka.PodsumujWszystko(new PaczkaStandardowa(5), new PrzesylkaEkspresowa(3), new PaczkaStandardowa(10)); – parametr params pozwala na taką składnię.
Ilustracja do zadania 3-6