Класс CultureInfo — один из наиболее широко используемых в Microsoft .NET Framework. Объекты этого типа применяются при загрузке ресурсов, форматировании, синтаксическом разборе, изменении регистра букв, сортировке и других преобразованиях, выполняемых
Я рассмотрю некоторые такие случаи и предоставлю вам достаточно информации о поведении класса, лучших методиках его использования и последствиях принятия неправильных решений. Прочитав статью, вы сможете сделать правильный выбор, когда будете работать с CultureInfo и связанными с ним классами пространства имен System.Globalization в своих будущих проектах.
Все начинается с создания объекта, и объект CultureInfo можно получить многими способами. Можно использовать культуры, задаваемые свойствами CultureInfo.CurrentCulture, CultureInfo.CurrentUICulture и CultureInfo.InvariantCulture. Кроме того, доступен встроенный объект CultureInfo, соответствующий выбранным или установленным методам ввода (input methods). Наконец, можно создать экземпляр CultureInfo в приложении или вообще не использовать никакой культуры.
Экземпляр CultureInfo, возвращаемый свойством CultureInfo.CurrentCulture, соответствует региональным стандартам, выбранным пользователем в апплете Regional Options (рис. 1). С точки зрения программиста, здесь выбирается локализующий идентификатор (locale), а в Windows XP и Windows Server 2003 в Standards and Formats этот параметр называется языком (language). Значение CultureInfo.CurrentCulture можно изменить в .NET на уровне потока, но, когда пользователь меняет язык в Regional Options, значение CultureInfo.CurrentCulture в приложении, выполняемом в этот момент, не изменится. Не обнаруживаются даже изменения, вносимые в отдельные параметры, если только приложение не вызвало метод CultureInfo.ClearCachedData.
Рис. 1. CurrentCulture представляет параметры, задаваемые в Regional Options
К сожалению, специального события, инициируемого при
изменении параметров в Regional Options, нет. Есть событие,
генерируемое при смене текущего языка ввода, но для изменения общих
параметров такого события не предусмотрено. Однако Windows отправляет
широковещательное сообщение WM_SETTINGSCHANGE всякий раз, когда
пользователь изменяет эти параметры. С помощью кода приложение для .NET Framework может реагировать
на изменения в этом контексте. Код прослушивает соответствующие
сообщения, а именно WM_SETTINGCHANGE, у которых LPARAM содержит
строку «intl». Получив это сообщение, код вносит соответствующие изменения
во все или отдельные параметры региональных стандартов. Если
private const int WM_SETTINGCHANGE = 0x001A; [DllImport("kernel32.dll", ExactSpelling=true)] private static extern int GetUserDefaultLCID(); CultureInfo m_ciOld = new CultureInfo(GetUserDefaultLCID()); protected override void WndProc(ref Message m) { switch(m.Msg) { // Изменение параметра системы или политики case WM_SETTINGCHANGE: if(m.LParam != IntPtr.Zero) { int localeCur = GetUserDefaultLCID(); string val = Marshal.PtrToStringAuto(m.LParam); if(val == "intl") { // Изменение параметров региональных стандартов Thread thread = Thread.CurrentThread; if(thread.CurrentCulture.LCID != localeCur() && thread.CurrentCulture.LCID == m_ciOld.LCID) { // Если изменены параметры региональных стандартов, // используемых по умолчанию, изменяем текущую // культуру thread.CurrentCulture = new CultureInfo(localeCur); } else { // Если изменен отдельный параметр, очищаем // кэшированные данные, чтобы отразить это // изменение thread.CurrentCulture.ClearCachedData(); } m_ciOld = new CultureInfo(localeCur); } } break; } base.WndProc(ref m);
Принимая решение использовать CurrentCulture или какие-либо другие объекты, учтите, что CurrentCulture — это параметры представления даты и времени, форматирования чисел и сортировки, выбранные пользователем.
Начальное значение объекта CurrentUICulture определяется языком интерфейса Windows. Если в Windows встроена поддержка MUI (Multilingual User Interface), это язык, выбираемый пользователем, в ином случае это язык, под который локализована Windows. При разработке приложения вы создаете UI и определяете, какой язык (или языки) оно будет поддерживать. Вы даже можете написать собственный UI для выбора языка, если хотите, чтобы пользователи имели возможность выбрать один из доступных языков. В большинстве случаев применение CurrentUICulture имеет смысл, только если вы поддерживаете выбор языка своего UI. Обычно CurrentUICulture — то же, что и CurrentCulture, но иногда они отличаются. Всегда учитывайте это, поддерживая несколько языков UI.
Язык ввода — это пара «культура/раскладка клавиатуры», задающая соответствие между клавишами на клавиатуре и символами языка. Класс System.Windows.Forms.InputLanguage предоставляет следующие возможности:
В Windows Forms также имеются два события — InputLanguageChanging и InputLanguageChanged, при каждом из которых передается объект InputLanguage, содержащий CultureInfo. Этот объект CultureInfo может оказаться полезным при любых операциях, при которых учитывается язык ввода. Например, можно сохранить информацию о языке или выбрать соответствующий словарь или справочник.
Во многих случаях может потребоваться объект, который не зависит от пользовательских параметров или изменений в параметрах приложения. С этой целью было введено новое свойство InvariantCulture.
Помимо получения и использования одной из культур (о чем рассказывалось выше), бывают случаи, когда правильный выбор — не использовать никакую культуру (даже инвариантную). Некоторые из этих случаев рассматриваются ниже. Важно понимать, когда требуется сделать такой выбор.
Хотя в большинстве приложений этого не нужно,
иногда возможность просто создать объект CultureInfo, не обязательно
соответствующий каким-либо пользовательским параметрам, оказывается очень
полезной. Вот некоторые из таких случаев: перечисление доступных для
выбора объектов CultureInfo; проверка того, как ведет себя приложение при
задании других параметров культуры; воспроизведение ошибок, возникающих
при использовании определенной культуры; создание объектов, присваиваемых
текущей культуре или текущей культуре UI в зависимости
от некоей внешней информации (например HTTP-запросов
к
Изменение регистра букв — пожалуй, самая простая
операция, тем не менее она часто влечет за собой ошибки
Номер |
Символ |
Кодовая позиция в Unicode |
Название символов в UI |
---|---|---|---|
1 |
I |
U+0049 |
Latin Capital Letter I |
2 |
ı |
U+0131 |
Latin Small Letter Dotless I |
3 |
İ |
U+0130 |
Latin Capital Letter I With Dot Above |
4 |
i |
U+0069 |
Latin Small Letter I |
Если вы выполните преобразование в верхний или нижний регистр по правилам для тюркских языков, а затем попытаетесь сравнить полученные в результате строки, не учитывая эти правила, возникнут серьезные проблемы с идентификацией строк.
Принимая решение, использовать ли правила изменения регистра букв для тюркских языков, важно иметь в виду конкретную ситуацию. Очевидно, что турецкие и азербайджанские пользователи рассчитывают, что, когда строки показываются на экране, при изменении регистра букв применяются правила для тюркских языков. Настолько же очевидно, что при любых операциях, связанных с именами файлов и разделов реестра, эти правила не годятся. Важно отличать данные, показываемые пользователю, от данных, которые должны быть совместимы с внешними источниками и средой, в которой работает приложение.
В других культурах и других известных мне языках альтернативных правил изменения регистра букв нет. Однако, если вы обеспечите корректную работу приложения в этом исключительном случае, вы справитесь и с любыми культурами, которые появятся в будущем. Правильное применение CurrentCulture для турецкого языка позволит добиться, что приложение будет вести себя так, как ожидают пользователи, при работе с любой будущей культурой с теми же особенностями.
Рассказывая о сравнении (collation), я буду рассматривать объект CultureInfo, но все методы, относящиеся к сравнению, реализованы в классе System.Globalization.CompareInfo. Однако, чтобы получить CompareInfo, нужно либо обратиться к соответствующему свойству класса CultureInfo, либо передать имя или идентификатор культуры статическому методу CompareInfo.GetCompareInfo. Поэтому мне будет проще пояснить, как выбрать метод сравнения, если я буду использовать понятия из области культур.
Сравнение (или сортировка) — просто упорядочение элементов. Это одна из главных операций, и все пользователи рассчитывают на то, что она выполняется правильно. В идеале сравнение должно быть полностью прозрачным. Щелкая заголовок списка (как в Windows Explorer), пользователь предполагает, что будет выполнена сортировка по этому столбцу в соответствии с его языком и культурой.
К счастью, большая часть работы уже сделана, и вам нужно лишь выбрать соответствующую культуру и параметры. Существует три свойства, с помощью которых можно корректно выполнить сравнение практически в любой мыслимой ситуации: CurrentCulture, InvariantCulture и CompareOptions.Ordinal.
CurrentCulture
Применяется, когда пользователю
показываются какие-либо данные и требуется отсортировать или сравнить
их по интуитивно понятному алгоритму. Это свойство используется
по умолчанию, но лучше указать его явно — тогда
не будут выводиться предупреждения при анализе кода средствами вроде
FxCop, и вы будете знать, что задали все параметры.
InvariantCulture
Используется, когда результаты
сравнения должны быть правильными с лингвистической точки зрения,
но при этом они не должны меняться при изменении
пользовательских параметров. При сортировке такое требование встречается
редко, а при сравнении — гораздо чаще.
CompareOptions.Ordinal
Флаг сравнения
по кодам символов (как и новый флаг OrdinalIgnoreCase, введенный
в .NET Framework 2.0), применяемый для сравнения двоичных
представлений строк без каких-либо изменений. Такое сравнение
не только выполняется быстрее, но и лучше подходит, если
символы, не учитываемые при сортировке (например форматирующие
символы, задающие направление при выводе в двух направлениях),
не должны игнорироваться.
Кроме того, возможен четвертый случай — когда необходимо решить, одинаковы ли имена файлов. К сожалению, ни одно из трех свойств, описанных выше, не позволяет совершенно точно ответить на этот вопрос независимо от того, какой код используется — управляемый или неуправляемый. Единственный способ получить правильный ответ — попытаться создать файл и, если файл уже есть, перехватить исключение.
Далее я расскажу о загрузке ресурсов, о кодировках, для которых поддерживаются параметры региональных стандартов, и о методах ввода. Скорее всего вы найдете эту функциональность более понятной, и при ее использовании ошибки, в которых сложно разобраться, менее вероятны.
Существует два метода разбора (parsing methods), применяемых
при разборе строк: Parse и ParseExact. Функциональность метода Parse
была реализована еще в COM (которая сама уходит своими корнями
в старые версии Visual Basic). При ее реализации стремились
любой ценой обеспечить преобразование строки в дату. Поэтому
у нее есть неприятный побочный эффект, с которым сталкиваются
программисты, работающие с двумя форматами дат —
дд/мм/гг и мм/дд/гг: разбор может быть выполнен
некорректно. Метод DateTime.Parse, реализованный в Microsoft .NET
Framework, решает во многом те же задачи, что и его
предшественники, но, к сожалению, унаследовал и некоторые
их недостатки. Код работает медленнее
В отличие от Parse метод DateTime.ParseExact принимает точные форматы, заданные в объекте DateTimeFormatInfo, и использует только их. Строки, не соответствующие формату, не допускаются. А по вопросу, допускать или нет лишние пробелы, в Microsoft наверняка была весьма интересная дискуссия. Однако метод ParseExact не просто позволяет потребовать: «Вот формат, вот строки в этом формате — выполни свою работу». Он осуществляет разбор быстрее и точнее с семантической точки зрения, поэтому лучше подходит в тех случаях, когда не нужна гибкость метода DateTime.Parse.
Когда возникает необходимость в разборе строк, не исключено, что имеет смысл использовать ParseExact вместо Parse (если это возможно), чтобы сделать код безопаснее. Гибкость — вещь замечательная тем, где она действительно нужна, в ином случае лучше не рисковать и постараться избежать проблем, связанных с ее обеспечением. На врезке «Parse и ParseExact» рассказывается о различиях между этими методами.
Из всех областей применения культур наиболее сложная — функции форматирования и разбора, при работе с которыми возникает больше всего проблем. В идеале это взаимно обратные операции, поэтому я рассматриваю их вместе. Имеется два случая, когда операции разбора и форматирования не являются противоположными. Первый — для преобразования числа, даты или времени в строку применяется собственный формат, а при разборе строки и получении данных используется другая форматирующая строка. Второй — форматирующая строка отличается от применяемой по умолчанию и используется метод Parse, а не ParseExact, поскольку логика метода Parse такова, что в некоторых случаях информация может считываться неправильно.
Сердцевиной поддержки форматирования и разбора является интерфейс IFormatProvider, который реализуют многие классы пространства имен System.Globalization (в том числе CultureInfo, NumberFormatInfo и DateTimeFormatInfo). У этого интерфейса единственный метод — GetFormat, с помощью которого код разбора и форматирования получает сведения о формате. У большинства методов форматирования и разбора есть минимум одна перегруженная версия, принимающая IFormatProvider.
Тот факт, что используется один и тот же
интерфейс, может сыграть злую шутку. Код должен обрабатывать ситуацию,
когда методу DateTime.Parse передается NumberFormat; в этом случае
параметр игнорируется. (Однако в текущих версиях FxCop в таких
случаях не выводится предупреждение о некорректной работе
с IFormatProvider.) Та же проблема возможна при форматировании
или передаче объекта DateTimeFormatInfo методам форматирования
и разбора чисел. Обычно я советую передавать объект CultureInfo,
поскольку по нему всегда можно получить более специфичные классы
DateTimeFormatInfo и NumberFormatInfo. Исключением является случай,
когда используется специально подготовленный форматирующий объект,
в корректности которого вы уверены. Будем надеяться, что
в следующих версиях FxCop будет предупреждать о передаче
параметра, который является ничем иным, как более медленной версией NULL
При разработке приложений, которые должны поддерживать разные языки, следуйте простому правилу: по умолчанию используйте CurrentUICulture. Если вы локализуете приложение ASP.NET или Windows Forms, методы загрузки ресурсов по умолчанию задействуют эту культуру. Конечно, можно переопределить это поведение, передав объект CultureInfo, в котором указан другой язык.
Задавайте CurrentUICulture в соответствии
с ожиданиями пользователя. Какой бы язык UI ни был
установлен в Windows,
Для поддержки кодировок служат класс System.Text.Encoding и другие классы, поддерживающие его интерфейсы. На эту тему можно написать много статей, но поддержка, зависящая от культуры, ограничена следующими кодовыми страницами каждого языка: ANSI, OEM, Mac и EBCDIC. На практике, когда надо преобразовать какой-либо текст в унаследованную кодовую страницу или из нее, вы должны точно знать кодовую страницу, а не пытаться определить ее по культуре. Поэтому от работы с кодировками на основе культур не так уж много пользы. Лучше всего работать с кодировкой, исходя из того, к какой кодовой странице она относится, и выполнять преобразование именно для той кодовой страницы, в которой хранится текст.
Для поддержки языков ввода (также называемых методами ввода)
служит класс InputLanguage. Возможности этого класса ограничены, поскольку
.NET Framework позволяет использовать или выбирать только языки, уже
установленные пользователем. У каждого объекта InputLanguage есть
свойство Handle. Описатели не всегда имеют строго определенный размер
(как, например, на
При разработке приложений данные о культуре могут
оказаться полезными, когда программа должна выбрать
Если вы пойдете по этому пути, возможно, вам потребуется логика работы с символами, основанная на том, какие символы вводятся, а не только на том, какой язык выбран. Тогда вы избежите проблем в ситуации, справиться с которой не в состоянии даже Word, — подключение клавиатуры, язык которой не имеет ничего общего с используемым языком (например установлен венгерский язык, а клавиатура поддерживает арабский). .NET Framework не позволяет определить язык клавиатуры, но это можно сделать с помощью P/Invoke (листинг 2).
[DllImport("user32.dll")] private static extern bool GetKeyboardLayoutName( StringBuilder pwszKLID); private const int KL_NAMELENGTH = 9; private CultureInfo CultureOfCurrentLayout() { StringBuilder sb = new StringBuilder(KL_NAMELENGTH); if(GetKeyboardLayoutName(sbKLID)) { int klid = int.Parse( sbKLID.ToString().Substring(KL_NAMELENGTH - 1), NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture); // Оставляем только младшую половину числа klid &= 0xffff; return new CultureInfo(klid, false); } return(null); }
Эта функция возвращает объект CultureInfo, представляющий язык, регион и систему письма, для которой предназначена клавиатура.
Культуры только для Windows
В .NET Framework
поддерживается больше культур, чем в различных версиях Windows.
Однако в Windows XP SP2 добавлено 25 новых региональных
стандартов, которых нет в .NET Framework.
В .NET Framework 2.0 эта проблема будет решаться следующим образом. При любой попытке создать культуру, недоступную в Framework, но доступную в Windows, будет синтезироваться объект CultureInfo. При соблюдении всех принципов, рассмотренных в предыдущих разделах, ваши приложения будут корректно работать с этими объектами CultureInfo «только для Windows».
Новое, усовершенствованное перечислимое
CultureTypes
В .NET Framework 1.x перечислимое CultureTypes было
определено так:
[Flags] public enum CultureTypes { NeutralCultures = 0x0001, SpecificCultures = 0x0002, InstalledWin32Cultures = 0x0004, AllCultures = NeutralCultures | SpecificCultures | InstalledWin32Cultures, }
Такое определение оставляло не так уж много места для добавления новых типов культур (только для Windows, Custom и Replacement). В .NET Framework 2.0 будет использоваться усовершенствованное перечислимое CultureTypes, показанное на листинге. 3.
public enum CultureTypes { // Нейтральные культуры, такиe как "en", "de" и "zh" NeutralCultures = 0x0001, // Отличные от нейтральных культуры, // например "en-us" и "zh-tw" SpecificCultures = 0x0002, // Культуры, существующие и в Win32, и в Framework InstalledWin32Cultures = 0x0004, AllCultures = NeutralCultures | SpecificCultures | InstalledWin32Cultures, // Собственная пользовательская культура UserCustomCulture = 0x0008, // Пользовательская культура, замещающая собственную ReplacementCultures = 0x0010, // Культура, существующая в Win32, но не в Framework WindowsOnlyCultures = 0x0020, // Тэг языка, который соответствует культуре, входящей // в Framework FrameworkCultures = 0x0040, }
Такое перечислимое не только поддерживает новые типы
культур, но и допускает дальнейшее расширение. Существующий код,
который использует AllCultures,
Возможность создания собственных культур также является новшеством .NET Framework 2.0. Для создания новых культур служит класс CultureAndRegionInfoBuilder. При соблюдении принципов, изложенных в этой статье, вероятность того, что ваше управляемое приложение будет корректно работать с собственными культурами, значительно повысится!
CultureInfo — класс с обширными возможностями, в котором отражены почти все стороны взаимодействия пользователей с компьютерами и приложениями. Поскольку это очень мощный класс, принимая решение, какой объект CultureInfo подходит в каждом конкретном случае, важно сделать правильный выбор. Во многих случаях, потратив немного больше времени на обдумывание, вы значительно повысите эффективность применения класса CultureInfo.