Rozdział 9. Łańcuchy w C#.

Adam Boduch

Łańcuchy w informatyce oznaczają ciąg znaków. Do tej pory pojęcie „ciąg” kojarzyło Ci się zapewne z typem string. Powiedzmy sobie szczerze, że jest to jeden z najczęściej wykorzystywanych typów danych w C#. W tym rozdziale mam zamiar wyłożyć tematykę wykorzystania łańcuchów w C#, również przy użyciu klasy System.StringBuilder. Omówione zostaną podstawowe metody służące do operowania na łańcuchach w środowisku .NET Framework.

1 Typ System.String
     1.1 Unicode w łańcuchach
     1.2 Niezmienność łańcuchów
     1.3 Konstruktory klasy
     1.4 Operacje na łańcuchach
          1.4.1 Porównywanie łańcuchów
          1.4.2 Modyfikacja łańcuchów
               1.4.2.1 Concat()
               1.4.2.2 Contains()
               1.4.2.3 Length
               1.4.2.4 Copy()
               1.4.2.5 Insert()
               1.4.2.6 Join()
               1.4.2.7 Split()
               1.4.2.8 Replace()
               1.4.2.9 Remove()
               1.4.2.10 SubString()
               1.4.2.11 ToUpper(), ToLower()
               1.4.2.12 Trim(), TrimStart(), TrimEnd()
2 Łańcuchy w WinForms
3 Klasa StringBuilder
     3.5 Metody klasy StringBuilder
     3.6 Zastosowanie klasy StringBuilder
4 Formatowanie łańcuchów
5 Specyfikatory formatów
     5.7 Własne specyfikatory formatowania
     5.8 Specyfikatory typów wyliczeniowych
6 Typ System.Char
7 Podsumowanie

Typ System.String

Podstawowym typem danych w środowisku .NET Framework, który umożliwia operacje na łańcuchach, jest System.String. W C# odpowiednikiem tego typu jest string i taki zapis stosuje się najczęściej ze względu na skróconą formę. Każdy język obsługujący platformę .NET posiada własny odpowiednik typu System.String, np. w Delphi jest to po prostu String.

W zamierzchłych czasach Turbo Pascala, który również posiadał obsługę łańcuchów, długość jednego łańcucha mogła wynosić maksymalnie 255 znaków. Nie jest to zbyt praktyczne, gdy musimy przechować w zmiennej naprawdę dużą ilość tekstu. W nowoczesnych językach programowania długość tekstu, jaki możemy przypisać do łańcucha, jest niemal nieograniczona, a praktycznie ogranicza ją tylko ilość pamięci, jaką posiadamy w komputerze. Ilość pamięci, jaka jest rezerwowana na potrzeby danego łańcucha, zależy od jego długości. Wszystko odbywa się automatycznie, nie musimy się martwić o przydzielanie czy zwalnianie pamięci.

Łańcuch w .NET jest właściwie sekwencją (tablicą) znaków typu String.Char połączoną w jedną całość. Do każdego elementu łańcucha możemy odwoływać się jak do elementu tablicy przy pomocy symboli []:

System.String S = "Hello World";
Console.WriteLine(S[0]);

Wspomniana tablica jest indeksowana od zera, co oznacza, że pierwsza litera w łańcuchu posiada indeks 0, druga — 1 itd. Czyli pod konstrukcją S[0] kryje się litera H.

W rzeczywistości taką możliwość zapewnia indekser zadeklarowany w klasie System.String.

W dalszej części rozdziału, mówiąc o typie System.String, będę się posługiwał skróconą nazwą string.

Unicode w łańcuchach

Platforma .NET w całości wspiera system kodowania znaków Unicode. Standard ten w zamierzeniu obejmuje wszystkie języki świata i eliminuje problem tzw. krzaczków. Oznacza to, że łańcuchy, m.in. języka C#, są kodowane w systemie Unicode, co sprawia, że każdy znak zajmuje 2 bajty pamięci. Co więcej — kodowanie Unicode możemy również stosować w kodzie źródłowym, używając np. polskich znaków w nazwie zmiennych:

string gżegżółka;

Jest to bardzo dobre rozwiązanie mające ułatwić pisanie aplikacji wielojęzykowych. Dzięki temu unikamy również problemu kodowania i ewentualnej konwersji systemu znaków.

Niezmienność łańcuchów

Każdą wartość raz przypisaną łańcuchowi możesz zmienić — to fakt. Co więc oznacza, że łańcuchy są niezmienne? Oznacza to, że jeżeli raz stworzyliśmy egzemplarz typu string, to nie będziemy mogli zmienić jego wartości. Nie oznacza to jednak, że poniższy kod spowoduje błąd:

System.String S = "Hello World";
S = "Witaj Świecie";

Jest on jak najbardziej prawidłowy, jednak jego realizacja wymaga zastosowania dwóch egzemplarzy (obiektów) w pamięci komputera. Co prawda kod wygląda tak, jakbyśmy tę wartość modyfikowali, ale w rzeczywistości w pamięci tworzony jest nowy obiekt z nową wartością, która jest zwracana. Stara wartość znajduje się w pamięci i czeka na usunięcie przez mechanizm platformy .NET — garbage collection. Nie należy się więc tym zbytnio przejmować.

Abyś się przekonał, że modyfikowana wartość w rzeczywistości wcale nie ulega modyfikacji, możesz wykonać małe ćwiczenie. Oto prosty fragment kodu:

System.String S = "Hello World";
            
 Console.WriteLine(S.ToUpper());
 Console.WriteLine(S);

Metoda ToUpper() powoduje zamianę wszystkich znaków w łańcuchu na wielkie i zwrócenie nowej, zmodyfikowanej wartości. Pod zmienną S kryje się jednak oryginalna, pierwotnie zadeklarowana wartość. Aby zastąpić dotychczasową wartość, należy użyć oczywiście operatora przypisania:

S = S.ToUpper(); 

Ze względu na to, iż łańcuchy są niezmienne, nie zaleca się przeprowadzania na nich wielu operacji dodawania tekstu, zamiany czy usuwania fragmentów łańcucha. Taki kod będzie po prostu działał niezwykle wolno. Zamiast tego zaleca się użycie klasy StringBuilder.

Klasa System.String jest zaplombowana. Oznacza to, że żadna klasa nie może po niej dziedziczyć.

Przeanalizujmy teraz taką sytuację:

System.String S = "Adam";
S = S.Insert(4, " Boduch").ToUpper().Replace("CH", "SZEK");

Mimo iż ten kod może wydawać się nieco dziwny, jest jak najbardziej prawidłowy. W pierwszej kolejności pod indeksem nr 4 wstawiany jest nowy tekst. To owocuje powstaniem w pamięci nowego, zamienionego obiektu. Następnie wszystkie znaki w takim łańcuchu są zamieniane na wielkie, co znowu tworzy nowy obiekt. Na końcu następuje zamiana znaków i utworzenie nowego obiektu, który zostaje przypisany do zmiennej S. Pozostałe obiekty będą dostępne w pamięci do czasu ich usunięcia przez odśmiecacz pamięci (ang. garbage collection).

Aby bardziej zobrazować całą sytuację, proponuję wkleić do projektu następujący kod:

System.String S = "Adam";

Console.WriteLine(S.Insert(4, " Boduch"));
Console.WriteLine(S.ToUpper());
Console.WriteLine(S.Replace("CH", "SZEK"));
            
// wartość oryginalna
Console.WriteLine(S);

Wszystkie operacje zawarte w tym kodzie będą przeprowadzane na łańcuchu oryginalnym znajdującym się w zmiennej S.

Konstruktory klasy

Aby korzystać z właściwości klasy string, należy jedynie zadeklarować zmienną wskazującą na ten typ danych. Nie ma potrzeby jawnego wywoływania konstruktora tej klasy. Jeżeli jednak zajdzie taka potrzeba, klasa System.String posiada kilka przeciążonych konstruktorów, które mogą przyjąć różne wartości. Oto przykład wywołania konstruktora, który jako argument przyjmuje tablicę znaków char:

char[] charArr = new char[] { 'H', 'e', 'l', 'l', 'o' };
System.String S = new System.String(charArr);

Klasa udostępnia wiele konstruktorów. Ciekawych odsyłam do dokumentacji języka C#.

Operacje na łańcuchach

Klasa System.String udostępnia kilka ciekawych metod służących do operowania na łańcuchach. Zapewne z wielu z nich będziesz nie raz korzystał w trakcie programowania w C#, więc postanowiłem tutaj opisać kilka najciekawszych.

Porównywanie łańcuchów

Zacznijmy od rzeczy najprostszej, czyli od porównywania wartości łańcuchów. Najprościej użyć przeciążonych operatorów != oraz ==, które zadeklarowane zostały w klasie System.String. Czyli porównywanie wartości łańcuchów wygląda tak jak np. porównywanie wartości typów liczbowych:

string s1, s2;

s1 = "Hello";
s2 = "hello";

if (s1 != s2)
{
    Console.WriteLine("Wartości są różne");
}

Taka instrukcja warunkowa zwróci wartość true, ponieważ operator != rozróżnia wielkość liter, która jest różna w zmiennych s1 i s2. Jeżeli chcemy, aby aplikacja ignorowała wielkość znaków, należy skorzystać z metody Compare():

s1 = "Hello";
s2 = "hello";

if (String.Compare(s1, s2, true) == 0)
{
    Console.WriteLine("Wartości są równe");
}

Pierwsze dwa parametry metody Compare() to oczywiście nazwy zmiennych. Trzeci parametr określa, czy różnice w wielkości znaków mają być ignorowane (true), czy też uwzględniane (false). Metoda Compare() zwraca wartość liczbową. Jeżeli jest ona mniejsza od zera, oznacza to, że łańcuch s1 jest mniejszy od s2. Wartość większa od zera oznacza, że s1 jest większy od s2, a zero oznacza, iż są one równe.

Trzeci parametr metody Compare() jest opcjonalny. Jeżeli go nie określimy, metoda domyślnie będzie rozróżniać wielkość znaków.

Porównywanie łańcuchów przy pomocy operatorów == oraz != zapewnia w rzeczywistości mechanizm przeładowania operatorów, który został użyty w klasie System.String.

Istnieje również metoda CompareOrdinal(), której z pewnością będziesz rzadziej używał. Porównuje ona ciągi zgodnie z kodami ASCII przypisanymi do danego znaku; nie odzwierciedla porządku alfabetycznego:

s1 = "hello";
s2 = "HELLO";

Console.WriteLine(String.Compare(s1, s2, true));
Console.WriteLine(String.CompareOrdinal(s1, s2));

Po uruchomieniu takiego kodu na ekranie konsoli zostanie wyświetlone:

0
32

Wartości zwracane przez metodę CompareOrdinal() oznaczają to samo, co te zwracane przez Compare() (tzn. wartość 0 oznacza, iż ciągi są takie same).

Skoro jesteśmy przy temacie porównywania wartości łańcuchów, warto wspomnieć o możliwości porównywania z uwzględnieniem różnych kultur. Służy do tego przeciążona metoda Compare():

s1 = "info";
s2 = "INFO";

Console.WriteLine(String.Compare(s1, s2, true, new CultureInfo("pl-PL")));
Console.WriteLine(String.Compare(s1, s2, true, new CultureInfo("tr-TR")));

Umożliwia ona porównywanie wartości z uwzględnieniem regionu (kultury). W pierwszym wypadku porównywanie odbyło się przy zastosowaniu kultury pl-PL (Polska), a drugie tr-TR (Turcja). Okazuje się, że w Turcji mała i duża litera i ma zupełnie inne znaczenie i takie porównywanie zakończy się wyświetleniem na konsoli:

0
1

Typ CultureInfo() znajduje się w przestrzeni nazw System.Globalization.

Modyfikacja łańcuchów

W tabeli 9.1 zawarłem najważniejsze metody klasy System.String służące do modyfikacji tekstu. Tabela zawiera skrótowy opis ich przeznaczenia; w dalszej części rozdziału zawarłem krótkie przykłady prezentujące działanie każdej z nich.

Tabela 9.1. Główne metody klasy System.String

Funkcja/ProceduraOpis
`Concat()`Łączy ze sobą dwie lub więcej instancji klasy string.
`Contains()`Sprawdza, czy część łańcucha nie znajduje się w podanym łańcuchu.
`Length()`Zwraca ilość znaków znajdujących się w ciągu znakowym.
`Copy()`Tworzy nową instancję ciągu znakowego (kopiuje jego zawartość).
`Insert()`Wstawia tekst w określone miejsce ciągu znakowego.
`Join()`Funkcja łączy kilka elementów tekstowych w jeden.
`Remove()`Funkcja umożliwia usunięcie kawałka ciągu znaków.
`Replace()`Zamienia część ciągu znakowego.
`Split()`Umożliwia rozdzielenie ciągu znaków na mniejsze fragmenty na podstawie podanego znaku.
`SubString()`Wycina część ciągu znaków.
`ToLower()`Zwraca ciąg znaków zapisany małymi literami.
`ToUpper()`Zwraca ciąg znaków zapisany wielkimi literami.
`Trim()`Usuwa wszystkie spacje z początku i z końca ciągu znaków.
`TrimEnd()`Usuwa wszystkie spacje z końca ciągu znaków.
`TrimStart()`Usuwa wszystkie spacje z początku ciągu znaków.

Concat()

Metoda Concat() służy do łączenia ze sobą dwóch lub większej ilości łańcuchów. Jej użycie jest proste, bowiem ogranicza się do podania na liście parametrów łańcuchów, które mają zostać połączone:

s1 = "Hello";
s2 = " World";

Console.WriteLine(String.Concat(s1, s2));

Metoda zwraca wartość połączonych ze sobą łańcuchów.

Moim zdaniem łatwiejszym i przejrzystszym sposobem jest wykorzystanie do tego celu operatora + (dodawanie), który z równie dobrym skutkiem połączy dane ciągi:

Console.WriteLine(s1 + s2);

Contains()

Metoda Contains() może być wykorzystana do wyszukiwania danej frazy w łańcuchu. Zwraca true, jeżeli dana fraza została odnaleziona, lub false jeżeli nie albo łańcuch jest pusty. Należy zwracać uwagę na wielkość znaków, gdyż metoda Contains() je rozróżnia. Oto prosty przykład jej wykorzystania:

string s1 = "Jaś i Małgosia świetną parą byli!";

if (s1.Contains("Małgosia"))
{
    Console.WriteLine("String zawiera słowo \"Małgosia\"");
}

Jeżeli chcemy w łańcuchu tekstowym zawrzeć znak cudzysłowu ("), należy poprzedzić go backslashem (). Znak \ również musi być poprzedzony backslashem, jeżeli ma zostać wyświetlony:
s1 = "C:\Windows\System";

Length

Podobnie jak w przypadku tablic właściwość Length() podaje rozmiar, tak w kontekście użycia z łańcuchami zwraca ilość znajdujących się w nich znaków:

s1 = "Jaś i Małgosia świetną parą byli!";
Console.WriteLine(s1.Length);

Właściwość Length jest jedynie do odczytu, nie można przypisywać do niej żadnych wartości.

Copy()

Metoda Copy() tworzy nową instancję klasy z identyczną zawartością:

s1 = "Jaś i Małgosia świetną parą byli!";
s2 = String.Copy(s1);

Insert()

Metoda Insert() umożliwia wstawienie tekstu w dane miejsce łańcucha. Pierwszym parametrem musi być pozycja (indeks), w jakiej zostanie umieszczony nowy tekst. Drugim parametrem musi być sam tekst do wstawienia. Oto przykład:

s1 = "Jaś i Małgosia świetną parą byli!";
s1 = s1.Insert(s1.Length, " I co im po tym?");
Console.WriteLine(s1);

Join()

Często w trakcie zmagania się z jakimś problemem programistycznym możesz się natknąć na konieczność połączenia elementów tablicy w jeden ciąg. Metoda Join() łączy elementy tablicy w jedną całość na podstawie podanego łącznika (dowolny ciąg znaków). Prosty przykład:

string[] Foo = { "Ala", "ma", "kota" };
string s1;

s1 = String.Join(" ", Foo);
Console.WriteLine(s1);

Zmienna s1 po wykonaniu takiego kodu będzie miała wartość Ala ma kota.

Split()

Metoda Split(), znana zapewne programistom Perla czy PHP, wykonuje czynność odwrotną niż Join(). Rozbija mianowicie łańcuch znaków na podstawie podanego separatora i zapisuje do tablicy kolejne elementy ciągu.

Parametrem metody Split() jest lista separatorów w postaci tablicy typu char. Metoda zwraca tablicę typu string, w której znajdują się odseparowane elementy:

string s1 = "Ala ma kota";
string[] Foo = s1.Split(new char[] { ' ' });

foreach (string Bar in Foo)
{
    Console.WriteLine(Bar);
}       

Console.Read();      

Replace()

Metoda Replace() służy do podmieniania pewnych elementów w łańcuchu na nowe. Jej użycie zaprezentowałem na początku tego rozdziału. Pierwszym parametrem musi być tekst, który ma zostać zastąpiony, a drugim — nowa fraza:

string s1 = "Ala ma kota";

Console.WriteLine(s1.Replace("Ala", "Bartek"));

W wyniku wykonania takiego kodu na ekranie konsoli zostanie wyświetlony napis Bartek ma kota.

Remove()

Czasem może zaistnieć potrzeba usunięcia danej frazy łańcucha. Wówczas z pomocą przychodzi metoda Remove(). Pierwszym parametrem musi być indeks, od którego metoda rozpocznie usuwanie tekstu. To jest właściwie jedyny wymagany przez nią parametr. Wtedy usunie ona wszystko, co znajduje się za podanym indeksem. Istnieje jednak możliwość określenia ilości tekstu, jaka ma zostać skasowana:

string s1 = "Ala ma kota";

Console.WriteLine(s1.Remove(0, 4));

Taki kod wyświetli na konsoli napis ma kota.

SubString()

Metoda SubString pozwala na skopiowanie części ciągu, począwszy od podanego znaku, i zwrócenie skopiowanej wartości. Jej budowa i zastosowanie są proste. Pierwszym parametrem musi być pozycja, od której rozpocznie się kopiowanie ciągu znaków, a drugim jest liczba znaków do skopiowania:

string s1 = "Ala ma kota";
/* wyświetli napis: Ala */
Console.WriteLine(s1.Substring(0, 4));

ToUpper(), ToLower()

Zastosowanie tych metod jest proste. Pierwsza z nich (ToUpper()) zamienia wszystkie znaki w łańcuchu na wielkie, a druga (ToLower()) na małe.

Trim(), TrimStart(), TrimEnd()

Jeżeli na początku lub/i na końcu łańcucha znajdują się tzw. białe znaki (spacje, znaki nowej linii), możemy je usunąć przy pomocy metody Trim(). Oto przykład:

string s1 = "     Ala ma kota\t\n";
Console.WriteLine(s1.Trim());

Po wykonaniu metody Trim() wartością łańcucha będzie Ala ma kota. Podobnie TrimStart() usuwa białe znaki na początku łańcucha, a TrimEnd() — na jego końcu.

\n w łańcuchu powoduje wstawienie w danym miejscu znaku nowej linii, natomiast \t to znak tabulacji.

Łańcuchy w WinForms

Co prawda biblioteka WinForms nie jest tematem tego rozdziału, ale chciałbym tutaj wspomnieć o kilku jej tekstowych kontrolkach, które wykorzystują mechanizmy łańcuchów.

Musisz sobie uświadomić, że biblioteka WinForms, tak jak cała biblioteka klas .NET Framework, opiera się na klasach. Komponenty (które również są klasami) posiadają właściwości oraz metody, z których duża część jest właśnie typu System.String.

Podstawowe dwa komponenty służące do edycji tekstu to Textbox oraz RichTextBox. Pierwsza z nich jest kontrolką jednoliniową służącą do wpisywania prostych i krótkich notek.
Drugi komponent jest rozbudowaną kontrolką tekstową — miniedytorem tekstu. Może przechowywać długie teksty, wyświetlać zawartość plików tekstowych.

Maksymalną długość tekstu, jaką może przechowywać komponent RichTextBox, definiuje jego właściwość — MaxLength. Domyślnie jest to 2147483647 znaków.

Komponent TextBox może służyć jako kontrolka wieloliniowa, lecz jest to możliwość rzadko używana. Jeżeli mimo wszystko chcesz, aby komponent mógł przechowywać wiele linii tekstu, zmień właściwość Multiline na true.

Za wyświetlanie tekstu w komponencie TextBox odpowiada właściwość Text. Możesz przypisać tekst wyświetlany w kontrolce za pośrednictwem okna Properties lub bezpośrednio w kodzie:

TextBox1.Text = "Witam";

Właściwość Text jest typu System.String, więc automatycznie na jej wartości możemy operować tak jak na zwykłych zmiennych typu string.

W komponencie RichTextBox sprawa jest nieco bardziej skomplikowana, bo i on sam jest bardziej skomplikowany. Zasadniczo mamy możliwość korzystania z właściwości Lines, która w rzeczywistości jest tablicą typu string. Zasadniczo lepszym rozwiązaniem będzie korzystanie z właściwości Text typu System.String. Należy pamiętać, iż w takim wypadku znak \n jest odpowiedzialny za przejście do nowej linii.

Listing 9.1 prezentuje prosty program WinForms służący do wczytywania zawartości pliku tekstowego. Oprócz wczytania zawartości do komponentu RichTextBox program wyświetla ścieżkę do wybranego pliku w komponencie TextBox (rysunek 9.1).

Rysunek 9.1. Program wyświetlający zawartość plików tekstowych
csharp9.1.jpg

Listing 9.1. Program służący do wczytywania zawartości pliku tekstowego

using System;
using System.ComponentModel;
using System.Data;
using System.Windows.Forms;
using System.IO;

namespace WinForms
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void button1_Click(object sender, EventArgs e)
        {
            // wyświetlenie okna „Otwórz”
            openFileDialog1.ShowDialog();

            // przypisanie ścieżki do pliku            
            textBox1.Text = openFileDialog1.FileName;
            // odczytanie zawartości pliku
            FileStream FileStr = (FileStream)openFileDialog1.OpenFile();
            StreamReader Reader = new StreamReader(FileStr);

            richTextBox1.Clear();
            // przypisanie tekstu
            richTextBox1.Text = Reader.ReadToEnd();
        }
    }
}

Do zbudowania interfejsu aplikacji skorzystałem z kontrolki OpenFileDialog, Button, TextBox, Panel oraz oczywiście RichTextBox.

Klasa StringBuilder

Klasa StringBuilder (w odróżnieniu od System.String) reprezentuje zmienny łańcuch. Stosowanie klasy StringBuilder jest szczególnie zalecane w sytuacjach, w których musisz często modyfikować łańcuchy w swoim programie (np. w pętli). Klasa StringBuilder została zdefiniowana w przestrzeni nazw System.Text.

Do utworzenia obiektu tej klasy można skorzystać z jednego z kilku przeciążonych konstruktorów. W zależności od rodzaju konstruktora będziesz musiał podać wartości dla następujących parametrów:

*capacity — początkowy rozmiar tablicy znaków. Domyślnie jest to 16 znaków. Jeżeli okaże się, że rozmiar tablicy jest zbyt mały, by zrealizować dane zadanie, klasa StringBuilder podwaja tę liczbę. Ze względów wydajnościowych warto jest nadać już w konstruktorze rozmiar, który bez zwiększania wystarczy do wykonania danej operacji.
*length — długość łańcucha przekazanego w konstruktorze (wyrażona w znakach).
*value — początkowa wartość (łańcuch znaków) przekazana do klasy.

Metody klasy StringBuilder

W tabeli 9.2 znajdują się najważniejsze metody klasy StringBuilder.

Tabela 9.2. Najważniejsze metody klasy StringBuilder

MetodaOpis
`Append()`Metoda służy do dodawania tekstu na końcu łańcucha utrzymywanego przez klasę StringBulider.
`AppendFormat()`Metoda o podobnym działaniu do Append(). Jej dodatkowym atutem jest możliwość przekazania tzw. specyfikatorów formatu.
`AppendLine()`Umożliwia wstawienie znaku końca linii.
`Insert()`Umożliwia wstawienie łańcuchowej reprezentacji danego obiektu we wskazanym miejscu.
`Remove()`Usuwa wskazaną część łańcucha.
`Replace()`Umożliwia podmianę części łańcucha.

Zastosowanie klasy StringBuilder

Wspominałem wcześniej o tym, iż klasa StringBuilder (w przeciwieństwie do String) reprezentuje zmienny łańcuch. W sytuacjach gdy musimy często modyfikować wartość danego łańcucha, ze względów wydajnościowych zalecane jest używanie klasy StringBuilder (listing 9.2).

Listing 9.2. Dodawanie wartości do ciągu tekstowego

using System;
using System.Diagnostics;
using System.Text;

namespace FooConsole
{
    class Program
    {
        static void Main(string[] args)
        {
            Stopwatch Timer = new Stopwatch();
            string Bar = "";

            // rozpoczynamy odliczanie
            Timer.Start();

            for (int i = 0; i < 10000; i++)
            {
                Bar += "Ala ma kota ";
            }

            Timer.Stop();

            Console.WriteLine("Czas wykonywania: {0} ms", Timer.ElapsedMilliseconds);
            Console.ReadLine();            
        }
    }
}

Ten prosty program dodaje w pętli wartość do zmiennej Bar. Czas potrzebny na wykonanie 10 000 tys. powtórzeń pętli to ok. 9000 milisekund (w przypadku mojego procesora), czyli 9 sekund.
Aby przyspieszyć działanie aplikacji, można skorzystać z klasy StringBuilder, która znajduje się w przestrzeni System.Text. Użycie tej klasy spowoduje przyspieszenie działania aplikacji aż do 3 milisekund! Różnicę w wydajności widać więc bardzo wyraźnie.

Aby skorzystać z klasy StringBuilder, należy utworzyć jej instancję i — oczywiście — dodać przestrzeń nazw System.Text. Przerobiony program z listingu 9.2 zaprezentowany został na listingu 9.3.

Listing 9.3. Program napisany z użyciem klasy StringBuilder

using System;
using System.Diagnostics;
using System.Text;

namespace FooConsole
{
    class Program
    {
        static void Main(string[] args)
        {
            Stopwatch Timer = new Stopwatch();
            StringBuilder Bar = new StringBuilder();

            // rozpoczynamy odliczanie
            Timer.Start();

            for (int i = 0; i < 10000; i++)
            {
                Bar.Append("Ala ma kota ");
            }

            Timer.Stop();

            Console.WriteLine("Czas wykonywania: {0} ms", Timer.ElapsedMilliseconds);
            Console.ReadLine();            
        }
    }
}

Do sprawdzenia czasu wykonywania danego kodu użyłem klasy Stopwatch, która znajduje się w przestrzeni nazw System.Diagnostics. Podstawowe dwie metody tej klasy to Start(), która rozpoczyna „odliczanie”, oraz Stop() oznaczająca zakończenie pomiaru czasu. Czas potrzebny na wykonanie danego kodu możemy odczytać z właściwości ElapsedMilliseconds (czas w milisekundach).

Formatowanie łańcuchów

Nieraz podczas czytania tej książki mogłeś zauważyć, iż stosowałem formatowanie łańcuchów — np.:

Console.WriteLine("Wartość: {0}", Bar);

Chciałbym na chwilę zatrzymać się przy tym zagadnieniu i poświęcić nieco więcej uwagi możliwościom i korzyściom wynikającym z użycia formatowania łańcuchów. Korzyścią jest przede wszystkim przejrzystszy kod. Ale nie tylko, bo jak się za chwilę przekonasz, pojęcie formatowania łańcuchów to coś więcej niż tylko podstawianie wartości w określonym miejscu ciągu.

Owszem — jest to podstawowe zadanie formatowania. W miejsce symboli zastępczych, znajdujących się pomiędzy klamrami — { oraz } — w procesie wykonywania kodu zostaną podstawione wartości odpowiadające danemu indeksowi. Oto przykład:

Console.WriteLine("{0}, lat {1}, {2} cm wzrostu", "Jan Kowalski", 42, 170);

Każdy kolejny parametr metody WriteLine() jest numerowany od zera. Czyli parametr Jan Kowalski ma indeks 0, parametr 42 — indeks 1 itd. Taka instrukcja spowoduje wyświetlenie na ekranie konsoli tekstu Jan Kowalski, lat 42, 170 cm wzrostu.

Pomiędzy klamrami należy umieścić numer indeksu, nic nie stoi na przeszkodzie, aby takie dane wyświetlić w dowolnej kolejności:

Console.WriteLine("{2} cm wzrostu, {1} lat, {0}, ", "Jan Kowalski", 42, 170);

Jeżeli chcemy wyświetlić w konsoli znak { lub }, musimy te symbole zapisać podwójnie: Console.WriteLine("{{Ten tekst będzie ujęty w klamry}}");

Przeanalizujmy kolejny przykład. Symbol zastępczy może zawierać parametr określający rozmiar wyrównania. Jest to liczba całkowita określająca szerokość danego pola oraz wyrównanie do prawej lub lewej strony. Spójrz na listing 9.4.

Listing 9.4. Symbole zastępcze z parametrem wyrównania

using System;

namespace FooConsole
{
    class Program
    {
        public struct User
        {
            public string Name;
            public byte Age;
        }

        static void Main(string[] args)
        {
            User[] Bar = new User[2];

            Bar[0].Name = "Jan Kowalski";
            Bar[0].Age = 32;
            Bar[1].Name = "Piotr Nowak";
            Bar[1].Age = 56;

            for (int i = 0; i < Bar.Length; i++)
            {
                Console.WriteLine("{0,-15} | {1,5}",
                    Bar[i].Name, Bar[i].Age
                );
            }
            Console.ReadLine();            
        }
    }
}

Program jest prosty. Zadeklarowałem w nim dwuelementową tablicę struktur, a następnie w pętli wyświetliłem jej zawartość. W symbolach zastępczych, po przecinku określiłem wartości wyrównania. Wartość -15 oznacza, iż pole będzie posiadać rozmiar w wielkości 15 znaków, a tekst będzie wyrównany do lewej. Wartość dodatnia oznacza wyrównanie do prawej (zobacz rysunek 9.2).

csharp9.2.jpg
Rysunek 9.2. Program prezentujący właściwości wyrównania

Do tej pory prezentowałem zastosowanie formatowania na przykładzie metody WriteLine(). Chciałbym zaznaczyć, że taka możliwość jest dostępna dla każdego łańcucha typu string dzięki statycznej metodzie Format():

string Bar = String.Format("{0}, {1}", "Jan Kowalski", 42);

Specyfikatory formatów

Symbole zastępcze mają o wiele większe zastosowanie niż wspomniana możliwość wyrównywania wartości. Środowisko .NET Framework definiuje zestaw symboli nazwanych specyfikatorami formatów. Umożliwiają one formatowanie wartości liczbowych, daty i czasu oraz wyliczeniowych, według własnego upodobania.

Spójrz na poniższy kod:

int X = 110003242;
Console.WriteLine("{0:X}", X);

Oznacza on wyświetlenie liczby, która jest zapisana w zmiennej X, w postaci szesnastkowej. Symbol X jest specyfikatorem nakazującym prezentację danych w postaci szesnastkowej (heksadecymalnej). Kolejny przykład prezentuje wyświetlenie danej wartości w postaci walutowej:

     int X = 10;
     Console.WriteLine("{0:C}", X); 

Po uruchomieniu takiego kodu na konsoli zostanie wyświetlona wartość walutowa 10,00 zł. W tabeli 9.3 znajdują się specyfikatory dla wartości liczbowych.

Symbol waluty (zł) oczywiście zależy od lokalizacji kraju, w jakim się znajdujemy. W USA taki kod spowodowałby wyświetlenie następującej wartości: $10.00.

Tabela 9.3. Specyfikatory formatów dla wartości liczbowych

SymbolOpis
C lub cWalutowy
D lub dDziesiętny
E lub e Naukowy (wykładniczy)
F lub fStałoprzecinkowy
G lub gOgólny
N lub nLiczbowy
P lub pProcentowy
R lub rZaokrąglony
X lub xSzesnastkowy

Przykład programu używającego specyfikatorów liczbowych znajduje się na listingu 9.5.

Listing 9.5. Przykład zastosowania specyfikatorów

using System;

namespace FooConsole
{
    class Program
    {
        static void Main(string[] args)
        {
            System.Int32 Integer = 10;
            System.Double Decimal = 10.25;
            System.Double Percent = 0.50;

            Console.WriteLine("Walutowy: {0:C}", Integer);
            Console.WriteLine("Dziesiętny: {0:D5}", Integer);
            Console.WriteLine("Naukowy: {0:E}", Integer);
            Console.WriteLine("Stałoprzecinkowy: {0:F2}", Integer);
            Console.WriteLine("Ogólny: {0:G2}", Integer);
            Console.WriteLine("Liczbowy: {0:N2}", Integer);
            Console.WriteLine("Procentowy: {0:P0}", Percent);
            Console.WriteLine("Zaokrąglony: {0:R1}", Decimal);
            Console.WriteLine("Heksadecymalny: {0:X}", Integer);
            Console.ReadLine();            
        }
    }
}

W programie zadeklarowałem 3 przykładowe zmienne, którym nadałem odpowiednie wartości. Przy pomocy odpowiednich specyfikatorów mogę spreparować wyświetlane dane według własnych upodobań. Rezultat działania takiego programu prezentuje rysunek 9.3.

csharp9.3.jpg
Rysunek 9.3. Przykład zastosowania specyfikatorów liczbowych

Zwróć uwagę, iż symbol specyfikatora określa się po znaku dwukropka, zaraz po numerze indeksu symbolu zastępczego:
{<index>:<symbol specyfikatora="specyfikatora">}

Może zauważyłeś, iż w kilku przypadkach po symbolu specyfikatora znajduje się cyfra. Ta cyfra określa precyzję wyświetlanych danych. Np. poniższy kod oznacza, iż liczba będzie wyświetlana z precyzją dwóch miejsc po przecinku:

Console.WriteLine("Stałoprzecinkowy: {0:F2}", Integer);

Własne specyfikatory formatowania

Kiedy okaże się, że standardowe specyfikatory formatów nie spełniają naszych oczekiwań w zakresie formatowania wartości liczbowych, możemy stworzyć i zastosować własne łańcuchy formatowania. Przykładowo, przechowujemy w zmiennej liczbę 100230786 i dla zwiększenia jej czytelności chcemy ją wyświetlić w postaci 100 230 786. Umiejętne wykorzystanie niestandardowych specyfikatorów nie powinno sprawić problemu w realizacji tego zadania. Oto kod:

System.Int32 Integer = 100230786;
Console.WriteLine("{0:### ### ###}", Integer);

Symbol # oznacza dowolną liczbę. Innymi słowy, symbol ten zostanie zastąpiony kolejną cyfrą z liczby przekazanej jako parametr. Kolejne przykłady:

System.Double Integer = 123.345;

// 123.3
Console.WriteLine("{0:#.#}", Integer);
// 123.3450
Console.WriteLine("{0:#.###0}", Integer);

W tabeli 9.4 przedstawiono listę niestandardowych specyfikatorów formatowania.

Tabela 9.4. Niestandardowe specyfikatory formatowania

SymbolOpis
0Symbol zastępczy dla zera
#Symbol zastępczy dla cyfry
.Kropka dziesiętna
,Separator tysiąca
%Symbol zastępczy dla procentów
E0, E+0, E-0, e0, e+0, e-0Notacja naukowa
'AAA', "AAA"Stały łańcuch
;Separator sekcji

Pozostałe znaki Wszystkie inne znaki wykorzystywane w łańcuchach formatowania

Tematyka niestandardowych specyfikatorów formatowania wykracza poza ramy tej książki. Po więcej informacji na ten temat odsyłam do dokumentacji firmy Microsoft na temat platformy .NET.

Specyfikatory typów wyliczeniowych

Dla typów wyliczeniowych środowisko .NET Framework udostępnia cztery specyfikatory przedstawione w tabeli 9.5.

Tabela 9.5. Specyfikatory typów wyliczeniowych

SymbolOpis
G lub gWyświetla wyliczenie w postaci łańcucha. Jeśli nie jest to możliwe, wyświetla wartość całkowitoliczbową.
F lub fWyświetla wyliczenie w postaci łańcucha. Jeśli nie jest to możliwe, wyświetla wartość liczbową. Wyświetla sumę tych wartości — jeśli jest to możliwe, łańcuchy są konkatenowane i oddzielane przecinkami.
D lub dWyświetla zmienną typu wyliczeniowego w postaci wartości liczbowej.
X lub xWyświetla zmienną typu wyliczeniowego w postaci wartości szesnastkowej.

Na listingu 9.6 znajduje się przykładowy program prezentujący w praktyce zastosowanie specyfikatorów zaprezentowanych w tabeli 9.4.

Listing 9.6. Prezentacja specyfikatorów formatowania dla typów wyliczeniowych

using System;

namespace FooConsole
{
    class Program
    {
        static void Main(string[] args)
        {
            DayOfWeek MyDays = DayOfWeek.Friday;

            Console.WriteLine(MyDays.ToString("G"));
            // Wyświetli „Friday”.

            Console.WriteLine(MyDays.ToString("F"));
            // Wyświetli „Friday”.

            Console.WriteLine(MyDays.ToString("D"));
            // Wyświetli „5”.

            Console.WriteLine(MyDays.ToString("X"));
            // Wyświetli „00000005”.

            Console.ReadLine();            
        }
    }
}

Przykład z listingu 9.6 operuje na typie wyliczeniowym DayOfWeek, a konkretnie na jego elemencie Friday. Zwróć również uwagę, w jaki sposób zastosowałem funkcję formatowania łańcuchów. Należy wspomnieć, iż jest to możliwe również w metodzie ToStrnig(). Zauważ także, iż w takim przypadku nie jest konieczne wpisywanie numeru indeksu. Jeżeli metodę ToString() wywołam ze zmiennej MyDays, to formatowaną wartością jest ta znajdująca się w tej zmiennej.

DayOfWeek jest typem wyliczeniowym znajdującym się w przestrzeni System, reprezentującym dni tygodnia. Pierwszym elementem tego typu jest Sunday. Typ wyliczeniowy jest numerowany od zera.

Typ System.Char

Typ char języka C#, będący odpowiednikiem klasy System.Char z CTS, służy do przechowywania pojedynczych znaków typu Unicode. Wartość zmiennej typu char musi być zawarta w apostrofach:

char C = 'A';

Zmienna typu char nie może zawierać więcej znaków niż jeden, gdyż kompilator wskaże błąd Too many characters in character literal.

Wartość przypisywana do zmiennej typu char może być zapisana w formie heksadecymalnej:

char C1 = '\x0058';
char C2 = (char)65;
char C3 = 'A';
char C4 = '\t'; // znak tabulacji

W tabeli 9.6 zebrano kilka ciekawych metod zawartych w klasie System.Char.

Tabela 9.6. Metody klasy System.Char

MetodaOpis
`IsControl()`Zwraca true, jeżeli znak zawarty w zmiennej jest znakiem kontrolnym (np. \t czy \n).
`IsDigit()`Zwraca true, jeżeli znak zawarty w zmiennej jest liczbą.
`IsLetter()`Zwraca true, jeżeli znak zawarty w zmiennej jest literą.
`IsLower()`Zwraca true, jeżeli znak zawarty w zmiennej jest małą literą.
`IsSymbol()`Zwraca true, jeżeli znak zawarty w zmiennej jest symbolem.
`IsWhiteSpace()`Zwraca true, jeżeli znak zawarty w zmiennej można zaliczyć do tzw. białych znaków (spacje, znaki nowej linii itp.).

Podsumowanie

Tematyka operowania na łańcuchach wbrew pozorom jest całkiem rozległa. Ja w tym rozdziale opisałem podstawowe mechanizmy operowania na ciągach znaków, w tym metody służące do modyfikacji łańcuchów. Wspomniałem również o klasie StringBuilder oraz o formatowaniu łańcuchów, co z pewnością pomoże Ci efektywnie wykorzystać możliwości platformy .NET w tym zakresie. Bardziej zaawansowanym operowaniem na łańcuchach jest mechanizm wyrażeń regularnych (ang. regular expression), lecz ta tematyka wykracza poza ramy niniejszej publikacji. Po więcej informacji na ten temat odsyłam do dokumentacji środowiska .NET Framework.

[[C_Sharp/Wprowadzenie|Spis treści]]

[[C_Sharp/Wprowadzenie/Prawa autorskie|©]] Helion 2006. Autor: Adam Boduch. Zabrania się rozpowszechniania tego tekstu bez zgody autora.

2 komentarzy

Tak, ale ten "błąd" jest celowy :) Miał on na celu zobrazowanie tego fragmentu kodu względem poprzedniego o podobnej formule, dającego jednak inny efekt.

Mimo wszystko dzięki za czujność.

W podrozdziale 1.2 Niezmienność łańcuchów jest (według mnie) błąd.
W kodzie:
System.String S = "Adam";

Console.WriteLine(S.Insert(4, " Boduch"));
Console.WriteLine(S.ToUpper());
Console.WriteLine(S.Replace("CH", "SZEK"));

// wartość oryginalna
Console.WriteLine(S);

w linijce:
Console.WriteLine(S.Replace("CH", "SZEK"));

powinno być na przykład:
Console.WriteLine(S.Replace("m", "mcio")); /:)

Jeżeli chcemy "CH" zamienić na "SZEK" to:

  1. Łańcuch S w pierwszej linijce powinien wyglądać:
    System.String S = "Boduch";
  2. W linijce:
    Console.WriteLine(S.Replace("CH", "SZEK"));
    Powinno być "ch" a nie "CH" ponieważ nie znajduje łańcucha.