En mi sistema que con frecuencia operan con códigos de aeropuertos ( "YYZ"
, "LAX"
, "SFO"
, etc.), que siempre están en el mismo formato exacto (de 3 letras, representada en mayúsculas). El sistema generalmente maneja entre 25 y 50 de estos (diferentes) códigos por solicitud de API, con más de mil asignaciones en total, se pasan a través de muchas capas de nuestra aplicación y se comparan por la igualdad con bastante frecuencia.
Comenzamos simplemente pasando cadenas, lo que funcionó bien por un tiempo, 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 encontramos con problemas en los que se suponía que debíamos hacer una comparación entre mayúsculas y minúsculas y, en cambio, no lo hicimos, lo que resultó en errores.
A partir de esto, decidí dejar de pasar cadenas y crear una Airport
clase, que tiene un solo 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 nuestros controles de igualdad, uso de diccionario / conjunto. Ahora sabemos que si nuestros métodos aceptan una Airport
instancia que se comportará de la manera que esperamos, ha simplificado nuestras verificaciones de métodos a una verificación de referencia nula.
Sin embargo, lo que sí noté fue que la recolección de basura se ejecutaba con mucha más frecuencia, lo que rastreé hasta muchos casos de recolección Airport
.
Mi solución a esto fue convertir el class
a struct
. Principalmente 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 default(Airport)
se usa.
Mis preguntas:
¿Crear una
Airport
clase o estructura era una buena solución en general, o estoy resolviendo el problema incorrecto / resolviéndolo de manera incorrecta creando el tipo? Si no es una buena solución, ¿cuál es una mejor solución?¿Cómo debe manejar mi aplicación las instancias donde
default(Airport)
se usa? Un tipo de nodefault(Airport)
tiene sentido para mi aplicación, por lo que he estado haciendoif (airport == default(Airport) { throw ... }
en lugares donde obtener una instancia deAirport
(y suCode
propiedad) es fundamental para la operación.
Notas: Revisé las preguntas C # / VB struct: ¿cómo evitar el caso con valores predeterminados cero, que se considera invá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.
fuente
default(Airport)
problema es simplemente deshabilitando las instancias predeterminadas. Puede hacerlo escribiendo un constructor sin parámetros y lanzándoloInvalidOperationException
oNotImplementedException
en él.Respuestas:
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 internados.
Si puede controlar los datos que ingresan a su sistema, use una clase tal como la publicó en su pregunta. Si alguien corre
default(Airport)
obtendrá unnull
valor de vuelta. Asegúrese de escribir suEquals
método privado para devolver falso cada vez que compare objetos nulos del aeropuerto, y luego dejeNullReferenceException
volar el otro en el código.Sin embargo, si está ingresando datos al sistema desde fuentes que no controla, no es necesario que bloquee todo el hilo. En este caso, una estructura es ideal porque el simple hecho
default(Airport)
le dará algo más que unnull
puntero. Cree un valor obvio para representar "sin 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, simplemente mantendría locode
privado y no expondría unaCode
propiedad en absoluto, solo me centraría en el comportamiento aquí.En el peor de los casos, la conversión
default(Airport)
a una cadena se imprime"---"
y devuelve falso en comparación 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 están destinadas a ser valores asignados en la pila, y cualquier puntero a la memoria del montón básicamente niega las ventajas de rendimiento de las estructuras, pero en este caso el valor predeterminado de una estructura tiene significado y proporciona alguna resistencia adicional al resto de la bala. solicitud.
Doblaría un poco las reglas aquí, por eso.
Respuesta original (con algunos errores de hecho)
Si puede controlar los datos que ingresan a su sistema, haría lo que Robert Harvey sugirió en los comentarios: crear un constructor sin parámetros y lanzar una excepción cuando se llame. Esto evita que datos no válidos ingresen al sistema a través de
default(Airport)
.Sin embargo, si está ingresando datos al sistema desde fuentes que no controla, no es necesario que bloquee todo el hilo. En este caso, puede crear un código de aeropuerto que no sea válido, pero que parezca un error obvio. Esto implicaría crear un constructor sin parámetros y establecer
Code
algo como "---":Como está utilizando a
string
como el Código, no tiene sentido utilizar una estructura. La estructura se asigna en la pila, solo para que seCode
asigne como un puntero a una cadena en la memoria del montón, por lo que no hay diferencia aquí entre la clase y la estructura.Si cambia el código del aeropuerto a una matriz de 3 elementos de caracteres, una estructura se asignaría completamente en la pila. Incluso entonces, el volumen de datos no es tan grande como para marcar la diferencia.
fuente
Code
propiedad, ¿cambiaría eso su justificación con respecto a su punto de la cadena que está en la memoria del montón?Usa el patrón Flyweight
Como el aeropuerto es, correctamente, inmutable, no hay necesidad de 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 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, verifique la tabla de hash. Nunca está liberando aeropuertos, por lo que GC no tiene necesidad de liberarlos.
Una ventaja menor adicional (al menos en Java, no estoy seguro acerca de C #) es que no necesita escribir un
equals()
método, lo==
hará de manera simple . Lo mismo parahashcode()
.fuente
getAirportOrCreate()
código se sincroniza correctamente, no hay ninguna razón técnica para que no pueda crear nuevos aeropuertos según sea necesario durante el tiempo de ejecución. Puede haber razones comerciales.No soy un programador particularmente avanzado, pero ¿no sería un uso perfecto para un Enum?
Hay diferentes formas de construir clases enum a partir de listas o cadenas. Aquí hay uno que he visto en el pasado, aunque no estoy seguro de si es la mejor manera.
https://blog.kloud.com.au/2016/06/17/converting-webconfig-values-into-enum-or-list/
fuente
Una de las razones por las que está viendo más actividad de GC es porque ahora está creando una segunda cadena: la
.ToUpperInvariant()
versión de la cadena original. La cadena original es elegible para GC justo después de que se ejecuta el constructor y la segunda es elegible al mismo tiempo que elAirport
objeto. Es posible que pueda minimizarlo de una manera diferente (tenga en cuenta el tercer parámetro parastring.Equals()
):fuente
GetHashCode
, solo debe usarStringComparer.OrdinalIgnoreCase.GetHashCode(Code)
o similar