Преобразование строк в C# — это перевод текста (string) в значение другого типа (число, дату, bool, перечисление) и обратно, а также перевод строки в массив символов или байтов. Это три разных механизма.
Содержание
Мини-словарь, который держим сквозь статью:
| Операция | Что делает | Чем пользоваться |
|---|---|---|
| Разбор (parsing) | текст -> значение: "42" -> 42 |
int.Parse, int.TryParse, Convert.ToInt32 |
| Форматирование | значение -> текст: 1234.5 -> "1 234,50" |
ToString(формат, культура), интерполяция |
| Кодирование | текст -> байты и обратно: "Я" -> D0 AF |
Encoding.UTF8.GetBytes / GetString |
Разбор и форматирование зависят от культуры (разделитель дробной части, группы разрядов, формат даты). Кодирование от культуры не зависит, зато зависит от выбранной кодировки. Примеры рассчитаны на .NET 10 / C# 14 и работают в .NET 6-9.
Минимальный рабочий пример: строка в int
Консольное приложение (dotnet new console):
using System;
string input = "42";
if (int.TryParse(input, out int number))
{
Console.WriteLine(number + 1); // 43
}
else
{
Console.WriteLine($"Не число: {input}");
}
В консоли появится 43. Если заменить "42" на "abc", программа не упадет, а напечатает Не число: abc. В этом и смысл TryParse: он возвращает true/false, а результат кладет в out-переменную.
Parse, TryParse и Convert: чем отличаются
Все три разбирают строку, но по-разному ведут себя на плохом входе:
| Вход | int.Parse(s) |
int.TryParse(s, out n) |
Convert.ToInt32(s) |
|---|---|---|---|
"42" |
42 | true, n = 42 |
42 |
" 42 " |
42 (пробелы по краям допустимы) | true, n = 42 |
42 |
"abc" |
FormatException |
false, n = 0 |
FormatException |
"" |
FormatException |
false |
FormatException |
null |
ArgumentNullException |
false |
0 |
"3000000000" |
OverflowException |
false |
OverflowException |
Как выбрать:
- Пользовательский ввод, файл, запрос —
TryParse. Ошибочный ввод здесь нормальная ситуация, а не исключение. - Значение, которое обязано быть числом (конфиг, собственный формат) —
Parse: исключение сразу покажет, что данные испорчены. Convert.ToInt32(string)— по сутиint.Parseс одной особенностью:nullпревращается в0. Так пустое поле молча становится нулем в расчете. Для строк чаще берутParse/TryParse.
У Convert есть своя сильная сторона — преобразование между числовыми типами и системы счисления:
Console.WriteLine(Convert.ToInt32(2.5)); // 2
Console.WriteLine(Convert.ToInt32(3.5)); // 4
Console.WriteLine((int)2.7); // 2
Console.WriteLine(Convert.ToInt32("ff", 16)); // 255
Console.WriteLine(Convert.ToString(255, 2)); // 11111111
Convert.ToInt32(double) округляет до ближайшего целого, а ровно половину — к четному (так называемое банковское округление), поэтому 2.5 дает 2, а 3.5 — 4. Приведение (int) просто отбрасывает дробную часть.
Культура: почему «3.14» может не разобраться
Здесь основной источник ошибок. double.Parse(s) без явной культуры использует CultureInfo.CurrentCulture, то есть настройки ОС или потока. В ru-RU разделитель дробной части — запятая.
Неверно — полагаться на текущую культуру для данных из файла или API:
using System;
using System.Globalization;
CultureInfo.CurrentCulture = new CultureInfo("ru-RU");
bool ok = double.TryParse("3.14", out double x);
Console.WriteLine(ok); // False
Результат: False (а double.Parse("3.14") бросит FormatException). Хуже бывает в культурах, где точка — разделитель групп разрядов: там "3.14" может разобраться без ошибки, но в другое число.
Исправление — явно указывать культуру. Для машинных данных (JSON, CSV, конфиги, API) это CultureInfo.InvariantCulture:
using System;
using System.Globalization;
double x = double.Parse("3.14", CultureInfo.InvariantCulture);
Console.WriteLine(x.ToString(CultureInfo.InvariantCulture)); // 3.14
var ru = new CultureInfo("ru-RU");
double y = double.Parse("3,14", ru);
Console.WriteLine(y.ToString(ru)); // 3,14
Правило выбора простое: данные для машины — InvariantCulture, текст для человека (ввод в форме, вывод на экран) — культура пользователя. Console.WriteLine(x) без культуры тоже форматирует по текущей.
NumberStyles: шестнадцатеричные числа, валюта, разряды
Перегрузки Parse(string, NumberStyles) и Parse(string, NumberStyles, IFormatProvider) уточняют, что допускается во входной строке. Флаги объединяются оператором |:
using System;
using System.Globalization;
int hex = int.Parse("1F", NumberStyles.HexNumber);
Console.WriteLine(hex); // 31
int big = int.Parse("1,234,567",
NumberStyles.AllowThousands, CultureInfo.InvariantCulture);
Console.WriteLine(big); // 1234567
По умолчанию int.Parse использует NumberStyles.Integer: пробелы по краям и знак перед числом допустимы, знак после числа ("42-") и разделители разрядов — нет. Поэтому int.Parse("1,234,567") без флага AllowThousands бросит FormatException.
Значение в строку: ToString, формат и интерполяция
Обратное направление — форматирование. Частые строки формата: N (число с разрядами), F (фиксированная точка), D (целое с ведущими нулями), X (шестнадцатеричное).
using System;
using System.Globalization;
var inv = CultureInfo.InvariantCulture;
double price = 1234.5;
Console.WriteLine(price.ToString("N2", inv)); // 1,234.50
Console.WriteLine(price.ToString("F1", inv)); // 1234.5
Console.WriteLine(7.ToString("D3")); // 007
Console.WriteLine(255.ToString("X")); // FF
С культурой ru-RU тот же price.ToString("N2", new CultureInfo("ru-RU")) даст 1 234,50, где между разрядами стоит неразрывный пробел (U+00A0), а не обычный. Если потом сравнить такую строку с "1 234,50", набранной с клавиатуры, сравнение вернет false. При этом double.Parse("1 234,50", ru) с обычным пробелом число разберет: для культур с неразрывным пробелом-разделителем .NET принимает и обычный.
Интерполяция $"{price:N2}" использует текущую культуру; для машинного текста есть FormattableString.Invariant($"...").
Даты, bool и перечисления
Даты — та же история с культурой, плюс формат. Когда формат строки известен точно, надежнее ParseExact/TryParseExact:
using System;
using System.Globalization;
bool ok = DateTime.TryParseExact("23.09.2026", "dd.MM.yyyy",
CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime d);
Console.WriteLine(ok); // True
Console.WriteLine(d.ToString("yyyy-MM-dd")); // 2026-09-23
В строке формата MM — месяц, mm — минуты. Перепутать их — частая ошибка: с форматом "dd.mm.yyyy" та же строка разберется без ошибки, но в 23 января 2026 года, 00:09 (09 ушло в минуты, месяц стал первым).
bool.Parse принимает только "True"/"False" (без учета регистра и с пробелами по краям): bool.Parse("true") вернет true, а bool.Parse("1") бросит FormatException. Перечисления разбирает Enum.TryParse:
using System;
if (Enum.TryParse("Friday", ignoreCase: true, out DayOfWeek day))
Console.WriteLine((int)day); // 5
Граница: Enum.TryParse вернет true и для строки "42", хотя такого элемента нет; нужна проверка Enum.IsDefined.
Строка в char[] и byte[]
Строка в C# неизменяема: методы вроде Replace или ToUpper возвращают новую строку. Чтобы менять символы по месту, строку переводят в массив char:
using System;
char[] chars = "hello".ToCharArray();
chars[0] = char.ToUpperInvariant(chars[0]);
Array.Reverse(chars);
Console.WriteLine(new string(chars)); // olleH
Для частых изменений длинного текста удобнее StringBuilder.
Цепочка «символ -> кодовая точка -> байты» показывает, почему string и byte[] — разные вещи. Буква Я — это кодовая точка U+042F; в UTF-8 она занимает два байта D0 AF:
using System;
using System.Text;
byte[] bytes = Encoding.UTF8.GetBytes("Я");
Console.WriteLine(Convert.ToHexString(bytes)); // D0AF
string back = Encoding.UTF8.GetString(bytes);
Console.WriteLine(back); // Я
byte[] ascii = Encoding.ASCII.GetBytes("Я");
Console.WriteLine(Encoding.ASCII.GetString(ascii)); // ?
Encoding.ASCII не умеет кириллицу и молча заменяет ее на ?. Данные потеряны, исключения нет. Convert.ToHexString появился в .NET 5; в старых версиях используют BitConverter.ToString(bytes), который дает D0-AF.
Если не получилось
| Симптом | Вероятная причина | Что сделать |
|---|---|---|
FormatException на "3.14" |
текущая культура с запятой | передать CultureInfo.InvariantCulture |
| На одном ПК работает, на сервере нет | разная культура ОС | явная культура во всех Parse и ToString |
OverflowException |
число вне диапазона int |
long.TryParse или decimal.TryParse |
| Пустое поле стало нулем | Convert.ToInt32(null) |
TryParse и проверка string.IsNullOrWhiteSpace |
CultureNotFoundException в Docker |
включен режим инвариантной глобализации | отключить InvariantGlobalization и установить ICU, либо работать только с InvariantCulture |
| Вопросики вместо кириллицы | Encoding.ASCII или несовпадение кодировок |
Encoding.UTF8 на обеих сторонах |
Выводы
- Разбор (строка -> значение), форматирование (значение -> строка) и кодирование (строка -> байты) — три разных механизма с разными API.
- Для ввода пользователя и внешних данных —
TryParse;Parse— когда плохие данные должны остановить программу;Convert.ToInt32(null)дает 0. - Числа и даты для машин разбирать и форматировать с
CultureInfo.InvariantCulture, для людей — с культурой пользователя. - Байты раскодируются только той кодировкой, которой их закодировали; ASCII теряет кириллицу молча.
Где применяется / связь с практикой
Преобразование строк встречается в любом C#-проекте: разбор форм и запросов в ASP.NET Core, чтение CSV и конфигов, вывод цен и дат. Ошибки культуры и кодировки обычно всплывают уже на сервере с другими настройками.
Освойте тему на практике
Системно разобрать типы, строки и работу с данными можно на курсе «C# Developer. Basic». Попробовать формат занятий бесплатно можно на открытых уроках Otus.
FAQ
Чем int.Parse отличается от приведения (int)?
Приведение (int) работает только между числовыми типами (и, например, char), строку так не преобразовать: (int)"42" не скомпилируется. Для строки нужен разбор.
Почему bool.Parse("1") не работает, а Convert.ToBoolean(1) работает?
Второй вызов преобразует число, а не строку: любое ненулевое число дает true. Строку "1" сначала разбирают в int.
Как разобрать число из части строки без Substring?
В .NET Core 2.1+ у int.Parse и int.TryParse есть перегрузки для ReadOnlySpan<char>: int.Parse("id=42".AsSpan(3)) вернет 42 без выделения новой строки.



