Spis treści Poprzednia strona: Operatory i wyrażenia Następna strona: Pętle
Komputery są głupie. Może inaczej: komputery są co najwyżej tak mądre, jak programiści, którzy je oprogramowali. Wiedza to nie to samo, co mądrość (patrz co mówił Dex w 31-szej minucie Gwiezdnych Wojen – Wojna Klonów epizod 2). Komputery są dobre w przechowywaniu i przetwarzaniu wiedzy, ale mądrość to zdolność do wykorzystywania wiedzy, czyli do podejmowania decyzji opartych na wiedzy. Komputery same nie podejmą decyzji, dlatego programista musi to za nie zrobić. Więcej: musi przewidzieć wszystkie możliwe sytuacje i zaprogramować automatyczne podejmowanie odpowiednich decyzji w każdej możliwej sytuacji. To, jaka jest sytuacja badamy przy pomocy warunków logicznych (operatory relacji połączone lub nie operatorami logicznymi, albo inne wyrażenia, których wynik został zrzutowany na typ bool). Dany warunek może być albo prawdziwy, albo fałszywy. W obu przypadkach należy podjąć decyzję o tym jaka powinna być akcja (reakcja) na sytuację opisaną przez dany warunek (nawet jeśli ta akcja polega na nie robieniu nic).
Instrukcja warunkowa używa słów kluczowych if oraz else w taki ogólny sposób:
if (warunek) instrukcja_wykonywana_jeśli_warunek_jest_prawdziwy; else instrukcja_wykonywana_jeśli_warunek_jest_fałszywy;
Druga część nie musi wcale wystąpić, tak jak poniżej:
if (warunek) instrukcja_wykonywana_jeśli_warunek_jest_prawdziwy;
Całość można powtarzać tyle razy, ile potrzeba:
if (warunek_1) instrukcja_wykonywana_jeśli_warunek_1_jest_prawdziwy; else if (warunek_2) instrukcja_wykonywana_jeśli_warunek_2_jest_prawdziwy; else if (warunek_3) instrukcja_wykonywana_jeśli_warunek_3_jest_prawdziwy; else instrukcja_wykonywana_jeśli_powyższe_warunki_są_fałszywe;
Spójrzmy na poniższy przykład programu, który służy do rozwiązywania równania kwadratowego:
using System; class RównanieKwadratowe { static void Main(string[] args) { Console.WriteLine("Rozwiązywanie równania kwadratowego"); Console.WriteLine("postaci a*x*x + b*x + c = 0."); Console.WriteLine("Podaj współczynniki a,b,c równania:"); Console.Write("a = "); string input = Console.ReadLine(); double a = double.Parse(input); Console.Write("b = "); input = Console.ReadLine(); double b = double.Parse(input); Console.Write("c = "); input = Console.ReadLine(); double c = double.Parse(input); double delta = b * b - 4 * a * c; if (delta < 0) Console.WriteLine("Równanie przy podanych"+ " współczynnikach nie ma rozwiązań."); else if (delta == 0) { double x = -b / (2 * a); Console.WriteLine("Rozwiązanie jest jedno:"); Console.WriteLine("x = {0}", x); } else // if (delta > 0) { double x1 = (-b - Math.Sqrt(delta)) / (2 * a); double x2 = (-b + Math.Sqrt(delta)) / (2 * a); Console.WriteLine("Rozwiązania są dwa:"); Console.WriteLine("x1 = {0}\nx2 = {1}", x1, x2); } Console.ReadKey(true); } }
Wykorzystaliśmy tutaj instrukcję warunkową if (dwukrotnie) oraz poznane w poprzednim rozdziale operatory arytmetyczne i nawiasy. Ponadto skorzystaliśmy ze statycznej metody Sqrt klasy Math, która służy do obliczania pierwiastka. Zwróć uwagę na komentarz w linii 32. Moglibyśmy tam użyć trzeciej instrukcji if, ale nie jest to potrzebne, bo poprzednie dwie wyczerpują już wszystkie pozostałe możliwości. Skoro wartość zmiennej delta nie jest ani mniejsza od zera, ani równa zeru to już bez sprawdzania wiemy, że musi być większa od zera. Ale czy na pewno? Zaraz zobaczymy, że naiwne przyjmowanie takich założeń może być dla programisty zgubne. Najpierw zobaczmy rezultat działania programu:
Mamy dokładnie to, czego się spodziewaliśmy. Co prawda komputer nie wie, że 0,333333333 to 1/3, czyli jedna trzecia, ale i tak jesteśmy szczęśliwi, że dobrze liczy. To jest pierwszy program w tym kursie, który robi coś pożytecznego. Być może chcielibyśmy mieć wynik w postaci 1/3, ale na razie zadowolimy się takim rozwiązaniem. Spróbujmy tak dobrać liczby, żeby mieć jedno rozwiązanie: .
Ooops. Nasze szczęście nie trwało długo. Nie dość, że program twierdzi, że mamy dwa rozwiązania zamiast jednego, to jeszcze uparcie próbuje je obliczyć i oblicza źle. Ten wynik nie jest całkiem zły. Jest on dobry z dokładnością 0,00000002. Dlaczego taka mała dokładność? przecież podobno liczby typu double przechowują od 15 do 17 cyfr znaczących. Tak, ale kiedy je dwukrotnie pomnożymy przez siebie (b*b i a*c w linii 22) dokładność też spada dwukrotnie i zamiast średnio 16 cyfr mamy 8 cyfr. Wszystko się zgadza. Czy można to jakoś obejść? I tak, i nie. Na błędy w obliczaniu pierwiastka nie mamy wpływu (chyba że potrafimy napisać lepszą funkcję niż Math.Sqrt()). Błąd jest w podejmowaniu decyzji przez program. Drugi warunek if jest fałszywy podczas, kiedy powinien być prawdziwy. Obliczona delta jest bardzo mała, ale większa od zera i dlatego program przeskakuje ten warunek przechodząc do ostatniego bloku else. W ogóle porównywanie liczb zmiennoprzecinkowych z zerem za pomocą operatora == jest złym pomysłem. Jeśli dobrze pogrzebiemy w dokumentacji języka C#, to znajdziemy pewną stałą double.Epsilon i jest ona równa 4,94065645841247E-324. Opis mówi, że to najmniejsza liczba dodatnia, jaką można reprezentować przy pomocy typu double. Na nic się nam to nie przyda, bo to jest tzw. najmniejsza dodatnia liczba subnormal (patrz standard liczb zmiennoprzecinkowych [floating-point] IEEE 754). Jak mała jest faktycznie ta nasza delta? Wynosi w powyższym przykładzie tyle 1,38777878078145E-17. Dla bezpieczeństwa (zasada dmuchania na zimne) weźmy więcej, na przykład 2E-17.
Możemy poprawić nasz program tak:
if (Math.Abs(delta) < 2E-17) { double x = -b / (2 * a); Console.WriteLine("Rozwiązanie jest jedno:"); Console.WriteLine("x = {0}", x); } else if (delta < 0) Console.WriteLine("Równanie przy podanych" + " współczynnikach nie ma rozwiązań."); else // if (delta > 0) { double x1 = (-b - Math.Sqrt(delta)) / (2 * a); double x2 = (-b + Math.Sqrt(delta)) / (2 * a); Console.WriteLine("Rozwiązania są dwa:"); Console.WriteLine("x1 = {0}\nx2 = {1}", x1, x2); }
Zmieniliśmy kolejność dwóch pierwszych instrukcji warunkowych i użyliśmy funkcji Math.Abs() obliczającej wartość bezwzględną [absolute value]. Czy na pewno wartość 2E-17 jest odpowiednia? Nie jesteśmy tego pewni 🙁 Dla innych danych wejściowych (tych w zmiennych a, b i c) może być przecież inaczej, a skoro może, to na pewno będzie (tzw. prawo Murphy’ego). Więcej: nie możemy być tego pewni, bo nie możemy sprawdzić wszystkich kombinacji możliwych danych wejściowych (jest ich za dużo). Potrzebujemy tutaj czegoś, co nazywamy epsilonem maszynowym. Jest to najmniejsza liczba, która daje różnicę rozpoznawalną dla operatorów relacji. Obliczamy ją tak:
double MachEps = 1; while (MachEps + 1 != 1) MachEps /= 2; MachEps *= 2;
Nie znamy jeszcze pętli while, ale nie o to tutaj chodzi. Wartość MachEps to 2.22044604925031E-16 i tej stałej powinniśmy używać zawsze w podobnych sytuacjach. Błędy obliczeń są zwykle mniejsze niż MachEps, a jeśli nie, to są mniejsze niż jakaś mała całkowita wielokrotność MachEps. Takie są po prostu prawa tzw. obliczeń numerycznych na liczbach zmiennoprzecinkowych. Oczywiście należy powiedzieć, że ta wartość związana jest tylko z typem double w .NET Framework w systemie Windows. Nie należy zakładać, że gdzie indziej będzie taka sama. Zawsze warto to obliczyć i sprawdzić. A zatem poprawiamy nasz program tak:
double MachEps = 2.22044604925031E-16; if (Math.Abs(delta) < MachEps)
Dopiero teraz działa on jak należy:
Podejrzewam, że Microsoft nie udostępnia wartości MachEps, bo (w przeciwieństwie do mało przydatnego w praktyce double.Epsilon) zawsze zależy ona od zastosowanej metody obliczeniowej i nie ma jednej liczby, która mogłaby zadowolić wszystkich programistów we wszystkich możliwych przypadkach.
Operator warunkowy
Instrukcja if jest… instrukcją, a co zrobić, jeśli potrzebujemy warunkowego przetwarzania w wyrażeniu? Oczywiście można zapisać kawałki wyrażenia w zmiennej, ale po co angażować nową zmienną? Przecież zajmuje to pamięć i czas. Jest taki specjalny operator trójargumentowy, tzw. operator warunkowy, który używa wyrażeń i sam jest wyrażeniem (tzn. ma wartość). Jak to trójargumentowy? Operator jednoargumentowy może być prefiksem (przedrostkiem, czyli po lewej) albo sufiksem (przyrostkiem, czyli po prawej) swojego operandu (argumentu). Zwykły dwuargumentowy operator ma dwa operandy: jeden z lewej, a drugi z prawej strony znaku operatora. Jak operator trójargumentowy może mieć 3 operandy? Gdzie? Otóż składa się on z dwóch znaków! Operator warunkowy to znak zapytania „?” i znak dwukropka „:”, ale nie napisane obok siebie. Między te znaki, a także przed nimi i za nimi wstawiamy operandy tak
operand1 ? operand2 : operand3
warunek ? wartość_dla_warunku_prawdziwego : wartość_dla_warunku_fałszywego
Właśnie tak, pierwszy operand jest wyrażeniem o wartości typu bool i w zależności od jego wartości obliczane jest wyrażenie operand2 albo operand3 (muszą dawać ten sam typ albo typy niejawnie rzutowalne na siebie).
Kiedy to się może przydać i czy warto tego używać? Takie pytanie należy sobie zadawać zawsze w przypadku konstrukcji, które udziwniają kod, czyli czynią go może nie mniej zrozumiałym, ale na pewno wolniej zrozumiałym. Przy czytaniu cudzego kodu ważna jest szybkość oraz to, żeby kod przemawiał do każdego poprzez swoją własną strukturę, a jeśli nie jest ona oczywista, to poprzez komentarze. Sztandarowym przykładem jest prosta ochrona przed znaną sytuacją wyjątkową (czyli generującą wyjątek), na przykład dzielenie przez zero jak w przykładzie poniżej:
double sinxdivx = x != 0.0 ? Math.Sin(x) / x : 1.0;
Spis treści Poprzednia strona: Operatory i wyrażenia Następna strona: Pętle