Dans mon système je fonctionne souvent avec des codes d'aéroport ( "YYZ"
, "LAX"
, "SFO"
, etc.), ils sont toujours dans le même format exact (3 lettres, représentée en majuscules). Le système traite généralement 25 à 50 de ces codes (différents) par demande d'API, avec plus de mille allocations au total, ils sont transmis à travers de nombreuses couches de notre application et sont comparés assez souvent pour l'égalité.
Nous avons commencé par simplement passer des chaînes, ce qui a bien fonctionné un peu, mais nous avons rapidement remarqué beaucoup d'erreurs de programmation en passant un mauvais code quelque part où le code à 3 chiffres était attendu. Nous avons également rencontré des problèmes où nous étions censés faire une comparaison insensible à la casse et qui ne l'ont pas fait, ce qui a entraîné des bogues.
À partir de cela, j'ai décidé d'arrêter de passer des chaînes et de créer une Airport
classe, qui a un seul constructeur qui prend et valide le code de l'aéroport.
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);
}
}
Cela a rendu notre code beaucoup plus facile à comprendre et nous avons simplifié nos vérifications d'égalité, nos utilisations de dictionnaire / ensemble. Nous savons maintenant que si nos méthodes acceptent une Airport
instance qui se comportera comme nous l'attendons, cela a simplifié nos vérifications de méthodes en une vérification de référence nulle.
La chose que j'ai remarquée, cependant, était que la collecte des ordures fonctionnait beaucoup plus souvent, ce que j'ai retrouvé dans de nombreux cas de Airport
collecte.
Ma solution à cela a été de convertir le fichier class
en a struct
. Il s'agissait principalement d'un changement de mot clé, à l'exception de GetHashCode
et ToString
:
public override string ToString()
{
return Code ?? string.Empty;
}
public override int GetHashCode()
{
return Code?.GetHashCode() ?? 0;
}
Pour gérer le cas où default(Airport)
est utilisé.
Mes questions:
La création d'une
Airport
classe ou d'une structure était-elle une bonne solution en général, ou suis-je en train de résoudre le mauvais problème / le résoudre de la mauvaise façon en créant le type? Si ce n'est pas une bonne solution, quelle est la meilleure solution?Comment mon application doit-elle gérer les instances où le
default(Airport)
est utilisé? Un type dedefault(Airport)
n'a pas de sens pour mon application, donc je l'ai faitif (airport == default(Airport) { throw ... }
dans des endroits où l'obtention d'une instance deAirport
(et de sesCode
propriétés) est essentielle à l'opération.
Notes: J'ai passé en revue les questions C # / VB struct - comment éviter la casse avec des valeurs par défaut nulles, qui est considérée comme invalide pour une structure donnée? et utilisez struct ou non avant de poser ma question, mais je pense que mes questions sont suffisamment différentes pour justifier son propre message.
la source
default(Airport)
problème en interdisant simplement les instances par défaut. Vous pouvez le faire en écrivant un constructeur et de jeter parameterlessInvalidOperationException
ouNotImplementedException
en elle.Réponses:
Mise à jour: j'ai réécrit ma réponse pour corriger certaines hypothèses incorrectes sur les structures C #, ainsi que l'OP nous informant dans les commentaires que des chaînes internes sont utilisées.
Si vous pouvez contrôler les données entrant dans votre système, utilisez une classe telle que vous l'avez publiée dans votre question. Si quelqu'un court,
default(Airport)
il récupérera unenull
valeur. Assurez-vous d'écrire votreEquals
méthode privée pour renvoyer false chaque fois que vous comparez des objets Airport nuls, puis laissez-leNullReferenceException
voler ailleurs dans le code.Cependant, si vous importez des données dans le système à partir de sources que vous ne contrôlez pas, vous ne voulez pas nécessairement planter tout le thread. Dans ce cas, une structure est idéale car le simple fait
default(Airport)
vous donnera autre chose qu'unnull
pointeur. Composez une valeur évidente pour représenter «aucune valeur» ou la «valeur par défaut» afin d'avoir quelque chose à imprimer à l'écran ou dans un fichier journal (comme «---» par exemple). En fait, je voudrais simplement garder lecode
privé et ne pas exposer du tout uneCode
propriété - me concentrer uniquement sur le comportement ici.Dans le pire des cas, la conversion
default(Airport)
en une chaîne s'imprime"---"
et renvoie faux par rapport à d'autres codes d'aéroport valides. Tout code d'aéroport "par défaut" ne correspond à rien, y compris les autres codes d'aéroport par défaut.Oui, les structures sont censées être des valeurs allouées sur la pile, et tout pointeur vers la mémoire de tas annule fondamentalement les avantages de performance des structures, mais dans ce cas, la valeur par défaut d'une structure a un sens et fournit une résistance supplémentaire aux balles pour le reste de la application.
Je plierais un peu les règles ici, à cause de cela.
Réponse originale (avec quelques erreurs factuelles)
Si vous pouvez contrôler les données entrant dans votre système, je ferais ce que Robert Harvey a suggéré dans les commentaires: créer un constructeur sans paramètre et lever une exception lors de son appel. Cela empêche les données non valides d'entrer dans le système via
default(Airport)
.Cependant, si vous importez des données dans le système à partir de sources que vous ne contrôlez pas, vous ne voulez pas nécessairement planter tout le thread. Dans ce cas, vous pouvez créer un code d'aéroport non valide, mais le faire apparaître comme une erreur évidente. Cela impliquerait la création d'un constructeur sans paramètre et la définition
Code
de quelque chose comme "---":Puisque vous utilisez a
string
comme code, il est inutile d'utiliser une structure. La structure est allouée sur la pile, uniquement pour avoir l'Code
allocation comme pointeur vers une chaîne dans la mémoire du tas, donc aucune différence ici entre la classe et la structure.Si vous avez changé le code d'aéroport en un tableau de 3 éléments de caractères, une structure serait entièrement allouée sur la pile. Même alors, le volume de données n'est pas si important pour faire la différence.
la source
Code
propriété, cela changerait-il votre justification concernant votre point de la chaîne étant en mémoire de tas?Utilisez le modèle Flyweight
Étant donné que Airport est, à juste titre, immuable, il n'est pas nécessaire de créer plus d'une instance d'une instance particulière, disons, SFO. Utilisez une table de hachage ou similaire (notez que je suis un gars Java, pas C # donc les détails exacts peuvent varier) pour mettre en cache les aéroports lorsqu'ils sont créés. Avant d'en créer un nouveau, archivez le Hashtable. Vous ne libérez jamais d'aéroports, donc GC n'a pas besoin de les libérer.
Un autre avantage mineur (au moins en Java, vous n'êtes pas sûr de C #) est que vous n'avez pas besoin d'écrire une
equals()
méthode, un simple==
fera l'affaire. Pareil pourhashcode()
.la source
getAirportOrCreate()
code est correctement synchronisé, il n'y a aucune raison technique pour laquelle vous ne pouvez pas créer de nouveaux aéroports au besoin pendant l'exécution. Il peut y avoir des raisons commerciales.Je ne suis pas un programmeur particulièrement avancé, mais ne serait-ce pas une utilisation parfaite pour un Enum?
Il existe différentes façons de construire des classes enum à partir de listes ou de chaînes. En voici un que j'ai vu dans le passé, je ne sais pas si c'est la meilleure façon.
https://blog.kloud.com.au/2016/06/17/converting-webconfig-values-into-enum-or-list/
la source
L'une des raisons pour lesquelles vous voyez plus d'activité GC est parce que vous créez maintenant une deuxième chaîne - la
.ToUpperInvariant()
version de la chaîne d'origine. La chaîne d'origine est éligible pour GC juste après l'exécution du constructeur et la seconde est éligible en même temps que l'Airport
objet. Vous pourrez peut-être le réduire d'une manière différente (notez le troisième paramètrestring.Equals()
):la source
GetHashCode
, devrait simplement utiliserStringComparer.OrdinalIgnoreCase.GetHashCode(Code)
ou similaire