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

Pętla foreach iteruje po wszystkich elementach kolekcji bez ręcznego operowania indeksem. Działa na każdym typie implementującym interfejs IEnumerable (tablice, listy, słowniki). Jest czytelniejsza i mniej podatna na błędy niż klasyczna pętla for, bo nie grozi wyjściem poza zakres indeksu.

// iteracja po tablicy
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 działa też na własnych klasach, jeśli zaimplementują interfejs IEnumerable (metodę GetEnumerator). Najprościej zrobić to słowem kluczowym yield return, które samo generuje enumerator.

// 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;
}
}
// foreach po własnej klasie działa dzięki GetEnumerator
foreach (var player in new Team())
Console.WriteLine(player);

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.


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.

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

Interfejs to kontrakt definiujący zestaw metod, własności lub zdarzeń, które klasa go implementująca musi udostępnić. Interfejs nie zawiera implementacji, 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).

// 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ć obiekt dowolnej klasy, która go implementuje
IVehicle vehicle = new Car();
// kod operuje na interfejsie, nie znając konkretnej klasy
vehicle.Drive(); // wywoła się implementacja z klasy Car

Wiele interfejsów:

// klasa implementuje kilka interfejsów naraz (po przecinku)
class Duck : IFlying, ISwimming
{
// implementacje metod z IFlying i ISwimming
}

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.
  • Klasa musi dostarczyć implementację wszystkich składowych; nazwy z prefiksem I.
  • Wiele interfejsów na klasę -> obejście braku wielodziedziczenia.
  • Daje polimorfizm i luźne powiązania (programowanie pod interfejs).
  • 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.