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

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

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

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

Строки

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

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

По умолчанию в .NET используются строки Юникода. Строка Юникода состоит из 0, одного или нескольких объектов 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:   {0}", composite.IndexOf('\u00C0'));
        Console.WriteLine("Comparing using String: {0}", 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.Compare, также используют объект CompareInfo.

В .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: {0:D}", dates[0]);
        Console.WriteLine("   Sunrise: {0:T}", dates[0]);
        Console.WriteLine("   Sunset:  {0:T}", dates[1]);
    }
}

// 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-е сентября, а вторая не анализируется, поскольку в григорианском календаре нет 18-го месяца.

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: {0}",
                          Thread.CurrentThread.CurrentCulture.DisplayName);
        foreach (var dateStr in dateStrings)
        {
            DateTime restoredDate;
            if (DateTime.TryParse(dateStr, out restoredDate))
                Console.WriteLine("The date is {0:D}", restoredDate);
            else
                Console.WriteLine("ERROR: Unable to parse {0}", 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: {0}",
                          Thread.CurrentThread.CurrentCulture.DisplayName);
        foreach (var dateStr in dateStrings)
        {
            DateTime restoredDate;
            if (DateTime.TryParse(dateStr, out restoredDate))
                Console.WriteLine("The date is {0:D}", restoredDate);
            else
                Console.WriteLine("ERROR: Unable to parse {0}", 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: {0}",
                          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 {0:D}", restoredDate);
            else
                Console.WriteLine("ERROR: Unable to parse {0}", 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: {0}",
                          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 {0:D}", restoredDate);
            else
                Console.WriteLine("ERROR: Unable to parse {0}", 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"). Если значение времени представляет определенный момент времени и восстанавливается из сериализуемого значения, необходимо убедиться, что оно представляет тот же момент времени независимо от географического положения пользователя или часового пояса.

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

  • "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("'{0}' --> {1} {2}",
                              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" (цикл обработки). Последующая десериализация в целевой системе.
  • Преобразование его во время в формате 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

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

'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("{0:g} + {1:N1} hours = {2:g}",
                          date1, interval.TotalHours, date2);
    }
}

// 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("{0:g} + {1:N1} hours = {2:g}",
                          date1, interval.TotalHours, date2);
    }
}

// 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("{0:d} is a {1}.", midYear, 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.

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

С помощью строк даты и времени пользовательского формата или свойств объекта 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("{0:d} is a {1}.", midYear, DateUtilities.GetDayName(midYear));
        Console.WriteLine("{0:d} is a {1}.", midYear, DateUtilities.GetDayName((int)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));
    }
}

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 параметров любого числового типа.
  • 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: {0}", 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: {0}", 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: {0}",
                          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 '{0}'", 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: {0}",
                          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 '{0}'", 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: {0}", CultureInfo.CurrentCulture.DisplayName);
      Console.WriteLine("Currency Value: {0:C2}", value);

      // 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 '{0}'", 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: {0}",
                        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 '{0}'", 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). Обычно для приложений, предназначенных для конечных пользователей, необходимо учитывать предпочтения пользователя и предоставлять пользователю данные в том формате, в котором он ожидает их увидеть.

См. также