Partager via


Meilleures pratiques pour comparer des chaînes dans .NET

.NET offre une prise en charge étendue du développement d’applications localisées et globalisées, et facilite l’application des conventions de la culture actuelle ou d’une culture spécifique lors de l’exécution d’opérations courantes telles que le tri et l’affichage de chaînes. Toutefois, le tri ou la comparaison de chaînes n'est pas toujours une opération dépendante de la culture. Par exemple, les chaînes utilisées en interne par une application doivent généralement être gérées de manière identique dans toutes les cultures. Lorsque des données de chaîne indépendantes culturellement, telles que des balises XML, des balises HTML, des noms d’utilisateur, des chemins d’accès aux fichiers et les noms d’objets système, sont interprétées comme si elles étaient sensibles à la culture, le code d’application peut être soumis à des bogues subtils, à des performances médiocres et, dans certains cas, à des problèmes de sécurité.

Cet article examine les méthodes de tri, de comparaison et de casse de chaîne dans .NET, présente des recommandations pour sélectionner une méthode de gestion de chaîne appropriée et fournit des informations supplémentaires sur les méthodes de gestion des chaînes.

Recommandations relatives à l’utilisation de chaînes

Lorsque vous développez avec .NET, suivez ces recommandations lorsque vous comparez des chaînes.

Conseil / Astuce

Différentes méthodes liées à des chaînes effectuent une comparaison. Exemples : String.Equals, String.Compare, String.IndexOfet String.StartsWith.

Évitez les pratiques suivantes lorsque vous comparez des chaînes de caractères :

  • N'utilisez pas de surcharges qui ne spécifient pas explicitement ou implicitement les règles de comparaison de chaînes pour les opérations de chaînes.
  • N’utilisez pas d’opérations de chaîne basées sur StringComparison.InvariantCulture dans la plupart des cas. L’une des rares exceptions est que vous conservez des données linguistiquement significatives mais indépendantes culturellement.
  • N'utilisez pas de surcharge des méthodes String.Compare ou CompareTo et testez si la valeur de retour est zéro pour déterminer si deux chaînes sont égales.

Spécifier explicitement les comparaisons de chaînes

La plupart des méthodes de manipulation de chaînes dans .NET sont surchargées. En règle générale, une ou plusieurs surcharges acceptent les paramètres par défaut, tandis que d’autres n’acceptent aucune valeur par défaut et définissent plutôt la façon précise dont les chaînes doivent être comparées ou manipulées. La plupart des méthodes qui ne reposent pas sur les valeurs par défaut incluent un paramètre de type StringComparison, qui est une énumération qui spécifie explicitement les règles de comparaison de chaînes par culture et cas. Le tableau suivant décrit les membres de l'énumération StringComparison .

Membre StringComparison Descriptif
CurrentCulture Effectue une comparaison respectant la casse à l'aide de la culture actuelle.
CurrentCultureIgnoreCase Effectue une comparaison ne respectant pas la casse à l'aide de la culture actuelle.
InvariantCulture Effectue une comparaison respectant la casse à l'aide de la culture dite indifférente.
InvariantCultureIgnoreCase Effectue une comparaison ne respectant pas la casse à l'aide de la culture dite indifférente.
Ordinal Effectue une comparaison ordinale.
OrdinalIgnoreCase Effectue une comparaison ordinale ne respectant pas la casse.

Par exemple, la IndexOf méthode, qui retourne l’index d’une sous-chaîne dans un String objet qui correspond à un caractère ou une chaîne, a neuf surcharges :

Nous vous recommandons de sélectionner une surcharge qui n’utilise pas les valeurs par défaut, pour les raisons suivantes :

  • Certaines surcharges ayant des paramètres par défaut (celles qui recherchent un Char dans l'instance de chaîne) effectuent une comparaison ordinale, tandis que d'autres (celles qui recherchent une chaîne dans l'instance de chaîne) sont dépendantes de la culture. Il est difficile de se rappeler quelle méthode utilise la valeur par défaut et facile à confondre les surcharges.

  • L’intention du code qui s’appuie sur des valeurs par défaut pour les appels de méthode n’est pas claire. Dans l’exemple suivant, qui est basé sur des valeurs par défaut, il est difficile de savoir si le développeur voulait en fait une comparaison ordinale ou linguistique de deux chaînes, ou si une différence de casse entre url.Scheme et "https" peut entraîner le retour de 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
    

En général, nous vous recommandons d’appeler une méthode qui ne s’appuie pas sur les valeurs par défaut, car elle rend l’intention du code non ambiguë. Cela rend le code plus lisible et plus facile à déboguer et à gérer. L’exemple suivant aborde les questions soulevées sur l’exemple précédent. Il indique clairement que la comparaison ordinale est utilisée et que les différences dans le cas sont ignorées.

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

Détails de la comparaison de chaînes

La comparaison de chaînes est le cœur de nombreuses opérations liées aux chaînes, en particulier le tri et le test d'égalité. Les chaînes sont triées dans un ordre déterminé : si et seulement si « my » apparaît avant « string » dans une liste triée de chaînes, « my » doit être considérée comme inférieure ou égale à « string ». En outre, la comparaison définit implicitement l’égalité. L’opération de comparaison retourne zéro pour les chaînes qu’elle considère comme égales. Une bonne interprétation est que aucune chaîne n’est inférieure à l’autre. La plupart des opérations significatives impliquant des chaînes incluent une ou les deux procédures suivantes : comparaison avec une autre chaîne et exécution d’une opération de tri bien définie.

Remarque

Vous pouvez télécharger les tables de pondération de tri, un ensemble de fichiers texte qui contiennent des informations sur les pondérations de caractères utilisées dans les opérations de tri et de comparaison pour les systèmes d’exploitation Windows, ainsi que la table d’éléments de classement Unicode par défaut, la dernière version de la table de pondération de tri pour Linux et macOS. La version spécifique de la table de pondération de tri sur Linux et macOS dépend de la version des composants internationaux pour les bibliothèques Unicode installées sur le système. Pour plus d’informations sur les versions d’ICU et les versions Unicode qu’ils implémentent, consultez Téléchargement d’ICU.

Toutefois, l’évaluation de deux chaînes pour l’égalité ou l’ordre de tri ne génère pas de résultat unique et correct ; le résultat dépend des critères utilisés pour comparer les chaînes. En particulier, les comparaisons de chaînes qui sont ordinales ou basées sur les conventions de casse et de tri de la culture actuelle ou la culture invariante (culture aux paramètres régionaux non spécifiés basée sur la langue anglaise) peuvent produire des résultats différents.

En outre, les comparaisons de chaînes utilisant différentes versions de .NET ou .NET sur différents systèmes d’exploitation ou versions de système d’exploitation peuvent retourner des résultats différents. Pour plus d’informations, consultez Chaînes et Norme Unicode.

Comparaisons de chaînes qui utilisent la culture actuelle

L'un des critères à prendre en compte est l'utilisation des conventions de la culture actuelle lors de la comparaison de chaînes. Les comparaisons basées sur la culture actuelle utilisent la culture ou les paramètres régionaux actuels du thread. Si la culture n’est pas définie par l’utilisateur, elle est définie par défaut sur le paramètre du système d’exploitation. Vous devez toujours utiliser des comparaisons basées sur la culture actuelle lorsque les données sont linguistiquesment pertinentes et lorsqu’elles reflètent l’interaction de l’utilisateur sensible à la culture.

Toutefois, le comportement de la comparaison et de la casse dans .NET change en fonction de la culture. Cela se produit lorsqu’une application s’exécute sur un ordinateur qui a une culture différente de celle de l’ordinateur sur lequel l’application a été développée, ou lorsque le thread en cours d’exécution modifie sa culture. Ce comportement est intentionnel, mais il reste non évident pour de nombreux développeurs. L’exemple suivant illustre les différences dans l’ordre de tri entre les cultures américaines anglais («en-US») et suédois («sv-SE»). Notez que les mots « ångström », « Windows » et « Visual Studio » apparaissent dans différentes positions dans les tableaux de chaînes triés.

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

Les comparaisons ne respectant pas la casse qui utilisent la culture actuelle sont identiques aux comparaisons dépendantes de la culture, mais elles ignorent la casse, comme défini par la culture actuelle du thread. Ce comportement peut également se manifester dans les ordres de tri.

Les comparaisons qui utilisent la sémantique de culture actuelle sont la valeur par défaut pour les méthodes suivantes :

Dans tous les cas, nous vous recommandons d'appeler une surcharge qui a un paramètre StringComparison , afin que l'objectif de l'appel de la méthode soit clair.

Les bogues subtils et pas si subtils peuvent apparaître lorsque les données de chaîne non linguistiques sont interprétées linguistiquement, ou lorsque les données de chaîne d’une culture particulière sont interprétées à l’aide des conventions d’une autre culture. L’exemple canonique est le problème Turkish-I.

Pour presque tous les alphabets latins, y compris l’anglais américain, le caractère « i » (\u0069) est la version minuscule du caractère « I » (\u0049). Cette règle de casse devient rapidement la valeur par défaut pour quelqu'un qui effectue de la programmation dans une telle culture. Toutefois, l’alphabet turc ( »tr-TR« ) comprend un caractère « I avec un point » « İ » (\u0130), qui est la version majuscule de « i ». Turc comprend également un caractère minuscule « i sans point », « ı » (\u0131), qui capitalise sur « I ». Ce comportement se produit également dans la culture azerbaïdjan (« az »).

Par conséquent, les hypothèses faites au sujet de la capitalisation du « i » ou de la mise en minuscule du « I » ne sont pas valides dans toutes les cultures. Si vous utilisez les surcharges par défaut pour les routines de comparaison de chaînes, elles sont soumises à une variance entre les cultures. Si les données à comparer sont non linguistiques, l'utilisation de surcharges par défaut peut produire des résultats indésirables, comme le montre la tentative suivante d'exécution d'une comparaison ne respectant pas la casse des chaînes "bill" et "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

Cette comparaison peut entraîner des problèmes significatifs si la culture est utilisée par inadvertance dans les paramètres sensibles à la sécurité, comme dans l’exemple suivant. Un appel de méthode comme IsFileURI("file:") renvoie true si la culture actuelle est l’anglais américain, mais false si la culture actuelle est turque. Par conséquent, sur les systèmes turcs, quelqu'un pourrait contourner les mesures de sécurité qui bloquent l'accès aux URI ne respectant pas la casse qui commencent par "FILE:".

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

Dans ce cas, étant donné que « file : » est destiné à être interprété comme un identificateur non linguistique et non sensible à la culture, le code doit être écrit comme indiqué dans l’exemple suivant :

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

Opérations de chaîne ordinale

Spécifier la valeur StringComparison.Ordinal ou StringComparison.OrdinalIgnoreCase dans un appel de méthode signifie une comparaison non linguistique dans laquelle les caractéristiques des langues naturelles sont ignorées. Les méthodes qui sont appelées avec ces valeurs StringComparison font reposer les décisions d'opération de chaîne sur de simples comparaisons d'octets plutôt que sur la casse ou des tables d'équivalences paramétrables par la culture. Dans la plupart des cas, cette approche convient le mieux à l’interprétation prévue des chaînes tout en rendant le code plus rapide et plus fiable.

Les comparaisons ordinales sont des comparaisons de chaînes dans lesquelles chaque octet de chaque chaîne est comparé sans interprétation linguistique ; par exemple, « windows » ne correspond pas à « Windows ». Il s’agit essentiellement d’un appel à la fonction runtime strcmp C. Utilisez cette comparaison quand le contexte indique que les chaînes doivent correspondre exactement ou demande une stratégie de correspondance classique. En outre, la comparaison ordinale est l’opération de comparaison la plus rapide, car elle n’applique aucune règle linguistique lors de la détermination d’un résultat.

Les chaînes dans .NET peuvent contenir des caractères Null incorporés (et d’autres caractères non imprimables). L’une des différences les plus claires entre la comparaison ordinale et sensible à la culture (y compris les comparaisons qui utilisent la culture invariante) concerne la gestion des caractères Null incorporés dans une chaîne. Ces caractères sont ignorés lorsque vous utilisez les méthodes String.Compare et String.Equals pour effectuer des comparaisons sensibles à la culture (y compris les comparaisons qui utilisent la culture invariante). Par conséquent, les chaînes qui contiennent des caractères null incorporés peuvent être considérées comme égales aux chaînes qui ne le font pas. Les caractères non imprimables incorporés peuvent être ignorés à des fins de méthodes de comparaison de chaînes, telles que String.StartsWith.

Importante

Bien que les méthodes de comparaison de chaînes ignorent les caractères null incorporés, les méthodes de recherche de chaîne telles que String.Contains, String.EndsWith, String.IndexOf, String.LastIndexOfet String.StartsWith ne le font pas.

L’exemple suivant effectue une comparaison sensible à la culture de la chaîne « Aa » avec une chaîne similaire qui contient plusieurs caractères null incorporés entre « A » et « a », et montre comment les deux chaînes sont considérées comme égales :

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

Toutefois, les chaînes ne sont pas considérées comme égales lorsque vous utilisez la comparaison ordinale, comme l’illustre l’exemple suivant :

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

Les comparaisons ordinales ne respectant pas la casse sont l'approche la plus conservatrice suivante. Ces comparaisons ignorent la plus grande partie de la casse ; par exemple, "windows" correspond à "Windows". Lors du traitement de caractères ASCII, cette stratégie est équivalente à StringComparison.Ordinal, excepté qu'elle ignore la casse ASCII habituelle. Par conséquent, tout caractère dans [A, Z] (\u0041-\u005A) correspond au caractère correspondant dans [a,z] (\u0061-\007A). La casse hors de la plage ASCII utilise les tables de la culture dite indifférente. Par conséquent, la comparaison suivante :

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

équivaut à (mais plus rapidement que) à cette comparaison :

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

Ces comparaisons sont encore très rapides.

Les deux StringComparison.Ordinal et StringComparison.OrdinalIgnoreCase utilisent directement les valeurs binaires et sont les mieux adaptés pour la correspondance. Lorsque vous n’êtes pas sûr de vos paramètres de comparaison, utilisez l’une de ces deux valeurs. Toutefois, étant donné qu’ils effectuent une comparaison d’octets par octets, ils ne trient pas par ordre de tri linguistique (comme un dictionnaire anglais) mais par un ordre de tri binaire. Les résultats peuvent sembler étranges dans la plupart des contextes s’ils sont affichés aux utilisateurs.

La sémantique ordinale est la valeur par défaut pour les surcharges de String.Equals qui n’incluent pas d’argument StringComparison (notamment l’opérateur d’égalité). Dans tous les cas, nous vous recommandons d'appeler une surcharge ayant un paramètre StringComparison .

Opérations de chaîne qui utilisent la culture invariante

Les comparaisons avec la culture invariante utilisent la CompareInfo propriété retournée par la propriété statique CultureInfo.InvariantCulture . Ce comportement est le même sur tous les systèmes ; il traduit tous les caractères en dehors de sa plage en ce qu’il croit être des caractères invariants équivalents. Cette stratégie peut être utile pour maintenir un ensemble de comportements de chaîne entre les cultures, mais elle fournit souvent des résultats inattendus.

Les comparaisons ne respectant pas la casse avec la culture dite indifférente utilisent également la propriété CompareInfo statique retournée également par la propriété CultureInfo.InvariantCulture statique pour les informations de comparaison. Toutes les différences de cas entre ces caractères traduits sont ignorées.

Comparaisons qui utilisent StringComparison.InvariantCulture et StringComparison.Ordinal fonctionnent de façon identique sur des chaînes ASCII. Toutefois, StringComparison.InvariantCulture prend des décisions linguistiques qui peuvent ne pas être appropriées pour les chaînes qui doivent être interprétées comme un ensemble d’octets. L’objet CultureInfo.InvariantCulture.CompareInfo fait que la méthode Compare interprète certains jeux de caractères comme équivalents. Par exemple, l’équivalence suivante est valide dans la culture invariante :

InvariantCulture : a + ̊ = å

Le caractère LETTRE MINUSCULE LATINE A "a" (\u0061), quand il se trouve à côté du caractère DIACRITIQUE ROND EN CHEF " + " ̊" (\u030a), est interprété comme une LETTRE MINUSCULE LATINE A AVEC DIACRITIQUE ROND EN CHEF "å" (\u00e5). Comme l’illustre l’exemple suivant, ce comportement diffère de la comparaison ordinale.

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

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

Console.WriteLine($"Equal sort weight of {separated} and {combined} using Ordinal: {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

Lors de l’interprétation des noms de fichiers, des cookies ou tout autre élément où une combinaison telle que « å » peut apparaître, les comparaisons ordinales offrent toujours le comportement le plus transparent et le plus adapté.

En équilibre, la culture invariante a peu de propriétés qui le rendent utile pour la comparaison. Elle fait une comparaison d’une manière linguistiquement pertinente, ce qui l’empêche de garantir une équivalence symbolique complète, mais ce n’est pas le choix pour l’affichage dans n’importe quelle culture. L’une des rares raisons d'utiliser StringComparison.InvariantCulture pour la comparaison est de permettre de conserver les données ordonnées pour un affichage identique à travers les cultures. Par exemple, si un fichier de données volumineux qui contient une liste d’identificateurs triés pour l’affichage accompagne une application, l’ajout à cette liste nécessite une insertion avec un tri de style indifférent.

Choix d’un membre StringComparison pour votre appel de méthode

Le tableau suivant présente le mappage du contexte de chaîne sémantique à un membre d’énumération StringComparison :

Données Comportement Valeur System.StringComparison

valeur
Identificateurs internes respectant la casse.

Identificateurs respectant la casse dans des normes telles que XML et HTTP.

Paramètres liés à la sécurité respectant la casse.
Identificateur non linguistique, où correspondent exactement les octets. Ordinal
Identificateurs internes ne respectant pas la casse.

Identificateurs ne respectant pas la casse dans des normes telles que XML et HTTP.

Chemins d’accès aux fichiers.

Clés et valeurs de Registre.

Variables d'environnement.

Identificateurs de ressource (par exemple, gérer les noms).

Paramètres liés à la sécurité ne respectant pas la casse.
Identificateur non linguistique, où le cas n’est pas pertinent. OrdinalIgnoreCase
Certaines données persistantes, linguistiquesment pertinentes.

Affichage de données linguistiques nécessitant un ordre de tri fixe.
Données indépendantes culturelles qui sont toujours pertinentes sur le plan linguistique. InvariantCulture

- ou -

InvariantCultureIgnoreCase
Données affichées à l’utilisateur.

La plupart des entrées des utilisateurs.
Données nécessitant des coutumes linguistiques locales. CurrentCulture

- ou -

CurrentCultureIgnoreCase

Méthodes de comparaison de chaînes courantes dans .NET

Les sections suivantes décrivent les méthodes les plus couramment utilisées pour la comparaison de chaînes.

String.Compare

Interprétation par défaut : StringComparison.CurrentCulture.

En tant qu'opération la plus centrale de l'interprétation de chaînes, toutes les instances de ces appels de méthode doivent être examinées pour déterminer si les chaînes doivent être interprétées d'après la culture actuelle ou être dissociées de la culture (symboliquement). L'opération appropriée est, en général, la dernière, et une comparaison StringComparison.Ordinal doit être utilisée à la place.

La classe System.Globalization.CompareInfo , retournée par la propriété CultureInfo.CompareInfo , inclut également une méthode Compare qui fournit un grand nombre d'options de correspondance (ordinale, ignorance des espaces blancs, ignorance du type Kana, etc.) au moyen de l'énumération d'indicateur CompareOptions .

String.CompareTo

Interprétation par défaut : StringComparison.CurrentCulture.

Cette méthode n'offre actuellement pas de surcharge spécifiant un type StringComparison . Il est généralement possible de convertir cette méthode en formulaire recommandé String.Compare(String, String, StringComparison) .

Types qui implémentent les interfaces IComparable et IComparable<T> implémentent cette méthode. Étant donné qu’il n’offre pas l’option d’un StringComparison paramètre, l’implémentation de types permet souvent à l’utilisateur de spécifier un StringComparer dans son constructeur. L’exemple suivant définit une FileName classe dont le constructeur de classe inclut un StringComparer paramètre. Cet StringComparer objet est ensuite utilisé dans la FileName.CompareTo méthode.

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

Interprétation par défaut : StringComparison.Ordinal.

La String classe vous permet de tester l’égalité en appelant les surcharges de méthode statique ou d’instance Equals , ou en utilisant l’opérateur d’égalité statique. Par défaut, les surcharges et l'opérateur utilisent la comparaison ordinale. Toutefois, nous vous recommandons quand même d'appeler une surcharge qui spécifie explicitement le type StringComparison , même si vous voulez effectuer une comparaison ordinale ; cela le simplifie la recherche d'une interprétation de chaîne particulière dans du code.

String.ToUpper et String.ToLower

Interprétation par défaut : StringComparison.CurrentCulture.

Soyez prudent lorsque vous utilisez les méthodes String.ToUpper() et String.ToLower(), car le fait de forcer une chaîne à prendre la forme d'une majuscule ou d'une minuscule est souvent utilisé comme une petite normalisation pour comparer des chaînes sans tenir compte de la casse. Si tel est le cas, vous devez envisager d'utiliser une comparaison ne respectant pas la casse.

Les méthodes String.ToUpperInvariant et String.ToLowerInvariant sont également disponibles. ToUpperInvariant est le moyen standard de normaliser la casse. Les comparaisons faites à l'aide de StringComparison.OrdinalIgnoreCase sont, sur le plan comportemental, la composition de deux appels : appel à ToUpperInvariant sur les deux arguments de chaîne, et exécution d'une comparaison à l'aide de StringComparison.Ordinal.

Des surcharges sont également disponibles pour la conversion en majuscules et en minuscules dans une culture spécifique, en passant à la méthode un objet CultureInfo qui représente cette culture.

Char.ToUpper et Char.ToLower

Interprétation par défaut : StringComparison.CurrentCulture.

Les méthodes Char.ToUpper(Char) et Char.ToLower(Char) fonctionnent de la même façon que les méthodes String.ToUpper() et String.ToLower() décrites dans la section précédente.

String.StartsWith et String.EndsWith

Interprétation par défaut : StringComparison.CurrentCulture.

Par défaut, ces deux méthodes effectuent une comparaison sensible à la culture. En particulier, ils peuvent ignorer les caractères non imprimables.

String.IndexOf et String.LastIndexOf

Interprétation par défaut : StringComparison.CurrentCulture.

Il existe un manque de cohérence dans la façon dont les surcharges par défaut de ces méthodes effectuent des comparaisons. Toutes les méthodes String.IndexOf et String.LastIndexOf qui incluent un paramètre Char effectuent une comparaison ordinale, mais les méthodes String.IndexOf et String.LastIndexOf par défaut qui incluent un paramètre String effectuent une comparaison sensible à la culture.

Si vous appelez la méthode String.IndexOf(String) ou String.LastIndexOf(String) et que vous lui passez une chaîne à localiser dans l'instance actuelle, nous vous recommandons d'appeler une surcharge qui spécifie explicitement le type StringComparison . Les surcharges qui incluent un Char argument ne vous permettent pas de spécifier un StringComparison type.

Méthodes qui effectuent indirectement une comparaison de chaînes

Certaines méthodes non-chaînes qui ont une comparaison de chaînes en tant qu’opération centrale utilisent le StringComparer type. La StringComparer classe inclut six propriétés statiques qui retournent StringComparer des instances dont StringComparer.Compare les méthodes effectuent les types de comparaisons de chaînes suivants :

Array.Sort et Array.BinarySearch

Interprétation par défaut : StringComparison.CurrentCulture.

Lorsque vous stockez des données dans une collection ou que vous lisez des données persistantes d’un fichier ou d’une base de données dans une collection, le basculement de la culture actuelle peut invalider les invariants dans la collection. La Array.BinarySearch méthode suppose que les éléments du tableau à rechercher sont déjà triés. Pour trier n’importe quel élément de chaîne dans le tableau, la Array.Sort méthode appelle la String.Compare méthode pour classer des éléments individuels. L’utilisation d’un comparateur sensible à la culture peut être dangereuse si la culture change entre le moment où le tableau est trié et son contenu est recherché. Par exemple, dans le code suivant, le stockage et la récupération fonctionnent sur le comparateur fourni implicitement par la Thread.CurrentThread.CurrentCulture propriété. Si la culture peut changer entre les appels à StoreNames et DoesNameExist, et surtout si le contenu du tableau est conservé à un moment donné entre les deux appels de méthode, la recherche binaire peut échouer.

// 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

Une variante recommandée apparaît dans l’exemple suivant, qui utilise la même méthode de comparaison ordinale (non sensible à la culture) à la fois pour trier et rechercher dans le tableau. Le code de modification est reflété dans les lignes étiquetées Line A et Line B dans les deux exemples.

// 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

Si ces données sont conservées et déplacées entre les cultures, et que le tri est utilisé pour présenter ces données à l’utilisateur, vous pouvez envisager d’utiliser StringComparison.InvariantCulture, ce qui fonctionne linguistiquement pour une meilleure sortie utilisateur, mais n’est pas affecté par les modifications de la culture. L’exemple suivant modifie les deux exemples précédents pour utiliser la culture invariante pour le tri et la recherche dans le tableau.

// 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

Exemple de collection : Constructeur de table de hachage

Les chaînes de hachage fournissent un deuxième exemple d’opération affectée par la façon dont les chaînes sont comparées.

L’exemple suivant instancie un objet Hashtable en lui passant l'objet StringComparer qui est retourné par la propriété StringComparer.OrdinalIgnoreCase. Étant donné qu'une classe StringComparer, dérivée de StringComparer, implémente l'interface IEqualityComparer, sa méthode GetHashCode est utilisée pour calculer le code de hachage des chaînes dans la table de hachage.

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

Voir aussi