struct con un valor predeterminado sin sentido

12

En mi sistema, frecuentemente uso los códigos de aeropuerto ( "YYZ" , "LAX" , "SFO" , etc.), siempre están en el mismo formato (3 letras, representado en mayúsculas). El sistema normalmente trata con 25-50 de estos códigos (diferentes) por solicitud de API, con un total de más de mil asignaciones, se transmiten a través de muchas capas de nuestra aplicación y se comparan con la igualdad con bastante frecuencia.

Comenzamos con solo pasar cadenas, lo cual funcionó bien un poco, pero rápidamente notamos muchos errores de programación al pasar un código incorrecto en algún lugar donde se esperaba el código de 3 dígitos. También nos topamos con problemas en los que se suponía que debíamos hacer una comparación que no distinguía entre mayúsculas y minúsculas y, en su lugar, no lo hicimos, lo que resultó en errores.

A partir de esto, decidí dejar de pasar cadenas y crear una clase Airport , que tiene un único constructor que toma y valida el código del aeropuerto.

public sealed class Airport
{
    public Airport(string code)
    {
        if (code == null)
        {
            throw new ArgumentNullException(nameof(code));
        }

        if (code.Length != 3 || !char.IsLetter(code[0]) 
        || !char.IsLetter(code[1]) || !char.IsLetter(code[2]))
        {
            throw new ArgumentException(
                "Must be a 3 letter airport code.", 
                nameof(code));
        }

        Code = code.ToUpperInvariant();
    }

    public string Code { get; }

    public override string ToString()
    {
        return Code;
    }

    private bool Equals(Airport other)
    {
        return string.Equals(Code, other.Code);
    }

    public override bool Equals(object obj)
    {
        return obj is Airport airport && Equals(airport);
    }

    public override int GetHashCode()
    {
        return Code?.GetHashCode() ?? 0;
    }

    public static bool operator ==(Airport left, Airport right)
    {
        return Equals(left, right);
    }

    public static bool operator !=(Airport left, Airport right)
    {
        return !Equals(left, right);
    }
}

Esto hizo que nuestro código fuera mucho más fácil de entender y simplificamos nuestras verificaciones de igualdad, diccionario / uso de conjuntos. Ahora sabemos que si nuestros métodos aceptan una instancia Airport que se comportará de la forma que esperamos, ha simplificado nuestras verificaciones de métodos a una verificación de referencia nula.

Lo que sí noté, sin embargo, fue que la recolección de basura se ejecutaba mucho más a menudo, lo que localicé en muchas instancias de Airport que se recolectaba.

Mi solución a esto fue convertir class en struct . En su mayoría fue solo un cambio de palabra clave, con la excepción de GetHashCode y ToString :

public override string ToString()
{
    return Code ?? string.Empty;
}

public override int GetHashCode()
{
    return Code?.GetHashCode() ?? 0;
}

Para manejar el caso donde se usa default(Airport) .

Mis preguntas:

  1. ¿Estaba creando una clase o estructura Airport una buena solución en general, o estoy resolviendo el problema incorrecto / resolviéndolo de manera incorrecta al crear el tipo? Si no es una buena solución, ¿cuál es una mejor solución?

  2. ¿Cómo debería mi aplicación manejar las instancias en las que se usa default(Airport) ? Un tipo de default(Airport) no tiene sentido para mi aplicación, por lo que he estado haciendo if (airport == default(Airport) { throw ... } en lugares donde obtener una instancia de Airport (y su propiedad Code ) es fundamental para la operación.

Notas: Revisé las preguntas C # / VB struct: ¿cómo evitar el caso con cero valores predeterminados, que se considera no válido para la estructura dada? , y use struct o no antes de hacer mi pregunta, sin embargo, creo que mis preguntas son lo suficientemente diferentes como para justificar su propia publicación.

    
pregunta Matthew 10.09.2018 - 19:36

4 respuestas

6

Actualización: Reescribí mi respuesta para abordar algunas suposiciones incorrectas sobre las estructuras de C #, así como el OP que nos informa en los comentarios que se están utilizando cadenas internadas.

Si puede controlar los datos que ingresan a su sistema, use una clase como publicó en su pregunta. Si alguien ejecuta default(Airport) obtendrá un valor de null . Asegúrese de escribir su método privado Equals para devolver falso cada vez que compare objetos nulos de Airport, y luego deje que NullReferenceException vuele en otra parte del código.

Sin embargo, si está llevando datos al sistema desde fuentes que no controla, no es necesario bloquear todo el hilo. En este caso, una estructura es ideal para el simple hecho de que default(Airport) le dará algo más que un puntero null . Cree un valor obvio para representar "ningún valor" o el "valor predeterminado" para que tenga algo que imprimir en la pantalla o en un archivo de registro (como "---" por ejemplo). De hecho, solo mantendría el code privado y no expondría una propiedad Code en absoluto, solo me centraré en el comportamiento aquí.

public struct Airport
{
    private string code;

    public Airport(string code)
    {
        // Check 'code' for validity, throw exceptions if not valid

        this.code = code;
    }

    public override string ToString()
    {
        return code ?? (code = "---");
    }

    // int GetHashcode()

    // bool Equals(...)

    // bool operator ==(...)

    // bool operator !=(...)

    private bool Equals(Airport other)
    {
        if (other == null)
            // Even if this method is private, guard against null pointers
            return false;

        if (ToString() == "---" || other.ToString() == "---")
            // "Default" values should never match anything, even themselves
            return false;

        // Do a case insensitive comparison to enforce logic that airport
        // codes are not case sensitive
        return string.Equals(
            ToString(),
            other.ToString(),
            StringComparison.InvariantCultureIgnoreCase);
    }
}

En el peor de los casos, la conversión de default(Airport) en una cadena imprime "---" y devuelve false cuando se compara con otros códigos de aeropuerto válidos. Cualquier código de aeropuerto "predeterminado" no coincide con nada, incluidos otros códigos de aeropuerto predeterminados.

Sí, las estructuras deben ser valores asignados en la pila, y cualquier puntero para acumular memoria básicamente niega las ventajas de rendimiento de las estructuras, pero en este caso el valor predeterminado de una estructura tiene un significado y proporciona cierta resistencia adicional a la bala. resto de la aplicación.

Doblaría un poco las reglas aquí, por eso.

Respuesta original (con algunos errores de hecho)

Si puede controlar los datos que llegan a su sistema, haría lo que Robert Harvey sugirió en los comentarios: cree un constructor sin parámetros y lance una excepción cuando se llame. Esto evita que los datos no válidos ingresen al sistema a través de default(Airport) .

public Airport()
{
    throw new InvalidOperationException("...");
}

Sin embargo, si está llevando datos al sistema desde fuentes que no controla, no es necesario bloquear todo el hilo. En este caso, puede crear un código de aeropuerto que no es válido, pero puede parecer un error obvio. Esto implicaría crear un constructor sin parámetros y configurar Code en algo como "---":

public Airport()
{
    Code = "---";
}

Ya que estás usando string como el Código, no tiene sentido usar una estructura. La estructura se asigna en la pila, solo para tener el Code asignado como un puntero a una cadena en la memoria del montón, por lo que no hay diferencia entre clase y estructura.

Si cambia el código del aeropuerto a una matriz de 3 elementos de caracteres, entonces una estructura se asignará completamente en la pila. Incluso entonces el volumen de datos no es tan grande como para marcar la diferencia.

    
respondido por el Greg Burghardt 10.09.2018 - 20:09
13

Utilice el patrón de peso de mosca

Como Airport es, correctamente, inmutable, no es necesario crear más de una instancia de una en particular, por ejemplo, SFO. Use un Hashtable o similar (tenga en cuenta que soy un tipo de Java, no C #, por lo que los detalles exactos pueden variar), para almacenar en caché los aeropuertos cuando se crean. Antes de crear uno nuevo, ingresa en el Hashtable. Nunca liberas aeropuertos, por lo que GC no tiene necesidad de liberarlos.

Una pequeña ventaja adicional (al menos en Java, no estoy seguro acerca de C #) es que no necesita escribir un método equals() , un simple == servirá. Lo mismo para hashcode() .

    
respondido por el user949300 11.09.2018 - 07:50
3

No soy un programador particularmente avanzado, pero ¿no sería este un uso perfecto para un Enum?

Hay diferentes maneras de construir clases de enumeración a partir de listas o cadenas. Aquí hay uno que he visto en el pasado, aunque no estoy seguro de que sea la mejor manera.

enlace

    
respondido por el Adam B 10.09.2018 - 21:54
3

Una de las razones por las que está viendo más actividad de GC es porque está creando una segunda cadena ahora: la versión .ToUpperInvariant() de la cadena original. La cadena original es elegible para GC inmediatamente después de que se ejecuta el constructor y la segunda es elegible al mismo tiempo que el objeto Airport . Es posible que pueda minimizarlo de una manera diferente (tenga en cuenta el tercer parámetro para string.Equals() ):

public sealed class Airport : IEquatable<Airport>
{
    public Airport(string code)
    {
        if (code == null)
        {
            throw new ArgumentNullException(nameof(code));
        }

        if (code.Length != 3 || !char.IsLetter(code[0])
                             || !char.IsLetter(code[1]) || !char.IsLetter(code[2]))
        {
            throw new ArgumentException(
                "Must be a 3 letter airport code.",
                nameof(code));
        }

        Code = code;
    }

    public string Code { get; }

    public override string ToString()
    {
        return Code; // TODO: Upper-case it here if you really need to for display.
    }

    public bool Equals(Airport other)
    {
        return string.Equals(Code, other?.Code, StringComparison.InvariantCultureIgnoreCase);
    }

    public override bool Equals(object obj)
    {
        return obj is Airport airport && Equals(airport);
    }

    public override int GetHashCode()
    {
        return Code.GetHashCode();
    }

    public static bool operator ==(Airport left, Airport right)
    {
        return Equals(left, right);
    }

    public static bool operator !=(Airport left, Airport right)
    {
        return !Equals(left, right);
    }
}
    
respondido por el Jesse C. Slicer 10.09.2018 - 23:47

Lea otras preguntas en las etiquetas