Procedimientos recomendados para la comparación de cadenas en .NET

.NET proporciona una gran compatibilidad para desarrollar aplicaciones localizadas y globalizadas, y simplifica la aplicación de las convenciones de la referencia cultural actual o de una referencia cultural concreta al realizar operaciones comunes como ordenar y mostrar cadenas. Pero ordenar o comparar cadenas no es siempre una operación dependiente de la referencia cultural. Por ejemplo, las cadenas usadas internamente por una aplicación normalmente se deben administrar de forma idéntica en todas las referencias culturales. Cuando los datos de cadenas independientes de la referencia cultural (como etiquetas XML, etiquetas HTML, nombres de usuario, rutas de acceso de archivos y nombres de objetos del sistema) se interpretan como si fueran dependientes de la referencia cultural, el código de aplicación puede estar sujeto a errores imperceptibles, un rendimiento inadecuado y, en algunos casos, a problemas de seguridad.

En este artículo, se examinan los métodos de ordenación, comparación y uso de mayúsculas y minúsculas de cadenas de .NET, se presentan recomendaciones para seleccionar un método adecuado de control de cadenas y se proporciona información adicional sobre los métodos de control de cadenas.

Recomendaciones sobre el uso de cadenas

Cuando desarrolle con .NET, siga estas recomendaciones a la hora de comparar cadenas.

Sugerencia

Hay varios métodos relacionados con cadenas que realizan comparaciones. Algunos ejemplos son String.Equals, String.Compare, String.IndexOf y String.StartsWith.

Evite lo siguiente cuando compare cadenas:

  • No emplee sobrecargas que no especifiquen explícita o implícitamente las reglas de comparación de cadenas para las operaciones de cadena.
  • No use operaciones de cadena basadas en StringComparison.InvariantCulture en la mayoría de los casos. Una de las pocas excepciones es cuando vaya a conservar datos lingüísticamente significativos pero válidos culturalmente.
  • No emplee ninguna sobrecarga del método String.Compare o CompareTo y pruebe si se devuelve un valor cero para determinar si dos cadenas son iguales.

Especificar comparaciones de cadenas explícitamente

La mayoría de los métodos de manipulación de cadenas de .NET están sobrecargados. Normalmente, una o más sobrecargas aceptan la configuración predeterminada, mientras que otras no aceptan ningún valor predeterminado y en su lugar definen la manera precisa en la que se van a comparar o manipular las cadenas. La mayoría de los métodos que no confían en los valores predeterminados incluye un parámetro de tipo StringComparison, que es una enumeración que especifica explícitamente reglas para la comparación de cadenas por referencia cultural y uso de mayúsculas y minúsculas. En la tabla siguiente se describen los miembros de la enumeración StringComparison .

Miembro de StringComparison Descripción
CurrentCulture Realiza una comparación con distinción entre mayúsculas y minúsculas usando la referencia cultural actual.
CurrentCultureIgnoreCase Realiza una comparación sin distinción entre mayúsculas y minúsculas usando la referencia cultural actual.
InvariantCulture Realiza una comparación con distinción entre mayúsculas y minúsculas usando la referencia cultural de todos los idiomas.
InvariantCultureIgnoreCase Realiza una comparación sin distinción entre mayúsculas y minúsculas usando la referencia cultural de todos los idiomas.
Ordinal Realiza una comparación ordinal.
OrdinalIgnoreCase Realiza una comparación ordinal sin distinción entre mayúsculas y minúsculas.

Por ejemplo, el método IndexOf , que devuelve el índice de una subcadena en un objeto String que coincide con un carácter o una cadena, tiene nueve sobrecargas:

Se recomienda seleccionar una sobrecarga que no use valores predeterminados, por las razones siguientes:

  • Algunas sobrecargas con parámetros predeterminados (las que buscan un valor Char en la instancia de la cadena) realizan una comparación ordinal, mientras que otras (las que buscan una cadena en la instancia de la cadena) son dependientes de la referencia cultural. Es difícil recordar qué método usa cada valor predeterminado y resulta fácil confundir las sobrecargas.

  • La intención del código que usa valores predeterminados para las llamadas al método no está clara. En el ejemplo siguiente, en el cual se usan valores predeterminados, es difícil saber si el desarrollador pretendía realizar una comparación ordinal o lingüística de dos cadenas, o si había alguna diferencia al usar mayúsculas y minúsculas entre url.Scheme y "http" que pudiera hacer que la prueba de igualdad devolviera el valor 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 general, se recomienda llamar a un método que no use los valores predeterminados, ya que hace que la intención del código no sea ambigua. Esto, a su vez, hace el código más legible y más fácil de depurar y mantener. En el ejemplo siguiente se abordan las cuestiones que se derivan del ejemplo anterior. Indica claramente que se usa la comparación ordinal y que se omiten las diferencias en cuanto al uso de mayúsculas y minúsculas.

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

Detalles de la comparación de cadenas

La comparación de cadenas es el corazón de muchas operaciones relacionadas con cadenas, especialmente la ordenación y la comprobación de igualdad. Las cadenas se ordenan en un orden determinado: si "mi" aparece antes que "cadena" en una lista ordenada de cadenas, "mi" debe compararse como menor o igual que "cadena". Además, la comparación define la igualdad implícitamente. La operación de comparación devuelve cero para las cadenas que considera iguales. Una buena interpretación es que ninguna cadena es menor que otra. La mayoría de las operaciones significativas que implican cadenas incluyen uno o ambos de estos procedimientos: comparar con otra cadena y ejecutar una operación de ordenación bien definida.

Nota

Puede descargar las tablas de pesos de ordenación, un conjunto de archivos de texto que contienen información sobre los pesos de caracteres que se usan en las operaciones de ordenación y comparación para los sistemas operativos Windows, además de la tabla de elementos de intercalación Unicode predeterminada, que es la última versión de la tabla de pesos de ordenación para Linux y macOS. La versión específica de la tabla de pesos de ordenación en Linux y macOS depende de la versión de las bibliotecas de componentes internacionales de Unicode instaladas en el sistema. Para más información sobre las versiones de los componentes internacionales de Unicode y las versiones de Unicode que implementan, vea la información sobre la descarga de componentes internacionales de Unicode.

Sin embargo, la evaluación de dos cadenas para comprobar su igualdad o su criterio de ordenación no produce ningún resultado correcto único; el resultado depende de los criterios empleados para comparar las cadenas. En especial, las comparaciones de cadenas que son ordinales o que se basan en las convenciones de ordenación y uso de mayúsculas y minúsculas de la referencia cultural actual o de la referencia cultural invariable (una referencia cultural válida para la configuración regional basada en el idioma inglés) pueden producir resultados diferentes.

Además, las comparaciones de cadenas mediante las diferentes versiones de .NET o con .NET en distintos sistemas operativos o versiones de sistema operativo pueden devolver resultados diferentes. Para más información, vea Las cadenas y el estándar Unicode.

Comparaciones de cadenas que usan la referencia cultural actual

Un criterio implica usar las convenciones de la referencia cultural actual a la hora de comparar cadenas. Las comparaciones que se basan en la referencia cultural actual usan la referencia cultural o la configuración regional actual del subproceso. Si el usuario no establece la referencia cultural, el valor predeterminado es la configuración del sistema operativo. Siempre debe usar comparaciones basadas en la referencia cultural actual cuando los datos sean lingüísticamente pertinentes y cuando refleje una interacción con el usuario dependiente de la referencia cultural.

En cambio, el comportamiento de comparación y uso de mayúsculas y minúsculas de .NET cambia cuando la referencia cultural cambia. Esto ocurre cuando una aplicación se ejecuta en un equipo que tiene una referencia cultural diferente que el equipo en el que se desarrolló la aplicación o cuando el subproceso en ejecución cambia su referencia cultural. Este comportamiento es deliberado, pero sigue resultando no obvio para muchos desarrolladores. En el ejemplo siguiente, se muestran las diferencias en el criterio de ordenación entre las referencias culturales de inglés de EE. UU. ("en-US") y sueco ("sv-SE"). Tenga en cuenta que las palabras "ångström", "Windows" y "Visual Studio" aparecen en distintas posiciones en las matrices de cadenas ordenadas.

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

Las comparaciones sin distinción entre mayúsculas y minúsculas que usan la referencia cultural actual son iguales que las comparaciones dependientes de la referencia cultural, excepto que omiten el uso de mayúsculas y minúsculas según indica la referencia cultural actual del subproceso. Este comportamiento también se puede manifestar en los criterios de ordenación.

Las comparaciones que usan semántica de la referencia cultural actual son el valor predeterminado para los métodos siguientes:

En cualquier caso, se recomienda llamar a una sobrecarga que tenga un parámetro StringComparison para aclarar la intención de la llamada al método.

Pueden surgir errores imperceptibles y no tan imperceptibles cuando los datos de cadenas no lingüísticos se interpretan lingüísticamente, o cuando los datos de cadenas de una referencia cultural determinada se interpretan usando las convenciones de otra referencia cultural. El ejemplo canónico es el problema con I en turco.

Para casi todos los alfabetos latinos, incluso en inglés de EE. UU., el carácter "i" (\u0069) es la versión en minúsculas del carácter "I" (\u0049). Esta regla de mayúsculas y minúsculas se convierte rápidamente en el valor predeterminado para alguien que programe en esa referencia cultural. En cambio, el alfabeto turco ("tr-TR") incluye un carácter "I con punto" "İ" (\u0130), que es la versión en mayúsculas de "i". El turco también incluye un carácter "i sin punto" en minúscula, "ı" (\u0131), que en mayúsculas es "I". Este comportamiento también se produce en la referencia cultural de azerbaiyano ("az").

Por tanto, los supuestos sobre poner en mayúsculas "i" o escribir "I" en minúsculas no son válidos en todas las referencias culturales. Si usa las sobrecargas predeterminadas para las rutinas de comparación de cadenas, estarán sujetas a variaciones entre distintas referencias culturales. Si los datos que se van a comparar son no lingüísticos, el uso de las sobrecargas predeterminadas puede generar resultados no deseables, como ilustra el siguiente intento de realizar una comparación sin distinción entre mayúsculas y minúsculas de las cadenas "bill" y "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

Esta comparación podría producir problemas importantes si la referencia cultural se usa involuntariamente en configuraciones que afectan a la seguridad, como en el ejemplo siguiente. Una llamada al método como IsFileURI("file:") devuelve true si la referencia cultural actual es inglés de EE. U.U., pero false si la referencia cultural actual es el turco. Así, en los sistemas turcos, alguien podría sortear las medidas de seguridad que bloquean el acceso a los URI sin distinción entre mayúsculas y minúsculas que comienzan con "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

En este caso, como "file:" se debe interpretar como un identificador no lingüístico e independiente de la referencia cultural, el código se debe escribir como se muestra en el ejemplo siguiente:

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

Operaciones de cadenas ordinales

Al especificar el valor StringComparison.Ordinal o StringComparison.OrdinalIgnoreCase en una llamada al método, se indica una comparación no lingüística en la que se omiten las características de los lenguajes naturales. Los métodos que se invocan con estos valores StringComparison basan las decisiones sobre las operaciones con cadenas en simples comparaciones de bytes en lugar de usos de mayúsculas y minúsculas o tablas de equivalencia parametrizadas por referencia cultural. En la mayoría de los casos, este enfoque se adapta mejor a la interpretación prevista de cadenas, y el código es más rápido y más confiable.

Las comparaciones ordinales son comparaciones de cadenas en las que cada byte de cada cadena se compara sin ninguna interpretación lingüística; por ejemplo, "windows" no coincide con "Windows". Esta es, esencialmente, una llamada a la función strcmp en tiempo de ejecución de C. Use esta comparación cuando el contexto indique que las cadenas deben coincidir exactamente o exija una directiva de coincidencia conservadora. Además, la comparación ordinal es la operación de comparación más rápida porque no aplica ninguna regla lingüística al determinar un resultado.

Las cadenas de .NET pueden contener caracteres nulos insertados (y otros caracteres no imprimibles). Una de las diferencias más claras entre la comparación ordinal y dependiente de la referencia cultural (incluyendo las comparaciones que usan la referencia cultural de todos los idiomas) tiene que ver con el control de caracteres nulos incrustados en una cadena. Estos caracteres se omiten cuando usa métodos String.Compare y String.Equals para realizar comparaciones dependientes de la referencia cultural (incluyendo las comparaciones que usan la referencia cultural de todos los idiomas). Por tanto, las cadenas que contienen caracteres nulos insertados pueden considerarse iguales que las cadenas que no los contienen. Es posible que se omitan caracteres no imprimibles insertados para los métodos de comparación de cadenas, como String.StartsWith.

Importante

Aunque los métodos de comparación de cadenas hacen caso omiso de los caracteres nulos incrustados, los métodos de búsqueda de cadenas como String.Contains, String.EndsWith, String.IndexOf, String.LastIndexOfy String.StartsWith sí los tienen en cuenta.

En el ejemplo siguiente se realiza una comparación dependiente de la referencia cultural de la cadena "Aa" con una cadena similar que contiene varios caracteres nulos insertados entre "A" y "a", y se muestra cómo las dos cadenas se consideran iguales:

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

Pero las cadenas no se consideran iguales cuando se usa la comparación ordinal, como se muestra en el ejemplo siguiente:

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

Las comparaciones ordinales sin distinción entre mayúsculas y minúsculas son el siguiente enfoque más conservador. Estas comparaciones omiten la mayor parte del uso de mayúsculas y minúsculas; por ejemplo, "windows" coincide con "Windows". A la hora de tratar con caracteres ASCII, esta directiva es equivalente a StringComparison.Ordinal, salvo que omite el uso de mayúsculas y minúsculas habitual de ASCII. Por tanto, cualquier carácter de [A, Z] (\u0041-\u005A) coincide con el carácter correspondiente de [a, z] (\u0061-\007A). El uso de mayúsculas y minúsculas fuera del intervalo ASCII emplea las tablas de la referencia cultural de todos los idiomas. Por tanto, la siguiente comparación:

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

es equivalente a esta comparación (pero más rápida):

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

Estas comparaciones siguen siendo muy rápidas.

Tanto StringComparison.Ordinal como StringComparison.OrdinalIgnoreCase usan los valores binarios directamente y son más adecuados para la búsqueda de coincidencias. Si no sabe con seguridad qué configuración de comparación debe emplear, use uno de estos dos valores. Sin embargo, puesto que realizan una comparación byte a byte, no ordenan según un criterio de ordenación lingüístico (como un diccionario de inglés) sino según un criterio de ordenación binario. Los resultados pueden parecer extraños en la mayoría de los contextos si se muestran a los usuarios.

La semántica ordinal es el valor predeterminado para las sobrecargas de String.Equals que no incluyen un argumento StringComparison (incluyendo el operador de igualdad). En cualquier caso, se recomienda llamar a una sobrecarga que tenga un parámetro StringComparison .

Operaciones de cadenas que usan la referencia cultural de todos los idiomas

Las comparaciones con la referencia cultural de todos los idiomas usan la propiedad CompareInfo devuelta por la propiedad estática CultureInfo.InvariantCulture . Este comportamiento es igual en todos los sistemas; traduce cualquier carácter que esté fuera de su intervalo en lo que cree que son caracteres invariables equivalentes. Esta directiva puede ser útil para mantener un conjunto de comportamientos de las cadenas en distintas referencias culturales, pero a menudo proporciona resultados inesperados.

Las comparaciones sin distinción entre mayúsculas y minúsculas con la referencia cultural de todos los idiomas usan también la propiedad estática CompareInfo devuelta por la propiedad estática CultureInfo.InvariantCulture para obtener información de comparación. Cualquier diferencia en el uso de mayúsculas y minúsculas entre estos caracteres traducidos se pasa por alto.

Las comparaciones que usan StringComparison.InvariantCulture y StringComparison.Ordinal funcionan de manera idéntica en cadenas ASCII. Sin embargo, StringComparison.InvariantCulture toma decisiones lingüísticas que podrían no ser adecuadas para las cadenas que tienen que interpretarse como un conjunto de bytes. El objeto CultureInfo.InvariantCulture.CompareInfo hace que el método Compare interprete ciertos conjuntos de caracteres como equivalentes. Por ejemplo, la siguiente equivalencia es válida en la referencia cultural de todos los idiomas:

InvariantCulture: a + ̊ = å

El carácter LETRA LATINA A MINÚSCULA "a" (\u0061), cuando está junto al carácter ANILLO SUPERIOR COMBINABLE "+ " ̊" (\u030a), se interpreta como el carácter LETRA LATINA MINÚSCULA A CON ANILLO SUPERIOR "å" (\u00e5). Como se muestra en el ejemplo siguiente, este comportamiento difiere de la comparación ordinal.

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

A la hora de interpretar nombres de archivo, cookies u otros elementos donde pueda aparecer una combinación como "å", las comparaciones ordinales siguen ofreciendo el comportamiento más transparente y adecuado.

En conjunto, la referencia cultural de todos los idiomas tiene pocas propiedades que la hagan útil para la comparación. Realiza la comparación de manera lingüísticamente pertinente, lo que le impide garantizar una equivalencia simbólica completa, pero no es la opción ideal para la presentación en cualquier referencia cultural. Una de las pocas razones para usar StringComparison.InvariantCulture con el fin de realizar una comparación es para conservar datos ordenados cuando se desea realizar una presentación idéntica transculturalmente. Por ejemplo, si un archivo de datos grande que contiene una lista de identificadores ordenados para su presentación acompaña una aplicación, al agregar datos a esta lista se necesitaría realizar una inserción con ordenación de estilo invariable.

Elegir un miembro StringComparison para la llamada al método

En la tabla siguiente se describe la asignación del contexto de cadena semántico a un miembro de la enumeración StringComparison:

Datos Comportamiento Valor de System.StringComparison

value
Identificadores internos con distinción entre mayúsculas y minúsculas.

Identificadores con distinción entre mayúsculas y minúsculas en estándares como XML y HTTP.

Configuraciones relacionadas con la seguridad con distinción entre mayúsculas y minúsculas.
Identificador no lingüístico, donde los bytes coinciden exactamente. Ordinal
Identificadores internos sin distinción entre mayúsculas y minúsculas.

Identificadores sin distinción entre mayúsculas y minúsculas en estándares como XML y HTTP.

Rutas de acceso a archivos.

Claves del Registro y valores.

Variables de entorno.

Identificadores de recursos (por ejemplo, nombres de identificadores).

Configuraciones relacionadas con la seguridad sin distinción entre mayúsculas y minúsculas.
Un identificador no lingüístico, donde el uso de mayúsculas y minúsculas no es pertinente. OrdinalIgnoreCase
Algunos datos almacenados lingüísticamente pertinentes.

Presentación de datos lingüísticos que necesitan un criterio de ordenación fijo.
Datos válidos culturalmente que siguen siendo lingüísticamente pertinentes. InvariantCulture

o bien

InvariantCultureIgnoreCase
Datos mostrados al usuario.

La mayoría de los datos proporcionados por el usuario.
Datos que necesitan personalizaciones lingüísticas locales. CurrentCulture

o bien

CurrentCultureIgnoreCase

Métodos comunes de comparación de cadenas en .NET

En las secciones siguientes se describen los métodos que se usan con más frecuencia para la comparación de cadenas.

String.Compare

Interpretación predeterminada: StringComparison.CurrentCulture.

Al ser la operación fundamental para la interpretación de cadenas, todas las instancias de estas llamadas al método se deben examinar para determinar si las cadenas se deben interpretar según la referencia cultural actual o se deben separar de la referencia cultural (simbólicamente). Normalmente, se trata del último caso y se debe usar en su lugar una comparación StringComparison.Ordinal.

La clase System.Globalization.CompareInfo , devuelta por la propiedad CultureInfo.CompareInfo , también incluye un método Compare que proporciona un gran número de opciones de coincidencia (ordinal, omitir el espacio en blanco, omitir el tipo de kana, etc.) por medio de la enumeración de marca CompareOptions .

String.CompareTo

Interpretación predeterminada: StringComparison.CurrentCulture.

Este método no ofrece actualmente una sobrecarga que especifique un tipo StringComparison. Normalmente es posible convertir este método en el formato recomendado del método String.Compare(String, String, StringComparison).

Los tipos que implementan interfaces IComparable y IComparable<T> implementan este método. Puesto que no ofrece la opción de un parámetro StringComparison, la implementación de tipos suele permitir al usuario especificar StringComparer en su constructor. En el ejemplo siguiente se define una clase FileName cuyo constructor de clase incluye un parámetro StringComparer . Este objeto StringComparer se usa entonces en el método 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

Interpretación predeterminada: StringComparison.Ordinal.

La clase String le permite comprobar la igualdad llamando a las sobrecargas de método estático o de instancia Equals , o usando el operador de igualdad estático. Las sobrecargas y el operador usan la comparación ordinal de forma predeterminada. Sin embargo, todavía sigue siendo recomendable llamar a una sobrecarga que especifique explícitamente el tipo StringComparison aunque desee realizar una comparación ordinal; esto facilita la búsqueda de cierta interpretación de la cadena en el código.

String.ToUpper y String.ToLower

Interpretación predeterminada: StringComparison.CurrentCulture.

Debe tener cuidado al usar estos métodos String.ToUpper() y String.ToLower(), ya que forzar que una cadena esté en mayúsculas o en minúsculas se usa a menudo como una pequeña normalización para comparar cadenas independientemente del uso de mayúsculas y minúsculas. En tal caso, considere la posibilidad de emplear una comparación sin distinción entre mayúsculas y minúsculas.

También están disponibles los métodos String.ToUpperInvariant y String.ToLowerInvariant . ToUpperInvariant es la manera estándar de normalizar el uso de mayúsculas y minúsculas. Las comparaciones realizadas mediante StringComparison.OrdinalIgnoreCase tienen un comportamiento que es la composición de dos llamadas: llamar a ToUpperInvariant en ambos argumentos de cadena y realizar una comparación mediante StringComparison.Ordinal.

También hay sobrecargas para convertir a mayúsculas y minúsculas en una referencia cultural concreta, pasando al método un objeto CultureInfo que representa esa referencia cultural.

Char.ToUpper y Char.ToLower

Interpretación predeterminada: StringComparison.CurrentCulture.

Los métodos Char.ToUpper(Char) y Char.ToLower(Char) funcionan de manera similar a los métodos String.ToUpper() y String.ToLower() descritos en la sección anterior.

String.StartsWith y String.EndsWith

Interpretación predeterminada: StringComparison.CurrentCulture.

De forma predeterminada, estos dos métodos realizan una comparación dependiente de la referencia cultural. En concreto, puede que omitan los caracteres no imprimibles.

String.IndexOf y String.LastIndexOf

Interpretación predeterminada: StringComparison.CurrentCulture.

No hay coherencia en cómo las sobrecargas predeterminadas de estos métodos realizan las comparaciones. Todos los métodos String.IndexOf y String.LastIndexOf que incluyen un parámetro Char realizan una comparación ordinal, pero los métodos String.IndexOf y String.LastIndexOf predeterminados que incluyen un parámetro String realizan una comparación dependiente de la referencia cultural.

Si llama al método String.IndexOf(String) o String.LastIndexOf(String) y le pasa una cadena para ubicar en la instancia actual, se recomienda llamar a una sobrecarga que especifique explícitamente el tipo StringComparison . Las sobrecargas que incluyen un argumento Char no le permiten especificar un tipo StringComparison.

Métodos que realizan la comparación de cadenas indirectamente

Algunos métodos sin cadenas que tienen la comparación de cadenas como operación fundamental usan el tipo StringComparer . La clase StringComparer incluye seis propiedades estáticas que devuelven instancias de StringComparer cuyos métodos StringComparer.Compare realizan los siguientes tipos de comparaciones de cadenas:

Array.Sort y Array.BinarySearch

Interpretación predeterminada: StringComparison.CurrentCulture.

Cuando se almacenan datos en una colección, o cuando se leen datos almacenados de un archivo o una base de datos en una colección, el cambio de la referencia cultural actual puede invalidar los valores invariables de la colección. El método Array.BinarySearch supone que los elementos de la matriz que se van a buscar ya están ordenados. Para ordenar cualquier elemento de cadena de la matriz, el método Array.Sort llama al método String.Compare para ordenar los elementos individuales. El uso de un comparador dependiente de la referencia cultural puede ser peligroso si la referencia cultural cambia desde que se ordena la matriz hasta que se busca en su contenido. Por ejemplo, en el código siguiente, el almacenamiento y la recuperación funcionan en el comparador que la propiedad Thread.CurrentThread.CurrentCulture . Si la referencia cultural puede cambiar entre las llamadas a StoreNames y DoesNameExist, y especialmente si el contenido de la matriz se conserva en alguna parte entre las dos llamadas al método, se puede producir un error en la búsqueda binaria.

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

Aparece una variación recomendada en el ejemplo siguiente, que usa el mismo método de comparación ordinal (independiente de la referencia cultural) para ordenar y buscar en la matriz. El código cambiado se refleja en las líneas etiquetadas como Line A y Line B en los dos ejemplos.

// 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 estos datos se conservan y mueven entre distintas referencias culturales, y se usa la ordenación para presentar estos datos al usuario, es mejor usar StringComparison.InvariantCulture, que funciona lingüísticamente para obtener una mejor salida para el usuario pero no se ve afectado por los cambios en la referencia cultural. En el ejemplo siguiente se modifican los dos ejemplos anteriores para usar la referencia cultural de todos los idiomas con el fin de ordenar y buscar en la matriz.

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

Ejemplo de colecciones: Constructor de tablas hash

Al aplicar un algoritmo hash a las cadenas se proporciona un segundo ejemplo de una operación que se ve afectada por la forma en que se comparan las cadenas.

En el ejemplo siguiente se crea una instancia de un objeto Hashtable pasándole el objeto StringComparer devuelto por la propiedad StringComparer.OrdinalIgnoreCase . Puesto que una clase StringComparer que se deriva de StringComparer implementa la interfaz IEqualityComparer , su método GetHashCode se usa para calcular el código hash de cadenas de la tabla hash.

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

Vea también