OOP Course
Сьогодні

Підрозділ 11.1

Рядки та клас String

Вводить рядки та клас String: створення рядків, конструктори, доступ до символів через індексатор, перебір, порівняння за значенням і перелік основних методів.

11.1. Рядки та клас String

Значна частина завдань у розробці медичних застосунків пов'язана з обробкою текстових даних: парсинг діагнозів за МКХ-10, перевірка формату номера телефону пацієнта, побудова звітів, пошук у клінічних нотатках. У мові C# рядкові значення представляє тип string, а вся функціональність роботи з ним зосереджена в класі System.String. Власне string — це псевдонім (alias) класу String; обидва записи еквівалентні і компілятор не робить між ними різниці. Прийнято використовувати string (з малої літери) в оголошеннях змінних, а String (з великої) — при зверненні до статичних членів: String.IsNullOrEmpty, String.Format.

Об'єкти String зберігають текст як послідовність символів Unicode у кодуванні UTF-16. Це означає, що кожен символ займає 2 або 4 байти — залежно від того, чи потрапляє він у базову багатомовну площину Unicode (BMP) чи виходить за її межі. Більшість символів кирилиці, латиниці, цифр і знаків пунктуації кодуються двома байтами. Максимальний розмір об'єкта String у пам'яті — близько 2 ГБ або приблизно 1 мільярд символів — цей ліміт рідко досягається на практиці, однак він важливий для розуміння масштабованості при обробці великих текстових файлів.

Незмінність рядків (immutability)

Найважливіша властивість string у C# — незмінність (immutability). Після того як об'єкт рядка створено і поміщено у heap, його вміст не може бути змінений жодним засобом. Це не обмеження платформи — це свідоме архітектурне рішення: незмінність дозволяє безпечно передавати рядки між потоками без синхронізації, кешувати їх хеш-коди, використовувати в словниках як ключі.

Наслідок цього рішення: кожен метод класу String, який «змінює» рядок — ToUpper, Replace, Trim, конкатенація через + — насправді не змінює оригінал, а створює новий об'єкт у heap і повертає посилання на нього. Оригінальна змінна продовжує вказувати на незмінений об'єкт:

string diagnosis = "Гіпертензія артеріальна";
string step1 = diagnosis.ToLower();      // новий об'єкт у heap
string step2 = step1 + " есенціальна";  // ще один новий об'єкт
string step3 = step2.ToUpper();          // ще один

Console.WriteLine(diagnosis); // "Гіпертензія артеріальна" — не змінився!

На діаграмі нижче видно: змінні на стеку зберігають посилання на різні об'єкти в heap. Оригінал залишається незачепленим — і це є фундаментальною особливістю, яку необхідно розуміти перед тим як працювати з будь-якими рядковими операціями.

string — незмінний тип: кожна операція створює новий об'єкт у heap

Практичний наслідок: якщо у циклі виконується багато операцій конкатенації, кожна ітерація виділяє нову пам'ять і копіює весь попередній вміст. При N ітераціях загальна кількість скопійованих символів складає 1+2+3+...+N = N(N+1)/2, тобто складність — O(N²). Для таких сценаріїв існує StringBuilder (розд. 11.4), який працює з мутабельним буфером і не копіює вміст при кожному додаванні.

String interning

Компілятор C# застосовує оптимізацію, яку називають string interning (інтернування рядків): однакові рядкові літерали, які зустрічаються в коді, компілюються в один спільний об'єкт у спеціальному пулі рядків (string intern pool). Цей пул зберігається у керованій купі (managed heap) і не підлягає збиранню сміттям протягом усього часу роботи програми.

Завдяки інтернуванню оператор == для рядків порівнює значення (вміст), а не посилання — і дає очікуваний результат навіть тоді, коли технічно обидва рядки є різними об'єктами:

string a = "I10";
string b = "I10";

Console.WriteLine(a == b);                // true  — порівняння значень
Console.WriteLine(ReferenceEquals(a, b)); // true  — один об'єкт у пулі (literal interning)

string c = new string(new char[] { 'I', '1', '0' });
Console.WriteLine(a == c);                // true  — значення однакові
Console.WriteLine(ReferenceEquals(a, c)); // false — c створено через new, поза пулом

Рядки, створені динамічно (через конкатенацію, new string(...), читання з файлу тощо), за замовчуванням не потрапляють у пул. Метод string.Intern(s) дозволяє вручну помістити рядок у пул, щоб наступні порівняння через ReferenceEquals давали true. Але на практиці для перевірки рівності рядків завжди достатньо == — він коректно порівнює значення незалежно від того, де зберігається об'єкт.

Важливо також розуміти, що перевантажений оператор == для рядків — це не той самий оператор, що для об'єктів. Для більшості класів == перевіряє посилання; для string він перевизначений і порівнює саме вміст. Тому "hello" == new string(new char[]{'h','e','l','l','o'}) дає true, хоча технічно це два різних об'єкти.

Тип char

Кожен елемент рядка — це значення типу char. Тип char — це значущий тип (struct) розміром 2 байти, що представляє один Unicode-символ у кодуванні UTF-16. На відміну від string, char не є посилальним типом і не розміщується в heap окремим об'єктом — він зберігається безпосередньо у полі або на стеку.

char letter = 'А';           // одинарні лапки — обов'язково
string word  = "Артеріальна"; // подвійні лапки

Відмінність між char і string принципова: 'A' — це число 65 (код символу), а "A" — це об'єкт у heap з одним символом. Конвертування між ними виконується явно: char c = "A"[0] або string s = c.ToString().

Клас char містить статичні методи для класифікації символів — вони широко використовуються при покрокому аналізі рядків, наприклад, для перевірки форматів ідентифікаторів:

Метод Що перевіряє
char.IsLetter(c) Чи є символ літерою (будь-якого алфавіту)
char.IsDigit(c) Чи є символ десятковою цифрою
char.IsLetterOrDigit(c) Літера або цифра
char.IsWhiteSpace(c) Пробіл, табуляція, перенос рядка
char.IsUpper(c) Верхній регістр
char.IsLower(c) Нижній регістр
char.IsPunctuation(c) Знак пунктуації
char.ToUpper(c) Перетворити на верхній регістр
char.ToLower(c) Перетворити на нижній регістр

Створення рядків

Рядок можна створити кількома способами. Найчастіше використовується рядковий літерал, проте конструктори класу String відкривають додаткові можливості:

// Рядковий літерал — найпоширеніший спосіб
string diagnosis = "Гіпертензія артеріальна";

// Конструктор: повторити символ N разів — корисно для роздільників
string separator = new string('-', 40);  // "----------------------------------------"

// Конструктор: із масиву символів — корисно при побудові рядка посимвольно
string code = new string(new char[] { 'I', '1', '0' });  // "I10"

// Конструктор: частина масиву (startIndex, count) — витягує підпослідовність
string sub = new string(new char[] { 'I', '1', '0', '.', '9' }, 0, 3);  // "I10"

// Порожній рядок — два еквівалентні способи
string empty1 = "";
string empty2 = string.Empty; // string.Empty — рекомендований варіант, не виділяє нових об'єктів

Варто окремо зупинитися на string.Empty. Це статичне поле, що завжди посилається на єдиний порожній рядок у пулі. Записи "" і string.Empty еквівалентні з погляду значення, але явне використання string.Empty підкреслює намір коду й є більш виразним стилістично.

Рядок як масив символів

Клас String реалізує інтерфейс IEnumerable<char>, тому рядок можна перебирати як послідовність символів у циклі foreach. Крім того, визначено індексатор для доступу до окремих символів за позицією:

string icd = "I10.9";

char first = icd[0];            // 'I'
int length  = icd.Length;       // 5

// Перебір через for з індексом
for (int i = 0; i < icd.Length; i++)
    Console.Write(icd[i] + " "); // I 1 0 . 9

// Перебір через foreach — без доступу до індексу
foreach (char c in icd)
    Console.Write(c + " ");      // I 1 0 . 9

Ключова відмінність від масиву: індексатор рядка доступний тільки для читання — спроба присвоїти icd[0] = 'X' призведе до помилки компіляції. Це ще раз підкреслює незмінність рядка на рівні мови. Якщо потрібно отримати масив символів для редагування — використовується метод ToCharArray(), який повертає копію символів у вигляді звичайного масиву char[], після роботи з яким можна знову створити рядок через конструктор new string(chars).

Властивість Length повертає кількість об'єктів char у рядку. Для більшості символів це еквівалентно кількості видимих символів, але для деяких рідкісних Unicode-символів (так звані сурогатні пари), які кодуються двома char, значення Length може бути більшим, ніж кількість гліфів.

Перевірка на порожній рядок

Ситуація, коли рядок не містить корисного значення, зустрічається постійно: незаповнені поля форми, відсутні дані з бази, порожні рядки при парсингу. Для таких перевірок клас String надає два статичні методи:

string.IsNullOrEmpty(s)      // true якщо s == null або s == ""
string.IsNullOrWhiteSpace(s) // true якщо s == null, "" або "   " (тільки пробіли)

IsNullOrWhiteSpace є кращим вибором у більшості випадків: він захищає не лише від null і порожнього рядка, а й від рядків, що складаються виключно з пробільних символів (' ', '\t', '\n'). Такі рядки технічно не є порожніми з точки зору IsNullOrEmpty, але з ділової логіки — це відсутність значення. Наприклад, якщо лікар ввів у поле діагнозу кілька пробілів, це та сама ситуація, що й порожнє поле:

void PrintDiagnosis(string? text)
{
    if (string.IsNullOrWhiteSpace(text))
    {
        Console.WriteLine("Діагноз не вказано");
        return;
    }
    Console.WriteLine($"Діагноз: {text.Trim()}");
}

Зверніть увагу: параметр позначено як string? (nullable string) — це C# 8+ функціональність, яка на рівні компілятора попереджає, що значення може бути null. Методи IsNullOrEmpty і IsNullOrWhiteSpace коректно обробляють null без виключень.

Порівняння рядків

На відміну від більшості класів, оператор == для рядків перевизначений і порівнює значення (вміст), а не посилання. Це дає природну семантику: два рядки з однаковим текстом вважаються рівними.

Однак оператор == за замовчуванням є чутливим до регістру. Для порівняння без урахування регістру використовується метод string.Equals з параметром StringComparison:

string d1 = "Гіпертензія";
string d2 = "гіпертензія";

Console.WriteLine(d1 == d2);  // false — різний регістр
Console.WriteLine(string.Equals(d1, d2, StringComparison.OrdinalIgnoreCase)); // true

Перерахування StringComparison надає кілька режимів порівняння. Для медичних ідентифікаторів та кодів найбільш підходить OrdinalIgnoreCase — він порівнює символи за їхніми байтовими (Unicode code point) значеннями без урахування регістру і без прив'язки до локалі системи. Це важливо: режими CurrentCulture і InvariantCulture можуть давати різні результати залежно від регіональних налаштувань ОС, що небажано для алгоритмів обробки стандартизованих кодів.

Перелік основних методів

Клас String містить десятки методів. Нижче — найбільш уживані у повсякденній розробці:

Метод Що робить
Contains(s) Чи містить рядок підрядок
StartsWith(s) / EndsWith(s) Чи починається / закінчується підрядком
IndexOf(s) / LastIndexOf(s) Індекс першого / останнього входження
Replace(old, new) Замінити всі входження
Split(separator) Розбити на масив підрядків
Substring(start, length) Витягти підрядок
Trim() / TrimStart() / TrimEnd() Видалити пробіли
ToUpper() / ToLower() Зміна регістру
Insert(index, value) Вставити підрядок
Remove(start, count) Видалити символи
PadLeft(n) / PadRight(n) Доповнити пробілами до ширини n
ToCharArray() Отримати копію символів як char[]
Equals(s, comparison) Порівняти з урахуванням режиму
Compare(s1, s2) Лексикографічне порівняння

Кожен з цих методів розглядається детально в розділі 11.2.

Рядки у клінічному контексті — runnable приклад

Базові операції зі рядками на прикладі даних медичної картки:

Символьний аналіз рядка — runnable приклад

Перевірка формату коду МКХ-10 через char-методи та IsNullOrWhiteSpace:

Розроблено Tomka Yurii · © 2026 ·