Rozdział 6. Tablice C#

> Dodaj do ulubionych

Potrafimy już tworzyć programy wczytujące wartości, obliczające wyniki i je wyświetlające. Pisane przez nas programy mogą także podejmować decyzje na podstawie wartości przekazanych przez użytkownika, a także powtarzać czynności określoną liczbę razy. Wygląda na to, że poznaliście już prawie wszystkie elementy C#, które są potrzebne do zaimplementowania dowolnego programu. Musicie opanować jeszcze tylko jedną rzecz: tworzenie programów przechowujących duże ilości danych. Umożliwiają to np. tablice C#, dlatego teraz poszerzymy wiedzę na ich temat.

6.1. Do czego potrzebne są tablice C#

Zdążyliście już wyrobić sobie renomę jako programiści. Waszym kolejnym klientem jest facet prowadzący lokalny klub piłki nożnej. Potrzebuje programu, który pomoże mu analizować skuteczność piłkarzy. W futbolu do zwycięstwa drużyny najbardziej przyczyniają się zdobyte gole. Nasz klient oczekuje czegoś prostego: chce, aby po wprowadzeniu statystyk bramkowych zawodników po każdym meczu tworzona była lista najlepszych strzelców, w rosnącej kolejności.

Przystępujecie zatem do opracowania specyfikacji i uzupełniacie ją o metadane. Omawiacie z klientem sensowny przedział dopuszczalnych wartości (żaden gracz nie jest w stanie strzelić w jednym meczu mniej niż 0 ani więcej niż 30 bramek). Przygotowujecie szkic programu uwzględniający dane wejściowe i wyjściowe. Negocjujecie również wysokość wynagrodzenia i termin wypłaty. Na koniec, by był to w pełni profesjonalny projekt, ustalacie termin oddania gotowego programu oraz sposób, w jaki będziecie informować Waszego nowego klienta o postępach prac. Na koniec sporządzacie i podpisujecie wraz z klientem umowę. Gdy już macie za sobą formalności, pozostaje Wam wziąć się za programowanie. Myślicie sobie: „łatwizna”. Na początek trzeba określić sposób przechowywania danych:

int score1, score2, score3, score4, score5, score6, score7
    score8, score9, score10, score11;

Teraz możecie zacząć umieszczać dane w każdej zmiennej. Może się do tego przydać nowa metoda wczytująca liczby:

score1 = readInt ( "Wynik zawodnika 1: ", 0,1000);
score2 = readInt ( "Wynik zawodnika 2: ", 0,1000);
score3 = readInt ( "Wynik zawodnika 3: ", 0,1000);
score4 = readInt ( "Wynik zawodnika 4: ", 0,1000);
score5 = readInt ( "Wynik zawodnika 5: ", 0,1000);
score6 = readInt ( "Wynik zawodnika 6: ", 0,1000);
score7 = readInt ( "Wynik zawodnika 7: ", 0,1000);
score8 = readInt ( "Wynik zawodnika 8: ", 0,1000);
score9 = readInt ( "Wynik zawodnika 9: ", 0,1000);
score10 = readInt ( "Wynik zawodnika 10: ", 0,1000);
score11 = readInt ( "Wynik zawodnika 11: ", 0,1000);

Pozostało jeszcze posortować wyniki… Hm… Coś strasznego! Chyba nie ma na to sposobu. Samo sprawdzenie, czy wynik score1 ma największą wartość wymagałoby porównania go z 10 innymi zmiennymi w instrukcji if! Musi więc istnieć jakieś lepsze rozwiązanie — w końcu wiemy, że komputery są dobre w tego typu zadaniach.

W języku C# dostępna jest struktura zwana tablicą. W tablicy można zadeklarować cały szereg pudełek określonego typu. By użyć określonego pudełka przechowywanego w tablicy, należy wskazać je za pomocą tak zwanego indeksu. Spójrzmy na poniższy przykład:

using System; 
class ArrayDemo 
{
    public static void Main () 
    {
        int [] scores = new int [11] ; 
        for ( int i=0; i<11; i=i+1) 
        {
            scores [i] = readInt ( "Wynik: ", 0,1000); 
        } 
    } 
}

Kod int [] scores mówi kompilatorowi, że chcemy utworzyć zmienną tablicową. To coś w stylu znacznika, przy użyciu którego możemy odwołać się do danej tablicy.

Za samo powstanie tablicy odpowiada natomiast fragment new int [11]. Widząc taką instrukcję, C# myśli sobie: „Acha! Tutaj potrzeba tablicy”. Następnie idzie po kilka desek i skleca z nich długie cienkie pudełko z 11 przegródkami, z których każda jest w stanie pomieścić jedną liczbę całkowitą. Później maluje je na kolor, jakim oznaczone są pudełka przechowujące liczby całkowite. Na koniec C# bierze kawałek sznurka i przywiązuje do pudełka etykietę z napisem scores. Sznurek doprowadzi nas zatem do tablicy.

6.2. Elementy tablicy

Każda przegródka pudełka to jeden element. By określić, który element mamy na myśli, umieszczamy jego numer w nawiasie kwadratowym po nazwie tablicy. Jest to tak zwany indeks. Jedną z najlepszych cech tablic jest możliwość wskazania elementu za pomocą zmiennej. W miejsce indeksu możemy tak naprawdę użyć dowolnego wyrażenia, którego wynik jest liczbą całkowitą, np. zapis:

scores [i+1]

jest do przyjęcia (o ile nie wyjdziemy poza ostatni element tablicy). Po utworzeniu tablicy wartości wszystkich jej elementów są ustawione na 0.

6.2.1. Numerowanie elementów tablicy

W C# przegródki tablicy są numerowane od 0. Oznacza to, że do pierwszego elementu odwołujemy się za pomocą indeksu 0. W naszej tablicy z wynikami nie istnieje zatem element scores [11]. Jeśli przyjrzycie się części programu wczytującej wartości do tablicy, to zobaczycie, że liczymy od 0 do 10. To bardzo ważne. Odwołanie się do elementu wykraczającego poza zakres tablicy powoduje błąd wykonania programu. Jeżeli trudno Wam zrozumieć, na czym polega indeksowanie od zera, to wyobraźcie sobie, że indeks jest położony o jeden stopień niżej od elementu, do którego chcemy się odwołać.

Dodatkowo mylące może być to, że zasady numerowania elementów tablicy nie są jednakowe we wszystkich językach. W Visual Basic stosowane jest indeksowanie od 1. Przykro mi, ale — jak widać — nie wszystko w programowaniu jest spójne.

6.3. Duże tablice C#

To, co stanowi o niezwykłej użyteczności tablic, to możliwość wskazania potrzebnego elementu za pomocą zmiennej. Dzięki inkrementacji wartości zmiennej w obrębie określonego przedziału możemy przejrzeć zawartość tablicy za pomocą małego programu. Aby więc zmienić liczbę wczytywanych wyników do 1000, wystarczy wprowadzić drobne zmiany:

using System; 
class ArrayDemo 
{
    public static void Main () 
    {
        int [] scores = new int [1000] ; 
        for ( int i=0; i<1000; i=i+1) 
        {
            scores [i] = readInt ( "Wynik: ", 0,1000); 
        } 
    } 
}

Zmienna i przyjmuje teraz wartości z przedziału od 0 do 999, co znacznie zwiększa ilość przechowywanych w tablicy danych.

6.3.1. Zarządzanie rozmiarem tablicy

Godną polecenia sztuczką jest wykorzystanie stałych do przechowywania rozmiaru tablicy. Zyskujemy dzięki temu dwie korzyści:

  • program staje się bardziej zrozumiały, a także
  • łatwo wprowadzić w nim zmiany.

W chwili deklaracji stała otrzymuje wartość, która jest przeznaczona tylko do odczytu. Program nie może jej już później zmienić. Jeśli chcemy więc napisać program zapisujący wyniki, który będzie można łatwo dostosować do rozmiaru drużyny, to napiszemy np. coś takiego:

using System; 
class ArrayDemo 
{
    public static void Main () 
    {
        const int SCORE_SIZE = 1000; 
        int [] scores = new int [SCORE_SIZE] ; 
        for ( int i=0; i < SCORE_SIZE; i=i+1) 
        {
            scores [i] = readInt ( "Wynik: ", 0,1000); 
        } 
    } 
}

Przykład 16. Zastosowanie tablicy

SCORE_SIZE to zmienna całkowitoliczbowa oznaczona słowem kluczowym const. Oznacza to, że instrukcje programu nie mogą zmienić jej wartości — będzie zawsze wynosić 1000. Zgodnie z konwencją nazwy stałych tego rodzaju zapisywane są WIELKIMI LITERAMI, a poszczególne słowa oddziela znak podkreślenia.

Wszędzie tam, gdzie do tej pory stosowałem niezmienną wartość do wyrażenia rozmiaru tablicy, teraz używam stałej. Jeśli rozmiar zespołu ulegnie zmianie, wystarczy tylko zmienić w deklaracji stałej przypisaną jej wartość i ponownie skompilować program. Ponadto dzięki temu rozwiązaniu pętla for jest bardziej zrozumiała. Zmienna i przyjmuje teraz wartości z zakresu od 0 do SCORE_SIZE, dzięki czemu czytelnik może się łatwiej domyślić, że za pomocą tej pętli przeglądamy tablicę C# z wynikami.

6.4. Tworzenie tablicy dwuwymiarowej

Czasami musimy przechować więcej niż jeden wiersz wartości. Może się zdarzyć, że będziemy potrzebowali tablicy złożonej z wierszy i kolumn. To tak zwana tablica dwuwymiarowa. O ile nie przyprawi to Was o ból głowy, to możecie postrzegać ją jako „tablicę złożoną z tablic”. Aby np. przechowywać planszę do gry w kółko i krzyżyk możemy posłużyć się poniższym kodem:

int [,] board = new int [3,3];
board [1,1] = 1;

Widzimy tu coś bardzo podobnego do tablicy jednowymiarowej, jednak są tu też istotne różnice. Pomiędzy nawiasami kwadratowymi znajduje się przecinek, co sugeruje, że po obu jego stronach będą znajdować się jakieś wartości. Tablica jest więc teraz dwuwymiarowa. Określając rozmiar planszy, musimy zatem podać dwa wymiary, a nie tylko jeden. Ponadto, odwołując się do konkretnego elementu, trzeba posłużyć się dwoma indeksami. W powyższym kodzie ustawiłem wartość środkowego (najlepszego) pola na 1.

Tablica dwuwymiarowa przypomina siatkę:

tablice c# - tablica dwuwymiarowa

Pierwszy indeks będzie wskazywał rząd, zaś drugi kolumnę, w której znajduje się dana wartość. Powyższy diagram ilustruje planszę, na której wykonano już najlepszy ruch.

W tym przykładzie tablica jest kwadratowa (czyli zawiera tyle samo kolumn co wierszy). Jeśli chcemy, możemy to zmienić:

int [,] board = new int [3,10];

jednak taka plansza nie nadawałaby się do rozegrania żadnej sensownej gry.

6.5. Tablice przeglądowe

Zwykłe tablice C# świetnie sprawdzają się jako tablice przeglądowe. Za ich pomocą można uzyskać dostęp do danych przez podanie liczby. Wyobraźmy sobie, że chcemy wyświetlić nazwę miesiąca o podanej wartości. Da się to zrobić przy użyciu testów:

int monthNo = 1;
string monthName;

if (monthNo == 1)
monthName = "styczeń";

if (monthNo == 2)
monthName = "luty";

Napisanie takiego kodu byłoby jednak żmudnym zadaniem. Lepiej będzie skorzystać z tablicy, którą możemy nazwać monthNames:

monthName = monthNames[monthNo];

Aby powyższy kod zadziałał, musimy upewnić się, że wszystkie elementy tablicy monthNames przechowują jej nazwę.

string[] monthNames = new string[13];

Przed użyciem tablicy przeglądowej należy więc dokonać odpowiedniej konfiguracji:

monthNames[1] = "styczeń";
monthNames[2] = "luty";

Złota myśl programisty: czasem można „zmarnować” zerowy element tablicy

Jeśli do tej pory czytaliście uważnie, to z pewnością zauważyliście coś dziwnego. Powiedziałem, że tablice C# są indeksowane od zera — innymi słowy pierwszy element tablicy monthNames ma indeks zero, a zatem powinienem umieścić łańcuch „styczeń” w elemencie monthNames[0]. To prawda, jeśli zależałoby mi na maksymalnym wykorzystaniu pamięci, jednak wtedy program byłby mniej czytelny. By odczytać nazwę miesiąca, za każdym razem musiałbym odejmować 1 od jego wartości (która oczywiście znajduje się w przedziale od 1 do 12) Uważam, że w tym przypadku uzasadnione jest utworzenie tablicy zawierającej jeden dodatkowy element (czyli mającej indeksy od 0 do 13) i zignorowanie początkowego elementu.

Puryści mogliby stwierdzić, że jeśli program użyje zerowego elementu tablicy, to może wyświetlić nieprawidłową wartość miesiąca. Możemy jednak temu zapobiec, zmieniając problematyczny element na referencję pustą.

monthNames[0] = null;

Jeśli kod spróbuje się do niego odwołać, to zostanie zgłoszony błąd i program zostanie zatrzymany. Oczywiście purystom może to nie wystarczyć, ponieważ wciąż istnieje ryzyko błędnego wykonania programu. Radziłbym im jednak zastanowić się, co by się stało, gdyby podający wartość miesiąca programista zapomniał pomniejszyć indeks o 1. Wówczas na ekranie pojawiłaby się błędna nazwa miesiąca, a kod wykonywany byłby dalej. Wolę więc, by program się spektakularnie „wysypał”, niż po cichu wykonał nieprawidłową operację.

Póki co wygląda na to, że zastąpiliśmy jedno mozolne rozwiązanie (wykonywanie wielu testów w celu uzyskania nazwy miesiąca) innym (inicjalizowaniem wielu elementów tablicy). Jak się jednak okazuje, w C# istnieje wygodny sposób na ustawienie wartości należących do tablicy elementów.

string[] monthNames = new string[] 
{
    null, // element null dla nieistniejącego miesiąca 0 
    "styczeń", "luty", "marzec", "kwiecień", 
    "maj", "czerwiec", "lipiec", "sierpień", 
    "wrzesień", "październik", "listopad", "grudzień" 
};

Program może zawierać listę wartości inicjalizacyjnych, które zostaną wykorzystane do utworzenia tablicy o określonej zawartości. Warto zauważyć, że jeśli będzie trzeba, to podczas inicjalizacji zostanie także automatycznie obliczony rozmiar tablicy. Inicjalizować możemy również tablice C# dwuwymiarowe.

int [,] squareWeights = new int [3,3] 
{
    {1,0,1}, 
    {0,2,0}, 
    {1,0,1} 
};

Za pomocą powyższego kodu utworzyłem tablicę dwuwymiarową zawierającą wartości ważone, które można by wykorzystać w sprytnym programie do gry w kółko i krzyżyk. Wyraziłem w ten sposób moje przekonanie, że najcenniejsze są pola narożne i pole środkowe. Gdy program będzie wykonywał swój pierwszy ruch, to poszuka pola o najwyższej wartości i spróbuje je zająć.

6.6. Wiele wymiarów

Od wielkiego dzwonu może się zdarzyć, że będziemy potrzebować więcej niż dwóch wymiarów. Tablicę trójwymiarową można wyobrazić sobie jako poukładane na sobie siatki. Konkretną siatkę wskazuje wówczas trzeci wymiar (oznaczony literą z). Jeśli chcielibyśmy zagrać w trójwymiarowe kółko i krzyżyk na planszy będącej sześcianem, to możemy zadeklarować następującą tablicę:

int [,,] board = new int [3,3,3]; 
board [1,1,1] = 1;

W ten sposób napisaliśmy trójwymiarową planszę, której najbardziej wartościowe pole znajduje się w środku sześcianu.

Utworzenie tablicy trójwymiarowej to dla C# żaden problem. Kłopoty możemy mieć jednak my sami — z jej zrozumieniem i wyobrażeniem sobie.

Czasem może się nam wydawać, że potrzebujemy drugiego wymiaru, podczas gdy tak naprawdę wystarczy dodatkowa tablica. Wróćmy na chwilę do programu przechowującego statystyki bramkowe. Jeśli trener zażyczy sobie, by w programie były przechowywane także nazwiska zawodników, to nie będziemy dodawać do tablicy kolejnego wymiaru, lecz zadeklarujemy przeznaczoną do tego nową tablicę C#:

int [] scores = new int [11];
string [] names = new string [11];

Program musiałby jeszcze dopilnować, aby nazwisko i liczba strzelonych bramek były odpowiednio skorelowane. Innymi słowy chodzi o to, by zerowy element tablicy z nazwiskami zawierał nazwisko gracza, który strzelił liczbę goli przechowaną pod indeksem 0 w tablicy z wynikami.

Złota myśl programisty: ogranicz liczbę wymiarów do minimum

W ciągu całej mojej kariery programistycznej nigdy nie musiałem korzystać z więcej niż trzech wymiarów. Jeśli więc korzystacie z wielowymiarowych tablic, może być to znak, że Wasze podejście jest nieprawidłowe i powinniście spojrzeć na problem z dystansu. Czasami można uzyskać o wiele wydajniejsze rozwiązanie poprzez utworzenie struktury i późniejsze umieszczenie jej elementów w tablicy. O strukturach powiemy w dalszej części książki. Tablice C# to nie jedyne rozwiązanie do przechowywania danych.

Autor: Rob Miles

Tłumaczenie: Joanna Liana