struct con valore predefinito privo di senso


12

Nel mio sistema ho spesso operare con codici aeroportuali ( "YYZ", "LAX", "SFO", etc.), sono sempre nello stesso identico formato (3 lettera, rappresentato in maiuscolo). Il sistema in genere gestisce 25-50 di questi (diversi) codici per richiesta API, con oltre un migliaio di allocazioni totali, vengono passati attraverso molti livelli della nostra applicazione e vengono confrontati abbastanza spesso per l'uguaglianza.

Abbiamo iniziato con il semplice passaggio di stringhe, il che ha funzionato bene per un po ', ma abbiamo notato rapidamente molti errori di programmazione passando un codice errato da qualche parte previsto per il codice a 3 cifre. Abbiamo anche riscontrato problemi in cui dovevamo fare un confronto senza distinzione tra maiuscole e minuscole e invece non lo abbiamo fatto, risultando in bug.

Da questo, ho deciso di smettere di passare le stringhe in giro e creare una Airportclasse, che ha un unico costruttore che prende e convalida il codice dell'aeroporto.

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);
    }
}

Ciò ha reso il nostro codice molto più semplice da comprendere e abbiamo semplificato i nostri controlli di uguaglianza, il dizionario / gli usi impostati. Ora sappiamo che se i nostri metodi accettano Airportun'istanza che si comporterà come ci aspettiamo, ha semplificato i nostri controlli di metodo a un controllo di riferimento null.

La cosa che ho notato, tuttavia, era che la garbage collection era in esecuzione molto più spesso, che ho rintracciato in molti casi di Airportraccolta.

La mia soluzione è stata quella di convertire il file a classin struct. Principalmente è stata solo una modifica della parola chiave, ad eccezione di GetHashCodee ToString:

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

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

Per gestire il caso in cui default(Airport)viene utilizzato.

Le mie domande:

  1. Creare una Airportclasse o strutturare una buona soluzione in generale o sto risolvendo il problema sbagliato / risolvendolo nel modo sbagliato creando il tipo? Se non è una buona soluzione, qual è una soluzione migliore?

  2. In che modo l'applicazione deve gestire le istanze in cui default(Airport)viene utilizzata? Un tipo di non ha default(Airport)senso per la mia applicazione, quindi l'ho fatto if (airport == default(Airport) { throw ... }in luoghi in cui ottenere un'istanza di Airport(e le sue Codeproprietà) è fondamentale per l'operazione.

Note: ho esaminato le domande C # / VB struct: come evitare il caso con valori predefiniti pari a zero, che è considerato non valido per la struttura data? e usa struct o no prima di porre la mia domanda, tuttavia penso che le mie domande siano abbastanza diverse da giustificare il proprio post.


7
La garbage collection ha un impatto materiale sulle prestazioni dell'applicazione? In altre parole, importa?
Robert Harvey,

Comunque, sì, la soluzione di classe era "buona". Il modo in cui sai che ha risolto il tuo problema senza crearne di nuovi.
Robert Harvey,

2
Un modo per risolvere il default(Airport)problema è semplicemente vietando le istanze predefinite. Puoi farlo scrivendo un costruttore senza parametri e lanciandolo InvalidOperationExceptiono NotImplementedExceptional suo interno.
Robert Harvey,

3
In una nota a margine, invece di confermare che la stringa di inizializzazione è in realtà 3 caratteri alfa, perché non confrontarla con l'elenco finito di tutti i codici aeroportuali (ad esempio, github.com/datasets/airport-codes o simili)?
Dan Pichelman,

2
Sono disposto a scommettere diverse birre che questa non è la radice di un problema di prestazioni. Un normale laptop può allocare nell'ordine 10 M oggetti / secondi.
Esben Skov Pedersen,

Risposte:


6

Aggiornamento: ho riscritto la mia risposta per affrontare alcune ipotesi errate sulle strutture C #, nonché l'OP che ci informa nei commenti che vengono utilizzate le stringhe internate.


Se riesci a controllare i dati che arrivano al tuo sistema, usa una classe come hai pubblicato nella tua domanda. Se qualcuno corre default(Airport)otterrà un nullvalore indietro. Assicurati di scrivere il tuo Equalsmetodo privato per restituire false ogni volta che confronti oggetti null Airport, quindi lascia NullReferenceExceptionvolare altrove nel codice.

Tuttavia, se si stanno trasferendo dati nel sistema da fonti che non si controllano, non è necessario arrestare in modo anomalo l'intero thread. In questo caso una struttura è l'ideale per il semplice fatto che default(Airport)ti darà qualcosa di diverso da un nullpuntatore. Crea un valore ovvio per rappresentare "nessun valore" o il "valore predefinito" in modo da avere qualcosa da stampare sullo schermo o in un file di registro (come "---" per esempio). In effetti, vorrei solo mantenere il codeprivato e non esporre affatto una Codeproprietà - mi concentro solo sul comportamento qui.

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);
    }
}

Lo scenario peggiore che si converte default(Airport)in una stringa viene stampato "---"e restituisce falso rispetto ad altri codici aeroportuali validi. Qualsiasi codice aeroporto "predefinito" non corrisponde a nulla, inclusi altri codici aeroporto predefiniti.

Sì, le strutture sono pensate per essere valori allocati nello stack e tutti i puntatori alla memoria dell'heap sostanzialmente annullano i vantaggi in termini di prestazioni delle strutture, ma in questo caso il valore predefinito di una struttura ha un significato e fornisce una resistenza proiettile aggiuntiva al resto del applicazione.

Vorrei piegare un po 'le regole qui, per questo.


Risposta originale (con alcuni errori di fatto)

Se riesci a controllare i dati che arrivano al tuo sistema, farei come suggerito da Robert Harvey nei commenti: Crea un costruttore senza parametri e genera un'eccezione quando viene chiamato. Ciò impedisce l'ingresso di dati non validi nel sistema tramite default(Airport).

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

Tuttavia, se si stanno trasferendo dati nel sistema da fonti che non si controllano, non è necessario arrestare in modo anomalo l'intero thread. In questo caso è possibile creare un codice aeroporto non valido, ma farlo sembrare un errore evidente. Ciò implicherebbe la creazione di un costruttore senza parametri e l'impostazione Codedi qualcosa come "---":

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

Dato che stai usando a stringcome Codice, non ha senso usare una struttura. La struttura viene allocata nello stack, solo per avere l' Codeallocazione come puntatore a una stringa nella memoria dell'heap, quindi qui nessuna differenza tra classe e struttura.

Se cambiassi il codice dell'aeroporto in un array di 3 elementi di char, una struttura verrebbe completamente allocata nello stack. Anche allora il volume di dati non è così grande da fare la differenza.


Se la mia applicazione stesse usando stringhe internate per la Codeproprietà, ciò cambierebbe la tua giustificazione riguardo al punto in cui la stringa si trova nella memoria dell'heap?
Matteo,

@Matthew: usare una classe ti dà un problema di prestazioni? In caso contrario, lancia una moneta per decidere quale utilizzare.
Greg Burghardt,

4
@Matthew: L'importante è che tu abbia centralizzato la fastidiosa logica della normalizzazione dei codici e dei confronti. Dopo quella "classe contro struttura" è solo una discussione accademica, fino a quando non si misura un impatto abbastanza grande in termini di prestazioni da giustificare il tempo extra per gli sviluppatori per avere una discussione accademica.
Greg Burghardt,

1
È vero, non mi dispiace avere una discussione accademica di volta in volta se mi aiuta a creare soluzioni più informate in futuro.
Matteo,

@Matthew: Sì, hai assolutamente ragione. Dicono "parlare costa poco". È certamente più economico che non parlare e costruire qualcosa di male. :)
Greg Burghardt,

13

Usa il modello Flyweight

Poiché l'aeroporto è, correttamente, immutabile, non è necessario creare più di un'istanza di una particolare, diciamo, OFS. Usa un Hashtable o simile (nota, sono un ragazzo Java, non C # quindi i dettagli esatti potrebbero variare), per memorizzare nella cache gli Aeroporti quando vengono creati. Prima di crearne uno nuovo, controlla nella Hashtable. Non stai mai liberando aeroporti, quindi GC non ha bisogno di liberarli.

Un ulteriore vantaggio minore (almeno in Java, non sono sicuro di C #) è che non è necessario scrivere un equals()metodo, un semplice ==farà. Lo stesso per hashcode().


3
Ottimo utilizzo del modello flyweight.
Neil,

2
Supponendo che OP continui a utilizzare una struttura e non una classe, l'internet di stringhe non sta già gestendo i valori di stringa riutilizzabili? Le strutture vivono già nello stack, le stringhe vengono già riutilizzate in modo da evitare valori duplicati in memoria. Quale ulteriore vantaggio si otterrebbe dal modello flyweight?
Flater,

Qualcosa a cui fare attenzione. Se un aeroporto viene aggiunto o rimosso, ti consigliamo di creare un modo per aggiornare questo elenco statico senza riavviare l'applicazione o ridistribuirla. Gli aeroporti non vengono aggiunti o rimossi spesso, ma gli imprenditori tendono ad essere un po 'turbati quando un semplice cambiamento diventa così complicato. "Non posso semplicemente aggiungerlo da qualche parte ?! Perché dobbiamo pianificare un riavvio / riavvio dell'applicazione e un inconveniente per i nostri clienti?" Ma all'inizio stavo anche pensando di usare una sorta di cache statica.
Greg Burghardt,

@Flater Punto ragionevole. Direi meno bisogno per i programmatori junior di ragionare su stack vs. heap. Inoltre vedi la mia aggiunta - non c'è bisogno di scrivere equals ().
user949300,

1
@Greg Burghardt Se il getAirportOrCreate()codice è sincronizzato correttamente, non vi è alcun motivo tecnico per cui non è possibile creare nuovi aeroporti in base alle esigenze durante il runtime. Potrebbero esserci motivi di lavoro.
user949300,

3

Non sono un programmatore particolarmente avanzato, ma non sarebbe un uso perfetto per un Enum?

Esistono diversi modi per costruire classi enum da elenchi o stringhe. Eccone uno che ho visto in passato, non sono sicuro che sia il modo migliore.

https://blog.kloud.com.au/2016/06/17/converting-webconfig-values-into-enum-or-list/


2
Quando ci sono potenzialmente migliaia di valori diversi (come nel caso dei codici aeroportuali), un enum non è pratico.
Ben Cottrell,

Sì, ma il link che ho pubblicato è come caricare le stringhe come enum. Ecco un altro link per caricare una tabella di ricerca come enum. Potrebbe essere un po 'di lavoro, ma trarrebbe vantaggio dal potere degli enum. exceptionnotfound.net/…
Adam B,

1
Oppure è possibile caricare un elenco di codici validi da un database o file. Quindi un codice aeroportuale viene appena verificato per far parte di tale elenco. Questo è ciò che in genere si fa quando non si desidera più codificare i valori e / o l'elenco diventa troppo lungo da gestire.
Neil,

@BenCottrell è esattamente ciò a cui servono i template di codice gen e modelli T4.
RubberDuck,

3

Uno dei motivi per cui stai vedendo più attività GC è perché stai creando una seconda stringa ora: la .ToUpperInvariant()versione della stringa originale. La stringa originale è idonea per GC subito dopo l'esecuzione del costruttore e la seconda è idonea allo stesso tempo Airportdell'oggetto. Potresti riuscire a minimizzarlo in un modo diverso (nota il terzo parametro su 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);
    }
}

Questo non produce codici hash diversi per aeroporti uguali (ma diversamente capitalizzati)?
Hero Wanders,

Sì, immagino di si. Dangit.
Jesse C. Slicer,

Questo è un ottimo punto, mai pensato, vedrò come apportare questi cambiamenti.
Matteo,

1
Per quanto riguarda GetHashCode, dovrebbe semplicemente usare StringComparer.OrdinalIgnoreCase.GetHashCode(Code)o simile
Matteo
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.