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

Pierwszy program C#

Ile cyfr ma liczba? Platforma .NET i C# Projekt aplikacji C# Klasy i obiekty Program w Visual C# Obsługa wyjątków Program w Visual C# (wersja 2) Następny przykład Program w Visual C++ Kontakt

Ile cyfr ma liczba?

Pierwsza aplikacja, którą utworzymy w języku C#, ma wczytać z klawiatury wartość całkowitą, wyznaczyć liczbę jej cyfr dziesię­tnych i wypisać wynik na monitorze (w oknie konsoli). Algo­rytm tej apli­kacji, omówiony dokładnie na stronie Pierwszy program C++, używa dwóch zmiennych całko­witych nk. Pierwsza przyjmuje na początku wczytaną wartość, druga zawiera na końcu wynik obliczeń, które opisuje pętla:

for (k = 1; n /= 10; k++)
    ;

Jej wykonanie przebiega następująco:

Podstawowe konstrukcje językowe C++ i C# są bardzo podobne. Dlatego w programie znajdującym liczbę cyfr danej liczby całko­witej spróbujemy bezpo­średnio zamieścić powyższy kod.

Platforma .NET i C#

Platforma .NET Framework jest technologią tworzenia i uruchamiania aplikacji dla różnych archi­tektur sprzętowych. Umożliwia korzystanie z szeregu języków progra­mowania, w tym C#, Visual Basic, C++/CLI i Delphi. Kompi­latory zgodne z platformą .NET kompilują kod źródłowy do języka pośre­dniego MSIL (Microsoft Intermediate Language), w skrócie IL. Tak przygotowany kod jest traktowany jako aplikacja, którą można wykonać na dowolnym kompu­terze z zainstalowanym środo­wiskiem uruchomie­niowym CLR (Common Language Runtime), składni­kiem platformy .NET. Jest to proces w pewnym stopniu podobny do wykorzy­stania wirtualnej maszyny w języku Java. Kod IL nie jest jednak interpre­towany, lecz kompi­lowany w locie (Just in Time, JIT) na macie­rzysty kod maszynowy (native code), który jest wykonywany. Kompi­lator JIT stosuje mechanizm "na żądanie", kompi­lując tylko to, co jest potrzebne i nie zostało jeszcze skompilowane.

Język C#, zaprojektowany przez firmę Microsoft i ściśle związany z platformą .NET, udostępnia szereg wbudo­wanych typów danych. Są one zdefi­niowane w przestrzeni nazw System.

Nazwa w języku C# jest ciągiem znaków spośród liter i cyfr rozpoczy­nającym się od litery, przy czym duże i małe litery są odróżniane. Znak podkre­ślenia _ traktowany jest jak litera, może więc wystę­pować w dowolnym miejscu nazwy, nawet na początku. W przeciwieństwie do większości środowisk programi­stycznych, w nazwie można używać polskich znaków diakrytycznych: Ą, ą, Ć, ć, Ę, ę, Ł, ł, Ń, ń, Ó, ó, Ś, ś, Ź, ź, Ż, ż. Platforma .NET i język C# obsługują bowiem standard kodowania znaków Unicode, dlatego te znaki są traktowane jako litery.

Ustawienia regionalne mają w C# wpływ na zapis i odczyt danych. Przy wypisy­waniu tekstu do okna konso­lowego polskie znaki są prawi­dłowo wyświe­tlane, pomimo że w konsoli stoso­wana jest strona kodowa CP852. Usta­wienia regio­nalne dotyczą nie tylko wyboru języka, lecz także formatu daty i godziny, zapisu liczb i waluty, wielkości liter i ich porówny­wania, systemu metry­cznego i kalendarza. W kon­wencji polskiej używamy przecinka jako separa­tora dziesię­tnego do oddzie­lania części całko­witej od ułam­kowej, dlatego należy pamiętać, by przy wprowa­dzaniu liczb rzeczy­wistych używać przecinka zamiast kropki (w kodzie źródłowym C# używamy kropki). Usta­wienia regio­nalne można ominąć, gdy zachodzi potrzeba uwzglę­dnienia innego regionu. Przydatna staje się wówczas klasa CultureInfo.

Projekt aplikacji C#

Visual Studio Community 2017 pozwala tworzyć programy konsolowe (Concole Applications) i okienkowe (Windows Forms) m.in. w języku C# z przeznaczeniem na platformę Windows .NET Framework. Aby utworzyć projekt nowego programu konso­lowego w C#, po urucho­mieniu środo­wiska postępujemy następująco:

1 Uruchamiamy kreatora nowego projektu za pomocą polecenia Plik Nowy Projekt... (alterna­tywnie możemy skorzystać z łącza Utwórz nowy projekt... w oknie startowym środo­wiska). Po ukazaniu się okna Nowy projekt doko­nujemy następu­jących ustawień (rys.):
  • w lewym panelu rozwijamy węzeł Inne języki (jeśli nie jest rozwinięty) i wybieramy Visual C#,
  • w części centralnej okna zaznaczamy opcję Aplikacja konsoli (.NET Framework),
  • w pierwszym polu na dole okna wpisujemy nazwę projektu,
  • w drugim polu wybieramy lub wpisujemy lokalizację rozwią­zania (folder nadrzędny),
  • w trzecim polu zmieniamy nazwę rozwiązania, gdy ma być inna niż nazwa projektu,
  • w czwartym polu wybieramy wersję platformy .NET lub pozosta­wiamy najnowszą.
Rozwiązanie może obejmować jeden lub większą liczbę projektów. W rozpatrywanym przypadku projekt Lcyfr1 będzie na razie jedynym projektem rozwią­zania Lcyfr, dla którego zostanie utworzony podfolder w folderze C:\Cpp\VCsh. Aby utworzyć projekt (i rozwiązanie, bo go jeszcze nie ma), naciskamy przycisk OK.
2 W tym momencie został utworzony folder rozwiązania zawierający szereg podfolderów i plików, a w oknie edytora został pokazany szablo­nowy kod źródłowy programu (rys.). W rozpatrywanym przypadku rozwią­zanie zajmuje folder C:\Cpp\VCsh\Lcyfr, w którym m.in. znajdują się:
  • Lcyfr.sin – plik rozwiązania z informacją o projekcie,
  • Lcyfr1 – folder projektu,
  • Lcyfr1.csproj – plik projektu (w folderze Lcyfr1),
  • Program.cs – kod źródłowy programu (w folderze Lcyfr1).
Pliki Lcyfr.sinLcyfr1.csproj mogą później posłużyć do otwarcia zapisa­nego projektu. Miano­wicie, po wybraniu pole­cenia Plik Otwórz Projekt/rozwiązanie... znajdujemy nazwę którego­kolwiek z nich i klikamy na niej dwukrotnie lewym przyciskiem myszy. Alterna­tywnie możemy otworzyć projekt, korzy­stając z listy ostatnio używanych projektów w oknie startowym środowiska.
3 Program daje się pomyślnie skompilować za pomocą polecenia Kompiluj Kompiluj rozwią­zanie. Program wynikowy można uruchomić, wybie­rając polecenie Debugowanie Uruchom bez debugo­wania. Jak widać (rys.), środo­wisko uruchamia program we własnej konsoli i zatrzymuje ją po jego zakoń­czeniu, oczekując na naciśnięcie dowolnego klawisza.

Okno edytora Visual Studio zawiera szablonowy kod programu konso­lowego. Na początku do programu włączonych jest kilka popu­larnych przestrzeni nazw (namespaces), które najczęściej są przydatne w aplikacjach konso­lowych. Dalej zdefi­niowana jest przestrzeń Lcyfr1, której zakres określają skrajne nawiasy {} (defi­nicję nazwy przestrzeni można pominąć), a w niej klasa Program zawiera­jąca metodę Main. Jest to metoda obowiąz­kowa, od niej rozpo­czyna się wyko­nanie programu.

Klasy i obiekty

Klasa jest pewnego rodzaju szablonem (wzorcem), według którego buduje się obiekty. Elementami składowymi klasy są pola (dane) określa­jące cechy obiektu i metody (funkcje) opisujące jego zacho­wanie. Przykładowo, dowolny punkt na ekranie monitora można opisać za pomocą klasy Punkt, której polami są

zaś metodami funkcje

Definicja klasy Punkt mogłaby, z pominięciem kilku metod (wielo­kropek), wyglądać następująco:

class Punkt
{
    int x, y;        // współrzędne punktu
    int kol;         // kolor punktu (RGB)

    public Punkt(int x, int y, int kol)
    {
        this.x = x;
        this.y = y;
        this.kol = kol;
        Pokaż();
    }

    public void Przesuń_o(int dx, int dy)
    {
        Ukryj();
        x += dx;     // zamiast x = x + dx;
        y += dy;     // zamiast y = y + dy;
        Pokaż();
    }
    ...              // pozostałe metody
}

Klasa nie zajmuje żadnego miejsca w pamięci, dopiero w momencie tworzenia obiektu (instancji klasy) za pomocą opera­tora new rezerwo­wane jest dla niego miejsce w pamięci i wywoływany jest konstru­ktor, który może inicjali­zować pola obiektu i wykonywać inne operacje związane z jego utworzeniem. Konstru­ktor ma taką samą nazwę jak klasa i nie zwraca żadnej wartości, nawet typu void. Brak konstru­ktora w definicji klasy nie jest błędem, tworzony jest wtedy tzw. konstru­ktor domyślny.

Zwalnianiem pamięci przydzielonej obiektowi zajmuje się w środowisku .NET specjalny mechanizm zwany garbage collector (odśmiecacz pamięci). Jeżeli zachodzi potrzeba zareago­wania w takim momencie, należy zdefi­niować destru­ktor, który ma nazwę jak klasa, ale poprze­dzoną znakiem tyldy, nie ma argumentów i nie zwraca żadnej wartości. W przypadku klasy Punkt destru­ktor miałby nazwę ~Punkt. Ze względu na mechanizm odśmie­cania pamięci w większości przypadków nie ma potrzeby definio­wania w języku C# destru­ktora.

Słowo kluczowe public jest tzw. modyfi­katorem dostępu, który oznacza, że do elementów klasy nim opatrzo­nych można się odwoływać spoza klasy (elementy publiczne). Gdyby zamiast public użyć modyfi­katora private albo go pominąć, elementy te byłyby dostępne tylko wewnątrz klasy (elementy prywatne). Z kolei słowo kluczowe this wskazuje na odwo­łanie do bieżą­cego obiektu (do aktualnej instancji klasy). W rozpatrywanym przykładzie argumenty konstru­ktora mają takie same nazwy jak pola klasy, więc użycie słowa this pozwo­liło jawnie określić, że polom obiektu należy przypisać wartości odpowia­dających im para­metrów wywołania konstru­ktora. Można by oczywiście użyć innych nazw argu­mentów, a wówczas this nie byłoby konieczne. Względy "estetyczne" przema­wiają jednak za zapropo­nowanym rozwiązaniem.

Kontynuując powyższy przykład, utwórzmy pośrodku ekranu o rozdziel­czości 1280x800 pikseli obraz złożony z trzech punktów w kolorach czerwonym, zielonym i niebieskim usta­wionych poziomo w równych względem siebie odległo­ściach co 100 pikseli. Następnie po naciśnięciu dowolnego klawisza przesuńmy dwa skrajne punkty tak, by powstało złudzenie obrotu tego obrazu o 90o zgodnie z ruchem wskazówek zegara (rys.).

Mając na uwadze odwrotny kierunek osi y układu współ­rzędnych ekra­nowych do ogólnie przyjętego, możemy to zadanie rozwiązać w następu­jący sposób:

Punkt pkt1 = new Punkt(540, 400, 255*256*256);   // czerwony punkt
Punkt pkt2 = new Punkt(640, 400, 255*256);       // zielony punkt
Punkt pkt3 = new Punkt(740, 400, 255);           // niebieski punkt
Console.ReadKey();
pkt1.Przesuń_o(100, -100);
pkt3.Przesuń_o(-100, 100);

Każda z trzech pierwszych instrukcji tworzy obiekt klasy Punkt (instancję tej klasy) i przypisuje stosownej zmiennej referencję do utworzo­nego obiektu. Referencja jest wartością informu­jącą o położeniu obiektu w pamięci. Operator new rezerwuje obszar pamięci dla obiektu i wywołuje konstruktora, który tu inicjalizuje trzy pola obiektu i wyświetla punkt na ekranie. Metoda ReadKey klasy Console zdefi­niowanej w przestrzeni System wstrzymuje działanie programu do momentu naciśnięcia klawisza. Na koniec dwie instrukcje odwołu­jące się do metody Przesuń_o dwóch obiektów klasy Punkt poprzez ich referencje zapa­miętane w zmiennych pkt1pkt3 powodują przesu­nięcie punktu czerwonego i niebieskiego do nowego miejsca na ekranie.

Klasa może mieć jeszcze tzw. właściwości, które umożli­wiają dostęp do pól i mogą realizować związane z nimi dodatkowe czynności. Gdyby przykła­dowa klasa Punkt miała właści­wość Kol dotyczącą koloru punktu, prawdopo­dobnie jej definicja miałaby postać:

public int Kol
{
    get
    {
        return kol;
    }
    set
    {
        if (value >= 0 && value < 16777216)
        {
            Ukryj();
            kol = value;
            Pokaż();
        }
    }
}

Pole kol jest prywatne, nie ma więc do niego dostępu spoza klasy Punkt. Jednak publiczna właści­wość Kol pozwala na pobranie jego wartości (akcesor get). Na przykład w przypadku trzech powyższych punktów wartością wywo­łania pkt3.Kol jest 255, która oznacza, że punkt ma kolor niebieski. Można także zmienić kolor punktu (akcesor set), np. wywołanie pkt2.Kol(255*256+255) spowo­duje zmianę koloru środko­wego punktu z zielonego na turkusowy (cyan). Słowo kluczowe value repre­zentuje wartość przypi­sywaną właści­wości. Gdyby przekra­czała dopuszczalny zakres (trzy bajty 0÷255), próba zmiany koloru nie przynio­słaby żadnego skutku.

Program w Visual C#

Przystąpmy wreszcie do rozbudowania szablonu aplikacji konsolowej udostępnio­nego w oknie edytora kodu środo­wiska programi­stycznego Visual C#, a właściwie jej metody Main ze względu na prostotę tej aplikacji. Oprócz wspomnia­nego na początku kodu algorytmu należy w niej umieścić dekla­racje dwóch zmiennych całko­witych i zaprogramować wejście-wyjście, które w przypadku apli­kacji konso­lowej można obsłużyć za pomocą metod klasy Console. Drobną niedogo­dność sprawia metoda ReadLine, która czyta łańcuch znaków, podczas gdy potrze­bujemy wartości typu long. Na szczęście typy wbudowane C# są klasami posiada­jącymi metodę Parse, która dokonuje konwersji łańcucha na wartość danego typu (równie dobrze można skorzy­stać z klasy Convert). Oto kod źródłowy rozbu­dowanej metody Main:

static void Main(string[] args)
{
    long n;
    int k;
    Console.Write("Liczba całkowita: ");
    n = long.Parse(Console.ReadLine());
    for (k = 1; n /= 10; k++)
        ;
    Console.WriteLine("Cyfr: " + k);
}

Zauważmy, że program wypisuje wynik dodawania "Cyfr: "+k, którego pierwszym argu­mentem jest wartość typu string, zaś drugim wartość typu int. W takim przy­padku drugi argu­ment zostaje nieja­wnie przekonwer­towany na wartość typu string i dołą­czony do pierw­szego (konkate­nacja łańcu­chów). Niestety, kompi­lacja programu ujawnia błąd (rys.) polega­jący na tym, że warunkiem konty­nuacji pętli for nie jest wyra­żenie logiczne.

Jak widać, nie zawsze to, co jest dozwolone w języku C++, daje się przenieść do języka C#. Program można łatwo naprawić, porównu­jąc występu­jący w roli warunku kontynu­acji pętli wynik wyra­żenia arytmety­cznego (iloraz n/10) z zerem. Pętla ma być kontynu­owana, gdy nowa wartość n jest różna od zera:

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

namespace Lcyfr1
{
    class Program
    {
        static void Main(string[] args)
        {
            long n;
            int k;
            Console.Write("Liczba całkowita: ");
            n = long.Parse(Console.ReadLine());
            for (k = 1; (n /= 10) != 0; k++)
                ;
            Console.WriteLine("Cyfr: " + k);
        }
    }
}

Program daje się skompilować i działa zgodnie z oczeki­waniami, oczywi­ście gdy na wejściu dostaje prawi­dłowe dane. A co się stanie, gdy zamiast liczby dostanie przypadkowy ciąg znaków? Program wypro­wadzi do okna konsoli obszerny komu­nikat informu­jący o zaistnia­łej sytuacji wyjątkowej i zakończy działanie (rys.). W tym przypa­dku błąd jest błahy, ale nie można dopuścić do tego, aby w rzeczy­wistych warunkach z powodu nieuwagi użytko­wnika lub innych przyczyn wyko­nanie programu zakoń­czyło się fiaskiem. To aplikacja powinna wychwy­tywać wyjątkowe sytuacje i właściwie na nie reagować, nie obarczając tym użytko­wnika.

Obsługa wyjątków

Jedna z dwóch prostszych konstrukcji językowych C# służących do przechwy­tywania wyjątków ma postać:

try
{
    ...    // Kod, który może spowodować wyjątek
}
catch
{
    ...    // Kod obsługi wyjątków
}

Kod realizujący określone zadanie jest umieszczony w bloku try, a kod obsłu­gujący wyjątki w bloku catch. Jeżeli podczas reali­zacji zadania wygene­rowany zostanie wyjątek, nastąpi natychmia­stowe przejście do wyko­nania instru­kcji w bloku catch bez wykony­wania dalszych instru­kcji w bloku try.

Druga konstrukcja służąca do przechwyty­wania wyjątków ma dodatkowo blok finally. Kod zamie­szczony w tym bloku jest wykony­wany zawsze, nieza­leżnie od tego, czy urucho­miony zostanie wyjątek czy nie. Blok ten jest używany w przy­padku, gdy wymagane są pewne czynności porządkowe, np. niezbędne przed zakoń­czeniem aplikacji. Jeżeli wystę­puje blok finally, blok catch jest opcjonalny.

Program w Visual C# (wersja 2)

Druga wersja programu będzie wzbogacona o obsługę wyjątków. Wprowa­dzimy również dwie drobne zmiany dotyczące konwersji łańcucha na liczbę i wyprowa­dzenia wyniku obliczeń. Rozpoczy­namy od utwo­rzenia projektu o nazwie Lcyfr2, który będzie po Lcyfr1 drugim projektem istnie­jącego już rozwią­zania Lcyfr (rys.).

Okazuje się, że środowisko Visual Studio Community 2017 zachowuje się nieprzewi­dzianie podczas tworzenia projektu apli­kacji C# włącza­nego do istnie­jącego już rozwią­zania. W przy­padku tworzenia kolejnego projektu aplikacji C++ w folderze rozwią­zania utworzony zostaje kolejny plik *.sin, dlatego w folderze rozwią­zania Lcyfr znalazły się dwa pliki Lcyfr1.sinLcyfr2.sin oraz dwa podfoldery Lcyfr1Lcyfr2 dla dwóch projektów składa­jących się na to rozwią­zanie. W przy­padku tworzenia drugiego projektu aplikacji C# włącza­nego do istnie­jącego rozwią­zania pojawia się komu­nikat, że plik rozwią­zania zostanie nadpi­sany (rys.).

Prawdopodobnie jest to błąd środowiska, które w folderze rozwią­zania utrzymuje tylko jeden plik *.sin z nazwą rozwią­zania, a nie kilka plików z nazwami poszcze­gólnych projektów. Na szczęście dla każdego projektu tworzony jest odrębny podfolder, więc do każdego projektu można sięgnąć i nic nie stoi na przeszko­dzie, by zgodzić się na nadpi­sanie pliku. Można również ominąć ten drobny problem, zmie­niając nazwę pliku Lcyfr.sin np. na Lcyfr1.sin przed utwo­rzeniem drugiego projektu.

Po skopiowaniu zawartości metody Main programu Lcyfr1 do pustej metody Main programu Lcyfr2 uzupełniamy ją o instrukcję try—catch. Do bloku try przeno­simy sekwencję instru­kcji rozpoczy­nającą się od instru­kcji zawiera­jącej operację konwersji łańcucha na liczbę, gdyż to ta operacja może urucho­mić wyjątek, zaś w bloku catch umieszczamy instru­kcję wypisu­jącą komu­nikat o niedo­puszczalnej postaci wprowa­dzanego łańcucha. Aby prze­kształcić łańcuch na liczbę, skorzy­stamy tym razem z metody ToInt64 klasy Convert zawiera­jącej wiele metod konwersji różnych typów danych. Ponadto do wypisania wyniku obliczeń zasto­sujemy tzw. formato­wanie łańcuchów, które przypo­mina używanie symboli formatu­jących w funkcji printf języka C. Forma­towany łańcuch zawiera symbole zastępcze {0}, {1}, {2} itd. określa­jące indeksy kolejnych wartości, które mają być wyprowa­dzone. W rozpatry­wanym przypadku łańcuch formatu­jący ma postać "Cyfr: {0}", bo wyprowa­dzana jest tylko wartość zmiennej k. Ostate­czna wersja programu 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 void Main(string[] args)
        {
            long n;
            int k;
            Console.Write("Liczba całkowita: ");
            try
            {
                n = Convert.ToInt64(Console.ReadLine());
                for (k = 1; (n /= 10) != 0; k++)
                    ;
                Console.WriteLine("Cyfr: {0}", k);
            }
            catch
            {
                Console.WriteLine("Nieprawidłowa wartość!");
            }
        }
    }
}

Gdy w trakcie uruchomienia programu wprowa­dzony zostanie łańcuch nierepre­zentujący wartości całko­witej typu long, wyświe­tlony zostanie krótki komunikat o zai­stniałej sytuacji (rys.) bez poda­wania szczegó­łowych infor­macji przyda­tnych jedynie programistom.

Opracowanie przykładu: lipiec 2018