40-783: Praca z typem String

W dzisiejszym odcinku – typ string. W jaki sposób możemy go tworzyć? jakie najważniejsze metody nam dostarcza? Oprócz stringa, poznamy kilka innych ważnych klas do pracy na ciągach znaków, takich jak: StringBuilder, StrinWriter i StringReader. Później pomówimy sobie o konwertowaniu innych typów na string – skupimy się głównie na metodach ToString oraz String.Format. Poświęcimy trochę czasu “Composite string”, czyli notacji służącej do dokładego opisu w jaki sposób dany obiekt ma być reprezentowany jako ciąg znaków. Na koniec powiemy sobie w jaki sposób możmy się “wpiąć” w C-Sharpową infrastrukturę konwertowania do stringa, czyli o interfejsach IFormattable, IFormatProvider oraz ICustomFormatter.

Planowałem, częstszą publikację, mniejszych “parti” materiału. Jednak narazie mi to zdecydowanie nie wychodzi :) Temat, którym zajmę się dzisiaj jest wbrew pozorom rozległy. Książka T. Covaci zamyka go w dwóch krótkich pod rozdziałach, ale MSDNa można przekopywać całymi godzinami pod tym kątem.

Jeszcze zanim zaczniemy, szybki disclaimer: dzisiaj będzie dużo bezpośrednich cytatów z dokuemtacji ;)

String

Typ string jest właściwie aliasem na System.String. Przechowuje on w sobie ciąg znaków zakodowany w formacie UTF-16. Podobnie jak w Javie string jest immutable a literały znane na etapie kompilacji są przechowywane wewnątrz (w c# naztywamy to miejsce intern pool).

String posiada przeciążone operatory == i != – w przeciwieństwie do Javy, gdzie do porównania wartości w String’u musielibyśmy użyć equals – w C# robią to za nas te przeciążone operatory.

if (firstString == theSameString)
{
	Console.WriteLine("C# overrides == and != operators for strings - shows if two strings are equals");
}

Literały

W C# mamy trzy rodzaje literałów dla stringa: – Quoted string – Verbatim string – Interpolated string

Quoted string, jest to najzwyklejszy literał – tak jak w Javie, zapisywany w cudzysłowiach. Wewnątrz niego można używać tzw. escape sequences – czyli specjalnch kombinacji, które zostaną zamienione na coś innego, np. \n na znak końca linii a \ na \ (pojedyńczy znak).

Verbatim string, albo inaczej @-quoted string jest typem literału w którego zapisie cudzysłów jest poprzedzony znakiem @. Podstawową jego cechą jest to, że nie działają wewnątrz niego literały. Dzięki temu możemy łatwiej zapisywać niektóre rzeczy, jak np. ścieżki do plików w windowsowym formacie, gdzie używa się znaku \, który w zwykłym stringu musiałby być escape’owany. Jedyny problem pojawiłby się gdybyśmy chcieli wewnątrz stringa użyć znaku cudzysłowia – w takim wypadku trzeba po prostu zapisać go podwójnie.

Ciekawostka – znak @ możemy użyć też jeżeli chcemy np nazwać zmienną tak jak jedno ze słów kluczowych języka – wtedy poprzedzamy znakiem @ naszą nazwę i wszystko jest ok, np. @this

Interpolated string, to notacja wprowadzona w C# 6.0. Jest to string w którego zapisie cudzysłów jest poprzedzony znakiem $. Może zawierać w sobie tzw. interpolated-expressions, które zostaną obliczone za każdym razem kiedy taki literał zastosujemy. Jeżeli chcielibyśmy wewnątrz literału użyć znaku { lub } to musimy go podwoić.

string quotedString = "I'm regular quoted String. I can contain escape characters like \\";
string verbatimString = @"I'm verbatim string. I don't need to escape characters like \ \ \ ";
int a = 10;
string interpolatedString = $"I'm interpolated string, i can have interpolated exceptions like= {a}";

Konstruktory

Najczęściej stringi tworzy się na podstawie literału, ale czasem też można wykorzystać do tego konstruktor. klasa string dostarcza nam kilka ciekawych konstruktorów:

  • konstruktor pozwalający nam na incjalizację stringa z tablicy char’ów
  • konstruktor pozwalający nam na inicjalizację stringa z dowolnego fragmentu tablicy char’ów (podajemy indeks początkowy i ile znaków z tablicy chcemy wykorzystać)
  • konstruktor pozwalający nam zainicjalizwoać stringa z dowlonego znaku, który powinien być powtórzony n razy.

Propertiesy i pola

Klasa string dostarcza nam statyczne pole Empty, reprezentujące pustego stringa (przydałoby się w Javie). Posiada ona również read-only properties Length, który powie nam jakiej długości jest dany string. Oprócz tego string zawiera read-only indekser Chars, dzięki któremu możemy stringa trawersować tak jakby to była tablica char’ów.

Metody statyczne

Klasa string posiada kilka przydatnych metod statycznych:

  • Compare – porównuje dwa stringi i zwraca -1, 0, 1 gdy, odpowiednio – pierwszy string jest “mniejszy”, “równy” lub “większy” od drugiego. Metoda ta posiada dużą liczbę przeciążonych wariantów, które potrafią operować na substringach, ignorować wielokość znaków, brać pod uwagę CultureInfo, itd. Do pary mamy jeszcze CompareOrdinal, która działa tak, że porównuje ze sobą każdy kolejny znak w stringu.
  • Concat, Join – Łączy ze sobą stringi, lub stringową reprezetnację przekazanych obiektów – Join wstawi między każdego stringa podany spearator. Istnieje wiele przeciążonych wersji tych metod: pozwalających przyjmować więcej niż jeden obiekt, tablice obiektów, etc.
  • Copy – Zwraca kopię stringa.
  • Equals – Porównuje czy dwa stringi są takie same. Potrafi przyjąć enum StringComparison, który pozwala dokładniej wyspecyfikować w jaki sposób ma zostać dokonane porównanie.
  • Format – Służy do stworzenia stringa na podstawie przekazanego formatu. O formatowaniu obszerniej w dalszej części artykułu.
  • Intern – metoda ta poszuka podanego na wejściu stringa w intern poolu i zwróci do niego referencje. Jeżeli podany string nie istniał w intern poolu to zostanie tam najpierw stworzony.
  • IsNullOrEmpty, IsNullOrWhiteSpace – nazwy metod raczej mówią same za siebie.

Przydatne metody

Oprócz kilku statycznych metod, string zawiera nie mniej użytecznych metod niestatycznych:

  • Clone – wbrew nazwie – nie klonuje. Tak naprawdę zwraca inną referencję do tego samego stringa.
  • CompareTo – zwraca -1, 0, 1 (czy ten string jest “mniejszy”, “równy”, “większy” od przekazanego jako argument), w przeciwieństwie do swojego statycznego brata, nie posiada zbyt wielu wariantów.
  • Contains – sprawdza czy string zawiera w sobie danego substringa
  • CopyTo – kopiuje podany kawałek stringa (od danej pozycji i o podanej długości) do tablicy char
  • EndsWith – sprawdza czy string kończy się zadanym ciągiem znaków.
  • Equals – sprawdza czy podany string jest równy stringowi na rzecz którego wywołujemy metodę. Potrafi przyjąć enuma StringComparison, który pozwala doprecyzować w jaki sposób ma zostać wykonane porównanie
  • IndexOf – zwraca indeks pierwszego wystąpienia znaku lub substringa. Możemy wyspecyfikować, że chcemy szukać tylko w zadanym substringu i możemy podać enuma StringComparison aby doprecyzować warunki wyszukiwania.
  • IndexOfAny – przyjmuje tablicę char i zwraca pozycję pierwszego wystąpienia dowolnego znaku z przekazanej tablicy.
  • Insert – Zwraca nowego stringa z ciągiem znaków podanym jako argument, wstawiawionym pod podanym indeksem.
  • LastIndexOf – analogicznie do IndexOf, ale szuka ostatniego wystąpienia.
  • LastIndexOfAny – znalogicznie do IndexOfAny, ale szuka ostatniego wystąpienia.
  • PadLeft – zwraca stringa wyrównanego spacjami od lewej. Możemy przekazać inny znak, który ma zostać użyty do wyrównania zamiast spacji.
  • PadRight – zwraca stringa wyrównanego spacjami od prawej. Możemy przekazać inny znak, który ma zostać użyty do wyrównania zamiast spacji.
  • Remove – zwraca stringa z usuniętym fragmentem. Fragment specyfikuje się przez podanie indeksu pierwszego znaku do usunięcia – usuwać możemy zadaną ilość znaków, lub do samego końca stringa.
  • Replace – Pozwala nam zastąpić jakiś znak wewnątrz stringa, innym – podanym na wejściu. Oczywiście zwrócimy nową instancję stringa.
  • Split – Umożliwia podzielenie stringa na tablicę substringów. Miejsce podziału jest specyfikowane po przez argument wejściowy w postaci tablicy znaków – każdy ze znaków w tablicy jest miejscem gdzie nastąpi podział. Możemy wspecyfikować na ile maksymalnie elementów ma nastąpić podział – jeżeli stringów będzie więcej to wszystkie nadmiarowe zostaną umieszczone w ostatnim fragmencie.
  • StartsWith – sprawdzi czy string rozpoczyna się danym ciągiem znaków.
  • Substring – zwróci fragment stringa źródłowego od podanego indeksu i podaną długość.
  • ToCharArray – zwraca tablicę char odpowiadającą stringowi, która jest mutable.
  • ToLower – zwraca stringa w którym wszystkie znaki zamieniono na małe listery
  • ToString – zwraca stringową reprezentację danego obiektu (raczej do obsługi innych typów danych, niż stringa, gdyż ta metoda przychodzi nam z klasy object).
  • ToUpper – zwróci stringa w którym wszystkie litery są wielkie.
  • Trim – zwraca stringa który zostanie pozbawiony spacji z początku i końca. Istnieje wersja do której przekazujemy tablicę charów wskazującą jakie znaki zamiast spacji chcielibyśmy trimować.
  • TrimEnd – jak trim ale usuwa tylko z końca
  • TrimStart – jak trim ale usuwa tylko z początku

Inne ważne klasy

Poza stringiem mamy jeszcze trzy inne istotne klasy służące do operowania na ciągach znaków: – StringBuilder – StringWriter – StringReader

StringBuilder

O klasie StringBuilder możemy myśleć jak o stringu w wersji mutable.

Wewnętrznie przechowuje on ciąg znaków w postaci tablicy char. StringBuilder alokuje sobie pewną ilość pamięci na tą tablicę (domyślnie na 16 znaków) i w momencie kiedy pamięć tą przekroczymy – nowa o podwojonej wielkości zostanie zaalokowana. Jeżeli wiemy, że operacja ponownej alokacji może wystąpić bardzo często (bo będziemy często doklejać kolejne ciągi znaków), to możemy za wczasu ustawić większy rozmiar. Możemy tego dokonać na trzy sposoby: – Po przez parametr konstruktora – Po przez wywołanie metody EnsureCapacity – Po przez ustawienie propertiesa Capacity

Jeżeli zachodzi potrzeba zmniejszenia maksymalnego możliwego rozmiaru StringBuildera (domyślnie Int32.MaxValue), to możemy tego dokonać po przez parametr konstruktora. Mamy też property MaxCapacity, ale ono jest read-only.

Jako, że string jest klasą immutable i modyfikacje na nim będą zwracać nową instancję, to powyżej pewnej ilości modyfikacji może być tak, że StringBuilder byłby szybszy. Nie zawsze jednak zawsze możemy określić kiedy wydajność StringBuildera zacznie w znaczący sposób górować nad zwykłym stringiem więc warto wstrzymać się z wymianą wszystkich stringów na StringBuildery w całym projekcie. Innym argumentem za użyciem StringBuildera może być zaistnienie sytuacji w której będziemy dokonywać nieznanej ilości sklejeń ciągów znaków. W takiej sytuacji kompilator nie dokona za nas optymalizacji (w przypadku “statycznej” konkatenacji, kompilator jest w stanie zoptymalizować wyrażenia)

Na niekorzyść StringBuildera świadczy fakt, że nie posiada on metod do wyszukiwania. Możemy to obejść na kilka sposobów, ale każdy z nich ma swoje minusy. W zależności od tego jaki efekt chcielibyśmy uzyskać: – jeżeli nie zależy nam na tym żeby dowiedzieć się pod jakim indeksem znajduje się szukany fragment a jedynie sam fakt występowania, to możemy szukać w stringach które doczepiamy do StringBuildera, jeszcze przed samym ich doczepieniem. – Jeżeli zależy nam konkretnie na indeksie, to możemy przeszukać StringBuildera ręcznie korzystając z indeksera. – Jeżeli potrzebujemy bardziej zaawansowanego API do wyszukiwania to możemy po prostu skonwertować StringBuildera do stringa i użyć tamtejszego API.

Propertiesy

StringBuilder dostarcza nam kilku propertiesów z których można skorzystać:

  • Capacity – Jest to property typu read/write, który podaje nam aktualną ilość znaków które mogą być trzymane w StringBuilderze, po przekroczeniu której nastąpi nowa alokacja.
  • MaxCapacity – Jest to property typu read-only. Domyślnie równe Int32.MaxValue i jest to maksymalna pojemność danej instancji StringBuildera.
  • Length – Property typu read/write. Zawiera aktualną ilość znaków w StringBuilderze – ustawienie wartości mniejszej niż aktualna “przytnie” stringa.
  • Chars – read/write indekser, który pozwala poruszać się po StringBuilderze jak po tablicy charów

Metody

Oto najważniejsze metody z których możemy korzystać używając StringBuildera:

  • Append – dokleja przekazany obiekt/wartość na koniec.
  • AppendFormat – dokleja na koniec stringa z podanym formatem. Przyjmuje albo stringa z tzw. composite format, albo instancję IFormatProvier.
  • ApendLine – dokleja znak końca lini. Istnieje wersja która przyjmuje innego stringa – wtedy ten string zostanie doklejony na koniec a zaraz po nim wstawiony znak końca wiersza.
  • Clear – kasuje całą zawartość StringBuildera
  • CopyTo – podobnie jak CopyTo z klasy string – kopiuje fragment zawratości do wskazanej tablicy char.
  • EnsureCapacity – pozwala zapewnić, że dany StringBuilder będzie w stanie pomieścić minimum n znaków – jeżeli w danym momencie nie jest to możliwe, to pamięć zostanie przealokowana
  • Insert – dodaje do StringBuildera typ/wartość pod wskazanym indeksem.
  • Remove – usuwa fragment StringBuildera od wskazanego indeksu z podaną długością.
  • Replace – Podmienia wszystkie wystąpienia jakiegoś znaku na inny. W innych wersjach tej metody można podmienić cały substring na inny.
  • ToString – zamienia StringBuildera w stringa

StringWriter

Na klasę StringWriter możnaby spojrzeć jak na bardzo wyspecjalizowany rodzaj StringBuildera, który służy do dopisywania się na końcu aktualnego ciągu znaków. Od strony technicznej, StringWriter trzyma w sobie StringBuildera na którym wykonywane są operacje.

Metody

Poniżej kilka metod, które mamy do dyspozycji:

  • Dispose – metoda z interfejsu IDisposable. Służy do tego, żeby zamknąć wszystkie ewentualne zasoby podpiete pod StringWritera. Wspominam o tej klasie raz, że jak coś jest IDisposable to warto to zamknąć jak przestaniemy używać a dwa, że widać tutaj pewne podobieństwo do Javowego Closable. Javowe Closable zostanie zamknięte samo, jeżeli używamy techniki try-with-resources, podobnie IDisposable zostanie zamknięte automatycznie, jeżeli użyjemy using – o czym, ponownie – może kiedy indziej ;)
  • Flush, FlushAsync – opróżnia wewnętrzny bufor w sposób zwykły lub asynchroniczny
  • GetStringBuilder – za pomocą ten metody dostaniemy StringBuildera, który jest podpięty pod daną instancję StringWritera
  • ToString – zwraca stringa na podstawie tego co znajduje się w StringWriterze.
  • Write, WriteAsync – Dopisuje na koniec – zwyczajnie bądź asynchronicznie. Metoda ta ma bardzo wiele przeciążonych wersji pozwalających doklejać różne typy.
  • WriteLine, WriteLineAsync – To samo co Write ale dopisze znak końca lini. Również w wersji zwykłej i asynchronicznej. Również z dużą ilością przeciążonych wersji.

StringReader

StringReader jest klasą, dzięki której możemy w łatwy sposób odczytywać sekwencyjnie ciągi znaków. Podobnie jak StringWriter, przechowuje w sobie StringBuildera na którym pracuje.

Metody

Poniżej kilka metod, które mamy do dyspozycji:

  • Dispose – tak jak w StringWriterze.
  • Peek – zwraca następny znak, ale go nie “konsumuje”
  • Read, ReadAsync – zwraca następny znak, konsumując go i przesuwając się do następnego. Zwyczajnie lub Asynchronicznie
  • ReadBlock, ReadBlockAsync – odczytuje zadany blok znaków z SB do wewnętrznego buforu
  • ReadLine, ReadLineAsync – odczytuje znaki do napotkania znaku nowej lini.
  • ReadToEnd, ReadToEndAsync – zwraca znaki od aktualnej pozycji do samego końca w postaci stringa

Formatowanie

Formatowaniem nazywamy konwersję w której jakiś typ zamieniamy na jego reprezentację wyrażoną ciągiem znaków. Jest to bardzo szeroki temat, dlatego będziemy się w niego zagłębiać po kawałeczku.

Na początek omówimy sobie ogólnie kilka metod, dzięki którym możemy dokonać formatowania:

  • metoda ToString z klasy object
  • statyczna metoda String.Format

Później powiemy sobie o tym jak możemy wpływać na formatowanie za pomocą:

  • Composite string, które pozwala nam zdefiniować szczegóły formatowania
  • Implementacji interfejsu IFormattable, dzięki czemu dany typ może być formatowany również w zależności od ustawień regionalnych
  • Implementacji IFormatProvider i ICustomFormatter, jeżeli chcemy formatować w inny sposób niż domyślna paleta formatów.

Metoda ToString

Najprostszym sposobem na otrzymanie stringowej reprezentacji danego typu jest wywołanie metody ToString. Metoda ta pochodzi z typu object i jej domyślna implementacja zwraca nazwę typu (fully-qualified name), przez co jest tylko odrobinę mniej użyteczna niż jej Javowy domyślny odpowiednik, gdzie dostawaliśmy jeszcze hash code danego obiektu. Z tego powodu bardzo często będziemy chcieli nadpisać domyślną implementację. Wiele standardowych typów już to robi – np. wszystkie primitive values będą zwracać z ToString ciąg znaków reprezentujący jego wartość (np. 2 -> “2”).

Tak jak w Javie – ToString jest bezargumentowy, natomiast bardzo popularne jest przeciążanie ToStringa tak aby mógł przyjmować format.

O formatach będzie dalej, więc króciutko. Format jest to informacja, która doprecyzowuje w jaki sposób dany typ ma być przekonwertowany do stringa. Zazwyczaj format jest podawany w formie stringa – w wypadku metody ToString nazywamy go Formatting Stringiem. Domyślnym formatem jest tzw. “general” – reprezentowany w Formatting Stringu jako “g”. Uznaje się, że wywołanie ToString bezargumentowego i z argumentem wskazującym na format general zwracają ten sam rezultat. Z tego też wynika konwencja, że jeżeli przeciążamy metodę ToString, która przyjmuje format, to minimalnie powinniśmy zapewnić wsparcie dla formatowania general (zazwyczaj będzie tak, że bezargumentowy ToString wywoła wewnętrznie ToString z formatem general). Zakłada się też, że przekazanie null lub pustego stringa w miejsce formatu traktuje się tak samo jakbyśmy przekazali “g”, tak więc i ten przypadek musimy rozpatrywać nadpisując ToString.

Króciutki przykład tej samej wartości użyej z róznymi formatami:

decimal value = 10.3M;
Console.WriteLine(value.ToString("G")); // general format
Console.WriteLine(value.ToString("C")); // currency
Console.WriteLine(value.ToString("P")); // percent

Z pozostałych konwencji dla metody ToString, na które wskazuje dokumentacja na MSDN można wymienić: – Nie powinniśmy zwracać pustego stringa. – Nie powinniśmy zwracać nulla. – Nie powinniśmy wyrzucać wyjątków. – Generalnie wywołanie nie powinno powodować side effectów. – Jeżeli implementujemy jakąkolwiek formę parsowania dla naszego typu to string który dostaniemy z metody ToString powinien być zdatny do przeprasowania spowrotem do naszego typu.

Statyczna metoda String.Format

Głównym zadaniem statycznej metody String.Format jest umożliwienie nam wstawienie stringowej reprezentacji jakiegoś typu wewnątrz innego stringa. Najczęściej wykorzystywana wersja tej metody przyjmuje jako pierwszy argument string formatujący który zawiera w sobie specjalne znaczniki wskazujące, że wewnątrz stringa zostaną użyte inne obiekty. Wszystkie te obiekty przekazujemy jako argumenty po stringu formatującym. Wiele więcej o tej metodzie nie napiszę, gdyż w zasadzie najważniejsze co trzeba zrozumieć używając tej metody jest to w jaki sposób przygotować format string a o tym napiszę więcej już za moment.

char c = 'c';
int i = 1;
float f = 1.4f;

Console.WriteLine(String.Format("Example of using String.Format: {0}, {1}, {2}", c, i, f));

Composite Format String

String, który zawiera wewnątrz siebie specjalne znaczniki, pod które zostanie podstawiona stringowa reprezentacja innych obiektów nazywa się Composite Foramt String. Najczęściej będziemy tej konstrukcji używać w metodzie String.Format, ale używa jej też sporo innych metod, np AppendFormat w StringBuilderze, czy też Write/WriteLine w klasie System.Console.

Najbardziej ogólnie rzecz ujmując – znaczniki wewnątrz Composite Format Stringa muszą mieć postać:

{index[, alignment] [:formatString]}

Indeks jest jedyną obowiązkową częścią znacznika. Wskazuje on, który argument przekazany oprócz formatu (np. w String.Format) ma zostać wstawiony w tym konkretnym miejscu. Nie ma żadnego problemu, żeby więcej niż jeden znacznik odwoływał się do tego samego argumentu.

Parametr alignment mów nam o tym ile “miejsca” (ile znaków) powinna zajmować stringowa reprezentacja tego konkretnego obiektu. Jeżeli wartość jest dodatnia to wartość będzie wyrównana do prawej (czyli padding z lewej strony), jeżeli wartość jest ujemna, to wyrównana będzie do lewej strony (czyli padding z prawej strony)

Format string mówi nam o tym w jaki sposób powinniśmy formatować nasz obiekt. Przyjrzyjmy się temu w kolejnej sekcji.

Formatting String

Formatting String występuje jako ostatnia część znacznika w Composite Format String, ale możemy też przekazać ją do metody ToString – w obu wypadkach rola Formatting stringa jest podobna – precyzuje w jaki sposób chcielibyśmy formatować dany obiekt. Oznacza to, że mając ten sam obiekt, możemy go formatować na wiele różnych sposobów zmieniając tylko Formating String.

Żeby nie było zbyt prosto, Formatting String występuje pod dwiema postaciami: – Standard – dzięki niemu możemy użyć predefiniowanego stylu formatowania. Przykładowo wybierając format dla daty (“d”) dostajemy out of the box jakiś predefiniowany sposób formatowania daty. – Custom – dzięki niemu możemy dużo dokładniej wyspecyfikować w jaki sposób chcielibyśmy formatować naszą wartość. Mamy do dyspozycji bardziej “drobnoziarniste” parametry formatowania – przykładowo formatując datę możemy wybrać, że chcemy wyświetlić tylko rok i miesiąc.

Jeżeli nie podamy Formatting stringa w ogóle, to wybrane domyślnie zostanie formatowanie “g”, czyli general.

“Biblioteka” możliwych do użycia formatów jest olbrzymia, więc pozwolę sobie wymienić tylko ważniejsze z nich.

Standard format dla daty i czasu: – d – “krótka” data – D – “długa” data – t – “krótki” czas – T – “długi” czas – f – “długa” data, plus “krótki” czas – F – “długa” data, plus “długi” czas

Custom format dla daty i czasu: – s – “krótka” sekunda – ss – “długa” sekunda – m – “krótka” minuta – mm – “długa minuta” – h – “krótka” godzina w formacie 12 godzinnym – hh – “długa” godzina w formacie 12 godzinnym – H – “krótka” godzina w formacie 24 godzinnym – HH – “długa” godzina w formacie 24 godzinnym – d – “krótki” dzień miesiąca – dd – “długi” dzień miesiąca – ddd – skrótowa nazwa dnia tygodnia – dddd – pełna nazwa dnia tygodnia – M – “krótki” numer miesiąca – MM – “długi” numer miesiąca – MMM – skrótowa nazwa miesiąca – MMMM – pełna nazwa miesiąca – y – “krótki” dwucyfrowy rok – yy – “długi” dwucyfrowy rok – yyyy – “pełen” czterocyfrowy rok

Zauważmy, że format “d” w custom formacie jest w kolizji z formatem standardowym – jeżeli użyjemy samego “d” to zawsze wybrany będzie standard format – żeby uzyskać samodzielny dzień miesiąca musimy conajmniej użyć spacji ( np. {0:d }), albo użyć conajmniej jeszcze jednego custom formattera.

Standard format dla enumów: – G lub g – jeżeli dla danej wartości istnieje stała w enumie to będzie wypisywać tą stałą, jeśli nie to po prostu wartość – D lub d – zawsze wyświetli enuma jako wartość liczbową

Standard format dla liczb całkowitych:

  • C lub c – waluta
  • D lub d – liczba dziesiętna (tylko inty)
  • E lub e – notacja naukowa
  • F lub f – liczba z częścią dziesiętną i ułamkową
  • N lub n – liczba w formacie respektującym lokalną notację (czyli z odpowiednim separatorem dziesiętnym i ułamkowym)
  • P lub p – procent. mnoży liczbę przez 100 i wstawia znak procenta na koniec.
  • X lub x – wartość heksadecymalna (tylko inty)

Ważne jest też, że zaraz po formacie możemy umieścić liczbę z zakresu 0-99 (np. d2, d15, etc.). Liczba ta mówi nam ile liczb powina zawierać liczba po sformatowaniu.

Custom format dla liczb:

  • 0 – tzw. zero placeholder jeżeli umieścimy np cztery zera a nasza liczba składa się z trzech cyfr, to trzy zera zostaną zamienione na wartość naszej liczby, natomiast początkowe zero zostanie nadal zerem
  • # – tzw. digit placeholder – zachowa się bardzo podobnie jak zero, ale jeżeli liczba będzie krótsza, to nadmiarowe # nie zostaną wyświetlone
  • . – separator dziesiętny
  • , – separator grup (tysięcy)
  • % – tzw. percent placeholder – sprawia, że wartość będzie przemnożona przez 100 a na koniec zostanie wstawiony znak procenta

Interfejs IFormattable

Interfejs IFormattable zawiera w sobię metodę ToString, która przyjmuje formating string i instancję IFormatProvider. Formatting string ma za zadanie zdefiniować ogólne zasady formatowania, natomiast IFormatProvider jest źródłem specyficznych danych, które są związane z lokalnymi zasadami formatowania pewnego rodzaju wartości. Przykładowo jeżeli formtujemy liczbę jako walutę, to IFormatProvider może dostarczyć nam znak waluty charakterystyczny dla jakiegoś regionu.

Jeżeli chcielibyśmy aby nasz typ mógł być formatowany w zależności od ustawień regionalnych, to powinniśmy zaimplementować interfejs IFormattable. W ramach tego interfejsu implementujemy metodę ToString, która przyjmuje formatting string oraz IFormatProvider’a. Instancję tą możemy wykorzystać do sformatowania części naszej klasy wg. ustawień regionalnych. Na MSDN jest podany ciekawy przykład klasy Temperature, która może być formatowana jako wartość w stopniach Celcuisza, Kelivna bądź Farenheita. Za wybór odpowiedniego rodzaju temperatury odpowiada formatting stirng, natomiast IFormatProvider został wykorzystany, żeby sformatować samą wartość liczbową (używając metody ToString).

Przykładowa implementacja klasy, która implementuje IFormattable. Warto zwrócić uwage, że metody ToString, “współpracują” ze sobą:

public class Length : IFormattable
{
	private double value;

	public Length(double meters)
	{
		this.value = meters;
	}

	public double Meters
	{
		get;
	}

	public double Miles
	{
		get
		{
			return value * 0.000621371d;
		}
	}

	public override string ToString()
	{
		return ToString("g", CultureInfo.CurrentCulture);
	}

	public string ToString(string format)
	{
		return ToString(format, CultureInfo.CurrentCulture);
	}

	public string ToString(string format, IFormatProvider formatProvider)
	{
		if (String.IsNullOrEmpty(format))
		{
			format = "G";
		}
		if (formatProvider == null)
		{
			formatProvider = CultureInfo.CurrentCulture;
		}

		switch (format.ToUpperInvariant())
		{
			case "G", "Me":
				return value.ToString("F4", formatProvider) + "meters";
			case "Mi":
				return value.ToString("F4", formatProvider) + "miles";
			default:
				throw new FormatException(String.Format("Format {0} is not supported", format));
		}

	}
}

Oraz użycie:

Length length = new Length(1000000);
CultureInfo english = new CultureInfo("en-GB");
CultureInfo polish = new CultureInfo("pl-PL");

Console.WriteLine(String.Format("Meters, English: {0}", length.ToString("Me", english)));
Console.WriteLine(String.Format("Meters, Polish: {0}", length.ToString("Me", polish)));
Console.WriteLine(String.Format("Miles, English: {0}", length.ToString("Mi", english)));
Console.WriteLine(String.Format("Miles, Polish: {0}", length.ToString("Mi", polish)));

Console.WriteLine(String.Format(“Meters, English: {0}”, length.ToString(“Me”, english))); Console.WriteLine(String.Format(“Meters, Polish: {0}”, length.ToString(“Me”, polish))); Console.WriteLine(String.Format(“Miles, English: {0}”, length.ToString(“Mi”, english))); Console.WriteLine(String.Format(“Miles, Polish: {0}”, length.ToString(“Mi”, polish)));[/csharp]

Jeśli chodzi o samo IFormatProvider to mamy do dyspozycji trzy domyślne implementacje: – NumberFormatInfo – zawiera w sobie informacje potrzebne do formatowania wszelkiego rodzaju wartości liczbowych. – DateTimeFormatInfo – zawiera w sobie informacje potrzebne do formatowania wszelkiego rodzaju dat. – CultureInfo – będzie to prawdopodobnie najczęściej wykorzystywany przez nas typ. Zawiera on w sobie informacje o konkretnej kulturze – jego metoda GetFormat w zależności od potrzeb zwróci albo NumberFormatInfo albo DateTimeFormatInfo.

Możemy dostarczyć własne implementacje IFormatProvidera jeśli chcielibyśmy napisać własne formatowanie. Wtedy tworzymy klasę, która implementuje równocześnie IFormatProvider jak ICustomFormatter, którego metoda Format powinna dokonać właściwego foramtowania.

Interpolated Strings

Interpolated string w zasadzie też jest pewną formą formatowania, dość podobną do tego co robi String.Format i placeholdery w nim przyjmują bardzo podobną postać, z tym, że pierwszym parametrem nie jest indeks tylko nazwa zmiennej która ma zostać użyta. Technicznie rzecz biorąc format wygląda następująco:

{interpolated-expression[, alignment] [:formatString]}

Siłą interpolated strings są ich implicit konwersje:

  1. Do stringa – przypisując interpolated stringa do pola typu string po prostu “wyliczymy” wartość wynikową.
  2. Do IFormattable – przydatne jeżeli chcemy operować stringiem w zgodzie z lokalnym formatowaniem.
  3. Do FormattableString – po przez ten obiekt możemy dokonać inspekcji wartości, które zostały podane do włączenia do stringa. Przykładowo jeżeli string przechowywałby query SQL, to moglibyśmy sprawdzić czy ktoś nie próbuje przeprowadzić ataku SQL Injection.

Podsumowanie

Dzisiaj zgłębiliśmy temat, który może nie jest zbyt ciekawy, ale napewno warto wiedzieć co nieco na jego temat. Nie jest też może bardzo skomplikowany ale dość obszerny.

Na początku omówiliśmy sobie typ string – typ służący do przechowywania ciągów znaków, który jest immutable. Omówiliśmy jego trzy podstawowe literały: – Najczęściej używany quoted string. – Verbatim string, który jest receptą na stringi zawierające bardzo dużo escape sequences, które mogą czynić stringa nieczytelnym. – Interpolated string, który można uznać za skróconą forme formatowania stringa.

Omówiliśmy sobie najważniejsze konstruktory, pola, propertiesy i metody jakich dostarcza nam klasa string.

Następnie przeszliśmy do omówienia ważnych klas pomocniczych: – StringBuilder, który z jednej strony można uznać za stringa, który jest “mutable” a z drugiej strony za klasę, która przy bardzo dużej ilości operacji na stringach takich jak konkatenacja – może działać w szybciej niż “goły” string. – StringWriter, który jest czymś w stylu specjalizowanej wersji StringBuildera, która służy do dopisywania rzeczy na koniec aktualnego ciągu znaków. – StringReader, który pomaga nam w sekwencyjnym odczycie ciągów znakowych.

Następnie omówiliśmy sobie formatowanie – specjalny rodzaj konwersji, w której jakiś typ jest konwertowany do łańcucha znaków. Najważniejsze metody służące do formatowania to pochodząca z klasy object metoda ToString, oraz statyczna metoda String.Format.

Metoda String.Format przyjmuje m.in. tzw. Composite String, będący “przepisem” na to jak połączyć jakiś string z obiektami, które chcielibyśmy do niego włączyć.

Jednym z elementu composite stringa jest tzw. formatting string (standard lub custom) w którym dokładnie specyfikujemy w jaki sposób dany obiekt powinien być reprezentowany. Poza bezargumentową metodą ToString z klasy object, często mamy do czynienia z przeciążonym ToString, który przyjmuje również formatting string.

Jeżeli reprezentacja naszego obiektu może wyglądać różnie dla różnych krajów (np daty, pieniądze, czy ogólnie wartości liczbowe), to możemy przekazać IFormatProvider’a, który zajmie się przeformatowaniem wartości w sposób respektujący regionalne zasady.

Jeżeli chcielibyśmy, żeby nasz obiekt mógł być formatowany w zależności od podanego formatting stringa i IFormatProvidera to powinniśmy zaimplementować interfejs IFormattable.

Jeżeli natomiast chcielibyśmy na swój sposób wpływać na formatowanie danych to powinniśmy stworzyć właśną implementację IFormatProvidera, który powinien również implementować ICustomFormatter.

Kod źródłowy

Oto projekt na githubie gdzie można znaleźć przykłady, rzeczy które zostały omówione w artykule. https://github.com/mprzybylak/minefields-csharp/tree/master/typesystem

Referencje

  1. T. Covaci – MCSD Certification Toolkit (Exam 70-483): Programming in C# – rozdział 4
  2. string (C# Reference)
  3. Interpolated Strings (C# and Visual Basic Reference)
  4. IFormattable Interface
  5. FormattableString Class
  6. Formatting Types in the .NET Framework
  7. What is the difference between a regular string and a verbatim string?
  8. StringComparison Enumeration
  9. StringBuilder Class
  10. StringWriter Class
  11. TextWriter Class (System.IO)
  12. StringReader Class
  13. TextReader Class (System.IO)
  14. Object.ToString Method ()
  15. String.Format method
  16. Composite Formatting
  17. Standard Numeric Format Strings
  18. Custom Numeric Format Strings
  19. Standard Date and Time Format Strings
  20. Custom Date and Time Format Strings
  21. Enumeration Format Strings
  22. Reference Implementation of IFormattable