Поделиться через


Глобализация

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

Как разработчики, у нас есть предположения о пользовательских интерфейсах и данных, сформированных нашими культурами. Например, для англоязычного разработчика в США сериализация данных даты и времени в виде строки в формате MM/dd/yyyy hh:mm:ss кажется совершенно разумным. Однако десериализация этой строки в системе с другими параметрами культуры может вызвать FormatException исключение или привести к неточным данным. Глобализация позволяет нам выявлять такие предположения, связанные с языком и региональными параметрами, и гарантировать, что они не влияют на дизайн или код приложения.

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

Строки

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

Используйте Юникод внутренне

По умолчанию .NET использует строки Юникода. Строка Юникода состоит из нуля, одного или нескольких Char объектов, каждая из которых представляет единицу кода UTF-16. Существует представление Юникода почти для каждого символа в каждом наборе символов, используемых во всем мире.

Многие приложения и операционные системы, включая операционную систему Windows, также могут использовать кодовые страницы для представления наборов символов. Кодовые страницы обычно содержат стандартные значения ASCII из 0x00 через 0x7F и сопоставляют другие символы с оставшимися значениями от 0x80 до 0xFF. Интерпретация значений из 0x80 через 0xFF зависит от конкретной кодовой страницы. Из-за этого следует избегать использования кодовых страниц в глобальном приложении, если это возможно.

В следующем примере показано, как интерпретировать данные кодовой страницы, если кодовая страница по умолчанию в системе отличается от кодовой страницы, на которой сохранены данные. (Для имитации этого сценария пример явно указывает различные кодовые страницы.) Во-первых, в примере определяется массив, состоящий из прописных символов греческого алфавита. Он кодирует их в массив байтов с помощью кодовой страницы 737 (также известной как MS-DOS греческий) и сохраняет массив байтов в файл. Если файл извлекается и его массив байтов декодируется с помощью кодовой страницы 737, исходные символы восстанавливаются. Однако если файл извлекается и его массив байтов декодируется с помощью кодовой страницы 1252 (или Windows-1252, представляющей символы в латинском алфавите), исходные символы теряются.

using System;
using System.IO;
using System.Text;

public class Example
{
    public static void CodePages()
    {
        // Represent Greek uppercase characters in code page 737.
        char[] greekChars =
        {
            'Α', 'Β', 'Γ', 'Δ', 'Ε', 'Ζ', 'Η', 'Θ',
            'Ι', 'Κ', 'Λ', 'Μ', 'Ν', 'Ξ', 'Ο', 'Π',
            'Ρ', 'Σ', 'Τ', 'Υ', 'Φ', 'Χ', 'Ψ', 'Ω'
        };

        Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);

        Encoding cp737 = Encoding.GetEncoding(737);
        int nBytes = cp737.GetByteCount(greekChars);
        byte[] bytes737 = new byte[nBytes];
        bytes737 = cp737.GetBytes(greekChars);
        // Write the bytes to a file.
        FileStream fs = new FileStream(@".\\CodePageBytes.dat", FileMode.Create);
        fs.Write(bytes737, 0, bytes737.Length);
        fs.Close();

        // Retrieve the byte data from the file.
        fs = new FileStream(@".\\CodePageBytes.dat", FileMode.Open);
        byte[] bytes1 = new byte[fs.Length];
        fs.Read(bytes1, 0, (int)fs.Length);
        fs.Close();

        // Restore the data on a system whose code page is 737.
        string data = cp737.GetString(bytes1);
        Console.WriteLine(data);
        Console.WriteLine();

        // Restore the data on a system whose code page is 1252.
        Encoding cp1252 = Encoding.GetEncoding(1252);
        data = cp1252.GetString(bytes1);
        Console.WriteLine(data);
    }
}

// The example displays the following output:
//       ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩ
//       €‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’""•–—

Imports System.IO
Imports System.Text

Module Example
    Public Sub CodePages()
        ' Represent Greek uppercase characters in code page 737.
        Dim greekChars() As Char = {"Α"c, "Β"c, "Γ"c, "Δ"c, "Ε"c, "Ζ"c, "Η"c, "Θ"c,
                                     "Ι"c, "Κ"c, "Λ"c, "Μ"c, "Ν"c, "Ξ"c, "Ο"c, "Π"c,
                                     "Ρ"c, "Σ"c, "Τ"c, "Υ"c, "Φ"c, "Χ"c, "Ψ"c, "Ω"c}

        Encoding.RegisterProvider(CodePagesEncodingProvider.Instance)

        Dim cp737 As Encoding = Encoding.GetEncoding(737)
        Dim nBytes As Integer = CInt(cp737.GetByteCount(greekChars))
        Dim bytes737(nBytes - 1) As Byte
        bytes737 = cp737.GetBytes(greekChars)
        ' Write the bytes to a file.
        Dim fs As New FileStream(".\CodePageBytes.dat", FileMode.Create)
        fs.Write(bytes737, 0, bytes737.Length)
        fs.Close()

        ' Retrieve the byte data from the file.
        fs = New FileStream(".\CodePageBytes.dat", FileMode.Open)
        Dim bytes1(CInt(fs.Length - 1)) As Byte
        fs.Read(bytes1, 0, CInt(fs.Length))
        fs.Close()

        ' Restore the data on a system whose code page is 737.
        Dim data As String = cp737.GetString(bytes1)
        Console.WriteLine(data)
        Console.WriteLine()

        ' Restore the data on a system whose code page is 1252.
        Dim cp1252 As Encoding = Encoding.GetEncoding(1252)
        data = cp1252.GetString(bytes1)
        Console.WriteLine(data)
    End Sub
End Module
' The example displays the following output:
'       ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩ
'       €‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’""•–—

Использование Юникода гарантирует, что одни и те же единицы кода всегда сопоставляться с одинаковыми символами, и что одни и те же символы всегда сопоставляют с одинаковыми массивами байтов.

Использование файлов ресурсов

Даже если вы разрабатываете приложение, предназначенное для одной культуры или региона, следует использовать файлы ресурсов для хранения строк и других ресурсов, отображаемых в пользовательском интерфейсе. Их никогда не следует добавлять непосредственно в код. Использование файлов ресурсов имеет ряд преимуществ:

  • Все строки находятся в одном расположении. Вам не нужно выполнять поиск по всему исходному коду, чтобы определить строки для изменений в зависимости от языка или культурных особенностей.
  • Не нужно дублировать строки. Разработчики, которые не используют файлы ресурсов, часто определяют одну строку в нескольких файлах исходного кода. Это дублирование увеличивает вероятность того, что один или несколько экземпляров будут пропущены при изменении строки.
  • Вы можете включить нестроковые ресурсы, такие как изображения или двоичные данные, в файл ресурсов вместо хранения их в отдельном автономном файле, чтобы их можно было легко получить.

Использование файлов ресурсов имеет определенные преимущества при создании локализованного приложения. При развертывании ресурсов в сателлитных сборках среда CLR автоматически выбирает соответствующий языку и культуре ресурс на основе текущего языка пользовательского интерфейса пользователя, заданного свойством CultureInfo.CurrentUICulture. Если вы предоставляете соответствующий культурно-специфический ресурс и правильно создаете экземпляр объекта ResourceManager или используете строго типизированный ресурсный класс, среда выполнения берет на себя обработку получения соответствующих ресурсов.

Дополнительные сведения о создании файлов ресурсов см. в разделе "Создание файлов ресурсов". Для получения информации о создании и развертывании сателлитных сборок см. статьи "Создание сателлитных сборок" и "Упаковка и развертывание ресурсов".

Поиск и сравнение строк

По возможности следует обрабатывать строки как целые строки, а не обрабатывать их в виде ряда отдельных символов. Это особенно важно при сортировке или поиске подстрок, чтобы предотвратить проблемы, связанные с разбором комбинированных символов.

Подсказка

Класс можно использовать StringInfo для работы с текстовыми элементами, а не отдельными символами в строке.

В строковых поисках и сравнениях распространенная ошибка заключается в том, чтобы рассматривать строку как коллекцию символов, каждая из которых представлена Char объектом. На самом деле один символ может быть сформирован одним, двумя или несколькими Char объектами. Такие символы чаще всего встречаются в строках из культур, алфавиты которых состоят из символов за пределами диапазона Базового латинского диапазона символов Юникода (U+0021–U+007E). В следующем примере показано, как найти индекс символа LATIN CAPITAL LETTER A WITH GRAVE (U+00C0) в строке. Однако этот символ может быть представлен двумя способами: как единая единица кода (U+00C0) или как составной символ (две единицы кода: U+0041 и U+0300). В этом случае символ представлен в строковом экземпляре двумя Char объектами, U+0041 и U+0300. В примере кода вызываются перегрузки String.IndexOf(Char) и String.IndexOf(String) для определения положения этого символа в строке, однако результаты различаются. Первый вызов метода имеет Char аргумент; он выполняет порядковое сравнение и поэтому не может найти совпадение. Второй вызов имеет аргумент String; он выполняет сравнение с учетом культурных особенностей и поэтому находит совпадение.

using System;
using System.Globalization;
using System.Threading;

public class Example17
{
    public static void Main17()
    {
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("pl-PL");
        string composite = "\u0041\u0300";
        Console.WriteLine($"Comparing using Char:   {composite.IndexOf('\u00C0')}");
        Console.WriteLine($"Comparing using String: {composite.IndexOf("\u00C0")}");
    }
}

// The example displays the following output:
//       Comparing using Char:   -1
//       Comparing using String: 0
Imports System.Globalization
Imports System.Threading

Module Example17
    Public Sub Main17()
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("pl-PL")
        Dim composite As String = ChrW(&H41) + ChrW(&H300)
        Console.WriteLine("Comparing using Char:   {0}", composite.IndexOf(ChrW(&HC0)))
        Console.WriteLine("Comparing using String: {0}", composite.IndexOf(ChrW(&HC0).ToString()))
    End Sub
End Module
' The example displays the following output:
'       Comparing using Char:   -1
'       Comparing using String: 0

Вы можете избежать некоторой неясности в этом примере (вызовы двух схожих перегрузок метода, возвращающих разные результаты), вызвав перегрузку с параметром StringComparison, например метод String.IndexOf(String, StringComparison) или метод String.LastIndexOf(String, StringComparison).

Однако поиск не всегда учитывает язык и региональные параметры. Если целью поиска является принятие решения о безопасности или разрешение или запрет доступа к некоторым ресурсам, сравнение должно быть порядково, как описано в следующем разделе.

Тестовые строки для равенства

Если вы хотите протестировать две строки для равенства, а не определить, как они сравниваются в порядке сортировки, используйте String.Equals метод вместо метода сравнения строк, например String.Compare или CompareInfo.Compare.

Сравнение равенства обычно выполняется для условного доступа к некоторым ресурсам. Например, можно выполнить сравнение равенства, чтобы проверить пароль или подтвердить наличие файла. Такие нелингвистические сравнения всегда должны быть порядковыми, а не учитывающими культурные особенности. Как правило, следует вызывать метод экземпляра String.Equals(String, StringComparison) или статический String.Equals(String, String, StringComparison) метод со значением StringComparison.Ordinal строк, таких как пароли, и значение StringComparison.OrdinalIgnoreCase строк, таких как имена файлов или URI.

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

В следующем примере показана опасность поиска с учетом языка и региональных параметров для нелингвистических данных. Этот AccessesFileSystem метод предназначен для запрета доступа к файловой системе для URI, начинающихся с подстроки FILE. Для этого он выполняет сравнение в начале URI со строкой "FILE", учитывающее культурные особенности и не зависящее от регистра. Так как универсальный код ресурса (URI), обращающийся к файловой системе, может начинаться с "FILE:" или "file:", неявное предположение заключается в том, что "i" (U+0069) всегда является строчным эквивалентом "I" (U+0049). Однако в турецком и азербайджанском языках верхняя версия "i" — "İ" (U+0130). Из-за этого несоответствия сравнение с учетом языка и региональных параметров позволяет получать доступ к файловой системе, если оно должно быть запрещено.

using System;
using System.Globalization;
using System.Threading;

public class Example10
{
    public static void Main10()
    {
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("tr-TR");
        string uri = @"file:\\c:\users\username\Documents\bio.txt";
        if (!AccessesFileSystem(uri))
            // Permit access to resource specified by URI
            Console.WriteLine("Access is allowed.");
        else
            // Prohibit access.
            Console.WriteLine("Access is not allowed.");
    }

    private static bool AccessesFileSystem(string uri)
    {
        return uri.StartsWith("FILE", true, CultureInfo.CurrentCulture);
    }
}

// The example displays the following output:
//         Access is allowed.
Imports System.Globalization
Imports System.Threading

Module Example10
    Public Sub Main10()
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("tr-TR")
        Dim uri As String = "file:\\c:\users\username\Documents\bio.txt"
        If Not AccessesFileSystem(uri) Then
            ' Permit access to resource specified by URI
            Console.WriteLine("Access is allowed.")
        Else
            ' Prohibit access.
            Console.WriteLine("Access is not allowed.")
        End If
    End Sub

    Private Function AccessesFileSystem(uri As String) As Boolean
        Return uri.StartsWith("FILE", True, CultureInfo.CurrentCulture)
    End Function
End Module
' The example displays the following output:
'       Access is allowed.

Эту проблему можно избежать, выполнив порядковое сравнение, которое игнорирует случай, как показано в следующем примере.

using System;
using System.Globalization;
using System.Threading;

public class Example11
{
    public static void Main11()
    {
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("tr-TR");
        string uri = @"file:\\c:\users\username\Documents\bio.txt";
        if (!AccessesFileSystem(uri))
            // Permit access to resource specified by URI
            Console.WriteLine("Access is allowed.");
        else
            // Prohibit access.
            Console.WriteLine("Access is not allowed.");
    }

    private static bool AccessesFileSystem(string uri)
    {
        return uri.StartsWith("FILE", StringComparison.OrdinalIgnoreCase);
    }
}

// The example displays the following output:
//         Access is not allowed.
Imports System.Globalization
Imports System.Threading

Module Example11
    Public Sub Main11()
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("tr-TR")
        Dim uri As String = "file:\\c:\users\username\Documents\bio.txt"
        If Not AccessesFileSystem(uri) Then
            ' Permit access to resource specified by URI
            Console.WriteLine("Access is allowed.")
        Else
            ' Prohibit access.
            Console.WriteLine("Access is not allowed.")
        End If
    End Sub

    Private Function AccessesFileSystem(uri As String) As Boolean
        Return uri.StartsWith("FILE", StringComparison.OrdinalIgnoreCase)
    End Function
End Module
' The example displays the following output:
'       Access is not allowed.

Порядок и сортировка строк

Как правило, упорядоченные строки, отображаемые в пользовательском интерфейсе, должны быть отсортированы с учётом культуры. В большинстве случаев такие сравнения строк обрабатываются неявно .NET при вызове метода, который сортирует строки, например Array.Sort или List<T>.Sort. По умолчанию строки сортируются в соответствии с соглашениями сортировки текущей культурной настройки. В следующем примере показано различие при сортировке массива строк с помощью соглашений английского языка (США) и шведской (Швеции) культуры.

using System;
using System.Globalization;
using System.Threading;

public class Example18
{
    public static void Main18()
    {
        string[] values = { "able", "ångström", "apple", "Æble",
                          "Windows", "Visual Studio" };
        // Change thread to en-US.
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US");
        // Sort the array and copy it to a new array to preserve the order.
        Array.Sort(values);
        string[] enValues = (String[])values.Clone();

        // Change culture to Swedish (Sweden).
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("sv-SE");
        Array.Sort(values);
        string[] svValues = (String[])values.Clone();

        // Compare the sorted arrays.
        Console.WriteLine("{0,-8} {1,-15} {2,-15}\n", "Position", "en-US", "sv-SE");
        for (int ctr = 0; ctr <= values.GetUpperBound(0); ctr++)
            Console.WriteLine("{0,-8} {1,-15} {2,-15}", ctr, enValues[ctr], svValues[ctr]);
    }
}

// The example displays the following output:
//       Position en-US           sv-SE
//
//       0        able            able
//       1        Æble            Æble
//       2        ångström        apple
//       3        apple           Windows
//       4        Visual Studio   Visual Studio
//       5        Windows         ångström
Imports System.Globalization
Imports System.Threading

Module Example18
    Public Sub Main18()
        Dim values() As String = {"able", "ångström", "apple",
                                   "Æble", "Windows", "Visual Studio"}
        ' Change thread to en-US.
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US")
        ' Sort the array and copy it to a new array to preserve the order.
        Array.Sort(values)
        Dim enValues() As String = CType(values.Clone(), String())

        ' Change culture to Swedish (Sweden).
        Thread.CurrentThread.CurrentCulture = New CultureInfo("sv-SE")
        Array.Sort(values)
        Dim svValues() As String = CType(values.Clone(), String())

        ' Compare the sorted arrays.
        Console.WriteLine("{0,-8} {1,-15} {2,-15}", "Position", "en-US", "sv-SE")
        Console.WriteLine()
        For ctr As Integer = 0 To values.GetUpperBound(0)
            Console.WriteLine("{0,-8} {1,-15} {2,-15}", ctr, enValues(ctr), svValues(ctr))
        Next
    End Sub
End Module
' The example displays the following output:
'       Position en-US           sv-SE
'       
'       0        able            able
'       1        Æble            Æble
'       2        ångström        apple
'       3        apple           Windows
'       4        Visual Studio   Visual Studio
'       5        Windows         ångström

Сравнение строк с учетом культурных особенностей определяется объектом CompareInfo, который возвращается свойством CultureInfo.CompareInfo каждого языка и региональных параметров. Сравнения строк с учетом языка и региональных параметров, использующие перегрузки метода, также используют String.CompareCompareInfo объект.

.NET использует таблицы для выполнения сортировки с учетом языка и региональных параметров в строковых данных. Содержимое этих таблиц, содержащих данные о весах сортировки и нормализации строк, определяется версией стандарта Юникода, реализованной определенной версией .NET. В следующей таблице перечислены версии Юникода, реализованные указанными версиями .NET. Этот список поддерживаемых версий Юникода применяется только к сравнению символов и сортировке; Он не применяется к классификации символов Юникода по категориям. Дополнительные сведения см. в статье String в разделе "Строки и Стандарт Юникода".

Версия платформы .NET Framework Операционная система Версия Юникода
.NET Framework 2.0 Все операционные системы Юникод 4.1
.NET Framework 3.0 Все операционные системы Юникод 4.1
.NET Framework 3.5 Все операционные системы Юникод 4.1
платформа .NET Framework 4 Все операционные системы Юникод 5.0
.NET Framework 4.5 и более поздних версий Windows 7 Юникод 5.0
.NET Framework 4.5 и более поздних версий Операционные системы Windows 8 и более поздних версий Юникод 6.3.0
.NET Core и .NET 5+ Зависит от версии Стандарта Юникода, поддерживаемой базовой ОС.

Начиная с .NET Framework 4.5 и во всех версиях .NET Core и .NET 5+, сравнение строк и сортировка зависят от операционной системы. .NET Framework 4.5 и более поздних версий, работающих в Windows 7, извлекает данные из собственных таблиц, реализующих Юникод 5.0. .NET Framework 4.5 и более поздних версий, работающих в Windows 8 и более поздних версиях, извлекает данные из таблиц операционной системы, реализующих Юникод 6.3. В .NET Core и .NET 5+ поддерживаемая версия Юникода зависит от базовой операционной системы. Если вы сериализуете данные, упорядоченные с учётом культурных особенностей, можно использовать SortVersion класс, чтобы определить, когда вашим сериализованным данным нужно быть отсортированными таким образом, чтобы они соответствовали порядку сортировки .NET и операционной системы. Пример см. в теме класса SortVersion.

Если ваше приложение выполняет обширные сортировки строковых данных, зависящие от специфики культуры, вы можете использовать класс SortKey для сравнения строк. Ключ сортировки отражает культурально обусловленные веса сортировки, включая веса алфавита, регистра и диакритики определенной строки. Так как сравнения с использованием ключей сортировки являются двоичными, они быстрее, чем сравнения, использующие CompareInfo объект неявно или явно. Для создания ключа сортировки, специфичного для культуры, для конкретной строки, строка передается в метод CompareInfo.GetSortKey.

Следующий пример аналогичен предыдущему примеру. Однако вместо вызова метода Array.Sort(Array), который неявно вызывает метод CompareInfo.Compare, определяется реализация System.Collections.Generic.IComparer<T>, которая сравнивает ключи сортировки, создаваемые и передаваемые методу Array.Sort<T>(T[], IComparer<T>).

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Threading;

public class SortKeyComparer : IComparer<String>
{
    public int Compare(string? str1, string? str2)
    {
        return (str1, str2) switch
        {
            (null, null) => 0,
            (null, _) => -1,
            (_, null) => 1,
            (var s1, var s2) => SortKey.Compare(
                CultureInfo.CurrentCulture.CompareInfo.GetSortKey(s1),
                CultureInfo.CurrentCulture.CompareInfo.GetSortKey(s1))
        };
    }
}

public class Example19
{
    public static void Main19()
    {
        string[] values = { "able", "ångström", "apple", "Æble",
                          "Windows", "Visual Studio" };
        SortKeyComparer comparer = new SortKeyComparer();

        // Change thread to en-US.
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US");
        // Sort the array and copy it to a new array to preserve the order.
        Array.Sort(values, comparer);
        string[] enValues = (String[])values.Clone();

        // Change culture to Swedish (Sweden).
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("sv-SE");
        Array.Sort(values, comparer);
        string[] svValues = (String[])values.Clone();

        // Compare the sorted arrays.
        Console.WriteLine("{0,-8} {1,-15} {2,-15}\n", "Position", "en-US", "sv-SE");
        for (int ctr = 0; ctr <= values.GetUpperBound(0); ctr++)
            Console.WriteLine("{0,-8} {1,-15} {2,-15}", ctr, enValues[ctr], svValues[ctr]);
    }
}

// The example displays the following output:
//       Position en-US           sv-SE
//
//       0        able            able
//       1        Æble            Æble
//       2        ångström        apple
//       3        apple           Windows
//       4        Visual Studio   Visual Studio
//       5        Windows         ångström
Imports System.Collections.Generic
Imports System.Globalization
Imports System.Threading

Public Class SortKeyComparer : Implements IComparer(Of String)
    Public Function Compare(str1 As String, str2 As String) As Integer _
           Implements IComparer(Of String).Compare
        Dim sk1, sk2 As SortKey
        sk1 = CultureInfo.CurrentCulture.CompareInfo.GetSortKey(str1)
        sk2 = CultureInfo.CurrentCulture.CompareInfo.GetSortKey(str2)
        Return SortKey.Compare(sk1, sk2)
    End Function
End Class

Module Example19
    Public Sub Main19()
        Dim values() As String = {"able", "ångström", "apple",
                                   "Æble", "Windows", "Visual Studio"}
        Dim comparer As New SortKeyComparer()

        ' Change thread to en-US.
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US")
        ' Sort the array and copy it to a new array to preserve the order.
        Array.Sort(values, comparer)
        Dim enValues() As String = CType(values.Clone(), String())

        ' Change culture to Swedish (Sweden).
        Thread.CurrentThread.CurrentCulture = New CultureInfo("sv-SE")
        Array.Sort(values, comparer)
        Dim svValues() As String = CType(values.Clone(), String())

        ' Compare the sorted arrays.
        Console.WriteLine("{0,-8} {1,-15} {2,-15}", "Position", "en-US", "sv-SE")
        Console.WriteLine()
        For ctr As Integer = 0 To values.GetUpperBound(0)
            Console.WriteLine("{0,-8} {1,-15} {2,-15}", ctr, enValues(ctr), svValues(ctr))
        Next
    End Sub
End Module
' The example displays the following output:
'       Position en-US           sv-SE
'       
'       0        able            able
'       1        Æble            Æble
'       2        ångström        apple
'       3        apple           Windows
'       4        Visual Studio   Visual Studio
'       5        Windows         ångström

Избегайте объединения строк

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

Обработка дат и времени

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

Отображение дат и времени

Как правило, когда даты и время отображаются в пользовательском интерфейсе, следует использовать соглашения форматирования, характерные для культуры пользователя, которая определяется свойством CultureInfo.CurrentCulture и объектом DateTimeFormatInfo, возвращаемым посредством свойства CultureInfo.CurrentCulture.DateTimeFormat. Соглашения о форматировании текущей культуры автоматически используются при форматировании даты с помощью любого из следующих методов:

В следующем примере отображаются данные о восходе и закате дважды в течение 11 октября 2012 года. Сначала он устанавливает текущую культуру хорватской (Хорватия), а затем на английский (Соединенное Королевство). В каждом случае даты и время отображаются в формате, подходящем для данной культурной среды.

using System;
using System.Globalization;
using System.Threading;

public class Example3
{
    static DateTime[] dates = { new DateTime(2012, 10, 11, 7, 06, 0),
                        new DateTime(2012, 10, 11, 18, 19, 0) };

    public static void Main3()
    {
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("hr-HR");
        ShowDayInfo();
        Console.WriteLine();
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-GB");
        ShowDayInfo();
    }

    private static void ShowDayInfo()
    {
        Console.WriteLine($"Date: {dates[0]:D}");
        Console.WriteLine($"   Sunrise: {dates[0]:T}");
        Console.WriteLine($"   Sunset:  {dates[1]:T}");
    }
}

// The example displays the following output:
//       Date: 11. listopada 2012.
//          Sunrise: 7:06:00
//          Sunset:  18:19:00
//
//       Date: 11 October 2012
//          Sunrise: 07:06:00
//          Sunset:  18:19:00
Imports System.Globalization
Imports System.Threading

Module Example3
    Dim dates() As Date = {New Date(2012, 10, 11, 7, 6, 0),
                            New Date(2012, 10, 11, 18, 19, 0)}

    Public Sub Main3()
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("hr-HR")
        ShowDayInfo()
        Console.WriteLine()
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-GB")
        ShowDayInfo()
    End Sub

    Private Sub ShowDayInfo()
        Console.WriteLine("Date: {0:D}", dates(0))
        Console.WriteLine("   Sunrise: {0:T}", dates(0))
        Console.WriteLine("   Sunset:  {0:T}", dates(1))
    End Sub
End Module
' The example displays the following output:
'       Date: 11. listopada 2012.
'          Sunrise: 7:06:00
'          Sunset:  18:19:00
'       
'       Date: 11 October 2012
'          Sunrise: 07:06:00
'          Sunset:  18:19:00

Сохранение дат и времени

Никогда не следует сохранять данные даты и времени в формате, который может отличаться в зависимости от культурных особенностей. Это распространенная ошибка программирования, которая приводит к повреждению данных или исключению среды выполнения. В следующем примере сериализуются две даты, 9 января 2013 г. и 18 августа 2013 г., в виде строк согласно культурным нормам английского языка (США). Когда данные извлекаются и анализируются с помощью соглашений английского языка (США), они успешно восстанавливаются. Однако, когда он извлекается и разбирается в соответствии с соглашениями английской культуры (Соединённое Королевство), первая дата неправильно интерпретируется как 1 сентября, а вторая не может быть разобрана, потому что григорианский календарь не имеет восемнадцатого месяца.

using System;
using System.IO;
using System.Globalization;
using System.Threading;

public class Example4
{
    public static void Main4()
    {
        // Persist two dates as strings.
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US");
        DateTime[] dates = { new DateTime(2013, 1, 9),
                           new DateTime(2013, 8, 18) };
        StreamWriter sw = new StreamWriter("dateData.dat");
        sw.Write("{0:d}|{1:d}", dates[0], dates[1]);
        sw.Close();

        // Read the persisted data.
        StreamReader sr = new StreamReader("dateData.dat");
        string dateData = sr.ReadToEnd();
        sr.Close();
        string[] dateStrings = dateData.Split('|');

        // Restore and display the data using the conventions of the en-US culture.
        Console.WriteLine($"Current Culture: {Thread.CurrentThread.CurrentCulture.DisplayName}");
        foreach (var dateStr in dateStrings)
        {
            DateTime restoredDate;
            if (DateTime.TryParse(dateStr, out restoredDate))
                Console.WriteLine($"The date is {restoredDate:D}");
            else
                Console.WriteLine($"ERROR: Unable to parse {dateStr}");
        }
        Console.WriteLine();

        // Restore and display the data using the conventions of the en-GB culture.
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-GB");
        Console.WriteLine($"Current Culture: {Thread.CurrentThread.CurrentCulture.DisplayName}");
        foreach (var dateStr in dateStrings)
        {
            DateTime restoredDate;
            if (DateTime.TryParse(dateStr, out restoredDate))
                Console.WriteLine($"The date is {restoredDate:D}");
            else
                Console.WriteLine($"ERROR: Unable to parse {dateStr}");
        }
    }
}

// The example displays the following output:
//       Current Culture: English (United States)
//       The date is Wednesday, January 09, 2013
//       The date is Sunday, August 18, 2013
//
//       Current Culture: English (United Kingdom)
//       The date is 01 September 2013
//       ERROR: Unable to parse 8/18/2013
Imports System.Globalization
Imports System.IO
Imports System.Threading

Module Example4
    Public Sub Main4()
        ' Persist two dates as strings.
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US")
        Dim dates() As DateTime = {New DateTime(2013, 1, 9),
                                    New DateTime(2013, 8, 18)}
        Dim sw As New StreamWriter("dateData.dat")
        sw.Write("{0:d}|{1:d}", dates(0), dates(1))
        sw.Close()

        ' Read the persisted data.
        Dim sr As New StreamReader("dateData.dat")
        Dim dateData As String = sr.ReadToEnd()
        sr.Close()
        Dim dateStrings() As String = dateData.Split("|"c)

        ' Restore and display the data using the conventions of the en-US culture.
        Console.WriteLine("Current Culture: {0}",
                          Thread.CurrentThread.CurrentCulture.DisplayName)
        For Each dateStr In dateStrings
            Dim restoredDate As Date
            If Date.TryParse(dateStr, restoredDate) Then
                Console.WriteLine("The date is {0:D}", restoredDate)
            Else
                Console.WriteLine("ERROR: Unable to parse {0}", dateStr)
            End If
        Next
        Console.WriteLine()

        ' Restore and display the data using the conventions of the en-GB culture.
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-GB")
        Console.WriteLine("Current Culture: {0}",
                          Thread.CurrentThread.CurrentCulture.DisplayName)
        For Each dateStr In dateStrings
            Dim restoredDate As Date
            If Date.TryParse(dateStr, restoredDate) Then
                Console.WriteLine("The date is {0:D}", restoredDate)
            Else
                Console.WriteLine("ERROR: Unable to parse {0}", dateStr)
            End If
        Next
    End Sub
End Module
' The example displays the following output:
'       Current Culture: English (United States)
'       The date is Wednesday, January 09, 2013
'       The date is Sunday, August 18, 2013
'       
'       Current Culture: English (United Kingdom)
'       The date is 01 September 2013
'       ERROR: Unable to parse 8/18/2013

Эту проблему можно избежать тремя способами:

  • Сериализируйте дату и время в двоичном формате, а не в виде строки.
  • Сохраните и разберите строковое представление даты и времени, используя пользовательскую строку форматирования, которая остается неизменной независимо от культурных настроек пользователя.
  • Сохраните строку, используя правила форматирования инвариантной культуры.

В следующем примере показан последний подход. Он использует соглашения о форматировании инвариантной культуры, которые возвращаются статическим CultureInfo.InvariantCulture свойством.

using System;
using System.IO;
using System.Globalization;
using System.Threading;

public class Example5
{
    public static void Main5()
    {
        // Persist two dates as strings.
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US");
        DateTime[] dates = { new DateTime(2013, 1, 9),
                           new DateTime(2013, 8, 18) };
        StreamWriter sw = new StreamWriter("dateData.dat");
        sw.Write(String.Format(CultureInfo.InvariantCulture,
                               "{0:d}|{1:d}", dates[0], dates[1]));
        sw.Close();

        // Read the persisted data.
        StreamReader sr = new StreamReader("dateData.dat");
        string dateData = sr.ReadToEnd();
        sr.Close();
        string[] dateStrings = dateData.Split('|');

        // Restore and display the data using the conventions of the en-US culture.
        Console.WriteLine($"Current Culture: {Thread.CurrentThread.CurrentCulture.DisplayName}");
        foreach (var dateStr in dateStrings)
        {
            DateTime restoredDate;
            if (DateTime.TryParse(dateStr, CultureInfo.InvariantCulture,
                                  DateTimeStyles.None, out restoredDate))
                Console.WriteLine($"The date is {restoredDate:D}");
            else
                Console.WriteLine($"ERROR: Unable to parse {dateStr}");
        }
        Console.WriteLine();

        // Restore and display the data using the conventions of the en-GB culture.
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-GB");
        Console.WriteLine($"Current Culture: {Thread.CurrentThread.CurrentCulture.DisplayName}");
        foreach (var dateStr in dateStrings)
        {
            DateTime restoredDate;
            if (DateTime.TryParse(dateStr, CultureInfo.InvariantCulture,
                                  DateTimeStyles.None, out restoredDate))
                Console.WriteLine($"The date is {restoredDate:D}");
            else
                Console.WriteLine($"ERROR: Unable to parse {dateStr}");
        }
    }
}

// The example displays the following output:
//       Current Culture: English (United States)
//       The date is Wednesday, January 09, 2013
//       The date is Sunday, August 18, 2013
//
//       Current Culture: English (United Kingdom)
//       The date is 09 January 2013
//       The date is 18 August 2013
Imports System.Globalization
Imports System.IO
Imports System.Threading

Module Example5
    Public Sub Main5()
        ' Persist two dates as strings.
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US")
        Dim dates() As DateTime = {New DateTime(2013, 1, 9),
                                    New DateTime(2013, 8, 18)}
        Dim sw As New StreamWriter("dateData.dat")
        sw.Write(String.Format(CultureInfo.InvariantCulture,
                               "{0:d}|{1:d}", dates(0), dates(1)))
        sw.Close()

        ' Read the persisted data.
        Dim sr As New StreamReader("dateData.dat")
        Dim dateData As String = sr.ReadToEnd()
        sr.Close()
        Dim dateStrings() As String = dateData.Split("|"c)

        ' Restore and display the data using the conventions of the en-US culture.
        Console.WriteLine("Current Culture: {0}",
                          Thread.CurrentThread.CurrentCulture.DisplayName)
        For Each dateStr In dateStrings
            Dim restoredDate As Date
            If Date.TryParse(dateStr, CultureInfo.InvariantCulture,
                             DateTimeStyles.None, restoredDate) Then
                Console.WriteLine("The date is {0:D}", restoredDate)
            Else
                Console.WriteLine("ERROR: Unable to parse {0}", dateStr)
            End If
        Next
        Console.WriteLine()

        ' Restore and display the data using the conventions of the en-GB culture.
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-GB")
        Console.WriteLine("Current Culture: {0}",
                          Thread.CurrentThread.CurrentCulture.DisplayName)
        For Each dateStr In dateStrings
            Dim restoredDate As Date
            If Date.TryParse(dateStr, CultureInfo.InvariantCulture,
                             DateTimeStyles.None, restoredDate) Then
                Console.WriteLine("The date is {0:D}", restoredDate)
            Else
                Console.WriteLine("ERROR: Unable to parse {0}", dateStr)
            End If
        Next
    End Sub
End Module
' The example displays the following output:
'       Current Culture: English (United States)
'       The date is Wednesday, January 09, 2013
'       The date is Sunday, August 18, 2013
'       
'       Current Culture: English (United Kingdom)
'       The date is 09 January 2013
'       The date is 18 August 2013

Осведомленность о сериализации и часовом поясе

Значение даты и времени может иметь несколько интерпретаций, начиная от общего времени ("Магазины открыты 2 января 2013 года в 9:00 утра") до определенного момента времени ("Дата рождения: 2 января 2013 г. 6:32:00 A.M."). Если значение времени представляет определенный момент времени и восстанавливаете его из сериализованного значения, следует убедиться, что он представляет тот же момент времени независимо от географического расположения или часового пояса пользователя.

В следующем примере показана эта проблема. Он сохраняет одно локальное значение даты и времени в виде строки в трех стандартных форматах:

  • "G" для общей даты и длительного времени.
  • "s" для сортировки даты и времени.
  • "o" для даты и времени круговой поездки.
using System;
using System.IO;

public class Example6
{
    public static void Main6()
    {
        DateTime dateOriginal = new DateTime(2023, 3, 30, 18, 0, 0);
        dateOriginal = DateTime.SpecifyKind(dateOriginal, DateTimeKind.Local);

        // Serialize a date.
        if (!File.Exists("DateInfo.dat"))
        {
            StreamWriter sw = new StreamWriter("DateInfo.dat");
            sw.Write("{0:G}|{0:s}|{0:o}", dateOriginal);
            sw.Close();
            Console.WriteLine("Serialized dates to DateInfo.dat");
        }
        Console.WriteLine();

        // Restore the date from string values.
        StreamReader sr = new StreamReader("DateInfo.dat");
        string datesToSplit = sr.ReadToEnd();
        string[] dateStrings = datesToSplit.Split('|');
        foreach (var dateStr in dateStrings)
        {
            DateTime newDate = DateTime.Parse(dateStr);
            Console.WriteLine($"'{dateStr}' --> {newDate} {newDate.Kind}");
        }
    }
}
Imports System.IO

Module Example6
    Public Sub Main6()
        ' Serialize a date.
        Dim dateOriginal As Date = #03/30/2023 6:00PM#
        dateOriginal = DateTime.SpecifyKind(dateOriginal, DateTimeKind.Local)
        ' Serialize the date in string form.
        If Not File.Exists("DateInfo.dat") Then
            Dim sw As New StreamWriter("DateInfo.dat")
            sw.Write("{0:G}|{0:s}|{0:o}", dateOriginal)
            sw.Close()
        End If

        ' Restore the date from string values.
        Dim sr As New StreamReader("DateInfo.dat")
        Dim datesToSplit As String = sr.ReadToEnd()
        Dim dateStrings() As String = datesToSplit.Split("|"c)
        For Each dateStr In dateStrings
            Dim newDate As DateTime = DateTime.Parse(dateStr)
            Console.WriteLine("'{0}' --> {1} {2}",
                              dateStr, newDate, newDate.Kind)
        Next
    End Sub
End Module

Когда данные восстанавливаются в системе в том же часовом поясе, что и система, в которой она была сериализована, десериализированные значения даты и времени точно отражают исходное значение, как показано в выходных данных:

'3/30/2013 6:00:00 PM' --> 3/30/2013 6:00:00 PM Unspecified
'2013-03-30T18:00:00' --> 3/30/2013 6:00:00 PM Unspecified
'2013-03-30T18:00:00.0000000-07:00' --> 3/30/2013 6:00:00 PM Local

Однако если восстановить данные в системе в другом часовом поясе, только значение даты и времени, отформатированное с помощью стандартной строки формата "o" (круговое преобразование), сохраняет информацию о часовом поясе и, таким образом, представляет тот же момент времени. Вот результат, когда данные даты и времени восстановлены в системе в стандартном часовом поясе Романски.

'3/30/2023 6:00:00 PM' --> 3/30/2023 6:00:00 PM Unspecified
'2023-03-30T18:00:00' --> 3/30/2023 6:00:00 PM Unspecified
'2023-03-30T18:00:00.0000000-07:00' --> 3/31/2023 3:00:00 AM Local

Чтобы точно отразить значение даты и времени, представляющее один момент времени независимо от часового пояса системы, в котором данные десериализированы, можно выполнить любое из следующих действий:

  • Сохраните значение как строку, используя стандартную строку формата "o" (round-trip). Затем выполните десериализацию на целевой системе.
  • Преобразуйте его в формате UTC и сохраните его в виде строки с помощью строки стандартного формата r (RFC1123). Затем выполните десериализацию на целевой системе и переведите его на местное время.
  • Преобразуйте его в формате UTC и сохраните его в виде строки с помощью стандартной строки формата u (универсальной сортировки). Затем выполните десериализацию на целевой системе и переведите его на местное время.

В следующем примере показан каждый метод.

using System;
using System.IO;

public class Example9
{
    public static void Main9()
    {
        // Serialize a date.
        DateTime dateOriginal = new DateTime(2023, 3, 30, 18, 0, 0);
        dateOriginal = DateTime.SpecifyKind(dateOriginal, DateTimeKind.Local);

        // Serialize the date in string form.
        if (!File.Exists("DateInfo2.dat"))
        {
            StreamWriter sw = new StreamWriter("DateInfo2.dat");
            sw.Write("{0:o}|{1:r}|{1:u}", dateOriginal,
                                          dateOriginal.ToUniversalTime());
            sw.Close();
        }

        // Restore the date from string values.
        StreamReader sr = new StreamReader("DateInfo2.dat");
        string datesToSplit = sr.ReadToEnd();
        string[] dateStrings = datesToSplit.Split('|');
        for (int ctr = 0; ctr < dateStrings.Length; ctr++)
        {
            DateTime newDate = DateTime.Parse(dateStrings[ctr]);
            if (ctr == 1)
            {
                Console.WriteLine($"'{dateStrings[ctr]}' --> {newDate} {newDate.Kind}");
            }
            else
            {
                DateTime newLocalDate = newDate.ToLocalTime();
                Console.WriteLine($"'{dateStrings[ctr]}' --> {newLocalDate} {newLocalDate.Kind}");
            }
        }
    }
}
Imports System.IO

Module Example9
    Public Sub Main9()
        ' Serialize a date.
        Dim dateOriginal As Date = #03/30/2023 6:00PM#
        dateOriginal = DateTime.SpecifyKind(dateOriginal, DateTimeKind.Local)

        ' Serialize the date in string form.
        If Not File.Exists("DateInfo2.dat") Then
            Dim sw As New StreamWriter("DateInfo2.dat")
            sw.Write("{0:o}|{1:r}|{1:u}", dateOriginal,
                                          dateOriginal.ToUniversalTime())
            sw.Close()
        End If

        ' Restore the date from string values.
        Dim sr As New StreamReader("DateInfo2.dat")
        Dim datesToSplit As String = sr.ReadToEnd()
        Dim dateStrings() As String = datesToSplit.Split("|"c)
        For ctr As Integer = 0 To dateStrings.Length - 1
            Dim newDate As DateTime = DateTime.Parse(dateStrings(ctr))
            If ctr = 1 Then
                Console.WriteLine("'{0}' --> {1} {2}",
                                  dateStrings(ctr), newDate, newDate.Kind)
            Else
                Dim newLocalDate As DateTime = newDate.ToLocalTime()
                Console.WriteLine("'{0}' --> {1} {2}",
                                  dateStrings(ctr), newLocalDate, newLocalDate.Kind)
            End If
        Next
    End Sub
End Module

Если данные сериализуются в системе в часовом поясе Pacific Standard и десериализируются в системе в часовом поясе "Романский стандартный", в примере отображаются следующие выходные данные:

'2023-03-30T18:00:00.0000000-07:00' --> 3/31/2023 3:00:00 AM Local
'Sun, 31 Mar 2023 01:00:00 GMT' --> 3/31/2023 3:00:00 AM Local
'2023-03-31 01:00:00Z' --> 3/31/2023 3:00:00 AM Local

Дополнительные сведения см. в разделе "Преобразование времени между часовых поясов".

Выполнение арифметики даты и времени

DateTime Оба типа DateTimeOffset поддерживают арифметические операции. Вы можете вычислить разницу между двумя значениями даты или добавлять и вычитать определенные интервалы времени к значению даты или из него. Однако арифметические операции с значениями даты и времени не учитывают часовые пояса и правила корректировки часового пояса. Из-за этого арифметика даты и времени для значений, представляющих моменты во времени, может возвращать неточные результаты.

Например, переход с Тихоокеанского стандартного времени на Тихоокеанское дневное время происходит во второй воскресенье марта, который составляет 10 марта 2013 года. Как показано в следующем примере, если вы вычисляете дату и время, соответствующее 48 часам после 9 марта 2013 г. в 10:30 утра, на системе с Тихоокеанским стандартным временем, результат, 11 марта 2013 г. в 10:30 утра, не учитывает промежуточную корректировку времени.

using System;

public class Example7
{
    public static void Main7()
    {
        DateTime date1 = DateTime.SpecifyKind(new DateTime(2013, 3, 9, 10, 30, 0),
                                              DateTimeKind.Local);
        TimeSpan interval = new TimeSpan(48, 0, 0);
        DateTime date2 = date1 + interval;
        Console.WriteLine($"{date1:g} + {interval.TotalHours:N1} hours = {date2:g}");
    }
}

// The example displays the following output:
//        3/9/2013 10:30 AM + 48.0 hours = 3/11/2013 10:30 AM
Module Example7
    Public Sub Main7()
        Dim date1 As Date = DateTime.SpecifyKind(#3/9/2013 10:30AM#,
                                                 DateTimeKind.Local)
        Dim interval As New TimeSpan(48, 0, 0)
        Dim date2 As Date = date1 + interval
        Console.WriteLine("{0:g} + {1:N1} hours = {2:g}",
                          date1, interval.TotalHours, date2)
    End Sub
End Module
' The example displays the following output:
'       3/9/2013 10:30 AM + 48.0 hours = 3/11/2013 10:30 AM

Чтобы убедиться, что арифметическая операция с значениями даты и времени выдает точные результаты, выполните следующие действия.

  1. Преобразуйте время в исходном часовом поясе в формате UTC.
  2. Выполните арифметическую операцию.
  3. Если результатом является значение даты и времени, преобразуйте его из UTC в время в исходном часовом поясе.

Следующий пример аналогичен предыдущему примеру, за исключением того, что он выполняет следующие три шага, чтобы правильно добавить 48 часов к 9 марта 2013 г. в 10:30 утра.

using System;

public class Example8
{
    public static void Main8()
    {
        TimeZoneInfo pst = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time");
        DateTime date1 = DateTime.SpecifyKind(new DateTime(2013, 3, 9, 10, 30, 0),
                                              DateTimeKind.Local);
        DateTime utc1 = date1.ToUniversalTime();
        TimeSpan interval = new TimeSpan(48, 0, 0);
        DateTime utc2 = utc1 + interval;
        DateTime date2 = TimeZoneInfo.ConvertTimeFromUtc(utc2, pst);
        Console.WriteLine($"{date1:g} + {interval.TotalHours:N1} hours = {date2:g}");
    }
}

// The example displays the following output:
//        3/9/2013 10:30 AM + 48.0 hours = 3/11/2013 11:30 AM
Module Example8
    Public Sub Main8()
        Dim pst As TimeZoneInfo = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time")
        Dim date1 As Date = DateTime.SpecifyKind(#3/9/2013 10:30AM#,
                                                 DateTimeKind.Local)
        Dim utc1 As Date = date1.ToUniversalTime()
        Dim interval As New TimeSpan(48, 0, 0)
        Dim utc2 As Date = utc1 + interval
        Dim date2 As Date = TimeZoneInfo.ConvertTimeFromUtc(utc2, pst)
        Console.WriteLine("{0:g} + {1:N1} hours = {2:g}",
                          date1, interval.TotalHours, date2)
    End Sub
End Module
' The example displays the following output:
'       3/9/2013 10:30 AM + 48.0 hours = 3/11/2013 11:30 AM

Дополнительные сведения см. в разделе "Выполнение арифметических операций с датами и временем".

Используйте культурно-чувствительные названия для элементов даты

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

using System;

public class Example12
{
   public static void Main12()
   {
      DateTime midYear = new DateTime(2013, 7, 1);
      Console.WriteLine($"{midYear:d} is a {GetDayName(midYear)}.");
   }

   private static string GetDayName(DateTime date)
   {
      return date.DayOfWeek.ToString("G");
   }
}

// The example displays the following output:
//        7/1/2013 is a Monday.
Module Example12
    Public Sub Main12()
        Dim midYear As Date = #07/01/2013#
        Console.WriteLine("{0:d} is a {1}.", midYear, GetDayName(midYear))
    End Sub

    Private Function GetDayName(dat As Date) As String
        Return dat.DayOfWeek.ToString("G")
    End Function
End Module
' The example displays the following output:
'       7/1/2013 is a Monday.

Однако этот код всегда возвращает имена дней недели на английском языке. Код, извлекающий имя месяца, часто становится еще более негибким. Он часто предполагает двенадцатимесячный календарь с именами месяцев на определенном языке.

Используя настраиваемые строки формата даты и времени или свойства DateTimeFormatInfo объекта, можно легко извлечь строки, отражающие имена дней недели или месяцев в культурных настройках пользователя, как показано в следующем примере. Он изменяет текущую культуру на французскую (Франция) и отображает название дня недели и название месяца для 1 июля 2013 года.

using System;
using System.Globalization;

public class Example13
{
    public static void Main13()
    {
        // Set the current culture to French (France).
        CultureInfo.CurrentCulture = CultureInfo.CreateSpecificCulture("fr-FR");

        DateTime midYear = new DateTime(2013, 7, 1);
        Console.WriteLine($"{midYear:d} is a {DateUtilities.GetDayName(midYear)}.");
        Console.WriteLine($"{midYear:d} is a {DateUtilities.GetDayName((int)midYear.DayOfWeek)}.");
        Console.WriteLine($"{midYear:d} is in {DateUtilities.GetMonthName(midYear)}.");
        Console.WriteLine($"{midYear:d} is in {DateUtilities.GetMonthName(midYear.Month)}.");
    }
}

public static class DateUtilities
{
    public static string GetDayName(int dayOfWeek)
    {
        if (dayOfWeek < 0 | dayOfWeek > DateTimeFormatInfo.CurrentInfo.DayNames.Length)
            return String.Empty;
        else
            return DateTimeFormatInfo.CurrentInfo.DayNames[dayOfWeek];
    }

    public static string GetDayName(DateTime date)
    {
        return date.ToString("dddd");
    }

    public static string GetMonthName(int month)
    {
        if (month < 1 | month > DateTimeFormatInfo.CurrentInfo.MonthNames.Length - 1)
            return String.Empty;
        else
            return DateTimeFormatInfo.CurrentInfo.MonthNames[month - 1];
    }

    public static string GetMonthName(DateTime date)
    {
        return date.ToString("MMMM");
    }
}

// The example displays the following output:
//       01/07/2013 is a lundi.
//       01/07/2013 is a lundi.
//       01/07/2013 is in juillet.
//       01/07/2013 is in juillet.
Imports System.Globalization
Imports System.Threading

Module Example13
    Public Sub Main13()
        ' Set the current culture to French (France).
        CultureInfo.CurrentCulture = CultureInfo.CreateSpecificCulture("fr-FR")

        Dim midYear As Date = #07/01/2013#
        Console.WriteLine("{0:d} is a {1}.", midYear, DateUtilities.GetDayName(midYear))
        Console.WriteLine("{0:d} is a {1}.", midYear, DateUtilities.GetDayName(midYear.DayOfWeek))
        Console.WriteLine("{0:d} is in {1}.", midYear, DateUtilities.GetMonthName(midYear))
        Console.WriteLine("{0:d} is in {1}.", midYear, DateUtilities.GetMonthName(midYear.Month))
    End Sub
End Module

Public Class DateUtilities
    Public Shared Function GetDayName(dayOfWeek As Integer) As String
        If dayOfWeek < 0 Or dayOfWeek > DateTimeFormatInfo.CurrentInfo.DayNames.Length Then
            Return String.Empty
        Else
            Return DateTimeFormatInfo.CurrentInfo.DayNames(dayOfWeek)
        End If
    End Function

    Public Shared Function GetDayName(dat As Date) As String
        Return dat.ToString("dddd")
    End Function

    Public Shared Function GetMonthName(month As Integer) As String
        If month < 1 Or month > DateTimeFormatInfo.CurrentInfo.MonthNames.Length - 1 Then
            Return String.Empty
        Else
            Return DateTimeFormatInfo.CurrentInfo.MonthNames(month - 1)
        End If
    End Function

    Public Shared Function GetMonthName(dat As Date) As String
        Return dat.ToString("MMMM")
    End Function
End Class
' The example displays the following output:
'       01/07/2013 is a lundi.
'       01/07/2013 is a lundi.
'       01/07/2013 is in juillet.
'       01/07/2013 is in juillet.

Числовые значения

Обработка чисел зависит от того, отображаются ли они в пользовательском интерфейсе или сохраняются. В этом разделе рассматриваются оба использования.

Замечание

В операциях синтаксического анализа и форматирования .NET распознает только основные латинские символы от 0 до 9 (U+0030 до U+0039) как числовые цифры.

Отображение числовых значений

Как правило, при отображении чисел в пользовательском интерфейсе следует использовать форматирование, учитывающее культуру пользователя, которая определяется CultureInfo.CurrentCulture свойством и NumberFormatInfo объектом, возвращаемым свойством CultureInfo.CurrentCulture.NumberFormat. Правила форматирования текущей культуры автоматически используются при создании формата даты следующими способами:

  • Использование метода любого числового типа без параметров.
  • Использование метода ToString(String) любого числового типа, который включает строку формата в качестве аргумента.
  • Использование составного форматирования с числовыми значениями.

В следующем примере показана средняя температура в месяц в Париже, Франция. Сначала программа устанавливает текущую культуру на французский (Франция) перед отображением данных, а затем изменяет её на английский (США). В каждом случае названия месяцев и температуры отображаются в формате, подходящем для этой культуры. Обратите внимание, что две культуры используют разные десятичные разделители в значении температуры. Кроме того, обратите внимание, что в примере используется настраиваемая строка формата даты и времени MMMM для отображения полного имени месяца, и что он выделяет соответствующее количество места для имени месяца в результирующей строке, определяя длину самого длинного месяца в массиве DateTimeFormatInfo.MonthNames .

using System;
using System.Globalization;
using System.Threading;

public class Example14
{
    public static void Main14()
    {
        DateTime dateForMonth = new DateTime(2013, 1, 1);
        double[] temperatures = {  3.4, 3.5, 7.6, 10.4, 14.5, 17.2,
                                19.9, 18.2, 15.9, 11.3, 6.9, 5.3 };

        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("fr-FR");
        Console.WriteLine($"Current Culture: {CultureInfo.CurrentCulture.DisplayName}");
        // Build the format string dynamically so we allocate enough space for the month name.
        string fmtString = "{0,-" + GetLongestMonthNameLength().ToString() + ":MMMM}     {1,4}";
        for (int ctr = 0; ctr < temperatures.Length; ctr++)
            Console.WriteLine(fmtString,
                              dateForMonth.AddMonths(ctr),
                              temperatures[ctr]);

        Console.WriteLine();

        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US");
        Console.WriteLine($"Current Culture: {CultureInfo.CurrentCulture.DisplayName}");
        fmtString = "{0,-" + GetLongestMonthNameLength().ToString() + ":MMMM}     {1,4}";
        for (int ctr = 0; ctr < temperatures.Length; ctr++)
            Console.WriteLine(fmtString,
                              dateForMonth.AddMonths(ctr),
                              temperatures[ctr]);
    }

    private static int GetLongestMonthNameLength()
    {
        int length = 0;
        foreach (var nameOfMonth in DateTimeFormatInfo.CurrentInfo.MonthNames)
            if (nameOfMonth.Length > length) length = nameOfMonth.Length;

        return length;
    }
}

// The example displays the following output:
//    Current Culture: French (France)
//       janvier        3,4
//       février        3,5
//       mars           7,6
//       avril         10,4
//       mai           14,5
//       juin          17,2
//       juillet       19,9
//       août          18,2
//       septembre     15,9
//       octobre       11,3
//       novembre       6,9
//       décembre       5,3
//
//       Current Culture: English (United States)
//       January        3.4
//       February       3.5
//       March          7.6
//       April         10.4
//       May           14.5
//       June          17.2
//       July          19.9
//       August        18.2
//       September     15.9
//       October       11.3
//       November       6.9
//       December       5.3
Imports System.Globalization
Imports System.Threading

Module Example14
    Public Sub Main14()
        Dim dateForMonth As Date = #1/1/2013#
        Dim temperatures() As Double = {3.4, 3.5, 7.6, 10.4, 14.5, 17.2,
                                         19.9, 18.2, 15.9, 11.3, 6.9, 5.3}

        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("fr-FR")
        Console.WriteLine("Current Culture: {0}", CultureInfo.CurrentCulture.DisplayName)
        Dim fmtString As String = "{0,-" + GetLongestMonthNameLength().ToString() + ":MMMM}     {1,4}"
        For ctr = 0 To temperatures.Length - 1
            Console.WriteLine(fmtString,
                              dateForMonth.AddMonths(ctr),
                              temperatures(ctr))
        Next
        Console.WriteLine()

        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US")
        Console.WriteLine("Current Culture: {0}", CultureInfo.CurrentCulture.DisplayName)
        ' Build the format string dynamically so we allocate enough space for the month name.
        fmtString = "{0,-" + GetLongestMonthNameLength().ToString() + ":MMMM}     {1,4}"
        For ctr = 0 To temperatures.Length - 1
            Console.WriteLine(fmtString,
                              dateForMonth.AddMonths(ctr),
                              temperatures(ctr))
        Next
    End Sub

    Private Function GetLongestMonthNameLength() As Integer
        Dim length As Integer
        For Each nameOfMonth In DateTimeFormatInfo.CurrentInfo.MonthNames
            If nameOfMonth.Length > length Then length = nameOfMonth.Length
        Next
        Return length
    End Function
End Module
' The example displays the following output:
'       Current Culture: French (France)
'       janvier        3,4
'       février        3,5
'       mars           7,6
'       avril         10,4
'       mai           14,5
'       juin          17,2
'       juillet       19,9
'       août          18,2
'       septembre     15,9
'       octobre       11,3
'       novembre       6,9
'       décembre       5,3
'       
'       Current Culture: English (United States)
'       January        3.4
'       February       3.5
'       March          7.6
'       April         10.4
'       May           14.5
'       June          17.2
'       July          19.9
'       August        18.2
'       September     15.9
'       October       11.3
'       November       6.9
'       December       5.3

Сохранение числовых значений

Никогда не следует сохранять числовые данные в форматах, специфичных для культуры. Это распространенная ошибка программирования, которая приводит к повреждению данных или исключению среды выполнения. В следующем примере генерируются десять случайных чисел с плавающей точкой, а затем они сериализуются в виде строк с использованием соглашений о форматировании английской (США) культуры. Когда данные извлекаются и анализируются с помощью соглашений английского языка (США), они успешно восстанавливаются. Однако при извлечении и анализе с помощью соглашений культуры французского языка (Франция) ни одно из чисел не может быть проанализировано, так как культуры используют разные десятичные разделители.

using System;
using System.Globalization;
using System.IO;
using System.Threading;

public class Example15
{
    public static void Main15()
    {
        // Create ten random doubles.
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US");
        double[] numbers = GetRandomNumbers(10);
        DisplayRandomNumbers(numbers);

        // Persist the numbers as strings.
        StreamWriter sw = new StreamWriter("randoms.dat");
        for (int ctr = 0; ctr < numbers.Length; ctr++)
            sw.Write("{0:R}{1}", numbers[ctr], ctr < numbers.Length - 1 ? "|" : "");

        sw.Close();

        // Read the persisted data.
        StreamReader sr = new StreamReader("randoms.dat");
        string numericData = sr.ReadToEnd();
        sr.Close();
        string[] numberStrings = numericData.Split('|');

        // Restore and display the data using the conventions of the en-US culture.
        Console.WriteLine($"Current Culture: {Thread.CurrentThread.CurrentCulture.DisplayName}");
        foreach (var numberStr in numberStrings)
        {
            double restoredNumber;
            if (Double.TryParse(numberStr, out restoredNumber))
                Console.WriteLine(restoredNumber.ToString("R"));
            else
                Console.WriteLine($"ERROR: Unable to parse '{numberStr}'");
        }
        Console.WriteLine();

        // Restore and display the data using the conventions of the fr-FR culture.
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("fr-FR");
        Console.WriteLine($"Current Culture: {Thread.CurrentThread.CurrentCulture.DisplayName}");
        foreach (var numberStr in numberStrings)
        {
            double restoredNumber;
            if (Double.TryParse(numberStr, out restoredNumber))
                Console.WriteLine(restoredNumber.ToString("R"));
            else
                Console.WriteLine($"ERROR: Unable to parse '{numberStr}'");
        }
    }

    private static double[] GetRandomNumbers(int n)
    {
        Random rnd = new Random();
        double[] numbers = new double[n];
        for (int ctr = 0; ctr < n; ctr++)
            numbers[ctr] = rnd.NextDouble() * 1000;
        return numbers;
    }

    private static void DisplayRandomNumbers(double[] numbers)
    {
        for (int ctr = 0; ctr < numbers.Length; ctr++)
            Console.WriteLine(numbers[ctr].ToString("R"));
        Console.WriteLine();
    }
}

// The example displays output like the following:
//       487.0313743534644
//       674.12000879371533
//       498.72077885024288
//       42.3034229512808
//       970.57311049223563
//       531.33717716268131
//       587.82905693530529
//       562.25210175023039
//       600.7711019370571
//       299.46113717717174
//
//       Current Culture: English (United States)
//       487.0313743534644
//       674.12000879371533
//       498.72077885024288
//       42.3034229512808
//       970.57311049223563
//       531.33717716268131
//       587.82905693530529
//       562.25210175023039
//       600.7711019370571
//       299.46113717717174
//
//       Current Culture: French (France)
//       ERROR: Unable to parse '487.0313743534644'
//       ERROR: Unable to parse '674.12000879371533'
//       ERROR: Unable to parse '498.72077885024288'
//       ERROR: Unable to parse '42.3034229512808'
//       ERROR: Unable to parse '970.57311049223563'
//       ERROR: Unable to parse '531.33717716268131'
//       ERROR: Unable to parse '587.82905693530529'
//       ERROR: Unable to parse '562.25210175023039'
//       ERROR: Unable to parse '600.7711019370571'
//       ERROR: Unable to parse '299.46113717717174'
Imports System.Globalization
Imports System.IO
Imports System.Threading

Module Example15
    Public Sub Main15()
        ' Create ten random doubles.
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US")
        Dim numbers() As Double = GetRandomNumbers(10)
        DisplayRandomNumbers(numbers)

        ' Persist the numbers as strings.
        Dim sw As New StreamWriter("randoms.dat")
        For ctr As Integer = 0 To numbers.Length - 1
            sw.Write("{0:R}{1}", numbers(ctr), If(ctr < numbers.Length - 1, "|", ""))
        Next
        sw.Close()

        ' Read the persisted data.
        Dim sr As New StreamReader("randoms.dat")
        Dim numericData As String = sr.ReadToEnd()
        sr.Close()
        Dim numberStrings() As String = numericData.Split("|"c)

        ' Restore and display the data using the conventions of the en-US culture.
        Console.WriteLine("Current Culture: {0}",
                          Thread.CurrentThread.CurrentCulture.DisplayName)
        For Each numberStr In numberStrings
            Dim restoredNumber As Double
            If Double.TryParse(numberStr, restoredNumber) Then
                Console.WriteLine(restoredNumber.ToString("R"))
            Else
                Console.WriteLine("ERROR: Unable to parse '{0}'", numberStr)
            End If
        Next
        Console.WriteLine()

        ' Restore and display the data using the conventions of the fr-FR culture.
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("fr-FR")
        Console.WriteLine("Current Culture: {0}",
                          Thread.CurrentThread.CurrentCulture.DisplayName)
        For Each numberStr In numberStrings
            Dim restoredNumber As Double
            If Double.TryParse(numberStr, restoredNumber) Then
                Console.WriteLine(restoredNumber.ToString("R"))
            Else
                Console.WriteLine("ERROR: Unable to parse '{0}'", numberStr)
            End If
        Next
    End Sub

    Private Function GetRandomNumbers(n As Integer) As Double()
        Dim rnd As New Random()
        Dim numbers(n - 1) As Double
        For ctr As Integer = 0 To n - 1
            numbers(ctr) = rnd.NextDouble * 1000
        Next
        Return numbers
    End Function

    Private Sub DisplayRandomNumbers(numbers As Double())
        For ctr As Integer = 0 To numbers.Length - 1
            Console.WriteLine(numbers(ctr).ToString("R"))
        Next
        Console.WriteLine()
    End Sub
End Module
' The example displays output like the following:
'       487.0313743534644
'       674.12000879371533
'       498.72077885024288
'       42.3034229512808
'       970.57311049223563
'       531.33717716268131
'       587.82905693530529
'       562.25210175023039
'       600.7711019370571
'       299.46113717717174
'       
'       Current Culture: English (United States)
'       487.0313743534644
'       674.12000879371533
'       498.72077885024288
'       42.3034229512808
'       970.57311049223563
'       531.33717716268131
'       587.82905693530529
'       562.25210175023039
'       600.7711019370571
'       299.46113717717174
'       
'       Current Culture: French (France)
'       ERROR: Unable to parse '487.0313743534644'
'       ERROR: Unable to parse '674.12000879371533'
'       ERROR: Unable to parse '498.72077885024288'
'       ERROR: Unable to parse '42.3034229512808'
'       ERROR: Unable to parse '970.57311049223563'
'       ERROR: Unable to parse '531.33717716268131'
'       ERROR: Unable to parse '587.82905693530529'
'       ERROR: Unable to parse '562.25210175023039'
'       ERROR: Unable to parse '600.7711019370571'
'       ERROR: Unable to parse '299.46113717717174'

Чтобы избежать этой проблемы, можно использовать один из следующих методов:

  • Сохраните и разберите строковое представление числа, используя пользовательскую строку форматирования, которая одинакова независимо от культуры пользователя.
  • Сохраните число в виде строки, используя соглашения форматирования инвариантной культуры, которые возвращаются свойством CultureInfo.InvariantCulture.

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

using System;
using System.Globalization;
using System.IO;
using System.Threading;

public class Example1
{
   public static void Main1()
   {
      // Display the currency value.
      Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US");
      Decimal value = 16039.47m;
      Console.WriteLine($"Current Culture: {CultureInfo.CurrentCulture.DisplayName}");
      Console.WriteLine($"Currency Value: {value:C2}");

      // Persist the currency value as a string.
      StreamWriter sw = new StreamWriter("currency.dat");
      sw.Write(value.ToString("C2"));
      sw.Close();

      // Read the persisted data using the current culture.
      StreamReader sr = new StreamReader("currency.dat");
      string currencyData = sr.ReadToEnd();
      sr.Close();

      // Restore and display the data using the conventions of the current culture.
      Decimal restoredValue;
      if (Decimal.TryParse(currencyData, out restoredValue))
         Console.WriteLine(restoredValue.ToString("C2"));
      else
         Console.WriteLine($"ERROR: Unable to parse '{currencyData}'");
      Console.WriteLine();

      // Restore and display the data using the conventions of the en-GB culture.
      Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-GB");
      Console.WriteLine($"Current Culture: {Thread.CurrentThread.CurrentCulture.DisplayName}");
      if (Decimal.TryParse(currencyData, NumberStyles.Currency, null, out restoredValue))
         Console.WriteLine(restoredValue.ToString("C2"));
      else
         Console.WriteLine($"ERROR: Unable to parse '{currencyData}'");
      Console.WriteLine();
   }
}
// The example displays output like the following:
//       Current Culture: English (United States)
//       Currency Value: $16,039.47
//       ERROR: Unable to parse '$16,039.47'
//
//       Current Culture: English (United Kingdom)
//       ERROR: Unable to parse '$16,039.47'
Imports System.Globalization
Imports System.IO
Imports System.Threading

Module Example1
    Public Sub Main1()
        ' Display the currency value.
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US")
        Dim value As Decimal = 16039.47D
        Console.WriteLine("Current Culture: {0}", CultureInfo.CurrentCulture.DisplayName)
        Console.WriteLine("Currency Value: {0:C2}", value)

        ' Persist the currency value as a string.
        Dim sw As New StreamWriter("currency.dat")
        sw.Write(value.ToString("C2"))
        sw.Close()

        ' Read the persisted data using the current culture.
        Dim sr As New StreamReader("currency.dat")
        Dim currencyData As String = sr.ReadToEnd()
        sr.Close()

        ' Restore and display the data using the conventions of the current culture.
        Dim restoredValue As Decimal
        If Decimal.TryParse(currencyData, restoredValue) Then
            Console.WriteLine(restoredValue.ToString("C2"))
        Else
            Console.WriteLine("ERROR: Unable to parse '{0}'", currencyData)
        End If
        Console.WriteLine()

        ' Restore and display the data using the conventions of the en-GB culture.
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-GB")
        Console.WriteLine("Current Culture: {0}",
                          Thread.CurrentThread.CurrentCulture.DisplayName)
        If Decimal.TryParse(currencyData, NumberStyles.Currency, Nothing, restoredValue) Then
            Console.WriteLine(restoredValue.ToString("C2"))
        Else
            Console.WriteLine("ERROR: Unable to parse '{0}'", currencyData)
        End If
        Console.WriteLine()
    End Sub
End Module
' The example displays output like the following:
'       Current Culture: English (United States)
'       Currency Value: $16,039.47
'       ERROR: Unable to parse '$16,039.47'
'       
'       Current Culture: English (United Kingdom)
'       ERROR: Unable to parse '$16,039.47'

Вместо этого следует сериализовать числовое значение вместе с некоторыми сведениями о культуре, такими как ее название, чтобы значение и его символ валюты можно было десериализовать независимо от текущей культуры. В следующем примере CurrencyValue определяется структура с двумя членами: Decimal значение и имя культуры, к которой принадлежит значение.

using System;
using System.Globalization;
using System.Text.Json;
using System.Threading;

public class Example2
{
    public static void Main2()
    {
        // Display the currency value.
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US");
        Decimal value = 16039.47m;
        Console.WriteLine($"Current Culture: {CultureInfo.CurrentCulture.DisplayName}");
        Console.WriteLine($"Currency Value: {value:C2}");

        // Serialize the currency data.
        CurrencyValue data = new()
        {
            Amount = value,
            CultureName = CultureInfo.CurrentCulture.Name
        };
        string serialized = JsonSerializer.Serialize(data);
        Console.WriteLine();

        // Change the current culture.
        CultureInfo.CurrentCulture = CultureInfo.CreateSpecificCulture("en-GB");
        Console.WriteLine($"Current Culture: {CultureInfo.CurrentCulture.DisplayName}");

        // Deserialize the data.
        CurrencyValue restoredData = JsonSerializer.Deserialize<CurrencyValue>(serialized);

        // Display the round-tripped value.
        CultureInfo culture = CultureInfo.CreateSpecificCulture(restoredData.CultureName);
        Console.WriteLine($"Currency Value: {restoredData.Amount.ToString("C2", culture)}");
    }
}

internal struct CurrencyValue
{
    public decimal Amount { get; set; }
    public string CultureName { get; set; }
}

// The example displays the following output:
//       Current Culture: English (United States)
//       Currency Value: $16,039.47
//
//       Current Culture: English (United Kingdom)
//       Currency Value: $16,039.47
Imports System.Globalization
Imports System.Text.Json
Imports System.Threading

Friend Structure CurrencyValue
    Public Property Amount As Decimal
    Public Property CultureName As String
End Structure

Module Example2
    Public Sub Main2()
        ' Display the currency value.
        Thread.CurrentThread.CurrentCulture = CultureInfo.CreateSpecificCulture("en-US")
        Dim value As Decimal = 16039.47D
        Console.WriteLine("Current Culture: {0}", CultureInfo.CurrentCulture.DisplayName)
        Console.WriteLine("Currency Value: {0:C2}", value)

        ' Serialize the currency data.
        Dim data As New CurrencyValue With {
            .Amount = value,
            .CultureName = CultureInfo.CurrentCulture.Name
        }

        Dim serialized As String = JsonSerializer.Serialize(data)
        Console.WriteLine()

        ' Change the current culture.
        CultureInfo.CurrentCulture = CultureInfo.CreateSpecificCulture("en-GB")
        Console.WriteLine("Current Culture: {0}", CultureInfo.CurrentCulture.DisplayName)

        ' Deserialize the data.
        Dim restoredData As CurrencyValue = JsonSerializer.Deserialize(Of CurrencyValue)(serialized)

        ' Display the round-tripped value.
        Dim culture As CultureInfo = CultureInfo.CreateSpecificCulture(restoredData.CultureName)
        Console.WriteLine("Currency Value: {0}", restoredData.Amount.ToString("C2", culture))
    End Sub
End Module

' The example displays the following output:
'       Current Culture: English (United States)
'       Currency Value: $16,039.47
'       
'       Current Culture: English (United Kingdom)
'       Currency Value: $16,039.47

Работа с настройками, специфичными для культуры

В .NET класс CultureInfo представляет определенную культуру или регион. Некоторые из его свойств возвращают объекты, предоставляющие конкретную информацию о некоторых аспектах культуры.

  • Свойство CultureInfo.CompareInfo возвращает объект, содержащий сведения о том, как культурные CompareInfo параметры сравнивают и упорядочивают строки.

  • Свойство CultureInfo.DateTimeFormat возвращает DateTimeFormatInfo объект, предоставляющий сведения о языке и региональных параметрах, используемые в форматировании данных даты и времени.

  • Свойство CultureInfo.NumberFormat возвращает объект NumberFormatInfo, предоставляющий культуру-специфическую информацию, используемую в форматировании числовых данных.

  • Свойство CultureInfo.TextInfo возвращает объект, предоставляющий TextInfo сведения о системе письма культуры.

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

  • Значения отдельных свойств могут изменяться со временем, по мере исправления данных, появления более качественных данных или изменения культурных норм.

  • Значения отдельных свойств могут отличаться в разных версиях .NET или операционных систем.

  • .NET поддерживает настраиваемые язык и региональные параметры. Это позволяет определить новую пользовательскую культуру, которая либо дополняет существующие стандартные культуры, либо полностью заменяет существующую стандартную культуру.

  • В системах Windows пользователь может настраивать региональные и языковые параметры с помощью приложения Регион и Язык на панели управления. При создании экземпляра CultureInfo объекта можно определить, отражает ли он эти настройки пользователя, вызвав CultureInfo(String, Boolean) конструктор. Как правило, для приложений конечных пользователей следует учитывать предпочтения пользователей, чтобы пользователь представлял данные в формате, который они ожидают.

См. также