Преобразование строк в C#: Parse, TryParse, Convert, ToString и Encoding

Преобразование строк в C#: Parse, TryParse, Convert, ToString и Encoding Полезное

Преобразование строк в 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 без выделения новой строки.

OTUS Журнал
Скидка 5% 12-19 октября на курсы (popup)
Бесплатные открытые уроки (поп-ап)