Globalizacja

Globalizacja obejmuje projektowanie i opracowywanie aplikacji gotowej do użycia na świecie, która obsługuje zlokalizowane interfejsy i dane regionalne dla użytkowników w wielu kulturach. Przed rozpoczęciem fazy projektowania należy określić, które kultury będą obsługiwane przez aplikację. Mimo że aplikacja jest przeznaczona dla pojedynczej kultury lub regionu jako domyślnego, można ją zaprojektować i napisać tak, aby można ją było łatwo rozszerzyć na użytkowników w innych kulturach lub regionach.

Jako deweloperzy mamy założenia dotyczące interfejsów użytkownika i danych tworzonych przez nasze kultury. Na przykład w przypadku dewelopera języka angielskiego w Stany Zjednoczone serializowanie danych daty i godziny jako ciągu w formacie MM/dd/yyyy hh:mm:ss wydaje się całkowicie uzasadnione. Jednak deserializacji tego ciągu w systemie w innej kulturze może zgłosić FormatException wyjątek lub wygenerować niedokładne dane. Globalizacja umożliwia nam zidentyfikowanie takich założeń specyficznych dla kultury i upewnienie się, że nie wpływają one na projekt lub kod naszej aplikacji.

W tym artykule omówiono niektóre główne problemy, które należy wziąć pod uwagę, oraz najlepsze rozwiązania, które można zastosować podczas obsługi ciągów, wartości daty i godziny oraz wartości liczbowych w zglobalizowanej aplikacji.

Ciągi

Obsługa znaków i ciągów jest centralnym celem globalizacji, ponieważ każda kultura lub region może używać różnych znaków i zestawów znaków i sortować je inaczej. Ta sekcja zawiera zalecenia dotyczące używania ciągów w aplikacjach zglobalizowanej.

Używanie kodu Unicode wewnętrznie

Domyślnie platforma .NET używa ciągów Unicode. Ciąg Unicode składa się z zera, jednego lub większej liczby Char obiektów, z których każdy reprezentuje jednostkę kodu UTF-16. Istnieje reprezentacja Unicode dla prawie każdego znaku w każdym zestawie znaków używanych na całym świecie.

Wiele aplikacji i systemów operacyjnych, w tym systemu operacyjnego Windows, może również używać stron kodu do reprezentowania zestawów znaków. Strony kodowe zazwyczaj zawierają standardowe wartości ASCII z 0x00 przez 0x7F i mapować inne znaki na pozostałe wartości z 0x80 przez 0xFF. Interpretacja wartości z 0x80 przez 0xFF zależy od określonej strony kodowej. W związku z tym należy unikać używania stron kodu w zglobalizowanej aplikacji, jeśli jest to możliwe.

Poniższy przykład ilustruje niebezpieczeństwa interpretacji danych strony kodowej, gdy domyślna strona kodowa w systemie różni się od strony kodowej, na której dane zostały zapisane. (Aby zasymulować ten scenariusz, przykład jawnie określa różne strony kodu). Najpierw w przykładzie zdefiniowano tablicę składającą się z wielkich liter alfabetu greckiego. Koduje je w tablicy bajtów przy użyciu strony kodowej 737 (znanej również jako MS-DOS Grecki) i zapisuje tablicę bajtów w pliku. Jeśli plik zostanie pobrany, a tablica bajtów zostanie zdekodowana przy użyciu strony kodowej 737, oryginalne znaki zostaną przywrócone. Jeśli jednak plik zostanie pobrany, a tablica bajtów zostanie zdekodowana przy użyciu strony kodowej 1252 (lub Windows-1252, która reprezentuje znaki alfabetu łacińskiego), oryginalne znaki zostaną utracone.

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:
'       ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩ
'       €‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’""•–—

Użycie formatu Unicode gwarantuje, że te same jednostki kodu zawsze mapowane są na te same znaki i że te same znaki są zawsze mapowane na te same tablice bajtów.

Korzystanie z plików zasobów

Nawet jeśli tworzysz aplikację przeznaczoną dla pojedynczej kultury lub regionu, należy użyć plików zasobów do przechowywania ciągów i innych zasobów wyświetlanych w interfejsie użytkownika. Nigdy nie należy dodawać ich bezpośrednio do kodu. Korzystanie z plików zasobów ma wiele zalet:

  • Wszystkie ciągi znajdują się w jednej lokalizacji. Nie musisz wyszukiwać w całym kodzie źródłowym, aby identyfikować ciągi, które mają być modyfikowane dla określonego języka lub kultury.
  • Nie ma potrzeby duplikowania ciągów. Deweloperzy, którzy nie używają plików zasobów, często definiują ten sam ciąg w wielu plikach kodu źródłowego. Duplikowanie zwiększa prawdopodobieństwo, że co najmniej jedno wystąpienie zostanie pominięte po zmodyfikowaniu ciągu.
  • Zasoby inne niż ciągi, takie jak obrazy lub dane binarne, można uwzględnić w pliku zasobów zamiast przechowywać je w osobnym pliku autonomicznym, aby można je było łatwo pobrać.

Korzystanie z plików zasobów ma szczególne zalety w przypadku tworzenia zlokalizowanej aplikacji. Podczas wdrażania zasobów w zestawach satelickich środowisko uruchomieniowe języka wspólnego automatycznie wybiera odpowiedni dla kultury zasób oparty na bieżącej kulturze interfejsu użytkownika zdefiniowanej CultureInfo.CurrentUICulture przez właściwość. Jeśli podasz odpowiedni zasób specyficzny dla kultury i poprawnie utworzysz wystąpienie ResourceManager obiektu lub użyj silnie typizowanej klasy zasobów, środowisko uruchomieniowe obsługuje szczegóły pobierania odpowiednich zasobów.

Aby uzyskać więcej informacji na temat tworzenia plików zasobów, zobacz Tworzenie plików zasobów. Aby uzyskać informacje o tworzeniu i wdrażaniu zestawów satelickich, zobacz Tworzenie zestawów satelickich oraz Tworzenie pakietów i wdrażanie zasobów.

Wyszukiwanie i porównywanie ciągów

Jeśli to możliwe, należy obsługiwać ciągi jako całe ciągi zamiast obsługiwać je jako serię pojedynczych znaków. Jest to szczególnie ważne w przypadku sortowania lub wyszukiwania podciągów, aby zapobiec problemom związanym z analizowaniem połączonych znaków.

Napiwek

Możesz użyć StringInfo klasy do pracy z elementami tekstowymi, a nie z poszczególnymi znakami w ciągu.

W wyszukiwaniu ciągów i porównaniach typowym błędem jest traktowanie ciągu jako kolekcji znaków, z których każdy jest reprezentowany przez Char obiekt. W rzeczywistości pojedynczy znak może być tworzony przez jeden, dwa lub więcej Char obiektów. Takie znaki są najczęściej spotykane w ciągach z kultur, których alfabety składają się z znaków poza zakresem znaków Unicode Basic Latin (od U+0021 do U+007E). Poniższy przykład próbuje znaleźć indeks alfabetu łacińskiego WIELKA LITERA A Z znakiem GRAVE (U+00C0) w ciągu. Ten znak można jednak przedstawić na dwa różne sposoby: jako pojedynczą jednostkę kodu (U+00C0) lub jako znak złożony (dwie jednostki kodu: U+0041 i U+0300). W tym przypadku znak jest reprezentowany w wystąpieniu ciągu przez dwa Char obiekty, U+0041 i U+0300. Przykładowy kod wywołuje String.IndexOf(Char) przeciążenia i String.IndexOf(String) w celu znalezienia pozycji tego znaku w wystąpieniu ciągu, ale zwracają one różne wyniki. Pierwsze wywołanie metody ma Char argument; wykonuje porównanie porządkowe i dlatego nie może odnaleźć dopasowania. Drugie wywołanie ma String argument; wykonuje porównanie wrażliwe na kulturę i w związku z tym znajduje dopasowanie.

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

Można uniknąć niektórych niejednoznaczności tego przykładu (wywołania dwóch podobnych przeciążeń metody zwracającej różne wyniki), wywołując przeciążenie, które zawiera StringComparison parametr, taki jak String.IndexOf(String, StringComparison) metoda lub String.LastIndexOf(String, StringComparison) .

Jednak wyszukiwanie nie zawsze jest wrażliwe na kulturę. Jeśli celem wyszukiwania jest podjęcie decyzji dotyczącej zabezpieczeń lub zezwolenie lub nie zezwalanie na dostęp do niektórych zasobów, porównanie powinno być porządkowe zgodnie z opisem w następnej sekcji.

Testowanie ciągów pod kątem równości

Jeśli chcesz przetestować dwa ciągi pod kątem równości, a nie określić sposobu ich porównywania w kolejności sortowania, użyj String.Equals metody zamiast metody porównania ciągów, takiej jak String.Compare lub CompareInfo.Compare.

Porównania równości są zwykle wykonywane w celu warunkowego uzyskania dostępu do niektórych zasobów. Na przykład można wykonać porównanie równości w celu zweryfikowania hasła lub potwierdzenia, że plik istnieje. Takie porównania nielingwistyczne powinny być zawsze porządkowe, a nie wrażliwe na kulturę. Ogólnie rzecz biorąc, należy wywołać metodę wystąpienia String.Equals(String, StringComparison) lub metodę statyczną String.Equals(String, String, StringComparison) z wartością StringComparison.Ordinal dla ciągów, takich jak hasła, oraz wartość StringComparison.OrdinalIgnoreCase dla ciągów, takich jak nazwy plików lub identyfikatory URI.

Porównania równości czasami obejmują wyszukiwania lub porównania podciągów, a nie wywołania String.Equals metody . W niektórych przypadkach można użyć wyszukiwania podciągów, aby określić, czy podciąg jest równy innemu ciągowi. Jeśli celem tego porównania jest nielingwistyczna, wyszukiwanie powinno być również porządkowe, a nie wrażliwe na kulturę.

Poniższy przykład ilustruje niebezpieczeństwo wyszukiwania wrażliwego na kulturę danych nielingwistycznych. Metoda została zaprojektowana AccessesFileSystem tak, aby uniemożliwić dostęp do systemu plików dla identyfikatorów URI rozpoczynających się od podciągu "FILE". W tym celu wykonuje ona porównanie z uwzględnieniem kultury, bez uwzględniania wielkości liter na początku identyfikatora URI z ciągiem "FILE". Ponieważ identyfikator URI, który uzyskuje dostęp do systemu plików, może zaczynać się od "FILE:" lub "file:", niejawnym założeniem jest to, że "i" (U+0069) jest zawsze małymi literami "I" (U+0049). Jednak w języku tureckim i azerbejdżskim wielkie litery "i" to "İ" (U+0130). Ze względu na tę rozbieżność porównanie wrażliwe na kulturę umożliwia dostęp do systemu plików, gdy powinien być zabroniony.

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.

Ten problem można uniknąć, wykonując porównanie porządkowe, które ignoruje wielkość liter, jak pokazano w poniższym przykładzie.

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.

Kolejność i sortowanie ciągów

Zazwyczaj uporządkowane ciągi, które mają być wyświetlane w interfejsie użytkownika, powinny być sortowane na podstawie kultury. W większości przypadków takie porównania ciągów są obsługiwane niejawnie przez platformę .NET podczas wywoływania metody, która sortuje ciągi, takie jak Array.Sort lub List<T>.Sort. Domyślnie ciągi są sortowane przy użyciu konwencji sortowania bieżącej kultury. Poniższy przykład ilustruje różnicę, gdy tablica ciągów jest sortowana przy użyciu konwencji kultury angielskiej (Stany Zjednoczone) i kultury szwedzkiej (Szwecja).

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

Porównanie ciągów wrażliwych na kulturę CompareInfo jest definiowane przez obiekt, który jest zwracany przez właściwość każdej kultury CultureInfo.CompareInfo . Porównania ciągów uwzględniających kulturę, które używają przeciążeń metody, również używają String.CompareCompareInfo obiektu .

Platforma .NET używa tabel do przeprowadzania sortowania uwzględniającego kulturę na danych ciągów. Zawartość tych tabel, które zawierają dane dotyczące wag sortowania i normalizacji ciągów, jest określana przez wersję standardu Unicode zaimplementowaną przez określoną wersję platformy .NET. W poniższej tabeli wymieniono wersje Unicode zaimplementowane przez określone wersje platformy .NET. Ta lista obsługiwanych wersji Unicode dotyczy tylko porównania znaków i sortowania; nie ma zastosowania do klasyfikacji znaków Unicode według kategorii. Aby uzyskać więcej informacji, zobacz sekcję "Ciągi i Standard Unicode" w String artykule.

Wersja systemu .NET Framework System operacyjny Wersja Unicode
.NET Framework 2.0 Wszystkie systemy operacyjne Unicode 4.1
.NET Framework 3.0 Wszystkie systemy operacyjne Unicode 4.1
.NET Framework 3.5 Wszystkie systemy operacyjne Unicode 4.1
.NET Framework 4 Wszystkie systemy operacyjne Unicode 5.0
Program .NET Framework 4.5 lub nowszy Windows 7 Unicode 5.0
Program .NET Framework 4.5 lub nowszy Systemy operacyjne Windows 8 i nowsze Unicode 6.3.0
.NET Core i .NET 5+ Zależy od wersji standardu Unicode obsługiwanej przez bazowy system operacyjny.

Począwszy od programu .NET Framework 4.5 i we wszystkich wersjach platformy .NET Core i .NET 5+, porównanie ciągów i sortowanie zależy od systemu operacyjnego. Program .NET Framework 4.5 lub nowszy w systemie Windows 7 pobiera dane z własnych tabel, które implementują standard Unicode 5.0. Program .NET Framework 4.5 lub nowszy z systemem Windows 8 lub nowszym pobiera dane z tabel systemu operacyjnego, które implementują standard Unicode 6.3. W programach .NET Core i .NET 5 lub nowszych obsługiwana wersja Unicode zależy od bazowego systemu operacyjnego. W przypadku serializacji danych posortowanych z uwzględnieniem kultury można użyć SortVersion klasy , aby określić, kiedy dane serializowane muszą być sortowane, tak aby były zgodne z platformą .NET i kolejnością sortowania systemu operacyjnego. Aby zapoznać się z przykładem, zobacz SortVersion temat klasy.

Jeśli aplikacja wykonuje obszerne rodzaje danych ciągów specyficznych dla kultury, możesz pracować z klasą SortKey , aby porównać ciągi. Klucz sortowania odzwierciedla wagi sortowania specyficzne dla kultury, w tym alfabetyczne, wielkość liter i wagi diakrytyczne określonego ciągu. Ponieważ porównania używające kluczy sortowania są binarne, są szybsze niż porównania, które używają CompareInfo obiektu niejawnie lub jawnie. Należy utworzyć klucz sortowania specyficzny dla kultury dla określonego ciągu, przekazując ciąg do CompareInfo.GetSortKey metody .

Poniższy przykład jest podobny do poprzedniego przykładu. Jednak zamiast wywoływać metodę Array.Sort(Array) , która niejawnie wywołuje CompareInfo.Compare metodę, definiuje implementację System.Collections.Generic.IComparer<T> , która porównuje klucze sortowania, które tworzy wystąpienie i przekazuje do Array.Sort<T>(T[], IComparer<T>) metody.

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

Unikaj łączenia ciągów

Jeśli w ogóle jest to możliwe, unikaj używania złożonych ciągów utworzonych w czasie wykonywania z połączonych fraz. Ciągi złożone są trudne do zlokalizowania, ponieważ często zakładają one kolejność gramatyczną w oryginalnym języku aplikacji, który nie ma zastosowania do innych zlokalizowanych języków.

Obsługa dat i godzin

Sposób obsługi wartości daty i godziny zależy od tego, czy są one wyświetlane w interfejsie użytkownika, czy utrwalone. W tej sekcji przeanalizować oba zastosowania. Omówiono również sposób obsługi różnic strefy czasowej i operacji arytmetycznych podczas pracy z datami i godzinami.

Wyświetlanie dat i godzin

Zazwyczaj gdy daty i godziny są wyświetlane w interfejsie użytkownika, należy użyć konwencji formatowania kultury użytkownika, która jest definiowana przez CultureInfo.CurrentCulture właściwość i przez DateTimeFormatInfo obiekt zwrócony przez CultureInfo.CurrentCulture.DateTimeFormat właściwość. Konwencje formatowania bieżącej kultury są automatycznie używane podczas formatowania daty przy użyciu dowolnej z następujących metod:

W poniższym przykładzie są wyświetlane dane o wschodzie słońca i zachodzie słońca dwa razy dla 11 października 2012 r. Najpierw ustawia obecną kulturę na Chorwacki (Chorwacja), a następnie na angielski (Wielka Brytania). W każdym przypadku daty i godziny są wyświetlane w formacie odpowiednim dla tej kultury.

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

Utrwalanie dat i godzin

Nigdy nie należy utrwalać danych daty i godziny w formacie, który może się różnić w zależności od kultury. Jest to typowy błąd programowania, który powoduje uszkodzenie danych lub wyjątek czasu wykonywania. Poniższy przykład serializuje dwie daty, 9 stycznia 2013 r. i 18 sierpnia 2013 r., jako ciągi przy użyciu konwencji formatowania kultury angielskiej (Stany Zjednoczone). Po pobraniu i przeanalizowaniu danych przy użyciu konwencji kultury angielskiej (Stany Zjednoczone) zostanie ona pomyślnie przywrócona. Jednak po pobraniu i przeanalizowaniu przy użyciu konwencji kultury angielskiej (Zjednoczone Królestwo) pierwsza data jest błędnie interpretowana jako 1 września, a druga nie analizuje się, ponieważ kalendarz gregoriański nie ma osiemnastu miesięcy.

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

Ten problem można uniknąć na dowolny z trzech sposobów:

  • Serializuj datę i godzinę w formacie binarnym, a nie jako ciąg.
  • Zapisz i przeanalizuj ciąg reprezentujący datę i godzinę przy użyciu niestandardowego ciągu formatu, który jest taki sam, niezależnie od kultury użytkownika.
  • Zapisz ciąg przy użyciu konwencji formatowania niezmiennej kultury.

Poniższy przykład ilustruje ostatnie podejście. Używa konwencji formatowania niezmiennej kultury zwracanej przez właściwość statyczną 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

Serializacja i rozpoznawanie stref czasowych

Wartość daty i godziny może mieć wiele interpretacji, od ogólnego czasu ("Sklepy otwarte 2 stycznia 2013 r., o 9:00 rano") do określonej chwili w czasie ("Data urodzenia: 2 stycznia 2013 6:32:00 czasu"). Gdy wartość godziny reprezentuje określony moment w czasie i przywracasz ją z serializowanej wartości, upewnij się, że reprezentuje ten sam moment w czasie niezależnie od lokalizacji geograficznej lub strefy czasowej użytkownika.

Poniższy przykład ilustruje ten problem. Zapisuje pojedynczą lokalną wartość daty i godziny jako ciąg w trzech standardowych formatach:

  • "G" dla ogólnej daty długiej.
  • "s" dla sortowalnej daty/godziny.
  • "o" dla daty/godziny w obie strony.
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

Gdy dane są przywracane w systemie w tej samej strefie czasowej co system, w którym została serializowana, deserializowane wartości daty i godziny dokładnie odzwierciedlają oryginalną wartość, jak pokazują dane wyjściowe:

'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

Jeśli jednak przywrócisz dane w systemie w innej strefie czasowej, tylko wartość daty i godziny sformatowana za pomocą standardowego ciągu formatu "o" (round-trip) zachowuje informacje o strefie czasowej i w związku z tym reprezentuje tę samą chwilę w czasie. Oto dane wyjściowe, kiedy dane daty i godziny są przywracane w systemie w strefie czasowej Romans (standardowy):

'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

Aby dokładnie odzwierciedlić wartość daty i godziny reprezentującą pojedynczą chwilę czasu niezależnie od strefy czasowej systemu, na którym są deserializowane dane, można wykonać dowolną z następujących czynności:

  • Zapisz wartość jako ciąg przy użyciu standardowego ciągu formatu "o" (round-trip). Następnie deserializuj go w systemie docelowym.
  • Przekonwertuj go na czas UTC i zapisz go jako ciąg przy użyciu standardowego ciągu formatu "r" (RFC1123). Następnie deserializuj go w systemie docelowym i przekonwertuj go na czas lokalny.
  • Przekonwertuj go na czas UTC i zapisz go jako ciąg przy użyciu standardowego ciągu formatu "u" (uniwersalnego sortowania). Następnie deserializuj go w systemie docelowym i przekonwertuj go na czas lokalny.

Poniższy przykład ilustruje każdą technikę.

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

Gdy dane są serializowane w systemie w strefie czasowej Pacyfik (standardowy czas) i deserializowane w systemie w strefie czasowej Romans (standardowy), w przykładzie są wyświetlane następujące dane wyjściowe:

'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

Aby uzyskać więcej informacji, zobacz Konwertowanie czasów między strefami czasowymi.

Wykonywanie arytmetyki daty i godziny

DateTime Oba typy obsługują DateTimeOffset operacje arytmetyczne. Możesz obliczyć różnicę między dwiema wartościami daty lub dodać lub odjąć określone interwały czasu do lub z wartości daty. Jednak operacje arytmetyczne w wartościach daty i godziny nie uwzględniają stref czasowych i reguł korekty strefy czasowej. W związku z tym arytmetyka daty i godziny dla wartości reprezentujących momenty w czasie może zwracać niedokładne wyniki.

Na przykład przejście z pacyficznego czasu standardowego na Pacyfik (czas letni) odbywa się w drugą niedzielę marca, czyli 10 marca 2013 r. Jak pokazano w poniższym przykładzie, jeśli obliczysz datę i godzinę 48 godzin po 9 marca 2013 r. o godzinie 10:30 w systemie w strefie czasowej Pacyfik (standardowy czas pacyficzny), wynik, 11 marca 2013 r. o godzinie 10:30, nie uwzględnia interweniowania korekty czasu.

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

Aby upewnić się, że operacja arytmetyczna w wartościach daty i godziny daje dokładne wyniki, wykonaj następujące kroki:

  1. Przekonwertuj czas w źródłowej strefie czasowej na utc.
  2. Wykonaj operację arytmetyczną.
  3. Jeśli wynik jest wartością daty i godziny, przekonwertuj ją z utc na godzinę w źródłowej strefie czasowej.

Poniższy przykład jest podobny do poprzedniego przykładu, z tą różnicą, że wykonuje te trzy kroki, aby poprawnie dodać 48 godzin do 9 marca 2013 r. o godzinie 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

Aby uzyskać więcej informacji, zobacz Wykonywanie operacji arytmetycznych z datami i godzinami.

Używanie nazw wrażliwych na kulturę dla elementów daty

Aplikacja może wymagać wyświetlenia nazwy miesiąca lub dnia tygodnia. W tym celu kod, taki jak poniżej, jest typowy.

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.

Jednak ten kod zawsze zwraca nazwy dni tygodnia w języku angielskim. Kod, który wyodrębnia nazwę miesiąca, jest często jeszcze bardziej nieelastyczny. Często zakłada się, że dwunastomiesięczny kalendarz z nazwami miesięcy w określonym języku.

Używając niestandardowych ciągów formatu daty i godziny lub właściwości DateTimeFormatInfo obiektu, można łatwo wyodrębnić ciągi odzwierciedlające nazwy dni tygodnia lub miesięcy w kulturze użytkownika, jak pokazano w poniższym przykładzie. Zmienia bieżącą kulturę na Francuski (Francja) i wyświetla nazwę dnia tygodnia oraz nazwę miesiąca na 1 lipca 2013 r.

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.

Wartości liczbowe

Obsługa liczb zależy od tego, czy są wyświetlane w interfejsie użytkownika, czy utrwalone. W tej sekcji przeanalizować oba zastosowania.

Uwaga

W operacjach analizowania i formatowania platforma .NET rozpoznaje tylko podstawowe znaki łacińskie od 0 do 9 (od U+0030 do U+0039) jako cyfry liczbowe.

Wyświetlanie wartości liczbowych

Zazwyczaj, gdy liczby są wyświetlane w interfejsie użytkownika, należy użyć konwencji formatowania kultury użytkownika, która jest definiowana przez CultureInfo.CurrentCulture właściwość i przez NumberFormatInfo obiekt zwrócony przez CultureInfo.CurrentCulture.NumberFormat właściwość. Konwencje formatowania bieżącej kultury są automatycznie używane podczas formatowania daty na następujące sposoby:

  • Przy użyciu metody bez ToString parametrów dowolnego typu liczbowego.
  • ToString(String) Przy użyciu metody dowolnego typu liczbowego, który zawiera ciąg formatu jako argument.
  • Używanie formatowania złożonego z wartościami liczbowymi.

W poniższym przykładzie przedstawiono średnią temperaturę miesięcznie w Paryżu we Francji. Najpierw ustawia bieżącą kulturę na francuską (Francja) przed wyświetleniem danych, a następnie ustawia ją na angielski (Stany Zjednoczone). W każdym przypadku nazwy i temperatury miesiąca są wyświetlane w formacie odpowiednim dla tej kultury. Należy pamiętać, że dwie kultury używają różnych separatorów dziesiętnych w wartości temperatury. Należy również pamiętać, że w przykładzie użyto niestandardowego ciągu formatu daty i godziny "MMMM", aby wyświetlić pełną nazwę miesiąca i że przydziela odpowiednią ilość miejsca dla nazwy miesiąca w ciągu wynikowym, określając długość najdłuższej nazwy miesiąca w tablicy 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

Utrwalanie wartości liczbowych

Nigdy nie należy utrwalać danych liczbowych w formacie specyficznym dla kultury. Jest to typowy błąd programowania, który powoduje uszkodzenie danych lub wyjątek czasu wykonywania. Poniższy przykład generuje dziesięć losowych liczb zmiennoprzecinkowych, a następnie serializuje je jako ciągi przy użyciu konwencji formatowania kultury angielskiej (Stany Zjednoczone). Po pobraniu i przeanalizowaniu danych przy użyciu konwencji kultury angielskiej (Stany Zjednoczone) zostanie ona pomyślnie przywrócona. Jednak po pobraniu i przeanalizowaniu przy użyciu konwencji kultury francuskiej (Francji) żadna z liczb nie może być analizowana, ponieważ kultury używają różnych separatorów dziesiętnych.

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'

Aby uniknąć tego problemu, możesz użyć jednej z następujących technik:

  • Zapisz i przeanalizuj ciąg reprezentujący liczbę przy użyciu niestandardowego ciągu formatu, który jest taki sam, niezależnie od kultury użytkownika.
  • Zapisz liczbę jako ciąg przy użyciu konwencji formatowania niezmiennej kultury, która jest zwracana przez CultureInfo.InvariantCulture właściwość .

Serializowanie wartości walutowych jest specjalnym przypadkiem. Ponieważ wartość waluty zależy od jednostki waluty, w której jest wyrażona, nie ma sensu traktować jej jako niezależnej wartości liczbowej. Jeśli jednak zapiszesz wartość waluty jako sformatowany ciąg, który zawiera symbol waluty, nie może być deserializowany w systemie, którego kultura domyślna używa innego symbolu waluty, jak pokazano w poniższym przykładzie.

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'

Zamiast tego należy serializować wartość liczbową wraz z niektórymi informacjami kulturowymi, takimi jak nazwa kultury, aby wartość i jej symbol waluty mogły być deserializowane niezależnie od bieżącej kultury. W poniższym przykładzie zdefiniowano CurrencyValue strukturę z dwoma elementami członkowskimi: Decimal wartość i nazwę kultury, do której należy wartość.

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

Praca z ustawieniami specyficznymi dla kultury

Na platformie .NET CultureInfo klasa reprezentuje określoną kulturę lub region. Niektóre jego właściwości zwracają obiekty, które zawierają określone informacje o pewnym aspekcie kultury:

Ogólnie rzecz biorąc, nie należy zakładać wartości określonych CultureInfo właściwości i powiązanych obiektów. Zamiast tego należy wyświetlać dane specyficzne dla kultury jako podlegające zmianie z następujących powodów:

  • Poszczególne wartości właściwości podlegają zmianom i poprawce w czasie, ponieważ dane są poprawiane, lepsze dane stają się dostępne lub zmieniają się konwencje specyficzne dla kultury.

  • Poszczególne wartości właściwości mogą się różnić w różnych wersjach platformy .NET lub systemu operacyjnego.

  • Platforma .NET obsługuje kultury zastępcze. Dzięki temu można zdefiniować nową kulturę niestandardową, która uzupełnia istniejące kultury standardowe lub całkowicie zastępuje istniejącą kulturę standardową.

  • W systemach Windows użytkownik może dostosować ustawienia specyficzne dla kultury przy użyciu aplikacji Region i Język w Panel sterowania. Podczas tworzenia wystąpienia CultureInfo obiektu można określić, czy odzwierciedla on te dostosowania użytkownika, wywołując CultureInfo(String, Boolean) konstruktor. Zazwyczaj w przypadku aplikacji użytkowników końcowych należy przestrzegać preferencji użytkownika, aby użytkownik był prezentowany dane w oczekiwanym formacie.

Zobacz też