Meilleures pratiques pour la comparaison de chaînes dans .NET

.NET offre une prise en charge complète 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 et la comparaison de chaînes ne constituent 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 la même manière dans toutes les cultures. Quand des données de type chaîne culturellement indépendantes, telles que des balises XML, des balises HTML, des noms d'utilisateurs, des chemins d'accès aux fichiers et des noms d'objets système, sont interprétées comme si elles étaient dépendantes de la culture, le code d'application peut faire l'objet de bogues subtils, de performances médiocres et, dans certains cas, de problèmes de sécurité.

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

Recommandations relatives à l’utilisation de chaînes

Dans le cadre de votre développement avec .NET, suivez ces recommandations ci-après quand vous comparez des chaînes.

Conseil

Diverses méthodes liées aux chaînes effectuent une comparaison. Exemples : String.Equals, String.Compare, String.IndexOf, et String.StartsWith.

Quand vous comparez des chaînes, évitez les pratiques suivantes :

  • N’utilisez pas de surcharges qui ne spécifient pas explicitement ni implicitement les règles de comparaison dans les opérations de chaînes.
  • Dans la plupart des cas, n’utilisez pas d’opérations de chaînes basées sur StringComparison.InvariantCulture. L’une des rares exceptions est la persistance des données significatives sur le plan linguistique, mais agnostiques sur le plan culturel.
  • N’utilisez pas de surcharge de la méthode String.Compare ni de la méthode CompareTo. Testez une valeur de retour de zéro pour déterminer si deux chaînes sont égales.

Spécification explicite de comparaisons de chaînes

La plupart des méthodes de manipulation de chaînes dans .NET sont surchargées. Une ou plusieurs surcharges acceptent généralement des paramètres par défaut, contrairement à d'autres qui définissent avec précision la façon dont les chaînes doivent être comparées ou manipulées. La plupart des méthodes qui ne s’appuient pas sur des valeurs par défaut incluent un paramètre de type StringComparison. Il s’agit d’une énumération spécifiant explicitement des règles pour la comparaison de chaînes selon la culture et la casse. Le tableau suivant décrit les membres de l'énumération StringComparison .

Membre StringComparison Description
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 méthode IndexOf , qui retourne l'index d'une sous-chaîne contenue dans un objet String correspondant à un caractère ou à une chaîne, a neuf surcharges :

Nous vous recommandons de sélectionner une surcharge qui n’utilise pas de 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 mémoriser quelle méthode utilise quelle valeur par défaut. Les surcharges peuvent être facilement confondues.

  • L’objectif du code qui s’appuie sur des valeurs par défaut pour les appels de méthode n’est pas clair. Dans l’exemple suivant, qui utilise 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 donner un test d’égalité 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 des valeurs par défaut afin de rendre rend l’objectif du code non équivoque. Cela rend ensuite le code plus lisible et plus facile à déboguer et à gérer. L'exemple suivant aborde les questions soulevées à propos de l'exemple précédent. Il explique que la comparaison ordinale est utilisée et que les différences de casse 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 "my" s'affiche avant "string" dans une liste triée de chaînes, "my" doit être considéré comme inférieur ou égal à "string". En outre, la comparaison définit implicitement l'égalité. L'opération de comparaison retourne zéro pour les chaînes qu'il estime égales. Considérer qu'aucune chaîne n'est inférieure à l'autre constitue une bonne interprétation. La plupart des opérations significatives impliquant des chaînes incluent l'une des procédures suivantes, ou les deux : comparaison avec une autre chaîne et exécution d'une opération de tri bien définie.

Notes

Vous pouvez télécharger les Sorting Weight Tables, un ensemble de fichiers texte qui contiennent des informations sur les poids des caractères utilisés dans les opérations de tri et de comparaison pour les systèmes d’exploitation Windows et la Default Unicode Collation Element Table, la version la plus récente 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 varie selon la version des bibliothèques International Components for Unicode installées sur le système. Pour plus d’informations sur les versions ICU et les versions Unicode qu’elles implémentent, consultez Téléchargement d’ICU.

Toutefois, l’évaluation de l’égalité ou de l’ordre de tri de deux chaînes ne produit pas un résultat correct unique. Le résultat dépend en effet 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 à l’aide de différentes versions de .NET ou à l’aide de .NET sur différents systèmes d’exploitation ou des versions différentes 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 prend par défaut le paramètre du système d’exploitation. Vous devez toujours utiliser des comparaisons basées sur la culture actuelle quand les données sont linguistiquement pertinentes et quand elles reflètent l'intervention de l'utilisateur dépendante de la culture.

Toutefois, le comportement de la comparaison et de la casse dans .NET change en fonction de la culture. Cela se produit quand une application s'exécute sur un ordinateur dont la culture est différente de celle de l'ordinateur sur lequel l'application a été développée, ou quand le thread en cours d'exécution change sa culture. Ce comportement est intentionnel, mais il reste peu évident à de nombreux développeurs. L'exemple suivant illustre les différences au niveau de l'ordre de tri entre les cultures Anglais (États-Unis) ("en-US") et Suédois ("sv-SE"). Notez que les mots « ångström », « Windows » et « Visual Studio » s’affichent à des positions différentes dans les tableaux de chaînes triées.

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 la 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.

Des bogues, plus ou moins subtils, peuvent émerger quand des données de type chaîne non linguistiques sont interprétées linguistiquement, ou quand des données de type 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 du caractère I en turc.

Dans presque tous les alphabets latins, y compris l'anglais (États-Unis), le caractère "i" (\u0069) est la version en 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") inclut un caractère "I avec un point", "İ" (\u0130), qui est la version en majuscule de "i". Le turc comprend également un caractère "i sans point" minuscule, "ı" (\u0131), dont la majuscule est "I". Ce comportement se produit également dans la culture azérie ("az").

Par conséquent, les suppositions formulées sur la mise en majuscule du « i » ou 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 varieront suivant les cultures. Si les données à comparer sont d’ordre non linguistique, le recours aux 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 provoquer de sérieux problèmes si la culture est utilisée par inadvertance dans des paramètres de sécurité sensibles, comme dans l'exemple suivant. Un appel de méthode comme IsFileURI("file:") retourne true si la culture actuelle est Anglais (États-Unis), mais false si la culture actuelle est Turc. 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: » doit être interprété comme un identificateur non linguistique indépendant de la culture, il convient plutôt d’écrire le code comme 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înes ordinales

La spécification de la valeur StringComparison.Ordinal ou StringComparison.OrdinalIgnoreCase dans un appel de méthode correspond à une comparaison non linguistique dans laquelle les fonctionnalités de langages naturels 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 correspond le mieux à l'interprétation de chaînes prévue tout en rendant le code plus rapide et plus fiable.

Les comparaisons ordinales désignent 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 fondamentalement d’un appel à la fonction strcmp du runtime 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 évidentes entre la comparaison ordinale et la comparaison dépendante de la culture (y compris les comparaisons qui utilisent la culture dite indifférente) concerne la gestion des caractères Null incorporés dans une chaîne. Ces caractères sont ignorés quand vous utilisez les méthodes String.Compare et String.Equals pour effectuer des comparaisons dépendantes de la culture (notamment des comparaisons qui utilisent la culture dite indifférente). Par conséquent, les chaînes qui contiennent des caractères Null incorporés peuvent être considérées comme égales à des chaînes qui n’en contiennent 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.

Important

Les méthodes de comparaison de chaînes ignorent les caractères Null incorporés, contrairement aux méthodes de recherche de chaînes telles que String.Contains, String.EndsWith, String.IndexOf, String.LastIndexOfet String.StartsWith .

L’exemple suivant effectue une comparaison dépendante de la culture de la chaîne « Aa » avec une chaîne semblable qui contient plusieurs caractères Null incorporés entre « A » et « a », et montre que 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 avec une comparaison ordinale, comme le montre 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 de la plage [A, Z] (\u0041-\u005A) correspond au caractère correspondant de la plage [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)

est équivalente à la comparaison suivante (mais plus rapide) :

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

Ces comparaisons restent très rapides.

StringComparison.Ordinal et StringComparison.OrdinalIgnoreCase utilisent tous les deux directement les valeurs binaires et sont les plus adaptés à la mise en correspondance. Si vous ne savez pas quels paramètres de comparaison choisir, utilisez l’une de ces deux valeurs. Toutefois, étant donné qu’elles effectuent une comparaison octet par octet, elles n’appliquent pas un ordre de tri linguistique (comme un dictionnaire français), mais 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 constitue la valeur par défaut des 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 dite indifférente

Les comparaisons avec la culture dite indifférente utilisent la propriété CompareInfo retournée par la propriété CultureInfo.InvariantCulture statique. 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 suppose être des caractères invariants équivalents. Cette stratégie peut être utile pour la gestion d'un jeu de comportements de chaîne dans toutes les cultures, mais elle donne 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 casse entre ces caractères traduits sont ignorées.

Les comparaisons qui utilisent StringComparison.InvariantCulture et StringComparison.Ordinal fonctionnent de la même manière sur les 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 jeu d'octets. L’objet CultureInfo.InvariantCulture.CompareInfo entraîne l’interprétation par la méthode Compare de certains jeux de caractères comme étant équivalents. Par exemple, l'équivalence suivante est valide dans la culture dite indifférente :

InvariantCulture: a + ̊ = å

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

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

Lors de l’interprétation de noms de fichiers, de cookies ou de tout autre élément dans lequel peut s’afficher une combinaison telle que "å", les comparaisons ordinales offrent toujours le comportement le plus transparent et le plus approprié.

En définitive, la culture invariante comprend peu de propriétés qui la rendent utile pour la comparaison. Elle effectue la comparaison d’une manière linguistiquement pertinente, ce qui l’empêche de garantir une équivalence symbolique complète, mais elle ne constitue dans aucune culture le choix approprié pour l’affichage. L'une des rares raisons justifiant l'utilisation de StringComparison.InvariantCulture pour la comparaison est de rendre persistantes des données classées pour un affichage identique dans toutes les cultures. Par exemple, si un fichier de données volumineux qui contient une liste d'identificateurs triés à des fins d'affichage accompagne une application, un ajout à cette liste nécessiterait une insertion avec un tri de style invariant.

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

Le tableau suivant décrit le mappage de contexte de chaîne sémantique avec 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ù les octets correspondent exactement. 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, noms de handles).

Paramètres liés à la sécurité ne respectant pas la casse.
Identificateur non linguistique, où la casse est sans importance. OrdinalIgnoreCase
Certaines données rendues persistantes et linguistiquement pertinentes.

Affichage de données linguistiques qui nécessitent un ordre de tri fixe.
Données dont la culture n'est pas spécifiée qui sont toutefois linguistiquement pertinentes. InvariantCulture

-ou-

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

La plupart des entrées d'utilisateur.
Données qui nécessitent des usages linguistiques locaux. CurrentCulture

-ou-

CurrentCultureIgnoreCase

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

Les sections suivantes décrivent les méthodes le plus souvent 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). Il s’agit en général du deuxième cas de figure. Il convient alors plutôt d’utiliser une comparaison StringComparison.Ordinal.

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 dans la forme String.Compare(String, String, StringComparison) recommandée.

Les types qui implémentent les interfaces IComparable et IComparable<T> implémentent cette méthode. Étant donné qu’elle n’offre pas la possibilité d’un paramètre StringComparison, les types permettent souvent à l’utilisateur de spécifier un StringComparer dans le constructeur. L'exemple suivant définit une classe FileName dont le constructeur de classe inclut un paramètre StringComparer . Cet objet StringComparer est ensuite utilisé dans la méthode 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

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

La classe String vous permet de tester l'égalité en appelant les surcharges de méthode Equals statique ou d'instance, 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.

Faites preuve de prudence quand vous utilisez les méthodes String.ToUpper() et String.ToLower(), car le fait d’imposer une majuscule ou une minuscule dans une chaîne est souvent utilisé à titre de petite normalisation pour la comparaison de chaînes indépendamment 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 dépendante de la culture. Elles peuvent notamment ignorer les caractères non imprimables.

String.IndexOf et String.LastIndexOf

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

La façon dont les surcharges par défaut de ces méthodes effectuent les comparaisons n’est pas cohérente. 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 dépendante de 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 argument Char ne permettent pas de spécifier un type StringComparison.

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

Certaines méthodes autres que les méthodes de chaîne dont l'opération centrale est la comparaison de chaînes utilisent le type StringComparer . La classe StringComparer inclut six propriétés statiques qui retournent des instances de StringComparer dont les méthodes StringComparer.Compare effectuent les types de comparaisons de chaînes suivants :

Array.Sort et Array.BinarySearch

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

Quand vous stockez des données dans une collection ou quand vous lisez des données persistantes à partir d’un fichier ou d’une base de données dans une collection, le changement de culture actuelle peut invalider les invariants de la collection. La méthode Array.BinarySearch suppose que les éléments du tableau dans lequel effectuer la recherche sont déjà triés. Pour trier tout élément de chaîne dans le tableau, la méthode Array.Sort appelle la méthode String.Compare pour classer les éléments individuels. L'utilisation d'un comparateur dépendant de la culture peut s'avérer dangereux si la culture change entre le moment où le tableau est trié et le moment où son contenu fait l'objet d'une recherche. Par exemple, dans le code suivant, le stockage et la récupération fonctionnent sur le comparateur fourni implicitement par la propriété Thread.CurrentThread.CurrentCulture statique. Si la culture peut changer entre les appels à StoreNames et DoesNameExist, et surtout si le contenu du tableau est rendu persistant à 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

L'exemple suivant, qui utilise la même méthode de comparaison ordinale (indépendante de la culture) pour trier le tableau et pour effectuer une recherche, présente une variation recommandée. Le code de modification est reflété dans les lignes libellé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 rendues persistantes et déplacées dans toutes les cultures, et que le tri est utilisé pour présenter ces données à l'utilisateur, vous pouvez envisager d'utiliser StringComparison.InvariantCulture, qui fonctionne linguistiquement pour une meilleure sortie d'utilisateur mais n'est pas affecté par les modifications apportées à la culture. L'exemple suivant modifie les deux exemples précédents pour utiliser la culture dite indifférente pour le tri du tableau et la recherche dans celui-ci.

// 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 collections : constructeur Hashtable

Le hachage de chaînes constitue un deuxième exemple d'opération qui est affectée par la façon dont des 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 de 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