Rozdział 5. Metody C#

01 marca 2021
1 gwiadka2 gwiazdki3 gwiazdki4 gwiazdki5 gwiazdek

W tym rozdziale rozwiniemy nasze umiejętności tworzenia programów podzielonych na łatwe w zarządzaniu części — metody C#. Potem dowiemy się, jak programy przechowują duże ilości danych i jak na nich operują przy użyciu tablic.

Widzieliśmy już metody C# Main, WriteLine i ReadLine. Main to metoda, którą piszemy samodzielnie. Od niej program rozpoczyna swoje działanie. WriteLine i ReadLine to natomiast metody udostępnione przez twórców C#, umożliwiające wyświetlanie tekstu i odczytywanie informacji pochodzących od użytkownika.

Oto właśnie chodzi w metodach. W programach będziemy korzystać z metod C#, które napiszemy sami w celu rozwiązania jakiegoś problemu, a także z tych opracowanych przez innych programistów. W tej sekcji zastanowimy się, dlaczego metody są użyteczne oraz sprawdzimy, jak można tworzyć własne metody C#.

5.1. Przydatność metod w C#

W przypadku kalkulatora materiałów okiennych spędziliśmy sporo czasu na sprawdzaniu, czy wprowadzone wartości mieszczą się w określonych przedziałach. Do kontrolowania szerokości i wysokości użyliśmy dokładnie tego samego kodu. Gdybyśmy wczytywali jeszcze jeden parametr, na przykład grubość ramy okna, to skopiowalibyśmy go po raz trzeci. Nie jest to zbyt wydajne rozwiązanie — w ten sposób program się rozrasta i sprawia coraz więcej kłopotów. Wolelibyśmy napisać kod sprawdzający raz, a następnie użyć go w odpowiednich miejscach programu. W tym celu musimy zdefiniować metodę, która zrobi to za nas.

5.1.1. Metody C# a lenistwo

Metody C# - lenistwo

Jak zdążyliśmy już ustalić, dobrzy programiści są kreatywnie leniwi. W związku z tym programista będzie starał się wykonać określone zadanie tylko raz.

Do tej pory wszystkie nasze programy zawierały się w jednej metodzie. To blok kodu, który następował po słowie Main. W języku C# można jednak tworzyć inne metody i korzystać z nich podczas wykonywania programu. Dzięki metodom zyskujemy dwie nowe programistyczne moce. Za pomocą metod C# możemy:

  • ponownie wykorzystać raz napisany fragment kodu,
  • a także podzielić jedno duże zadanie na kilka mniejszych.

Gdy przejdziemy do pisania większych programów, będziemy używać obu tych technik. Podobnie jak większość składników języka C#, metody tak naprawdę niczego nie umożliwiają, lecz ułatwiają organizację programów.

Bierzemy po prostu blok kodu i nadajemy mu nazwę, po której później możemy się do niego odwołać, jeśli chcemy z jego pomocą wykonać jakieś zadanie. Spójrzcie na ten głupi przykład:

using System ;
class MethodDemo 
{
    static void doit () 
    {
        Console.WriteLine ("Cześć"); 
    }
    public static void Main ()
    {
        doit();
        doit();
    }
}

Przykład 09. Prosta metoda C#

W głównej metodzie umieściłem dwa wywołania metody doit. Każde z nich powoduje wykonanie kodu stanowiącego treść główną tejże metody. W tym przypadku zawiera ona prostą instrukcję wyświetlającą w konsoli napis Cześć. Jeśli zatem wykonamy ten program, to na ekranie zobaczymy coś takiego:

Cześć
Cześć

Dzięki metodom C# możemy więc uniknąć pisania tego samego kodu dwa razy. Wystarczy umieścić go w metodzie i w razie potrzeby ją wywołać.

5.2. Parametry

Na tę chwilę metody są użyteczne, ponieważ dzięki nim można użyć tego samego bloku instrukcji w wielu miejscach programu. Ich przydatność wrasta jednak jeszcze bardziej, jeśli mogą mieć parametry.

Parametr to sposób na przekazanie wartości do wywoływanej metody. Metoda otrzymuje zatem dane, na których będzie operować. W ramach przykładu spójrzmy na poniższy kod:

using System ;
class MethodDemo
{
    static void silly ( int i )
    {
        Console.WriteLine ( "i wynosi: " + i ) ;
    }
    public static void Main ()
    {
        silly ( 101 ) ;
        silly ( 500 ) ;
    }
}

Przykład 10. Metoda z parametrami

Metoda silly ma jeden parametr całkowitoliczbowy. W bloku kodu stanowiącym treść główną metody C# możemy użyć parametru i jako zmiennej całkowitoliczbowej. Podczas wywołania metoda otrzymuje wartość podaną dla parametru. Wynik wykonania programu będzie zatem następujący:

i wynosi : 101
i wynosi : 500

5.3. Wartości zwrotne metod C#

Metoda może również zwrócić wartość do kodu inicjalizującego jej wywołanie. Już korzystaliśmy z tego elementu funkcjonalności — zarówno metoda ReadLine, jak i Parse zwracają wyniki, których użyliśmy w naszych programach:

using System ;
class ReturnDemo 
{
    static int sillyReturnPlus ( int i ) 
    {
        i = i + 1; Console.WriteLine ( "i wynosi: " + i ) ;
        return i;
    }
    public static void Main ()
    {
        int res;
        res = sillyReturnPlus (5);
        Console.WriteLine ( "res wynosi: " + res ) ;
    }
}

Przykład 11. Powiększona wartość zwrotna

Metoda sillyReturnPlus przyjmuje wartość parametru i zwraca ją powiększoną o jeden.

Z wartości zwrotnej można skorzystać w programie wszędzie tam, gdzie da się użyć zmiennej tego samego typu. Innymi słowy, wywołanie metody C# sillyReturnPlus może zająć miejsce zmiennej całkowitoliczbowej. Teraz zastanówcie się, co zobaczymy po wykonaniu poniższego kodu:

res = sillyReturnPlus (5) + sillyReturnPlus (7) + 1;
Console.WriteLine ( "res wynosi: " + res ) ;

Wartość zwracaną przez metodę można wprawdzie zignorować, jednak nie ma to większego sensu:

sillyReturnPlus (5); // kod zostanie skompilowany, lecz nie wyniknie z tego nic pożytecznego

5.4. Argumenty a parametry

Czytając publikacje na temat języka C#, zobaczycie, że słowa „parametr” i „argument” używane są zamiennie. Nie jest to do końca poprawne. Jeśli zrozumiecie różnicę pomiędzy tymi dwoma pojęciami, to dokumentacja języka C# i komunikaty o błędach nabiorą dla Was większego sensu. Być może poczujecie się też lepsi od innych programistów, którzy są w tym zakresie niedoinformowani.

Parametr to szczególny rodzaj zmiennej zdefiniowanej w nagłówku metody C# i używanej wewnątrz niej do reprezentacji wartości przekazywanej do wywołania tej metody.

static int sillyReturnPlus ( int i) 
{
    i = i + 1;
    Console.WriteLine ( "i wynosi: " + i );
    return i; 
}

Powyższa metoda sillyReturnPlus ma jeden parametr typu całkowitoliczbowego, oznaczony literą i.

Argument to natomiast konkretna wartość przekazana metodzie podczas jej wywołania.

sillyReturnPlus(99);

W powyższej instrukcji argumentem jest wartość 99. Oznacza to, że jeśli zrobię coś głupiego:

sillyReturnPlus("banjo");

to przy próbie kompilacji kodu otrzymam taki komunikat o błędzie:

Error 2          Argument 1: cannot convert from 'string' to 'int'

Kompilator mówi mi, że argument (to, co umieściłem w nawiasie w wywołaniu metody) jest niezgodny z definicją parametru (który został określony jako liczba całkowita).

5.5. Użyteczne metody C#

Teraz możemy przejść do pisania naprawdę przydatnych metod:

static double readValue (
    string prompt, // zapytanie kierowane do użytkownika 
    double low, 	// najniższa dopuszczalna wartość 
    double high 	// najwyższa dopuszczalna wartość
    ) 
{
    double result = 0; 
    do 
    {
        Console.WriteLine (prompt + " pomiędzy " + low + " a " + high ); 
        string resultString = Console.ReadLine (); 
        result = double.Parse(resultString); 
    } while ( (result < low) || (result > high) ); 
    return result ; 
}

Metoda readValue zawiera zapytanie, jakie ma zostać wyświetlone użytkownikowi oraz najniższą i najwyższą dopuszczalną wartość. Dzięki temu można za jej pomocą odczytywać dowolne wartości i sprawdzać, czy mieszczą się w określonym przedziale.

double windowWidth = readValue ("Podaj szerokość okna: ", MIN_WIDTH, MAX_WIDTH);
double age = readValue ( "Podaj swój wiek: ", 0, 70);

W pierwszym wywołaniu metoda readValue pobiera szerokość okna, w drugim zaś wczytuje wiek z przedziału od 0 do 70.

using System;
class UsefulMethod 
{
    static double readValue(
        string prompt, // zapytanie kierowane do użytkownika 
        double low, 	// najniższa dopuszczalna wartość 
        double high 	// najwyższa dopuszczalna wartość 
    ) 
    {
        double result = 0; 
        do 
        {
            Console.WriteLine(prompt + " pomiędzy " + low + " a " + high); 
            string resultString = Console.ReadLine(); 
            result = double.Parse(resultString); 
        } while ((result < low) || (result > high)); 
            return result; 
    }
    const double MAX_WIDTH = 5.0; 
    const double MIN_WIDTH = 0.5;
    public static void Main() 
    {
        double windowWidth = readValue("Podaj szerokość okna: ", MIN_WIDTH, MAX_WIDTH);
        Console.WriteLine("Szerokość: " + windowWidth);
        double age = readValue("Podaj swój wiek: ", 0, 70);
        Console.WriteLine("Wiek: " + age); 
    }
}

Przykład 12. Zastosowanie użytecznej metody C#

Złota myśl programisty: projektuj w oparciu o metody

Metody stanowią bardzo przydatne narzędzie programistyczne i odgrywają istotną rolę w procesie rozwoju oprogramowania. Gdy już ustalimy oczekiwania klienta i zbierzemy metadane, powinniśmy się zastanowić, jak rozbić nasz program na metody. Podczas pisania kodu nie raz przekonacie się, że powtarzacie tę samą czynność. W takim przypadku należy rozważyć, czy nie można by umieścić jej w metodzie. To dobre rozwiązanie z dwóch powodów:

  1. Nie trzeba pisać dwa razy tego samego kodu.
  2. Jeśli kod jest wadliwy, usterkę wystarczy naprawić tylko w jednym miejscu.

Przenoszenie kodu i tworzenie metod nazywamy refaktoryzacją. To istotna część inżynierii oprogramowania, którą będziemy się zajmować później.

5.6. Argumenty nazwane i opcjonalne

Tworząc metodę mającą parametry, informujemy kompilator o nazwie i typie każdego kolejnego parametru:

static double readValue (
    string prompt, // zapytanie kierowane do użytkownika 
    double low, 	// najniższa dopuszczalna wartość 
    double high 	// najwyższa dopuszczalna wartość 
    ) 
{
    ... 
}

ReadValue została zdefiniowana jako metoda o trzech parametrach. Wywołanie metody musi zatem przyjąć trzy wartości argumentów: zapytanie kierowane do użytkownika, najniższą, a także najwyższą wartość.

Poniższe wywołanie metody readValue zostanie zatem odrzucone przez kompilator:

x = readValue(25, 100, "Podaj swój wiek");

Stanie się tak, ponieważ łańcuch z zapytaniem musi być podany w pierwszej kolejności, a po nim najniższa i najwyższa wartość, jaką może wczytać program.

Jeśli chcemy wywoływać metody, nie przejmując się porządkiem argumentów, możemy każdy z nich nazwać:

x = readValue(low:25, high:100, prompt: "Podaj swój wiek");

Teraz kompilator będzie kierował się nazwą argumentu, a nie jego miejscem w szeregu. Rozwiązanie to ma także dodatkową zaletę — dzięki niemu osobie czytającej nasz kod jest o wiele łatwiej dokładnie zrozumieć znaczenie poszczególnych argumentów.

Złota myśl programisty: uwielbiam korzystać z argumentów nazwanych

To jedna z tych rzeczy, które w języku C# lubię najbardziej. Dzięki niej programy są czytelniejsze, a programista nie musi drapać się po głowie i zastanawiać, czy w wywołaniu metody należy najpierw podać najniższą czy najwyższą wartość.

5.6.1. Argumenty opcjonalne

Czasami domyślna wartość argumentu może być całkiem sensowna. Przykładowo jeśli chcielibyśmy tylko pobrać wartość od użytkownika bez wyświetlania żadnego tekstu, moglibyśmy przekazać metodzie readValue pusty łańcuch:

x = readValue(low:25, high:100, prompt: "");

To jednak nieco niechlujny zapis. Innym rozwiązaniem jest zmiana definicji metody i podanie w niej domyślnej wartości parametru prompt:

    static double readValue (
        double low,          // najniższa dopuszczalna wartość 
        double high,         // najwyższa dopuszczalna wartość 
        string prompt = "",  // opcjonalne zapytanie do użytkownika 
        ) 
{
        ... 
}

Możemy teraz wywołać metodę z pominięciem zapytania:

x = readValue(25, 100);

Jeśli użytkownik nie wprowadzi żadnej wartości, to podczas uruchomienia metody wartość argumentu prompt zostanie ustawiona na pusty łańcuch.

Zwróćcie uwagę, że podałem parametry w takiej kolejności, by zapytanie było ostatnie. Opcjonalne parametry należy umieszczać dopiero po tych wymaganych.

Wiąże się z tym jednak potencjalny problem. Spójrzmy na poniższą metodę:

static double readValue(
    double low, 	// najniższa dopuszczalna wartość 
    double high, 	// najwyższa dopuszczalna wartość 
    string prompt = "", // opcjonalne zapytanie do użytkownika 
    string error = "" 	// opcjonalny komunikat o błędzie
    ) 
{
    ... 
}

Mamy tutaj dwa opcjonalne parametry: zapytanie i komunikat o błędzie. Korzystający z metody programista — jeśli chce — może zatem wprowadzić własny komunikat o błędzie, który pojawi się w przypadku podania przez użytkownika nieprawidłowego wieku. Wywołamy teraz tę metodę w następujący sposób:

x = readValue(25, 100, "Podaj swój wiek", "Wiek poza zakresem");

Jeśli nie potrzebujemy zapytania ani komunikatu, to możemy je pominąć. Jeżeli jednak podamy tylko jeden opcjonalny argument, to zostanie on powiązany z pierwszym opcjonalnym parametrem. Innymi słowy nie można wprowadzić własnego komunikatu o błędzie bez podania własnego tekstu zapytania.

Jak zapewne się już domyślacie, możemy objeść to ograniczenie poprzez nazwanie używanych przez nas parametrów opcjonalnych.

x = readValue(25, 100, error:"Wiek poza zakresem");

W takim wywołaniu metody readValue zostanie użyte domyślne zapytanie, lecz także nasz własny komunikat o błędzie.

Złota myśl programisty: nie jestem wielkim zwolennikiem domyślnych wartości parametrów

Mam nadzieję, że na tym etapie zaczęliście już traktować programowanie jako rzemiosło. Oznacza to zatem, że programowanie określonych zachowań możemy postrzegać w kontekście rzemieślniczego pisania kodu — podobnie jak serowar, którego zaniepokoiłaby sugestia, że mógłby wytwarzać ser przy użyciu piły łańcuchowej.

Parametry domyślne mogą ukrywać informacje przed użytkownikami naszych metod i pełnić funkcję „sekretnych przełączników”, które użytkownik musi znać, jeśli metoda ma zadziałać poprawnie. Jeśli zdecydujecie się na takie rozwiązanie, opiszcie je w komentarzach — zarówno w kodzie metody, jak i w tych miejscach programu, w których jest ona wykorzystywana. Dzięki temu czytelnik kodu zrozumie na czym polega domyślny sposób działania i jak można go zmodyfikować.

5.7. Przekazywanie parametrów przez wartość

Metody dobrze sprawdzają się w wykonywaniu zadań, lecz mogą być ograniczone przez swój sposób działania. Jeśli na przykład chcę napisać metodę wczytującą imię i wiek użytkownika, to pojawia się tu kłopot. Z tego co do tej pory widzieliśmy wynika, że metody mogą zwracać tylko jedną wartość, zatem musielibyśmy zdecydować się na metodę, która albo zwracałaby imię, albo wiek. Nie da się jednak napisać metody zwracającej jednocześnie obie te wartości. Jak sami możecie się przekonać, zmiana wartości parametru nie zmienia wartości tego, co przekazujemy metodzie. O ile nie określimy inaczej, przekazana zostaje jedynie wartość argumentu.

Co zatem oznacza „przekazywanie parametrów przez wartość”? Rozważmy poniższy przykład:

static void addOne ( int i )
{
    i = i + 1;
    Console.WriteLine ( "i wynosi: " + i ) ; 
}

Metoda addOne dodaje 1 do wartości parametru, wyświetla wynik, a następnie zwraca to:

int test = 20;
addOne(test);
Console.WriteLine ( "test wynosi: " + test );

W powyższym kodzie C# wywołana została metoda ze zmienną test przekazaną w miejsce argumentu. Wykonanie metody sprawi, że wyświetli się taki wynik:

i wynosi: 21
test wynosi: 20

To bardzo istotne, by zrozumieć, co się tutaj dzieje. W wywołaniu metody addOne użyta zostaje wartość zmiennej test. Program oblicza wartość wyrażenia, która ma zostać przekazana do wywołania metody jako argument. Tak też się dzieje. Możemy zatem pisać wywołania tego rodzaju:

test = 20;
addOne(test + 99);

Na ekranie zobaczymy:

i wynosi: 120

Przekazywanie przez wartość jest bardzo bezpieczne, ponieważ zachowanie metody w żaden sposób nie wpływa na zmienne znajdujące się w kodzie, który ją wywołuje. Stanowi to jednak przeszkodę w tworzeniu metod zwracających więcej niż jedną wartość.

5.8. Przekazywanie parametrów przez referencję

Zrozumienie działania referencji jest bardzo ważne. Jeśli ich nie rozumiecie, to nie możecie się nazywać prawdziwymi programistami!

Na szczęście w C# możliwe jest także przekazanie metodzie referencji do zmiennej zamiast jej wartości. Wewnątrz metody pobierana jest zatem sama zmienna (za pośrednictwem referencji), a nie jej wartość. To, co przekazujemy metodzie, to tak naprawdę lokalizacja — czyli adres — zmiennej w pamięci, nie jej zawartość.

W powyższym wywołaniu zamiast wartości 20 kompilator wygeneruje zatem kod przekazujący „komórkę pamięci 5023” (zakładając, że zmienna test przechowywana jest w tej lokalizacji). Komórka pamięci zostanie użyta przez metodę w miejsce wartości. Innymi słowy:

„w przypadku przekazywania przez referencję zmiana parametru powoduje także zmianę zmiennej, do której prowadzi przekazana referencja”.

Jeśli referencje wydają się wam skomplikowane, to nie jesteście sami. W praktyce jednak programiści korzystają z nich cały czas bez żadnego problemu. Jeśli mówimy dostawcy, aby „zawiózł dywan na ul. Główną 23”, to przekazujemy mu referencję. Na tym samym polega działanie referencji w programach. Program powie zatem „pobierz wartość z lokalizacji 5023” zamiast „wartość wynosi 1”.

Spójrzmy na poniższy kod:

static void addOneToRefParam ( ref int i )
{
    i = i + 1;
    Console.WriteLine ( "i wynosi: " + i );
}

Zwróćcie uwagę, że informacje o parametrze zostały uzupełnione o słowo kluczowe ref.

test = 20;
addOneToRefParam(ref test);
Console.WriteLine ( "test wynosi : " + test );

Powyższy kod wywołuje nową metodę i również zawiera słowo ref przed parametrem. Wynik wywołania metody będzie tutaj następujący:

i wynosi: 21 
test wynosi: 21

W tym przypadku wywołanie metody spowodowało zmianę treści zmiennej. Warto zauważyć, że język C# zwraca szczególną uwagę na parametry przekazywane przez referencję. Aby skorzystać z tej techniki, w nagłówku metody oraz w jej wywołaniu należy umieścić słowo kluczowe ref.

Złota myśl programisty: dokumentuj skutki uboczne

Spowodowana przez metodę zmiana w kodzie znajdującym się poza jej obszarem to skutek uboczny metody. Innymi słowy wywołanie metody addOneToRefParam będzie mieć „skutek uboczny” w postaci zmiany wartości parametru przekazanego przez referencję). Ogólnie rzecz biorąc, należy zachować ostrożność w wykorzystywaniu skutków ubocznych — osoba czytająca nasz program musi wiedzieć, że wywołane przez metodę zmiany to właśnie skutki uboczne.

5.9. Przekazywanie wartości parametrów przez referencję z użyciem słowa kluczowego out

Przekazując parametr przez referencję, dajemy metodzie pełną kontrolę nad nim. Czasami jednak wolimy tego uniknąć i umożliwić metodzie jedynie zmianę zmiennej, np. gdy chcemy wczytać imię i wiek użytkownika. Metoda nie jest wówczas zainteresowana oryginalną wartością parametrów — chce jedynie umieścić w nich wartości wynikowe. W takim przypadku słowo ref zamieniamy na słowo kluczowe out:

static void readPerson ( out string name, out int age ) 
    {
        name = readString ( "Podaj swój wiek: " ) ; 
        age = readInt ( "Podaj swój wiek: ", 0, 100 ) ; 
    }

Metoda readPerson wczytuje imię i wiek osoby. Zauważcie, że korzysta także z dwóch innych utworzonych przeze mnie metod: readString i readInt.

Metodę readPerson można wywołać w następujący sposób:

string name ; 
int age ; 
readPerson ( out name, out age );

Zwróćcie uwagę, że słowa kluczowego out należy użyć także w samym wywołaniu metody.

Tak wywołana metoda readPerson wczyta dane osobowe i umieści uzyskane informacje w dwóch wskazanych zmiennych.

Złota myśl programisty: języki służą programistom

Słowo kluczowe out pokazuje, jak konstrukcja języka może ułatwić pisanie programów i uczynić je bezpieczniejszymi. Dzięki temu słowu programista nie może użyć wartości parametru w metodzie, a kompilator ma pewność, że w obrębie metody parametrom wynikowym zostaną przypisane wartości. To bardzo użyteczne. Jeśli oznaczę parametry słowem out, to w celu prawidłowej kompilacji programu muszę nadać im wartości. Nie da się więc o tym zapomnieć, dzięki czemu trudniej jest zepsuć program.

5.10. Biblioteki metod

Pierwszą rzeczą, jaką robi dobry programista po przystąpieniu do pisania kodu, jest utworzenie bibliotek ułatwiających pracę. W powyższym kodzie napisałem dwie biblioteki zawierające metody, którymi mogę się posłużyć do wczytania wartości różnego typu:

static string readString ( string prompt ) 
{
    string result ; 
    do 
    {
        Console.Write ( prompt );
        result = Console.ReadLine (); 
        } while ( result == "" );
            return result ; 
        }
        static int readInt ( string prompt, int low, int high ) 
        {
            int result;
            do
            { string intString = readString (prompt);
                result = int.Parse(intString); 
            } while ( ( result < low ) || ( result > high ) );
    return result;
}

Metoda readString wczytuje tekst i sprawdza, czy użytkownik nie podał pustego łańcucha. Metoda readInt wczytuje natomiast liczbę z określonego przedziału. Zwróćcie uwagę, jak całkiem sprytnie wykorzystałem readString wewnątrz metody readInt, dzięki czemu użytkownik nie może wprowadzić pustego łańcucha, jeśli wymagana jest liczba. Oto przykład użycia powyższych metod:

string name;
name = readString( "Podaj swoje imię: " );
int age;
age = readInt ( "Podaj swój wiek: ", 0, 100);

Mógłbym także dodać metody wczytujące wartości zmiennoprzecinkowe. Zwykle podczas pracy nad projektem opracowuję właśnie taką małą bibliotekę z metodami, których mogę użyć.

using System;
class MethodLibraries 
{
static string readString(string prompt) 
{
    string result;
    do
    {
        Console.Write(prompt);
        result = Console.ReadLine();
    } while (result == "");
        return result; 
    }
    static int readInt(string prompt, int low, int high)
    {
        int result;
        do
        {
            string intString = readString(prompt);
            result = int.Parse(intString);
            } while ((result < low) || (result > high));
        return result; 
}
    public static void Main()
    {
        string name;
        name = readString("Podaj swoje imię: ");
        Console.WriteLine("Imię: " + name);
        int age;
        age = readInt("Podaj swój wiek: ", 0, 100);
        Console.WriteLine("Wiek: " + age);
    } 
}

Przykład 13. Zastosowanie biblioteki z metodami

Złota myśl programisty: zawsze bierz pod uwagę błędy w działaniu programu

Za każdym razem, gdy piszemy metodę, powinniśmy się zastanowić nad możliwymi błędami w jej wykonaniu. Jeśli metoda uwzględnia komunikację z użytkownikiem, to użytkownik może zechcieć opuścić metodę bądź zrobić coś, co spowoduje błąd. Musicie zdecydować, czy metoda powinna poradzić sobie z takim problemem sama, czy zgłosić błąd systemowi, który z niej korzysta.

Jeśli to metoda ma poradzić sobie z błędem, to istnieje prawdopodobieństwo wystąpienia kolejnych problemów, ponieważ użytkownik może nie mieć możliwości anulowania polecenia. Natomiast jeśli postanowimy, że błędy mają być przesyłane do kodu inicjalizującego wywołanie, to musimy mieć także metodę, która będzie przekazywać informacje o błędach. Często stosowanym przeze mnie rozwiązaniem jest korzystanie z metod zwracających wartość kodową. Jeśli wartość zwrotna wynosi 0, to znaczy, że metoda zadziałała prawidłowo. Inna wartość oznacza zaś, że wykonanie metody nie powiodło się i wskazuje kod błędu precyzujący, co poszło nie tak. Projektowanie programów zyskuje zatem nowy wymiar — nie tylko musimy upewnić się, że kod spełnia swoje zadanie, ale też przewidzieć możliwe błędy w jego wykonaniu! Zarządzanie błędami omówimy jeszcze w dalszej części książki.

5.11. Zmienne i ich zasięg

Jak już wiemy, aby przechować w programie jakąś ilość, musimy utworzyć zmienną, w której umieścimy te informacje. Kompilator C# dba o to, by do przechowywania wartości wykorzystywana była odpowiednia ilość pamięci i aby wartość była zawsze używana prawidłowo. Ponadto kompilator czuwa nad fragmentem programu, w którym istnieje dana zmienna. To tak zwany zasięg zmiennej.

5.11.1. Zasięg a bloki

Jak już widzieliśmy, blok to zbiór instrukcji umieszczonych w klamrach. Blok może zawierać dowolną liczbę zmiennych lokalnych, czyli dostępnych tylko w tym bloku.

Zasięgiem zmiennej lokalnej jest blok, w którym została zadeklarowana. W języku C# zmienną można zadeklarować w dowolnym miejscu bloku, jednak trzeba to zrobić przed jej użyciem. Jeśli wykonanie programu opuści blok, to wszelkie zmienne lokalne w nim zadeklarowane zostaną automatycznie usunięte z pamięci. Tworzone przez nas metody niejednokrotnie zawierały zmienne lokalne, np. zmienna result w metodzie readInt jest lokalna dla bloku metody.

5.11.2. Bloki zagnieżdżone

Widzieliśmy już, że w języku C# można tworzyć bloki wewnątrz innych bloków. Każdy zagnieżdżony blok ma swoje własne zmienne lokalne:

{
    int i ; 
    {
        int j ; 
    } 
}

Zasięgiem zmiennej j jest blok wewnętrzny. Oznacza to, że tylko zmienne w obrębie bloku wewnętrznego mają dostęp do tej zmiennej. Innymi słowy poniższy kod:

{
    int i ; 
   {
        int j ; 
    } 
    j = 99 ; 
}

spowoduje błąd, ponieważ zmienna j nie istnieje w tym miejscu programu.

Aby się nie pogubić , tworząc dwie wersje zmiennej o tej samej nazwie, w języku C# należy przestrzegać jeszcze jednej zasady dotyczącej zmiennych w blokach zewnętrznych.

{
    int i ; 
    {
        int i ; 
    } 
}

Nie jest to poprawny program, ponieważ C# nie pozwala, by zmienna w bloku wewnętrznym miała taką samą nazwę jak zmienna z bloku zewnętrznego. Ograniczenie to istnieje dlatego, że w bloku wewnętrznym można by przez przypadek użyć wewnętrznej wersji zmiennej i zamiast zewnętrznej. Aby uniknąć takiej sytuacji, kompilator po prostu do niej nie dopuszcza. Warto zauważyć, że w innych językach, np. C++, jest to dozwolone.

Nic jednak nie stoi na przeszkodzie, by skorzystać z tej samej nazwy zmiennej w kolejnych blokach, ponieważ nie da się wtedy pomylić zmiennych.

{
    int i ; 
} 
{
    int i ; 
    {
        int j ; 
    } 
}

Pierwsza zmienna i została usunięta przed zadeklarowaniem drugiej, dlatego powyższy kod jest poprawny.

5.11.3. Zmienne lokalne pętli for

Podczas tworzenia pętli for można skorzystać ze specjalnego rodzaju zmiennej. To zmienna kontrolna, która istnieje tylko w czasie działania pętli.

for ( int i = 0 ; i < 10 ; i = i + 1 ) 
{
    Console.WriteLine ( "Cześć" );
}

Zmienna i jest deklarowana oraz inicjalizowana na początku pętli for i istnieje do końca bloku pętli.

5.11.4. Dane w klasach

Zmienne lokalne są w porządku, jednak znikają, gdy tylko program opuści blok, w którym zostały zadeklarowane. Często potrzebujemy zmiennych, które są dostępne poza metodami klasy.

Lokalne zmienne metody

Spójrzmy na poniższy kod:

    class LocalExample 
    {
        static void OtherMethod () 
        {
            local = 99; // kompilacja nie powiedzie się 
        }
        static void Main () 
        {
            int local = 0;
            Console.WriteLine ("local wynosi:" + local); 
        } 
}

Przykład 14. Zmienna lokalna powodująca błąd kompilacji

Zmienna local jest zadeklarowana i używana wewnątrz metody Main. Nie można z niej skorzystać nigdzie indziej. Jeśli jakaś instrukcja w OtherMethod spróbuje się odwołać do zmiennej local, to program nie zostanie skompilowany.

Zmienne stanowiące dane składowe klasy

Jeśli chcemy, by zmienna była współdzielona przez dwie metody należące do jednej klasy, to musimy tę zmienną ustanowić jako składową klasy. W tym celu należy ją zadeklarować poza metodami w klasie.

class MemberExample 
{
    // zmienna member jest częścią klasy 
    static int member = 0 ;
    static void OtherMethod () 
    {
        member = 99; 
    }
    static void Main () 
    {
        Console.WriteLine ("wartość zmiennej member to: " + member); 
        OtherMethod(); 
        Console.WriteLine ("nowa wartość zmiennej member to: " + member); 
    } 
}

Przykład 15. Zastosowanie zmiennej składowej

Zmienna member stanowi teraz część klasy MemberExample, dlatego jest już dostępna zarówno dla metody Main, jak i OtherMethod. Po wykonaniu powyższego programu na ekranie zobaczymy:

wartość zmiennej member to: 0
nowa wartość zmiennej member to: 99

W wyniku wywołania metody OtherMethod wartość zmiennej member zostanie zmieniona na 99, stąd też taki wynik.

Zmienne klasowe są bardzo przydatne, jeśli zależy nam, by kilka metod współdzieliło zbiór danych. Na przykład jeśli pisalibyśmy grę w szachy, to dobrze byłoby ustanowić składową klasy tę zmienną, która przechowuje szachownicę. Dzięki temu metody odczytujące ruchy gracza, wyświetlające planszę i obliczające ruch komputera mogłyby korzystać z tych samych informacji na temat szachownicy.

Statyczne składowe klasy C#

Zdefiniowałem składową statyczną klasy, która będzie należała do samej tej klasy, a nie jej konkretnych egzemplarzy. Na razie nie musicie się tym przejmować. O tym, co dokładnie oznacza słowo kluczowe static, powiem późnej. Pamiętajcie jedynie, by umieszczać je w kodzie, bowiem w przeciwnym razie kompilacja programu się nie powiedzie.

Często popełnianym błędem jest mylenie słów static i const. Słowo const oznacza, że „nie można zmienić wartości” danej zmiennej. Z kolei zmienne poprzedzone słowem static „stanowią część klasy i są zawsze dostępne”. Dla ułatwienia możecie traktować zmienne statyczne jako coś nieruchomego, co siłą rzeczy pozostaje w tym samym miejscu.

Złota myśl programisty: planuj użycie zmiennych

Należy zaplanować, jak zamierzamy skorzystać ze zmiennych w naszym programie. Powinniśmy zdecydować, które zmienne potrzebne są tylko w blokach lokalnych, a które powinny być składowymi klasy. Pamiętajcie, że jeśli zdefiniujecie zmienną jako składową klasy, to będzie ona dostępna w każdej metodzie tej klasy (co zwiększa prawdopodobieństwo, że z naszą zmienną może stać się coś niedobrego). Osobiście ograniczam zmienne klasy do niezbędnego minimum i korzystam ze zmiennych lokalnych, jeśli tylko muszę przechować wartość dostępną dla małej części kodu.

Autor: Rob Miles

Tłumaczenie: Joanna Liana

Dyskusja

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *