전역화

전역화는 다양한 문화권의 사용자를 위해 현지화된 인터페이스와 국가별 데이터를 지원하는 지역화 대비 응용 프로그램을 디자인하고 개발하는 작업을 수반합니다. 디자인 단계를 시작하기 전에 앱에서 지원할 문화권을 결정해야 합니다. 앱이 기본적으로 단일 문화권이나 국가를 대상으로 하더라도, 다른 문화권이나 국가의 사용자에게 쉽게 확장될 수 있도록 디자인하고 작성할 수 있습니다.

모든 개발자는 각 문화권에 의해 형성된 사용자 인터페이스와 데이터에 대해 가정하는 부분이 있습니다. 예를 들어, 미국에서 영어를 사용하는 개발자가 날짜 및 시간 데이터를 MM/dd/yyyy hh:mm:ss 서식의 문자열로 serialize하는 것은 매우 합당해 보입니다. 하지만 그 문자열을 다른 문화권의 시스템에서 역직렬화하면 FormatException 예외를 throw하거나 정확하지 않은 데이터를 생성할 수 있습니다. 전역화는 문화권별 가정을 식별하고 그것이 앱의 디자인이나 코드에 영향을 미치지 않도록 합니다.

이 문서에서는 세계화된 앱에서 고려해야 할 몇 가지 주요 문제와 문자열, 날짜, 시간 값과 숫자 값을 처리할 때 따를 수 있는 모범 사례를 설명합니다.

문자열

각 문화권이나 국가는 다양한 문자 및 문자 집합을 사용하고 다양한 방식으로 문자를 정렬하기 때문에 문자와 문자열 처리는 전역화의 중심점입니다. 이 섹션에서는 전역화된 앱에서 문자열 사용에 대한 권장 사항을 제공합니다.

내부적으로 유니코드 사용

기본적으로 .NET에서는 유니코드 문자열을 사용합니다. 유니코드 문자열은 0 또는 하나 이상의 Char 개체로 구성되며, 각 개체는 UTF-16 코드 단위를 나타냅니다. 전세계에서 사용되는 모든 문자 집합의 거의 모든 문자에 대해서는 유니코드 표현이 있습니다.

Windows 운영 체제를 비롯한 많은 애플리케이션과 운영 체제는 코드 페이지를 사용하여 문자 집합을 나타낼 수도 있습니다. 일반적으로 코드 페이지는 0x00부터 0x7F까지 표준 ASCII 값을 포함하며 다른 문자를 0x80부터 0xFF까지 나머지 값에 매핑합니다. 0x80부터 0xFF까지 값에 대한 해석은 특정 코드 페이지에 따라 달라집니다. 이 때문에 전역화된 앱에서 가능하면 코드 페이지 사용을 피해야 합니다.

다음 예제는 시스템의 기본 코드 페이지가 데이터가 저장된 코드 페이지와 다른 경우 코드 페이지 데이터 해석의 위험을 보여줍니다. (이 시나리오를 시뮬레이션하기 위해, 예제에 다른 코드 페이지를 명시적으로 지정합니다.) 우선, 예제에 그리스어 알파벳의 대문자로 구성된 배열을 정의합니다. 이것을 코드 페이지 737(MS-DOS Greek이라고도 함)을 사용하여 바이트 배열로 인코딩하고 바이트 배열을 파일로 저장합니다. 파일을 가져와서 코드 페이지 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:
'       ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩ
'       €‚ƒ„…†‡ˆ‰Š‹ŒŽ‘’""•–—

유니코드를 사용하면 동일한 코드 단위가 항상 동일한 문자에 매핑되고, 동일한 문자가 항상 동일한 바이트 배열에 매핑되도록 할 수 있습니다.

리소스 파일 사용

단일 문화권이나 국가를 대상하는 하는 앱을 개발하더라도 사용자 인터페이스로 표시되는 문자열 및 기타 리소스를 저장하는 리소스 파일을 사용해야 합니다. 이러한 리소스는 절대로 코드에 직접 추가하지 말아야 합니다. 리소스 파일을 사용하면 많은 장점이 있습니다.

  • 모든 문자열이 단일 위치에 존재합니다. 특정 언어나 문화권에 대해 수정하기 위해 문자열을 식별하려고 소스 코드 전체를 검색할 필요가 없습니다.
  • 문자열을 복제할 필요가 없습니다. 리소스 파일을 자주 사용하지 않는 개발자는 동일한 문자열을 여러 소스 코드 파일에 정의하곤 합니다. 이렇게 중복하면 문자열을 수정할 때 하나 이상의 인스턴스를 간과할 가능성이 높아집니다.
  • 이미지 또는 이진 데이터와 같이 문자열이 아닌 리소스를 별도의 독립 실행형 파일 대신 리소스 파일에 포함시키면 쉽게 가져올 수 있습니다.

리소스 파일을 사용하면 특히 현지화된 앱을 만드는 경우에 장점이 있습니다. 위성 어셈블리로 리소스를 배포하는 경우, 공용 언어 런타임은 CultureInfo.CurrentUICulture 속성에 의해 정의된 사용자의 현재 UI 문화권을 기반으로 적절한 문화권의 리소스를 자동으로 선택합니다. 문화권별로 적절한 리소스를 제공하고 ResourceManager 개체를 제대로 인스턴스화하거나 강력한 형식 리소스 클래스를 사용하기만 한다면, 런타임에서 적절한 리소스를 가져오는 세부 정보를 처리합니다.

리소스 파일을 만드는 방법에 대한 자세한 내용은 리소스 파일 만들기를 참조하세요. 위성 어셈블리를 만들고 배포하는 방법에 대한 자세한 내용은 위성 어셈블리 만들기리소스 패키징 및 배포를 참조하세요.

문자열 검색 및 비교

가능하면 문자열을 일련의 개별 문자로 처리하는 대신 전체 문자열로 처리합니다. 이것은 부분 문자열을 정렬하거나 검색하는 경우, 결합된 문자의 구문 분석과 관련된 문제를 방지하는 데 특히 중요합니다.

문자열의 개별 문자 보다는 텍스트 요소에 StringInfo 클래스를 사용할 수 있습니다.

문자열 검색 및 비교 시 일반적인 실수는 문자열을 각각 Char 개체로 표시되는 문자의 컬렉션으로 다루는 것입니다. 실제, 단일 문자는 하나, 둘, 또는 그 이상의 Char 개체로 형성될 수 있습니다. 이러한 문자는 알파벳이 유니코드 기본 라틴 문자의 범위(U+0021 ~ U+007E)에 속하지 않는 문자로 구성된 문화권의 문자열에서 가장 빈번하게 나타납니다. 다음 예제는 문자열에서 LATIN CAPITAL LETTER A WITH GRAVE 문자(U+00C0)의 인덱스를 찾으려고 합니다. 하지만 이 문자는 단일 코드 단위(U+00C0) 또는 복합 문자(두 코드 단위: U+0041 및 U+0300)라는 두 방법으로 표현됩니다. 이런 경우, 문자는 2개의 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

String.IndexOf(String, StringComparison) 또는 String.LastIndexOf(String, StringComparison) 메서드와 같이 StringComparison 매개 변수를 포함하는 오버로드를 호출하면 이 예제(다른 결과를 반환하는 메서드의 두 가지 유사한 오버로드에 대한 호출)의 모호성을 피할 수 있습니다.

하지만 검색에 항상 문화권을 구분하지는 않습니다. 검색의 목적이 보안 결정을 내리거나 리소스에 대한 액세스를 허용하거나 허용하지 않는 것이라면, 비교는 다음 섹션의 설명처럼 서수여야 합니다.

문자열이 같은지 테스트

두 개 문자열의 정렬 순서를 비교하는 방법을 결정하는 게 아니라 문자열 두 개가 같은지를 테스트하려면 String.Compare 또는 CompareInfo.Compare와 같은 문자열 비교 메서드 대신 String.Equals 메서드를 사용합니다.

같음 비교는 조건부로 일부 리소스에 액세스하기 위해 수행됩니다. 예를 들어, 암호를 확인하거나 파일이 있는지 확인하기 위해 같음 비교를 수행합니다. 이러한 비언어적인 비교는 문화권을 구분하지 않고 항상 서수여야 합니다. 일반적으로 암호와 같은 문자열에 대해서는 StringComparison.Ordinal 값으로, 파일 이름 또는 URI와 같은 문자열에 대해서는 StringComparison.OrdinalIgnoreCase 값으로, 인스턴스 String.Equals(String, StringComparison) 메서드 또는 정적 String.Equals(String, String, StringComparison) 메서드를 호출해야 합니다.

같음 비교에 String.Equals 메서드에 대한 호출이 아닌 검색 또는 부분 문자열 비교를 수반하는 경우가 있습니다. 경우에 따라, 부분 문자열이 다른 문자열과 같은지를 판단하기 위해 부분 문자열 검색을 사용할 수 있습니다. 비교의 목적이 비언어적인 경우, 검색은 문화권 구분이 아닌 서수여야 합니다.

다음 예제는 비언어적인 데이터에 대한 문화권 구분 검색의 위험을 설명합니다. AccessesFileSystem 메서드는 "FILE"이라는 부분 문자열로 시작되는 URI에 대한 파일 시스템 액세스를 금지하도록 설계되었습니다. 이를 위해, 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.

문자열 순서 및 정렬

일반적으로 사용자 인터페이스로 표시되는 순서가 지정된 문자열은 문화권을 기반으로 정렬되어야 합니다. 대부분의 경우, 이러한 문자열 비교는 Array.Sort 또는 List<T>.Sort와 같이 문자열을 정렬하는 메서드를 호출할 때 .NET에 의해 암시적으로 처리됩니다. 기본적으로 문자열은 현재 문화권의 정렬 규칙을 사용하여 정렬됩니다. 다음 예제는 문자열 배열이 영어(미국) 문화권과 스웨덴어(스웨덴) 문화권의 규칙을 사용하여 정렬되는 경우 차이점을 설명합니다.

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 모든 운영 체제 Unicode 4.1
.NET Framework 3.0 모든 운영 체제 Unicode 4.1
.NET Framework 3.5 모든 운영 체제 Unicode 4.1
.NET Framework 4 모든 운영 체제 Unicode 5.0
.NET Framework 4.5 이상 Windows 7 Unicode 5.0
.NET Framework 4.5 이상 Windows 8 이상 운영 체제 유니코드 6.3.0
.NET Core 및 .NET 5 이상 기본 OS에서 지원되는 유니코드 표준의 버전에 따라 달라집니다.

.NET Framework 4.5부터 모든 버전의 .NET Core 및 .NET 5 이상에서 문자열 비교와 정렬은 운영 체제에 따라 달라집니다. Windows 7에서 실행되는 .NET Framework 4.5 이상은 유니코드 5.0을 구현하는 자체 테이블의 데이터를 검색합니다. Windows 8 이상에서 실행되는 .NET Framework 4.5 이상은 유니코드 6.3을 구현하는 운영 체제 테이블의 데이터를 검색합니다. .NET Core 및 .NET 5 이상에서 지원되는 유니코드의 버전은 기본 운영 체제에 따라 달라집니다. 문화권 구분 정렬 데이터를 직렬화하면 SortVersion 클래스를 사용하여 직렬화된 데이터가 .NET 및 운영 체제의 정렬 순서와 일치하도록 정렬되어야 하는 시기를 결정할 수 있습니다. 예제는 SortVersion 클래스 항목을 참조하세요.

앱에서 문자열 데이터의 광범위한 문화권별 정렬을 수행하는 경우, SortKey 클래스를 사용하여 문자열을 비교합니다. 정렬 키는 알파벳, 대/소문자, 특정 문자열의 분음 부호 가중치를 비롯한 문화권별 정렬 가중치를 반영합니다. 정렬 키를 사용한 비교는 이진이기 때문에 암시적 또는 명시적으로 CompareInfo 개체를 사용하는 비교보다 빠릅니다. 문자열을 CompareInfo.GetSortKey 메서드에 전달하여 특정 문자열에 대한 문화권별 정렬 키를 만듭니다.

다음 예제는 이전 예제와 유사합니다. 하지만 CompareInfo.Compare 메서드를 암시적으로 호출하는 Array.Sort(Array) 메서드를 호출하는 대신, 정렬 키를 비교하는 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 속성 및 CultureInfo.CurrentCulture.DateTimeFormat 속성에 의해 반환되는 DateTimeFormatInfo 개체에 의해 정의됩니다. 현재 문화권의 서식 규칙은 다음 메서드 중 하나를 사용하여 날짜의 서식을 지정할 때 자동으로 사용됩니다.

다음 예제는 2012년 10월 11일에 대한 일출 및 일몰 데이터를 두 번 표시합니다. 처음에는 현재 문화권을 크로아티아어(크로아티아)로 설정하고 다음에는 영어(영국)로 설정합니다. 각각의 경우, 날짜 및 시간이 해당 문화권에 적절한 서식으로 표시됩니다.

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

날짜 및 시간 유지

문화권에 따라 달라질 수 있는 서식으로 날짜 및 시간 데이터를 유지하지 말아야 합니다. 이것은 일반적인 프로그래밍 오류이며, 손상된 데이터 또는 런타임 예외를 발생시킵니다. 다음 예제는 2013년 1월 9일 및 2013년 8월 18일이라는 두 개의 날짜를 영어(미국) 문화권의 서식 규칙을 사용하여 문자열로 serialize합니다. 영어(미국) 문화권의 형식을 사용하여 데이터를 가져와서 구문을 분석하면, 성공적으로 복원됩니다. 하지만, 영어(영국) 문화권의 규칙을 사용하여 데이터를 가져와서 구문을 분석하면, 첫 번째 날짜는 9월 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

세 가지 방법 중 하나를 통해 이러한 문제를 피할 수 있습니다.

  • 날짜 및 시간을 문자열이 아닌 이진 형식으로 serialize합니다.
  • 사용자의 문화권과 상관없이 동일한 사용자 지정 서식 문자열을 사용하여 날짜와 시간의 문자열 표현을 저장하고 구문 분석합니다.
  • 고정 문화권의 서식 규칙을 사용하여 문자열을 저장합니다.

다음 예제에서 마지막 방법을 보여 줍니다. 정적 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

Serialization 및 표준 시간대 인식

날짜 및 시간 값은 일반 시간("매장은 2013년 1월 2일 오전 9:00에 개장합니다.")에서 특정한 순간("생년월일: 2013년 1월 2일 오전 6:32:00")에 이르기까지 다수의 해석이 있을 수 있습니다. 시간 값이 특정한 순간을 나타내는 경우 serialize된 값으로부터 복원하며, 사용자의 지리적 위치 또는 표준 시간대와 상관없이 동일한 순간을 나타내도록 해야 합니다.

다음 예제에서는 이 문제를 보여 줍니다. 단일 로컬 날짜 및 시간 값을 세 가지 표준 형식의 문자열로 저장합니다.

  • 일반 날짜 긴 시간을 나타내는 "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

데이터가 serialize된 시스템과 표준 시간대가 같은 시스템에서 데이터가 복원되면, 역직렬화된 날짜 및 시간 값이 원래 값을 정확하게 반영하고 다음과 같이 출력됩니다.

'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

자세한 내용은 표준 시간대 간에 시간 변환을 참조하세요.

날짜 및 시간 연산 수행

DateTimeDateTimeOffset 형식 모두 산술 연산을 지원합니다. 두 날짜 값 사이의 차이를 계산하거나 날짜 값에서 특정한 시간 간격을 빼거나 더할 수 있습니다. 하지만 날짜 및 시간 값에 대한 산술 연산은 표준 시간대 및 표준 시간대 조정 규칙을 감안하지 않습니다. 이 때문에, 순간을 나타내는 값에 대한 날짜 및 시간 연산은 부정확한 결과를 반환할 수 있습니다.

예를 들어, 태평양 표준시는 3월 둘째 주 일요일 즉, 2013년 3월 10일에 태평양 일광 절약 시간으로 전환됩니다. 다음 예제와 같이, 태평양 표준 시간대 시스템에서 오전 10시 30분에 2013년 3월 9일에서 48시간이 지난 날짜와 시간을 계산하면 그 결과는 2013년 3월 11일 오전 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에서 소스 표준 시간대의 시간으로 변환합니다.

다음 예제는 2013년 3월 9일 오전 10시 30분에 48시간을 제대로 더하기 위하여 이러한 세 가지 단계를 수행한 것을 제외하면 이전 예제와 유사합니다.

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 개체의 속성을 사용하면 사용자 문화권의 월 또는 요일 이름을 반영하는 문자열을 쉽게 추출할 수 있습니다. 현재 문화권을 프랑스어(프랑스)로 변경하고 2013년 7월 1일에 대한 월 이름과 요일 이름을 나타냅니다.

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 속성 및 CultureInfo.CurrentCulture.NumberFormat 속성에 의해 반환되는 NumberFormatInfo 개체에 의해 정의됩니다. 현재 문화권의 서식 규칙은 다음과 같은 방법으로 날짜의 서식을 지정할 때 자동으로 사용됩니다.

  • 모든 숫자 형식의 매개 변수가 없는 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

숫자 값 유지

숫자 데이터를 문화권별 서식으로 유지하지 말아야 합니다. 이것은 일반적인 프로그래밍 오류이며, 손상된 데이터 또는 런타임 예외를 발생시킵니다. 다음 예제는 10개의 부동 소수점 난수를 생성한 다음 영어(미국) 문화권의 서식 규칙을 사용하여 문자열로 serialize합니다. 영어(미국) 문화권의 형식을 사용하여 데이터를 가져와서 구문을 분석하면, 성공적으로 복원됩니다. 하지만, 이것을 가져와서 프랑스어(프랑스) 문화권 규칙을 사용하여 구문 분석을 수행하면, 이 문화권에서 다른 소수 구분 기호를 사용하기 때문에 숫자를 구문 분석할 수 없습니다.

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 속성에 의해 반환되는 고정 문화권의 서식 규칙을 사용하여 숫자를 문자열로 저장합니다.

통화 값 serialize는 특별한 경우입니다. 통화 값은 값이 표현되는 통화의 단위에 따라 달라지므로, 이것을 독립적인 숫자 값으로 처리하는 것은 의미가 없습니다. 하지만, 통화 값을 통화 기호를 포함하는 서식이 지정된 문자열로 저장하면, 다음 예제에서 볼 수 있듯이, 다른 통화 기호를 사용하는 기본 문화권의 시스템에서 역직렬화될 수 없습니다.

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'

대신, 값과 그에 대한 통화 기호가 현재 문화권과 관계 없이 역직렬화될 수 있도록, 숫자 값을 문화권 정보(예: 문화권의 이름)와 함께 serialize해야 합니다. 다음 예제에서는 두 가지 멤버 즉, Decimal 값과 그 값이 속하는 문화권의 이름으로 CurrencyValue 구조를 정의하여 이것을 구현합니다.

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 속성의 값 및 그와 관련된 개체에 대한 가정을 하지 말아야 합니다. 대신, 문화권별 데이터를 변경의 대상으로 봐야 합니다. 그 이유는 다음과 같습니다.

  • 개별적인 속성 값은 데이터가 수정되거나, 더 나은 데이터를 사용할 수 있게 되거나, 문화권별 규칙이 변경되면서, 시간이 지남에 따라 변경 및 개정될 수 있습니다.

  • 개별적인 속성 값은 .NET 버전 또는 운영 체제 버전에 따라 달라질 수 있습니다.

  • .NET은 대체 문화권을 지원합니다. 이 때문에 기존의 표준 문화권을 보완하거나 완전히 대체하는 새로운 사용자 지정 문화권을 정의하는 것이 가능합니다.

  • Windows 시스템에서 사용자는 제어판의 국가 및 언어 앱을 사용하여 문화권별 설정을 사용자 지정할 수 있습니다. CultureInfo 개체를 인스턴스화할 때, CultureInfo(String, Boolean) 생성자를 호출하여 사용자 지정을 반영할지 여부를 결정할 수 있습니다. 일반적으로 최종 사용자 앱에 대해서는 사용자가 예상하는 서식으로 사용자에게 데이터가 표시되도록 사용자 기본 설정을 고려해야 합니다.

참조