Skip to content

5. Często używane klasy .NET

Poniżej przedstawiamy kilka często używanych klas platformy .NET. Wcześniej pokazujemy, jak uzyskać informacje na temat kilkuset dostępnych klas. Ta pomoc jest niezbędna nawet dla doświadczonego programisty C#. Poziom jakości pomocy (łatwy dostęp, przejrzysta struktura, trafność informacji itp.) może zadecydować o sukcesie lub porażce środowiska programistycznego.

5.1. Wyszukiwanie pomocy dotyczącej klas .NET

Poniżej przedstawiamy kilka wskazówek dotyczących wyszukiwania pomocy w programie Visual Studio.NET

5.1.1. Pomoc/Spis treści

  • w [1], wybierz opcję Help/Contents z menu.
  • w [2] należy wybrać opcję Visual C# Express Edition
  • w [3] drzewo pomocy dotyczące języka C#
  • w [4], kolejną przydatną opcją jest .NET Framework, która zapewnia dostęp do wszystkich klas frameworka .NET.

Przejrzyjmy nagłówki rozdziałów pomocy języka C#:

  • [1]: przegląd języka C#
  • [2]: seria przykładów dotyczących niektórych zagadnień języka C#
  • [3]: kurs języka C# – mógłby z powodzeniem zastąpić niniejszy dokument…
  • [4]: aby zagłębić się w szczegóły języka C#
  • [5]: przydatne dla programistów C++ lub Java. Pomaga uniknąć niektórych pułapek.
  • [6]: jeśli szukasz przykładów, możesz zacząć właśnie od tego.
  • [7]: co trzeba wiedzieć, aby tworzyć interfejsy graficzne
  • [8]: jak lepiej korzystać z programu IDE Visual Studio Express
  • [9]: SQL Server Express 2005 to wysokiej jakości oprogramowanie dystrybuowane bezpłatnie. Będziemy z niego korzystać w ramach tego kursu.

Pomoc dotycząca języka C# to tylko część tego, czego potrzebuje programista. Drugą częścią jest pomoc dotycząca setek klas frameworka .NET, które ułatwią mu pracę.

  • [1]: wybieramy dokumentację dotyczącą frameworka .NET
  • [2]: pomoc znajduje się w gałęzi .NET Framework SDK
  • [3]: gałąź .NET „Framework Class Library” zawiera wszystkie klasy .NET według przestrzeni nazw, do których należą
  • [4]: przestrzeń nazw System, która była najczęściej wykorzystywana w przykładach z poprzednich rozdziałów
  • [5]: w przestrzeni nazw System, przykładowo struktura DateTime
  • [6]: pomoc dotycząca struktury DateTime

5.1.2. Pomoc/Indeks/Wyszukiwanie

Pomoc dostarczana przez MSDN jest ogromna i można nie wiedzieć, gdzie szukać. W takim przypadku można skorzystać z indeksu pomocy:

  • w [1], należy użyć opcji [Help/Index], jeśli okno pomocy nie jest jeszcze otwarte; w przeciwnym razie należy użyć [2] w istniejącym oknie pomocy.
  • na [3], należy określić dziedzinę, w której ma odbywać się wyszukiwanie
  • w [4] należy określić, czego szukasz, w tym przypadku klasy
  • w [5] – wynik

Innym sposobem na znalezienie pomocy jest skorzystanie z funkcji wyszukiwania w pomocy:

  • w [1], użyj opcji [Help/Search], jeśli okno pomocy nie jest jeszcze otwarte, w przeciwnym razie użyj [2] w istniejącym oknie pomocy.
  • na [3], określ, czego szukasz
  • w [4], filtrować obszary wyszukiwania
  • w [5] – wynik w postaci różnych tematów, w których znaleziono szukany tekst.

5.2. Ciągi znaków

5.2.1. Klasa System.String

Klasa System.String jest identyczna z prostym typem string. Posiada wiele właściwości i metod. Oto kilka z nich:


public int Length { get; }
nombre de caractères de la chaîne

public bool EndsWith(string value)

rend vrai si la chaîne se termine par value

public bool StartsWith(string value)

rend vrai si la chaîne commence par value
public virtual bool Equals(object obj)

rend vrai si la chaînes est égale à obj - équivalent chaîne==obj

public int IndexOf(string value, int startIndex)

rend la première position dans la chaîne de la
chaîne value - la recherche commence à partir du
caractère n° startIndex

public int IndexOf(char value, int startIndex)

idem mais pour le caractère value

public string Insert(int startIndex, string value)

insère la chaîne value dans chaîne en position
startIndex
public static string Join(string separator, string[] value)

méthode de classe - rend une chaîne de caractères,
résultat de la concaténation des valeurs du tableau
value avec le séparateur separator

public int LastIndexOf(char value, int startIndex, int count)

public int LastIndexOf(string value, int startIndex, int count)

idem indexOf mais rend la dernière position au lieu
de la première

public string Replace(char oldChar, char newChar)

rend une chaîne copie de la chaîne courante où le
caractère oldChar a été remplacé par le caractère
newChar

public string[] Split(char[] separator)

la chaîne est vue comme une suite de champs séparés
par les caractères présents dans le tableau
separator. Le résultat est le tableau de ces champs

public string Substring(int startIndex, int length)

sous-chaîne de la chaîne courante commençant à la
position startIndex et ayant length caractères
public string ToLower()
rend la chaîne courante en minuscules
public string ToUpper()
rend la chaîne courante en majuscules
public string Trim()
rend la chaîne courante débarrassée de ses espaces
de début et fin

Należy zwrócić uwagę na ważną kwestię: gdy metoda zwraca ciąg znaków, jest to ciąg inny niż ten, do którego zastosowano metodę. Tak więc metoda S1.Trim() zwraca ciąg znaków S2, a ciągi S1 i S2 są dwoma różnymi ciągami znaków.

Ciąg C można traktować jako tablicę znaków. Zatem

  • C[i] jest i-tym znakiem ciągu C
  • C.Length to liczba znaków w C

Rozważmy następujący przykład:


using System;

namespace Chap3 {
    class Program {
        static void Main(string[] args) {
            string uneChaine = "l'oiseau vole au-dessus des nuages";
            affiche("uneChaine=" + uneChaine);
            affiche("uneChaine.Length=" + uneChaine.Length);
            affiche("chaine[10]=" + uneChaine[10]);
            affiche("uneChaine.IndexOf(\"vole\")=" + uneChaine.IndexOf("vole"));
            affiche("uneChaine.IndexOf(\"x\")=" + uneChaine.IndexOf("x"));
            affiche("uneChaine.LastIndexOf('a')=" + uneChaine.LastIndexOf('a'));
            affiche("uneChaine.LastIndexOf('x')=" + uneChaine.LastIndexOf('x'));
            affiche("uneChaine.Substring(4,7)=" + uneChaine.Substring(4, 7));
            affiche("uneChaine.ToUpper()=" + uneChaine.ToUpper());
            affiche("uneChaine.ToLower()=" + uneChaine.ToLower());
            affiche("uneChaine.Replace('a','A')=" + uneChaine.Replace('a', 'A'));
            string[] champs = uneChaine.Split(null);
            for (int i = 0; i < champs.Length; i++) {
                affiche("champs[" + i + "]=[" + champs[i] + "]");
            }//for
            affiche("Join(\":\",champs)=" + System.String.Join(":", champs));
            affiche("(\"  abc  \").Trim()=[" + "  abc  ".Trim() + "]");
        }//Główna

        public static void affiche(string msg) {
            // wyświetla komunikat
            Console.WriteLine(msg);
        }//wyświetla
    }//klasa
}//przestrzeń nazw

Wykonanie daje następujące wyniki:

uneChaine=l'oiseau vole au-dessus des nuages
uneChaine.Length=34
chaine[10]=o
uneChaine.IndexOf("vole")=9
uneChaine.IndexOf("x")=-1
uneChaine.LastIndexOf('a')=30
uneChaine.LastIndexOf('x')=-1
uneChaine.Substring(4,7)=seau vo
uneChaine.ToUpper()=L'OISEAU VOLE AU-DESSUS DES NUAGES
uneChaine.ToLower()=l'oiseau vole au-dessus des nuages
uneChaine.Replace('a','A')=l'oiseAu vole Au-dessus des nuAges
champs[0]=[l'oiseau]
champs[1]=[vole]
champs[2]=[au-dessus]
champs[3]=[des]
champs[4]=[nuages]
Join(":",champs)=l'oiseau:vole:au-dessus:des:nuages
("  abc  ").Trim()=[abc]

Rozważmy nowy przykład:


using System;

namespace Chap3 {
    class Program {
        static void Main(string[] args) {
            // analizowana linia
            string ligne = "un:deux::trois:";
            // separatory pól
            char[] séparateurs = new char[] { ':' };
            // podział
            string[] champs = ligne.Split(séparateurs);
            for (int i = 0; i < champs.Length; i++) {
                Console.WriteLine("Champs[" + i + "]=" + champs[i]);
            }
            // łączenie
            Console.WriteLine("join=[" + System.String.Join(":", champs) + "]");
        }
    }
}

oraz wyniki wykonania:

1
2
3
4
5
6
Champs[0]=un
Champs[1]=deux
Champs[2]=
Champs[3]=trois
Champs[4]=
join=[un:deux::trois:]

Metoda Split klasy String pozwala umieścić elementy ciągu znaków w tablicy. Definicja metody Split użytej w tym przypadku jest następująca:


    public string[] Split(char[] separator);
separator
tabela znaków. Znaki te odpowiadają znakom używanym do oddzielania pól w ciągu znaków. Jeśli więc ciąg znaków ma postać „champ1, champ2, champ3"”, można użyć separatora: separator=new char[] {','}. Jeśli separatorem jest ciąg spacji, należy użyć separator=null.
résultat
tablica ciągów znaków, w której każdy element tablicy stanowi pole ciągu.

Metoda Join jest metodą statyczną klasy String:


    public static string Join(string separator, string[] value);
value
tablica ciągów znaków
separator
ciąg znaków, który będzie służył jako separator pól
résultat
ciąg znaków utworzony przez połączenie elementów tablicy value, oddzielonych ciągiem znaków separator.

5.2.2. Klasa System.Text.StringBuilder

Wcześniej wspomnieliśmy, że metody klasy String stosowane do ciągu znaków S1 zwracają inny ciąg znaków S2. Klasa System.Text.StringBuilder pozwala na manipulowanie ciągiem S1 bez konieczności tworzenia ciągu S2. Poprawia to wydajność, unikając mnożenia ciągów o bardzo ograniczonym czasie życia.

Klasa ta obsługuje różne konstruktory:

StringBuilder()
constructeur par défaut
StringBuilder(String value)

construction et initialisation avec value
StringBuilder(String value, int capacité)

construction et initialisation avec value avec une taille de
bloc de capacité caractères.

Obiekt StringBuilder wykorzystuje bloki znaków o rozmiarze capacité do przechowywania ciąg znaków. Domyślnie wartość capacité wynosi 16. Trzeci z powyższych konstruktorów pozwala określić pojemność bloków. Liczba bloków znaków capacité niezbędnych do przechowywania ciągu znaków S jest automatycznie dostosowywana przez klasę StringBuilder. Istnieją konstruktory służące do ustalenia maksymalnej liczby znaków w obiekcie StringBuilder. Domyślnie ta maksymalna pojemność wynosi 2 147 483 647.

Oto przykład ilustrujący tę koncepcję capacité:


using System.Text;
using System;
namespace Chap3 {
    class Program {
        static void Main(string[] args) {
            // str
            StringBuilder str = new StringBuilder("test");
            Console.WriteLine("taille={0}, capacité={1}", str.Length, str.Capacity);
            for (int i = 0; i < 10; i++) {
                str.Append("test");
                Console.WriteLine("taille={0}, capacité={1}", str.Length, str.Capacity);
            }
            // str2
            StringBuilder str2 = new StringBuilder("test",10);
            Console.WriteLine("taille={0}, capacité={1}", str2.Length, str2.Capacity);
            for (int i = 0; i < 10; i++) {
                str2.Append("test");
                Console.WriteLine("taille={0}, capacité={1}", str2.Length, str2.Capacity);
            }
        }
    }
}
  • wiersz 7: utworzenie obiektu StringBuilder o rozmiarze bloku wynoszącym 16 znaków
  • wiersz 8: str.Length to aktualna liczba znaków w ciągu str. str.Capacity to liczba znaków, które może pomieścić obecny ciąg str przed przydzieleniem nowego bloku.
  • wiersz 10: str.Append(String S) umożliwia dołączenie ciągu znaków S typu String do ciągu znaków str typu StringBuilder.
  • wiersz 14: utworzenie obiektu StringBuilder o pojemności bloku wynoszącej 10 znaków

Wynik wykonania:

taille=4, capacité=16
taille=8, capacité=16
taille=12, capacité=16
taille=16, capacité=16
taille=20, capacité=32
taille=24, capacité=32
taille=28, capacité=32
taille=32, capacité=32
taille=36, capacité=64
taille=40, capacité=64
taille=44, capacité=64
taille=4, capacité=10
taille=8, capacité=10
taille=12, capacité=20
taille=16, capacité=20
taille=20, capacité=20
taille=24, capacité=40
taille=28, capacité=40
taille=32, capacité=40
taille=36, capacité=40
taille=40, capacité=40
taille=44, capacité=80

Wyniki te pokazują, że klasa stosuje własny algorytm przydzielania nowych bloków, gdy jej pojemność jest niewystarczająca:

  • wiersze 4–5: zwiększenie pojemności o 16 znaków
  • wiersze 8–9: zwiększenie pojemności o 32 znaki, podczas gdy wystarczyłoby 16.

Oto niektóre metody tej klasy:


public StringBuilder Append(string value)

ajoute la chaîne value à l'objet StringBuilder. Rend
l'objet StringBuilder. Cette méthode est surchargée
 pour admettre différents types pour value : byte,
int, float, double, decimal, ... 

public StringBuilder Insert(int index,
string value)

insère value à la position index. Cette méthode est
surchargée comme la précédente pour accepter
différents types pour value.

public StringBuilder Remove(int index, int length)

supprime length caractères à partir de la position
index.

public StringBuilder Replace(string oldValue,
string newValue)

remplace dans StringBuilder, la chaîne oldValue par
la chaîne newValue. Il existe une version surchargée
(char oldChar, char newChar).
public String ToString()

convertit l'objet StringBuilder en un objet de type
String.

Oto przykład:


using System.Text;
using System;
namespace Chap3 {
    class Program {
        static void Main(string[] args) {
            // str3
            StringBuilder str3 = new StringBuilder("test");
            Console.WriteLine(str3.Append("abCD").Insert(2, "xyZT").Remove(0, 2).Replace("xy", "XY"));
        }
    }
}

oraz jego wyniki:

XYZTstabCD

5.3. Tablice

Tablice wywodzą się z klasy Array:

Klasa Array posiada różne metody służące do sortowania tablicy, wyszukiwania elementu w tablicy, zmiany rozmiaru tablicy itp. Przedstawiamy niektóre właściwości i metody tej klasy. Prawie wszystkie z nich są nadpisane, c.a.d, co oznacza, że istnieją w różnych wariantach. Każda tablica dziedziczy je.

Właściwości

public int Length {get;}
nombre total d'éléments du tableau, quelque soit son nombre de dimensions
public int Rank {get;}
nombre total de dimensions du tableau

Metody

public static int BinarySearch<T>(T[] tableau,
 value)
rend la position de [value] dans tableau.
public static int BinarySearch<T>(T[] tableau,
nt index, int length, T value)
idem mais cherche dans tableau à partir de la
position [index] et sur [length] éléments
public static void Clear(Array tableau, int index,
int length)
met les [length] éléments de tableau commençant au
 n° [index] à 0 si numériques, false si booléens, null si références
public static void Copy(Array source,
Array destination, int length)
copie [length] éléments de source dans destination
public int GetLength(int i)
nombre d'éléments de la dimension n° i du tableau
public int GetLowerBound(int i)
indice du 1er élément de la dimension n° i
public int GetUpperBound(int i)
indice du dernier élément de la dimension n° i
public static int IndexOf<T>(T[] tableau,
T valeur)
rend la position de valeur dans tableau ou -1 si
valeur n'est pas trouvée.
public static void Resize<T>(ref T[] tableau,
int n)
redimensionne tableau à n éléments. Les éléments
déjà présents sont conservés.
public static void Sort<T>(T[] tableau,
IComparer<T> comparateur)
trie tableau selon un ordre défini par comparateur.
Cette méthode a été présentée au paragraphe Erreur : source de la référence non trouvée.

Poniższy program ilustruje wykorzystanie niektórych metod klasy Array:


using System;

namespace Chap3 {
    class Program {
        // rodzaj wyszukiwania
        enum TypeRecherche { linéaire, dichotomique };

        // metoda główna
        static void Main(string[] args) {
            // odczyt elementów tablicy wpisanych z klawiatury
            double[] éléments;
            Saisie(out éléments);
            // wyświetlanie nieposortowanej tablicy
            Affiche("Tableau non trié", éléments);
            // Wyszukiwanie liniowe w nieposortowanej tablicy
            Recherche(éléments, TypeRecherche.linéaire);
            // sortowanie tablicy
            Array.Sort(éléments);
            // wyświetlanie posortowanej tablicy
            Affiche("Tableau trié", éléments);
            // Wyszukiwanie dichotomiczne w posortowanej tablicy
            Recherche(éléments, TypeRecherche.dichotomique);
        }

        // Wprowadzanie wartości do tablicy elementów
        // elementy: odwołanie do tablicy utworzonej przez metodę
        static void Saisie(out double[] éléments) {
            bool terminé = false;
            string réponse;
            bool erreur;
            double élément = 0;
            int i = 0;
            // na początku tablica nie istnieje
            éléments = null;
            // pętla wprowadzania elementów tablicy
            while (!terminé) {
                // pytanie
                Console.Write("Elément (réel) " + i + " du tableau (rien pour terminer) : ");
                // odczyt odpowiedzi
                réponse = Console.ReadLine().Trim();
                // zakończenie wprowadzania danych, jeśli ciąg znaków jest pusty
                if (réponse.Equals(""))
                    break;
                // weryfikacja wprowadzonych danych
                try {
                    élément = Double.Parse(réponse);
                    erreur = false;
                } catch {
                    Console.Error.WriteLine("Saisie incorrecte, recommencez");
                    erreur = true;
                }//try-catch
                // jeśli nie ma błędu
                if (!erreur) {
                    // kolejny element w tablicy
                    i += 1;
                    // zmiana rozmiaru tablicy, aby pomieścić nowy element
                    Array.Resize(ref éléments, i);
                    // wstawienie nowego elementu
                    éléments[i - 1] = élément;
                }
            }//while
        }

        // ogólna metoda wyświetlania elementów tablicy
        static void Affiche<T>(string texte, T[] éléments) {
            Console.WriteLine(texte.PadRight(50, '-'));
            foreach (T élément in éléments) {
                Console.WriteLine(élément);
            }
        }

        // wyszukiwanie elementu w tablicy
        // elementy: tablica liczb rzeczywistych
        // TypeRecherche: dychotomiczna lub liniowa
        static void Recherche(double[] éléments, TypeRecherche type) {
            // Wyszukiwanie
            bool terminé = false;
            string réponse = null;
            double élément = 0;
            bool erreur = false;
            int i = 0;
            while (!terminé) {
                // pytanie
                Console.WriteLine("Elément cherché (rien pour arrêter) : ");
                // odczyt i weryfikacja odpowiedzi
                réponse = Console.ReadLine().Trim();
                // koniec?
                if (réponse.Equals(""))
                    break;
                // sprawdzenie
                try {
                    élément = Double.Parse(réponse);
                    erreur = false;
                } catch {
                    Console.WriteLine("Erreur, recommencez...");
                    erreur = true;
                }//try-catch
                // jeśli nie ma błędu
                if (!erreur) {
                    // szukamy elementu w tablicy
                    if (type == TypeRecherche.dichotomique)
                        // wyszukiwanie dichotomiczne
                        i = Array.BinarySearch(éléments, élément);
                    else
                        // wyszukiwanie liniowe
                        i = Array.IndexOf(éléments, élément);
                    // Wyświetlanie odpowiedzi
                    if (i >= 0)
                        Console.WriteLine("Trouvé en position " + i);
                    else
                        Console.WriteLine("Pas dans le tableau");
                }//if
            }//while
        }
    }
}
  • wiersze 27–62: metoda Saisie pobiera elementy tablicy éléments wprowadzone z klawiatury. Ponieważ nie można z góry określić rozmiaru tablicy (nie znamy jej ostatecznej wielkości), musimy zmieniać jej rozmiar przy każdym nowym elemencie (wiersz 57). Bardziej efektywnym rozwiązaniem byłoby przydzielanie miejsca dla tablicy w grupach po N elementów. Tablica nie jest jednak przeznaczona do zmiany rozmiaru. W tym przypadku lepiej sprawdzi się lista (ArrayList, List<T>).
  • wiersze 75–113: metoda Recherche umożliwia wyszukiwanie w tablicy éléments elementu wpisanego z klawiatury. Sposób wyszukiwania różni się w zależności od tego, czy tablica jest posortowana, czy nie. W przypadku tablicy nieposortowanej przeprowadza się wyszukiwanie liniowe za pomocą metody IndexOf z linii 106. W przypadku tablicy posortowanej przeprowadza się wyszukiwanie dichotomiczne za pomocą metody BinarySearch z linii 103.
  • wiersz 18: sortujemy tablicę éléments. Wykorzystujemy tutaj wariant metody Sort, który ma tylko jeden parametr: tablicę do posortowania. Relacją porządkową stosowaną do porównywania elementów tablicy jest wówczas relacja domyślna dla tych elementów. W tym przypadku elementy są liczbami. Stosowana jest naturalna kolejność liczb.

Wyniki wyświetlane na ekranie są następujące:

Elément (réel) 0 du tableau (rien pour terminer) : 3,6
Elément (réel) 1 du tableau (rien pour terminer) : 7,4
Elément (réel) 2 du tableau (rien pour terminer) : -1,5
Elément (réel) 3 du tableau (rien pour terminer) : -7
Elément (réel) 4 du tableau (rien pour terminer) :
Tableau non trié----------------------------------
3,6
7,4
-1,5
-7
Elément cherché (rien pour arrêter) :
7,4
Trouvé en position 1
Elément cherché (rien pour arrêter) :
0
Pas dans le tableau
Elément cherché (rien pour arrêter) :

Tableau trié--------------------------------------
-7
-1,5
3,6
7,4
Elément cherché (rien pour arrêter) :
7,4
Trouvé en position 3
Elément cherché (rien pour arrêter) :
0
Pas dans le tableau
Elément cherché (rien pour arrêter) :

5.4. Kolekcje generyczne

Oprócz tablicy istnieją różne klasy służące do przechowywania zbiorów elementów. W przestrzeni nazw System.Collections.Generic znajdują się wersje generyczne, a w przestrzeni nazw System.Collections — wersje niegeneryczne. Przedstawimy dwie często używane kolekcje generyczne: listę i słownik.

Lista kolekcji generycznych przedstawia się następująco:

Image

5.4.1. Klasa generyczna List<T>

Klasa System.Collections.Generic.List<T> umożliwia implementację kolekcji obiektów typu T, których rozmiar zmienia się w trakcie wykonywania programu. Obiekt typu List<T> obsługuje się niemal tak samo jak tablicę. W ten sposób element i listy l zapisuje się jako l[i].

Istnieje również niegeneryczny typ listy: ArrayList, który może przechowywać odwołania do dowolnych obiektów. ArrayList jest funkcjonalnie równoważny z List<Object>.. Obiekt typu ArrayList wygląda następująco:

W powyższym przykładzie elementy 0, 1 i i na liście odwołują się do obiektów różnych typów. Obiekt musi zostać najpierw utworzony, zanim jego odwołanie zostanie dodane do listy ArrayList. Chociaż obiekt typu ArrayList przechowuje odwołania do obiektów, możliwe jest również przechowywanie w nim liczb. Odbywa się to za pomocą mechanizmu o nazwie Boxing: liczba jest enkapsulowana w obiekcie O typu Object, a to właśnie odwołanie do obiektu O jest zapisywane na liście. Jest to mechanizm przezroczysty dla programisty. Można zatem zapisać:

ArrayList liste=new ArrayList();
liste.Add(4);

Da to następujący wynik:

W powyższym przykładzie liczba 4 została zamknięta w obiekcie O, a odwołanie do obiektu O jest przechowywane na liście. Aby je odzyskać, można zapisać:


            int i = (int)liste[0];

Operacja Object -> int nosi nazwę Unboxing. Jeśli lista składa się wyłącznie z typów int, zadeklarowanie jej jako List<int> poprawia wydajność. W rzeczywistości liczby typu int są wtedy przechowywane w samej liście, a nie w typach Object poza listą. Operacje boxing / unboxing nie mają już miejsca.

W przypadku obiektu typu List<T> lub T, który jest klasą, lista ponownie przechowuje odwołania do obiektów typu T:

Oto niektóre właściwości i metody list generycznych:

Właściwości

public int Count {get;}
nombre d'éléments de la liste
public int Capacity {get;}
nombre d'éléments que la liste peut contenir avant d'être redimensionnée. Ce
redimensionnement se fait automatiquement. Cette notion de capacité de liste
est analogue à celle de capacité décrite pour la classe StringBuilder paragraphe Erreur : source de la référence non trouvée.

Metody

public void Add(T item)
ajoute item à la liste
public int BinarySearch<T>(T item)
rend la position de item dans la liste s'il s'y
trouve sinon un nombre <0
public int BinarySearch<T>(T item,
IComparer<T> comparateur)
idem mais le 2ième paramètre permet de comparer deux
éléments de la liste. L'interface IComparer<T> a été
présentée au paragraphe Erreur : source de la référence non trouvée.
public void Clear()
supprime tous les éléments de la liste
public bool Contains(T item)
rend True si item est dans la liste, False sinon
public void CopyTo(T[] tableau)
copie les éléments de la liste dans tableau.
public int IndexOf(T item)
rend la position de item dans tableau ou -1 si
valeur n'est pas trouvée.
public void Insert(T item, int index)
insère item à la position index de la liste
public bool Remove(T item)
supprime item de la liste. Rend True si l'opération
réussit, False sinon.
public void RemoveAt(int index)
supprime l'élément n° index de la liste
public void Sort(IComparer<T> comparateur)
trie la liste selon un ordre défini par comparateur.
 Cette méthode a été présentée paragraphe Erreur : source de la référence non trouvée.
public void Sort()
trie la liste selon l'ordre défini par le type des
éléments de la liste
public T[] ToArray()
rend les éléments de la liste sous forme de tableau

Wróćmy do omówionego wcześniej przykładu z obiektem typu Array i przetwórzmy go teraz przy użyciu obiektu typu List<T>.. Ponieważ lista jest obiektem zbliżonym do tablicy, kod zmienia się nieznacznie. Przedstawiamy jedynie istotne zmiany:


using System;
using System.Collections.Generic;

namespace Chap3 {
    class Program {
        // rodzaj wyszukiwania
        enum TypeRecherche { linéaire, dichotomique };

        // metoda główna
        static void Main(string[] args) {
            // odczyt elementów listy wpisanych z klawiatury
            List<double> éléments;
            Saisie(out éléments);
            // liczba elementów
            Console.WriteLine("La liste a {0} éléments et une capacité de {1} éléments", éléments.Count, éléments.Capacity);
            // wyświetlanie nieposortowanej listy
            Affiche("Liste non triée", éléments);
            // Wyszukiwanie liniowe w nieposortowanej liście
            Recherche(éléments, TypeRecherche.linéaire);
            // sortowanie listy
            éléments.Sort();
            // wyświetlanie posortowanej listy
            Affiche("Liste triée", éléments);
            // Wyszukiwanie dychotomiczne na posortowanej liście
            Recherche(éléments, TypeRecherche.dichotomique);
        }

        // wprowadzanie wartości z listy elementów
        // elementy: odwołanie do listy utworzonej przez metodę
        static void Saisie(out List<double> éléments) {
...
            // na początku lista jest pusta
            éléments = new List<double>();
            // pętla wprowadzania elementów listy
            while (!terminé) {
...
                // jeśli nie ma błędu
                if (!erreur) {
                    // kolejny element na liście
                    éléments.Add(élément);
                }
            }//while
        }

        // ogólna metoda wyświetlania elementów obiektu, który można wyliczyć
        static void Affiche<T>(string texte, IEnumerable<T> éléments) {
            Console.WriteLine(texte.PadRight(50, '-'));
            foreach (T élément in éléments) {
                Console.WriteLine(élément);
            }
        }

        // wyszukiwanie elementu na liście
        // elementy: lista liczb rzeczywistych
        // TypeRecherche: dychotomiczna lub liniowa
        static void Recherche(List<double> éléments, TypeRecherche type) {
...
            while (!terminé) {
...
                // jeśli nie ma błędu
                if (!erreur) {
                    // szukamy elementu na liście
                    if (type == TypeRecherche.dichotomique)
                        // wyszukiwanie dychotomiczne
                        i = éléments.BinarySearch(élément);
                    else
                        // wyszukiwanie liniowe
                        i = éléments.IndexOf(élément);
                    // Wyświetlanie odpowiedzi
...
                }//if
            }//while
        }
    }
}
  • wiersze 46–51: metoda generyczna Affiche<T> przyjmuje dwa parametry:
  • pierwszy parametr to tekst do zapisania
  • drugi parametr to obiekt implementujący interfejs generyczny IEnumerable<T>:
1
2
3
4
public interface IEnumerable<T>{
    IEnumerator GetEnumerator();
    IEnumerator<T> GetEnumerator();
}

Konstrukcja foreach( T element in elementy) w wierszu 48 jest poprawna dla każdego obiektu éléments implementującego interfejs IEnumerable. Tablice (Array) i listy (List<T>) implementują interfejs IEnumerable<T>. Dlatego metoda Affiche nadaje się zarówno do wyświetlania tabel, jak i list.

Wyniki działania programu są takie same jak w przykładzie wykorzystującym klasę Array.

5.4.2. Klasa Dictionary<TKey,TValue>

Klasa System.Collections.Generic.Dictionary<TKey,TValue> umożliwia implementację słownika. Słownik można postrzegać jako tablicę o dwóch kolumnach:

klucz
wartość
klucz1
wartość1
klucz2
wartość2
..
...

W klasie Dictionary<TKey,TValue> klucze są typu Tkey, a wartości typu TValue. Klucze są unikalne, c.a.d, co oznacza, że nie mogą istnieć dwa identyczne klucze. Taki słownik mógłby wyglądać następująco, gdyby typy TKey i TValue określały klasy:

Wartość powiązana z kluczem C w słowniku D jest zapisywana w postaci D[C]. Wartość ta jest dostępna zarówno do odczytu, jak i do zapisu. Można zatem zapisać:

1
2
3
4
5
TValue v=...;
TKey c=...;
Dictionary<TKey,TValue> D=new Dictionary<TKey,TValue>();
D[c]=v;
v=D[c];

Jeśli klucz c nie istnieje w słowniku D, zapis D[c] powoduje wygenerowanie wyjątku.

Główne metody i właściwości klasy Dictionary<TKey,TValue> są następujące:

Konstruktory

public Dictionary<TKey,TValue>()
constructeur sans paramètres - construit un dictionnaire vide.
Il existe plusieurs autres constructeurs.

Właściwości

public int Count {get;}
nombre d'entrées (clé, valeur) dans le dictionnaire
public Dictionary<TKey,TValue>.KeyCollection Keys {get;}
collection des clés du dictionnaire.
public Dictionary<TKey,TValue>.ValueCollection Values {get;}
collection des valeurs du dictionnaire.

Metody

public void Add(TKey key, TValue value)
ajoute le couple (key, value) au dictionnaire
public void Clear()
supprime tous les couples du dictionnaire
public bool ContainsKey (TKey key)
rend True si key est une clé du dictionnaire,
False sinon
public bool ContainsValue (TValue value)
rend True si value est une valeur du dictionnaire,
False sinon
public void CopyTo(T[] tableau)
copie les éléments de la liste dans tableau.
public bool Remove(TKey key)
supprime du dictionnaire le couple de clé key.
Rend True si l'opération réussit, False sinon.
public bool TryGetValue(TKey key,
out TValue value)
rend dans value, la valeur associée à la clé key si
cette dernière existe, sinon rend la valeur par
défaut du type TValue (0 pour les nombres, false
pour les booléens, null pour les références d'objet)

Rozważmy następujący przykładowy program:


using System;
using System.Collections.Generic;

namespace Chap3 {
    class Program {
        static void Main(string[] args) {
            // tworzenie słownika <string,int>
            string[] liste = { "jean:20", "paul:18", "mélanie:10", "violette:15" };
            string[] champs = null;
            char[] séparateurs = new char[] { ':' };
            Dictionary<string,int> dico = new Dictionary<string,int>();
            for (int i = 0; i <liste.Length; i++) {
                champs = liste[i].Split(séparateurs);
                dico[champs[0]]= int.Parse(champs[1]);
            }//for
            // liczba elementów w słowniku
            Console.WriteLine("Le dictionnaire a " + dico.Count + " éléments");
            // lista kluczy
            Affiche("[Liste des clés]",dico.Keys);
            // lista wartości
            Affiche("[Liste des valeurs]", dico.Values);
            // lista kluczy i wartości
            Console.WriteLine("[Liste des clés & valeurs]");
            foreach (string clé in dico.Keys) {
                Console.WriteLine("clé=" + clé + " valeur=" + dico[clé]);
            }
            // usuwamy klucz „paul”
            Console.WriteLine("[Suppression d'une clé]");
            dico.Remove("paul");
            // lista kluczy i wartości
            Console.WriteLine("[Liste des clés & valeurs]");
            foreach (string clé in dico.Keys) {
                Console.WriteLine("clé=" + clé + " valeur=" + dico[clé]);
            }
            // wyszukiwanie w słowniku
            String nomCherché = null;
            Console.Write("Nom recherché (rien pour arrêter) : ");
            nomCherché = Console.ReadLine().Trim();
            int value;
            while (!nomCherché.Equals("")) {
                dico.TryGetValue(nomCherché, out value);
                if (value!=0) {
                    Console.WriteLine(nomCherché + "," + value);
                } else {
                    Console.WriteLine("Nom " + nomCherché + " inconnu");
                }
                // kolejne wyszukiwanie
                Console.Out.Write("Nom recherché (rien pour arrêter) : ");
                nomCherché = Console.ReadLine().Trim();
            }//while
        }

        // ogólna metoda wyświetlania elementów typu wyliczalnego
        static void Affiche<T>(string texte, IEnumerable<T> éléments) {
            Console.WriteLine(texte.PadRight(50, '-'));
            foreach (T élément in éléments) {
                Console.WriteLine(élément);
            }
        }

    }
}
  • wiersz 8: tablica typu string, która posłuży do zainicjowania słownika <string,int>
  • wiersz 11: słownik <string,int>
  • wiersze 12–15: jego inicjalizacja na podstawie tablicy o nazwie string z wiersza 8
  • wiersz 17: liczba wpisów w słowniku
  • wiersz 19: klucze słownika
  • wiersz 21: wartości słownika
  • wiersz 29: usunięcie wpisu ze słownika
  • wiersz 41: wyszukiwanie klucza w słowniku. Jeśli nie istnieje, metoda TryGetValue ustawi wartość 0 w value, ponieważ value jest typu liczbowego. Technika ta ma tutaj zastosowanie tylko dlatego, że wiadomo, iż wartość 0 nie występuje w słowniku.

Wyniki wykonania są następujące:

Le dictionnaire a 4 éléments
[Liste des clés]----------------------------------
jean
paul
mélanie
violette
[Liste des valeurs]-------------------------------
20
18
10
15
[Liste des clés & valeurs]
clé=jean valeur=20
clé=paul valeur=18
clé=mélanie valeur=10
clé=violette valeur=15
[Suppression d'une clé]
[Liste des clés & valeurs]
clé=jean valeur=20
clé=mélanie valeur=10
clé=violette valeur=15
Nom recherché (rien pour arrêter) : violette
violette,15
Nom recherché (rien pour arrêter) : x
Nom x inconnu

5.5. Pliki tekstowe

5.5.1. Klasa StreamReader

Klasa System.IO.StreamReader umożliwia odczytanie zawartości pliku tekstowego. W rzeczywistości jest ona w stanie przetwarzać strumienie, które nie są plikami. Oto niektóre z jej właściwości i metod:

Konstruktory

public StreamReader(string path)
construit un flux de lecture à partir du fichier de chemin path. Le
contenu du fichier peut être encodé de diverses façons. Il existe un
constructeur qui permet de préciser le codage utilisé. Par défaut,
c'est le codage UTF-8 qui est utilisé.

Właściwości

public bool EndOfStream {get;}
True si le flux a été lu entièrement

Metody

public void Close()
ferme le flux et libère les ressources allouées pour
sa gestion. A faire obligatoirement après
exploitation du flux.
public override int Peek()
rend le caractère suivant du flux sans le consommer.
Un Peek supplémentaire rendrait donc le même
caractère.
public override int Read()
rend le caractère suivant du flux et avance d'un
caractère dans le flux.
public override int Read(char[] buffer,
int index, int count)
lit count caractères dans le flux et les met dans
buffer à partir de la position index. Rend le nombre
de caractères lus - peut être 0.
public override string ReadLine()
rend la ligne suivante du flux ou null si on était à
la fin du flux.
public override string ReadToEnd()
rend la fin du flux ou "" si on était à la fin du
flux.

Oto przykład:


using System;
using System.IO;

namespace Chap3 {
    class Program {
        static void Main(string[] args) {
            // katalog uruchomieniowy
            Console.WriteLine("Répertoire d'exécution : "+Environment.CurrentDirectory);
            string ligne = null;
            StreamReader fluxInfos = null;
            // odczyt zawartości pliku infos.txt
            try {
                // odczyt 1
                Console.WriteLine("Lecture 1----------------");
                using (fluxInfos = new StreamReader("infos.txt")) {
                    ligne = fluxInfos.ReadLine();
                    while (ligne != null) {
                        Console.WriteLine(ligne);
                        ligne = fluxInfos.ReadLine();
                    }
                }
                // odczyt 2
                Console.WriteLine("Lecture 2----------------");
                using (fluxInfos = new StreamReader("infos.txt")) {
                    Console.WriteLine(fluxInfos.ReadToEnd());
                }
            } catch (Exception e) {
                Console.WriteLine("L'erreur suivante s'est produite : " + e.Message);
            }
        }
    }
}
  • wiersz 8: wyświetla nazwę katalogu wykonawczego
  • wiersze 12, 27: blok try/catch do obsługi ewentualnego wyjątku.
  • wiersz 15: konstrukcja using flux=new StreamReader(...) ułatwia uniknięcie konieczności jawnego zamykania strumienia po jego wykorzystaniu. Zamknięcie to następuje automatycznie po wyjściu z zakresu using.
  • wiersz 15: odczytywany plik nosi nazwę infos.txt. Ponieważ jest to nazwa względna, plik będzie wyszukiwany w katalogu uruchomieniowym wyświetlonym w wierszu 8. Jeśli pliku tam nie ma, zostanie wygenerowany wyjątek, który zostanie obsłużony przez blok try / catch.
  • wiersze 16–20: plik jest odczytywany po kolejnych wierszach
  • wiersz 25: plik jest odczytywany w całości

Plik infos.txt ma następującą treść:

12620:0:0
13190:0,05:631
15640:0,1:1290,5

i umieszczony w następującym folderze projektu C#:

Okazuje się, że bin/Release to plik wykonywalny, który uruchamia się po naciśnięciu klawiszy Ctrl+F5.

Uruchomienie daje następujące wyniki:

1
2
3
4
5
6
7
8
9
Répertoire d'exécution : C:\data\2007-2008\c# 2008\poly\Chap3\07\bin\Release
Lecture 1----------------
12620:0:0
13190:0,05:631
15640:0,1:1290,5
Lecture 2----------------
12620:0:0
13190:0,05:631
15640:0,1:1290,5

Jeśli w wierszu 15 wpiszemy nazwę pliku xx.txt, otrzymamy następujące wyniki:

1
2
3
Répertoire d'exécution : C:\data\2007-2008\c# 2008\poly\Chap3\07\bin\Release
Lecture 1----------------
L'erreur suivante s'est produite : Could not find file 'C:\...\Chap3\07\bin\Release\xx.txt'.

5.5.2. Klasa StreamWriter

Klasa System.IO.StreamReader umożliwia zapisywanie danych do pliku tekstowego. Podobnie jak klasa StreamReader, jest ona w stanie obsługiwać strumienie danych, które nie są plikami. Oto niektóre z jej właściwości i metod:

Konstruktory

public StreamWriter(string path)
construit un flux d'écriture dans le fichier de chemin path. Le
contenu du fichier peut être encodé de diverses façons. Il existe un
constructeur qui permet de préciser le codage utilisé. Par défaut,
c'est le codage UTF-8 qui est utilisé.

Właściwości

public virtual bool AutoFlush
{get;set;}
fixe le mode d'écriture dans le fichier du buffer associé au flux. Si
égal à False, l'écriture dans le flux n'est pas immédiate : il y a
d'abord écriture dans une mémoire tampon puis dans le fichier lorsque la
mémoire tampon est pleine sinon l'écriture dans le fichier est immédiate
(pas de tampon intermédiaire). Par défaut c'est le mode tamponné qui est
utilisé. Le tampon n'est écrit dans le fichier que lorsqu'il est plein ou
bien lorsqu'on le vide explicitement par une opération Flush ou encore
lorsqu'on ferme le flux StreamWriter par une opération Close. Le mode
AutoFlush=False est le plus efficace lorsqu'on travaille avec des
fichiers parce qu'il limite les accès disque. C'est le mode par défaut
pour ce type de flux. Le mode AutoFlush=False ne convient pas à tous les
flux, notamment les flux réseau. Pour ceux-ci, qui souvent prennent place
dans un dialogue entre deux partenaires, ce qui est écrit par l'un des
partenaires doit être immédiatement lu par l'autre. Le flux d'écriture
doit alors être en mode AutoFlush=True.
public virtual string NewLine {get;set;}
les caractères de fin de ligne. Par défaut "\r\n". Pour un système Unix,
il faudrait utiliser "\n".

Metody

public void Close()
ferme le flux et libère les ressources allouées pour sa
gestion. A faire obligatoirement après exploitation du flux.
public override void Flush()
écrit dans le fichier, le buffer du flux, sans attendre qu'il
soit plein.
public virtual void Write(T value)
écrit value dans le fichier associé au flux. Ici T n'est pas
un type générique mais symbolise le fait que la méthode
Write accepte différents types de paramètres (string, int,
object, ...). La méthode value.ToString est utilisée pour
produire la chaîne écrite dans le fichier.
public virtual void WriteLine(T value)
même chose que Write mais avec la marque de fin de ligne
(NewLine) en plus.

Rozważmy następujący przykład:


using System;
using System.IO;

namespace Chap3 {
    class Program2 {
        static void Main(string[] args) {
            // katalog wykonawczy
            Console.WriteLine("Répertoire d'exécution : " + Environment.CurrentDirectory);
            string ligne = null;                        // jedna linijka tekstu
            StreamWriter fluxInfos = null;    // plik tekstowy
            try {
                // utworzenie pliku tekstowego
                using (fluxInfos = new StreamWriter("infos2.txt")) {
                    Console.WriteLine("Mode AutoFlush : {0}", fluxInfos.AutoFlush);
                    // odczyt wiersza wprowadzonego z klawiatury
                    Console.Write("ligne (rien pour arrêter) : ");
                    ligne = Console.ReadLine().Trim();
                    // pętla, dopóki wprowadzony wiersz nie jest pusty
                    while (ligne != "") {
                        // zapis wiersza do pliku tekstowego
                        fluxInfos.WriteLine(ligne);
                        // odczyt nowego wiersza z klawiatury
                        Console.Write("ligne (rien pour arrêter) : ");
                        ligne = Console.ReadLine().Trim();
                    }//while
                }
            } catch (Exception e) {
                Console.WriteLine("L'erreur suivante s'est produite : " + e.Message);
            }
        }
    }
}
  • wiersz 13: ponownie używamy składni using(flux), aby nie musieć jawnie zamykać strumienia za pomocą operacji Close. Zamknięcie to następuje automatycznie po zakończeniu operacji using.
  • dlaczego użyto try / catch w wierszach 11 i 27? w wierszu 13 moglibyśmy podać nazwę pliku w postaci /rep1/rep2/ .../fichier z ścieżką /rep1/rep2/..., która nie istnieje, uniemożliwiając w ten sposób utworzenie pliku fichier. Wówczas zostałby zgłoszony wyjątek. Istnieją również inne możliwe przypadki wyjątków (zapełniony dysk, niewystarczające uprawnienia itp.)

Wyniki wykonania są następujące:

1
2
3
4
5
Répertoire d'exécution : C:\data\2007-2008\c# 2008\poly\Chap3\07\bin\Release
Mode AutoFlush : False
ligne (rien pour arrêter) : 1ère ligne
ligne (rien pour arrêter) : 2ième ligne
ligne (rien pour arrêter) :

Plik infos2.txt został utworzony w folderze bin/Release projektu:

 

5.6. Pliki binarne

Klasy System.IO.BinaryReader i System.IO.BinaryWriter służą do odczytu i zapisu plików binarnych.

Rozważmy następującą aplikację:

// składnia pg tekst bin logs
// odczytujemy plik tekstowy (text) i zapisujemy jego zawartość w pliku binarnym (bin
// plik tekstowy zawiera wiersze w postaci imię : wiek, które zostaną zapisane w strukturze typu string, int
// (logs) to tekstowy plik logów

Plik tekstowy zawiera następującą treść:

1
2
3
4
5
6
7
8
9
paul : 10
helene : 15

jacques : 11
sylvain : 12
xx : -1

xx: yy : zz
xx : yy

Program wygląda następująco:


using System;
using System.IO;

// składnia pg tekst bin logi
// odczytujemy plik tekstowy (text) i zapisujemy jego zawartość w pliku binarnym (bin)
// plik tekstowy zawiera wiersze w postaci „imię: wiek”, które zostaną zapisane w strukturze typu string, int
// (logs) to tekstowy plik dziennika

namespace Chap3 {
    class Program {
        static void Main(string[] arguments) {
            // potrzebne są 3 argumenty
            if (arguments.Length != 3) {
                Console.WriteLine("syntaxe : pg texte binaire log");
                Environment.Exit(1);
            }//if

            // zmienne
            string ligne=null;
            string nom=null;
            int age=0;
            int numLigne = 0;
            char[] séparateurs = new char[] { ':' };
            string[] champs=null;
            StreamReader input = null;
            BinaryWriter output = null;
            StreamWriter logs = null;
            bool erreur = false;
            // odczyt pliku tekstowego – zapis pliku binarnego
            try {
                // otwarcie pliku tekstowego do odczytu
                input = new StreamReader(arguments[0]);
                // otwarcie pliku binarnego do zapisu
                output = new BinaryWriter(new FileStream(arguments[1], FileMode.Create, FileAccess.Write));
                // otwarcie pliku logów w trybie zapisu
                logs = new StreamWriter(arguments[2]);
                // przetwarzanie pliku tekstowego
                while ((ligne = input.ReadLine()) != null) {
                    // kolejny wiersz
                    numLigne++;
                    // pusty wiersz?
                    if (ligne.Trim() == "") {
                        // pomijamy
                        continue;
                    }
                    // wiersz: imię: wiek
                    champs = ligne.Split(séparateurs);
                    // potrzebujemy 2 pól
                    if (champs.Length != 2) {
                        // rejestrujemy błąd
                        logs.WriteLine("La ligne n° [{0}] du fichier [{1}] a un nombre de champs incorrect", numLigne, arguments[0]);
                        // następny wiersz
                        continue;
                    }//if
                    // pierwsze pole nie może być puste
                    erreur = false;
                    nom = champs[0].Trim();
                    if (nom == "") {
                        // rejestrujemy błąd
                        logs.WriteLine("La ligne n° [{0}] du fichier [{1}] a un nom vide", numLigne, arguments[0]);
                        erreur = true;
                    }
                    // drugie pole musi być liczbą całkowitą >=0
                    if (!int.TryParse(champs[1],out age) || age<0) {
                        // rejestrujemy błąd
                        logs.WriteLine("La ligne n° [{0}] du fichier [{1}] a un âge [{2}] incorrect", numLigne, arguments[0], champs[1].Trim());
                        erreur = true;
                    }//if
                    // jeśli nie ma błędu, zapisujemy dane w pliku binarnym
                    if (!erreur) {
                        output.Write(nom);
                        output.Write(age);
                    }
                    // następna linia
                }//while
            }catch(Exception e){
                Console.WriteLine("L'erreur suivante s'est produite : {0}", e.Message);
            } finally {
                // zamknięcie plików
                if(input!=null) input.Close();
                if(output!=null) output.Close();
                if(logs!=null) logs.Close();
            }
        }
    }
}

Przyjrzyjmy się operacjom dotyczącym klasy BinaryWriter:

  • wiersz 34: obiekt BinaryWriter jest otwierany przez operację

            output=new BinaryWriter(new FileStream(arguments[1],FileMode.Create,FileAccess.Write));

Argumentem konstruktora musi być strumień (Stream). W tym przypadku jest to strumień utworzony na podstawie pliku (FileStream), którego treść jest następująca:

  • (ciąg dalszy)
    • nazwę
    • operację do wykonania, w tym przypadku FileMode.Create w celu utworzenia pliku
    • typ dostępu, w tym przypadku FileAccess.Write dla dostępu z prawem zapisu do pliku
  • wiersze 70–73: operacje zapisu
             // zapisujemy dane do pliku binarnego
            output.Write(nom);
            output.Write(age);

Klasa BinaryWriter posiada różne nadpisane metody Write służące do zapisywania różnych typów danych prostych

  • wiersz 81: operacja zamknięcia strumienia
        output.Close();

Trzy argumenty metody Main są przekazywane do projektu (poprzez jego właściwości) [1], a plik tekstowy, który ma zostać przetworzony, umieszczony jest w folderze bin/Release [2]:

Z następującym plikiem [personnes1.txt]:

1
2
3
4
5
6
7
8
9
paul : 10
helene : 15

jacques : 11
sylvain : 12
xx : -1

xx: yy : zz
xx : yy

wyniki wykonania są następujące:

  • w pliku [1], utworzony plik binarny [personnes1.bin] oraz plik dziennika [logs.txt]. Ten ostatni zawiera następującą treść:
1
2
3
La ligne n° [6] du fichier [personnes1.txt] a un âge [-1] incorrect
La ligne n° [8] du fichier [personnes1.txt] a un nombre de champs incorrect
La ligne n° [9] du fichier [personnes1.txt] a un âge [yy] incorrect

Zawartość pliku binarnego [personnes1.bin] zostanie nam podana przez poniższy program. Program ten przyjmuje również trzy argumenty:

// składnia pg bin tekst logi
// odczyt pliku binarnego bin i zapisanie jego zawartości w pliku tekstowym (tekst)
// plik binarny ma strukturę typu string, int
// plik tekstowy zawiera wiersze w postaci: imię : wiek
// logs to plik tekstowy zawierający logi

Wykonujemy więc operację odwrotną. Odczytujemy plik binarny, aby utworzyć plik tekstowy. Jeśli utworzony plik tekstowy jest identyczny z plikiem źródłowym, będziemy mieli pewność, że konwersja tekst → binarny → tekst przebiegła pomyślnie. Kod wygląda następująco:


using System;
using System.IO;

// składnia: pg, bin, tekst, logs
// odczytuje się plik binarny „bin” i zapisuje jego zawartość w pliku tekstowym (tekst)
// plik binarny ma strukturę typu string, int
// plik tekstowy zawiera wiersze w postaci: imię : wiek
// logs to plik tekstowy zawierający logi

namespace Chap3 {
    class Program2 {
        static void Main(string[] arguments) {
            // potrzebne są 3 argumenty
            if (arguments.Length != 3) {
                Console.WriteLine("syntaxe : pg binaire texte log");
                Environment.Exit(1);
            }//if

            // zmienne
            string nom = null;
            int age = 0;
            int numPersonne = 1;
            BinaryReader input = null;
            StreamWriter output = null;
            StreamWriter logs = null;
            bool fini;
            // odczyt pliku binarnego – zapis pliku tekstowego
            try {
                // otwarcie pliku binarnego do odczytu
                input = new BinaryReader(new FileStream(arguments[0], FileMode.Open, FileAccess.Read));
                // otwarcie pliku tekstowego do zapisu
                output = new StreamWriter(arguments[1]);
                // otwarcie pliku logów w trybie zapisu
                logs = new StreamWriter(arguments[2]);
                // przetwarzanie pliku binarnego
                fini = false;
                while (!fini) {
                    try {
                        // odczyt nazwy
                        nom = input.ReadString().Trim();
                        // odczyt wieku
                        age = input.ReadInt32();
                        // zapis do pliku tekstowego
                        output.WriteLine(nom + ":" + age);
                        // następna osoba
                        numPersonne++;
                    } catch (EndOfStreamException) {
                        fini = true;
                    } catch (Exception e) {
                        logs.WriteLine("L'erreur suivante s'est produite à la lecture de la personne n° {0} : {1}", numPersonne, e.Message);
                    }
                }//while
            } catch (Exception e) {
                Console.WriteLine("L'erreur suivante s'est produite : {0}", e.Message);
            } finally {
                // zamknięcie plików
                if (input != null)
                    input.Close();
                if (output != null)
                    output.Close();
                if (logs != null)
                    logs.Close();
            }
        }
    }
}

Przyjrzyjmy się operacjom dotyczącym klasy BinaryReader:

  • wiersz 30: obiekt BinaryReader jest otwierany przez operację

            input=new BinaryReader(new FileStream(arguments[0],FileMode.Open,FileAccess.Read));

Argumentem konstruktora musi być strumień (Stream). W tym przypadku jest to strumień utworzony na podstawie pliku (FileStream), dla którego podajemy:

  • (ciąg dalszy)
    • nazwę
    • operację do wykonania, w tym przypadku FileMode.Open w celu otwarcia istniejącego pliku
    • typ dostępu, w tym przypadku FileAccess.Read dla dostępu do pliku w trybie odczytu
  • wiersze 40, 42: operacje odczytu
nom=input.ReadString().Trim();
age=input.ReadInt32();

Klasa BinaryReader posiada różne metody ReadXX do odczytu różnych typów danych prostych

  • wiersz 60: operacja zamknięcia strumienia
        input.Close();

Jeśli uruchomimy oba programy po kolei, przekształcając personnes1.txt w personnes1.bin, a następnie personnes1.bin w personnes2.txt2, otrzymamy następujące wyniki:

  • w [1] projekt jest skonfigurowany tak, aby uruchamiać drugą aplikację
  • w [2] – argumenty przekazane do Main
  • w pliku [3] znajdują się pliki wygenerowane podczas uruchomienia aplikacji.

Zawartość pliku [personnes2.txt] jest następująca:

1
2
3
4
paul:10
helene:15
jacques:11
sylvain:12

5.7. Wyrażenia regularne

Klasa System.Text.RegularExpressions.Regex umożliwia stosowanie wyrażeń regularnych. Pozwalają one na sprawdzenie formatu ciągu znaków. W ten sposób można zweryfikować, czy ciąg znaków reprezentujący datę ma format dd/mm/rr. W tym celu stosuje się wzorzec i porównuje się ciąg znaków z tym wzorcem. Tak więc w tym przykładzie j, m i a muszą być cyframi. Szablon prawidłowego formatu daty to zatem „\d\d/\d\d/\d\d”, gdzie symbol \d oznacza cyfrę. W szablonie można używać następujących symboli:

Caractère
Opis
\ 
Oznacza następujący znak jako znak specjalny lub dosłowny. Na przykład „n” odpowiada znakowi „n”. „\n” odpowiada znakowi nowego wiersza. Sekwencja „\\” odpowiada znakowi „\”, natomiast „\(" odpowiada znakowi „(”.
^ 
Odpowiada początkowi wprowadzania danych.
$ 
Odpowiada końcowi wprowadzania.
* 
Odpowiada poprzedniemu znakowi zero lub więcej razy. Tak więc „zo*” odpowiada „z” lub „zoo”.
+ 
Odpowiada poprzedniemu znakowi jeden lub więcej razy. Tak więc „zo+” odpowiada „zoo”, ale nie odpowiada „z”.
? 
Odpowiada poprzedniemu znakowi zero lub jeden raz. Na przykład „a?ve?” odpowiada „ve” w słowie „lever”.
.
Odpowiada dowolnemu pojedynczemu znakowi, z wyjątkiem znaku nowej linii.
(modèle) 
Wyszukuje modèle i zapisuje dopasowanie. Odpowiedni podciąg można wyodrębnić z uzyskanej kolekcji Matches za pomocą elementu [0]...[n]. Aby znaleźć dopasowania zawierające znaki w nawiasach ( ), należy użyć „\(" lub „\)”.
x|y
Odpowiada albo x, albo y. Na przykład „z|foot” odpowiada „z” lub „foot”. „(z|f)oo” odpowiada „zoo” lub „foo”.
{n}
n jest liczbą całkowitą nieujemną. Odpowiada dokładnie n razy danemu znakowi. Na przykład „o{2}” nie odpowiada znakowi „o” w słowie „Bob”, ale dwóm pierwszym znakom „o” w słowie „fooooot”.
{n,} 
n jest liczbą całkowitą nieujemną. Odpowiada co najmniej n-krotności danego znaku. Na przykład „o{2,}” nie odpowiada literze „o” w słowie „Bob”, ale wszystkim literom „o” w słowie „fooooot”. „o{1,}” jest równoważne „o+”, a „o{0,}” jest równoważne „o*”.
{n,m} 
m i n to liczby całkowite nieujemne. Odpowiada co najmniej n i co najwyżej m wystąpieniom tego znaku. Na przykład „o{1,3}” odpowiada trzem pierwszym literom „o” w słowie „foooooot”, a „o{0,1}” jest równoważne „o?”.
[xyz] 
Zbiór znaków. Odpowiada jednemu z podanych znaków. Na przykład „[abc]” odpowiada znakowi „a” w słowie „plat”.
[^xyz] 
Zbiór znaków ujemnych. Odpowiada każdemu znakowi, który nie został wymieniony. Na przykład „[^abc]” odpowiada literze „p” w słowie „plat”.
[a-z] 
Zakres znaków. Odpowiada dowolnemu znakowi z podanego zakresu. Na przykład „[a-z]” odpowiada dowolnej małej literze alfabetu od „a” do „z”.
[^m-z] 
Zakres znaków ujemny. Odpowiada dowolnemu znakowi, który nie występuje w podanym ciągu. Na przykład „[^m-z]” odpowiada dowolnemu znakowi, który nie występuje między „m” a „z”.
\b 
Odpowiada granicy reprezentującej słowo, innymi słowy, pozycji między słowem a spacją. Na przykład „er\b” odpowiada „er” w słowie „lever”, ale nie odpowiada „er” w słowie „verbe”.
\B 
Odpowiada granicy, która nie reprezentuje słowa. „en*t\B” odpowiada „ent” w słowie „bien entendu”.
\d 
Odpowiada znakowi reprezentującemu cyfrę. Jest równoważne [0-9].
\D 
Odpowiada znakowi, który nie jest cyfrą. Odpowiada [^0-9].
\f 
Odpowiada znakowi końca strony.
\n 
Odpowiada znakowi nowego wiersza.
\r 
Odpowiada znakowi powrotu karetki.
\s 
Odpowiada dowolnemu znakowi spacji, w tym spacji, tabulacji, znakowi końca strony itp. Odpowiada „[ \f\n\r\t\v]”.
\S 
Odpowiada dowolnemu znakowi spacji niebędącemu spacją. Odpowiada „[^ \f\n\r\t\v]”.
\t 
Odpowiada znakowi tabulacji.
\v 
Odpowiada znakowi pionowego tabulacji.
\w 
Odpowiada dowolnemu znakowi reprezentującemu słowo i zawierającemu podkreślenie. Odpowiada „[A-Za-z0-9_]”.
\W 
Odpowiada dowolnemu znakowi, który nie reprezentuje słowa. Odpowiada „[^A-Za-z0-9_]”.
\num 
Odpowiada num, gdzie num jest dodatnią liczbą całkowitą. Odnosi się do zapisanych powiązań. Na przykład „(.)\1” odpowiada dwóm identycznym, następującym po sobie znakom.
\n
Odpowiada n, gdzie n jest ośmiobitową wartością escape. Ośmiobitowe wartości escape muszą składać się z 1, 2 lub 3 cyfr. Na przykład zarówno „\11”, jak i „\011” odpowiadają znakowi tabulacji. „\0011” jest równoważne „\001” i „1”. Oktalowe wartości escape nie mogą przekraczać 256. W przeciwnym razie w wyrażeniu brane pod uwagę będą tylko dwie pierwsze cyfry. Umożliwia używanie kodów ASCII w wyrażeniach regularnych.
\xn
Odpowiada n, gdzie n jest szesnastkową wartością escape. Wartości escape w systemie szesnastkowym muszą obowiązkowo składać się z dwóch cyfr. Na przykład „\x41” odpowiada literze „A”. „\x041” jest równoważne „\x04” i „1”. Umożliwia stosowanie kodów ASCII w wyrażeniach regularnych.

Element w szablonie może występować w jednej lub wielu instancjach. Rozważmy kilka przykładów dotyczących symbolu \d, który reprezentuje jedną cyfrę:

szablon
znaczenie
\d
cyfra
\d?
0 lub 1 cyfra
\d*
0 lub więcej cyfr
\d+
1 lub więcej cyfr
\d{2}
2 cyfry
\d{3,}
co najmniej 3 cyfry
\d{5,7}
od 5 do 7 cyfr

Wyobraźmy sobie teraz wzorzec, który może opisać oczekiwany format ciągu znaków:

poszukiwany ciąg znaków
model
data w formacie dd/mm/rr
\d{2}/\d{2}/\d{2}
godzina w formacie hh:mm:ss
\d{2}:\d{2}:\d{2}
liczba całkowita bez znaku
\d+
sekwencja spacji, która może być pusta
\s*
liczba całkowita bez znaku, przed którą lub po której mogą występować spacje
\s*\d+\s*
liczba całkowita, która może być ze znakiem i może być poprzedzona lub następować po spacjach
\s*[+|-]?\s*\d+\s*
liczba rzeczywista bez znaku, przed którą lub po której mogą występować spacje
\s*\d+(.\d*)?\s*
liczba rzeczywista, która może być ze znakiem i może być poprzedzona lub następowana przez spacje
\s*[+|]?\s*\d+(.\d*)?\s*
ciąg zawierający słowo juste
\bjuste\b
  

Można określić, w którym miejscu ciągu znaków ma być wyszukiwany wzorzec:

wzór
znaczenie
^wzór
szablon rozpoczyna ciąg
wzór$
szablon kończy ciąg
^wzorzec$
szablon rozpoczyna i kończy ciąg
wzorzec
wzorzec jest wyszukiwany w całym ciągu, zaczynając od jego początku.
wyszukiwany ciąg
wzorzec
ciąg kończący się wykrzyknikiem
!$
ciąg kończący się kropką
\.$
ciąg znaków rozpoczynający się sekwencją //
^//
ciąg zawierający tylko jedno słowo, po którym mogą następować lub przed którym mogą występować spacje
^\s*\w+\s*$
ciąg zawierający dwa słowa, po których mogą następować lub przed którymi mogą występować spacje
^\s*\w+\s*\w+\s*$
ciąg zawierający słowo secret
\bsecret\b

Podzbiory wzorca można „wyodrębnić”. Dzięki temu nie tylko można sprawdzić, czy ciąg znaków odpowiada konkretnemu wzorcowi, ale także wyodrębnić z tego ciągu elementy odpowiadające podzbiorom wzorca, które zostały ujęte w nawiasy. Jeśli więc analizujemy ciąg zawierający datę dd/mm/rr i chcemy dodatkowo wyodrębnić elementy dd, mm, rr z tej daty, użyjemy wzorca (\d\d)/(\d\d)/(\d\d).

5.7.1. Sprawdzanie, czy ciąg znaków odpowiada podanemu wzorcowi

Obiekt typu Regex tworzy się w następujący sposób:

public Regex(string pattern)
construit un objet "expression régulière" à partir d'un modèle passé
en paramètre (pattern)

Po utworzeniu wzorca wyrażenia regularnego można go porównać z ciągami znaków za pomocą metody IsMatch:

public bool IsMatch(string input)
vrai si la chaîne input correspond au modèle de l'expression
régulière

Oto przykład:


using System;
using System.Text.RegularExpressions;

namespace Chap3 {
    class Program {
        static void Main(string[] args) {
            // wyrażenie regularne wzorca
            string modèle1 = @"^\s*\d+\s*$";
            Regex regex1 = new Regex(modèle1);
            // porównanie wzorca z tekstem
            string exemplaire1 = "  123  ";
            if (regex1.IsMatch(exemplaire1)) {
                Console.WriteLine("[{0}] correspond au modèle [{1}]", exemplaire1, modèle1);
            } else {
                Console.WriteLine("[{0}] ne correspond pas au modèle [{1}]", exemplaire1, modèle1);
            }//if
            string exemplaire2 = "  123a  ";
            if (regex1.IsMatch(exemplaire2)) {
                Console.WriteLine("[{0}] correspond au modèle [{1}]", exemplaire2, modèle1);
            } else {
                Console.WriteLine("[{0}] ne correspond pas au modèle [{1}]", exemplaire2, modèle1);
            }//if
        }

    }
}

oraz wyniki wykonania:

[  123  ] correspond au modèle [^\s*\d+\s*$]
[  123a  ] ne correspond pas au modèle [^\s*\d+\s*$]

5.7.2. Wyszukiwanie wszystkich wystąpień wzorca w ciągu znaków

Metoda Matches pozwala pobrać elementy ciągu znaków pasujące do wzorca:

public MatchCollection Matches(string input)
rend la collection des éléments de la chaîne input
correspondant au modèle

Klasa MatchCollection posiada właściwość Count, która określa liczbę elementów w kolekcji. Jeśli résultats jest obiektem typu MatchCollection, to results[i] jest elementem i tej kolekcji i ma typ Match. Klasa Match posiada różne właściwości, w tym następujące:

  • Value: wartość obiektu Match,, czyli elementu odpowiadającego modelowi
  • Index: pozycja, w której element został znaleziony w przeszukiwanym ciągu

Przyjrzyjmy się poniższemu przykładowi:


using System;
using System.Text.RegularExpressions;

namespace Chap3 {
    class Program2 {
        static void Main(string[] args) {
            // wiele wystąpień wzorca w przykładzie
            string modèle2 = @"\d+";
            Regex regex2 = new Regex(modèle2);
            string exemplaire3 = "  123  456  789 ";
            MatchCollection résultats = regex2.Matches(exemplaire3);
            Console.WriteLine("Modèle=[{0}],exemplaire=[{1}]", modèle2, exemplaire3);
            Console.WriteLine("Il y a {0} occurrences du modèle dans l'exemplaire ", résultats.Count);
            for (int i = 0; i < résultats.Count; i++) {
                Console.WriteLine("[{0}] trouvé en position {1}", résultats[i].Value, résultats[i].Index);
            }//for
        }
    }
}
  • wiersz 8: poszukiwany wzorzec to ciąg cyfr
  • wiersz 10: ciąg znaków, w którym szukamy tego wzorca
  • wiersz 11: pobieramy wszystkie elementy z ciągu exemplaire3, które spełniają wzorzec modèle2
  • wiersze 14–16: wyświetla się je

Wyniki wykonania programu są następujące:

1
2
3
4
5
Modèle=[\d+],exemplaire=[  123  456  789 ]
Il y a 3 occurrences du modèle dans l'exemplaire
[123] trouvé en position 2
[456] trouvé en position 7
[789] trouvé en position 12

5.7.3. Pobieranie części wzorca

Podzbiory wzorca można „wyodrębnić”. Dzięki temu nie tylko można sprawdzić, czy ciąg znaków odpowiada konkretnemu wzorcowi, ale także wyodrębnić z tego ciągu elementy odpowiadające podzbiorom wzorca, które zostały ujęte w nawiasy. Jeśli więc analizujemy ciąg zawierający datę dd/mm/rr i chcemy dodatkowo wyodrębnić elementy dd, mm, rr z tej daty, użyjemy wzorca (\d\d)/(\d\d)/(\d\d).

Rozważmy następujący przykład:


using System;
using System.Text.RegularExpressions;

namespace Chap3 {
    class Program3 {
        static void Main(string[] args) {
            // przechwytywanie elementów w szablonie
            string modèle3 = @"(\d\d):(\d\d):(\d\d)";
            Regex regex3 = new Regex(modèle3);
            string exemplaire4 = "Il est 18:05:49";
            // weryfikacja szablonu
            Match résultat = regex3.Match(exemplaire4);
            if (résultat.Success) {
                // egzemplarz odpowiada szablonowi
                Console.WriteLine("L'exemplaire [{0}] correspond au modèle [{1}]",exemplaire4,modèle3);
                // wyświetlane są grupy nawiasów
                for (int i = 0; i < résultat.Groups.Count; i++) {
                    Console.WriteLine("groupes[{0}]=[{1}] trouvé en position {2}",i, résultat.Groups[i].Value,résultat.Groups[i].Index);
                }//for
            } else {
                // egzemplarz nie odpowiada szablonowi
                Console.WriteLine("L'exemplaire[{0}] ne correspond pas au modèle [{1}]", exemplaire4, modèle3);
            }
        }
    }
}

Wykonanie tego programu daje następujące wyniki:

1
2
3
4
5
L'exemplaire [Il est 18:05:49] correspond au modèle [(\d\d):(\d\d):(\d\d)]
groupes[0]=[18:05:49] trouvé en position 7
groupes[1]=[18] trouvé en position 7
groupes[2]=[05] trouvé en position 10
groupes[3]=[49] trouvé en position 13

Nowość znajduje się w wierszach 12–19:

  • wiersz 12: ciąg znaków exemplaire4 jest porównywany z wzorcem regex3 za pomocą metody Match. Metoda ta zwraca obiekt Match, który został już wcześniej przedstawiony. W tym miejscu wykorzystujemy dwie nowe właściwości tej klasy:
  • Success (wiersz 13): wskazuje, czy znaleziono dopasowanie
  • Groups (wiersze 17, 18): zbiór, w którym
    • Groups[0] odpowiada części ciągu znaków zgodnej z wzorcem
    • Groups[i] (i>=1) odpowiada grupie nawiasów o numerze i

Jeśli résultat jest typu Match, to résultats.Groups jest typu GroupCollection, a résultats.Groups[i] jest typu Group. Klasa Group posiada dwie właściwości, z których tutaj korzystamy:

  • Value (wiersz 18): wartość obiektu Group, który jest elementem odpowiadającym zawartości nawiasu
  • Index (wiersz 18): pozycja, w której element został znaleziony w analizowanym ciągu

5.7.4. Program do nauki

Znalezienie wyrażenia regularnego, które pozwala sprawdzić, czy ciąg znaków rzeczywiście odpowiada określonemu wzorcowi, bywa prawdziwym wyzwaniem. Poniższy program pozwala poćwiczyć tę umiejętność. Wymaga on podania wzorca i ciągu znaków, a następnie wskazuje, czy ciąg znaków odpowiada wzorcowi, czy nie.


using System;
using System.Text.RegularExpressions;

namespace Chap3 {
    class Program4 {
        static void Main(string[] args) {
            // dane
            string modèle, chaine;
            Regex regex = null;
            MatchCollection résultats;
            // użytkownik jest proszony o podanie szablonów i egzemplarzy, które mają zostać porównane z tym egzemplarzem
            while (true) {
                // wymagane jest podanie wzorca
                Console.Write("Tapez le modèle à tester ou rien pour arrêter :");
                modèle = Console.In.ReadLine();
                // gotowe?
                if (modèle.Trim() == "")
                    break;
                // tworzymy wyrażenie regularne
                try {
                    regex = new Regex(modèle);
                } catch (Exception ex) {
                    Console.WriteLine("Erreur : " + ex.Message);
                    continue;
                }
                // prosimy użytkownika o wskazanie egzemplarzy, które mają zostać porównane z wzorem
                while (true) {
                    Console.Write("Tapez la chaîne à comparer au modèle [{0}] ou rien pour arrêter :", modèle);
                    chaine = Console.ReadLine();
                    // koniec?
                    if (chaine.Trim() == "")
                        break;
                    // przeprowadzamy porównanie
                    résultats = regex.Matches(chaine);
                    // czy operacja zakończyła się powodzeniem?
                    if (résultats.Count == 0) {
                        Console.WriteLine("Je n'ai pas trouvé de correspondances");
                        continue;
                    }//if
                    // wyświetla się elementy odpowiadające szablonowi
                    for (int i = 0; i < résultats.Count; i++) {
                        Console.WriteLine("J'ai trouvé la correspondance [{0}] en position [{1}]", résultats[i].Value, résultats[i].Index);
                        // podelementów
                        if (résultats[i].Groups.Count != 1) {
                            for (int j = 1; j < résultats[i].Groups.Count; j++) {
                                Console.WriteLine("\tsous-élément [{0}] en position [{1}]", résultats[i].Groups[j].Value, résultats[i].Groups[j].Index);
                            }
                        }
                    }
                }
            }
        }
    }
}

Oto przykładowe wyniki działania:

Tapez le modèle à tester ou rien pour arrêter :\d+
Tapez la chaîne à comparer au modèle [\d+] ou rien pour arrêter :123 456 789
J'ai trouvé la correspondance [123] en position [0]
J'ai trouvé la correspondance [456] en position [4]
J'ai trouvé la correspondance [789] en position [8]
Tapez la chaîne à comparer au modèle [\d+] ou rien pour arrêter :
Tapez le modèle à tester ou rien pour arrêter :(\d{2}):(\d\d)
Tapez la chaîne à comparer au modèle [(\d{2}):(\d\d)] ou rien pour arrêter :14:15 abcd 17:18 xyzt
J'ai trouvé la correspondance [14:15] en position [0]
        sous-élément [14] en position [0]
        sous-élément [15] en position [3]
J'ai trouvé la correspondance [17:18] en position [11]
        sous-élément [17] en position [11]
        sous-élément [18] en position [14]
Tapez la chaîne à comparer au modèle [(\d{2}):(\d\d)] ou rien pour arrêter :
Tapez le modèle à tester ou rien pour arrêter :^\s*\d+\s*$
Tapez la chaîne à comparer au modèle [^\s*\d+\s*$] ou rien pour arrêter :   1456
J'ai trouvé la correspondance [   1456] en position [0]
Tapez la chaîne à comparer au modèle [^\s*\d+\s*$] ou rien pour arrêter :
Tapez le modèle à tester ou rien pour arrêter :^\s*(\d+)\s*$
Tapez la chaîne à comparer au modèle [^\s*(\d+)\s*$] ou rien pour arrêter :1456
J'ai trouvé la correspondance [1456] en position [0]
        sous-élément [1456] en position [0]
Tapez la chaîne à comparer au modèle [^\s*(\d+)\s*$] ou rien pour arrêter :abcd 1456
Je n'ai pas trouvé de correspondances
Tapez la chaîne à comparer au modèle [^\s*(\d+)\s*$] ou rien pour arrêter :
Tapez le modèle à tester ou rien pour arrêter :

5.7.5. Metoda Split

Z tą metodą zetknęliśmy się już w klasie String:


public string[] Split(char[] separator)
la chaîne est vue comme une suite de champs séparés par les
caractères présents dans le tableau separator. Le résultat est
le tableau de ces champs

Metoda Split klasy Regex pozwala nam określić separator na podstawie szablonu:


public string[] Split(string input)
La chaîne input est décomposée en champs, ceux-ci étant séparés
par un séparateur correspondant au modèle de l'objet Regex
courant.

Załóżmy na przykład, że w pliku tekstowym znajdują się wiersze o postaci pole1, pole2, …, polen. Pola są oddzielone przecinkiem, ale przed lub po nim mogą występować spacje. Metoda Split z klasy string nie jest w tym przypadku odpowiednia. Rozwiązaniem jest metoda RegEx. Jeśli ligne to odczytany wiersz, pola można uzyskać za pomocą

string[] champs=new Regex(@"s*,\s*").Split(ligne);

jak pokazuje poniższy przykład:


using System;
using System.Text.RegularExpressions;

namespace Chap3 {
    class Program5 {
        static void Main(string[] args) {
            // jeden wiersz
            string ligne = "abc  , def  , ghi";
            // szablon
            Regex modèle = new Regex(@"\s*,\s*");
            // podział wiersza na pola
            string[] champs = modèle.Split(ligne);
            // wyświetlanie
            for (int i = 0; i < champs.Length; i++) {
                Console.WriteLine("champs[{0}]=[{1}]", i, champs[i]);
            }
        }
    }
}

Wyniki wykonania:

1
2
3
champs[0]=[abc]
champs[1]=[def]
champs[2]=[ghi]

5.8. Przykładowa aplikacja – V3

Powracamy do aplikacji omówionej w punktach 3.6 (wersja 1) i 4.10 (wersja 2).

W ostatniej analizowanej wersji obliczenie podatku odbywało się w klasie abstrakcyjnej AbstractImpot:


namespace Chap2 {
    abstract class AbstractImpot : IImpot {

        // przedziały podatkowe niezbędne do obliczenia podatku
        // pochodzą ze źródła zewnętrznego

        protected TrancheImpot[] tranchesImpot;

        // obliczenie podatku
        public int calculer(bool marié, int nbEnfants, int salaire) {
            // obliczenie liczby udziałów
            decimal nbParts;
            if (marié) nbParts = (decimal)nbEnfants / 2 + 2;
            else nbParts = (decimal)nbEnfants / 2 + 1;
            if (nbEnfants >= 3) nbParts += 0.5M;
            // obliczenie dochodu podlegającego opodatkowaniu i współczynnika rodzinnego
            decimal revenu = 0.72M * salaire;
            decimal QF = revenu / nbParts;
            // obliczenie podatku
            tranchesImpot[tranchesImpot.Length - 1].Limite = QF + 1;
            int i = 0;
            while (QF > tranchesImpot[i].Limite) i++;
            // zwrot wyniku
            return (int)(revenu * tranchesImpot[i].CoeffR - nbParts * tranchesImpot[i].CoeffN);
        }//obliczyć
    }//klasa

}

Metoda calculer w wierszu 38 wykorzystuje tablicę tranchesImpot z wiersza 35, która nie została zainicjowana przez klasę AbstractImpot. Dlatego jest to metoda abstrakcyjna i musi zostać odziedziczona, aby mogła być użyteczna. Inicjalizacja ta była wykonywana przez klasę pochodną HardwiredImpot:


using System;

namespace Chap2 {
    class HardwiredImpot : AbstractImpot {

        // tabele danych niezbędnych do obliczenia podatku
        decimal[] limites = { 4962M, 8382M, 14753M, 23888M, 38868M, 47932M, 0M };
        decimal[] coeffR = { 0M, 0.068M, 0.191M, 0.283M, 0.374M, 0.426M, 0.481M };
        decimal[] coeffN = { 0M, 291.09M, 1322.92M, 2668.39M, 4846.98M, 6883.66M, 9505.54M };

        public HardwiredImpot() {
                // tworzenie tabeli przedziałów podatkowych
            tranchesImpot = new TrancheImpot[limites.Length];
                // wypełnianie
            for (int i = 0; i < tranchesImpot.Length; i++) {
                tranchesImpot[i] = new TrancheImpot { Limite = limites[i], CoeffR = coeffR[i], CoeffN = coeffN[i] };
                }
        }
    }// klasa
}// przestrzeń nazw

W powyższym przykładzie dane niezbędne do obliczenia podatku były „na stałe” zapisane w kodzie klasy. Nowa wersja przykładu umieszcza je w pliku tekstowym:

4962:0:0
8382:0,068:291,09
14753:0,191:1322,92
23888:0,283:2668,39
38868:0,374:4846,98
47932:0,426:6883,66
0:0,481:9505,54

Ponieważ przetwarzanie tego pliku może powodować wyjątki, tworzymy specjalną klasę do obsługi tych wyjątków:


using System;

namespace Chap3 {
    class FileImpotException : Exception {
        // kody błędów
        [Flags]
        public enum CodeErreurs { Acces = 1, Ligne = 2, Champ1 = 4, Champ2 = 8, Champ3 = 16 };
    
        // kod błędu
        public CodeErreurs Code { get; set; }

        // konstruktory
        public FileImpotException() {
        }
        public FileImpotException(string message)
            : base(message) {
        }
        public FileImpotException(string message, Exception e)
            : base(message,e) {
        }
    }
}
  • wiersz 4: klasa FileImpotException wywodzi się z klasy Exception. Będzie ona służyć do przechowywania wszystkich błędów występujących podczas przetwarzania pliku tekstowego z danymi.
  • wiersz 7: wyliczenie reprezentujące kody błędów:
    • Acces: błąd dostępu do pliku tekstowego z danymi
    • Ligne: wiersz nie zawiera trzech oczekiwanych pól
    • Champ1: błąd w polu nr 1
    • Champ2: błąd w polu nr 2
    • Champ3: błędne pole nr 3

Niektóre z tych błędów mogą występować łącznie (Champ1, Champ2, Champ3). Dlatego też wyliczenie CodeErreurs zostało opatrzone atrybutem [Flags], co oznacza, że poszczególne wartości wyliczenia muszą być potęgami liczby 2. Błąd w polach 1 i 2 spowoduje wówczas wygenerowanie kodu błędu Champ1 | Champ2.

  • wiersz 10: właściwość automatyczna Code zapamięta kod błędu.
  • wiersz 15: konstruktor umożliwiający utworzenie obiektu FileImpotException poprzez przekazanie mu jako parametru komunikatu o błędzie.
  • wiersz 18: konstruktor umożliwiający utworzenie obiektu FileImpotException poprzez przekazanie mu jako parametrów komunikatu o błędzie oraz wyjątku, który spowodował ten błąd.

Klasa inicjująca tablicę tranchesImpot klasy AbstractImpot ma teraz następujący wygląd:


using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;

namespace Chap3 {
    class FileImpot : AbstractImpot {

        public FileImpot(string fileName) {
            // dane
            List<TrancheImpot> listTranchesImpot = new List<TrancheImpot>();
            int numLigne = 1;
            // wyjątek
            FileImpotException fe = null;
            // odczyt zawartości pliku fileName, wiersz po wierszu
            Regex pattern = new Regex(@"s*:\s*");
            // na początku brak błędów
            FileImpotException.CodeErreurs code = 0;
            try {
                using (StreamReader input = new StreamReader(fileName)) {
                    while (!input.EndOfStream && code == 0) {
                        // bieżący wiersz
                        string ligne = input.ReadLine().Trim();
                        // puste wiersze są pomijane
                        if (ligne == "") continue;
                        // wiersz podzielony na trzy pola oddzielone znakiem:
                        string[] champsLigne = pattern.Split(ligne);
                        // czy mamy 3 pola?
                        if (champsLigne.Length != 3) {
                            code = FileImpotException.CodeErreurs.Ligne;
                        }
                        // konwersje trzech pól
                        decimal limite = 0, coeffR = 0, coeffN = 0;
                        if (code == 0) {
                            if (!Decimal.TryParse(champsLigne[0], out limite)) code = FileImpotException.CodeErreurs.Champ1;
                            if (!Decimal.TryParse(champsLigne[1], out coeffR)) code |= FileImpotException.CodeErreurs.Champ2;
                            if (!Decimal.TryParse(champsLigne[2], out coeffN)) code |= FileImpotException.CodeErreurs.Champ3; ;
                        }
                        // błąd?
                        if (code != 0) {
                            // odnotowujemy błąd
                            fe = new FileImpotException(String.Format("Ligne n° {0} incorrecte", numLigne)) { Code = code };
                        } else {
                            // zapisujemy nowy przedział podatkowy
                            listTranchesImpot.Add(new TrancheImpot() { Limite = limite, CoeffR = coeffR, CoeffN = coeffN });
                            // następny wiersz
                            numLigne++;
                        }
                    }
                }
                // przenosimy listę listImpot do tablicy tranchesImpot
                if (code == 0) {
                    // przenosimy listę listImpot do tabeli tranchesImpot
                    tranchesImpot = listTranchesImpot.ToArray();
                }
            } catch (Exception e) {
                // odnotowano błąd
                fe= new FileImpotException(String.Format("Erreur lors de la lecture du fichier {0}", fileName), e) { Code = FileImpotException.CodeErreurs.Acces };
            }
            // czy należy zgłosić błąd?
            if (fe != null) throw fe;
        }
    }
}
  • wiersz 7: klasa FileImpot wywodzi się z klasy AbstractImpot, podobnie jak w wersji 2 klasa HardwiredImpot.
  • wiersz 9: konstruktor klasy FileImpot służy do zainicjowania pola trancheImpot w jej klasie bazowej AbstractImpot. Jako parametr przyjmuje nazwę pliku tekstowego zawierającego dane.
  • wiersz 11: pole tranchesImpot klasy bazowej AbstractImpot jest tablicą, która musi zostać wypełniona danymi z pliku filename przekazanego jako parametr. Odczyt pliku tekstowego odbywa się sekwencyjnie. Liczbę wierszy poznajemy dopiero po odczytaniu całego pliku. Nie można zatem określić rozmiaru tablicy tranchesImpot. Dane zostaną tymczasowo zapisane w liście generycznej listTranchesImpot.

Przypominamy, że typ TrancheImpot jest strukturą:


namespace Chap3 {
    // przedział podatkowy
    struct TrancheImpot {
        public decimal Limite { get; set; }
        public decimal CoeffR { get; set; }
        public decimal CoeffN { get; set; }
    }
}
  • wiersz 14: fe typu FileImpotException służy do zawarcia ewentualnego błędu przetwarzania pliku tekstowego.
  • wiersz 16: wyrażenie regularne separatora pól w wierszu champ1:champ2:champ3 pliku tekstowego. Pola są oddzielone znakiem: poprzedzonym i następującym po nim dowolną liczbą spacji.
  • wiersz 18: kod błędu w przypadku wystąpienia błędu
  • wiersz 20: przetwarzanie pliku tekstowego za pomocą StreamReader
  • wiersz 21: pętla trwa tak długo, jak długo pozostaje jeszcze wiersz do odczytania i nie wystąpił błąd
  • wiersz 27: odczytany wiersz jest dzielony na pola za pomocą wyrażenia regularnego z wiersza 16
  • wiersze 29–31: sprawdzamy, czy wiersz rzeczywiście zawiera trzy pola – odnotowujemy ewentualny błąd
  • wiersze 33–38: konwersja trzech ciągów znaków na trzy liczby dziesiętne – odnotowuje się ewentualne błędy
  • wiersze 40–43: jeśli wystąpił błąd, generowany jest wyjątek typu FileImpotException.
  • wiersze 44–47: jeśli nie wystąpił błąd, przechodzi się do odczytu następnego wiersza pliku tekstowego po zapisaniu danych z bieżącego wiersza.
  • wiersze 52–55: po wyjściu z pętli while dane z listy ogólnej listTranchesImpot są kopiowane do tablicy tranchesImpot klasy bazowej AbstractImpot. Przypominamy, że taki był zamysł twórcy.
  • wiersze 56–59: obsługa ewentualnego wyjątku. Jest on enkapsulowany w obiekcie typu FileImpotException.
  • wiersz 61: jeśli wyjątek fe z wiersza 18 został zainicjowany, to jest on wywoływany.

Cały projekt w języku C# wygląda następująco:

  • w [1]: cały projekt
  • w [2,3]: właściwości pliku [DataImpot.txt] [2]. Właściwość [Copy to Output Directory] [3] jest ustawiona na „always”. Spowoduje to, że plik [DataImpot.txt] będzie kopiowany do folderu bin/Release (tryb Release) lub bin/Debug (tryb Debug) przy każdym uruchomieniu. To właśnie tam jest on wyszukiwany przez plik wykonywalny.
  • W przypadku pliku [4] postępuje się analogicznie z plikiem [DataImpotInvalide.txt].

Zawartość pliku [DataImpot.txt] jest następująca:

4962:0:0
8382:0,068:291,09
14753:0,191:1322,92
23888:0,283:2668,39
38868:0,374:4846,98
47932:0,426:6883,66
0:0,481:9505,54

Treść pliku [DataImpotInvalide.txt] jest następująca:

a:b:c

Program testowy [Program.cs] nie uległ zmianie: jest to program z wersji 2, paragraf 4.10, z następującą różnicą:


using System;

namespace Chap3 {
    class Program {
        static void Main() {
...
             // utworzenie obiektu IImpot
            IImpot impot = null;
            try {
                // utworzenie obiektu IImpot
                impot = new FileImpot("DataImpot.txt");
            } catch (FileImpotException e) {
                // wyświetlenie błędu
                string msg = e.InnerException == null ? null : String.Format(", Exception d'origine : {0}", e.InnerException.Message);
                Console.WriteLine("L'erreur suivante s'est produite : [Code={0},Message={1}{2}]", e.Code, e.Message, msg == null ? "" : msg);
                // zatrzymanie programu
                Environment.Exit(1);
            }

            // pętla nieskończona
            while (true) {
...
            }//while
        }
    }
}
  • wiersz 8: obiekt impot typu interfejsu IImpot
  • wiersz 11: instancja obiektu impot z obiektem typu FileImpot. Może to spowodować wyjątek, który jest obsługiwany przez blok try / catch w wierszach 9, 12 i 18.

Oto przykłady wykonania:

Z plikiem [DataImpot.txt]

1
2
3
Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :o 2 60000
Impot=4282 euros
Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :

Z nieistniejącym plikiem [xx]

L'erreur suivante s'est produite : [Code=Acces,Message=Erreur lors de la lecture du fichier xx, Exception d'origine : Could not find file 'C:\data\2007-2008\c#2008\poly\Chap3\10\bin\Release\xx'.]

Z plikiem [DataImpotInvalide.txt]

L'erreur suivante s'est produite : [Code=Champ1, Champ2, Champ3,Message=Ligne n° 1 incorrecte]