Многоликий класс CultureInfo — .NET-приложения станут дружелюбнее к пользователю


Класс CultureInfo — один из наиболее широко используемых в Microsoft .NET Framework. Объекты этого типа применяются при загрузке ресурсов, форматировании, синтаксическом разборе, изменении регистра букв, сортировке и других преобразованиях, выполняемых по-разному в зависимости от языка, региона или системы письма. Это относительно сложный класс, использование которого в каждом конкретном случае может оказаться непростым делом.

Я рассмотрю некоторые такие случаи и предоставлю вам достаточно информации о поведении класса, лучших методиках его использования и последствиях принятия неправильных решений. Прочитав статью, вы сможете сделать правильный выбор, когда будете работать с CultureInfo и связанными с ним классами пространства имен System.Globalization в своих будущих проектах.

Все начинается с создания объекта, и объект CultureInfo можно получить многими способами. Можно использовать культуры, задаваемые свойствами CultureInfo.CurrentCulture, CultureInfo.CurrentUICulture и CultureInfo.InvariantCulture. Кроме того, доступен встроенный объект CultureInfo, соответствующий выбранным или установленным методам ввода (input methods). Наконец, можно создать экземпляр CultureInfo в приложении или вообще не использовать никакой культуры.

Получение 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.

Изображение JPG  Рис. 1. CurrentCulture представляет параметры, задаваемые в Regional Options

К сожалению, специального события, инициируемого при изменении параметров в Regional Options, нет. Есть событие, генерируемое при смене текущего языка ввода, но для изменения общих параметров такого события не предусмотрено. Однако Windows отправляет широковещательное сообщение WM_SETTINGSCHANGE всякий раз, когда пользователь изменяет эти параметры. С помощью кода приложение для .NET Framework может реагировать на изменения в этом контексте. Код прослушивает соответствующие сообщения, а именно WM_SETTINGCHANGE, у которых LPARAM содержит строку «intl». Получив это сообщение, код вносит соответствующие изменения во все или отдельные параметры региональных стандартов. Если .NET-приложение должно реагировать на изменения, вносимые пользователем в панели управления, старайтесь применять подход, как на листинге. 1.

Листинг 1. Обработка изменений параметров региональных стандартов

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 — это параметры представления даты и времени, форматирования чисел и сортировки, выбранные пользователем.

Получение CultureInfo по языку UI

Начальное значение объекта CurrentUICulture определяется языком интерфейса Windows. Если в Windows встроена поддержка MUI (Multilingual User Interface), это язык, выбираемый пользователем, в ином случае это язык, под который локализована Windows. При разработке приложения вы создаете UI и определяете, какой язык (или языки) оно будет поддерживать. Вы даже можете написать собственный UI для выбора языка, если хотите, чтобы пользователи имели возможность выбрать один из доступных языков. В большинстве случаев применение CurrentUICulture имеет смысл, только если вы поддерживаете выбор языка своего UI. Обычно CurrentUICulture — то же, что и CurrentCulture, но иногда они отличаются. Всегда учитывайте это, поддерживая несколько языков UI.

Получение CultureInfo по методам ввода

Язык ввода — это пара «культура/раскладка клавиатуры», задающая соответствие между клавишами на клавиатуре и символами языка. Класс System.Windows.Forms.InputLanguage предоставляет следующие возможности:

В Windows Forms также имеются два события — InputLanguageChanging и InputLanguageChanged, при каждом из которых передается объект InputLanguage, содержащий CultureInfo. Этот объект CultureInfo может оказаться полезным при любых операциях, при которых учитывается язык ввода. Например, можно сохранить информацию о языке или выбрать соответствующий словарь или справочник.

Во многих случаях может потребоваться объект, который не зависит от пользовательских параметров или изменений в параметрах приложения. С этой целью было введено новое свойство InvariantCulture.

Помимо получения и использования одной из культур (о чем рассказывалось выше), бывают случаи, когда правильный выбор — не использовать никакую культуру (даже инвариантную). Некоторые из этих случаев рассматриваются ниже. Важно понимать, когда требуется сделать такой выбор.

Создание объектов CultureInfo

Хотя в большинстве приложений этого не нужно, иногда возможность просто создать объект CultureInfo, не обязательно соответствующий каким-либо пользовательским параметрам, оказывается очень полезной. Вот некоторые из таких случаев: перечисление доступных для выбора объектов CultureInfo; проверка того, как ведет себя приложение при задании других параметров культуры; воспроизведение ошибок, возникающих при использовании определенной культуры; создание объектов, присваиваемых текущей культуре или текущей культуре UI в зависимости от некоей внешней информации (например HTTP-запросов к ASP.NET-странице). Теперь, когда вы знаете, как получать объекты CultureInfo, рассмотрим операции, которые можно выполнить с помощью этих объектов.

Преобразование в верхний или нижний регистр букв

Изменение регистра букв — пожалуй, самая простая операция, тем не менее она часто влечет за собой ошибки из-за того, что разработчики неправильно ее понимают. Изменение регистра букв — это операция, при которой текст переводится в верхний или нижний регистр. Эта операция требует особого внимания в случае турецкого и азербайджанского языков, где используются правила тюркских языков. Как видно из табл. 1, в этих двух культурах варианты 1 и 2 задают представление одних и тех же символов в разных регистрах. То же самое относится к вариантам 3 и 4. Во всех остальных культурах для представления одних и тех же букв в разных регистрах используются варианты 1 и 4.

Табл. 1. Правила изменения регистра символов для тюркских языков
Номер
Символ
Кодовая позиция в 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), применяемый для сравнения двоичных представлений строк без каких-либо изменений. Такое сравнение не только выполняется быстрее, но и лучше подходит, если символы, не учитываемые при сортировке (например форматирующие символы, задающие направление при выводе в двух направлениях), не должны игнорироваться.

Кроме того, возможен четвертый случай — когда необходимо решить, одинаковы ли имена файлов. К сожалению, ни одно из трех свойств, описанных выше, не позволяет совершенно точно ответить на этот вопрос независимо от того, какой код используется — управляемый или неуправляемый. Единственный способ получить правильный ответ — попытаться создать файл и, если файл уже есть, перехватить исключение.

Далее я расскажу о загрузке ресурсов, о кодировках, для которых поддерживаются параметры региональных стандартов, и о методах ввода. Скорее всего вы найдете эту функциональность более понятной, и при ее использовании ошибки, в которых сложно разобраться, менее вероятны.

Parse и ParseExact

Существует два метода разбора (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, Web-серверы определяют язык пользователя по заголовку HTTP_ACCEPT_LANGUAGE или по выбору, сделанному пользователем в интерфейсе вашего приложения, т. е. язык, определенный Web-сервером, полностью зависит от вашего приложения. Хорошее правило — следовать той же модели, что и в Windows MUI: выводить список языков, поддерживаемых приложением, показывая «родное» название каждого языка (CultureInfo.NativeName). Этот весьма простой подход вполне оправдан: если пользователь не может прочитать название языка, как же он перенастроит интерфейс на этот язык?

Кодировки

Для поддержки кодировок служат класс System.Text.Encoding и другие классы, поддерживающие его интерфейсы. На эту тему можно написать много статей, но поддержка, зависящая от культуры, ограничена следующими кодовыми страницами каждого языка: ANSI, OEM, Mac и EBCDIC. На практике, когда надо преобразовать какой-либо текст в унаследованную кодовую страницу или из нее, вы должны точно знать кодовую страницу, а не пытаться определить ее по культуре. Поэтому от работы с кодировками на основе культур не так уж много пользы. Лучше всего работать с кодировкой, исходя из того, к какой кодовой странице она относится, и выполнять преобразование именно для той кодовой страницы, в которой хранится текст.

Языки ввода

Для поддержки языков ввода (также называемых методами ввода) служит класс InputLanguage. Возможности этого класса ограничены, поскольку .NET Framework позволяет использовать или выбирать только языки, уже установленные пользователем. У каждого объекта InputLanguage есть свойство Handle. Описатели не всегда имеют строго определенный размер (как, например, на 32-разрядной платформе размер 32 бита, а на 64-разрядной  — 64), но свойство Handle объекта InputLanguage всегда использует 32 бита, причем первая половина этого числа — значение LCID. Культура, присоединенная к InputLanguage, создается по этому LCID.

При разработке приложений данные о культуре могут оказаться полезными, когда программа должна выбрать что-либо в зависимости от языка, например средства проверки правописания или грамматики. Неуправляемые приложения вроде Microsoft Word используют LCID-часть описателя языка ввода именно для этой цели. Они помечают фрагменты документов как относящиеся к тому или иному языку, определяя язык по методу ввода, который использовался при наборе символов.

Если вы пойдете по этому пути, возможно, вам потребуется логика работы с символами, основанная на том, какие символы вводятся, а не только на том, какой язык выбран. Тогда вы избежите проблем в ситуации, справиться с которой не в состоянии даже Word, — подключение клавиатуры, язык которой не имеет ничего общего с используемым языком (например установлен венгерский язык, а клавиатура поддерживает арабский). .NET Framework не позволяет определить язык клавиатуры, но это можно сделать с помощью P/Invoke (листинг 2).

Листинг 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. Из-за этого возникла серьезная проблема со свойствами CurrentCulture/CurrentUICulture и классом InputLanguage, которые раньше могли просто создать культуру по значению локализующего идентификатора (LCID), полученного от Windows.

В .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.

Листинг 3. Усовершенствованное перечислимое CultureTypes

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, по-прежнему будет работать, а в новом коде следует задействовать новые члены перечислимого CultureTypes. Эти члены являются значениями перечислимого и флагами, поэтому их можно комбинировать. Например, член UserCustomCultures можно использовать в сочетании с флагом NeutralCultures или SpecificCultures. При вызове CultureInfo.GetCultures следует передать наиболее ограниченный тип, удовлетворяющий вашим требованиям.

Собственные культуры

Возможность создания собственных культур также является новшеством .NET Framework 2.0.  Для создания новых культур служит класс CultureAndRegionInfoBuilder. При соблюдении принципов, изложенных в этой статье, вероятность того, что ваше управляемое приложение будет корректно работать с собственными культурами, значительно повысится!

CultureInfo — класс с обширными возможностями, в котором отражены почти все стороны взаимодействия пользователей с компьютерами и приложениями. Поскольку это очень мощный класс, принимая решение, какой объект CultureInfo подходит в каждом конкретном случае, важно сделать правильный выбор. Во многих случаях, потратив немного больше времени на обдумывание, вы значительно повысите эффективность применения класса CultureInfo.

Реклама