在 .NET 中比較字串的最佳做法
.NET 可廣泛支援當地語系化和全球化應用程式的開發作業,使您在執行一般作業 (例如排序和顯示字串) 時,可輕鬆套用目前的文化特性或特定文化特性的慣例。 但是,排序或比較字串並不一定是區分文化特性的作業。 例如,應用程式內部使用的字串,通常應該跨所有文化特性皆進行相同處理。 若將與文化特性無關的字串資料 (例如 XML 標記、HTML 標記、使用者名稱、檔案路徑和系統物件的名稱) 進行區分文化特性的解譯時,應用程式程式碼可能會出現細微的 Bug、效能不佳,甚至在某些情況下,會產生安全性問題。
本文會詳述 .NET 中的字串排序、比較和大小寫方法,並提供適當字串處理方法的選擇建議,以及字串處理方法的其他資訊。
字串的使用建議
當您使用 .NET 進行開發時,請遵循下列字串比較建議:
提示
各種字串相關方法會執行比較。 範例包括 String.Equals、String.Compare、String.IndexOf 和 String.StartsWith。
- 使用明確指定字串比較規則的多載來進行字串作業。 一般而言,這需要呼叫具有 StringComparison類別參數的方法多載。
- 將 StringComparison.Ordinal 或 StringComparison.OrdinalIgnoreCase 做為安全的無從驗證文化特性字串預設比對,來進行比較。
- 使用 StringComparison.Ordinal 或 StringComparison.OrdinalIgnoreCase 來比較以提升效能。
- 向使用者顯示輸出時,您可以使用依據 StringComparison.CurrentCulture 的字串作業。
- 在進行語言無關的比較 (如符號) 時,使用非語言式 StringComparison.Ordinal 或 StringComparison.OrdinalIgnoreCase 值,而不是依據 CultureInfo.InvariantCulture 的字串作業。
- 正規化字串以進行比較,使用 String.ToUpperInvariant 方法,而非 String.ToLowerInvariant 方法。
- 使用 String.Equals 方法的多載,來測試兩個字串是否相等。
- 使用 String.Compare 和 String.CompareTo 方法來排序字串,而不檢查是否相等。
- 使用區分文化特性的格式來顯示使用者介面中的非字串資料,例如數字和日期。 使用不因文化特性而異的格式,來保存字串形式的非字串資料。
當您比較字串時,請避免下列作法:
- 請勿使用未明確或未隱含指定字串比較規則的多載來進行字串作業。
- 在大部分情況下,請勿使用依據 StringComparison.InvariantCulture 的字串作業。 若您要保存具語言意義但無從驗證文化特性的資料,就是少數的例外狀況之一。
- 請勿使用 String.Compare 或 CompareTo 方法的多載以及傳回零值的測試,來判斷兩個字串是否相等。
明確指定字串比較
在 .NET 中的字串操作方法大多都是多載。 一般而言,您可讓一或多個多載接受預設值,而其他不接受預設值的多載,則定義用來比較或操作字串的精確方式。 大部分不依賴預設值的方法都會包含 StringComparison 型別的參數,其為一種列舉類型,可明確指定依據文化特性和大小寫進行字串比較的規則。 下表說明 StringComparison 列舉類型成員。
StringComparison 成員 | 描述 |
---|---|
CurrentCulture | 使用目前文化特性執行區分大小寫的比較。 |
CurrentCultureIgnoreCase | 使用目前文化特性執行不區分大小寫的比較。 |
InvariantCulture | 使用不因國別而異的文化特性執行區分大小寫的比較。 |
InvariantCultureIgnoreCase | 使用不因國別而異的文化特性執行不區分大小寫的比較。 |
Ordinal | 執行序數比較。 |
OrdinalIgnoreCase | 執行不區分大小寫的序數比較。 |
例如, IndexOf 方法有下列九個多載,可傳回 String 物件中符合某字元或某字串之子字串的索引:
- IndexOf(Char)、 IndexOf(Char, Int32) 和 IndexOf(Char, Int32, Int32) 會預設執行字串字元的序數 (區分大小寫且區分文化特性) 搜尋。
- IndexOf(String)、 IndexOf(String, Int32) 和 IndexOf(String, Int32, Int32) 會預設執行字串中的子字串搜尋 (區分大小寫且區分文化特性)。
- IndexOf(String, StringComparison)、 IndexOf(String, Int32, StringComparison)和 IndexOf(String, Int32, Int32, StringComparison)包括 StringComparison 類型的參數,可指定比較形式。
基於下列原因,建議您選取不使用預設值的多載:
有些使用預設參數的多載 (其會在字串執行個體中搜尋 Char ) 會執行序數比較,而其他多載 (其會搜尋字串執行個體中的字串) 有區分文化特性。 使用者很難記住哪一種方法使用預設值,也很容易混淆多載。
依賴預設值來執行方法呼叫的程式碼意圖並不清楚。 在下列使用預設值的範例中,我們難以知道開發人員要進行兩個字串的序數或語言比較,也很難判斷
url.Scheme
和 "http" 之間的大小寫差異是否會造成相等測試傳回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"。 此外,比較也隱含定義相等性。 比較作業會針對它認為相等的字串傳回零。 明言之,就是沒有任一字串比另一個字串更小。 最有意義的字串作業包含下列一或兩個程序:和其他字串進行比較,並執行定義完善的排序作業。
注意
您可以下載排序權數資料表,該文字檔集合包含在 Windows 作業系統排序及比較作業中使用的字元權數資訊,以及下載預設 Unicode 定序元素資料表 (適用於 Linux 和 macOS 的最新版本排序權數資料表)。 Linux 和 macOS 上的特定版本排序權數資料表,取決於在系統上安裝的 International Components for Unicode 程式庫。 如需其實作的 ICU 版本及 Unicode 版本詳細資訊,請參閱下載 ICU。
不過,評估兩個字串是否相等或決定排序順序不會產生單一的正確結果,而要取決於用來比較字串的準則而定。 特別是,如果字串比較是序數或根據目前文化特性或不因文化特性而異的大小寫與排序慣例 (根據英文語言的無從驗證地區設定文化特性),則可能會產生不同的結果。
此外,使用不同版本 .NET 或使用不同作業系統或作業系統版本上 .NET 所做的字串比較,可能會傳回不同的結果。 如需詳細資訊,請參閱字串及 Unicode 標準。
使用目前之文化特性的字串比較
比較字串時,其中一個準則需使用目前文化特性的慣例。 如果比較是以目前文化特性為依據,就會使用執行緒的目前文化特性或地區設定。 如果使用者未設定文化特性,則預設為作業系統的設定。 當資料是語言相關資料,以及資料會反映區分文化特性的使用者互動時,請一律使用以目前文化特性為根據的比較。
不過,當文化特性變更時,.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 參數的 String.Compare 多載。
- String.CompareTo 多載。
- 預設的 String.StartsWith(String) 方法和含有 String.StartsWith(String, Boolean, CultureInfo) null
null
CultureInfo 多載。 - 預設的 String.EndsWith(String) 方法和含有 String.EndsWith(String, Boolean, CultureInfo) null
null
CultureInfo 多載。 - 可接受 String 作為搜尋參數且不含 StringComparison 參數的 String.IndexOf 多載。
- 可接受 String 作為搜尋參數且不含 StringComparison 參數的 String.LastIndexOf 多載。
在任何情況下,都建議您呼叫具有 StringComparison 參數的多載,以讓呼叫方法的目的更清晰。
若以語言方式解譯非語言式的字串資料,或使用其他文化特性的慣例解譯來自特定文化特性的字串資料時,可能會出現微妙或不太微妙的 Bug。 標準範例是土耳其文 I 的問題。
對於幾乎所有拉丁字母 (包括美國英文),字元 "i" (\u0069) 是字元 "I" (\u0049) 的小寫。 這種大小寫規則很快成為以這種文化特性進行程式設計的人所採用的預設規則。 不過,土耳其文 ("tr-TR") 字母包括「加上一點的 I」字元 "İ" (\u0130),也就是 "i" 的大寫。 土耳其文中也包含「不含點的 i 」字元,"ı" (\u0131),其大寫為 "I"。 亞塞拜然 ("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,來規避封鎖存取不區分大小寫之 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.Compare 和 String.Equals 方法來執行區分文化特性的比較 (包括使用不因文化特性而異的比較) 時,會忽略這些字元。 如此一來,即可將包含內嵌 Null 字元的字串視為等於不含這類字元的字串。 基於字串比較方法的目的,可能會略過內嵌的非列印字元,例如 String.StartsWith。
重要
雖然字串比較方法可以忽略內嵌的 Null 字元,但 String.Contains、 String.EndsWith、 String.IndexOf、 String.LastIndexOf和 String.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 字元時,這項原則相當於 StringComparison.Ordinal,不過它會忽略一般 ASCII 大小寫。 因此,[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.Ordinal 和 StringComparison.OrdinalIgnoreCase 兩者都直接使用二進位值,最適合用來比對。 當您不確定比較設定時,請使用這兩個值的其中一個。 不過,因為其會以位元組為單位逐一比較,所以不會依照語言排序次序來排序 (就像英文字典一樣),而是採用二進位排序次序。 因此,在大多數內容當中,使用者看到的結果可能很奇怪。
不包含 StringComparison 引數 (包括等號比較運算子) 的 String.Equals 多載是以序數語意為預設。 在任何情況下,我們建議您呼叫具有 StringComparison 參數的多載。
使用文化特性不變的字串作業
採用不因文化特性而異的比較會使用靜態 CompareInfo 屬性傳回的 CultureInfo.InvariantCulture 屬性。 這種行為在所有系統上都相同,它會將其範圍之外的任何字元轉譯成它認為是相等非變異字元的字元。 這項原則很適合跨文化特性來維護一套字串行為,但通常會產生非預期的結果。
採用不因文化特性而異的不分區大小寫比較,也會使用靜態 CompareInfo 屬性所傳回的靜態 CultureInfo.InvariantCulture 屬性來取得比較資訊。 這些轉譯的字元之間的任何大小寫差異都會被忽略。
使用 StringComparison.InvariantCulture 和 StringComparison.Ordinal 的比較在 ASCII 字串上的運作方式完全相同。 不過,對於必須解譯成一組位元組的字串, StringComparison.InvariantCulture 所做的語言決策就可能不適合。 由 CultureInfo.InvariantCulture.CompareInfo
物件使 Compare 方法將多組字元解譯成相等。 例如,下列等式在不因國別而異的文化特性之下有效:
InvariantCulture: a + ̊ = å
拉丁小寫字母 A 字元 "a" (\u0061) 在緊鄰著結合上圓圈字元 "+ " ̊" (\u030a) 時,解譯成拉丁小寫字母 A 帶上圓圈字元 "å" (\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
在解譯檔名、Cookie 或可能出現像 "å" 一樣的組合的其他任何字串時,序數比較仍然會展現最明確和最適當的行為。
權衡來看,不因國別而異的文化特性只有很少的屬性,因此適合用於比較。 不過,其會以語言相關的方式進行比較,這樣無法保證符號完全相等,因而不適合在任何文化特性中顯示。 使用 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) 形式。
實作 IComparable 和 IComparable<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.ToUpperInvariant 和 String.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.LastIndexOf 和 Char 方法都執行序數比較,但包含 String.IndexOf 參數的預設 String.LastIndexOf 和 String 方法會執行區分文化特性的比較。
如果您呼叫 String.IndexOf(String) 或 String.LastIndexOf(String) 方法,並將要在目前執行個體中尋找的字串傳遞至這個方法,我們建議您呼叫明確指定 StringComparison 類型的多載。 包含 Char 引數的多載不允許您指定 StringComparison 類型。
間接執行字串比較的方法
有一些以字串比較為主要作業的非字串方法會使用 StringComparer 類型。 StringComparer 類別包含六個靜態屬性,這些屬性會傳回 StringComparer 執行個體,而這些執行個體的 StringComparer.Compare 方法可以執行下列類型的字串比較:
- 使用目前文化特性的區分文化特性字串比較。 這個 StringComparer 物件是由 StringComparer.CurrentCulture 屬性傳回。
- 使用目前文化特性的不區分大小寫比較。 這個 StringComparer 物件是由 StringComparer.CurrentCultureIgnoreCase 屬性傳回。
- 使用不因文化特性而異之字組比較規則的不區分文化特性比較。 這個 StringComparer 物件是由 StringComparer.InvariantCulture 屬性傳回。
- 使用不因文化特性而異之字組比較規則的不區分大小寫和文化特性比較。 這個 StringComparer 物件是由 StringComparer.InvariantCultureIgnoreCase 屬性傳回。
- 序數比較。 這個 StringComparer 物件是由 StringComparer.Ordinal 屬性傳回。
- 不區分大小寫的序數比較。 這個 StringComparer 物件是由 StringComparer.OrdinalIgnoreCase 屬性傳回。
Array.Sort 和 Array.BinarySearch
預設解譯: StringComparison.CurrentCulture。
當您將任何資料儲存在集合中,或從檔案或資料庫將保存的資料讀入集合時,切換目前文化特性會使集合中的非變異失效。 Array.BinarySearch 方法假設要搜尋之陣列中的項目已排序。 若要排序陣列中的任何字串項目, Array.Sort 方法會呼叫 String.Compare 方法來排序個別項目。 從排序陣列到搜尋其內容這段時間當中,如果文化特性變更,則使用區分文化特性的比較子可能會有危險。 例如,在下列程式碼中,儲存和擷取作業在 Thread.CurrentThread.CurrentCulture
屬性。 如果文化特性在呼叫 StoreNames
和 DoesNameExist
之間可能變更,尤其是如果陣列內容在這兩個方法呼叫之間保存在某處,則二進位搜尋可能會失敗。
// 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 A
和 Line 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 建構函式
第二個受到字串比較方式而影響作業的範例是雜湊字串。
下列範例將 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