PROGRAMOWANIE – PRZYKŁADY
IDE C++ WinBGI C# Wpadki

Przykład C#

Wydawanie reszty Tablice jednowymiarowe Program w Visual C# Łańcuchy w C# Zgłoszenie wyjątku Program w Visual C# (wersja 2) Poprzedni przykład Następny przykład Program w Visual C++ Kontakt

Wydawanie reszty

Problem wydawania reszty w sklepie lub wypłacania kwoty w banku polega na złożeniu żądanej kwoty przy użyciu jak najmniejszej liczby banknotów i monet. Zadaniem programu w języku C# jest wczytanie nieujemnej liczby rzeczy­wistej wyrażającej kwotę złotowo-groszową i wyprowadzenie liczby nominałów, z których ta kwota jest złożona. Na przykład dla liczby 145,74 prawi­dłowym wynikiem jest:

    100 zł: 1
     20 zł: 2
      5 zł: 1
     50 gr: 1
     20 gr: 1
      2 gr: 2

Podobnie jak w analogicznym programie języku C++ przyjmu­jemy, że wartością zmiennej Reszta jest wczytana na początku liczba rzeczy­wista wyraża­jąca złotowo-groszową resztę do wydania, zaś 15-elemen­towa tablica Nom zawiera uporządko­wane malejąco liczby całkowite wyraża­jące wartości nominałów banknotów i monet przeli­czone na grosze: warto­ścią elementu Nom[0] jest 50000 (500 zł), warto­ścią Nom[1] jest 20000 (200 zł), ..., wartością Nom[14] jest 1 (1 gr). Algorytm polega na przeglą­daniu nomi­nałów w kolej­ności od najwię­kszego do najmniej­szego i obli­czeniu dla każdego z nich, ile razy mieści się on w kwocie wyrażonej w gro­szach, wypisaniu tego nominału i wyli­czonej liczby jego wystą­pień na wyjściu oraz pomniej­szeniu kwoty o wartość wyliczo­nej liczby nominałów:

Kwota = 100 * Reszta;        // Kwota do wypłacenia
for (k = 0; Kwota > 0; k++)
{
    n = Kwota / Nom[k];      // Liczba nominałów Nom[k]
    if (n > 0)
    {
        ...                  // Wypisz nominał Nom[k]
        ...                  // Wypisz n
        Kwota %= Nom[k];     // Kwota pozostała
    }
}

Tablice jednowymiarowe

Tablica jednowymiarowa jest w C# ciągiem elementów tego samego typu o wspólnej nazwie. Elementy te są ponume­rowane kolejnymi liczbami całko­witymi od zera wzwyż, zwanymi indeksami. W rozpa­trywanym programie potrzebna jest tablica eleme­ntów całko­witych reprezen­tujących przeli­czone na grosze wartości wszystkich używa­nych obecnie nomi­nałów banknotów i monet uporządko­wane w kolej­ności od najwię­kszej do najmniej­szej. Tablicę można najpierw zadekla­rować, następnie przydzielić jej pamięć, a potem przypisać stosowne wartości jej elementom:

int[] Nom;
Nom = new int[15];
Nom[0] = 50000;      // 500 zł
Nom[1] = 20000;      // 200 zł
...                  // inne przypisania
Nom[14] = 1;         // 1 gr

Możliwy jest również zwarty zapis deklaracji i przydziału pamięci:

int[] Nom = new int[15];

Wszystkie elementy tak zdefiniowanej tablicy mają wartość 0. Można oczywiście przypisać im później inne wartości, jak zrobiono to powyżej, ale prościej jest zainicja­lizować te elementy od razu, podając listę inicja­lizacyjną:

int[] Nom = new int[15] {50000, 20000, 10000, 5000, 2000, 1000, 500, 200, 100, 50, 20, 10, 5, 2, 1};

Możliwy jest jeszcze bardziej uproszczony zapis – bez operatora new i liczby elementów (kompi­lator sam oblicza rozmiar tablicy, anali­zując listę warto­ści w nawia­sach klamrowych):

int[] Nom = {50000, 20000, 10000, 5000, 2000, 1000, 500, 200, 100, 50, 20, 10, 5, 2, 1};

Każda tablica jest obiektem klasy Array. Liczbę ele­mentów tablicy udostę­pnia właści­wość Length. Przykła­dowo, warto­ścią wyra­żenia Nom.Length jest 15.

Program w Visual C#

Po utworzeniu projektu aplikacji konsolowej C# przechodzimy do rozbudo­wywania jej szablo­nowego kodu źródłowego udostępnio­nego w oknie edytora. Definicja metody Main, od której zawsze rozpoczyna się wykonanie aplikacji, jest poprze­dzona modyfi­katorem static, który oznacza, że jest to tzw. metoda statyczna i jako taka może być wywołana bez uprzedniego utworzenia obiektu (instancji) klasy, w której została zdefi­niowana. Dzięki temu unika się sytuacji patowej, gdyż w przeciwnym razie należa­łoby najpierw utworzyć obiekt klasy Program, by aplikacja mogła wystar­tować od metody Main, a z kolei instrukcja tworzenia obiektu klasy Program musiałaby zawierać się w metodzie jakiegoś już istniejącego obiektu.

Metoda statyczna może w swojej klasie jedynie wywołać inne metody statyczne i odwoływać się do pól staty­cznych. Najwygo­dniej jest zatem zdefi­niować tablicę Nom jako pole statyczne w klasie Program. Wtedy będzie można odwo­ływać się do elementów tej tablicy bez tworzenia obiektu klasy Program.

Można również tworzyć klasy statyczne. Wszystkie elementy składowe klasy statycznej są statyczne. Klasę statyczną można uważać za pojemnik zawierający zestaw metod realizu­jących podobne zadania, wygodnych do używania ze względu na możliwość ich wywoły­wania bez uprze­dniego tworzenia obiektu tej klasy. Ba, nawet nie jest dozwolone tworzenie obiektów klasy statycznej. Przykładami klas staty­cznych są:

W poniższym programie korzystamy z metod dwóch wymienionych wyżej klas staty­cznych: ConsoleConvert.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Reszta1
{
    class Program
    {
        static int[] Nom = {50000, 20000, 10000, 5000, 2000, 1000,
                            500, 200, 100, 50, 20, 10, 5, 2, 1};

        static void Main(string[] args)
        {
            Console.Write("Reszta do wydania: ");
            double Reszta = Convert.ToDouble(Console.ReadLine());
            Console.WriteLine("----------------------------");
            int Kwota = Convert.ToInt32(100 * Reszta);
            for (int k = 0; Kwota > 0; k++)
            {
                int n = Kwota / Nom[k];
                if (n > 0)
                {
                    if (Nom[k] >= 100)
                        Console.Write("{0,3} zł: ", Nom[k] / 100);
                    else
                        Console.Write("{0,3} gr: ", Nom[k]);
                    Console.WriteLine(n);
                    Kwota %= Nom[k];
                }
            }
            Console.WriteLine("----------------------------");
        }
    }
}

Symbole {0,3} występujące w łańcu­chach formatu­jących wartość nominału złoto­wego i groszo­wego w metodzie Write klasy Console określają indeks wyprowa­dzanej wartości równy 0 (wyprowa­dzane argu­menty są numero­wane od zera wzwyż) i szero­kość pola na wyjściu równą 3 (dodatnia liczba oznacza również wyrówny­wanie do prawego brzegu pola, ujemna do lewego). Metoda ToDouble klasy Convert służy tu do konwersji wczytanego łańcucha na wartość rzeczy­wistą wyraża­jącą resztę złotowo-groszową do wydania, a metoda ToInt32 klasy Convert do przekształ­cenia tej reszty na wartość całko­witą przedsta­wiającą tę samą kwotę w groszach. Zamiast metody ToInt32 można skorzystać z rzuto­wania znanego z języka C:

int Kwota = (int)(100 * Reszta);

Kontrola prowadzona przez metodę ToDouble w trakcie konwersji wczytanego łańcucha na wartość rzeczy­wistą reprezen­tującą złotowo-groszową resztę do wydania jest niewystar­czająca (rys.). Powinna to być bowiem wartość nieujemna, a w zapisie jej części ułam­kowej powinny wystąpić dokła­dnie dwie cyfry. Współist­nienie zapisów liczb z kropką (sposób anglo­saski) i prze­cinkiem (sposób polski) w roli separa­tora dziesię­tnego oddziela­jącego część całko­witą od ułamkowej oraz ułatwienie dla mniej uważnego użytko­wnika mogą być usprawiedli­wieniem, by dopuścić oba znaki. Można również przyjąć, że część ułamkową można pominąć, gdy reszta jest kwotą złotową, i że oprócz ew. separa­tora dziesię­tnego w zapisie liczby wystę­pują jedynie cyfry, a cały zapis może być otoczony dowolną liczbą spacji.

Łańcuchy w C#

W dużym uproszczeniu łańcuch można traktować jak ciąg znaków typu char, do których można odwoływać się jak do elementów tablicy, podając nazwę łańcucha i w nawiasach kwadra­towych [] indeks od 0 wzwyż. Znaki są kodowane w systemie Unicode, każdy zajmuje 2 bajty pamięci. Typ łańcu­chowy ma nazwę string. Zamiast niej można używać synonimu String z prze­strzeni nazw System (wszystkie typy standar­dowe C# mają synonimy w prze­strzeni System). Klasa String udostępnia kilka właci­wości i wiele metod, m.in.:

Compare Porównuje łańcuchy
Concat Łączy dwa lub więcej łańcuchów
Contains Sprawdza, czy łańcuch zawiera wskazany w parametrze ciąg znaków
Copy Kopiuje cały łańcuch lub jego fragment
Equals Sprawdza, czy łańcuchy są takie same
Format Formatuje łańcuch (podobnie jak metody WriteWriteln klasy Console)
IndexOf Zwraca pozycję pierwszego wystąpienia znaku w łańcuchu (–1, gdy znak nie występuje)
Insert Wstawia tekst w określone miejsce łańcucha
Join Łączy kilka łańcuchów przy użyciu określonego separatora
LastIndexOf Zwraca pozycję ostatniego wystąpienia znaku w łańcuchu (–1, gdy znak nie występuje)
Length Liczba znaków łańcucha (właściwość)
PadLeft Uzupełnia łańcuch od strony lewej spacjami lub innym znakiem
PadRight Uzupełnia łańcuch od strony prawej spacjami lub innym znakiem
Remove Usuwa fragment łańcucha
Replace Zamienia część łańcucha na określony ciąg znaków
Split Dzieli łańcuch na tablicę łańcuchów
StartsWith Sprawdza, czy łańcuch rozpoczyna się od określonego ciągu znaków
SubString Zwraca część łańcucha
ToLower Konwertuje wielkie litery łańcucha na małe
ToUpper Konwertuje małe litery łańcucha na wielkie
Trim Usuwa wszystkie spacje z początku i końca łańcucha

Wczytanie łańcucha reprezentującego resztę do wydania z pominięciem ewentu­alnych spacji na jego początku i końcu sprowadza się do prostej instrukcji:

string Reszta = Console.ReadLine().Trim();

a znalezienie w nim pozycji separatora dziesiętnego (przecinka lub kropki) do następu­jącej sekwencji:

int k = Reszta.IndexOf(',');
if (k < 0)
    k = Reszta.IndexOf('.');

Ujemna wartość k wskazuje, że w łańcuchu Reszta nie ma separa­tora dziesię­tnego, czyli cały łańcuch repre­zentuje kwotę złotową. W przeciwnym razie znak separa­tora dziesię­tnego znajdujący się na pozycji k dzieli ten łańcuch na dwie części, z których pierwsza określa liczbę złotych, a druga liczbę groszy wczytanej kwoty złotowo-groszowej:

Algotytm konwersji wczytanego łańcucha reprezentu­jącego resztę do wydania na liczbę całko­witą wyra­żającą tę kwotę w groszach może w uproszczeniu, przy niepełnej kontroli popra­wności ciągu znaków łańcucha, wyglądać następująco:

int Kwota;
if (k < 0)
    Kwota = 100 * Convert.ToInt32(Reszta);
else
{
    Kwota = 100 * Convert.ToInt32(Reszta.Substring(0, k));
    Kwota += Convert.ToInt32(Reszta.Substring(k + 1));
}

Zauważmy, że powyższy kod zawiera wywołania metody Substring z różną liczbą argu­mentów, co w przypadku wielu popu­larnych języków progra­mowania jest błędem. W rzeczywistości są to wywołania dwóch metod o tej samej nazwie. W języku C# możliwe jest bowiem, a nawet często spotykane, tzw. przeciążanie lub przełado­wywanie (ang. overloading) metod polega­jące na tym, że w danej klasie może istnieć wiele metod o tej samej nazwie, jeżeli różnią się one liczbą argu­mentów lub ich typami.

Zgłoszenie wyjątku

Dopuszczalnymi znakami łańcucha Reszta reprezen­tującego nieujemną resztę do wydania są cyfry i separator dziesiętny (przecinek lub kropka), który może wystąpić lub nie. Częściową kontrolę popra­wności wczytanego łańcucha zapewnia metoda ToInt32 klasy Convert. Aby osiągnąć pełną kontrolę, należy wychwycić jeszcze dwie sytuacje wyjątkowe (wyjątki):

Cyfry zajmują ciągły zakres znaków od 0 do 9, więc wychwycenie pierwszego wyjątku można zaprogramować następująco:

if (Reszta.Length == 0 || Reszta[0] < '0' || Reszta[0] > '9')
    ...       // Zgłoś wyjątek

W celu wychwycenia drugiego wyjątku wygodniej jest uprzednio wyznaczyć podłańcuch wystę­pujący po separa­torze dziesię­tnym, co prowadzi do następu­jącego kodu:

Reszta = Reszta.Substring(k + 1);
if (Reszta.Length != 2 || Reszta[0] < '0' || Reszta[0] > '9')
    ...       // Zgłoś wyjątek

Warto zwrócić uwagę na konsekwencje przypisania, które po utwo­rzeniu obiektu podłań­cucha zapamię­tuje referencję do niego (wartość informacją o położeniu obiektu w pamięci) w zmiennej Reszta. Tym samym referencja do obiektu pierwo­tnego łańcucha zostaje zgubiona, ponieważ w programie nie utworzono uprzednio jej kopii. Oznacza to, że obiekt ten stał się zbędny, czeka więc na zwol­nienie zajmo­wanej przez niego pamięci przez mechanizm garbage collection (odśmie­canie pamięci) platformy .NET.

Wyjątki są obiektami ogólnej klasy Exception lub klasy potomnej wywo­dzącej się od niej bezpo­średnio lub pośrednio. Platforma .NET udostępnia wiele klas wyjątków, można również definiować własne. Zgłoszenie wyjątku polega na użyciu instrukcji throw, w której należy utworzyć obiekt wyjątku za pomocą operatora new. W najpro­stszym przypadku instru­kcja ta ma postać:

throw new Exception();

Program w Visual C# (wersja 2)

Druga wersja programu wydawania reszty przy użyciu najmniejszej liczby nominałów z własną obsługą wyjątków może wyglą­dać następująco:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Lcyfr2
{
    class Program
    {
        static int[] Nom = {50000, 20000, 10000, 5000, 2000, 1000,
                            500, 200, 100, 50, 20, 10, 5, 2, 1};

        static void Main(string[] args)
        {
            Console.Write("Reszta do wydania: ");
            string Reszta = Console.ReadLine().Trim();
            Console.WriteLine("----------------------------");
            try
            {
                if (Reszta.Length == 0 || Reszta[0] < '0' || Reszta[0] > '9')
                    throw new Exception();
                int Kwota, k = Reszta.IndexOf(',');
                if (k < 0)
                    k = Reszta.IndexOf('.');
                if (k < 0)
                    Kwota = 100 * Convert.ToInt32(Reszta);
                else
                {
                    Kwota = 100 * Convert.ToInt32(Reszta.Substring(0, k));
                    Reszta = Reszta.Substring(k + 1);
                    if (Reszta.Length != 2 || Reszta[0] < '0' || Reszta[0] > '9')
                        throw new Exception();
                    Kwota += Convert.ToInt32(Reszta);
                }
                for (k = 0; Kwota > 0; k++)
                {
                    int n = Kwota / Nom[k];
                    if (n > 0)
                    {
                        if (Nom[k] >= 100)
                            Console.Write("{0,3} zł: ", Nom[k] / 100);
                        else
                            Console.Write("{0,3} gr: ", Nom[k]);
                        Console.WriteLine(n);
                        Kwota %= Nom[k];
                    }
                }
                Console.WriteLine("----------------------------");
            }
            catch
            {
                Console.WriteLine("Nieprawidłowa wartość!");
            }
        }
    }
}

W przeciwieństwie do pierwszej wersji programu wydawania reszty ta wersja potra­ktuje liczbę rzeczy­wistą z 4-cyfrową częścią ułamkową jako niedo­puszczalną (rys.).

Opracowanie przykładu: lipiec 2018