Правильная работа с локалью в C#/.NET #
В этой статье описано, как сделать код устойчивым к смене локали и как проверить это в тестах.
Суть проблемы #
В C# / .NET почти все методы форматирования строк зависимы от текущей локали:
- метод
double.ToString()форматирует с учётом текущей локали - метод
string.Format(string format, ...)форматирует с учётом текущей локали - и даже интерполяция строк работает с учётом текущей локали
Это означает, что форматирование чисел в строку зависит от языка и страны, установленных в операционной системе (или в контейнере):
| Локаль | Как форматируется число |
|---|---|
| en-US | 3.14 |
| fr-FR | 3,14 |
| ru-RU | 3,14 |
Использование запятой для разделения целой и дробной части характерно для всех европейских локалей, включая русскую.
Как это проявляется #
Допустим, разработчик запускал свою программу или её тесты на системе, где стоит локаль “en-US”, и все ожидания настроены на форматирование по правилам английского языка и США: например, в проверках ожидается “3.14159” вместо “3,14159”.
Другой разработчик запустит программу или тесты на системе, где стоит локаль “ru-RU”. В результате поведение программы изменится, а тесты упадут.
Пример ошибки для XUnit (тест ожидает 6.28, а в системе установлена локаль 6.28):
/home/user/my-compiler/tests/Interpreter.IntegrationTests/ExpressionsTest.cs(18): error TESTERROR:
Interpreter.IntegrationTests.ExpressionsTest.Can_evaluate_expressions(code: "fn main(): int { printf(3.14 * 2, 2); return 0; }", expected: "6.28") (< 1ms): Сообщение об ошибке: Assert.Equal() Failure: Strings differ
↓ (pos 1)
Expected: "6.28"
Actual: "6,28"
↑ (pos 1)
Трассировка стека:
at Interpreter.IntegrationTests.ExpressionsTest.Can_evaluate_expressions(String code, String expected) in /home/dev/teaching/compilers2026/rrtry-PS-VM/tests/Interpreter.IntegrationTests/ExpressionsTest.cs:line 18
at InvokeStub_ExpressionsTest.Can_evaluate_expressions(Object, Span`1)
at System.Reflection.MethodBaseInvoker.InvokeWithFewArgs(Object obj, BindingFlags invokeAttr, Binder binder, Object[] parameters, CultureInfo culture)
Как выявить проблему в тестах #
Плохой способ: глобальная смена локали #
Можно поменять локаль глобально:
- поменять локаль в операционной системе
- или установить переменную окружения
DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=1перед запускомdotnet testдля проверки сCultureInfo.InvariantCulture
Однако, глобальная смена локали может лишь замаскировать проблему.
Хороший способ: атрибуты XUnit v3 #
В XUnit v3 появились атрибуты CulturedTheory и CulturedFact, позволяющие запустить тест с одной или несколькими локалями для проверки способности кода работать в любых условиях.
Если проект ещё использует XUnit v2, то можно отредактировать *.csproj для обновления на XUnit v3.
Допустим, у вас в проекте указано:
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>
Замените пакет xunit на xunit.v3 и обновите версию до актуальной (не забудьте обновить версию xunit.runner.visualstudio):
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="xunit.v3" Version="3.2.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5" />
</ItemGroup>
Теперь можно дописать тесты, заменяя:
- атрибут
[Theory]на[CulturedTheory(["ru-RU", "en-US"])] - атрибут
[Fact]на[CulturedFact(["ru-RU", "en-US"])]
Пример теста, который будет запущен дважды с двумя кульутрами:
[CulturedTheory(["ru-RU", "en-US"])]
[MemberData(nameof(GetTypeMismatch))]
public void Can_interpret_variable_type_mismatch(string code, Type expected)
{
FakeEnvironment environment = new();
Interpreter interpreter = new(environment);
Assert.Throws(expected, () => interpreter.Execute(code));
}
Как исправить код #
Для компилятора или интерпретатора обычно требуется независимое от локали поведение: например, литерал числа с плавающей точкой всегда записывается в стиле локали en-US — то есть “3.14159”.
В таких случаях используйте CultureInfo.InvariantCulture для всех методов форматирования и разбора строк.
Примеры:
value.ToString(CultureInfo.InvariantCulture)double.Parse(s, CultureInfo.InvariantCulture)_outputBuffer.Append(value.ToString(CultureInfo.InvariantCulture))
Перегрузки, принимающие объект CultureInfo, есть у всех методов форматирования и разбора, включая классы вроде StringBuilder.
Ссылки #
Дополнительная информация есть по ссылкам:
- What does CultureInfo.InvariantCulture mean?
- A Primer on CultureInfo and Internationalisation in C#/.NET
- Обновление XUnit, в котором появились
CulturedTheoryиCulturedFact: Core Framework v3 3.0.1