Classe flag invalid vs exception

1

Quindi ho il seguente Leggo in linee da un file di testo. Ogni riga deve essere analizzata e ottenere i dati importanti estratti (non andrò nei dettagli qui).

Ho creato una classe Parser che fa questo lavoro. Unforunately può accadere abbastanza frequentemente, che i dati di linea non sono utilizzabili. Lo trovo solo quando leggo la riga nella classe e lo analizzo. Così ho fatto un bit valido che indica se i dati sono ok. Al momento controllo il bit valido prima di prendere i dati. Mi stavo chiedendo se è una pratica migliore qui per lanciare un'eccezione. D'altra parte ho sentito solo eccezioni quando il caso non si verifica frequentemente. Nel mio caso, come metà delle righe che analizzo con la classe Parser non sono dati validi.

Ecco un codice incompleto semplificato su come è stato fatto adesso per ottenere l'idea:

struct Symbol_data {        // possible data which can be extracted out of the symbolik
    //Struct with the extracted data...
};

class Parse_symbol_line {   //class extracts data from a line. it is possible the data is unsusable
public:
    Parse_symbol_line(std::string& l);
    bool is_valid() const { return valid; }         //during processing the data can become unusable
    Symbol_data get_symbolic_data() { return sym_data; }
private:
    //Private Methods here which extract and analyse the line in many steps
    void read_data_from_line();
    void analyze_raw_comment();
    void mask_out_device_id();
    void analyze_device_id();
    void mask_out_vessel();
    void create_symbol_data();

    bool valid;             // if in valid dont use the data
    Symbol_data sym_data;
};

//constructor calls all the steps to read out the data out of the line
//it is possible after each step, that the line is not valid and therefore doesnt need more checks
Parse_symbol_line::Parse_symbol_line(std::string& l)
    :valid{ true }, EA{ true }, line{ l }, sym_data{ Symbol_data{} }
{
    read_raw_data_from_line();
    if (!valid) return;         //dont continue if data is invalid anyway
    analyze_raw_comment();
    if (!valid) return;
    mask_out_device_id();
    if (!valid) return;
    analyze_device_id();
    if (!valid) return;
    mask_out_vessel();
    if (!valid) return;
    create_symbol_data();
    if (!valid) return;
}

//another class provides the lines to the parse class and gets the data from it
void Compute_Symbols::read_symbolic()       
{
    std::ifstream ifs{ in_sym_fname };
    if (!ifs) throw std::runtime_error("void Compute_Symbols::read_symbolic(); symbolic file could not be opened\n");

    for (std::string line; std::getline(ifs, line);) {  
        Parse_symbol_line psl{ line };

        if (psl.is_valid()) {
            //continue work with the data   
        }
        else if (!psl.is_valid()) {
            //dont use the data
        }
    }
}
    
posta Sandro4912 20.03.2018 - 13:09
fonte

3 risposte

6

Le eccezioni sono un modo per segnalare che il tuo codice non può completare l'operazione richiesta. Forse qualcosa era nullo che non ti saresti mai aspettato di essere nullo. Forse le precondizioni sono state violate. Forse altri presupposti o vincoli erano falsi. Un'eccezione è un modo per far sapere qualcos'altro che l'operazione non può essere completata e lasciare che il codice con più contesto / più capacità lo gestisca in qualche modo.

Quindi molto di questo dipende da due cose, in primo luogo, quali sono i contratti e le ipotesi del tuo codice e due, come il codice che chiama la tua funzione / oggetto potrebbe eventualmente gestire un errore.

Quindi qual è il contratto per il tuo codice? L'idea è che "analizzerò questi dati e restituirò alcuni dati estratti. Devi fornirmi dati validi". Se questo è il caso, allora è appropriata un'eccezione. Il codice non può soddisfare il suo contratto quando i dati non sono validi (violazione di precondizione).

Se il tuo contratto è "Passami qualche dato, cercherò di analizzarlo. Se avrò successo, restituirò i dati estratti. In caso contrario, restituirò qualcosa che indica un errore". Quindi un tipo di dati opzione sarebbe un tipo di ritorno appropriato e un'eccezione sarebbe la strada sbagliata.

Questo è molto simile alla differenza tra int.Parse e int.TryParse in C #. Il primo si aspetta dati validi e ti darà un intero o esploderà. Quest'ultimo tenterà l'analisi e restituirà una booleana che dice se i dati potrebbero essere analizzati e il valore analizzato (se ha successo).

L'altra cosa da considerare è come ogni codice chiamante potrebbe / potrebbe eventualmente gestire un'eccezione. Se il tuo codice chiamante sta solo per ingoiare l'eccezione e ignorare i dati non validi, è probabile che si tratti di un uso inappropriato di un'eccezione (anche se ci sono momenti in cui ciò è corretto, sono solo pochi e distanti tra loro). Soprattutto se questo è un evento comune, non userei un'eccezione. Se il codice chiamante può gestire l'eccezione in modo significativo, allora è una possibilità. Se l'eccezione è appena usata per il controllo del flusso, è male. Ecco a cosa servono i tipi di opzione / flag booleani.

    
risposta data 20.03.2018 - 14:59
fonte
1

Nel tuo codice utilizzi già uno schema predefinito:

std::ifstream ifs{ in_sym_fname };
if (!ifs) throw std::runtime_error("void Compute_Symbols::read_symbolic(); symbolic file could not be opened\n");

Se segui questo schema, il tuo codice sarà simile a questo:

void Compute_Symbols::read_symbolic()       
{
    std::ifstream ifs{ in_sym_fname };
    if (!ifs) throw std::runtime_error("void Compute_Symbols::read_symbolic(); symbolic file could not be opened\n");

    for (std::string line; std::getline(ifs, line);) {  
        Parse_symbol_line psl{ line };

        if (!psl) {
            // do whatever error handling neccessary
        }

        // process data here
    }
}

Probabilmente vuoi implementare

operator void*() const; 

e / o

explicit operator bool() const;

perché funzioni.

Puoi sicuramente discutere se questo è un modello buono . Altre lingue e quadri usano modelli diversi. Direi che restare negli schemi e comportamenti che sono già usati e che rimangono coerenti nel tuo stile in tutto il tuo progetto è più importante che sia perfettamente corretto in un posto, ma infrangere le regole e quindi le aspettative dei futuri manutentori. Puoi gestire qualsiasi modello, a condizione che sia coerente.

    
risposta data 20.03.2018 - 17:20
fonte
1

Per lo meno, mi sembra che sarebbe meglio che le funzioni restituiscano una bool contenente il valore che stai ora inserendo in valid :

bool read_data_from_line();
bool analyze_raw_comment();
bool mask_out_device_id();
bool analyze_device_id();
bool mask_out_vessel();
bool create_symbol_data();

Quindi probabilmente creerei un vettore (o array) di puntatori alle funzioni membro così potrei chiamarli in un ciclo:

typedef bool (Parse_symbol_line::*mem_ptr)();

// ...

Parse_symbol_line(std::string& l)
{
    std::vector<mem_ptr> analyzers { 
        &Parse_symbol_line::read_data_from_line, 
        &Parse_symbol_line::analyze_raw_comment,
        &Parse_symbol_line::mask_out_device_id,
        &Parse_symbol_line::analyze_device_id,
        &Parse_symbol_line::mask_out_vessel,
        &Parse_symbol_line::create_symbol_data
    };

    for (mem_ptr a : analyzers)
        if (!(this->*a)())
            handle_error();
}

Per coloro che trovano brutti o problematici i riferimenti ai membri, potresti usare solo qualcosa del tipo:

if (!(read_data_from_line() 
   && analyze_raw_comment() 
   && mask_out_device_id()
   && analyze_device_id()
   && mask_out_vessel()
   && create_symbol_data()))
{
    handle_error();
}

Alla fine funzionano più o meno allo stesso modo, ma con più di circa 3, in genere preferirei un ciclo.

Ora arriviamo alla vera domanda: cosa facciamo in handle_error() ? La risposta a questo dipende dalla risposta ad un'altra domanda. Se il fallimento di una di queste funzioni significa che non abbiamo veramente creato un oggetto Parse_symbol_line valido, allora la cosa corretta da fare solo è un'eccezione. Questo è un costruttore, e c'è un solo modo corretto per un costruttore di segnalare che non è stato in grado di creare correttamente un oggetto, e questo è quello di generare un'eccezione. Questo è davvero indipendente dalla frequenza con cui ciò potrebbe accadere. Non c'è altro modo per un ctor di segnalare un errore, quindi ha come eccezione.

Come regola generale, direi che una classe che contiene un membro chiamato valid dovrebbe essere un campanello d'allarme che la classe probabilmente ha un problema. Un oggetto dovrebbe sempre essere valido. Un ctor ha un requisito abbastanza semplice: creare un oggetto valido. Se non può farlo, deve lanciare un'eccezione in modo che l'oggetto non venga mai creato. Non mi piace essere dogmatico riguardo alle cose, ma in questo caso non c'è davvero un'alternativa decente. Avere una funzione membro valid che controlli costantemente per vedere se la classe è in uno stato valido non è un'alternativa decente. È molto meglio decidere che l'oggetto è valido o non esiste affatto. La creazione di oggetti che non sono validi è una ricetta per il disastro. Se non puoi creare un oggetto valido, è meglio farlo subito, non prolungare l'agonia.

Una possibilità da considerare piuttosto che (almeno direttamente) usando la gestione delle eccezioni sarebbe avere una funzione che restituisce qualcosa come std::optional<symbol_data> :

std::optional<Symbol_data> parse(std::istream &input) { 
    // ...
}

Quindi il tuo read_symbolic sarà simile a questo:

void Compute_Symbols::read_symbolic()       
{
    std::ifstream ifs{ in_sym_fname };
    if (!ifs) throw std::runtime_error("symbolic file could not be opened\n");

    for (std::string line; std::getline(ifs, line);) {
        if (auto foo = parse(line))
            // use foo
        else
            // ignore line
    }
}
    
risposta data 20.03.2018 - 14:38
fonte

Leggi altre domande sui tag