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:
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
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:
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:
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);
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. | |
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);
tablica ciągów znaków | |
ciąg znaków, który będzie służył jako separator pól | |
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:
| |
|
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:
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:
| |
| |
| |
| |
|
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:
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
Metody
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:
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:

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ć:
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
Metody
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>:
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ć:
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
Właściwości
Metody
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:
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
Właściwości
Metody
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ść:
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:
Jeśli w wierszu 15 wpiszemy nazwę pliku xx.txt, otrzymamy następujące wyniki:
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
Właściwości
Metody
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:
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ść:
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
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
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]:
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ść:
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
Klasa BinaryReader posiada różne metody ReadXX do odczytu różnych typów danych prostych
- wiersz 60: operacja zamknięcia strumienia
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:
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:
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. | |
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 „\)”. | |
Odpowiada albo x, albo y. Na przykład „z|foot” odpowiada „z” lub „foot”. „(z|f)oo” odpowiada „zoo” lub „foo”. | |
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 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*”. | |
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?”. | |
Zbiór znaków. Odpowiada jednemu z podanych znaków. Na przykład „[abc]” odpowiada znakowi „a” w słowie „plat”. | |
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”. | |
Zakres znaków. Odpowiada dowolnemu znakowi z podanego zakresu. Na przykład „[a-z]” odpowiada dowolnej małej literze alfabetu od „a” do „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”. | |
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”. | |
Odpowiada granicy, która nie reprezentuje słowa. „en*t\B” odpowiada „ent” w słowie „bien entendu”. | |
Odpowiada znakowi reprezentującemu cyfrę. Jest równoważne [0-9]. | |
Odpowiada znakowi, który nie jest cyfrą. Odpowiada [^0-9]. | |
Odpowiada znakowi końca strony. | |
Odpowiada znakowi nowego wiersza. | |
Odpowiada znakowi powrotu karetki. | |
Odpowiada dowolnemu znakowi spacji, w tym spacji, tabulacji, znakowi końca strony itp. Odpowiada „[ \f\n\r\t\v]”. | |
Odpowiada dowolnemu znakowi spacji niebędącemu spacją. Odpowiada „[^ \f\n\r\t\v]”. | |
Odpowiada znakowi tabulacji. | |
Odpowiada znakowi pionowego tabulacji. | |
Odpowiada dowolnemu znakowi reprezentującemu słowo i zawierającemu podkreślenie. Odpowiada „[A-Za-z0-9_]”. | |
Odpowiada dowolnemu znakowi, który nie reprezentuje słowa. Odpowiada „[^A-Za-z0-9_]”. | |
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. | |
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. | |
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:
Po utworzeniu wzorca wyrażenia regularnego można go porównać z ciągami znaków za pomocą metody IsMatch:
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:
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:
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:
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:
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:
5.7.5. Metoda Split
Z tą metodą zetknęliśmy się już w klasie String:
|
Metoda Split klasy Regex pozwala nam określić separator na podstawie szablonu:
|
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ą
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:
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:
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]
Z nieistniejącym plikiem [xx]
Z plikiem [DataImpotInvalide.txt]






























