Skip to content

Zadania (11-20)

Charakterystyka typu tablicowego dostępnego w C#

Section titled “Charakterystyka typu tablicowego dostępnego w C#”

Tablica w C# to typ referencyjny przechowujący uporządkowany zbiór elementów tego samego typu, indeksowany od zera. Po utworzeniu jej rozmiar jest stały i nie można go zmienić. Wszystkie tablice dziedziczą z klasy System.Array, dzięki czemu mają wspólne właściwości (np. Length) i metody (np. Sort, Copy).

Wyróżniamy tablice jednowymiarowe, wielowymiarowe prostokątne (np. int[,], gdzie każdy wiersz ma tę samą długość) oraz tablice nieregularne (jagged, int[][], czyli tablice tablic, gdzie wiersze mogą mieć różną długość). Po tablicy wygodnie iteruje się pętlą foreach.

Liczba wymiarów nie jest ograniczona do dwóch. Tablica prostokątna może mieć trzy i więcej wymiarów (np. int[,,] to tablica trójwymiarowa, a każdy dodatkowy przecinek dokłada kolejny wymiar). Podobnie tablice nieregularne można zagnieżdżać głębiej (np. int[][][] to tablica tablic tablic), a oba podejścia da się też łączyć (np. int[][,]).

// tablica jednowymiarowa o 5 elementach typu int
int[] numbers = new int[5];
// tablica zainicjowana wartościami
string[] days = { "pn", "wt", "sr" };
// tablica dwuwymiarowa (prostokątna) 2 wiersze na 3 kolumny
int[,] matrix = new int[2, 3];
// tablica nieregularna (jagged) - wiersze różnej długości
int[][] jagged = new int[2][];
jagged[0] = new int[] { 1 };
jagged[1] = new int[] { 1, 2, 3 };
// tablica prostokątna trójwymiarowa (każdy przecinek to kolejny wymiar)
int[,,] cube = new int[2, 3, 4];
// głębsze zagnieżdżenie tablicy nieregularnej (tablica tablic tablic)
int[][][] deepJagged = new int[2][][];
// Length zwraca liczbę elementów
Console.WriteLine(days.Length);

Skrót do zapamiętania: Tablica to typ referencyjny, zbiór elementów tego samego typu, indeksowany od 0, o stałym rozmiarze, dziedziczy z System.Array. Rodzaje: jednowymiarowe, prostokątne, nieregularne (jagged).

  • Typ referencyjny, indeks od 0, stały rozmiar po utworzeniu.
  • Dziedziczy z System.Array (Length, Sort, Copy).
  • Rodzaje: 1D, prostokątne int[,], nieregularne int[][].
  • Liczba wymiarów nieograniczona: int[,,], int[][][] itd. (można też łączyć, np. int[][,]).
  • Wygodna iteracja przez foreach.

Charakterystyka dziedziczenia w przypadku języka C#

Section titled “Charakterystyka dziedziczenia w przypadku języka C#”

Dziedziczenie to mechanizm pozwalający tworzyć nową klasę (pochodną) na bazie istniejącej (bazowej). Klasa pochodna przejmuje pola, metody i własności klasy bazowej oraz może dodawać własne lub zmieniać odziedziczone zachowanie. Realizuje zasadę ponownego użycia kodu i relację “jest rodzajem”.

W C# obowiązuje dziedziczenie pojedyncze: klasa może mieć tylko jedną klasę bazową. Wielokrotne dziedziczenie obchodzi się przez implementację wielu interfejsów. Słowo virtual oznacza metodę, którą można nadpisać, a override nadpisuje ją w klasie pochodnej. sealed blokuje dalsze dziedziczenie, a abstract pozwala stworzyć klasę bez pełnej implementacji. Wszystkie klasy ostatecznie dziedziczą z System.Object.

// klasa bazowa
class Animal
{
// metoda wirtualna - można ją nadpisać w klasie pochodnej
public virtual void MakeSound()
{
Console.WriteLine("...");
}
}
// klasa pochodna dziedziczy po Animal (dwukropek)
class Dog : Animal
{
// override nadpisuje metodę z klasy bazowej
public override void MakeSound()
{
Console.WriteLine("Hau");
}
}
// klasa abstrakcyjna - nie można utworzyć jej obiektu (new Shape() jest błędem)
abstract class Shape
{
// metoda abstrakcyjna - bez ciała, klasa pochodna musi ją nadpisać
public abstract double Area();
}
// klasa pochodna musi dostarczyć implementację metody abstrakcyjnej
class Circle : Shape
{
private double radius;
public Circle(double radius)
{
this.radius = radius;
}
// override implementuje metodę abstrakcyjną z klasy bazowej
public override double Area()
{
return Math.PI * radius * radius;
}
}
// klasa sealed - nie można po niej dziedziczyć
sealed class Square : Shape
{
private double side;
public Square(double side)
{
this.side = side;
}
public override double Area()
{
return side * side;
}
}
// class SmallSquare : Square { } // BŁĄD: nie można dziedziczyć po klasie sealed

Skrót do zapamiętania: Klasa pochodna przejmuje składowe bazowej. W C# tylko dziedziczenie pojedyncze (jedna klasa bazowa), wiele interfejsów. virtual/override do nadpisywania, sealed blokuje, abstract dla klas niepełnych.

  • Klasa pochodna przejmuje pola, metody, własności bazowej.
  • Tylko pojedyncze dziedziczenie klas; wiele interfejsów dozwolone.
  • virtual + override = nadpisywanie metod; base sięga do bazowej.
  • sealed = brak dalszego dziedziczenia; abstract = klasa niepełna.
  • Wspólny przodek wszystkich klas: System.Object.

Pojęcie konstruktora oraz destruktora w języku C#

Section titled “Pojęcie konstruktora oraz destruktora w języku C#”

Konstruktor to specjalna metoda wywoływana automatycznie przy tworzeniu obiektu (operatorem new). Jego zadaniem jest inicjalizacja pól obiektu. Nazywa się tak samo jak klasa, nie ma typu zwracanego i może być przeciążony (kilka wersji z różnymi parametrami). Jeśli nie zdefiniujemy żadnego, kompilator dodaje domyślny bezparametrowy.

Destruktor (finalizator) to metoda o nazwie poprzedzonej tyldą (~NazwaKlasy), wywoływana przez Garbage Collector tuż przed zwolnieniem obiektu. Służy do zwalniania zasobów niezarządzanych. W .NET używa się go rzadko, bo pamięcią zarządza GC, a moment jego wywołania jest nieokreślony. Zamiast destruktora do sprzątania zasobów preferuje się wzorzec IDisposable z metodą Dispose.

class Person
{
private string name;
// konstruktor - nazwa jak klasa, brak typu zwracanego
public Person(string name)
{
// inicjalizacja pola wartością z parametru
this.name = name;
}
// destruktor (finalizator) - wywoływany przez GC
~Person()
{
// tu zwalnianie zasobów niezarządzanych
}
}
// preferowany zamiast destruktora: wzorzec IDisposable z metodą Dispose
class Resource : IDisposable
{
// Dispose zwalnia zasoby od razu, gdy nie są już potrzebne
public void Dispose()
{
// tu zwalnianie zasobów
}
}
// blok using gwarantuje wywołanie Dispose, nawet gdy wystąpi wyjątek
using (var r = new Resource())
{
// korzystanie z zasobu
} // tutaj automatycznie wywoła się r.Dispose()

Skrót do zapamiętania: Konstruktor inicjalizuje obiekt przy new (nazwa = klasa, bez typu zwracanego, może być przeciążony). Destruktor (~Klasa) wywołuje GC przy zwalnianiu obiektu; w .NET rzadki, lepiej IDisposable/Dispose.

  • Konstruktor: inicjalizacja pól, nazwa jak klasa, brak typu zwracanego, można przeciążać.
  • Brak własnego konstruktora -> kompilator daje domyślny bezparametrowy.
  • Destruktor ~Klasa: wywoływany przez GC, sprząta zasoby niezarządzane.
  • Moment wywołania destruktora nieokreślony; preferowany Dispose (IDisposable).

Podać przykłady potwierdzające przydatność konstrukcji foreach

Section titled “Podać przykłady potwierdzające przydatność konstrukcji foreach”

Pętla foreach iteruje po wszystkich elementach kolekcji bez konieczności ręcznego operowania indeksem czy licznikiem. Działa na każdym typie implementującym interfejs IEnumerable, czyli na tablicach, listach, słownikach i wielu innych kolekcjach. Jej zaletą jest czytelność oraz brak ryzyka wyjścia poza zakres indeksu.

Przydaje się wszędzie tam, gdzie chcemy odczytać po kolei elementy, a nie zależy nam na indeksie. Jest mniej podatna na błędy niż klasyczna pętla for i lepiej wyraża intencję “przejdź po wszystkich elementach”.

// iteracja po tablicy - bez indeksu, bez ryzyka wyjścia poza zakres
int[] numbers = { 1, 2, 3 };
foreach (int n in numbers)
{
Console.WriteLine(n);
}
// iteracja po liście
List<string> names = new List<string> { "Ala", "Ola" };
foreach (string name in names)
{
Console.WriteLine(name);
}
// iteracja po słowniku - element to para klucz-wartość
Dictionary<string, int> ages = new Dictionary<string, int> { { "Ala", 20 } };
foreach (var pair in ages)
{
Console.WriteLine(pair.Key + ": " + pair.Value);
}

Foreach nie jest ograniczony do gotowych kolekcji. Tworząc własną klasę, można sprawić, by dało się po niej iterować pętlą foreach, implementując interfejs IEnumerable (metoda GetEnumerator). Najprościej zrobić to słowem kluczowym yield return, które samo generuje enumerator. Dzięki temu nasza klasa zachowuje się jak wbudowana kolekcja i korzysta z tej samej, czytelnej składni.

// własna klasa, po której da się iterować foreach
class Team : IEnumerable
{
// dane trzymamy wewnątrz w zwykłej tablicy
private string[] players = { "Ala", "Ola", "Ela" };
// GetEnumerator mówi, jak foreach ma przechodzić po elementach
// yield return oddaje kolejne elementy jeden po drugim
public IEnumerator GetEnumerator()
{
foreach (string player in players)
yield return player;
}
}
// teraz własnej klasy używa się tak samo jak wbudowanej kolekcji
foreach (string player in new Team())
{
Console.WriteLine(player); // wypisze Ala, Ola, Ela
}

To samo można zrobić na kilka sposobów:

  1. Oddelegować iterację do gotowej kolekcji wewnątrz klasy (zwracamy jej GetEnumerator):
class Team : IEnumerable
{
private List<string> players = new List<string> { "Ala", "Ola", "Ela" };
// po prostu zwracamy enumerator wewnętrznej listy
public IEnumerator GetEnumerator() => players.GetEnumerator();
}
  1. Napisać własny enumerator ręcznie, czyli klasę z metodą MoveNext i właściwością Current (tak naprawdę foreach pod spodem wywołuje właśnie je). To pokazuje, jak działa mechanizm w środku:
class Team : IEnumerable
{
private string[] players = { "Ala", "Ola", "Ela" };
public IEnumerator GetEnumerator() => new TeamEnumerator(players);
}
// własny enumerator: pamięta pozycję i udostępnia kolejne elementy
class TeamEnumerator : IEnumerator
{
private string[] players;
private int index = -1; // przed pierwszym elementem
public TeamEnumerator(string[] players)
{
this.players = players;
}
// Current zwraca bieżący element
public object Current => players[index];
// MoveNext przesuwa się do następnego elementu, false gdy koniec
public bool MoveNext()
{
index++;
return index < players.Length;
}
// Reset wraca na początek
public void Reset() => index = -1;
}
  1. Co ważne, foreach nie wymaga wprost interfejsu IEnumerable. Wystarczy, że klasa ma publiczną metodę GetEnumerator zwracającą obiekt z Current i MoveNext (tzw. dopasowanie “kaczkowe”). Implementacja interfejsów jest jednak zalecana, bo wtedy klasa współpracuje też z LINQ i innym kodem oczekującym IEnumerable.

Podczas działania foreach nie wolno zmieniać struktury kolekcji (dodawać ani usuwać elementów), bo grozi to wyjątkiem InvalidOperationException. Wolno za to zmieniać właściwości obiektów, gdy elementy są typami referencyjnymi.

// ŹLE: modyfikacja kolekcji w trakcie foreach -> InvalidOperationException
List<int> numbers = new List<int> { 1, 2, 3 };
foreach (int n in numbers)
{
numbers.Add(4);
}

Skrót do zapamiętania: foreach iteruje po wszystkich elementach kolekcji (każdej z IEnumerable) bez indeksu. Czytelniejsza i bezpieczniejsza niż for (brak wyjścia poza zakres). Implementując IEnumerable (np. przez yield return), można dać foreach także własnej klasie.

  • Działa na wszystkim, co implementuje IEnumerable (tablice, listy, słowniki).
  • Bez ręcznego indeksu i licznika -> mniej błędów.
  • Brak ryzyka wyjścia poza zakres.
  • Czytelnie wyraża “przejdź po wszystkich”; minus: nie ma dostępu do indeksu.
  • W trakcie foreach nie wolno zmieniać struktury kolekcji (Add/Remove -> InvalidOperationException); można zmieniać właściwości obiektów referencyjnych.
  • Własna klasa też może wspierać foreach: wystarczy zaimplementować IEnumerable (GetEnumerator), najłatwiej przez yield return.
  • Inne sposoby: oddelegować GetEnumerator do wewnętrznej kolekcji albo napisać własny enumerator (MoveNext + Current).
  • Foreach nie wymaga wprost IEnumerable: wystarczy publiczna metoda GetEnumerator (dopasowanie kaczkowe), ale interfejs zalecany (LINQ, zgodność).

Omówić własności oraz indeksery stosowane w klasach

Section titled “Omówić własności oraz indeksery stosowane w klasach”

Własność (property) to składowa klasy wyglądająca z zewnątrz jak pole, ale działająca jak para metod dostępowych: get (odczyt) i set (zapis). Pozwala kontrolować dostęp do prywatnych pól, dodawać walidację i realizować enkapsulację. Można też użyć skróconej formy (auto-property), gdy nie potrzebujemy dodatkowej logiki.

Indekser pozwala traktować obiekt klasy tak, jakby był tablicą, czyli odwoływać się do niego przez nawiasy kwadratowe i indeks. Definiuje się go słowem this z parametrem indeksu. Jest przydatny w klasach reprezentujących kolekcje.

class Account
{
// prywatne pole
private double balance;
// własność z walidacją w secie
public double Balance
{
// get zwraca wartość pola
get { return balance; }
// set ustawia wartość, value to przekazywana wartość
set
{
if (value >= 0)
balance = value;
}
}
// auto-property - kompilator sam tworzy ukryte pole
public string Owner { get; set; }
}
class MyList
{
private string[] data = new string[10];
// indekser - dostęp przez nawiasy kwadratowe obiekt[i]
public string this[int i]
{
get { return data[i]; }
set { data[i] = value; }
}
}

Użycie z zewnątrz wygląda jak zwykłe pole albo jak tablica, ale pod spodem wołane są metody get/set:

Account acc = new Account();
// przypisanie uruchamia set (value = 100, walidacja przepuszcza)
acc.Balance = 100;
// ujemna wartość: set ją odrzuca, balance zostaje 100
acc.Balance = -50;
// odczyt uruchamia get
Console.WriteLine(acc.Balance); // 100
// auto-property działa tak samo, bez własnej logiki
acc.Owner = "Ala";
Console.WriteLine(acc.Owner); // Ala
MyList list = new MyList();
// nawias kwadratowy z przypisaniem uruchamia set indeksera
list[0] = "pierwszy";
// nawias kwadratowy z odczytem uruchamia get indeksera
Console.WriteLine(list[0]); // pierwszy

Skrót do zapamiętania: Własność = pole z metodami get/set (enkapsulacja, walidacja); auto-property to forma skrócona. Indekser (this[i]) pozwala używać obiektu jak tablicy.

  • Własność: get/set, kontrola dostępu i walidacja, enkapsulacja.
  • Auto-property: { get; set; } bez własnego pola.
  • value w secie to przekazywana wartość.
  • Indekser: this[int i] -> dostęp obiekt[i] jak do tablicy.

Czym jest interfejs, jego zastosowanie i przeznaczenie

Section titled “Czym jest interfejs, jego zastosowanie i przeznaczenie”

Interfejs to kontrakt definiujący zestaw metod, własności lub zdarzeń, które klasa go implementująca musi udostępnić. Tradycyjnie interfejs nie zawiera implementacji (od C# 8 dopuszczalne są metody domyślne), a jedynie sygnatury. Określa “co” obiekt potrafi, nie “jak” to robi.

Interfejsy umożliwiają polimorfizm i luźne powiązania między elementami systemu, bo kod może operować na interfejsie, nie znając konkretnej klasy. Pozwalają też obejść brak wielodziedziczenia klas w C#, ponieważ klasa może implementować dowolnie wiele interfejsów. Nazwy interfejsów zwyczajowo zaczyna się od dużej litery I (np. IVehicle, IDisposable). Składowe interfejsu są domyślnie publiczne, a klasa implementująca musi udostępnić je wszystkie (chyba że jest abstrakcyjna).

// interfejs - tylko sygnatury, bez implementacji
interface IVehicle
{
void Drive();
int Speed { get; }
}
// klasa implementuje interfejs i musi dostarczyć jego składowe
class Car : IVehicle
{
public void Drive()
{
Console.WriteLine("Jadę");
}
public int Speed { get; } = 100;
}

Polimorfizm - kluczowa zaleta. Zmienna typu interfejsu może wskazywać na obiekt dowolnej klasy, która go implementuje. Dzięki temu ten sam kod obsługuje różne klasy, nie wiedząc, która to konkretnie:

class Bike : IVehicle
{
public void Drive() { Console.WriteLine("Pedałuję"); }
public int Speed { get; } = 20;
}
// lista interfejsu trzyma obiekty różnych klas
List<IVehicle> vehicles = new List<IVehicle> { new Car(), new Bike() };
// ten sam kod działa na każdym pojeździe, niezależnie od klasy
foreach (IVehicle v in vehicles)
{
v.Drive(); // wywoła się właściwa wersja: Car albo Bike
}

Wiele interfejsów - klasa może implementować ich dowolnie wiele (oddzielone przecinkami), co zastępuje brak wielodziedziczenia klas:

interface IFlying { void Fly(); }
interface ISwimming { void Swim(); }
// jedna klasa realizuje kilka kontraktów naraz
class Duck : IFlying, ISwimming
{
public void Fly() { Console.WriteLine("Lecę"); }
public void Swim() { Console.WriteLine("Pływam"); }
}

Metody domyślne (od C# 8) - interfejs może mieć metodę z gotowym ciałem; klasa nie musi jej implementować, ale może nadpisać:

interface ILogger
{
void Log(string msg);
// metoda domyślna - ma implementację, klasa nie musi jej pisać
void LogError(string msg) => Log("BŁĄD: " + msg);
}

Jawna implementacja - gdy dwa interfejsy mają metodę o tej samej nazwie, można je rozróżnić, podając nazwę interfejsu przed metodą (taka metoda jest dostępna tylko przez referencję interfejsu):

class Logger : ILogger
{
// jawna implementacja: dostępna tylko po rzutowaniu na ILogger
void ILogger.Log(string msg) => Console.WriteLine(msg);
}

Interfejs, a klasa abstrakcyjna - częste pytanie. Interfejs to czysty kontrakt bez pól i (tradycyjnie) bez implementacji, a klasa może implementować ich wiele. Klasa abstrakcyjna może mieć pola, konstruktory i gotowe metody, ale dziedziczy się tylko po jednej. Interfejsu używamy do określenia “co potrafi” niespokrewnione klasy, a klasy abstrakcyjnej jako wspólnej bazy spokrewnionych klas.

Wśród wbudowanych interfejsów .NET często spotyka się: IEnumerable (iteracja foreach), IDisposable (zwalnianie zasobów, Dispose), IComparable (porównywanie i sortowanie) oraz IEquatable (porównywanie równości).

Skrót do zapamiętania: Interfejs to kontrakt (sygnatury bez implementacji) mówiący “co” klasa potrafi. Daje polimorfizm, luźne powiązania i pozwala implementować wiele interfejsów (obejście braku wielodziedziczenia).

  • Kontrakt: zestaw składowych do zaimplementowania, bez ciała (poza metodami domyślnymi od C# 8).
  • Składowe domyślnie publiczne; klasa musi dostarczyć implementację wszystkich; nazwy z prefiksem I.
  • Wiele interfejsów na klasę -> obejście braku wielodziedziczenia.
  • Daje polimorfizm i luźne powiązania (programowanie pod interfejs).
  • Jawna implementacja rozróżnia metody o tej samej nazwie z różnych interfejsów.
  • Interfejs vs klasa abstrakcyjna: interfejs = czysty kontrakt, wiele naraz, bez pól; klasa abstrakcyjna = jedna baza, może mieć pola, konstruktory i gotowe metody.
  • Wbudowane: IEnumerable, IDisposable, IComparable, IEquatable.

Klasa wieloczęściowa (partial) to klasa, której definicję można rozbić na kilka części, zwykle w osobnych plikach. Oznacza się to słowem kluczowym partial przy każdej części. Kompilator scala wszystkie części w jedną klasę, jakby były napisane razem.

Główne zastosowanie to oddzielenie kodu generowanego automatycznie (np. przez projektant formularzy w Visual Studio) od kodu pisanego ręcznie przez programistę, tak by jeden nie nadpisywał drugiego. Ułatwia też pracę zespołową nad jedną dużą klasą.

// pierwsza część klasy (np. plik Person1.cs)
partial class Person
{
public string Name;
}
// druga część tej samej klasy (np. plik Person2.cs)
partial class Person
{
public void Introduce()
{
Console.WriteLine(Name);
}
}

Skrót do zapamiętania: Klasa partial może być zapisana w kilku częściach/plikach, które kompilator łączy w jedną. Używana do oddzielenia kodu generowanego od własnego oraz pracy zespołowej.

  • Słowo kluczowe partial przy każdej części.
  • Kompilator scala części w jedną klasę.
  • Zastosowanie: kod generowany (designer) + kod własny.
  • Ułatwia pracę zespołową nad dużą klasą.

Przeciążanie operatorów pozwala zdefiniować własne działanie standardowych operatorów (np. +, -, ==, !=) dla obiektów własnych typów. Dzięki temu można na obiektach wykonywać działania w naturalny, czytelny sposób, zamiast wywoływać metody. Operator definiuje się jako metodę publiczną i statyczną ze słowem kluczowym operator.

Nie wszystkie operatory można przeciążać, a niektóre trzeba definiować w parach (np. == razem z !=). Klasycznym przykładem jest dodawanie wektorów lub liczb zespolonych.

class Vector
{
public int X, Y;
public Vector(int x, int y)
{
X = x;
Y = y;
}
// przeciążenie operatora + dla dwóch obiektów Vector
// metoda musi być public static, ze słowem operator
public static Vector operator +(Vector a, Vector b)
{
// tworzymy nowy wektor będący sumą składowych
return new Vector(a.X + b.X, a.Y + b.Y);
}
}
// użycie wygląda naturalnie jak dla liczb
// Vector v = v1 + v2;

Skrót do zapamiętania: Przeciążanie operatorów to definiowanie własnego działania operatorów dla własnych typów, metodą public static operator. Niektóre operatory definiuje się parami (== z !=).

  • Pozwala używać operatorów (+, -, ==) na obiektach własnych typów.
  • Składnia: public static Type operator +(...).
  • Niektóre operatory tylko w parach (== i !=).
  • Nie wszystkie operatory można przeciążać.

Program obsługujący stos do stu elementów ze zdarzeniami

Section titled “Program obsługujący stos do stu elementów ze zdarzeniami”

Program implementuje stos napisów o pojemności maksymalnie 100 elementów. Stos działa według zasady LIFO (ostatni wchodzi, pierwszy wychodzi). Kiedy ze stosu próbujemy zdjąć element, a stos jest pusty, zgłaszane jest zdarzenie. Podobnie zdarzenie pojawia się, gdy próbujemy dodać element do już pełnego stosu. Zdarzenia oparte są na delegatach.

using System;
// klasa reprezentująca stos napisów o pojemności do 100 elementów
class Stack
{
// tablica przechowująca elementy (maksymalnie 100)
private string[] data = new string[100];
// indeks wierzchołka stosu, -1 oznacza stos pusty
private int top = -1;
// zdarzenie zgłaszane przy próbie zdjęcia z pustego stosu
public event EventHandler StackEmpty;
// zdarzenie zgłaszane przy próbie dodania do pełnego stosu
public event EventHandler StackFull;
// dodanie elementu na stos
public void Push(string element)
{
// sprawdzenie czy stos jest pełny (ostatni indeks to 99)
if (top == data.Length - 1)
{
// zgłoszenie zdarzenia o pełnym stosie, jeśli ktoś go słucha
StackFull?.Invoke(this, EventArgs.Empty);
return;
}
// przesunięcie wierzchołka i zapis elementu
top++;
data[top] = element;
}
// zdjęcie elementu ze stosu
public string Pop()
{
// sprawdzenie czy stos jest pusty
if (top == -1)
{
// zgłoszenie zdarzenia o pustym stosie
StackEmpty?.Invoke(this, EventArgs.Empty);
return null;
}
// pobranie elementu z wierzchołka i przesunięcie w dół
string element = data[top];
top--;
return element;
}
}
class Program
{
static void Main()
{
// utworzenie stosu
Stack stack = new Stack();
// podpięcie obsługi zdarzenia pustego stosu
stack.StackEmpty += (sender, args) =>
Console.WriteLine("Stos jest pusty!");
// podpięcie obsługi zdarzenia pełnego stosu
stack.StackFull += (sender, args) =>
Console.WriteLine("Stos jest pełny!");
// dodanie i zdjęcie przykładowego elementu
stack.Push("A");
Console.WriteLine(stack.Pop());
// próba zdjęcia z pustego stosu - wywoła zdarzenie
stack.Pop();
}
}

Skrót do zapamiętania: Stos LIFO na tablicy string[100] z indeksem wierzchołka. Pop z pustego stosu i Push do pełnego zgłaszają zdarzenia (event + delegat EventHandler, wywołanie przez ?.Invoke).

  • Tablica string[100] + pole top (-1 = pusty).
  • Push sprawdza pełność, Pop sprawdza pustość.
  • Dwa zdarzenia: StackEmpty i StackFull (typ EventHandler).
  • Zgłoszenie zdarzenia: Zdarzenie?.Invoke(this, EventArgs.Empty).
  • Subskrypcja operatorem += w miejscu użycia.

Omówić zastosowanie zdarzeń (przedstawić także konstrukcję delegatu)

Section titled “Omówić zastosowanie zdarzeń (przedstawić także konstrukcję delegatu)”

Delegat to typ referencyjny przechowujący referencję do metody, czyli bezpieczny typowo “wskaźnik na metodę”. Definiuje sygnaturę metod, które może wskazywać. Delegaty pozwalają przekazywać metody jako argumenty i wywoływać je pośrednio.

Zdarzenie to mechanizm powiadamiania zbudowany na delegatach. Obiekt-nadawca (publisher) zgłasza zdarzenie, gdy coś się stanie, a obiekty-odbiorcy (subscribers) reagują na nie swoimi metodami obsługi, podpiętymi operatorem +=. Realizuje to wzorzec obserwatora i zapewnia luźne powiązanie: nadawca nie musi wiedzieć, kto słucha. Zdarzenia stosuje się np. w obsłudze interfejsu użytkownika (kliknięcia) czy do sygnalizacji zmian stanu.

// definicja delegatu - opisuje sygnaturę metody (tu: void z jednym int)
delegate void Notification(int value);
class Counter
{
private int count;
// zdarzenie oparte na delegacie
public event Notification LimitReached;
public void Increment()
{
count++;
// gdy stan osiąga 3, zgłaszamy zdarzenie do wszystkich słuchaczy
if (count == 3)
LimitReached?.Invoke(count);
}
}
class Program
{
static void Main()
{
Counter c = new Counter();
// podpięcie metody obsługi zdarzenia operatorem +=
c.LimitReached += (v) => Console.WriteLine("Limit: " + v);
// trzykrotne zwiększenie wywoła zdarzenie
c.Increment();
c.Increment();
c.Increment();
}
}

Skrót do zapamiętania: Delegat to typowo bezpieczny wskaźnik na metodę (definiuje jej sygnaturę). Zdarzenie to mechanizm powiadamiania oparty na delegatach: nadawca zgłasza, odbiorcy podpięci przez += reagują (wzorzec obserwatora, luźne powiązanie).

  • Delegat: typ wskazujący na metodę o danej sygnaturze (delegate void X(int)).
  • Zdarzenie (event): powiadamianie oparte na delegacie.
  • Nadawca zgłasza (?.Invoke), odbiorcy subskrybują przez += / -=.
  • Wzorzec obserwatora, luźne powiązanie nadawcy z odbiorcami.
  • Typowe użycie: zdarzenia UI (kliknięcia), sygnalizacja zmian stanu.