struct con valore di default senza senso

12

Nel mio sistema utilizzo spesso i codici aeroportuali ( "YYZ" , "LAX" , "SFO" , ecc.), sono sempre nello stesso formato esatto (3 lettere, rappresentate 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 per l'uguaglianza abbastanza spesso.

Abbiamo iniziato con il passaggio delle stringhe, che ha funzionato bene per un po ', ma abbiamo subito notato molti errori di programmazione passando un codice errato in un punto in cui era previsto il codice a 3 cifre. Abbiamo anche riscontrato problemi in cui dovevamo fare un confronto insensibile alle maiuscole e minuscole e invece non ci siamo riusciti, causando bug.

Da questo ho deciso di interrompere il passaggio delle stringhe e creare una classe Airport , che ha un singolo costruttore che accetta e convalida il codice aeroportuale.

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

Questo ha reso il nostro codice molto più facile da capire e abbiamo semplificato i nostri controlli di uguaglianza, usi dizionario / set. Ora sappiamo che se i nostri metodi accettano un'istanza Airport 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 un sacco di casi in cui Airport veniva raccolta.

La mia soluzione è stata quella di convertire class in struct . Principalmente si trattava solo di una modifica di parole chiave, ad eccezione di GetHashCode e ToString :

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

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

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

Le mie domande:

  1. Stavo creando una classe Airport o strutturavo una buona soluzione in generale, oppure sto risolvendo il problema sbagliato / risolvendolo nel modo sbagliato creando il tipo? Se non è una buona soluzione, qual è la soluzione migliore?

  2. Come dovrebbe la mia applicazione gestire le istanze in cui viene utilizzato default(Airport) ? Un tipo di default(Airport) non ha senso per la mia applicazione, quindi ho fatto if (airport == default(Airport) { throw ... } in luoghi in cui ottenere un'istanza di Airport (e la sua proprietà Code ) è fondamentale per l'operazione.

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

    
posta Matthew 10.09.2018 - 19:36
fonte

4 risposte

6

Aggiornamento: ho riscritto la mia risposta per rispondere ad alcuni presupposti errati relativi alle strutture C #, nonché all'OP che ci informa nei commenti che le stringhe internate vengono utilizzate.

Se puoi controllare i dati che arrivano al tuo sistema, usa una classe come hai postato nella tua domanda. Se qualcuno esegue default(Airport) , otterrà un valore null indietro. Assicurati di scrivere il tuo metodo Equals privato per restituire false ogni volta che confronti gli oggetti nulli aeroportuali e poi lascia che NullReferenceException voli altrove nel codice.

Tuttavia, se stai prendendo dati nel sistema da fonti che non controlli, non è necessario voler arrestare l'intero thread. In questo caso una struttura è ideale per il semplice fatto che default(Airport) ti darà qualcosa di diverso da un puntatore null . 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, terrei il code privato e non esporrai una proprietà Code - concentrati 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 caso peggiore convertendo default(Airport) in una stringa stampa "---" e restituisce false se confrontato con altri codici di aeroporto validi. Qualsiasi codice di aeroporto "predefinito" non corrisponde a nulla, inclusi altri codici aeroportuali predefiniti.

Sì, le strutture sono pensate per essere valori allocati nello stack, e qualsiasi puntatore per accumulare memoria sostanzialmente annulla i vantaggi prestazionali delle strutture, ma in questo caso il valore predefinito di una struct ha un significato e fornisce una resistenza aggiuntiva al proiettile resto dell'applicazione.

Vorrei piegare un po 'le regole qui, a causa di ciò.

Risposta originale (con alcuni errori effettivi)

Se puoi controllare i dati che arrivano nel tuo sistema, farei come suggerito da Robert Harvey nei commenti: crea un costruttore senza parametri e genera un'eccezione quando viene chiamato. Questo impedisce ai dati non validi di entrare nel sistema tramite default(Airport) .

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

Tuttavia, se stai prendendo dati nel sistema da fonti che non controlli, non è necessario voler arrestare l'intero thread. In questo caso è possibile creare un codice aeroportuale non valido, ma farlo sembrare un errore evidente. Ciò comporterebbe la creazione di un costruttore senza parametri e l'impostazione di Code su qualcosa come "---":

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

Dal momento che stai usando un string come codice, non ha senso usare una struct. La struct viene allocata nello stack, solo per avere il Code allocato come puntatore a una stringa nella memoria heap, quindi nessuna differenza tra classe e struct.

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

    
risposta data 10.09.2018 - 20:09
fonte
13

Utilizza il modello di peso vivo

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

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

    
risposta data 11.09.2018 - 07:50
fonte
3

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

Ci sono diversi modi per costruire classi enum da liste o stringhe. Ecco uno che ho visto in passato, non so se è il modo migliore però.

link

    
risposta data 10.09.2018 - 21:54
fonte
3

Uno dei motivi per cui stai vedendo più attività del GC è perché ora stai creando una seconda stringa: la versione .ToUpperInvariant() della stringa originale. La stringa originale è idonea per GC subito dopo l'esecuzione del costruttore e la seconda è idonea contemporaneamente all'oggetto Airport . Potresti essere in grado di 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);
    }
}
    
risposta data 10.09.2018 - 23:47
fonte

Leggi altre domande sui tag