Tworzone przez nas do tej pory programy były bardzo proste — wczytywały jedynie dane, coś z nimi robiły, po czym wyświetlały wynik. Jako programiści będziemy musieli jednak pisać programy o znacznie bardziej skomplikowanym działaniu. Nasz program będzie musiał podejmować decyzje na podstawie otrzymanych danych. Czasami trzeba będzie wykonywać fragment kodu wielokrotnie, aż do spełnienia określonego warunku. Może też się zdarzyć, że będziemy musieli wczytać dużą ilość danych, a następnie przetworzyć je na kilka różnych sposobów.
W tej sekcji zastanowimy się, jak tworzyć bardziej zaawansowane programy, a także przeanalizujemy pisanie programów w ogóle.
4.1. Program niczym opowieść
Zdaniem niektórych pisanie programu przypomina nieco pisanie opowiadania. Osobiście nie jestem co do tego w pełni przekonany. Zdarzyło mi się czytać podręczniki dla użytkowników komputerów, które były fikcją, lecz programy to inna bajka. Choć sam w sobie nie jest opowieścią, to dobrze napisany program ma kilka cech wspólnych z dobrą literaturą:
- Łatwo się go czyta. Nie możemy dopuścić, by czytelnik musiał odświeżać sobie wiedzę, która zdaniem autora zawarta jest w programie. Wszystkie użyte w tekście nazwy powinny być znaczące i się od siebie różnić.
- Jest poprawny pod względem gramatyki i interpunkcji. Składniki programu powinny być rozmieszczone w czytelny, spójny sposób.
- Dobrze prezentuje się na stronie. Dobry program jest dobrze sformatowany. Poszczególne bloki kodu powinny rozpoczynać się od wcięcia, a układ instrukcji powinien być przejrzysty.
- Wiadomo kto go napisał i kiedy dokonano w nim ostatnich zmian. Jeśli napisaliście dobry program, to powinniście opatrzyć go swoim nazwiskiem. Gdy coś w nim zmienicie, powinniście zamieścić informację na temat wprowadzonych zmian i ich powodu.
W dobrze napisanym programie sporo miejsca zajmują komentarze programisty. Program bez komentarzy przypomina trochę samolot, który ma funkcję autopilota, lecz jest pozbawiony okien. Być może dolecimy w nim na miejsce, lecz siedząc wewnątrz trudno będzie ocenić, czy kierujemy się we właściwą stronę.
4.1.1. Komentarze blokowe
Gdy kompilator C# napotyka sekwencję /*
, oznaczającą początek komentarza, to myśli sobie tak:
„Acha! Oto wiadomość, której treść będą zgłębiać tęższe umysły od mojego. Od tej chwili będę więc wszystko ignorować, dopóki nie ujrzę zamykającej komentarz sekwencji */
..
Przykład:
/* Ten program oblicza ilość szyby i drewna
potrzebną do produkcji okien z podwójnym szkleniem */
Nie szczędźcie komentarzy. Dzięki nim wasz program będzie o wiele bardziej zrozumiały. Zdziwicie się jak szybko zapominamy w jaki sposób opracowaliśmy nasz program. W komentarzach możecie też informować innych o danej wersji programu, o tym kiedy i dlaczego była ostatnio zmodyfikowana, a także zamieścić nazwisko autora — nawet jeśli sami nim jesteście.
W edytorach obsługujących kolorowanie składni komentarze są zazwyczaj wyświetlane na zielono.
4.1.2. Komentarze jednowierszowe
Istnieje jeszcze inny rodzaj komentarzy, oznaczany sekwencją //
. Sygnalizuje ona początek komentarza, który rozciąga się do końca danego wiersza kodu. To wygodny sposób na dodanie zwięzłej uwagi na końcu instrukcji:
pozycja = pozycja + 1; // przejdź do następnego klienta
Opatrzyłem tę instrukcję komentarzem, by użytkownik wiedział, jakie dokładnie zadanie wykonuje kod.
Złota myśl programisty: co za dużo, to niezdrowo
Pisanie komentarzy jest bardzo rozsądne. Trzeba jednak znać umiar. Pamiętajcie, że osoba czytająca program zna zapewne język C# i nie potrzebuje zbyt szczegółowych wyjaśnień:
liczbaKóz = liczbaKóz + 1 ; // dodaj jeden do liczbaKóz
Taki komentarz byłby wręcz obraźliwy. Jeśli wybierzecie sensowne identyfikatory, to przekonacie się, że sam kod wyraża wiele na temat działania programu.
4.2. Kontrola przepływu programu
Pierwszy napisany przez nas program dla producenta okien jest bardzo prosty: wykonuje wszystkie instrukcje od pierwszej do ostatniej, po czym jego działanie dobiega końca. Często jednak zachowanie programu musi ulec zmianie w zależności od otrzymanych danych. Ogólnie rzecz ujmując, możemy wyróżnić trzy rodzaje przepływu sterowania w programie:
- prosty
- warunkowy
- warunkowy z powtórzeniem grupy wyrażeń.
Wszystkie napisane kiedykolwiek programy składają się głównie z tych trzech elementów! Możecie z pożytkiem wykorzystać tę wiedzę podczas projektowania ogólnego zarysu działania programu. Do tej pory omawialiśmy jedynie programy wykonywane prosto od początku do końca. Ścieżka, którą przebiega program nazywana bywa „wątkiem wykonawczym”. Wywołanie metody powoduje przekazanie do niej wątku wykonawczego na czas jej wykonywania.
4.2.1. Wykonanie warunkowe — instrukcja if
Nasz kalkulator drewna i szkła potrzebnego do produkcji okien to dobry program — klient byłby pewnie z niego całkiem zadowolony. Nie jest to jednak rozwiązanie idealne. Problem nie leży po stronie programu, lecz użytkownika.
Jeśli jako szerokość okna podalibyśmy -1, to wartość ta zostanie zaakceptowana i obliczony zostanie niedorzeczny wynik. W naszym programie nie ma żadnego mechanizmu, który wykrywałby nieprawidłowe wartości szerokości i wysokości. Użytkownik może mieć podstawy do złożenia reklamacji, jeśli program nie zareaguje na podaną mu głupią wartość. W Stanach Zjednoczonych toczy się nawet obecnie wiele spraw dotyczących konsekwencji niewykrycia przez program błędnych danych.
Jeśli zatem użytkownik poda programowi naprawdę idiotyczną odpowiedź, to powinniśmy zwrócić mu uwagę. W specyfikacji naszego programu — która trafia do klienta — zamieściliśmy coś takiego (to metadane).
Program odrzuci wymiary okna, które nie będą mieścić się w następujących przedziałach:
szerokość mniejsza niż 0,5 metra szerokość większa niż 5,0 metrów wysokość mniejsza niż 0,75 metra wysokość większa niż 3,0 metry.
Zrobiliśmy więc wszystko co w naszej mocy. Jeśli program otrzyma wartość szerokości wynoszącą 1 zamiast 10, to jest to zmartwienie użytkownika. Co istotne z naszego punktu widzenia, dzięki tak napisanej specyfikacji klient nie może nas pozwać!
Byśmy jednak byli zabezpieczeni, program musi wykrywać kłopotliwe wartości i je odrzucać. W tym celu możemy skorzystać z instrukcji:
If (warunek) instrukcja lub blok kodu, który ma być wykonany, jeśli warunek jest prawdziwy else instrukcja lub blok kodu, który ma być wykonany, jeśli warunek jest fałszywy
Od spełnienia bądź niespełnienia warunku zależy dalsze działanie programu. Czym zatem jest warunek? C# umożliwia bezpośrednie wyrażanie w programie wartości prawdy (true
) i fałszu (false
). Jak już widzieliśmy, do przechowywania wartości logicznych służą zmienne typu bool
.
Możemy zatem tworzyć warunki zwracające logiczny wynik. To tak zwane „warunki logiczne”. Logiczne, prawda? Najprostszy warunek to wartość true
lub false
, np.:
if (true)
Console.WriteLine ( "cześć, mamo" );
To poprawny kod, choć nieco bezsensowny, ponieważ warunek zawsze jest spełniony i napis "cześć, mamo"
zostanie zawsze wydrukowany (zwróćcie uwagę, że pominęliśmy część instrukcji z else — nic nie szkodzi, jest ona opcjonalna).
4.2.2. Warunki i operatory relacyjne
Aby warunki działały, potrzebujemy zestawu dodatkowo operatorów relacyjnych, których używa się w wyrażeniach logicznych. Operatory relacyjne, podobnie jak liczbowe, operują na argumentach wyrażenia. Wyrażenie zawierające tego typu operatory może jednak zwrócić tylko dwa rodzaje wyniku: true lub false. Oto lista dostępnych operatorów relacyjnych:
4.2.3. Operator relacyjny ==
To jest operator równości. Jeśli lewa i prawa strona są sobie równe, to wyrażenie ma wartość true
, a jeśli nie — false
.
4 == 5
Takie wyrażenie zostanie uznane za fałszywe. Warto zauważyć, że porównywanie zmiennych zmiennoprzecinkowych nie ma zbyt wielkiego sensu — nie da się w ten sposób ocenić, czy przechowują dokładnie te same wartości. Ponieważ liczby zmiennoprzecinkowe są przechowywane z ograniczoną dokładnością, może się zdarzyć, że wbrew oczekiwaniom warunek nie zostanie spełniony. Przykładowo w poniższym równaniu:
x = 3.0 * (1.0 / 3.0);
Wartość x
mogłaby być równa 0,99999999, co oznaczałoby, że warunek:
x == 1.0
jest fałszywy — mimo że z matematycznego punktu widzenia powinno być na odwrót.
By porównać wartości zmiennoprzecinkowe, należy odjąć jedną od drugiej i sprawdzić, czy różnica jest bardzo niewielka.
4.2.4. Operator relacyjny !=
To jest operator nierówności. Przeciwieństwo operatora równości. Jeśli argumenty nie są sobie równe, wyrażenie przyjmuje wartość true
, zaś w przeciwnym wypadku — false
. Również i ten test nie jest polecany dla liczb zmiennoprzecinkowych.
4.2.5. Operator relacyjny mniejszości — <
To jest operator mniejszości. Jeśli argument znajdujący się po lewej stronie jest mniejszy od argumentu po prawej, to wyrażenie przyjmuje wartość true
. Jeśli natomiast argument po lewej stronie jest większy od argumentu po prawej bądź jest mu równy, to wyrażenie przyjmie wartość false
. Operator ten jest całkiem przydatny przy porównywaniu wartości zmiennoprzecinkowych.
4.2.6. Operator relacyjny większości — >
To jest operator większości. Jeśli argument znajdujący się po lewej stronie jest większy od argumentu po prawej, to wynik porównania ma wartość true
. Jeśli zaś argument znajdujący się po lewej stronie jest mniejszy od argumentu po prawej bądź jest mu równy, to wynik porównania będzie miał wartość false
.
4.2.7. Operator <=
To jest operator mniejszości lub równości. Jeśli argument znajdujący się po lewej stronie jest mniejszy od argumentu po prawej bądź jest mu równy, to wyrażenie przyjmie wartość true
, zaś w przeciwnym wypadku — false
.
4.2.8. Operator >=
To jest operator większości lub równości. Jeśli argument znajdujący się po lewej stronie jest większy od argumentu po prawej bądź jest mu równy, to wyrażenie przyjmuje wartość true
, a w przeciwnym wypadku — false
.
4.2.9. Operator negacji logicznej — !
To jest operator negacji logicznej. Za pomocą tego operatora można uzyskać odwrotność wartości lub wyrażenia. Możemy np. napisać !true
, co oznacza false
, bądź zapisać poniższe wyrażenie: !(x==y)
— oznacza ono to samo co (x!=y)
. Operator negacji logicznej odwraca zatem znaczenie wyrażenia.
4.2.10. Łączenie operatorów logicznych
Czasami potrzebujemy połączyć wyrażenia logiczne, aby móc dokonać bardziej skomplikowanych wyborów. By np. ocenić czy podana szerokość okna jest prawidłowa, musimy sprawdzić, czy jest większa od wartości minimalnej, a jednocześnie mniejsza od dopuszczalnego maksimum. W C# istnieją dodatkowe operatory umożliwiające łączenie wartości logicznych:
4.2.11. Operator iloczynu logicznego &&
To jest operator iloczynu logicznego (AND). Jeśli argumenty znajdujące się po obu stronach operatora && są prawdziwe, to zostanie zwrócona wartość true
. Jeśli jeden z nich będzie fałszywy, to otrzymamy wynik false
, np.:
(width >= 0.5) && (width <= 5.0)
To wyrażenie będzie prawdziwe, jeśli podana wysokość znajduje się w opisanym przez nas przedziale. Aby przykład był czytelniejszy, umieściłem oba warunki w nawiasie. Kompilator wie jednak, że operator && musi znajdować się pomiędzy wynikami dwóch wyrażeń logicznych, dlatego nawias nie jest wymagany.
4.2.12. Operator sumy logicznej ||
To jest operator sumy logicznej (OR). Jeśli któryś z argumentów znajdujących się po obu stronach operatora ||
jest prawdziwy, to wyrażenie przyjmie wartość true
. Wartość false
możliwa jest tylko wówczas, gdy oba argumenty wyrażenia są fałszywe, np.:
Width < 0.5 || width > 5.0
Gdyby podano nieprawidłową szerokość, to wynik tego wyrażenia byłby prawdą. Szerokość jest nieprawidłowa, jeśli jest mniejsza od minimalnej bądź większa od maksymalnej dopuszczalnej wartości. Warto zauważyć, że by odwrócić ten warunek (tak by zwracał prawdę, jeśli wartość będzie nieprawidłowa) nie wystarczy w każdym wyrażeniu zamienić operatora >
na <=
, lecz trzeba także dokonać zmiany &&
na ||
.
Podstawą teoretyczną jest tutaj II prawo de Morgana.
Korzystając z wymienionych operatorów w połączeniu z konstrukcją if
, możemy podejmować decyzje i zmieniać przebieg działania programu w zależności od otrzymanych danych.
Złota myśl programisty: dziel duże warunki na mniejsze
Jeśli okaże się, że napisane przez was warunki są z konieczności bardzo obszerne i skomplikowane, możecie uprościć kod za pomocą kilku instrukcji if
i wcięć w odpowiednich miejscach. Dzięki temu łatwiej będzie również przeprowadzić debugowanie, ponieważ będziecie mogli zobaczyć jak program przechodzi po kolei przez każdy warunek z osobna, a nie podejmuje decyzji od razu na podstawie jednego wielkiego warunku, który trzeba by przeanalizować w całości.
4.2.13. Łączenie instrukcji w bloki
Postanowiliśmy, że jeśli użytkownik poda wartość znajdującą się poza dopuszczalnym zakresem, to zostanie zgłoszony błąd, a wprowadzona wartość zostanie odpowiednio zmniejszona bądź zwiększona, tak by mieściła się w granicach ustalonego przedziału. W tym celu musimy napisać dwie instrukcje, które zostaną wykonane dla danego warunku: jedną wyświetlającą komunikat, drugą dokonującą przypisania. Możemy posłużyć się do tego znakami {
i }
. Kilka ujętych w klamrę instrukcji traktuje się jak jedną, dlatego zapiszemy nasz kod następująco:
if( width > 5.0 )
{
Console.WriteLine ("Zbyt duża szerokość maksimum wynosi\n");
width = 5.0;
}
Nasze dwie instrukcje stanowią teraz jeden blok, wykonywany jeśli szerokość przekracza 5,0. W ten sposób można łączyć ze sobą setki instrukcji — kompilator nie ma nic przeciwko. Ponadto bloki kodu można umieszczać w innych blokach. Jest to tak zwane zagnieżdżanie.
Liczba zawartych w programie otwierających i zamykających klamer musi się zgadzać. W przeciwnym wypadku możemy spodziewać się dziwnych błędów — kompilator może np. dotrzeć do końca pliku w połowie bloku, albo zakończyć program w połowie pliku!
Złota myśl programisty: formatuj kod z myślą o czytelniku
Staram się ułatwić zrozumienie mojego kodu poprzez stosowanie wcięć na kilka spacji, np. zawsze gdy otwieram blok znakiem {, przesuwam nieco lewy margines. Dzięki temu wystarczy jedno spojrzenie, bym wiedział na jakim poziomie kodu się znajduję. Tak robią porządni programiści.
4.2.14. Metadane, magiczne liczby i stałe
Magiczna liczba to wartość mająca specjalne znaczenie. Na przestrzeni programu nigdy się nie zmienia, jest stała. Podczas pisania kalkulatora materiałów okiennych wykorzystam magiczne liczby do określenia maksymalnych i minimalnych wymiarów wysokości i szerokości.
Wymiary te pochodzą z metadanych, które skrupulatnie zbierałem w trakcie prac nad specyfikacją programu.
Teoretycznie mógłbym się po prostu posłużyć wartościami 0,5, 5,0, 0,75 i 3,0 — nie mają one jednak przypisanego znaczenia i sprawiają, że wprowadzanie zmian w programie jest utrudnione. Jeśli z jakiegoś powodu maksymalny rozmiar szyby zmieni się na 4,5 metra, to będę musiał przejrzeć cały program i zmienić odpowiednie wartości. Nie podoba mi się koncepcja „magicznych liczb” i wolałbym zamienić je na coś bardziej ekspresyjnego.
Rozwiązaniem jest utworzenie stałej, czyli zmiennej, która nigdy się nie zmienia.
constdouble PI = 3.141592654;
Korzystając z powyższej stałej, moglibyśmy napisać coś takiego:
circ = rad * 2 * PI ;
To o wiele bardziej ekspresyjny sposób (użyliśmy PI a nie jakiejś anonimowej wartości), który przyspiesza pisanie programu. W miejsce magicznej liczby zawsze powinniśmy używać tego rodzaju stałej, np.:
constdouble MAX_WIDTH = 5.0;
Dzięki temu programy łatwiej się czyta, a jednocześnie łatwiej jest w nich wprowadzić zmiany.
Istnieje konwencja, zgodnie z którą stałym zmiennym nadajemy nazwy pisane Z WIELKIEJ LITERY. Taki zapis pozwala nam odgadnąć, co zostało zdefiniowane.
Możemy zatem zmodyfikować nasz kalkulator w następujący sposób:
using System;
classGlazerCalc
{
static void Main()
{
double width, height, woodLength, glassArea;
const doubleMAX_WIDTH = 5.0;
const double MIN_WIDTH = 0.5;
const double MAX_HEIGHT = 3.0;
const double MIN_HEIGHT = 0.75;
stringwidthString, heightString;
Console.Write ( "Podaj szerokość okna: " );
widthString = Console.ReadLine();
width = double.Parse(widthString);
if (width< MIN_WIDTH) {
Console.WriteLine ( "Za mała szerokość.\n\n " );
Console.WriteLine ( "Przyjęto wartość minimalną." );
width = MIN_WIDTH ;
}
if (width > MAX_WIDTH) {
Console.WriteLine ( "Za duża szerokość.\n\n" );
Console.WriteLine ("Przyjęto wartość maksymalną.");
width = MAX_WIDTH ;
}
Console.Write ( "Podaj wysokość okna: " );
heightString = Console.ReadLine();
height = double.Parse(heightString);
if (height < MIN_HEIGHT) {
Console.WriteLine( "Za mała wysokość.\n\n" ); Console.WriteLine ( "Przyjęto wartość minimalną." ); height = MIN_HEIGHT;
}
if (height > MAX_HEIGHT) {
Console.WriteLine( "Za duża wysokość.\n\n" ); Console.WriteLine ( "Przyjęto wartość maksymalną" ); height = MAX_HEIGHT ;
}
woodLength = 2 * ( width + height ) * 3.25;
glassArea = 2 * ( width * height );
Console.WriteLine ( "Długość drewna: " + woodLength + " stopy" );
Console.WriteLine( "Powierzchnia szyby: " + glassArea + " m kw." );
}
}
Przykład kodu 04. Gotowy kalkulator materiałów do produkcji okien
Powyższy program spełnia nasze wymagania — nie będzie wykorzystywał wartości niezgodnych ze specyfikacją. Wciąż jednak brakuje mu trochę do ideału. Jeśli klient poda złą wysokość, to zostanie ona dopasowana do określonego zakresu, lecz program trzeba będzie uruchomić ponowne, by wpisać wartość raz jeszcze.
Najlepiej byłoby wielokrotnie pobierać szerokość i wysokość, do czasu aż podane wartości będą mieścić się w zdefiniowanym przedziale. W języku C# możemy do tego celu wykorzystać pętle.
4.3. Pętle w języku C#
Instrukcje warunkowe pozwalają wymusić określone działanie programu, jeśli podany warunek został spełniony. Czasami jednak chcemy w takim wypadku coś powtórzyć, raz lub kilka razy.
W C# istnieją na to trzy sposoby, zależnie od efektu jaki chcemy uzyskać. Choć aż trzy techniki nie są wymagane, to udostępniono je, ponieważ ułatwiają pisanie programów (są trochę jak dodatki do wspomnianej wcześniej piły łańcuchowej, które umożliwiają drwalowi wykonanie konkretnego zadania w prostszy sposób). Programowanie polega w dużej mierze na umiejętności doboru odpowiedniego narzędzia lub dodatku pod kątem rozwiązywanego problemu. (pozostały procent naszych talentów będziemy wykorzystywać po to, by ustalić dlaczego dane narzędzie nie zadziałało!).
W przypadku naszego programu chcemy wczytywać liczby tyle razy, aż otrzymamy zestaw poprawnych wartości — innymi słowy podanie odpowiedniej liczby powinno zakończyć pętlę. Jeśli zatem otrzymamy prawidłową liczbę za pierwszym podejściem, to pętla zostanie wykonana tylko raz. Może wam się wydawać, że próbuję was oszukać. W końcu zmieniłem jedynie polecenie
Pobieraj wartości do czasu otrzymania prawidłowej
na
Pobieraj wartości tak długo, jak będą nieprawidłowe.
Sztuka programowania to też umiejętność zmiany podejścia do problemu, tak by rozwiązanie dało się zastosować w danym języku.
4.3.1. Pętla do…while
W przypadku naszego małego programu C# skorzystamy z pętli do…while
, która wygląda następująco:
do instrukcja lub blok instrukcji while (warunek);
W ten sposób możemy powtórzyć wykonywanie fragmentu kodu do czasu, aż podany na końcu warunek zwróci wartość false
. Zauważcie, że warunek sprawdzany jest dopiero po instrukcji — a zatem nawet jeśli od razu wiadomo, że jest fałszywy, to i tak instrukcja zostanie wykonana raz.
W tym kontekście warunek oznacza to samo co w przypadku instrukcji warunkowej if
, dzięki czemu programy mogą działać na jeszcze bardziej intrygujące sposoby:
using System;
class Forever
{
public static void Main ()
{
do
Console.WriteLine ( "Cześć, mamo" );
while ( true );
}
}
Przykład kodu 05. Pętla nieskończona
To jak najbardziej poprawny program C#. Jak długo będzie trwało wykonywanie tej pętli? To bardzo ciekawe pytanie, a odpowiedź na nie odwołuje się do ludzkiej psychiki, kwestii energetycznych i kosmologii. Pętla będzie wykonywana dotąd aż:
- Znudzimy się nią.
- Zabraknie nam prądu
- Nastąpi implozja wszechświata.
Znów możemy tu znaleźć analogię pomiędzy programowaniem a piłą łańcuchową (taką zwykłą, bez bajerów). Tak jak starą piłą łańcuchową, przy odpowiedniej determinacji, można sobie uciąć nogę, tak też w dowolnym języku można napisać program, którego działanie nigdy się nie kończy. Przypomina mi to moją ulubioną instrukcję użycia szamponu:
- Nanieść szampon na wilgotne włosy
- Masować energicznie aż do wytworzenia piany.
- Spłukać letnią wodą.
- Czynność powtórzyć.
Ciekawe ilu użytkowników tego szamponu nieprzerwanie myje sobie włosy?
4.3.2. Pętla while
Czasami o powtórzeniu pętli chcemy zdecydować przed jej wykonaniem. Przeanalizujmy raz jeszcze działanie pętli do...while
: warunek sprawdzany jest po jednokrotnym wykonaniu kodu, który może zostać ewentualnie powtórzony. Dokładnie czegoś takiego potrzebujemy w naszym programie: chcemy otrzymać wartość, a dopiero później zadecydować czy jest ona prawidłowa. By jednak zapewnić jak najbardziej elastyczne rozwiązania, w C# dostępny jest jeszcze jeden rodzaj pętli, w której warunek jest sprawdzany w pierwszej kolejności:
while (warunek) instrukcja lub blok kodu
Zauważcie, że w powyższej instrukcji C# pominięto słowo do, upraszczając ją tym samym do niezbędnego minimum (jeśli jednak umieścimy do w naszym kodzie, to — jak się zapewne domyślacie — kompilator z radością poinformuje nas o wystąpieniu błędu).
4.3.3. Pętla for
W naszych programach często będziemy chcieli powtórzyć fragment kodu określoną liczbę razy. Możemy to zrobić w dosyć łatwy sposób, korzystając ze wspomnianej już pętli:
using System;
class WhileLoopInsteadOfFor
{
public static void Main ()
{
int i ;
i = 1 ;
while ( i < 11 )
{
Console.WriteLine ( "Cześć, mamo" ) ;
i = i + 1 ;
}
}
}
Przykład kodu 06. Pętla while zamiast pętli for
Zmienna, która steruje powtarzaniem pętli to zmienna kontrolna. Zwykle oznacza się ją literą i
.
Ten bezużyteczny program wyświetla napis Cześć, mamo
10 razy. By było to możliwe, w kodzie programu znajduje się zmienna sterująca wykonaniem pętli. Przyjmuje ona wartość początkową (1) i jest sprawdzana przy każdym wykonaniu pętli. Wraz z każdym nowym wykonaniem instrukcji zmienna jest zwiększana o 1. Gdy zmienna kontrolna osiągnie wartość 11, wykonywanie pętli dobiega końca. Kończy się również działanie naszego programu.
W C# dostępna jest osobna pętla tego typu:
for ( wartość początkowa zmiennej kontrolnej; warunek kończący; zmiana wartości ) { kod, który chcemy wykonać kilkukrotnie }
Korzystając z niej, możemy zatem przepisać powyższy program w następujący sposób:
using System;
class ForLoop
{
public static void Main ()
{
int i ;
for ( i = 1; i < 11; i = i + 1 )
{
Console.WriteLine ( "Cześć, mamo" ) ;
}
}
}
Przykład kodu 07. Pętla for
W nawiasie widzimy trzy elementy pętli for
oddzielone średnikami. Dokładny przebieg jej wykonania wygląda następująco:
- Zmiennej kontrolnej przypisujemy wartość początkową.
- Sprawdzamy, czy pętla powinna się już zakończyć — jeśli tak, przechodzimy do kolejnej instrukcji następującej po pętli
for
. - Wykonujemy instrukcje, które mają zostać powtórzone.
- Aktualizujemy wartość zmiennej kontrolnej.
- Powtarzamy wszystko od kroku 2.
Pisanie tego rodzaju pętli w opisany sposób jest szybsze i łatwiejsze niż w postaci pętli while
— wszystkie elementy pętli nie są rozproszone, lecz znajdują się w jednym miejscu. Dzięki temu istnieje mniejsze prawdopodobieństwo, że któryś z nich pominiemy, np. zapomnimy ustawić początkową wartość zmiennej kontrolnej bądź ją zaktualizować.
Jeśli przyjdzie wam do głowy, by kombinować coś z wartością zmiennej kontrolnej, to program zacznie się głupio zachowywać, np. jeśli wewnątrz pętli ustawicie wartość i na 0, to pętla nigdy się nie skończy. Będziecie mieć nauczkę.
Złota myśl programisty: Nie zgrywaj mądrali/głupka
Niektórzy lubią się popisywać i próbują wyczyniać różne cuda z instrukcjami pętli for
odpowiedzialnymi za przypisanie wartości, jej inkrementację i sprawdzenie warunku. Wydaje im się, że wykażą się sprytem, jeśli zdołają upchnąć całe działanie pętli w nawiasie po słowie for
.
Takich ludzi nazywam głupcami. Bardzo rzadko wymagany jest kod o takim stopniu skomplikowania. Pisząc programy, powinniście martwić się o dwie rzeczy: „Jak udowodnię, że to działa?” i „Jak łatwo jest zrozumieć ten kod?”. Komplikowanie kodu nie pomoże wam w rozwiązaniu tych problemów.
4.4. Przerywanie pętli
Czasami będziemy chcieli opuścić pętlę w trakcie jej wykonywania — program będzie mógł w pewnym momencie zdecydować, że kontynuowanie pętli nie ma już sensu. Będzie chciał z niej wyjść i przejść do kolejnej instrukcji.
Czegoś takiego możemy dokonać przy pomocy instrukcji break
, która powoduje natychmiastowe przerwanie pętli. Zwykle program sam podejmie decyzje o opuszczeniu pętli w taki sposób. W programie możemy umieścić opcję awaryjną pt. „wynosimy się stąd”, co moim zdaniem jest bardzo przydatne. W poniższym kodzie mamy na przykład zmienną aborted
, która jest prawdziwa tylko jeśli zachodzi konieczność opuszczenia pętli i zmienną runningOK
— zwykle ma wartość true
, a staje się fałszywa wówczas, gdy czas zakończyć pętlę w normalnych okolicznościach.
while (runningOK)
{
skomplikowanykod
…
if (aborted)
{
break;
}
…
więcej skomplikowanego kodu
…
}
…
kod do którego przejdziemy, jeśli aborted przyjmie wartość true
…
Zwróćcie uwagę, że wspomniane dwie zmienne pełnią funkcję przełączników i nie przechowują wartości jako takich. Tak naprawdę określają stany wykonywanego programu. To standardowa i, jak się przekonacie, bardzo użyteczna sztuczka programistyczna.
Za pomocą instrukcji break
można wydostać się ze wszystkich trzech wymienionych rodzajów pętli. Po opuszczeniu pętli, program zawsze kontynuuje swoje działanie od instrukcji, która jest następna w kolejności.
Złota myśl programisty: ostrożnie z instrukcją break
Słowo kluczowe break
przypomina owianą złą sławą instrukcję goto
, z której programiści często boją się korzystać. Pozwala ona „przeskoczyć” z wykonywaniem programu z jednego miejsca w kodzie w inne. Instrukcja ta jest więc potencjalnie niebezpieczna i myląca, dlatego odradza się jej stosowanie. Z kolei instrukcja break
pozwala przeskoczyć z dowolnego miejsca w pętli do kodu następującego bezpośrednio po niej. Program może zatem przejść do wykonania instrukcji znajdującej zaraz po pętli z różnych miejsc — w tym z każdej umieszczonej w pętli instrukcji break
. Utrudnia to zrozumienie kodu i choć instrukcja break
nie wprowadza tyle chaosu co goto
, to i tak może powodować problemy. Z tego względu radzę wam zachować ostrożność, gdy będziecie z niej korzystać.
4.4.1. Powrót na początek pętli
Czasem będziemy chcieli wrócić na początek pętli i wykonać ją ponownie. Może się tak zdarzyć, jeśli wykonamy wystarczającą liczbę znajdujących się w niej instrukcji. W C# dostępne jest słowo kluczowe continue
, które przekazuje naszemu programowi coś takiego:
Proszę, nie wykonuj dalszych instrukcji w tym powtórzeniu. Wróć na początek pętli, zaktualizuj i sprawdź co trzeba, a jeśli zajdzie taka konieczność, to wykonaj pętlę ponownie.
W poniższym programie zmienna logiczna Done_All_We_Need_This_Time
przyjmie wartość true
, gdy wykonamy wszystkie niezbędne instrukcje z pętli.
for ( item = 1; item < Total_Items; item = item + 1 )
{
…
kod wykonujący operacje na elementach
…
if (Done_All_We_Need_This_Time) continue ;
…
dodatkowy kod operujący na elementach
…
}
Instrukcja continue
spowoduje ponowne wykonanie pętli ze zaktualizowaną zmienną item
, o ile warunek pętli zostanie spełniony. Można to potraktować jako punkt 2 opisanego wyżej działania pętli.
4.4.2. Bardziej skomplikowane decyzje
Teraz możemy zastanowić się jak za pomocą pętli sprawdzić czy podana szerokość bądź wysokość jest prawidłowa. Zasadniczo chcemy prosić użytkownika o podanie wartości tak długo, aż otrzymamy odpowiednią liczbę — jeśli będzie większa od określonego maksimum bądź mniejsza od minimum, to poprosimy o nią ponownie. W tym celu musimy jednocześnie sprawdzić dwie rzeczy. Pętla powinna być kontynuowana jeśli:
width > MAX_WIDTH oraz width < MIN_WIDTH
By sprawdzić, czy liczba spełnia powyższe kryteria, posłużymy się jednym z opisanych wcześniej operatorów logicznych. Napisany przez nas warunek zwróci wartość true
, jeśli podana szerokość będzie nieprawidłowa.
if ( width < MIN_WIDTH || width > MAX_WIDTH ) …
Złota myśl programisty: odwracanie warunków to chleb powszedni
Będziecie musieli się przyzwyczaić, że często trzeba postrzegać problem w odwrotny sposób niż mogłoby się wydawać. Zamiast prosić program o wczytanie prawidłowej wartości, będziemy chcieli pobierać liczby tak długo, jak nie będą spełniać określonych kryteriów. Niejednokrotnie będziemy zatem sprawdzać czy wprowadzone wartości są błędne, a nie czy są prawidłowe. Pamiętajcie o stosowaniu takich odwrotności podczas pisania kodu.
4.4.3. Gotowy kalkulator materiałów do produkcji okien
Oto kompletne rozwiązanie omawianego problemu, w którym wykorzystano wszystkie wymienione sztuczki.
using System;
classGlazerCalc
{
static void Main()
{
double width, height, woodLength, glassArea;
const double MAX_WIDTH = 5.0;
const double MIN_WIDTH = 0.5;
const double MAX_HEIGHT = 3.0;
const double MIN_HEIGHT = 0.75;
string widthString, heightString;
do {
Console.Write ( "Podaj szerokość okna w przedziale od " + MIN_WIDTH + " do " + MAX_WIDTH + " :" );
widthString = Console.ReadLine();
width = double.Parse(widthString);
} while ( width < MIN_WIDTH || width > MAX_WIDTH );
do {
Console.Write ( "Podaj wysokość okna w przedziale od " + MIN_HEIGHT + " do " + MAX_HEIGHT + " :" );
heightString = Console.ReadLine();
height = double.Parse(heightString);
} while ( height < MIN_HEIGHT || height > MAX_HEIGHT );
woodLength = 2 * ( width + height ) * 3.25;
glassArea = 2 * ( width * height ) ;
Console.WriteLine ( "Długośćdrewna: " + woodLength + " stopy" );
Console.WriteLine( "Powierzchniaszyby: " + glassArea + " m kw." );
}
}
Przykład kodu 08. Kalkulator wykorzystujący pętle
4.5. Operatory umożliwiające skrótowy zapis wyrażeń
Do tej pory mówiliśmy o operatorach występujących w wyrażeniach i operujących na dwóch argumentach, np.:
window_count = window_count + 1
W tym przypadku operator +
operuje na zmiennej window_count
i wartości 1. Powyższa instrukcja ma na celu dodać 1 do zmiennej window_count
. To jednak dosyć długi sposób na wyrażenie tej operacji — zarówno pod względem kodu jaki musimy wpisać, jak i zadania jakie musi wykonać komputer. Jeśli chcemy, to w C# możemy zapisać tę samą instrukcję w bardziej zwięzły sposób. Poniższy wiersz kodu:
window_count++
spełni to samo zadanie. Wyraziliśmy się treściwiej, a ponadto dzięki takiemu zapisowi kompilator może wygenerować wydajniejszy kod, ponieważ od razu wie, że dodajemy 1 do określonej zmiennej. Użyty przez nas operator ++
to tak zwany operator jednoargumentowy — operuje tylko na jednym argumencie wyrażenia. Jego działanie polega na zwiększeniu wartości argumentu o jeden. Istnieje także analogiczny operator --
służący do zmniejszania (dekrementacji) wartości.
Inny skrót możemy wykorzystać, by dodać określoną wartość do zmiennej. Teoretycznie możemy napisać:
house_cost = house_cost + window_cost;
Powyższy kod jest jak najbardziej prawidłowy, lecz — podobnie jak w poprzednim przypadku — dosyć skomplikowany. W C# dostępne są dodatkowe operatory, dzięki którym możemy skrócić ten zapis do postaci:
house_cost += window_cost;
Operator +=
łączy operacje dodawania i przypisania, dzięki czemu wartość zmiennej house_cost
zostanie zwiększona o wartość window_cost
. Oto kilka innych operatorów umożliwiających skrócony zapis wyrażeń:
a += b
— wartość a
zostaje zamieniona na a + b
.
a -= b
— wartość a
zostaje zamieniona na a – b
.
a /= b
— wartość a
zostaje zamieniona na a / b
.
a *= b
— wartość a
zostaje zamieniona na a * b
.
Istnieje jeszcze więcej operatorów tego typu — musicie je jednak odkryć sami!
4.5.1. Instrukcje i wartości
W C# wszystkie instrukcje zwracają wartość, którą można wykorzystać w innej instrukcji, co stanowi jedną z najfajniejszych cech tego języka. Do zwracanej wartości zwykle nie przywiązujemy uwagi i nie ma w tym nic złego, jednak czasem okazuje się ona bardzo przydatna — zwłaszcza w podejmowaniu decyzji (patrz dalej). By zobaczyć jak można ją wykorzystać, spójrzcie na poniższy przykład:
i = (j=0);
To zupełnie poprawny (i całkiem sensowny) kod C#. Powoduje on ustawienie wartości zmiennych i
oraz j
na 0. Instrukcja przypisania zawsze zwraca przypisywaną wartość (czyli to, co znajduje się po prawej stronie znaku równości). Wartości tej można użyć ponownie jako wartości bądź argumentu wyrażenia. Instrukcję wykorzystywaną jako wartość najlepiej umieścić w nawiasie — nasze intencje będą wówczas czytelniejsze zarówno dla nas, jak i dla kompilatora!
Pominięcie nawiasu np. w przypadku operatora ++
może prowadzić do niejasności — nie wiadomo, czy otrzymujemy wartość przed inkrementacją czy po niej. W C# możemy otrzymać obie wartości, zależnie od efektu jaki chcemy uzyskać. To, która wartość ma zostać zwrócona zaznaczamy poprzez pozycję operatora ++
.
i++
oznacza podaj wartość przed inkrementacją.
++i
oznacza podaj wartość po inkrementacji.
Przykład:
int i = 2, j;
j = ++i;
Tutaj wartość j wyniesie 3. Wszystkie specjalne operatory, takie jak +=
, zwracają wartość po wykonaniu operacji.
Złota myśl programisty: pisz prosty kod
Choć oczywiście bez przesady. Można wprawdzie napisać kod w stylu:
height = width = speed = count = size = 0;
To jednak nie znaczy, że jest to dobre rozwiązanie. Pisząc program, w pierwszej kolejności zastanawiam się, czy jest on łatwo zrozumiały. Moim zdaniem przytoczona powyżej instrukcja taka nie jest, dlatego nie zdecydowałbym się na nią, mimo iż mogłaby być wydajniejsza od innego zapisu.
4.6. Lepsze wyświetlanie liczb
Zauważcie, że sposób wyświetlania liczby nie wpływa na sposób jej przechowywania w programie. Określamy jedynie, jak ma zostać wyświetlona przez metodę drukującą.
Jeśli uruchomiliście któryś z przedstawionych programów, to wiecie, że sposób wyświetlania liczb pozostawia wiele do życzenia. O ile z liczbami całkowitymi nie ma problemu, tak liczby zmiennoprzecinkowe żyją własnym życiem. By rozwiązać ten problem, w C# wprowadzono nieco inny sposób wyświetlania liczb. Daje nam on większą elastyczność, a w przypadku dużych wartości jest nieco łatwiejszy w użytku od standardowego sposobu.
4.6.1. Wykorzystywanie symboli zastępczych w wyświetlanych łańcuchach
Symbol zastępczy oznacza po prostu miejsce, w którym ma zostać wyświetlona wartość. Spójrzcie na poniższy przykład:
int i = 150;
double f = 1234.56789;
Console.WriteLine ( "i: {0} f: {1}", i, f );
Console.WriteLine ( "i: {1} f: {0}", f, i );
Za pomocą tego kodu wyświetlimy coś takiego:
i: 150 f: 1234.56789 i: 150 f: 1234.56789
Zawarty w łańcuchu symbol {n}
oznacza „parametr numer n, licząc od 0”. W drugiej instrukcji zapisu zamieniłem kolejność liczb, jednak wynik jest ten sam, ponieważ odwróciłem także kolejność parametrów.
Jeśli chciałbym zrobić coś szalonego i np. spróbował pobrać 99. parametr za pomocą symbolu {99}
, to wykonanie metody WriteLine
zakończy się błędem. Kompilator go jednak nie zauważy — błąd wystąpi dopiero w trakcie wykonywania programu.
4.6.2. Określanie precyzji liczb zmiennoprzecinkowych
Symbole zastępcze można uzupełnić o informacje dotyczące formatowania liczb:
int i = 150;
double f = 1234.56789;
Console.WriteLine ( "i: {0:0} f: {1:0,00}", i, f );
Po wykonaniu tego kodu na ekranie pojawi się:
i: 150 f: 1234,57
Zera oznaczają jedną lub więcej cyfr. Umieszczone po znaku dziesiętnym mogą określać liczbę miejsc po przecinku, jakie ma mieć wyrażana wartość. Jeśli więc będzie to liczba całkowita, to zostanie wyświetlona np. jako 12,00.
4.6.3. Określanie liczby wyświetlanych cyfr
Możemy ustalić konkretną liczbę cyfr za pomocą odpowiedniej liczby zer:
int i = 150;
double f = 1234.56789;
Console.WriteLine ( "i: {0:0000} f: {1:00000,00}", i, f );
Na ekranie zobaczymy:
i: 0150 f: 01234,57
Zauważcie, że wykonanie takiego kodu spowoduje wyświetlenie zer wiodących — to przydatne, jeśli chcemy wyświetlić np. wartość czeku.
4.6.4. Naprawdę bajeranckie formatowanie
Jeśli marzy wam się bardzo wyszukane formatowanie, to możecie skorzystać ze znaku #
. Umieszczony w łańcuchu formatującym znak # oznacza miejsce, w którym może zostać umieszczona cyfra:
int i = 150;
double f = 1234.56789;
Console.WriteLine ( "i: {0:#,##0} f: {1:##.##0,00}", i, f );
Za pomocą kratki sprawiłem, że liczba tysięczna zostanie oddzielona kropką:
i: 150 f: 1.234,57
Zwróćcie uwagę, że formater korzysta tylko z tych kratek i kropek, które są potrzebne. Wartość 150 nie ma liczby tysięcznej, dlatego została wyświetlona bez kropki. Zauważcie również, że uwzględniłem 0 jako najmniejszą cyfrę. Dzięki temu ewentualna wartość 0 zostanie wyświetlona na ekranie, a nie zupełnie pominięta.
4.6.5. Wyświetlanie liczb w kolumnach
Na koniec uzupełnimy informacje o formacie wyświetlanych liczb o szerokość. To bardzo przydatne, jeśli chcemy wyświetlać tekst w kolumnach:
int i = 150 ;
double f = 1234.56789 ;
Console.WriteLine ( "i: {0,10:0} f: {1,15:0,00}", i, f ) ;
Console.WriteLine ( "i: {0,10:0} f: {1,15:0,00}", 0, 0 ) ;
Po wykonaniu tego kodu zobaczymy:
i: 150 f: 1234,57 i: 0 f: 0,00
Wartość całkowitoliczbowa wyświetlana jest w kolumnie o szerokości 10 znaków, a zmiennoprzecinkowa o podwójnej precyzji w kolumnie szerokiej na 15 znaków. Na tę chwilę liczby wyrównywane są do prawej strony. By ustawić wyrównanie do lewej, zmienię szerokość na wartość ujemną:
int i = 150;
double f = 1234.56789;
Console.WriteLine ( "i: {0,-10:0} f: {1,-15:0,00}", i, f );
Console.WriteLine ( "i: {0,-10:0} f: {1,-15:0,00}", 0, 0 );
Na ekranie zobaczymy:
i: 150 f: 1234,57 i: 0 f: 0,00
Takie wyrównywanie działa również w przypadku łańcuchów, zatem możemy wykorzystać tę technikę także do wyświetlania kolumn słów.
Wartość szerokości można określić dla dowolnego elementu, nawet fragmentu tekstu, dzięki czemu formatowanie w kolumnach jest bardzo łatwe.