Klasa Array i jej metody
W poprzedniej lekcji wszystko robiliśmy ręcznie — i dobrze, bo teraz wiesz, co się dzieje pod spodem. Ale sortowanie napisane od zera to trzydzieści linii, a gotowa metoda to jedna. Ta lekcja jest o narzędziach, które .NET daje ci za darmo.
Czego się dziś nauczysz
- Posortujesz tablicę rosnąco i malejąco jedną instrukcją
- Znajdziesz element metodą
IndexOfzamiast pisać własną pętlę - Skopiujesz tablicę i zmienisz jej rozmiar
- Porównasz zawartość dwóch tablic
- Rozpoznasz, które metody zmieniają tablicę, a które zwracają nową
Czym jest klasa Array
Array to klasa z biblioteki .NET, w której zebrano gotowe operacje na tablicach. Jej metody wywołujemy przez nazwę klasy, a tablicę podajemy jako pierwszy argument:
int[] liczby = { 42, 17, 93, 8, 55, 23 };
Array.Sort(liczby); // przez nazwe klasy
Console.WriteLine(string.Join(", ", liczby));
Console.WriteLine(liczby.Length); // ale Length juz przez zmienna
8, 17, 23, 42, 55, 93
6
Dwa sposoby zapisu i skąd ta różnica
Array.Sort(liczby) to metoda statyczna — należy do klasy, nie do konkretnej tablicy, więc tablicę trzeba jej podać. liczby.Length i liczby.Clone() należą już do samej tablicy. Na razie wystarczy zapamiętać, które jest które; pełne wyjaśnienie przyjdzie w bloku o klasach i metodach statycznych.
Najważniejsze rozróżnienie w tej lekcji
Część metod zmienia tablicę na miejscu i nie zwraca nic (Sort, Reverse, Fill, Clear). Część zwraca wynik, a tablicy nie rusza (IndexOf, BinarySearch). Pomylenie tych dwóch grup to najczęstszy błąd: zapis liczby = Array.Sort(liczby); nie skompiluje się, bo Sort nie zwraca tablicy.
Sortowanie
int[] liczby = { 42, 17, 93, 8, 55, 23 };
Array.Sort(liczby); // rosnaco - zmienia tablice!
Console.WriteLine($"Rosnaco: {string.Join(", ", liczby)}");
Array.Reverse(liczby); // odwraca kolejnosc
Console.WriteLine($"Malejaco: {string.Join(", ", liczby)}");
Rosnaco: 8, 17, 23, 42, 55, 93
Malejaco: 93, 55, 42, 23, 17, 8
Podstawowe Array.Sort(tablica) sortuje rosnąco według domyślnego porównywania. Przeciążenia pozwalają podać własną regułę porównania, także malejącą. Żeby uzyskać porządek malejący, sortujemy, a potem odwracamy — dwie instrukcje, jedna po drugiej.
Sort niszczy pierwotną kolejność
Po Array.Sort(liczby) oryginalna kolejność jest bezpowrotnie stracona. Jeśli jest ci jeszcze potrzebna (bo np. druga tablica opisuje te same obiekty), posortuj kopię:
int[] oryginal = { 42, 17, 93, 8, 55, 23 };
int[] posortowana = oryginal[..]; // kopia z lekcji 23
Array.Sort(posortowana);
Console.WriteLine($"Oryginal: {string.Join(", ", oryginal)}");
Console.WriteLine($"Posortowana: {string.Join(", ", posortowana)}");
Oryginal: 42, 17, 93, 8, 55, 23
Posortowana: 8, 17, 23, 42, 55, 93
Sortowanie napisów
string[] imiona = { "Zofia", "Adam", "Ewa", "Bartek", "Celina" };
Array.Sort(imiona);
Console.WriteLine(string.Join(", ", imiona));
Adam, Bartek, Celina, Ewa, Zofia
Polskie znaki w sortowaniu
Napisy sortują się według reguł kultury systemu, więc na polskim systemie „ą” trafi zaraz za „a”, a nie na koniec alfabetu. To zwykle jest to, czego chcemy. Gdyby wynik miał być identyczny na każdym komputerze (np. dane wysyłane dalej), użyj Array.Sort(imiona, StringComparer.Ordinal) — wtedy porównywane są numery znaków, a wielkie litery wypadają przed małymi.
Sortowanie dwóch tablic naraz
To rozwiązuje problem z poprzedniej lekcji: dwie tablice opisujące te same obiekty. Podana wersja Sort przestawia elementy w obu tablicach jednakowo:
string[] uczniowie = { "Kowalski", "Nowak", "Zielinski", "Adamczyk" };
int[] punkty = { 68, 91, 45, 77 };
Array.Sort(punkty, uczniowie); // klucz, potem wartosci
for (int i = 0; i < punkty.Length; i++)
{
Console.WriteLine($"{uczniowie[i],-12}{punkty[i],5}");
}
Zielinski 45
Kowalski 68
Adamczyk 77
Nowak 91
Pierwsza tablica to klucz — według niej odbywa się sortowanie. Druga jedzie za nią, więc nazwiska nadal pasują do punktów. Obie muszą mieć ten sam rozmiar.
Wyszukiwanie
int[] liczby = { 42, 17, 93, 8, 55, 23 };
int gdzie = Array.IndexOf(liczby, 55);
Console.WriteLine($"55 stoi na indeksie {gdzie}");
int brak = Array.IndexOf(liczby, 100);
Console.WriteLine($"100 -> {brak}");
if (Array.IndexOf(liczby, 8) >= 0)
{
Console.WriteLine("Osemka jest w tablicy.");
}
55 stoi na indeksie 4
100 -> -1
Osemka jest w tablicy.
To dokładnie ta sama pętla, którą pisaliśmy w lekcji 23 z tą samą konwencją: −1 oznacza „nie znaleziono”. Metoda LastIndexOf szuka od końca.
Sprawdzaj >= 0, nie != -1
Oba zapisy działają, ale >= 0 lepiej opisuje intencję („dostaliśmy poprawny indeks”) i nie zawiedzie, gdyby kiedyś zmieniła się wartość oznaczająca brak. Nigdy natomiast nie pisz if (Array.IndexOf(...)) — indeks to liczba, a nie wartość logiczna; kompilator zgłosi CS0029.
BinarySearch — szybciej, ale tylko na posortowanej
int[] liczby = { 42, 17, 93, 8, 55, 23 };
Array.Sort(liczby); // WARUNEK KONIECZNY
Console.WriteLine(string.Join(", ", liczby));
int gdzie = Array.BinarySearch(liczby, 55);
Console.WriteLine($"55 stoi na indeksie {gdzie}");
8, 17, 23, 42, 55, 93
55 stoi na indeksie 4
| Cecha | IndexOf | BinarySearch |
|---|---|---|
| Wymaga posortowania | nie | tak |
| Sposób działania | sprawdza po kolei | dzieli zakres na pół (jak zgadywanie liczby z lekcji 16) |
| Milion elementów | do miliona porównań | około 20 porównań |
| Gdy nie znajdzie | -1 | liczba ujemna, ale nie zawsze −1 |
| Przy duplikatach | pierwsze wystąpienie | dowolne z wystąpień |
BinarySearch na nieposortowanej tablicy nie zgłasza błędu
Po prostu zwraca bzdurę — najczęściej „nie znalazłem”, choć element tam jest. To wyjątkowo podstępna pomyłka, bo program działa i nie skarży się. Sortuj przed użyciem albo zostań przy IndexOf.
Kopiowanie i zmiana rozmiaru
int[] zrodlo = { 1, 2, 3, 4, 5 };
// pelna kopia
int[] cel = new int[5];
Array.Copy(zrodlo, cel, 5); // ile elementow przepisac
Console.WriteLine(string.Join(", ", cel));
// tylko fragment - do tego wygodniejszy jest zakres z lekcji 23
int[] fragment = zrodlo[1..4];
Console.WriteLine(string.Join(", ", fragment));
1, 2, 3, 4, 5
2, 3, 4
Array.Resize — pozorna zmiana rozmiaru
Tablica ma stały rozmiar, więc Resize tak naprawdę tworzy nową tablicę, przepisuje do niej zawartość i podmienia zmienną. Dlatego wymaga słowa ref:
int[] liczby = { 1, 2, 3 };
Array.Resize(ref liczby, 5); // powiekszenie
Console.WriteLine(string.Join(", ", liczby));
Array.Resize(ref liczby, 2); // pomniejszenie - nadmiar PRZEPADA
Console.WriteLine(string.Join(", ", liczby));
1, 2, 3, 0, 0
1, 2
Po co ref
Metoda nie zmienia zawartości tablicy, tylko podmienia adres zapisany w zmiennej. Bez ref zmieniłaby swoją własną kopię adresu i po powrocie do programu nic by się nie stało. Do słowa ref wrócimy w bloku o funkcjach.
Nie powiększaj tablicy w pętli
Każde wywołanie Resize tworzy nową tablicę i przepisuje wszystko od nowa. Dodanie tysiąca elementów po jednym oznacza tysiąc przepisań i pół miliona kopiowań. Gdy liczba elementów rośnie w trakcie działania, właściwym narzędziem jest List<T> z bloku o C# nowoczesnym.
Pozostałe przydatne metody
// wypelnienie jedna wartoscia
int[] punkty = new int[5];
Array.Fill(punkty, 10);
Console.WriteLine(string.Join(", ", punkty));
// wypelnienie napisow - ratunek na puste elementy z lekcji 23
string[] imiona = new string[3];
Array.Fill(imiona, "");
Console.WriteLine($"Dlugosc pierwszego: {imiona[0].Length}");
// wyczyszczenie fragmentu (od indeksu 1, dwa elementy)
Array.Clear(punkty, 1, 2);
Console.WriteLine(string.Join(", ", punkty));
// pusta tablica bez tworzenia obiektu
int[] nic = Array.Empty<int>();
Console.WriteLine($"Rozmiar: {nic.Length}");
10, 10, 10, 10, 10
Dlugosc pierwszego: 0
10, 0, 0, 10, 10
Rozmiar: 0
Array.Clear nie usuwa elementów — przywraca im wartość domyślną. Rozmiar tablicy zostaje bez zmian, bo zmienić go nie można.
Minimum, maksimum i suma — bez pętli
Klasa Array ich nie ma, ale są w LINQ: liczby.Max(), liczby.Min(), liczby.Sum(), liczby.Average(). Działają od razu, bo przestrzeń System.Linq jest dołączana automatycznie. Poznasz je porządnie w bloku o C# nowoczesnym; tutaj warto wiedzieć, że istnieją, ale w zadaniach z tej lekcji pisz pętle — na egzaminie zwykle chodzi o pokazanie algorytmu.
Porównywanie tablic
Z poprzedniej lekcji wiesz, że == porównuje adresy. Oto trzy sposoby na porównanie zawartości:
int[] a = { 1, 2, 3 };
int[] b = { 1, 2, 3 };
// 1. operator == - porownuje ADRESY
Console.WriteLine($"a == b : {a == b}");
// 2. gotowa metoda - porownuje ZAWARTOSC
Console.WriteLine($"SequenceEqual : {a.SequenceEqual(b)}");
// 3. recznie, petla
bool takieSame = a.Length == b.Length;
for (int i = 0; i < a.Length && takieSame; i++)
{
if (a[i] != b[i])
{
takieSame = false;
}
}
Console.WriteLine($"Recznie : {takieSame}");
a == b : False
SequenceEqual : True
Recznie : True
Sprawdzenie rozmiaru na początku
W wersji ręcznej najpierw porównujemy długości. Bez tego pętla po krótszej tablicy uznałaby, że {1, 2} i {1, 2, 3} są takie same — a nie są. Ta sama pułapka wraca przy porównywaniu napisów i list.
Zestawienie metod
| Metoda | Zwraca | Działanie |
|---|---|---|
Array.Sort(t) | nic | Zmienia tablicę. Sortuje rosnąco. Wersja Array.Sort(klucze, wartosci) przestawia dwie tablice jednakowo. |
Array.Reverse(t) | nic | Zmienia tablicę. Odwraca kolejność elementów. Razem z Sort daje porządek malejący. |
Array.IndexOf(t, w) | int |
Indeks pierwszego wystąpienia wartości albo -1. Tablicy nie zmienia. LastIndexOf szuka od końca. |
Array.BinarySearch(t, w) | int |
Szybkie wyszukiwanie, ale wyłącznie na tablicy posortowanej. Wynik ujemny oznacza brak. |
Array.Copy(z, c, n) | nic | Przepisuje n pierwszych elementów z tablicy z do c. Tablica docelowa musi już istnieć i być dość duża. |
Array.Resize(ref t, n) | nic | Podmienia tablicę na nową o rozmiarze n. Nadmiarowe elementy przepadają, brakujące dostają wartość domyślną. Wymaga ref. |
Array.Fill(t, w) | nic | Zmienia tablicę. Wpisuje tę samą wartość do wszystkich elementów. |
Array.Clear(t, i, n) | nic | Zmienia tablicę. Przywraca wartości domyślne n elementom od indeksu i. Rozmiaru nie zmienia. |
Array.Empty<T>() | tablica | Pusta tablica danego typu. Przydatna jako „nic nie znaleziono” bez tworzenia nowego obiektu. |
t.SequenceEqual(u) | bool |
Czy obie tablice mają te same elementy w tej samej kolejności. Pochodzi z LINQ, ale działa bez dopisywania using. |
t.Clone() | object |
Kopia tablicy; wynik wymaga rzutowania: (int[])t.Clone(). |
string.Join(sep, t) | string |
Skleja elementy w jeden napis z separatorem — najprostszy sposób wypisania całej tablicy. |
Jak zapamiętać, która metoda co zwraca
Reguła praktyczna: jeśli metoda przestawia zawartość (Sort, Reverse, Fill, Clear), zmienia tablicę i nie zwraca nic. Jeśli szuka (IndexOf, BinarySearch) — zwraca liczbę i niczego nie rusza. Resize jest wyjątkiem: podmienia całą tablicę, dlatego potrzebuje ref.
Wyniki egzaminu
Program analizuje wyniki dziesięciu uczniów: liczy statystyki, wskazuje najlepszych i sprawdza, kto zdał. Wszystko, co da się zrobić gotową metodą, robimy gotową metodą.
const int ProgZdania = 50;
string[] uczniowie = { "Kowalski", "Nowak", "Wisniewski", "Wojcik", "Kowalczyk",
"Kaminski", "Lewandowski", "Zielinski", "Szymanski", "Wozniak" };
int[] wyniki = { 68, 91, 45, 77, 52, 88, 34, 95, 61, 73 };
// --- statystyki: jedno przejscie ---
int suma = 0;
int zdanych = 0;
foreach (int w in wyniki)
{
suma += w;
if (w >= ProgZdania)
{
zdanych++;
}
}
double srednia = (double)suma / wyniki.Length;
double procentZdanych = (double)zdanych / wyniki.Length * 100;
// --- kopie do sortowania, zeby nie stracic kolejnosci ---
int[] posortowane = wyniki[..];
Array.Sort(posortowane);
int najgorszy = posortowane[0];
int najlepszy = posortowane[^1];
// mediana: srednia dwoch srodkowych, bo elementow jest parzyscie
double mediana = (posortowane[4] + posortowane[5]) / 2.0;
// --- ranking: sortujemy dwie tablice naraz ---
string[] rankingNazwiska = uczniowie[..];
int[] rankingPunkty = wyniki[..];
Array.Sort(rankingPunkty, rankingNazwiska); // rosnaco
Array.Reverse(rankingPunkty); // teraz malejaco
Array.Reverse(rankingNazwiska); // OBIE trzeba odwrocic!
// --- wypisanie ---
Console.WriteLine("=== WYNIKI EGZAMINU ===");
Console.WriteLine();
Console.WriteLine($"{"Lp.",-5}{"Nazwisko",-16}{"Punkty",8} Status");
Console.WriteLine(new string('-', 40));
for (int i = 0; i < rankingPunkty.Length; i++)
{
string status = rankingPunkty[i] >= ProgZdania ? "zdany" : "niezdany";
Console.WriteLine($"{i + 1,-5}{rankingNazwiska[i],-16}{rankingPunkty[i],8} {status}");
}
Console.WriteLine(new string('-', 40));
Console.WriteLine();
Console.WriteLine($"{"Srednia:",-20}{srednia,8:F2}");
Console.WriteLine($"{"Mediana:",-20}{mediana,8:F2}");
Console.WriteLine($"{"Najlepszy wynik:",-20}{najlepszy,8}");
Console.WriteLine($"{"Najgorszy wynik:",-20}{najgorszy,8}");
Console.WriteLine($"{"Zdalo:",-20}{zdanych,8}");
Console.WriteLine($"{"Zdawalnosc:",-20}{procentZdanych,7:F1}%");
Console.WriteLine();
Console.WriteLine($"Podium: {string.Join(", ", rankingNazwiska[..3])}");
=== WYNIKI EGZAMINU ===
Lp. Nazwisko Punkty Status
----------------------------------------
1 Zielinski 95 zdany
2 Nowak 91 zdany
3 Kaminski 88 zdany
4 Wojcik 77 zdany
5 Wozniak 73 zdany
6 Kowalski 68 zdany
7 Szymanski 61 zdany
8 Kowalczyk 52 zdany
9 Wisniewski 45 niezdany
10 Lewandowski 34 niezdany
----------------------------------------
Srednia: 68,40
Mediana: 70,50
Najlepszy wynik: 95
Najgorszy wynik: 34
Zdalo: 8
Zdawalnosc: 80,0%
Podium: Zielinski, Nowak, Kaminski
Omówienie
| Fragment | Co robi i dlaczego tak |
|---|---|
int[] posortowane = wyniki[..]; |
Kopia przed sortowaniem. Bez niej Array.Sort przestawiłby oryginalną tablicę i przestałaby ona pasować do tablicy nazwisk — wyniki przypisałyby się do niewłaściwych osób. To najgroźniejszy błąd w tym programie, bo nic się nie wysypie, a dane będą fałszywe. |
posortowane[^1] |
Ostatni element posortowanej tablicy to największa wartość. Sortowanie daje minimum i maksimum „przy okazji”, bez osobnej pętli. |
(posortowane[4] + posortowane[5]) / 2.0 |
Mediana przy parzystej liczbie elementów to średnia dwóch środkowych. Dla dziesięciu wyników są to indeksy 4 i 5. Dzielimy przez 2.0, a nie przez 2 — inaczej byłoby to dzielenie całkowite i 141 / 2 dałoby 70 zamiast 70,5. |
Array.Sort(rankingPunkty, rankingNazwiska) |
Sortowanie z kluczem: punkty decydują, nazwiska jadą za nimi. Obie tablice muszą być kopiami, bo obie zostaną przestawione. |
dwa razy Array.Reverse |
Uwaga: trzeba odwrócić obie tablice. Odwrócenie tylko punktów rozjechałoby je z nazwiskami — i znowu: program by nie protestował, tylko skłamał. |
rankingNazwiska[..3] |
Trzy pierwsze elementy posortowanej malejąco tablicy, czyli podium. Zakres z lekcji 23 zastępuje pętlę. |
(double)zdanych / wyniki.Length * 100 |
Rzutowanie przed dzieleniem — bez niego 8 / 10 dałoby 0, a zdawalność wyszłaby zerowa. |
Sprawdź rachunek
Suma wyników: 68 + 91 + 45 + 77 + 52 + 88 + 34 + 95 + 61 + 73 = 684, więc średnia to 68,40. Po posortowaniu: 34, 45, 52, 61, 68, 73, 77, 88, 91, 95 — środkowe to 68 i 73, mediana (68 + 73) / 2 = 70,50. Zdało 8 z 10, czyli 80,0%. Zwróć uwagę, że mediana jest wyższa od średniej — bo dwa bardzo słabe wyniki ciągną średnią w dół, a mediany prawie nie ruszają.
Na czym się najczęściej potykamy
| Błąd | Co się dzieje i jak poprawić |
|---|---|
liczby = Array.Sort(liczby); |
CS0029 — Sort nie zwraca tablicy, tylko zmienia tę podaną. Poprawnie: Array.Sort(liczby);. |
liczby.Sort() |
CS1061 — tablica nie ma metody Sort. To metoda klasy Array: Array.Sort(liczby). (Typ List<T> ma własną Sort() — stąd zamieszanie.) |
| sortowanie oryginału zamiast kopii | Bezpowrotna utrata pierwotnej kolejności. Jeśli druga tablica opisuje te same obiekty, dane przestają do siebie pasować — i nic tego nie zasygnalizuje. |
| odwrócenie tylko jednej z dwóch tablic | Nazwiska rozjeżdżają się z wynikami. Array.Reverse trzeba wywołać dla każdej tablicy osobno. |
BinarySearch bez sortowania |
Zwraca błędny wynik i nie zgłasza problemu. Zawsze sortuj wcześniej. |
Array.Resize(liczby, 5) bez ref |
CS1620 — argument musi być przekazany ze słowem ref. |
Array.Copy do zbyt małej tablicy |
ArgumentException przy uruchomieniu. Tablica docelowa musi istnieć i pomieścić kopiowane elementy. |
a == b dla tablic |
Porównuje adresy. Do zawartości: a.SequenceEqual(b) albo pętla — z porównaniem długości na początku. |
Array.Clear jako „usuwanie” |
Nie usuwa elementów, tylko wpisuje wartości domyślne. Rozmiar tablicy jest niezmienny. |
Array.Resize w pętli |
Kwadratowo rosnąca liczba kopiowań. Gdy rozmiar ma rosnąć, użyj List<T>. |
Zadania
Mając tablicę liczb, wypisz ją w trzech postaciach: oryginalnej, posortowanej rosnąco i posortowanej malejąco. Oryginał ma pozostać nietknięty.
Sprawdź, czy tablica jest już posortowana rosnąco — bez jej sortowania i bez tworzenia kopii.
Napisz program, który „usuwa” z tablicy element o podanym indeksie. Skoro rozmiaru zmienić się nie da, utwórz nową tablicę mniejszą o jeden.
Mając tablicę nazw produktów i tablicę sprzedanych sztuk, wypisz ranking malejąco wraz z udziałem procentowym każdego produktu w całości sprzedaży.
Mając dwie posortowane rosnąco tablice, utwórz z nich jedną — też posortowaną. Zrób to bez sortowania wyniku: przechodź obie tablice jednocześnie i za każdym razem bierz mniejszy element.
Z tablicy liczb usuń powtórzenia, zachowując kolejność pierwszych wystąpień. Wynik ma być tablicą o dokładnie takim rozmiarze, ile jest różnych wartości.
Co trzeba zapamiętać
- Array.Sort(tablica) i Array.Reverse(tablica) zmieniają kolejność na miejscu, nie zwracają nowej tablicy.
- Sort ma też przeciążenia z własnym porównywaniem. Nie gwarantuje stabilności.
- IndexOf i BinarySearch zwracają wynik wyszukiwania; liczba ujemna oznacza brak.
- BinarySearch wymaga porządku zgodnego z porównywaniem użytym podczas wyszukiwania.
- Array.Resize zmienia referencję przez ref i w razie zmiany długości tworzy nową tablicę. Częste powiększanie lepiej zastąpić List<T>.
- SequenceEqual porównuje elementy w kolejności; == porównuje tożsamość tablic.
- Sortuj kopię, gdy pierwotna kolejność jest nadal potrzebna.