Эти занимательные региональные настройки

    Сегодня мы поговорим о региональных настройках. Но сперва — небольшая задачка: что выведет нижеприведённый код? (Код приведён на языке C#, но рассматривается достаточно общая проблематика, так что вы можете представить на его месте какой-нибудь другой язык.)

    Console.WriteLine((-42).ToString() == "-42");
    Console.WriteLine(double.NaN.ToString() == "NaN");
    Console.WriteLine(int.Parse("-42") == -42);
    Console.WriteLine(1.1.ToString().Contains("?") == false);
    Console.WriteLine(new DateTime(2014, 1, 1).ToString().Contains("2014"));
    Console.WriteLine("i".ToUpper() == "I" || "I".ToLower() == "i");
    

    Сколько значений true у вас получилось? Если больше 0, то вам не мешает узнать больше про региональные настройки, т. к. правильный ответ: «зависит». К сожалению, многие программисты вообще не задумываются о том, что настройки эти в различных окружениях могут отличаться. А выставлять для всего кода InvariantCulture этим программистом лениво, в результате чего их прекрасные приложения ведут себя очень странно, попадая к пользователям из других стран.Ошибки бывают самые разные, но чаще всего связаны они с форматированием и парсингом строк — достаточно частыми задачами для многих программистов. В статье приведена краткая подборка некоторых важных моментов, на которые влияют региональные настройки.

    CultureInfoExplorer Sceenshot

    Совсем немного теории: в .NET все сведения об определённом языке и региональных параметрах можно найти с помощью класса CultureInfo. Если вы ранее не сталкивались с культурами, то для первичного ознакомления хорошо подойдёт этот пост. Искушённый программист, увлечённый изучением различных существующих региональных настроек, может утомиться от ручного просмотра всех CultureInfo. Лично я в какой-то момент утомился. Поэтому появилось небольшое WPF-приложение под названием CultureInfoExplorer (ссылка на GitHub, бинарники), представленное на вышеприведённом скриншоте. Оно позволяет:
    • По данной CultureInfo посмотреть значение основных её свойств и то, как в ней выглядят некоторые заранее заготовленные строковые паттерны.
    • По данному свойству посмотреть его возможные значения и список всех CultureInfo, которые соответствуют каждому значению.
    • По данному паттерну посмотреть возможные варианты того, во что он может превратиться, и для каждого варианта также посмотреть список соответствующих CultureInfo.
    Надеюсь, найдутся читатели, которым данная программка будет полезна. Можно узнать много нового о различных региональных настройках. Ну, а теперь перейдём к примерам.

    Числа


    За представление чисел у нас отвечает NumberFormatInfo (доступный через CultureInfo.NumberFormat). И имеются в виду не только обычные числа, а также процентные и денежные значения. Обратите внимание на то, что значения бывают положительные и отрицательные: если вы работаете с локализацией/глобализацией, то важно обращать на это внимание. Настоятельно рекомендую хотя бы пробежаться глазами по документации и посмотреть доступные свойства.

    Одно из самых популярных свойств, которое вызывает проблемы у людей, называется NumberDecimalSeparator. Оно отвечает за то, чем будет при форматировании числа отделяться целая часть от дробной. Типичный пример ошибки: программист сливает массив дробных чисел в строчку, разделяя их запятыми. После этого он пытается распарсить строчку обратно в массив. Если NumberDecimalSeparator равен точке, то всё будет хорошо. Скажем, при выставленной культуре en-US у программиста всё заработало, он выпустил свой продукт. Этот продукт скачивает пользователь с культурой ru-RU и начинает грустить: ведь у него NumberDecimalSeparator равен запятой: массив из элементов 1.2 и 3.4 при таком слиянии превратится в строчку «1,2,3,4», а её распарсить будет проблемно. Лично мне становится ещё грустнее тогда, когда встретивший подобную проблему программист не пытается решить её нормально, указывая правильный NumberFormatInfo при форматировании, а начинает колдовать с заменами точек на запятые или запятых на точки. Нужно понимать, что NumberDecimalSeparator, в принципе, может быть любой. Например, в культуре fa-IR (Persian) он равен слешу ('/').

    Ещё в нашем распоряжении имеются аналогичные свойства для процентов и валют: PercentDecimalSeparator и CurrencyDecimalSeparator. Все эти три значения вовсе не обязаны совпадать. Например, у казахов (kk-KZ) NumberDecimalSeparator и PercentDecimalSeparator равны запятой, а CurrencyDecimalSeparator равен знаку минус (точно такому же, с помощью которого обозначаются отрицательные числа).

    Некоторые считают, что целое число при конвертации в строку даёт значение, состоящее только из цифр. Но цифры эти могут разбиваться на группы. За размер групп отвечает свойство NumberGroupSizes, а за их разделитель — NumberGroupSeparator (аналогичные свойства есть у процентов и валют, но они опять-таки не обязаны совпадать). Группы могут быть разного размера: например, во многих культурах (as-IN, bn-BD, gu-IN, hi-IN и т.п.) NumberGroupSizes равно {3, 2}. Скажем, число 1234567 в культуре as-IN будет выглядеть как «12,34,567». В качестве разделителя групп может выступать пробел \u0020 (например в af-ZA и lt-LT), но, увидев его, не торопитесь вбивать очередной костыль на парсинг и форматирование строк. Чаще всего вместо обычного пробела используется неразрывный пробел \u00A0 (наша родная ru-RU).

    Знаки для обозначения отрицательных и положительных чисел также входят в культуру: NegativeSign, PositiveSign. Слава богу, во всех доступных культурах они равны минусу и плюсу, но закладываться на это не стоит: окружение можно переопределить и задать свойствам любые значения. А самое интересное заключается не в знаках, а в паттернах форматирования положительных и отрицательных значений. Например, форматирование отрицательного числа определяется с помощью NumberNegativePattern, у которого есть пять возможных значений:

    0 (n)
    1 -n
    2 - n
    3 n-
    4 n -
    

    Например, в культуре ti-ET (Tigrinya (Ethiopia)) значение -5 предстанет в виде (5). С процентами и валютами (PercentNegativePattern, PercentPositivePattern, CurrencyNegativePattern, CurrencyPositivePattern) дело обстоит ещё веселее. Например, для CurrencyNegativePattern есть целых шестнадцать возможных значений:

    0  ($n)
    1  -$n
    2  $-n
    3  $n-
    4  (n$)
    5  -n$
    6  n-$
    7  n$-
    8  -n $
    9  -$ n
    10 n $-
    11 $ n-
    12 $ -n
    13 n- $
    14 ($ n)
    15 (n $)
    

    Также есть специальные свойства для специальных знаков и специальных численных значений: PercentSymbol, PerMilleSymbol, NaNSymbol, NegativeInfinitySymbol, PositiveInfinitySymbol. Мне доводилось видеть реальный проект, в котором брался double, форматировался в строку (разумеется, в текущей культуре пользователя), а затем в строковом виде сравнивался с «-Infinity». А в зависимости от этой самой текущей культуры NegativeInfinitySymbol может принимать самые разные значения:

    '- безкрайност', '-- អនន្ត', '(-) முடிவிலி', '-∞', '-Anfeidredd', '-Anfin', '-begalybė', '-beskonačnost', 'Éigríoch dhiúltach', '-ifedh', '-INF', '-Infini', '-infinit', '-Infinit', '-Infinito', '-Infinitu', '-infinity', 'Infinity-', '-Infinity', 'miinuslõpmatus', 'mínusz végtelen', '-nekonečno', '-neskončnost', '-nieskończoność', '-njekónčne', '-njeskóńcnje', '-onendlech', '-Sonsuz', '-tükeniksizlik', '-unendlich', '-Unendlich', '-Άπειρο', '-бесконачност', 'терс чексиздик', '-უსასრულობა', 'אינסוף שלילי', '-لا نهاية', 'منهای بی نهایت', 'مەنپىي چەكسىزلىك', '-අනන්තය', 'ᠰᠦᠬᠡᠷᠬᠦ ᠬᠢᠵᠠᠭᠠᠷᠭᠦᠢ ᠶᠡᠬᠡ', 'མོ་གྲངས་ཚད་མེད་ཆུང་བ།', 'ߘߊ߲߬ߒߕߊ߲߫-', 'ꀄꊭꌐꀋꉆ', '負無窮大', '负无穷大'
    

    Примеры разных полезных свойств мы разобрали. А теперь давайте немножко пошалим: чуть-чуть изменим русскую культуру, чтобы её новое значение портило нам жизнь в примере из начала поста:

    var myCulture = (CultureInfo)new CultureInfo("ru-RU").Clone();
    myCulture.NumberFormat.NegativeSign = "!";
    myCulture.NumberFormat.PositiveSign = "-";
    myCulture.NumberFormat.PositiveInfinitySymbol = "+Inf";
    myCulture.NumberFormat.NaNSymbol = "Not a number";
    myCulture.NumberFormat.NumberDecimalSeparator = "?";
    Thread.CurrentThread.CurrentCulture = myCulture;
    Console.WriteLine(-42); // !42
    Console.WriteLine(double.NaN); // Not a number
    Console.WriteLine(int.Parse("-42")); // 42
    Console.WriteLine(1.1); // 1?1
    

    Возможно, кто-то тут скажет мне: «Да зачем такие примеры вообще рассматривать? Ни один программист такое никогда писать не будет!». А я отвечу: «Ну-ну, ни один не будет, как же». Ситуация становится печальной, когда вы распространяете некоторую библиотеку, а один из её пользователей решил поразвлекаться с культурой. Может, он просто любит развлекаться, а может, пишет приложение для какой-то диковиной культуры (скажем, мёртвого или вымышленного языка). Но это не важно. А важно то, что ваша библиотека начинает вести себя странно в непривычном для неё окружении. Поэтому не стоит закладываться на то, что NegativeSign и PositiveSign никогда не меняются. Лучше просто явно указать нужную вам культуру и жить счастливо.

    А ещё, всем советую прочитать недавний пост Джона Скита The BobbyTables culture. Краткая суть: Джон Скит ругается на тех, кто не экранирует параметры в SQL-запросах, даже если это числа и даты. И тогда Джон берёт пару запросов

    "SELECT * FROM Foo WHERE BarDate > '" + DateTime.Today + "'"
    "SELECT * FROM Foo WHERE BarValue = " + (-10)
    

    и определяет чудо-культуру:

    CultureInfo bobby = (CultureInfo) CultureInfo.InvariantCulture.Clone();
    bobby.DateTimeFormat.ShortDatePattern = @"yyyy-MM-dd'' OR ' '=''";
    bobby.DateTimeFormat.LongTimePattern = "";
    bobby.NumberFormat.NegativeSign = "1 OR 1=1 OR 1=";
    

    Легким движением руки запросы превращаются в:

    SELECT * FROM Foo WHERE BarDate > '2014-08-08' OR ' '=' '
    SELECT * FROM Foo WHERE BarValue = 1 OR 1=1 OR 1=10
    

    Ну, думаю, дальнейшие пояснения не нужны.

    Дата и время


    С датами и временем всё особенно тяжело. За даты у нас отвечает класс DateTimeFormatInfo (свойство CultureInfo.DateTimeFormat), а в нём есть Calendar. Причём есть основной календарь культуры (CultureInfo), а есть список доступных для использования календарей (CultureInfo.OptionalCalendar). В нашем распоряжении имеется большая пачка стандартных календарей: ChineseLunisolarCalendar, EastAsianLunisolarCalendar, GregorianCalendar, HebrewCalendar, HijriCalendar, JapaneseCalendar, JapaneseLunisolarCalendar, JulianCalendar, KoreanCalendar, KoreanLunisolarCalendar, PersianCalendar, TaiwanCalendar, TaiwanLunisolarCalendar, ThaiBuddhistCalendar, UmAlQuraCalendar (у некоторых есть ряд дополнительных важных параметров). Логика у них, доложу я вам, самая разная. Не будем останавливаться подробно, ибо на эту тему подробной информации в интернете достаточно, а материала хватит на серию самостоятельных постов. Правила форматирования дат и времени ещё более весёлые, чем у чисел: куча паттернов для разных вариантов форматирования даты, нативные имена для месяцев и дней недели, обозначения для AM/PM, разделители и т.п. Скажем, 31 декабря 2014 года может быть представлено (dateTime.ToString(«d»)) в следующих форматах:

    09/03/36
    10/3/1436
    12/31/2014
    1436/3/10
    2014.12.21.
    2014/12/21
    2014-12-21
    31. 12. 2014
    31.12.14
    31.12.14 ý.
    31.12.2014
    31.12.2014 г.
    31.12.2014.
    31/12/14
    31/12/2014
    31/12/2557
    31-12-14
    31-12-2014
    31-дек 14
    31-жел-14
    

    И это только дефолтные значения (без подключения опциональных календарей). Но даже тут видно разнообразие летоисчислений: у кого-то на дворе 1436 год, а у кого-то — 2557 (это отсылка к предпоследней строчке примера из начала статьи). Если вы оперируете с датами, то следует задуматься: стоит ли их показывать всегда в одинаковом формате или же подстроиться под пользователя и отобразить дату в более привычном для него виде. Ну, а про парсинг дат я вообще умолчу.

    The Turkey Test


    Turkey flag

    Есть классический пост от 2008 года под называнием Does Your Code Pass The Turkey Test?. Подробно пересказывать его не буду, лучше самостоятельно прочитать оригинал. Краткая суть The Turkey Test такова: поменяйте текущую культуру на tr-TR (Turkish (Turkey)) и запустите ваше приложение. Всё ли нормально работает? В этой культуре хватает веселья и с датами, и с числами, и со строками. Если вернуться к нашему первому примеру, то в рассматриваемой культуре «i».ToUpper() не равно «I», а «I».ToLower() не равно «i» (если вам интересно больше узнать про заглавные и строчные буквы, то крайне рекомендую этот пост и вот этот SO-ответ про UTF-8, это просто прекрасно). В конце поста приводится замечательный пример, в котором под регулярное выражение \d{5} подходит строка состоящая из арабских цифр "٤٦٠٣٨".

    Вместо заключения


    Наука о региональных настройках сложна. В этом посте я ни в коем случае не претендую на то, чтобы выдать полную информацию о том, на что же они могут влиять. Есть ещё очень много разных интересностей, связанных с интернализацией (думаю, только про идущий справа налево текст можно написать отдельный пост, да и не один). Мне просто хотелось показать несколько занимательных примеров того, как CultureInfo.CurrentCulture может повлиять на ваше приложение. Надеюсь, в плане расширения общей эрудиции этот материал окажется кому-то полезным. Общая мораль такова: если вы не хотите думать о том, что в мире существует много разных культур, то используйте везде CultureInfo.InvariantCulture (или другую подходящую вам культуру) — в подавляющем большинстве случаев вы сможете спать спокойно. А если вы об этом задумываетесь, то неплохо бы поизучать эту область более основательно. В этом может помочь вот эта хорошая книжка: Net Internationalization: The Developer's Guide to Building Global Windows and Web Applications.

    Приветствуются любые дополнительные факты о том, как CultureInfo может повлиять на работу различных функций. Думаю, у многих найдутся собственные увлекательные истории.
    Enterra
    Company
    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 32

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

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

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




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

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

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

                      Не нужно вообще допускать моментов когда в коде существует число или дата в виде строки, кроме случаев когда это ввод/вывод пользователя еще не прошедший валидацию.
                        0
                        Согласен с вами. Если задачи сериализации всплывают во всём проекте, то явно нужно сидеть на хорошей, проверенной библиотеке.
                        Но вот какой момент есть: бывает, что на большой проект приходится одна единственная маленькая задачка на сериализацию. Стандартные решения из BCL не подходят, а тащить зависимость на внешнюю библиотеку ради единственной функции не хочется. И появляются всякие string.Join, int.Parse и т.п. Валидация пользовательского ввода — отдельная тема для обсуждения. Человечество уже давно придумало разные стандартные решения и библиотеки для валидации, но опять-таки, ради парсинга одного-двух полей многим лениво тянуть зависимости и в чём-то разбираться. И возникает «да я сейчас сам всё распарсю!» В определённых ситуациях это разумно, но при этом разработчик должен хорошо понимать, что же он делает, и как будет работать его чудо-парсер под различными культурами.
                        +2
                        Есть очень простое решение — делаем IDisposable, на входе устанавливаюший CultureInfo.InvariantCulture, а на выходе восстанавливающий старое значение.
                        После этого, всё, что нужно оформляем в блок using.
                        Класс тут:
                        gist.github.com/IUntyped/03d09f70c694b5481fbd
                          +1
                          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 — неочевидно.
                            +1
                            Собственно, будь всё это сделано не в комментарии, а как исправление на GitHub Gist'е — я бы и согласился (не со всем конечно, но это дело вкуса) и ещё душевно поблагодарил.

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

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

                              Only users with full accounts can post comments. Log in, please.