Znaki, char i Unicode

Robot odczytuje kod z identyfikatora: A7B2. Zanim policzysz w nim cyfry, musisz umieć wyjąć z tekstu pojedynczy znak i odróżnić znak cyfry od jej wartości.

C# char Unicode indeksowanie 45 min
CEL LEKCJI

Czego się dziś nauczysz

  • Odróżnisz literał char od jednoznakowego napisu
  • Wyjmiesz z napisu znak po indeksie i podasz indeks ostatniego znaku
  • Wyjaśnisz, dlaczego '7' to nie liczba 7, i zamienisz znak cyfry na jej wartość
  • Użyjesz char.IsDigit, IsLetter i pokrewnych
  • Wyjaśnisz, dlaczego emoji ma Length równe 2

Przygotowanie: lekcje 01–06. Przewidywany czas: 45–90 minut z zadaniami. Przykłady wymagają .NET 8 lub nowszego, z włączonymi ImplicitUsings i Nullable.

TEORIA

Jeden znak to nie to samo co tekst

W lekcji 06 poznałeś typ string — tekst dowolnej długości. Obok niego istnieje typ char, który mieści dokładnie jeden znak. Różni je nie tylko rozmiar, ale i zapis:

Program.cs
char litera = 'A';        // APOSTROFY - jeden znak
string napis  = "A";      // CUDZYSLOW  - tekst o dlugosci 1

char pusty = ' ';         // spacja to tez znak
// char zly = "A";        // BLAD CS0029: nie mozna przypisac string do char
// char zly2 = 'Ala';     // BLAD CS1012: w apostrofach mieszcza sie tylko znak

Napis udostępnia swoje znaki po numerze — indeksie. Numerujemy od zera, tak samo jak później elementy tablic.

Program.cs
string kod = "A7B2";

Console.WriteLine(kod.Length);   // 4  - liczba znakow
Console.WriteLine(kod[0]);       // A  - pierwszy znak
Console.WriteLine(kod[1]);       // 7
Console.WriteLine(kod[3]);       // 2  - ostatni, bo Length - 1
wynik w konsoli
4
A
7
2

Ostatni indeks to Length - 1

Napis o czterech znakach ma indeksy 0, 1, 2 i 3. Odwołanie kod[4] kończy się wyjątkiem IndexOutOfRangeException i przerwaniem programu. Ta sama reguła obowiązuje przy tablicach w lekcji 23 warto ją zapamiętać raz.

Znak można wyjąć z napisu i przypisać do zmiennej typu char, a pojedynczy znak można doliczyć do napisu:

Program.cs
string kod = "A7B2";
char pierwszy = kod[0];

Console.WriteLine(pierwszy);                    // A
Console.WriteLine("Kod zaczyna sie od " + pierwszy);
Console.WriteLine($"Ostatni znak: {kod[kod.Length - 1]}");
TEORIA

Znak to liczba w przebraniu

Komputer nie przechowuje kształtu litery — przechowuje jej numer. Rzutowanie na int pokazuje ten numer:

Program.cs
Console.WriteLine((int)'A');    // 65
Console.WriteLine((int)'a');    // 97
Console.WriteLine((int)'0');    // 48
Console.WriteLine((int)'7');    // 55

Console.WriteLine((char)66);    // B  - w druga strone tez dziala

Stąd bierze się pułapka, która myli niemal każdego na początku: znak cyfry to nie jest wartość cyfry. Znak '7' ma numer 55, a nie 7.

Program.cs
char cyfra = '7';

Console.WriteLine((int)cyfra);       // 55 - numer znaku
Console.WriteLine(cyfra - '0');      // 7  - WARTOSC cyfry
Console.WriteLine('9' - '0');        // 9
Console.WriteLine('A' - '0');        // 17 - bez sensu, bo 'A' nie jest cyfra

Dlaczego odejmowanie działa

Cyfry ASCII leżą w tablicy znaków po kolei: '0' ma numer 48, '1' — 49, aż do '9' o numerze 57. Odjęcie numeru zera od numeru dowolnej cyfry daje jej wartość. Ten sam trik działa dla liter: 'C' - 'A' daje 2, czyli numer litery w alfabecie łacińskim.

Ponieważ znaki mają numery, można je porównywać operatorami znanymi z arytmetyki:

Program.cs
Console.WriteLine('a' > 'A');     // True  - 97 > 65
Console.WriteLine('b' > 'a');     // True  - kolejnosc alfabetyczna
Console.WriteLine('Z' < 'a');     // True  - WSZYSTKIE wielkie litery przed malymi

Kolejność nie jest alfabetem polskim

Porównywanie numerów ustawia znaki w kolejności tablicy Unicode, nie słownikowej. 'ż' > 'z' da true, bo polskie litery mają wyższe numery, a wielkie litery wypadają przed wszystkimi małymi. Do porządkowania nazwisk służą osobne narzędzia — poznasz je przy porównywaniu napisów w lekcji 08 i przy sortowaniu w lekcji 51.

TEORIA

Rozpoznawanie: char.IsDigit i spółka

Zamiast porównywać numery ręcznie, korzystamy z gotowych metod klasy char. Każda zwraca bool — wartość logiczną z lekcji 06.

Program.cs
char z = '7';

Console.WriteLine(char.IsDigit(z));        // True
Console.WriteLine(char.IsLetter(z));       // False
Console.WriteLine(char.IsLetterOrDigit(z));// True
Console.WriteLine(char.IsWhiteSpace(z));   // False
Console.WriteLine(char.IsUpper('a'));      // False
Console.WriteLine(char.IsLower('a'));      // True

Console.WriteLine(char.ToUpper('ł'));      // Ł
Console.WriteLine(char.ToLower('X'));      // x

Wynik bool możesz na razie po prostu wypisać. Od lekcji 14 użyjesz go w instrukcji warunkowej, a od lekcji 17 w pętli przechodzącej po wszystkich znakach napisu. Wtedy z tych metod zbudujesz sprawdzanie haseł, numerów PESEL i kodów.

IsDigit nie zawsze znaczy „0–9″

char.IsDigit uznaje za cyfrę każdy znak, który w Unicode jest cyfrą dziesiętną — także cyfry arabsko-indyjskie czy dewanagari. Dla nich odejmowanie - '0' da bezsensowny wynik. W zadaniach szkolnych to nie przeszkadza, ale przy danych z internetu warto o tym pamiętać.

TEORIA

Dlaczego emoji ma Length równe 2

char zajmuje 16 bitów, czyli dwa bajty. To wystarcza na ponad 65 tysięcy znaków — wszystkie litery alfabetów europejskich, cyfry i znaki interpunkcyjne. Nie wystarcza jednak na cały Unicode, który ma ich ponad milion.

Znaki spoza tego zakresu — emoji, rzadkie znaki chińskie, symbole matematyczne — zapisywane są parą jednostek char. Napis liczy jednostki, nie symbole widziane przez człowieka:

Program.cs
Console.WriteLine("A".Length);      // 1
Console.WriteLine("ł".Length);      // 1  - polskie litery mieszcza sie w jednym char
Console.WriteLine("€".Length);      // 1
Console.WriteLine("😀".Length);     // 2  - jeden symbol, DWIE jednostki
WyrażenieWynikDlaczego
"Ala".Length3trzy zwykłe znaki
"żółw".Length4polskie litery mieszczą się w jednym char
"😀".Length2symbol spoza podstawowego zakresu — para jednostek
"a\tb".Length3tabulator to jeden znak, mimo że wygląda jak kilka spacji

Znaki specjalne

Niektórych znaków nie da się wpisać wprost — zapisujemy je sekwencją zaczynającą się od ukośnika wstecznego:

ZapisZnaczenie
\nprzejście do nowego wiersza
\ttabulator
\\ukośnik wsteczny
\"cudzysłów wewnątrz napisu
\'apostrof wewnątrz char
\u0141znak o podanym numerze szesnastkowym (tu: Ł)
Program.cs
Console.WriteLine("Pierwszy\nDrugi");
Console.WriteLine("Imie:\tAla");
Console.WriteLine("Sciezka: C:\\dane\\plik.txt");
Console.WriteLine("Powiedzial \"dobrze\" i wyszedl.");
Console.WriteLine('\u0141');
wynik w konsoli
Pierwszy
Drugi
Imie:	Ala
Sciezka: C:\dane\plik.txt
Powiedzial "dobrze" i wyszedl.
Ł
PRZYKŁAD

Przykład: rozbiór kodu identyfikatora

Wklej całość do nowego projektu konsolowego jako Program.cs. Zanim uruchomisz — przewidź każdy wynik.

Program.cs
string kod = "A7B2";

Console.WriteLine($"Kod: {kod}, dlugosc: {kod.Length}");
Console.WriteLine();

char pierwszy = kod[0];
char drugi    = kod[1];

Console.WriteLine($"[0] = {pierwszy}  litera? {char.IsLetter(pierwszy)}  cyfra? {char.IsDigit(pierwszy)}");
Console.WriteLine($"[1] = {drugi}  litera? {char.IsLetter(drugi)}  cyfra? {char.IsDigit(drugi)}");
Console.WriteLine();

Console.WriteLine($"numer znaku '{drugi}' to {(int)drugi}");
Console.WriteLine($"wartosc cyfry '{drugi}' to {drugi - '0'}");
Console.WriteLine();

int suma = (kod[1] - '0') + (kod[3] - '0');
Console.WriteLine($"Suma cyfr na pozycjach 1 i 3: {suma}");
Console.WriteLine($"Kod malymi literami: {kod.ToLower()}");
wynik w konsoli
Kod: A7B2, dlugosc: 4

[0] = A  litera? True  cyfra? False
[1] = 7  litera? False  cyfra? True

numer znaku '7' to 55
wartosc cyfry '7' to 7

Suma cyfr na pozycjach 1 i 3: 9
Kod malymi literami: a7b2

Co dzieje się po kolei

  • kod.Length to właściwość — odczytujemy ją bez nawiasów. char.IsLetter(x) to metoda — wymaga nawiasów i argumentu.
  • Wewnątrz interpolacji $"…" wolno umieścić całe wyrażenie: {(int)drugi} albo {drugi - '0'}.
  • Nawiasy w (kod[1] - '0') nie są konieczne, ale pokazują zamiar: najpierw wyjmij znak, potem zamień na wartość.
  • ToLower zwraca nowy napis. Zmienna kod nadal zawiera A7B2 — dlaczego, wyjaśnia następna lekcja.
ELEMENTY WBUDOWANE

Co robi każdy użyty element

ElementWynikDziałanie i ograniczenia
'x'charLiterał znakowy — apostrofy, dokładnie jeden znak. "x" to napis, nie znak.
napis[i]charZnak o indeksie i, licząc od zera. Indeks od Length w górę daje IndexOutOfRangeException.
napis.LengthintLiczba jednostek char — właściwość, bez nawiasów. Nie zawsze równa liczbie widocznych symboli.
(int)znakintNumer jednostki kodowej UTF-16. Rzutowanie odwrotne (char)liczba też działa.
znak - '0'intWartość cyfry — poprawna tylko dla cyfr ASCII od '0' do '9'.
char.IsDigit(z)boolCzy znak jest cyfrą dziesiętną Unicode. Argumentu nie zmienia.
char.IsLetter(z)boolCzy znak jest literą. Jest też IsLetterOrDigit.
char.IsWhiteSpace(z)boolSpacja, tabulator, znak końca wiersza.
char.IsUpper(z) / IsLower(z)boolWielkość litery. Dla cyfr i znaków interpunkcyjnych oba dają false.
char.ToUpper(z) / ToLower(z)charZwraca zmieniony znak; oryginału nie rusza. Wersje …Invariant ignorują ustawienia regionalne.
\n, \t, \\, \"znakSekwencje ucieczki — jeden znak mimo dwóch symboli w kodzie.
CZĘSTE BŁĘDY

Zanim utkniesz

ZapisProblem
char z = "A";CS0029 — cudzysłów tworzy string. Znak wymaga apostrofów.
int x = '7';Kompiluje się, ale x będzie miało 55, a nie 7. Użyj '7' - '0'.
kod[kod.Length]Zawsze poza zakresem. Ostatni znak to kod[kod.Length - 1].
kod[0] na pustym napisieWyjątek. Pusty napis "" ma Length równe 0 i żadnego indeksu.
char.ToUpper(z) bez przypisaniaMetoda zwraca nowy znak, starego nie zmienia. Zapisz wynik: z = char.ToUpper(z);.
Zakładanie, że Length to liczba symboliEmoji i część znaków azjatyckich zajmują dwie jednostki.
ZADANIA

Zadania

ZAD 1Numery znaków★☆☆

Wypisz numery znaków 'A', 'Z', 'a', '0' i ' '. Następnie wypisz, jakie znaki kryją się pod numerami 65, 90 i 122.

ZAD 2Rozbiór napisu★☆☆

Dla napisu "Sala 204" wypisz jego długość oraz znaki o indeksach 0, 4 i ostatnim. Ostatni pobierz wyrażeniem z Length, a nie wpisaną na sztywno liczbą.

ZAD 3Cyfra czy litera★☆☆

Dla znaków 'K', '4', ' ' i 'ż' wypisz wynik char.IsLetter, char.IsDigit i char.IsWhiteSpace. Zestaw wyniki w kolumnach oddzielonych tabulatorem.

ZAD 4Wartość kontra numer★★☆

Z napisu "9081" wyjmij pierwszą i ostatnią cyfrę, zamień je na wartości liczbowe i wypisz ich sumę oraz iloczyn. Wypisz też, ile wyszłoby, gdyby zapomnieć o odjęciu '0'.

ZAD 5Numer litery w alfabecie★★☆

Wypisz, którą literą alfabetu łacińskiego jest 'C', 'M' i 'Z' — licząc od 1. Skorzystaj z odejmowania znaków. Sprawdź, co wypisze twój kod dla 'c', i wyjaśnij wynik.

ZAD 6Eksperyment Unicode★★☆

Wypisz Length napisów "A", "ł", "€", "😀" i "a\tb". Przy każdym dopisz własnymi słowami, dlaczego wynik jest właśnie taki. Jeden z nich zaskoczy cię dwa razy.

PODSUMOWANIE

Co trzeba zapamiętać

  • char mieści jeden znak i zapisujemy go w apostrofach; string — w cudzysłowie.
  • Znaki napisu numerujemy od zera; ostatni ma indeks Length - 1.
  • Znak jest przechowywany jako numer — (int)'A' daje 65.
  • Znak cyfry to nie jej wartość: '7' - '0' daje 7, samo '7' daje 55.
  • char.IsDigit, IsLetter, IsWhiteSpace zwracają bool i niczego nie zmieniają.
  • Length liczy jednostki UTF-16, a nie symbole widziane przez człowieka — emoji ma długość 2.

Dokumentacja: Microsoft Learn — temat tej lekcji.