Pull to refresh

Comments 32

Спасибо за пост, общую эрудицию и заинтересованность предметом точно прокачал. Тема-то универсальная ― не только про C#, да и не только про региональные настройки. Скорее про то, что хороший программист думает не только о коде перед глазами, но и о готовой программе как цельном произведении, которым будут пользоваться.
Спасибо за статью!
Многих моментов не знал, хотя и стараюсь быть аккуратным с такими вещами в разаботке.
Забавные (ну, кому как) факты всплывают в ASP.NET MVC: текущая культура потока зависит от заголовков, посланных браузером. При этом GET-параметры и ROUTE-параметры парсятся в нейтральной культуре (поскольку входят в URL страницы, а URL — он общий для всех); POST-параметры же парсятся в текущей культуре (что не лишено смысла — ведь они могут быть сформированы браузером в соответствии с его региональными настройками).

Поэтому никогда не забывайте писать .ToString(CultureInfo.InvariantCulture) формируя на сервере URL.
Спасибо за комментарий, очень ценная информация.
Например, у казахов (kk-KZ) NumberDecimalSeparator и PercentDecimalSeparator равны запятой, а CurrencyDecimalSeparator равен знаку минус

Я в шоке, впервые слышу.
Видимо у нас живут не правильные казахи




Или я чего то не догоняю.
Криво написанные программы перевоспитали нацию?..
Можете проверить сами:
Thread.CurrentThread.CurrentCulture = new CultureInfo("kk-KZ");
Console.WriteLine("{0:C}", 12.34); // ₸12-34

Нужно понимать отличие региональных настроек от того, как реально делают люди. В русской культуре число 1234.5 запишется в виде 1 234,5. Но всё равно зачастую пишут просто 1234.5. Мне думается, что со стороны казахов разумно отделять целую часть от дробной точками и запятыми — так будет понятней для основной аудитории. Однако, если вы будете формировать денежное значение программно с использованием культуры kk-KZ, то увидете минус посреди числа.
Меня вообще очень сильно смущают даже десятичные дроби в русском Excel. Например, мы в школе отделяли целую часть числа от дробной точкой.
Хотя, конечно, запятая в качестве десятичного разделителя уже стимулирует писать более правильный код :-).
Да не говорите, я сам постоянно смущаюсь во время работы с Excel. А с кодом всё будет хорошо, если выбрать одну целевую культуру на всё приложение и повсеместно её использовать.
Думаю, многие наши программисты в обиде на русскую культуру из-за этого. Выходишь за пределы исходников в реальный мир, а там запятая.
А я наоборот рад. Потому что сразу вылазит и бьет по рукам. В итоге ловишь это на самых первых этапах разработки.
Да нет, в реальном мире-то запятая привычна с детства. А вот когда в первый раз открываешь Excel после какого-нибудь паскаля, не выходя в «реальный мир», а там внезапно запятая
Да ладно, к Excel-то тоже привыкнуть можно. А вот когда (по незнанию) твоя собственная программа начинает на вход запятую требовать…
Проверить то не проблема

Просто никогда не обращал на это внимания. Да и не замечал такого, чтобы где-то было написано, например, «100-00 теңге».
Кстати, хотел найти соответствующее правило, или документ, но ничего не нашел.
Вообще этот вопрос чаще всего всплывает, если делают какую-нибудь наколенную сериализацию. Так что основной совет — писать/читать стандартные форматы через библиотеки. Не нужно клеить и парсить руками JSON/SQL/XML/CSV, не нужно придумывать свои форматы сериализации на пустом месте (те же самые числа через запятую).

Не нужно вообще допускать моментов когда в коде существует число или дата в виде строки, кроме случаев когда это ввод/вывод пользователя еще не прошедший валидацию.
Согласен с вами. Если задачи сериализации всплывают во всём проекте, то явно нужно сидеть на хорошей, проверенной библиотеке.
Но вот какой момент есть: бывает, что на большой проект приходится одна единственная маленькая задачка на сериализацию. Стандартные решения из BCL не подходят, а тащить зависимость на внешнюю библиотеку ради единственной функции не хочется. И появляются всякие string.Join, int.Parse и т.п. Валидация пользовательского ввода — отдельная тема для обсуждения. Человечество уже давно придумало разные стандартные решения и библиотеки для валидации, но опять-таки, ради парсинга одного-двух полей многим лениво тянуть зависимости и в чём-то разбираться. И возникает «да я сейчас сам всё распарсю!» В определённых ситуациях это разумно, но при этом разработчик должен хорошо понимать, что же он делает, и как будет работать его чудо-парсер под различными культурами.
Есть очень простое решение — делаем IDisposable, на входе устанавливаюший CultureInfo.InvariantCulture, а на выходе восстанавливающий старое значение.
После этого, всё, что нужно оформляем в блок using.
Класс тут:
gist.github.com/IUntyped/03d09f70c694b5481fbd
1. Dispose pattern в этом случае не нужен. Он вообще нужен, только если у вас неуправляемые ресурсы прямо в вашем классе проживают без обёрток (что не рекомендуется само по себе).
2. Если заменить class на struct, не нужно будет никаких плясок с бубном вокруг сборки мусора. И вообще меньше мусора будет. Правда будет «неправильный» конструктор по умолчанию, что минус.
3. Комментарии в стиле «Ваш КО» — зло. Вы замусориваете код и ничего не добавляете по делу. Лучше б один комментарий к классу написали про назначение и использование, чем ваши десять комментариев ни о чём.
4. У вас переменная со старой культурой залезла в регион «IDisposable». Я считаю регионы бредом, особенно в мелких классах, но если уж припёрло — хоть соблюдайте свои же регионы.

Достаточный код:

Код
    public class ForceCulture : IDisposable
    {
        private readonly CultureInfo _oldCulture;

        public ForceCulture (CultureInfo newCulture)
        {
            _oldCulture = Thread.CurrentThread.CurrentCulture;
            Thread.CurrentThread.CurrentCulture = newCulture ?? CultureInfo.InvariantCulture;
            GC.SuppressFinalize(this);
        }

        public void Dispose ()
        {
            Thread.CurrentThread.CurrentCulture = _oldCulture;
        }
    }

Или:

    public struct ForceCulture : IDisposable
    {
        private readonly CultureInfo _oldCulture;

        public ForceCulture (CultureInfo newCulture)
        {
            _oldCulture = Thread.CurrentThread.CurrentCulture;
            Thread.CurrentThread.CurrentCulture = newCulture ?? CultureInfo.InvariantCulture;
        }

        public void Dispose ()
        {
            Thread.CurrentThread.CurrentCulture = _oldCulture;
        }
    }

Я бы ещё добавил:

        public static ForceCulture Invariant
        {
            get { return new ForceCulture(null); }
        }

        public static ForceCulture Culture (CultureInfo culture)
        {
            return new ForceCulture(culture);
        }

        public static ForceCulture Name (string cultureName)
        {
            return new ForceCulture(CultureInfo.GetCultureInfo(cultureName));
        }

А то конструктор без параметров или принимающий null — неочевидно.
Собственно, будь всё это сделано не в комментарии, а как исправление на GitHub Gist'е — я бы и согласился (не со всем конечно, но это дело вкуса) и ещё душевно поблагодарил.

А так просто соглашаюсь и благодарю. )
Добавлю свои пять копеек.
Культура, возвращаемая методом CultureInfo.GetCultureInfo() и свойство CultureInfo.CurrenCulture — это две разные культуры, даже если в метод GetCultureInfo передать имя текущей культуры ОС. Метод GetCultureInfo возвращает культуру по умолчанию для указанного языка. Свойство CurrentCulture строится с учетом пользовательских региональных настроек, которые можно задать через панель управления Windows. При желании, в качестве разделителей и форматов можно указать вообще любые строки. При этом методы XXX.Parse, конечно, будут работать правильно.
С этим связана ошибка WPF. Механизм DataBinding использует внутри класс XmlLanguage, который в свою очередь использует SafeSecurityHelper, который уже вызывает CultureInfo.GetCultureInfo. При этом, само собой, настройки панели управления теряются. Поэтому с вероятностью 99% WPF приложение не будет реагировать на изменения региональных настроек в панели управления. Для решения этой проблемы в своё время написал небольшой патч.
XmlLanguage Fix
/// <summary>
/// Патч, который исправляет баг WPF, из-за которого игнорируются настройки локали, измененные в панели управления.
/// </summary>
public static class LocalePatch
{
    static LocalePatch()
    {
        CultureInfo currentCulture = CultureInfo.CurrentCulture;
        XmlLanguage lang = XmlLanguage.GetLanguage(currentCulture.Name);
        lang.GetEquivalentCulture();
        lang.GetSpecificCulture();

        Type langType = typeof(XmlLanguage);
        BindingFlags accessFlags =
            BindingFlags.ExactBinding | BindingFlags.SetField |
            BindingFlags.Instance | BindingFlags.NonPublic;

        FieldInfo field;
        field = langType.GetField("_equivalentCulture", accessFlags);
        field.SetValue(lang, currentCulture);
        field = langType.GetField("_specificCulture", accessFlags);
        field.SetValue(lang, currentCulture);
        field = langType.GetField("_compatibleCulture", accessFlags);
        field.SetValue(lang, currentCulture);

        FrameworkElement.LanguageProperty.OverrideMetadata(
            typeof(FrameworkElement), new FrameworkPropertyMetadata(lang));
    }

    /// <summary>
    /// Применить патч.
    /// </summary>
    /// <remarks>
    public static void Init()
    {   
    }
}



Помнится, писал об этой проблеме в Microsoft, лет эдак 5 назад. А воз и ныне там.
Зачем вы положили код в статический конструктор? Нетривиальный код в статическом конструкторе — зло. Особенно при рефлекшене по приватным полям фреймворка, которое может рухнуть в любой момент.
Легкий способ написать код, который должен выполниться не больше одного раза за время работы приложения. А что не так? Ну рухнет — выпадет исключение TypeLoadException из вызова Init. В любом случае, продолжение работы приложения при неудаче инициализации патча не предусмотрено.
Это настолько критический функционал, что надо всё приложение рушить?
Ха, ну вы завели речь про обработку исключений. Спорить об этом можно бесконечно, по тому что вопрос философский. Критично, если в виджете, показывающем погоду, будет стоять точка вместо запятой в градусах? А критично, если юзер вбил код 453225123535.124125 в поле подтверждения запуска ядерной ракеты, а ему вместо запуска выпадет сообщение о том, что строка имеет неверный формат?
В любом случае, от чего только приложение в котором этот код заюзан за 5 лет не падало, но только не от этого куска кода. Так что дело вкуса. На мой вкус — уж луче падает, чем иногда работает не как предполагалось.
Ничего философского. Если поместить этот код в просто статический метод, то поведение при ошибке выбирает само приложение. У вас же класс сам решает, что приложению нужно упасть, причём в произвольный момент (у CLR полная свобода выбора). Так как такой код обычно выполняется один раз при запуске, то, во-первых, защита от повторного запуска бесполезна, во-вторых, ошибка может запросто не попасть в лог.

В общем, это друной тон. Не упало здесь — упадёт в другом месте, где вы используете такой же «паттерн».
Свободы выбора у CLR нет, статические конструкторы вызываются из стека потока, который обращается к классу в первый раз. Это может быть обращение к статическому члену класса или создание первого экземпляра. Поведение статических конструкторов задокументировано.
Если из точки входа в приложение — метода Main не вызывать Init, то момент применения патча становится случайным, а если вызывать, то детерминированным. Единственное, что тут можно возразить, это что такой код требует от приложения, которое использует библиотеку, в которой расположен патч, что бы оно явно вызвало Init, иначе его может ожидать сюрприз. Ну да, расчет на то, что Init будет вызван.
По поводу выполнения. Как раз рекомендуется в статических конструкторах размещать код, который должен выполняться один раз, а при исключении не должно происходить попыток выполнить его еще раз. Т.е. если вызвать метод Init раз, то, если код выполнится успешно, можно вызвать Init еще какое угодно количество раз, при этом статический конструктор уже не выполнится. Если при выполнении возникнет исключение, то из метода выпадет TypeLoadException. Если перехватить исключение, то при повторных вызовах Init будет всегда выпадать TypeLoadExceptuion, без попыток повторно выполнить код. Это именно такое поведение, которого я добивался.
Таким паттерном я пользуюсь постоянно для написания инициализирующего кода.
Надо понимать, что первой строчкой в Main приложения при этом является подписка на событие AppDomain.UnhandledException, а следующими идет вызов инициализирующего кода, путем обращения к статическим методам классов-патчей (их несколько). Поэтому, всё таки приложение решает — обработать исключение или нет. И мимо лога ошибки не пролезут.
Более того, статические конструкторы очень удобны для использования в качестве кода инициализации плагинов, если требуется сделать приложение расширяемым. Просто достаточно пометить класс в сборке-плагине атрибутом, а из главного приложения обойти все классы в сборке и для помеченных атрибутом вызвать RuntimeHelpers.RunClassConstructor. На халяву получается защита от повторной инициализации плагина.
Не совсем на тему топика, но на смежную — рассказ об интернационализации продукта от Tom Scott:
А ещё никто не понимает разницу между CurrentCulture и CurrentUICulture. Ну то есть вообще никто. Мне пришлось локаль в системе переключать на британскую и впихивать в неё русские параметры форматирования.
Расскажите, в чем же там разница — если знаете.
Sign up to leave a comment.