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

Przykład C#

Kalendarz roczny Strumienie w C# Pliki tekstowe w C# Program w Visual C# Instrukcja using a czas życia strumieni Program w Visual C# (wersja 2) Poprzedni przykład Następny przykład Program w C++ Kontakt

Kalendarz roczny

Zadaniem programu jest wyprowadzenie kalen­darza rocznego do pliku teksto­wego. Zakładamy, że nazwa pliku jest łańcu­chem postaci RRRR.txt, w którym RRRR jest zapisem liczby całko­witej wczytanej z klawia­tury i określa­jącej numer roku, dla którego należy zbudować kalen­darz. Program powinien utworzyć 12 bloków tekstu reprezen­tujących kolejne miesiące roku w układzie podobnym do wyświe­tlanego na ekranie kalen­darza miesię­cznego i zapisać je w pliku.

Strumienie w C#

Strumień jest sekwencją danych przesyłanych pomiędzy ich magazynem a pamięcią dostępną dla programu. Za każdym razem, gdy program wykonuje operację wysy­łania (zapisu) lub pobie­rania (odczytu) danych, posłu­guje się strumie­niem. Magazynem danych może być plik, pamięć kompu­tera lub współdzie­lony zasób sieciowy. Przestrzeń nazw System.IO zawiera szereg klas umożli­wiających obsługę stru­mieni, m.in.:

a także wiele klas pozwalających na modyfi­kowanie struktury kata­logów (folderów) i plików (ich tworzenie i usu­wanie, kopio­wanie i przeno­szenie oraz odczyty­wanie infor­macji o nich). Wyszcze­gólnione wyżej strumienie są dość prymitywne – mogą odczy­tywać albo zapi­sywać tylko poje­dyncze bajty lub tablice bajtów. Ich składo­wymi, oprócz licznej grupy przecią­żonych konstru­ktorów, są m.in.:

Close Zamyka strumień i zwalnia wszystkie zasoby z nim związane
Length Zwraca długość strumienia w bajtach (właściwość)
Position Pobiera lub ustawia pozycję w strumieniu (właściwość)
Read Wczytuje sekwencję bajtów i zwraca ich liczbę, która może być mniejsza niż żądana lub równa zero, gdy napotkano koniec strumienia
ReadByte Wczytuje pojedynczy bajt i zwraca go jako nieujemną wartość typu int lub -1, gdy napotkano koniec strumienia
Seek Ustawia pozycję w strumieniu względem wskazanego punktu odniesienia
Write Zapisuje sekwencję bajtów do strumienia
WriteByte Zapisuje pojedynczy bajt do strumienia

Poniżej podany jest przykład programu, który pobiera nazwę pliku z klawia­tury, nastę­pnie otwiera go i czyta bajt po bajcie, zliczając jego cyfry, a na koniec wypisuje tę liczbę na ekranie monitora. Strumień jest tylko ciągiem bajtów, ale ze względu na porówny­wanie ich ze znakami (cyframi) zakładamy, że jest plikiem tekstowym.

using System;
using System.IO;

class Program
{
    static void Main()
    {
        Console.Write("Nazwa pliku: ");
        string nazwa = Console.ReadLine().Trim();
        FileStream fs = new FileStream(nazwa, FileMode.Open);
        int n = 0, b;
        while ((b = fs.ReadByte()) >= 0)
            if (b >= '0' && b <= '9') n++;
        fs.Close();
        Console.WriteLine("Liczba cyfr: {0}", n);
    }
}

Jak widać, w programie nie uwzglę­dniono obsługi wyjątków. Koniec pliku jest zdarzeniem oczeki­wanym w pętli pobiera­jącej kolejne bajty i zostanie zasygna­lizowany za pomocą wartości -1 zwracanej przez metodę ReadByte, ale jeżeli pliku nie da się otworzyć lub nastąpi błąd jego odczytu, zgłoszony zostanie wyjątek. System wyświetli wówczas obszerną infor­mację o niepo­prawnym stanie wykonania programu.

W drugiej wersji programu wyjątki są przechwy­tywane za pomocą instru­kcji try-catch-finally. Tym razem obsługa wyjątku polega na wyko­naniu kodu zawartego w bloku catch, czyli wyświe­tleniu dostar­czonego komuni­katu o błędzie. Kod w bloku finally zostanie wykonany nieza­leżnie od tego, czy wystąpił wyjątek, czy też nie. Plik zostanie zatem zamknięty zawsze, jeśli tylko został wcześniej otwarty.

using System;
using System.IO;

class Program
{
    static void Main()
    {
        FileStream fs = null;
        try
        {
            Console.Write("Nazwa pliku: ");
            string nazwa = Console.ReadLine().Trim();
            fs = new FileStream(nazwa, FileMode.Open);
            int n = 0, b;
            while ((b = fs.ReadByte()) >= 0)
                if (b >= '0' && b <= '9') n++;
            Console.WriteLine("Liczba cyfr: {0}", n);
        }
        catch (Exception e)
        {
            Console.WriteLine(e.Message);
        }
        finally
        {
            if (fs != null) fs.Close();
        }
    }
}

Pierwszy argument konstruktora klasy FileStream użytego w przedsta­wionych wyżej dwóch wersjach programu określa nazwę pliku (wraz z ew. ścieżką dostępu), drugi podaje sposób otwarcia lub utwo­rzenia stru­mienia. Może on przyj­mować nastę­pujące wartości typu wylicze­niowego FileMode:

Append Otwarcie pliku do dopisywania na końcu (jeśli plik nie istnieje, zostaje utworzony)
Create Utworzenie pliku do pisania (jeśli plik istnieje, jego zawartość zostaje skasowana)
CreateNew Utworzenie pliku do pisania (jeśli plik istnieje, wyrzucony zostaje wyjątek)
Open Otwarcie istniejącego pliku do odczytu lub zapisu (jeśli plik nie istnieje, wyrzucony zostaje wyjątek)
OpenOrCreate Otwarcie pliku do odczytu lub zapisu (jeśli plik nie istnieje, zostaje utworzony)
Truncate Otwarcie istniejącego pliku do zapisu (dotychczasowa jego zawartość zostaje skasowana)

Niekiedy trzeba w dodatkowym argumencie typu wylicze­niowego FileAccess doprecy­zować tryb dostępu do pliku:

Read Dane mogą być czytane z pliku (plik tylko do odczytu)
Write Dane mogą być zapisywane do pliku (plik tylko do zapisu)
ReadWrite Dane mogą być czytane z pliku i zapisywane do niego (plik do odczytu i zapisu)

Pliki tekstowe w C#

Klasa FileStream reprezentuje strumień bajtów skojarzony z plikiem i zawiera jedynie bardzo proste metody przesy­łania danych: odczyt lub zapis jednego bajtu albo tablicy bajtów. Z tego względu używa się jej głównie jako pośre­dnika pomiędzy klasami ułatwia­jącymi pracę z danymi typu łańcu­chowego (ang. stream wrapper, opako­wanie stru­mienia) a plikami fizycznymi (rys.). Strumień FileStream odczy­tuje bajty z pliku, a następnie StreamReader czyta je jako łańcuchy, dokonując stosownej konwersji. Wprost przeciwnie strumień StreamWriter zapisuje łańcuchy, konwer­tując je na bajty, a następnie FileStream zapisuje te bajty do pliku. Tak więc FileStream przesyła bajty, zaś StreamReaderStreamWriter przesyłają łańcuchy tworzone z tych bajtów. Oba strumienie domyślnie pracują ze znakami Unicode, ale można to w razie potrzeby zmienić.

Najważniejszymi oprócz konstruktorów metodami klasy StreamReader są:

Close Zamyka strumień i zwalnia wszystkie zasoby z nim związane
Peek Zwraca wartość typu int reprezentującą następny znak lub -1, gdy napotkano koniec strumienia (bieżąca pozycia w strumieniu nie jest zmieniana)
Read Wczytuje jeden lub większą liczbę znaków (zależnie od podanych argumentów)
ReadBlock Wczytuje określoną liczbę znaków do bufora (tablicy znaków)
ReadLine Wczytuje wiersz znaków i zwraca go jako łańcuch (null oznacza koniec strumienia)
ReadToEnd Wczytuje wszystkie znaki od bieżącej pozycji do końca strumienia i zwraca je jako łańcuch

Z kolei najważniejszymi składowymi klasy StreamWriter są:

Close Zamyka strumień i zwalnia wszystkie zasoby z nim związane
Flush Opróżnia wszystkie bufory i zapisuje je w strumieniu (strumień nie zostaje zamknięty)
NewLine Znak nowego wiersza (właściwość, domyślnie w Windows \r\n – znak powrotu karetki i wysunięcia wiersza)
Write Zapisuje reprezentację tekstową wartości argumentu lub sformatowany łańcuch
WriteLine Zapisuje reprezentację tekstową wartości argumentu lub sformatowany łańcuch, a następnie znak nowego wiersza

Klasy StreamReader i StreamWriter mają cały szereg konstru­ktorów, toteż ich strumienie można tworzyć na wiele sposobów, co pozwala na dużą elasty­czność ich łączenia z maga­zynem danych. Pokazany na powyższym rysunku jeden ze schematów kojarzenia tych stru­mieni z plikiem tekstowym polega na użyciu obiektu klasy FileStream. Zaprezen­towany w poprze­dnim punkcie niniejszej strony pierwszy przykład programu pobiera­jącego nazwę pliku z klawia­tury, otwiera­jącego ten plik i zlicza­jącego jego cyfry można zapisać z użyciem strumieni FileStreamStreamReader następująco:

using System;
using System.IO;

class Program
{
    static void Main()
    {
        Console.Write("Nazwa pliku: ");
        string s = Console.ReadLine().Trim();
        FileStream fs = new FileStream(s, FileMode.Open);
        StreamReader plik = new StreamReader(fs);
        int n = 0;
        while ((s = plik.ReadLine()) != null)
            foreach (char c in s)
                if (char.IsDigit(c)) n++;
        plik.Close();
        fs.Close();
        Console.WriteLine("Liczba cyfr: {0}", n);
    }
}

Tym razem z pliku nie są pobierane pojedyncze bajty, lecz całe wiersze interpre­towane jako łańcuchy. Wszystkie znaki każdego z tych łańcuchów są w kolejnych krokach pętli foreach utożsa­miane ze zmienną itera­cyjną c typu char. Zauważmy, że do sprawdzenia, czy zmienna c określa cyfrę, posłużyła metoda IsDigit klasy char.

Strumienie StreamReaderStreamWriter można uzyskać za pomocą innych klas, jak np. File czy FileInfo. Można je również utworzyć bezpo­średnio. Na przykład powyższy program można zmodyfi­kować następująco:

using System;
using System.IO;

class Program
{
    static void Main()
    {
        Console.Write("Nazwa pliku: ");
        string s = Console.ReadLine().Trim();
        StreamReader plik = new StreamReader(s);
        int n = 0;
        while ((s = plik.ReadLine()) != null)
            foreach (char c in s)
                if (char.IsDigit(c)) n++;
        plik.Close();
        Console.WriteLine("Liczba cyfr: {0}", n);
    }
}

Program w Visual C#

Poniżej przedstawiony jest pełny kod źródłowy programu w języku C# wyprowadza­jącego kalen­darz roczny do pliku tekstowego o nazwie RRRR.txt, w której RRRR jest ciągiem znaków określa­jących numer roku wczytany z klawia­tury. Plik kalen­darza tworzony jest przy użyciu stru­mienia teksto­wego StreamWriter. Kolejne miesiące roku od stycznia do grudnia są w nim zapisywane w układzie podobnym do wyświe­tlanego na ekranie kalen­darza miesię­cznego obejmu­jącym nagłówek z nazwą miesiąca, numerem roku i skrótami nazw dni tygodnia oraz numery dni miesiąca.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using Kalendarz.Greg;

namespace KRocz1
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                Console.Write("Numer roku (1583-3000): ");
                int r = Convert.ToInt32(Console.ReadLine().Trim());
                if (r < 1583 || r > 3000)
                    throw new Exception("Nieprawidłowy zakres numeru roku.");
                StreamWriter plik = new StreamWriter(r + ".txt");
                for (int m = 1; m <= 12; m++)
                {
                    Miesiac(m, r, plik);
                    if (m < 12)
                        plik.WriteLine();
                }
                plik.Close();
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
            }
        }

        static void Miesiac(int m, int r, StreamWriter plik)
        {
            plik.Write("{0," + (17 - Mc[m].Length) / 2 + "}", "");
            plik.WriteLine("{0} {1}", Mc[m], r);
            plik.WriteLine(" --------------------");
            plik.WriteLine(" Nd Po Wt Śr Cz Pt So");
            plik.WriteLine(" --------------------");
            int n = Kalend.Dtyg(1, m, r), max = Kalend.Dmax(m, r);
            if (n > 0)
                plik.Write("{0," + 3 * n + "}", "");
            for (int d = 1; d <= max; d++)
            {
                plik.Write("{0,3}", d);
                if ((d + n) % 7 == 0)
                    plik.WriteLine();
            }
            if ((n + max) % 7 != 0)
                plik.WriteLine();
        }

        static readonly string[] Mc = {null,
            "Styczeń", "Luty", "Marzec", "Kwiecień", "Maj", "Czerwiec", "Lipiec",
            "Sierpień", "Wrzesień", "Październik", "Listopad", "Grudzień"};
    }
}

Zasadniczą pracę programu wykonuje metoda Miesiac wywoły­wana 12-krotnie w metodzie Main. Formato­wanie i zapi­sywanie tekstu do stru­mienia plik klasy StreamWriter jest w niej realizo­wane przez metody WriteWriteLine, które działają podobnie jak metody klasy Console o tych samych nazwach wyświe­tlające tekst na ekranie. W przy­padku plików nie ma oczywiście pozycjo­nowania kursora, toteż do wycentro­wania pierwszego wiersza nagłówka z nazwą miesiąca i numerem roku oraz do ustawienia jedynki pod skrótem nazwy dnia tygodnia rozpoczy­nającego miesiąc użyto metody Write, która wypro­wadza łańcuch pusty do pola o zmiennej szerokości, wypełniając je spacjami. Poniższe łącze udostępnia przykła­dowy wynik wykonania programu.

Instrukcja using a czas życia strumieni

W języku C# zwalnianiem pamięci przydzielonej obiektom zajmuje się specjalny mecha­nizm o nazwie GC (skrót od ang. garbage collector, odśmie­cacz pamięci). Jego zadaniem jest poszuki­wanie obiektów, do których nie ma referencji, i usuwanie ich z pamięci. Nie zagłę­biając się w złożone zagadnienie zarzą­dzania pamięcią, trzeba na początek jedynie wiedzieć, że GC automa­tycznie śledzi przebieg programu i co jakiś czas po napotkaniu obiektów, do których nie odwołuje się żaden inny obiekt, niszczy je, przeka­zując zajmowane przez nie obszary pamięci do puli pamięci wolnej. Gdy na przykład w jakimś bloku został utworzony obiekt, do którego referencja została zapamiętana tylko w zmiennej lokalnej w tym bloku, to po wyjściu z niego obiekt ten może zostać zniszczony, bo nie ma do niego referencji. Nie oznacza to jednak, że nastąpi to natych­miast. Na pewno zrobi to GC, gdy stwierdzi, że obiekt nie jest już potrzebny, ale nie można określić, kiedy się to stanie – nawet w przy­padku zmuszenia go do odśmie­cania pamięci:

GC.Collect();

Istnienie automatycznego mechanizmu odśmiecania pamięci jest wielką zaletą, gdyż nie trzeba się martwić o to, jak i kiedy tworzone obiekty mają być zwalniane. Jednak w sytua­cjach, gdy obiekt działa na zewnętrznych zasobach, takich jak połączenie z inter­netem bądź plikiem, zasoby te powinny być używane w czasie możliwie najkrótszym dla prawidło­wego działania, a potem jawnie zwolnione bez oczekiwania na akcję GC. W przypadku plików zazwyczaj używa się w tym celu metody Close, np.:

FileStream plik = new FileStream("Dane.txt", FileMode.Open);
...                 // Obsługa pliku
plik.Close();

Jeśli jednak w trakcie obsługi pliku zostanie zgłoszony wyjątek, metoda Close nie zostanie wykonana i plik pozo­stanie otwarty do końca działania programu. Aby temu zaradzić, można użyć instrukcji try-catch-finally, zamy­kając plik w bloku finally (por. przytoczony wyżej drugi przykład programu zlicza­jącego cyfry w pliku). Najlepiej jednak ograni­czyć czas życia obiektu za pomocą instrukcji using:

using (FileStream plik = new FileStream("Dane.txt", FileMode.Open))
{
    ...             // Obsługa pliku
}

Instrukcja automatycznie, gdy tylko nastąpi wyjście poza zakres związanego z nią bloku, usuwa zadekla­rowany w niej obiekt, wywołując metodę Dispose (w przy­padku strumieni metoda ta jest równo­ważna Close). Zatem korzy­stając z instrukcji using mamy pewność, że po zakończeniu operacji na pliku zasoby systemowe zostaną zwolnione. Instrukcja ma również dodatkowe atuty:

Powróćmy jeszcze do przytoczonego wyżej przykładu programu zlicza­jącego cyfry pliku tekstowego, w którym wyjątki są przechwy­tywane za pomocą instrukcji try-catch-finally. Plik jest zamykany w bloku finally, co gwarantuje, że jeśli tylko został otwarty, to zostanie zamknięty nieza­leżnie od tego, czy pojawił się wyjątek, czy nie. Program można udosko­nalić, wydzie­lając obsługę pliku w instrukcji using i jej bloku, ale pozo­stając przy własnej obsłudze wyjątków za pomocą instrukcji try-catch. Oto ulepszona wersja tego programu:

using System;
using System.IO;

class Program
{
    static void Main()
    {
        try
        {
            Console.Write("Nazwa pliku: ");
            string nazwa = Console.ReadLine().Trim();
            using (FileStream fs = new FileStream(nazwa, FileMode.Open))
            {
                int n = 0, b;
                while ((b = fs.ReadByte()) >= 0)
                    if (b >= '0' && b <= '9') n++;
                Console.WriteLine("Liczba cyfr: {0}", n);
            }
        }
        catch (Exception e)
        {
            Console.WriteLine(e.Message);
        }
    }
}

Program w Visual C# (wersja 2)

Poniżej zaprezentowany jest pełny kod źródłowy drugiej wersji programu genero­wania kalen­darza rocznego w pliku tekstowym, w której obsługa strumienia StreamWriter odbywa się przy użyciu instrukcji using. Dodatkowa niezna­czna modyfi­kacja metody Miesiac polega na wycentro­waniu pierwszego wiersza nagłówka z nazwą miesiąca i numerem roku poprzez wypisanie go do pola o zmiennej szerokości bez uprze­dniego poprze­dzania polem wypełnianym spacjami.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.IO;
using Kalendarz.Greg;

namespace KRocz2
{
    class Program
    {
        static void Main(string[] args)
        {
            try
            {
                Console.Write("Numer roku (1583-3000): ");
                int r = Convert.ToInt32(Console.ReadLine().Trim());
                if (r < 1583 || r > 3000)
                    throw new Exception("Nieprawidłowy zakres numeru roku.");
                using (StreamWriter plik = new StreamWriter(r + ".txt"))
                {
                    for (int m = 1; m <= 12; m++)
                    {
                        Miesiac(m, r, plik);
                        if (m < 12)
                            plik.WriteLine();
                    }
                }
            }
            catch (Exception e)
            {
                Console.WriteLine(e.Message);
            }
        }

        static void Miesiac(int m, int r, StreamWriter plik)
        {
            plik.WriteLine("{0," + (17 + Mc[m].Length) / 2 + "} {1}", Mc[m], r);
            plik.WriteLine(" --------------------");
            plik.WriteLine(" Nd Po Wt Śr Cz Pt So");
            plik.WriteLine(" --------------------");
            int n = Kalend.Dtyg(1, m, r), max = Kalend.Dmax(m, r);
            if (n > 0)
                plik.Write("{0," + 3 * n + "}", "");
            for (int d = 1; d <= max; d++)
            {
                plik.Write("{0,3}", d);
                if ((d + n) % 7 == 0)
                    plik.WriteLine();
            }
            if ((n + max) % 7 != 0)
                plik.WriteLine();
        }

        static readonly string[] Mc = {null,
            "Styczeń", "Luty", "Marzec", "Kwiecień", "Maj", "Czerwiec", "Lipiec",
            "Sierpień", "Wrzesień", "Październik", "Listopad", "Grudzień"};
    }
}

Opracowanie przykładu: listopad 2018