Строки в C# — это неизменяемые (immutable) объекты. Это означает, что каждый раз, когда вы “изменяете” строку, на самом деле создается новый объект в памяти. Понимание этого факта критически важно для написания эффективного кода.
Представьте строку как высеченную в камне надпись. Вы не можете изменить букву в этой надписи — вам придется высечь новую надпись целиком. Именно поэтому операции со строками могут быть дорогостоящими с точки зрения производительности.
Часть 1: Основные методы работы со строками
1.1 Методы форматирования и очистки
Trim(), TrimStart(), TrimEnd()
Эти методы удаляют пробельные символы (пробелы, табуляции, переносы строк) с начала и/или конца строки.
string userInput = " Иван Петров ";
string cleanName = userInput.Trim(); // "Иван Петров"
// Можно указать конкретные символы для удаления
string data = "---данные---";
string cleaned = data.Trim('-'); // "данные"Когда использовать: Всегда применяйте Trim() при обработке пользовательского ввода из форм, файлов или баз данных. Пользователи часто случайно добавляют лишние пробелы, и это может привести к проблемам при сравнении строк или сохранении в базу данных.
Replace()
Заменяет все вхождения подстроки или символа на другую подстроку или символ.
string template = "Здравствуйте, {name}! Ваш заказ {order} готов.";
string message = template.Replace("{name}", "Александр")
.Replace("{order}", "#12345");
// Результат: "Здравствуйте, Александр! Ваш заказ #12345 готов."Важное замечание: Replace() создает новую строку при каждом вызове. Если нужно выполнить множество замен, рассмотрите использование StringBuilder (см. раздел 2).
1.2 Методы поиска и проверки
Contains(), StartsWith(), EndsWith()
Эти методы проверяют наличие подстроки в строке.
string fileName = "document.pdf";
if (fileName.EndsWith(".pdf"))
{
// Это PDF-файл, обрабатываем соответствующим образом
}
string email = "user@example.com";
if (!email.Contains("@"))
{
// Email невалидный
}IndexOf() и LastIndexOf()
Находят позицию первого или последнего вхождения подстроки.
string path = "C:/Users/Documents/file.txt";
int lastSlash = path.LastIndexOf('/');
string fileName = path.Substring(lastSlash + 1); // "file.txt"
// IndexOf возвращает -1, если подстрока не найдена
if (text.IndexOf("ошибка") != -1)
{
// В тексте есть слово "ошибка"
}1.3 Методы преобразования
ToUpper(), ToLower()
Преобразуют регистр строки. Критически важны при сравнении строк без учета регистра.
string userAnswer = "ДА";
string normalized = userAnswer.ToLower();
if (normalized == "да" || normalized == "yes")
{
// Пользователь согласился
}Важный момент: Для сравнения без учета регистра лучше использовать специальные методы сравнения:
// Более правильный подход
if (string.Equals(userAnswer, "да", StringComparison.OrdinalIgnoreCase))
{
// Сравнение без учета регистра и более эффективное
}Substring()
Извлекает часть строки.
string phoneNumber = "+7-495-123-45-67";
string countryCode = phoneNumber.Substring(0, 2); // "+7"
string localNumber = phoneNumber.Substring(3); // "495-123-45-67"1.4 Методы разделения и объединения
Split()
Разделяет строку на массив подстрок по указанному разделителю.
string csvLine = "Иванов;Петр;35;Москва";
string[] parts = csvLine.Split(';');
// parts[0] = "Иванов", parts[1] = "Петр", и т.д.
// Можно использовать несколько разделителей
string text = "слово1, слово2; слово3 | слово4";
string[] words = text.Split(new char[] {',', ';', '|'});Join()
Объединяет элементы массива в строку с указанным разделителем.
string[] tags = {"C#", "программирование", "разработка"};
string tagString = string.Join(", ", tags);
// Результат: "C#, программирование, разработка"1.5 Методы парсинга
Parse() и TryParse()
Преобразуют строку в другой тип данных. TryParse безопаснее, так как не выбрасывает исключение при ошибке.
// Опасный способ (может выбросить исключение)
string ageText = "25";
int age = int.Parse(ageText);
// Безопасный способ
string userInput = "не число";
int value;
if (int.TryParse(userInput, out value))
{
// Преобразование успешно, value содержит число
Console.WriteLine($"Вы ввели число: {value}");
}
else
{
// Преобразование не удалось
Console.WriteLine("Введите корректное число");
}
// Работа с датами
DateTime date;
if (DateTime.TryParse("2024-01-15", out date))
{
// Дата успешно распознана
}Правило: Всегда используйте TryParse при работе с пользовательским вводом. Parse используйте только когда уверены в формате данных (например, при чтении конфигурационных файлов).
Часть 2: StringBuilder — когда и зачем
Проблема конкатенации строк
Рассмотрим простой пример:
// ПЛОХОЙ подход
string result = "";
for (int i = 0; i < 1000; i++)
{
result += "Строка " + i + "\n"; // Создается 3000+ промежуточных строк!
}При каждой операции += создается новая строка в памяти. Для 1000 итераций это означает создание тысяч промежуточных объектов, что приводит к:
- Замедлению работы программы
- Излишней нагрузке на сборщик мусора
- Избыточному использованию памяти
Решение: StringBuilder
StringBuilder — это изменяемый буфер символов. Он работает как динамический массив, который может эффективно изменять свое содержимое.
// ХОРОШИЙ подход
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++)
{
sb.Append("Строка ");
sb.Append(i);
sb.AppendLine(); // Добавляет перенос строки
}
string result = sb.ToString();Основные методы StringBuilder
StringBuilder sb = new StringBuilder();
// Добавление в конец
sb.Append("Текст");
sb.AppendLine("Строка с переносом");
sb.AppendFormat("Число: {0}, Дата: {1}", 42, DateTime.Now);
// Вставка в произвольную позицию
sb.Insert(0, "Начало: ");
// Замена
sb.Replace("старый", "новый");
// Удаление
sb.Remove(0, 5); // Удаляет 5 символов начиная с позиции 0
// Очистка
sb.Clear();
// Установка длины (обрезка)
sb.Length = 10; // Оставляет только первые 10 символовКогда использовать StringBuilder
Используйте StringBuilder когда:
- Выполняете множественные операции конкатенации (более 5-10)
- Строите строку в цикле
- Генерируете отчеты, HTML, XML или другой текстовый контент
- Работаете с большими объемами текста
НЕ используйте StringBuilder когда:
- Выполняете 2-4 простые конкатенации
- Работаете с константными строками
- Используете string.Format() или интерполяцию строк (в C# 6)
// Для простых случаев обычная конкатенация приемлема
string fullName = firstName + " " + lastName;
// Или используйте string.Format
string message = string.Format("Привет, {0}!", userName);
// В C# 6 доступна интерполяция строк
string greeting = $"Привет, {userName}! Сегодня {DateTime.Now:dd.MM.yyyy}";Часть 3: Работа с путями файловой системы — класс Path
Класс Path предоставляет методы для безопасной работы с путями файлов и директорий. Никогда не собирайте пути вручную через конкатенацию строк!
Основные методы Path
// Объединение частей пути (учитывает разделители ОС)
string folder = @"C:\Users\Documents";
string fileName = "report.txt";
string fullPath = Path.Combine(folder, fileName);
// Результат: C:\Users\Documents\report.txt
// Получение компонентов пути
string filePath = @"C:\Projects\MyApp\src\main.cs";
string directory = Path.GetDirectoryName(filePath); // C:\Projects\MyApp\src
string fileName = Path.GetFileName(filePath); // main.cs
string fileNameWithoutExt = Path.GetFileNameWithoutExtension(filePath); // main
string extension = Path.GetExtension(filePath); // .cs
// Изменение расширения
string newPath = Path.ChangeExtension(filePath, ".txt");
// Результат: C:\Projects\MyApp\src\main.txt
// Получение полного пути из относительного
string relativePath = @"..\..\file.txt";
string absolutePath = Path.GetFullPath(relativePath);
// Генерация уникального имени временного файла
string tempFile = Path.GetTempFileName();
// Получение пути к временной директории
string tempPath = Path.GetTempPath();Почему важно использовать Path
// ПЛОХО: Ручная конкатенация
string badPath = folder + "\\" + subfolder + "\\" + file;
// Проблемы:
// - Не работает на Linux/Mac (там разделитель /)
// - Двойные слеши при ошибках
// - Нет проверки корректности
// ХОРОШО: Использование Path.Combine
string goodPath = Path.Combine(folder, subfolder, file);
// Преимущества:
// - Кроссплатформенность
// - Автоматическая обработка разделителей
// - Обработка пустых и null значенийЧасть 4: Практические сценарии и оптимизация
Сценарий 1: Обработка CSV-файла
// Задача: Прочитать CSV и построить отчет
StringBuilder report = new StringBuilder();
report.AppendLine("Отчет по продажам");
report.AppendLine(new string('-', 50));
string[] lines = File.ReadAllLines("sales.csv");
foreach (string line in lines.Skip(1)) // Пропускаем заголовок
{
string[] parts = line.Split(',');
if (parts.Length >= 3)
{
string product = parts[0].Trim();
int quantity;
decimal price;
if (int.TryParse(parts[1].Trim(), out quantity) &&
decimal.TryParse(parts[2].Trim(), out price))
{
decimal total = quantity * price;
report.AppendFormat("{0,-20} {1,10:C}\n", product, total);
}
}
}
string finalReport = report.ToString();Сценарий 2: Валидация и нормализация email
public static string NormalizeEmail(string email)
{
if (string.IsNullOrWhiteSpace(email))
return null;
// Убираем пробелы и приводим к нижнему регистру
email = email.Trim().ToLower();
// Базовая проверка формата
int atIndex = email.IndexOf('@');
int lastDotIndex = email.LastIndexOf('.');
if (atIndex <= 0 ||
lastDotIndex <= atIndex ||
lastDotIndex >= email.Length - 2)
{
return null; // Невалидный формат
}
return email;
}Сценарий 3: Генерация HTML-отчета
public static string GenerateHtmlReport(List<Product> products)
{
// Для генерации HTML всегда используем StringBuilder
StringBuilder html = new StringBuilder();
html.AppendLine("<!DOCTYPE html>");
html.AppendLine("<html>");
html.AppendLine("<head><title>Отчет</title></head>");
html.AppendLine("<body>");
html.AppendLine("<table border='1'>");
html.AppendLine("<tr><th>Название</th><th>Цена</th><th>Количество</th></tr>");
foreach (var product in products)
{
html.AppendFormat("<tr><td>{0}</td><td>{1:C}</td><td>{2}</td></tr>\n",
System.Net.WebUtility.HtmlEncode(product.Name), // Экранирование HTML
product.Price,
product.Quantity);
}
html.AppendLine("</table>");
html.AppendLine("</body>");
html.AppendLine("</html>");
return html.ToString();
}Сценарий 4: Работа с путями в кроссплатформенном приложении
public static string GetDataFilePath(string fileName)
{
// Получаем базовую директорию приложения
string baseDir = AppDomain.CurrentDomain.BaseDirectory;
// Создаем путь к папке данных
string dataDir = Path.Combine(baseDir, "Data");
// Проверяем существование директории
if (!Directory.Exists(dataDir))
{
Directory.CreateDirectory(dataDir);
}
// Возвращаем полный путь к файлу
return Path.Combine(dataDir, fileName);
}
// Использование
string configPath = GetDataFilePath("config.json");
string logPath = GetDataFilePath($"log_{DateTime.Now:yyyy-MM-dd}.txt");Часть 5: Советы по производительности и лучшие практики
1. Сравнение строк
// Для точного сравнения используйте Ordinal
bool areEqual = string.Equals(str1, str2, StringComparison.Ordinal);
// Для сравнения без учета регистра
bool areEqualIgnoreCase = string.Equals(str1, str2,
StringComparison.OrdinalIgnoreCase);
// Для сравнения с учетом культуры (для пользовательского интерфейса)
bool areEqualCulture = string.Equals(str1, str2,
StringComparison.CurrentCulture);2. Проверка на пустоту
// Используйте специальные методы вместо сравнения с ""
if (string.IsNullOrEmpty(text))
{
// Строка null или пустая
}
if (string.IsNullOrWhiteSpace(text))
{
// Строка null, пустая или содержит только пробелы
// Это предпочтительный метод для валидации пользовательского ввода
}3. Оптимизация StringBuilder
// Если знаете примерный размер, укажите начальную емкость
StringBuilder sb = new StringBuilder(1000);
// Это предотвратит множественные перевыделения памяти
// при росте внутреннего буфера4. Интернирование строк
// Для часто используемых строк можно использовать интернирование
string internedStr = string.Intern(someString);
// Но будьте осторожны — интернированные строки
// остаются в памяти до конца работы приложения5. Форматирование строк
// В порядке предпочтения для C# 6:
// 1. Интерполяция строк (C# 6) — самый читаемый способ
string message = $"Пользователь {userName} вошел в {loginTime:HH:mm}";
// 2. String.Format — универсальный способ
string message = string.Format("Пользователь {0} вошел в {1:HH:mm}",
userName, loginTime);
// 3. StringBuilder — для сложной логики построения
StringBuilder sb = new StringBuilder();
sb.AppendFormat("Пользователь {0} ", userName);
sb.AppendFormat("вошел в {0:HH:mm}", loginTime);
// 4. Конкатенация — только для 2-3 простых строк
string message = "Пользователь " + userName + " вошел";Заключение: Ключевые принципы
-
Помните о неизменяемости строк. Каждая “модификация” создает новый объект.
-
Используйте StringBuilder для множественных операций. Это не преждевременная оптимизация, а базовая грамотность.
-
Всегда используйте Path для работы с путями. Это гарантирует кроссплатформенность и безопасность.
-
Применяйте TryParse для пользовательского ввода. Исключения дорого обходятся производительности.
-
Выбирайте правильный метод сравнения строк. StringComparison.Ordinal для технических строк, CurrentCulture для UI.
-
Валидируйте и нормализуйте входные данные. Trim() и проверка на IsNullOrWhiteSpace — ваши друзья.
-
Думайте о производительности заранее. Правильный выбор между string и StringBuilder может ускорить программу в десятки раз.
Эти принципы помогут вам писать не только работающий, но и эффективный, поддерживаемый код. Помните: строки — один из самых используемых типов данных, и правильная работа с ними — признак профессионализма разработчика.