.NET에서의 문자열 비교 모범 사례

.NET에서는 지역화된 애플리케이션과 전역화된 애플리케이션을 개발하기 위한 광범위한 지원을 제공하고 문자열 정렬 및 표시와 같은 일반적인 작업을 수행할 때 현재 문화권이나 특정 문화권의 규칙을 쉽게 적용할 수 있습니다. 그러나 문자열 정렬이나 비교가 항상 문화권을 구분하는 작업은 아닙니다. 예를 들어 애플리케이션에서 내부적으로 사용되는 문자열은 대기 모든 문화권에서 동일하게 처리해야 합니다. XML 태그, HTML 태그, 사용자 이름, 파일 경로 및 시스템 개체 이름과 같이 문화권을 구분하지 않는 문자열 데이터가 문화권이 구분되는 것처럼 해석되면 애플리케이션 코드에는 감지하기 어려운 버그, 성능 저하 및 경우에 따라 보안 문제가 발생할 수 있습니다.

이 문서에서는 .NET의 문자열 정렬, 비교 및 대/소문자 구분 방법을 살펴보고, 적절한 문자열 처리 방법 선택을 위한 권장 사항을 제공하고, 문자열 처리 방법에 대한 추가 정보를 제공합니다.

문자열 사용에 대한 권장 사항

.NET으로 개발하는 경우 문자열을 비교할 때 다음 권장 사항을 따르세요.

다양한 문자열 관련 메서드로 비교를 수행합니다. 예를 들면 String.Equals, String.Compare, String.IndexOf, String.StartsWith입니다.

문자열을 비교할 때 다음 사례는 사용하지 마세요.

  • 문자열 작업에 대한 문자열 비교 규칙을 명시적 또는 암시적으로 지정하지 않는 오버로드를 사용하지 마세요.
  • 대부분 경우에 StringComparison.InvariantCulture를 기준으로 한 문자열 작업을 사용하지 마세요. 몇 가지 예외의 하나는 언어적으로 의미가 있지만 문화적으로 독립적인 데이터를 유지하는 경우입니다.
  • String.Compare 또는 CompareTo 메서드의 오버로드 및 두 문자열이 같은지를 확인하기 위한 반환 값 0에 대한 테스트를 사용하지 마세요.

명시적으로 문자열 비교 지정

.NET의 문자열 조작 메서드는 대부분 오버로드됩니다. 일반적으로 오버로드 하나 이상은 기본 설정을 그대로 사용하지만, 다른 오버로드는 기본값을 사용하지 않고 문자열을 비교 및 조작하는 정확한 방법을 정의합니다. 기본값을 사용하지 않는 대부분 메서드에는 문화권 및 사례별로 문자열 비교에 대한 규칙을 명시적으로 지정하는 열거형인 StringComparison형식 매개 변수가 포함됩니다. 다음 표에서는 StringComparison 열거형 멤버에 대해 설명합니다.

StringComparison 멤버 설명
CurrentCulture 현재 문화권을 사용하여 대/소문자를 구분하는 비교를 수행합니다.
CurrentCultureIgnoreCase 현재 문화권을 사용하여 대/소문자를 구분하지 않는 비교를 수행합니다.
InvariantCulture 고정 문화권을 사용하여 대/소문자를 구분하는 비교를 수행합니다.
InvariantCultureIgnoreCase 고정 문화권을 사용하여 대/소문자를 구분하지 않는 비교를 수행합니다.
Ordinal 서수 비교를 수행합니다.
OrdinalIgnoreCase 대소문자를 구분하지 않는 서수 비교를 수행합니다.

예를 들어 문자 또는 문자열과 일치하는 IndexOf 개체의 하위 문자열 인덱스를 반환하는 String 메서드에는 다음과 같은 오버로드 9개가 포함됩니다.

다음 이유로 기본값을 사용하지 않는 오버로드를 선택하는 것이 좋습니다.

  • 기본 매개 변수가 있는 일부 오버로드(문자열 인스턴스에서 Char 을 검색하는 오버로드)는 서수 비교를 수행하지만, 다른 오버로드(문자열 인스턴스에서 문자열을 검색하는 오버로드)는 문화권이 구분됩니다. 어떤 메서드가 어떤 기본값을 사용하는지 기억하기 어렵고 오버로드를 혼동하기 쉽습니다.

  • 메서드 호출에 기본값을 사용하는 코드의 의도는 분명하지 않습니다. 기본값을 사용하는 다음 예제에서는 개발자가 실제로 두 문자열의 서수 또는 언어 비교를 의도했는지 아니면 url.Scheme과 "https" 간의 대/소문자 차이로 인해 같음 여부 테스트에서 false를 반환할 수 있는지 알기가 어렵습니다.

    Uri url = new("https://learn.microsoft.com/");
    
    // Incorrect
    if (string.Equals(url.Scheme, "https"))
    {
        // ...Code to handle HTTPS protocol.
    }
    
    Dim url As New Uri("https://learn.microsoft.com/")
    
    ' Incorrect
    If String.Equals(url.Scheme, "https") Then
        ' ...Code to handle HTTPS protocol.
    End If
    

일반적으로 기본값을 사용하지 않는 메서드를 사용하면 코드의 의도가 분명해지므로 이 메서드를 호출하는 것이 좋습니다. 이 메서드를 사용하면 코드를 더 쉽게 읽을 수 있고 더 쉽게 디버그 및 유지 관리할 수도 있습니다. 다음 예제에서는 이전 예제와 관련된 질문을 해결합니다. 예제에서는 서수 비교가 사용되고 대/소문자 차이가 무시됩니다.

Uri url = new("https://learn.microsoft.com/");

// Correct
if (string.Equals(url.Scheme, "https", StringComparison.OrdinalIgnoreCase))
{
    // ...Code to handle HTTPS protocol.
}
Dim url As New Uri("https://learn.microsoft.com/")

' Incorrect
If String.Equals(url.Scheme, "https", StringComparison.OrdinalIgnoreCase) Then
    ' ...Code to handle HTTPS protocol.
End If

문자열 비교 세부 정보

문자열 비교는 특히 정렬 및 같음 테스트와 같은 다양한 문자열 관련 작업의 핵심입니다. 결정된 순서의 문자열 정렬: 정렬된 문자열 목록에서 "my"가 "string" 앞에 나타나면 "my"가 "string"과 같거나 작은지 비교해야 합니다. 또한 비교는 암시적으로 같음을 정의합니다. 비교 작업은 같은 것으로 판단되는 문자열에 대해 0을 반환합니다. 다른 문자열보다 작은 문자열이 없다고 해석하는 것이 적절합니다. 문자열과 관련된 대부분 의미 있는 작업에는 다른 문자열과 비교 및 잘 정의된 정렬 작업 실행이라는 두 가지 절차가 둘 다 또는 두 가지 중 하나가 포함됩니다.

참고 항목

Windows 운영 체제에 대한 정렬 및 비교 작업에 사용되는 문자 가중치에 대한 정보를 포함하는 텍스트 파일 집합인 정렬 가중치 테이블 및 Linux 및 macOS용 정렬 가중치 테이블의 최신 버전인 기본 유니코드 데이터 정렬 요소 테이블을 다운로드할 수 있습니다. Linux 및 macOS에서 정렬 가중치 테이블의 특정 버전은 시스템에 설치된 International Components for Unicode 라이브러리 버전에 따라 달라집니다. ICU 버전 및 ICU 버전이 구현하는 유니코드 버전에 대한 자세한 내용은 ICU 다운로드를 참조하세요.

그러나 두 문자열의 같음 또는 정렬 순서를 평가할 때 하나의 올바른 결과가 생성되지는 않습니다. 결과는 문자열 비교에 사용되는 기준에 따라 다릅니다. 특히 서수 문자열 비교나 현재 문화권이나 고정 문화권(영어를 기준으로 로캘을 구분하지 않는 문화권)의 대/소문자 구분 및 정렬 규칙을 기준으로 한 문자열 비교에서는 다른 결과가 생성될 수 있습니다.

또한 다른 버전의 .NET을 사용하거나 다른 운영 체제 또는 운영 체제 버전의 .NET을 사용하여 문자열을 비교하면 다른 결과가 반환될 수 있습니다. 자세한 내용은 문자열과 유니코드 표준을 참조하세요.

현재 문화권을 사용하는 문자열 비교

문자열 비교 시 현재 문화권의 규칙을 사용하는 한 가기 기준이 포함됩니다. 현재 문화권을 기준으로 한 비교에는 스레드의 현재 문화권 또는 로캘이 사용됩니다. 사용자가 문화권을 설정하지 않은 경우 기본적으로 운영 체제 설정으로 설정됩니다. 데이터가 언어적으로 관련되는 경우와 문화권이 구분되는 사용자 조작을 반영하는 경우에는 항상 현재 문화권을 기준으로 한 비교를 사용해야 합니다.

그러나 문화권이 변경되면 .NET의 비교 및 대/소문자 지정 동작도 바뀝니다. 애플리케이션이 개발된 컴퓨터와 다른 문화권을 포함하는 컴퓨터에서 애플리케이션을 실행하거나 실행 스레드가 문화권을 변경할 경우 이 동작이 수행됩니다. 이 동작은 의도적이지만 대부분 개발자가 이해하기가 분명하지는 않습니다. 다음 예제에서는 미국 영어("en-US") 및 스웨덴어("sv-SE") 문화권의 정렬 순서 차이를 보여 줍니다. 정렬된 문자열 배열에서 단어 "ångström", "Windows" 및 "Visual Studio"가 다른 위치에 나타남을 알 수 있습니다.

using System.Globalization;

// Words to sort
string[] values= { "able", "ångström", "apple", "Æble",
                    "Windows", "Visual Studio" };

// Current culture
Array.Sort(values);
DisplayArray(values);

// Change culture to Swedish (Sweden)
string originalCulture = CultureInfo.CurrentCulture.Name;
Thread.CurrentThread.CurrentCulture = new CultureInfo("sv-SE");
Array.Sort(values);
DisplayArray(values);

// Restore the original culture
Thread.CurrentThread.CurrentCulture = new CultureInfo(originalCulture);

static void DisplayArray(string[] values)
{
    Console.WriteLine($"Sorting using the {CultureInfo.CurrentCulture.Name} culture:");
    
    foreach (string value in values)
        Console.WriteLine($"   {value}");

    Console.WriteLine();
}

// The example displays the following output:
//     Sorting using the en-US culture:
//        able
//        Æble
//        ångström
//        apple
//        Visual Studio
//        Windows
//
//     Sorting using the sv-SE culture:
//        able
//        apple
//        Visual Studio
//        Windows
//        ångström
//        Æble
Imports System.Globalization
Imports System.Threading

Module Program
    Sub Main()
        ' Words to sort
        Dim values As String() = {"able", "ångström", "apple", "Æble",
                                  "Windows", "Visual Studio"}

        ' Current culture
        Array.Sort(values)
        DisplayArray(values)

        ' Change culture to Swedish (Sweden)
        Dim originalCulture As String = CultureInfo.CurrentCulture.Name
        Thread.CurrentThread.CurrentCulture = New CultureInfo("sv-SE")
        Array.Sort(values)
        DisplayArray(values)

        ' Restore the original culture
        Thread.CurrentThread.CurrentCulture = New CultureInfo(originalCulture)
    End Sub

    Sub DisplayArray(values As String())
        Console.WriteLine($"Sorting using the {CultureInfo.CurrentCulture.Name} culture:")

        For Each value As String In values
            Console.WriteLine($"   {value}")
        Next

        Console.WriteLine()
    End Sub
End Module

' The example displays the following output:
'     Sorting using the en-US culture:
'        able
'        Æble
'        ångström
'        apple
'        Visual Studio
'        Windows
'
'     Sorting using the sv-SE culture:
'        able
'        apple
'        Visual Studio
'        Windows
'        ångström
'        Æble

현재 문화권을 사용하는 대/소문자를 구분하지 않는 비교는 스레드의 현재 문화권에 지정된 대/소문자를 무시한다는 점을 제외하고 문화권 구분 비교와 같습니다. 이 동작은 정렬 순서에서도 나타날 수 있습니다.

현재 문화권 의미 체계를 사용하는 비교는 다음 메서드의 기본값입니다.

모든 경우에 메서드 호출의 의도를 분명하게 나타내도록 StringComparison 매개 변수를 포함하는 오버로드를 호출하는 것이 좋습니다.

비언어적인 문자열 데이터가 언어적으로 해석되거나 특정 문화권의 문자열 데이터가 다른 문화권의 규칙을 통해 해석되면 감지하기 어려운 버그와 감지할 수 있는 버그가 발생할 수 있습니다. 대표적인 예는 Turkish-I 문제입니다.

미국 영어를 포함한 거의 모든 라틴어 알파벳에서 문자 "i"(\u0069)는 문자 "I"(\u0049)의 소문자 버전입니다. 이 대/소문자 구분 규칙은 해당 문화권에서 프로그래밍하는 사용자에게 빠르게 기본 규칙이 되었습니다. 그러나 터키어("tr-TR") 알파벳에는 "i"의 대문자 버전인 "점이 있는 I" 문자 "İ"(\u0130)가 포함됩니다. 터키어에는 대문자 버전이 "I"인 "점이 없는 i" 문자 , "ı"(\u0131)도 포함됩니다. 이 동작은 아제르바이잔어("az") 문화권에서도 발생합니다.

따라서 "i"를 대문자로 변환하거나 "I"를 소문자로 변환하는 방법에 대한 가정이 모든 문화권에서 유효한 것은 아닙니다. 문자열 비교 루틴에 대해 기본 오버로드를 사용하면 이 오버로드에는 문화권 간의 차이가 적용됩니다. 문자열 "bill" 및 "BILL"의 대/소문자를 구분하지 않는 비교를 수행하는 다음 시도와 같이, 비교할 데이터가 비언어적일 경우 기본 오버로드를 사용하면 원하지 않는 결과가 생성될 수 있습니다.

using System.Globalization;

string name = "Bill";

Thread.CurrentThread.CurrentCulture = new CultureInfo("en-US");
Console.WriteLine($"Culture = {Thread.CurrentThread.CurrentCulture.DisplayName}");
Console.WriteLine($"   Is 'Bill' the same as 'BILL'? {name.Equals("BILL", StringComparison.OrdinalIgnoreCase)}");
Console.WriteLine($"   Does 'Bill' start with 'BILL'? {name.StartsWith("BILL", true, null)}");
Console.WriteLine();

Thread.CurrentThread.CurrentCulture = new CultureInfo("tr-TR");
Console.WriteLine($"Culture = {Thread.CurrentThread.CurrentCulture.DisplayName}");
Console.WriteLine($"   Is 'Bill' the same as 'BILL'? {name.Equals("BILL", StringComparison.OrdinalIgnoreCase)}");
Console.WriteLine($"   Does 'Bill' start with 'BILL'? {name.StartsWith("BILL", true, null)}");

//' The example displays the following output:
//'
//'     Culture = English (United States)
//'        Is 'Bill' the same as 'BILL'? True
//'        Does 'Bill' start with 'BILL'? True
//'     
//'     Culture = Turkish (Türkiye)
//'        Is 'Bill' the same as 'BILL'? True
//'        Does 'Bill' start with 'BILL'? False
Imports System.Globalization
Imports System.Threading

Module Program
    Sub Main()
        Dim name As String = "Bill"

        Thread.CurrentThread.CurrentCulture = New CultureInfo("en-US")
        Console.WriteLine($"Culture = {Thread.CurrentThread.CurrentCulture.DisplayName}")
        Console.WriteLine($"   Is 'Bill' the same as 'BILL'? {name.Equals("BILL", StringComparison.OrdinalIgnoreCase)}")
        Console.WriteLine($"   Does 'Bill' start with 'BILL'? {name.StartsWith("BILL", True, Nothing)}")
        Console.WriteLine()

        Thread.CurrentThread.CurrentCulture = New CultureInfo("tr-TR")
        Console.WriteLine($"Culture = {Thread.CurrentThread.CurrentCulture.DisplayName}")
        Console.WriteLine($"   Is 'Bill' the same as 'BILL'? {name.Equals("BILL", StringComparison.OrdinalIgnoreCase)}")
        Console.WriteLine($"   Does 'Bill' start with 'BILL'? {name.StartsWith("BILL", True, Nothing)}")
    End Sub

End Module

' The example displays the following output:
'
'     Culture = English (United States)
'        Is 'Bill' the same as 'BILL'? True
'        Does 'Bill' start with 'BILL'? True
'     
'     Culture = Turkish (Türkiye)
'        Is 'Bill' the same as 'BILL'? True
'        Does 'Bill' start with 'BILL'? False

다음 예제와 같이 보안 관련 설정에서 문화권이 의도치 않게 사용되면 이 비교로 인해 큰 문제가 발생할 수 있습니다. IsFileURI("file:")와 같은 메서드 호출은 현재 문화권이 미국 영어이면 true를 반환하지만 현재 문화권이 터키어이면 false를 반환합니다. 따라서 터키어 시스템에서는 누군가 "FILE:"으로 시작하는 대/소문자를 구분하지 않는 URI에 대한 액세스를 차단하는 보안 조치를 회피할 수 있습니다.

public static bool IsFileURI(string path) =>
    path.StartsWith("FILE:", true, null);
Public Shared Function IsFileURI(path As String) As Boolean
    Return path.StartsWith("FILE:", True, Nothing)
End Function

이 경우 "file:"은 비언어적, 문화권을 구분하지 않는 식별자로 해석되므로 코드를 다음 예제와 같이 작성해야 합니다.

public static bool IsFileURI(string path) =>
    path.StartsWith("FILE:", StringComparison.OrdinalIgnoreCase);
Public Shared Function IsFileURI(path As String) As Boolean
    Return path.StartsWith("FILE:", StringComparison.OrdinalIgnoreCase)
End Function

서수 문자열 작업

메서드 호출에서 StringComparison.Ordinal 또는 StringComparison.OrdinalIgnoreCase 값을 지정하는 것은 자연어의 특징이 무시되는 비언어적인 비교를 나타냅니다. 이들 StringComparison 값을 사용하여 호출되는 메서드의 문자열 작업 결정은 문화권별로 매개 변수화되는 대/소문자 구분 또는 동일성 테이블이 아닌 단순 바이트 비교에 기반을 둡니다. 대부분 경우에 이 접근 방법은 의도된 문자열 해석에 가장 적합하지만 코드를 더 빠르고 더 안정적으로 만듭니다.

서수 비교는 언어적 해석 없이 각 문자열의 각 바이트를 비교하는 문자열 비교입니다. 예를 들어 "windows"는 "Windows"와 일치하지 않습니다. 이 비교는 기본적으로 C 런타임 strcmp 함수에 대한 호출입니다. 컨텍스트가 문자열이 정확히 일치해야 함을 나타내거나 보수적인 일치 정책을 요구할 경우 이 비교를 사용합니다. 또한 서수 비교는 결과를 결정할 때 언어 규칙을 적용하지 않으므로 가장 빠른 비교 작업입니다.

.NET의 문자열에는 포함된 null 문자(및 기타 인쇄되지 않는 문자)가 포함될 수 있습니다. 서수 비교와 문화권 구분 비교(고정 문화권을 사용하는 비교 포함) 간 가장 분명한 차이의 하나는 문자열의 포함된 null 문자 처리와 관련됩니다. String.CompareString.Equals 메서드를 사용하여 문화권 구분 비교(고정 문화권을 사용하는 비교 포함)를 수행하면 이들 문자가 무시됩니다. 결과적으로 포함된 null 문자를 포함하는 문자열은 그렇지 않은 문자열과 같은 것으로 간주될 수 있습니다. 포함된 인쇄되지 않은 문자는 String.StartsWith와 같은 문자열 비교 메서드를 위해 건너뛸 수 있습니다.

Important

문자열 비교 메서드는 포함된 null 문자를 무시하지만 String.Contains, String.EndsWith, String.IndexOf, String.LastIndexOfString.StartsWith 와 같은 문자열 검색 메서드는 무시하지 않습니다.

다음 예제에서는 문자열 "Aa" 및 "A"와 "a" 사이에 여러 포함된 null 문자를 포함하는 비슷한 문자열의 문화권 구분 비교를 수행하고 두 문자열을 어떻게 같은 것으로 간주하는지 보여 줍니다.

string str1 = "Aa";
string str2 = "A" + new string('\u0000', 3) + "a";

Thread.CurrentThread.CurrentCulture = System.Globalization.CultureInfo.GetCultureInfo("en-us");

Console.WriteLine($"Comparing '{str1}' ({ShowBytes(str1)}) and '{str2}' ({ShowBytes(str2)}):");
Console.WriteLine("   With String.Compare:");
Console.WriteLine($"      Current Culture: {string.Compare(str1, str2, StringComparison.CurrentCulture)}");
Console.WriteLine($"      Invariant Culture: {string.Compare(str1, str2, StringComparison.InvariantCulture)}");
Console.WriteLine("   With String.Equals:");
Console.WriteLine($"      Current Culture: {string.Equals(str1, str2, StringComparison.CurrentCulture)}");
Console.WriteLine($"      Invariant Culture: {string.Equals(str1, str2, StringComparison.InvariantCulture)}");

string ShowBytes(string value)
{
   string hexString = string.Empty;
   for (int index = 0; index < value.Length; index++)
   {
      string result = Convert.ToInt32(value[index]).ToString("X4");
      result = string.Concat(" ", result.Substring(0,2), " ", result.Substring(2, 2));
      hexString += result;
   }
   return hexString.Trim();
}

// The example displays the following output:
//     Comparing 'Aa' (00 41 00 61) and 'Aa' (00 41 00 00 00 00 00 00 00 61):
//        With String.Compare:
//           Current Culture: 0
//           Invariant Culture: 0
//        With String.Equals:
//           Current Culture: True
//           Invariant Culture: True

Module Program
    Sub Main()
        Dim str1 As String = "Aa"
        Dim str2 As String = "A" & New String(Convert.ToChar(0), 3) & "a"

        Console.WriteLine($"Comparing '{str1}' ({ShowBytes(str1)}) and '{str2}' ({ShowBytes(str2)}):")
        Console.WriteLine("   With String.Compare:")
        Console.WriteLine($"      Current Culture: {String.Compare(str1, str2, StringComparison.CurrentCulture)}")
        Console.WriteLine($"      Invariant Culture: {String.Compare(str1, str2, StringComparison.InvariantCulture)}")
        Console.WriteLine("   With String.Equals:")
        Console.WriteLine($"      Current Culture: {String.Equals(str1, str2, StringComparison.CurrentCulture)}")
        Console.WriteLine($"      Invariant Culture: {String.Equals(str1, str2, StringComparison.InvariantCulture)}")
    End Sub

    Function ShowBytes(str As String) As String
        Dim hexString As String = String.Empty

        For ctr As Integer = 0 To str.Length - 1
            Dim result As String = Convert.ToInt32(str.Chars(ctr)).ToString("X4")
            result = String.Concat(" ", result.Substring(0, 2), " ", result.Substring(2, 2))
            hexString &= result
        Next

        Return hexString.Trim()
    End Function

    ' The example displays the following output:
    '     Comparing 'Aa' (00 41 00 61) and 'Aa' (00 41 00 00 00 00 00 00 00 61):
    '        With String.Compare:
    '           Current Culture: 0
    '           Invariant Culture: 0
    '        With String.Equals:
    '           Current Culture: True
    '           Invariant Culture: True
End Module

그러나 다음 예제에서 나타나듯이 서수 비교를 사용할 경우 문자열은 같은 것으로 간주하지 않습니다.

string str1 = "Aa";
string str2 = "A" + new String('\u0000', 3) + "a";

Console.WriteLine($"Comparing '{str1}' ({ShowBytes(str1)}) and '{str2}' ({ShowBytes(str2)}):");
Console.WriteLine("   With String.Compare:");
Console.WriteLine($"      Ordinal: {string.Compare(str1, str2, StringComparison.Ordinal)}");
Console.WriteLine("   With String.Equals:");
Console.WriteLine($"      Ordinal: {string.Equals(str1, str2, StringComparison.Ordinal)}");

string ShowBytes(string str)
{
    string hexString = string.Empty;
    for (int ctr = 0; ctr < str.Length; ctr++)
    {
        string result = Convert.ToInt32(str[ctr]).ToString("X4");
        result = " " + result.Substring(0, 2) + " " + result.Substring(2, 2);
        hexString += result;
    }
    return hexString.Trim();
}

// The example displays the following output:
//    Comparing 'Aa' (00 41 00 61) and 'A   a' (00 41 00 00 00 00 00 00 00 61):
//       With String.Compare:
//          Ordinal: 97
//       With String.Equals:
//          Ordinal: False
Module Program
    Sub Main()
        Dim str1 As String = "Aa"
        Dim str2 As String = "A" & New String(Convert.ToChar(0), 3) & "a"

        Console.WriteLine($"Comparing '{str1}' ({ShowBytes(str1)}) and '{str2}' ({ShowBytes(str2)}):")
        Console.WriteLine("   With String.Compare:")
        Console.WriteLine($"      Ordinal: {String.Compare(str1, str2, StringComparison.Ordinal)}")
        Console.WriteLine("   With String.Equals:")
        Console.WriteLine($"      Ordinal: {String.Equals(str1, str2, StringComparison.Ordinal)}")
    End Sub

    Function ShowBytes(str As String) As String
        Dim hexString As String = String.Empty

        For ctr As Integer = 0 To str.Length - 1
            Dim result As String = Convert.ToInt32(str.Chars(ctr)).ToString("X4")
            result = String.Concat(" ", result.Substring(0, 2), " ", result.Substring(2, 2))
            hexString &= result
        Next

        Return hexString.Trim()
    End Function

    ' The example displays the following output:
    '    Comparing 'Aa' (00 41 00 61) and 'A   a' (00 41 00 00 00 00 00 00 00 61):
    '       With String.Compare:
    '          Ordinal: 97
    '       With String.Equals:
    '          Ordinal: False
End Module

대/소문자를 구분하지 않는 서수 비교는 다음으로 가장 보수적인 접근 방법입니다. 이들 비교는 대부분 대/소문자 구분을 무시합니다. 예를 들어 "windows"는 "Windows"와 일치합니다. ASCII 문자를 처리할 때 이 정책은 사용 가능한 ASCII 대/소문자 구분을 무시한다는 점을 제외하고 StringComparison.Ordinal과 같습니다. 따라서 [A, Z]\(\u0041-\u005A)의 모든 문자는 [a,z]\(\u0061-\007A)의 해당 문자와 일치합니다. ASCII 범위를 벗어난 대/소문자 구분에는 고정 문화권 테이블이 사용됩니다. 따라서 다음 비교는

string.Compare(strA, strB, StringComparison.OrdinalIgnoreCase);
String.Compare(strA, strB, StringComparison.OrdinalIgnoreCase)

다음 비교와 같지만 더 빠릅니다.

string.Compare(strA.ToUpperInvariant(), strB.ToUpperInvariant(), StringComparison.Ordinal);
String.Compare(strA.ToUpperInvariant(), strB.ToUpperInvariant(), StringComparison.Ordinal)

이들 비교는 매우 빠릅니다.

StringComparison.OrdinalStringComparison.OrdinalIgnoreCase 는 둘 다 직접 이진 값을 사용하고 일치 항목 찾기에 가장 적합합니다. 비교 설정을 확신할 수 없으면 다음 두 값 중 하나를 사용하세요. 그러나 이들 값은 바이트 단위 비교를 수행하므로 영어 사전처럼 언어 정렬 순서별로 정렬하는 것이 아니라 이진 정렬 순서별로 정렬합니다. 사용자에게 표시되는 결과가 대부분 컨텍스트에서 이상하게 표시될 수 있습니다.

서수 의미 체계는 String.Equals 인수(같음 연산자 포함)를 포함하지 않는 StringComparison 오버로드의 기본값입니다. 모든 경우에 StringComparison 매개 변수를 포함하는 오버로드를 호출하는 것이 좋습니다.

고정 문화권을 사용하는 문자열 작업

고정 문화권을 사용한 비교에는 정적 CompareInfo 속성에서 반환되는 CultureInfo.InvariantCulture 속성이 사용됩니다. 이 동작은 모든 시스템에서 동일하며, 범위 밖의 모든 문자를 동일한 고정 문화권으로 간주하는 문자로 변환합니다. 이 정책은 문화권에 걸쳐 단일 문자열 동작 집합을 유지 관리할 경우 유용하지만 예기치 않은 경과를 제공하기도 합니다.

고정 문화권을 사용한 대/소문자를 비교하지 않는 비교에는 비교 정보를 위해 정적 CompareInfo 속성에서 반환되는 CultureInfo.InvariantCulture 속성이 사용됩니다. 모든 경우에 변환된 문자 간의 차이는 무시됩니다.

StringComparison.InvariantCultureStringComparison.Ordinal 을 사용하는 비교는 ASCII 문자열과 똑같이 작동합니다. 그러나 StringComparison.InvariantCulture 는 바이트 집합으로 해석되어야 하는 문자열에 적절하지 않을 수 있는 언어적 결정을 내립니다. CultureInfo.InvariantCulture.CompareInfo 개체를 사용하면 Compare 메서드가 특정 문자 집합을 같은 것으로 해석합니다. 예를 들어 다음 동일성은 고정 문화권에서 유효합니다.

InvariantCulture: a + ̊ = å

LATIN SMALL LETTER A 문자 “a”(\u0061)는 COMBINING RING ABOVE 문자 "+ " ̊"(\u030a) 옆에 있을 경우 LATIN SMALL LETTER A WITH RING ABOVE 문자 “å”(\u00e5)로 해석됩니다. 다음 예제와 같이 이 동작은 서수 비교와 다릅니다.

string separated = "\u0061\u030a";
string combined = "\u00e5";

Console.WriteLine("Equal sort weight of {0} and {1} using InvariantCulture: {2}",
                  separated, combined,
                  string.Compare(separated, combined, StringComparison.InvariantCulture) == 0);

Console.WriteLine("Equal sort weight of {0} and {1} using Ordinal: {2}",
                  separated, combined,
                  string.Compare(separated, combined, StringComparison.Ordinal) == 0);

// The example displays the following output:
//     Equal sort weight of a° and å using InvariantCulture: True
//     Equal sort weight of a° and å using Ordinal: False
Module Program
    Sub Main()
        Dim separated As String = ChrW(&H61) & ChrW(&H30A)
        Dim combined As String = ChrW(&HE5)

        Console.WriteLine("Equal sort weight of {0} and {1} using InvariantCulture: {2}",
                          separated, combined,
                          String.Compare(separated, combined, StringComparison.InvariantCulture) = 0)

        Console.WriteLine("Equal sort weight of {0} and {1} using Ordinal: {2}",
                          separated, combined,
                          String.Compare(separated, combined, StringComparison.Ordinal) = 0)

        ' The example displays the following output:
        '     Equal sort weight of a° and å using InvariantCulture: True
        '     Equal sort weight of a° and å using Ordinal: False
    End Sub
End Module

파일 이름, 쿠키 또는 "å"와 같은 조합이 나타날 수 있는 다른 항목을 해석할 때 서수 비교는 가장 투명하고 적합한 동작을 제공합니다.

모든 것을 고려해 보면 고정 문화권에는 비교에 유용할 수 있는 속성이 거의 없습니다. 고정 문화권은 완전한 기호 동일성을 보장하지 못하게 하는 언어 관련 방식으로 비교를 수행하지만 모든 문화권에서 표시하기에 적합하지 않습니다. 비교에 StringComparison.InvariantCulture 를 사용하는 몇 가지 이유의 하나는 서로 다른 문화에서 동일하게 표시되도록 순서가 지정된 데이터를 유지하는 것입니다. 예를 들어 표시하기 위한 정렬된 식별자 목록을 포함하는 큰 데이터 파일이 애플리케이션과 함께 제공될 경우 이 목록에 추가하려면 고정 스타일 정렬을 사용한 삽입이 필요합니다.

메서드 호출을 위한 StringComparison 멤버 선택

다음 표에서는 의미 체계 문자열 컨텍스트에서 StringComparison 열거형 멤버로의 매핑을 간략하게 설명합니다.

데이터 동작 해당 System.StringComparison

value
대/소문자 구분 내부 식별자.

XML 및 HTTP와 같은 표준의 대/소문자 구분 식별자.

대/소문자 구분 보안 관련 설정.
바이트가 정확히 일치하는 비언어적 식별자. Ordinal
대/소문자를 구분하지 않는 내부 식별자.

XML 및 HTTP와 같은 표준의 대/소문자를 구분하지 않는 식별자.

파일 경로.

레지스트리 키 및 값.

환경 변수입니다.

리소스 식별자(예: 핸들 이름).

대/소문자를 구분하지 않는 보안 관련 설정.
대/소문자와 관련이 없는 비언어적 식별자입니다. OrdinalIgnoreCase
일부 지속되는 언어적으로 관련된 데이터.

고정 정렬 순서가 필요한 언어 데이터 표시.
언어적으로 관련된 문화권을 구분하지 않는 데이터. InvariantCulture

또는

InvariantCultureIgnoreCase
사용자에게 표시된 데이터.

대부분 사용자 입력.
로컬 언어적 사용자 지정이 필요한 데이터. CurrentCulture

또는

CurrentCultureIgnoreCase

.NET에서 일반적인 문자열 비교 방법

다음 섹션에서는 문자열 비교에 가장 일반적으로 사용되는 메서드에 대해 설명합니다.

String.Compare

기본 해석: StringComparison.CurrentCulture.

문자열 해석의 가장 중심적인 작업으로, 이들 메서드 호출의 모든 인스턴스를 검사하여 문자열이 현재 문화권에 따라 해석되는지, 아니면 문화권과 관계가 없는지를 결정해야 합니다. 일반적으로 이는 후자이므로 StringComparison.Ordinal 비교를 사용해야 합니다.

System.Globalization.CompareInfo 속성에서 반환되는 CultureInfo.CompareInfo 클래스에는 Compare 플래그 열거형을 통해 다양한 일치 항목 찾기 옵션(서수, 공백 무시, 가나 형식 무시 등)을 제공하는 CompareOptions 메서드가 포함됩니다.

String.CompareTo

기본 해석: StringComparison.CurrentCulture.

이 메서드는 현재 StringComparison 형식을 지정하는 오버로드를 제공하지 않습니다. 일반적으로 이 메서드를 권장되는 String.Compare(String, String, StringComparison) 양식으로 변환할 수 있습니다.

IComparableIComparable<T> 인터페이스를 구현하는 형식이 이 메서드를 구현합니다. StringComparison 매개 변수의 옵션을 제공하지 않으므로 형식 구현 시 사용자가 생성자에서 StringComparer를 지정해야 할 수 있습니다. 다음 예제에서는 클래스 생성자에 FileName 매개 변수가 포함된 StringComparer 클래스를 정의합니다. 이 StringComparer 개체는 FileName.CompareTo 메서드에 사용됩니다.

class FileName : IComparable
{
    private readonly StringComparer _comparer;

    public string Name { get; }

    public FileName(string name, StringComparer? comparer)
    {
        if (string.IsNullOrEmpty(name)) throw new ArgumentNullException(nameof(name));

        Name = name;

        if (comparer != null)
            _comparer = comparer;
        else
            _comparer = StringComparer.OrdinalIgnoreCase;
    }

    public int CompareTo(object? obj)
    {
        if (obj == null) return 1;

        if (obj is not FileName)
            return _comparer.Compare(Name, obj.ToString());
        else
            return _comparer.Compare(Name, ((FileName)obj).Name);
    }
}
Class FileName
    Implements IComparable

    Private ReadOnly _comparer As StringComparer

    Public ReadOnly Property Name As String

    Public Sub New(name As String, comparer As StringComparer)
        If (String.IsNullOrEmpty(name)) Then Throw New ArgumentNullException(NameOf(name))

        Me.Name = name

        If comparer IsNot Nothing Then
            _comparer = comparer
        Else
            _comparer = StringComparer.OrdinalIgnoreCase
        End If
    End Sub

    Public Function CompareTo(obj As Object) As Integer Implements IComparable.CompareTo
        If obj Is Nothing Then Return 1

        If TypeOf obj IsNot FileName Then
            Return _comparer.Compare(Name, obj.ToString())
        Else
            Return _comparer.Compare(Name, DirectCast(obj, FileName).Name)
        End If
    End Function
End Class

String.Equals

기본 해석: StringComparison.Ordinal.

String 클래스를 통해 정적 또는 인스턴스 Equals 메서드 오버로드를 호출하거나 정적 같음 연산자를 사용하여 같음 여부를 테스트할 수 있습니다. 오버로드 및 연산자에는 기본적으로 서수 비교가 사용됩니다. 그러나 서수 비교를 수행하려 해도 StringComparison 형식을 명시적으로 지정하는 오버로드를 호출하는 것이 좋습니다. 이렇게 하면 특정 문자열 해석을 위해 코드를 더 쉽게 검색할 수 있습니다.

String.ToUpper 및 String.ToLower

기본 해석: StringComparison.CurrentCulture.

문자열을 대문자 또는 소문자에 적용하는 방법은 대/소문자와 관계없이 문자열 비교 시 작은 정규화로 사용되는 경우가 많이 때문에 String.ToUpper()String.ToLower() 메서드를 사용할 때는 주의해야 합니다. 이들 메서드를 사용할 경우 대/소문자를 구분하지 않는 비교를 사용하는 것이 좋습니다.

String.ToUpperInvariantString.ToLowerInvariant 메서드도 사용할 수 있습니다. ToUpperInvariant 는 대/소문자를 정규화하는 표준 방법입니다. StringComparison.OrdinalIgnoreCase 를 사용한 비교는 동작으로 두 가지 호출(두 문자열 인수에 대한 ToUpperInvariant 호출 및 StringComparison.Ordinal을 사용한 비교 수행)의 컴퍼지션입니다.

문화권을 나타내는 CultureInfo 개체를 메서드에 전달하여 특정 문화권에서 대문자 및 소문자로 변환하는 데도 오버로드를 사용할 수 있습니다.

Char.ToUpper 및 Char.ToLower

기본 해석: StringComparison.CurrentCulture.

Char.ToUpper(Char)Char.ToLower(Char) 메서드는 이전 섹션에서 설명된 String.ToUpper()String.ToLower() 메서드와 유사하게 작동합니다.

String.StartsWith 및 String.EndsWith

기본 해석: StringComparison.CurrentCulture.

기본적으로 이들 메서드는 둘 다 문화권 구분 비교를 수행합니다. 특히 인쇄되지 않는 문자는 무시될 수 있습니다.

String.IndexOf 및 String.LastIndexOf

기본 해석: StringComparison.CurrentCulture.

이러한 메서드의 기본 오버로드가 비교를 수행하는 방식에는 일관성이 부족합니다. String.IndexOf 매개 변수를 포함하는 모든 String.LastIndexOfChar 메서드는 서수 비교를 수행하지만, String.IndexOf 매개 변수를 포함하는 기본 String.LastIndexOfString 메서드는 문화권 구분 비교를 수행합니다.

String.IndexOf(String) 또는 String.LastIndexOf(String) 메서드를 호출하고 현재 인스턴스에서 찾을 문자열을 전달할 경우 StringComparison 형식을 명시적으로 지정하는 오버로드를 호출하는 것이 좋습니다. Char 인수를 포함하는 오버로드를 사용하면 StringComparison 형식을 지정할 수 없게 됩니다.

문자열 비교를 간접적으로 수행하는 방법

문자열 비교를 중심 작업으로 포함하는 일부 문자열이 아닌 메서드에는 StringComparer 형식이 사용됩니다. StringComparer 클래스에는 포함된 StringComparer 메서드가 다음 형식의 문자열 비교를 수행하는 StringComparer.Compare 인스턴스를 반환하는 정적 속성 6개가 포함됩니다.

Array.Sort 및 Array.BinarySearch

기본 해석: StringComparison.CurrentCulture.

컬렉션에 데이터를 저장하거나 영구 데이터를 파일이나 데이터베이스에서 컬렉션으로 읽을 때 현재 문화권을 전환하면 컬렉션에서 고정이 무효화될 수 있습니다. Array.BinarySearch 메서드는 검색할 배열의 요소가 이미 정렬되었다고 가정합니다. 배열에서 문자열 요소를 정렬하려고 Array.Sort 메서드는 String.Compare 메서드를 호출하여 개별 요소의 순서를 지정합니다. 배열이 정렬된 시간과 콘텐츠가 검색된 시간 사이에 문화권이 변경될 경우 문화권 구분 비교자 사용이 위험할 수 있습니다. 예를 들어 다음 코드에서 스토리지 및 검색은 Thread.CurrentThread.CurrentCulture 속성에 의해 암시적으로 제공되는 비교자에서 작동합니다. StoreNamesDoesNameExist에 대한 호출 사이에 문화권이 변경되고 특히 배열 콘텐츠가 두 메서드 호출 사이에 지속되면 이진 검색이 실패할 수 있습니다.

// Incorrect
string[] _storedNames;

public void StoreNames(string[] names)
{
    _storedNames = new string[names.Length];

    // Copy the array contents into a new array
    Array.Copy(names, _storedNames, names.Length);

    Array.Sort(_storedNames); // Line A
}

public bool DoesNameExist(string name) =>
    Array.BinarySearch(_storedNames, name) >= 0; // Line B
' Incorrect
Dim _storedNames As String()

Sub StoreNames(names As String())
    ReDim _storedNames(names.Length - 1)

    ' Copy the array contents into a new array
    Array.Copy(names, _storedNames, names.Length)

    Array.Sort(_storedNames) ' Line A
End Sub

Function DoesNameExist(name As String) As Boolean
    Return Array.BinarySearch(_storedNames, name) >= 0 ' Line B
End Function

배열을 정렬 및 검색할 때 둘 다 같은 서수(문화권을 구분하지 않는) 비교 메서드를 사용하는 다음 예제에서 권장되는 변형을 보여 줍니다. 변경 코드는 두 예제의 Line ALine B 줄에 반영됩니다.

// Correct
string[] _storedNames;

public void StoreNames(string[] names)
{
    _storedNames = new string[names.Length];

    // Copy the array contents into a new array
    Array.Copy(names, _storedNames, names.Length);

    Array.Sort(_storedNames, StringComparer.Ordinal); // Line A
}

public bool DoesNameExist(string name) =>
    Array.BinarySearch(_storedNames, name, StringComparer.Ordinal) >= 0; // Line B
' Correct
Dim _storedNames As String()

Sub StoreNames(names As String())
    ReDim _storedNames(names.Length - 1)

    ' Copy the array contents into a new array
    Array.Copy(names, _storedNames, names.Length)

    Array.Sort(_storedNames, StringComparer.Ordinal) ' Line A
End Sub

Function DoesNameExist(name As String) As Boolean
    Return Array.BinarySearch(_storedNames, name, StringComparer.Ordinal) >= 0 ' Line B
End Function

이 데이터가 문화권에 걸쳐 지속 및 이동되고 이 데이터를 사용자에게 제공할 때 정렬을 사용할 경우 향상된 사용자 출력을 위해 언어적으로 작동하지만 문화권 변경의 영향을 받지 않는 StringComparison.InvariantCulture를 사용하는 것이 좋을 수 있습니다. 다음 예제에서는 두 가지 이전 예제에서 배열 정렬 및 검색에 고정 문화권을 사용하도록 수정합니다.

// Correct
string[] _storedNames;

public void StoreNames(string[] names)
{
    _storedNames = new string[names.Length];

    // Copy the array contents into a new array
    Array.Copy(names, _storedNames, names.Length);

    Array.Sort(_storedNames, StringComparer.InvariantCulture); // Line A
}

public bool DoesNameExist(string name) =>
    Array.BinarySearch(_storedNames, name, StringComparer.InvariantCulture) >= 0; // Line B
' Correct
Dim _storedNames As String()

Sub StoreNames(names As String())
    ReDim _storedNames(names.Length - 1)

    ' Copy the array contents into a new array
    Array.Copy(names, _storedNames, names.Length)

    Array.Sort(_storedNames, StringComparer.InvariantCulture) ' Line A
End Sub

Function DoesNameExist(name As String) As Boolean
    Return Array.BinarySearch(_storedNames, name, StringComparer.InvariantCulture) >= 0 ' Line B
End Function

컬렉션 예제: 해시 가능한 생성자

문자열 비교 방법이 영향을 미치는 작업의 두 번째 예로는 문자열 해시가 있습니다.

다음 예제에서는 Hashtable 속성에서 반환되는 StringComparer 개체를 전달하여 StringComparer.OrdinalIgnoreCase 개체를 인스턴스화합니다. StringComparer 에서 파생된 StringComparer 클래스는 IEqualityComparer 인터페이스를 구현하므로 해당 GetHashCode 메서드는 해시 테이블에서 문자열의 해시 코드를 컴퓨팅하는 데 사용됩니다.

using System.IO;
using System.Collections;

const int InitialCapacity = 100;

Hashtable creationTimeByFile = new(InitialCapacity, StringComparer.OrdinalIgnoreCase);
string directoryToProcess = Directory.GetCurrentDirectory();

// Fill the hash table
PopulateFileTable(directoryToProcess);

// Get some of the files and try to find them with upper cased names
foreach (var file in Directory.GetFiles(directoryToProcess))
    PrintCreationTime(file.ToUpper());


void PopulateFileTable(string directory)
{
    foreach (string file in Directory.GetFiles(directory))
        creationTimeByFile.Add(file, File.GetCreationTime(file));
}

void PrintCreationTime(string targetFile)
{
    object? dt = creationTimeByFile[targetFile];

    if (dt is DateTime value)
        Console.WriteLine($"File {targetFile} was created at time {value}.");
    else
        Console.WriteLine($"File {targetFile} does not exist.");
}
Imports System.IO

Module Program
    Const InitialCapacity As Integer = 100

    Private ReadOnly s_creationTimeByFile As New Hashtable(InitialCapacity, StringComparer.OrdinalIgnoreCase)
    Private ReadOnly s_directoryToProcess As String = Directory.GetCurrentDirectory()

    Sub Main()
        ' Fill the hash table
        PopulateFileTable(s_directoryToProcess)

        ' Get some of the files and try to find them with upper cased names
        For Each File As String In Directory.GetFiles(s_directoryToProcess)
            PrintCreationTime(File.ToUpper())
        Next
    End Sub

    Sub PopulateFileTable(directoryPath As String)
        For Each file As String In Directory.GetFiles(directoryPath)
            s_creationTimeByFile.Add(file, IO.File.GetCreationTime(file))
        Next
    End Sub

    Sub PrintCreationTime(targetFile As String)
        Dim dt As Object = s_creationTimeByFile(targetFile)

        If TypeOf dt Is Date Then
            Console.WriteLine($"File {targetFile} was created at time {DirectCast(dt, Date)}.")
        Else
            Console.WriteLine($"File {targetFile} does not exist.")
        End If
    End Sub
End Module

참고 항목