Saltar al contenido

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript... y lo que venga ;)

17 años online

el blog de José M. Aguilar

Inicio El autor Contactar

Artículos, tutoriales, trucos, curiosidades, reflexiones y links sobre programación web
ASP.NET Core, MVC, Blazor, SignalR, Entity Framework, C#, Azure, Javascript...

¡Microsoft MVP!
martes, 8 de noviembre de 2022
C#

En código que veo, incluso escrito por mí un tiempo atrás, es muy habitual encontrar comparaciones de cadenas de caracteres en las que, para asegurar que el casing no sea tenido en cuenta, se fuerza una conversión de alguno de los operandos, o incluso ambos, a mayúsculas o minúsculas.

En escenarios muy simples esto funcionará bien y no tendrá contraindicaciones especialmente graves para nuestro sistema. Veamos unos ejemplos:

// Ejemplo 1: conversión de un único operando
int Calculate(string op, int a, int b)
{
    // Pasamos a minúsculas el operador para
    // asegurar que encaja con la constante
    if(op.ToLower()=="add")
    {
        return a+b;
    } 
    else if(op.ToLower()=="sub")
    {
        return a-b;
    }
    ...
}

// Ejemplo  2: conversión de ambos operandos
bool AreBrothers(User user1, User user2)
{
    // Pasamos a mayúsculas ambos apellidos por
    // si alguno se ha escrito usando otro casing
    var areBrothers = user1.Surname.ToUpper() == user2.Surname.ToUpper();
    return areBrothers;
}

Sin embargo, aunque pueda parecer despreciable, estas operaciones de transformación a mayúsculas o minúsculas tienen un coste importante, que se pone especialmente de manifiesto cuando estamos hablando de aplicaciones con mucha carga de usuarios, alojada en infraestructura muy ajustada o cuando se requiere un rendimiento extremo.

La forma correcta de hacer la comparación sería esta:

bool AreBrothers(User user1, User user2)
{
    var areBrothers = user1.Surname.Equals(
            user2.Surname, 
            StringComparison.OrdinalIgnoreCase
    );
}

La diferencia entre ambas soluciones puede verse rápidamente si creamos una aplicación de consola con el siguiente Program.cs y le añadimos el paquete NuGet BenchmarkDotNet, como vimos un tiempo atrás:

using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;

BenchmarkRunner.Run<StringComparisonBenchmark>(); // Go!

[MemoryDiagnoser()]
public class StringComparisonBenchmark
{
    string _str1 = "QWsiEJCAleesJJSKDestoSDFKIERJesCFKWQqueJCPEKestasCLKMCXmuyDAXGKXmuyDIHCDZXatentoFJF";
    string _str2 = "yDXMCVJRdebeDDriasCSDXsKKMpublicarXCKDJCunCFKKFcomentarioCKFparaJKCJdemostrASDFarlo";

    [Benchmark]
    public bool UsingToUpper()
    {
        return _str1.ToUpper() == _str2.ToUpper();
    }

    [Benchmark]
    public bool UsingEqualsOrdinal()
    {
        return _str1.Equals(_str2, StringComparison.OrdinalIgnoreCase);
    }
}

Ejecutando esta aplicación, el resultado del benchmark podremos verlo en unos segundos:

BenchmarkDotNet=v0.13.1, OS=Windows 10.0.22000
Intel Core i9-9900K CPU 3.60GHz (Coffee Lake), 1 CPU, 16 logical and 8 physical cores
.NET SDK=6.0.201
  [Host]     : .NET 6.0.3 (6.0.322.12309), X64 RyuJIT
  DefaultJob : .NET 6.0.3 (6.0.322.12309), X64 RyuJIT

|             Method |       Mean |     Error |    StdDev |  Gen 0 | Allocated |
|------------------- |-----------:|----------:|----------:|-------:|----------:|
| UsingToUpper       | 128.492 ns | 2.4934 ns | 2.3323 ns | 0.0477 |     400 B |
| UsingEqualsOrdinal |   4.251 ns | 0.0206 ns | 0.0182 ns |      - |         - |

Fijaos bien: utilizar Equals() es 30 veces más rápido que la alternativa con ToUpper(), y no requiere ninguna asignación ni reserva de memoria (0 allocations). Una maravilla :)

Observad también que en la llamada a Equals() estamos utilizando el tipo de comparación StringComparison.OrdinalIgnoreCase, que es muy eficiente y recomendable normalmente, pero existen otras opciones cuya idoneidad podéis consultar en la documentación oficial.

Por supuesto, para evitar tener que teclear tanto, podríamos crear un extensor con el objeto de simplificar su uso:

public static class StringExtensions
{
    public static bool EqualsIgnoreCase(this string str1, string str2) 
    {
        return String.Equals(str1, str2, StringComparison.OrdinalIgnoreCase);
    }
}

Y de esta forma, comparar eficientemente ignorando mayúsculas y minúsculas se quedaría en una llamada bastante elegante:

bool AreBrothers(User user1, User user2)
{
    var areBrothers = user1.Surname.EqualsIgnoreCase(user2.Surname);
    return areBrothers;
}

Publicado en Variable not found.

Aún no hay comentarios, ¡sé el primero!