2009-12-05 7 views
16

Mi sembra alternative di Google per le eccezioni sonoLa dichiarazione di ritorno a più valori della lingua "Google" di Google è un'alternativa alle eccezioni?

  • GO: multi-valore di ritorno "ritorno val, err;"
  • GO, C++: zero controlli (rientro anticipato)
  • GO, C++: "gestire l'errore maledetta" (il mio mandato)
  • C++: assert (espressione)

  • GO: rinviare/panic/recupero sono caratteristiche del linguaggio aggiunti dopo questa domanda è stato chiesto

è multi-valore di ritorno abbastanza utile per agire come alternativa? Perché le "asserzioni" sono considerate alternative? Google la pensa O.K. se un programma si ferma se si verifica un errore che non viene gestito correttamente?

Effective GO: Multiple return values

One of Go's unusual features is that functions and methods can return multiple values. This can be used to improve on a couple of clumsy idioms in C programs: in-band error returns (such as -1 for EOF) and modifying an argument.

In C, a write error is signaled by a negative count with the error code secreted away in a volatile location. In Go, Write can return a count and an error: “Yes, you wrote some bytes but not all of them because you filled the device”. The signature of *File.Write in package os is:

func (file *File) Write(b []byte) (n int, err Error)

and as the documentation says, it returns the number of bytes written and a non-nil Error when n != len(b). This is a common style; see the section on error handling for more examples.

Effective GO: Named result parameters

The return or result "parameters" of a Go function can be given names and used as regular variables, just like the incoming parameters. When named, they are initialized to the zero values for their types when the function begins; if the function executes a return statement with no arguments, the current values of the result parameters are used as the returned values.

The names are not mandatory but they can make code shorter and clearer: they're documentation. If we name the results of nextInt it becomes obvious which returned int is which.

func nextInt(b []byte, pos int) (value, nextPos int) {

Because named results are initialized and tied to an unadorned return, they can simplify as well as clarify. Here's a version of io.ReadFull that uses them well:

func ReadFull(r Reader, buf []byte) (n int, err os.Error) { 
    for len(buf) > 0 && err == nil { 
    var nr int; 
    nr, err = r.Read(buf); 
    n += nr; 
    buf = buf[nr:len(buf)]; 
    } 
    return; 
} 

Why does Go not have exceptions?

Exceptions are a similar story. A number of designs for exceptions have been proposed but each adds significant complexity to the language and run-time. By their very nature, exceptions span functions and perhaps even goroutines; they have wide-ranging implications. There is also concern about the effect they would have on the libraries. They are, by definition, exceptional yet experience with other languages that support them show they have profound effect on library and interface specification. It would be nice to find a design that allows them to be truly exceptional without encouraging common errors to turn into special control flow that requires every programmer to compensate.

Like generics, exceptions remain an open issue.

Google C++ Style Guide: Exceptions

Decision:

On their face, the benefits of using exceptions outweigh the costs, especially in new projects. However, for existing code, the introduction of exceptions has implications on all dependent code. If exceptions can be propagated beyond a new project, it also becomes problematic to integrate the new project into existing exception-free code. Because most existing C++ code at Google is not prepared to deal with exceptions, it is comparatively difficult to adopt new code that generates exceptions.

Given that Google's existing code is not exception-tolerant, the costs of using exceptions are somewhat greater than the costs in in a new project. The conversion process would be slow and error-prone. We don't believe that the available alternatives to exceptions, such as error codes and assertions, introduce a significant burden.

Our advice against using exceptions is not predicated on philosophical or moral grounds, but practical ones. Because we'd like to use our open-source projects at Google and it's difficult to do so if those projects use exceptions, we need to advise against exceptions in Google open-source projects as well. Things would probably be different if we had to do it all over again from scratch.

GO: Defer, Panic and Recover

Defer statements allow us to think about closing each file right after opening it, guaranteeing that, regardless of the number of return statements in the function, the files will be closed.

The behavior of defer statements is straightforward and predictable. There are three simple rules:

1. A deferred function's arguments are evaluated when the defer statement is evaluated.

In this example, the expression "i" is evaluated when the Println call is deferred. The deferred call will print "0" after the function returns.

func a() { 
     i := 0 
     defer fmt.Println(i) 
     i++ 
     return  
    } 

2. Deferred function calls are executed in Last In First Out order after the surrounding function returns. This function prints "3210":

 func b() { 
     for i := 0; i < 4; i++ { 
      defer fmt.Print(i) 
     } 
    } 

3. Deferred functions may read and assign to the returning function's named return values.

In this example, a deferred function increments the return value i after the surrounding function returns. Thus, this function returns 2:

func c() (i int) { 
     defer func() { i++ }() 
     return 1 
    } 

This is convenient for modifying the error return value of a function; we will see an example of this shortly.

Panic is a built-in function that stops the ordinary flow of control and begins panicking. When the function F calls panic, execution of F stops, any deferred functions in F are executed normally, and then F returns to its caller. To the caller, F then behaves like a call to panic. The process continues up the stack until all functions in the current goroutine have returned, at which point the program crashes. Panics can be initiated by invoking panic directly. They can also be caused by runtime errors, such as out-of-bounds array accesses.

Recover is a built-in function that regains control of a panicking goroutine. Recover is only useful inside deferred functions. During normal execution, a call to recover will return nil and have no other effect. If the current goroutine is panicking, a call to recover will capture the value given to panic and resume normal execution.

Here's an example program that demonstrates the mechanics of panic and defer:

<snip> 

For a real-world example of panic and recover, see the json package from the Go standard library. It decodes JSON-encoded data with a set of recursive functions. When malformed JSON is encountered, the parser calls panic is to unwind the stack to the top-level function call, which recovers from the panic and returns an appropriate error value (see the 'error' and 'unmarshal' functions in decode.go). There is a similar example of this technique in the Compile routine of the regexp package. The convention in the Go libraries is that even when a package uses panic internally, its external API still presents explicit error return values.

Other uses of defer (beyond the file.Close() example given earlier) include releasing a mutex:

mu.Lock() 
defer mu.Unlock 
+11

C++ può restituire anche più valori - è sufficiente avvolgerli in una struttura. –

+5

per compensare l'assenza di eccezioni, penso che cambiare il tipo di valore di ritorno di una funzione (e chiamanti se necessario) con "struct" personalizzate a mano sia MOLTO disturbante per la logica, non pratico e soggetto a errori. – reechard

+2

@Neil: +1 Non hai nemmeno bisogno di definire le tue strutture: 'std :: pair',' boost :: tuple' (c'è anche una tupla in C++ 0x) –

risposta

14

rendimenti multipli non sono unici andare, e non sono un sostituto per le eccezioni. In termini C (o C++), sono un sostituto conciso e user-friendly per la restituzione di una struct (oggetto) contenente più valori.

Forniscono un mezzo conveniente per indicare gli errori, se questo è ciò che intendi.

Perché le "affermazioni" sono considerate alternative?

Gli avvisi sono inizialmente per il debug. Fermano il programma in situazioni in cui è in uno stato "impossibile", quello che il progetto dice che non dovrebbe accadere, ma che ha comunque. Restituire un errore è improbabile che aiuti molto. La base di codice ovviamente non funziona ancora, quindi come mai riuscirà a recuperare? Perché lo vorresti, quando c'è un bug che richiede attenzione?

L'utilizzo di asserti nel codice di produzione è un po 'diverso, ovviamente ci sono problemi di prestazioni e dimensioni del codice, quindi l'approccio normale è rimuoverli una volta che l'analisi del codice e i test ti hanno convinto che le situazioni "impossibili" sono impossibiliMa, se stai usando il codice a questo livello di paranoia, che sta controllando se stesso, allora probabilmente sei anche paranoico che se lo lasci continuare a correre in uno stato "impossibile", allora potrebbe fare qualcosa di pericoloso: corrompere dati preziosi, che superano l'allocazione dello stack e forse creano vulnerabilità di sicurezza. Quindi, di nuovo, vuoi solo spegnerlo il prima possibile.

La roba si utilizza afferma per la realtà non è lo stesso di quello che usate eccezioni per: durante la programmazione di linguaggi come C++ e Java fornire eccezioni per situazioni "impossibili" (logic_error, ArrayOutOfBoundsException), hanno involontariamente incoraggiano alcuni programmatori a pensa che i loro programmi dovrebbero tentare di recuperare da situazioni in cui sono davvero fuori controllo. A volte è appropriato, ma il consiglio di Java di non rilevare RuntimeExceptions è lì per una buona ragione. Ogni tanto è una buona idea prenderne una, ed è per questo che esistono. Quasi sempre non è una buona idea catturarli, nel senso che equivalgono a fermare il programma (o almeno il thread) comunque.

+0

Sì, questo è quello che intendo, un modo per indicare una condizione di errore (fino allo stack delle chiamate) che non disturba troppo la logica esistente. – reechard

+0

Dipende da come si utilizza il valore restituito. Per la maggior parte del tempo non è necessario l'intero intervallo del tipo di reso. Se stai restituendo il numero di dipendenti della tua azienda, allora non penso che (numero, iscorrect) dia alcuna differenza nella logica dal numero di ritorno, o -1 per errore. Rende più difficile per il chiamante ignorare il caso di errore, ma la loro logica per gestire l'errore è la stessa: se (qualcosa) poi (gestirlo). –

+0

@Steve: non del tutto, con eccezioni è possibile avere un blocco di codice logico in bundle ed elaborare le condizioni di errore in un secondo momento ('catch'). Questo fa la differenza poiché la logica è raggruppata insieme e quindi più facile da seguire. Ricordo alcuni compiti a casa a scuola in C, dove per ogni riga di codice reale c'era un 'se' e ​​un paio di righe di elaborazione degli errori che risultavano sempre sostanzialmente uguali: un messaggio all'utente e la restituzione di un errore nella banda token al chiamante, quando le eccezioni si sarebbero propagate bene e potrebbero essere facilmente elaborate dove ha senso. –

2

Sì, i valori di ritorno degli errori sono buoni ma non catturano il vero significato della gestione delle eccezioni ... vale a dire l'abilità e la gestione di casi eccezionali in cui normalmente non si intende.

Java (cioè) la progettazione considera eccezioni IMO di essere validi workflow scenari e hanno un punto circa la complessità delle interfacce e librerie dover dichiarare e la versione questi eccezione generata, ma ahimè eccezioni servono un ruolo importante nella pila di domino.

Pensa al caso alternativo in cui i codici di ritorno eccezionali vengono gestiti in modo condizionale in diverse dozzine di chiamate di metodo in profondità. Che aspetto avrebbero le tracce in termini di posizione in cui si trova il numero di riga illecito?

4

Si dovrebbe leggere un paio di articoli sulle eccezioni per rendersi conto che i valori di ritorno non sono eccezioni. Non nel modo C 'in-band' o in nessun altro modo.

Senza entrare in un argomento profondo, le eccezioni sono pensate per essere lanciate dove viene trovata la condizione di errore e catturate dove la condizione di errore può essere gestita in modo significativo. I valori restituiti vengono elaborati solo nella primissima funzione nello stack di gerarchia, che potrebbe o non potrebbe essere in grado di elaborare il problema. Un semplice esempio potrebbe essere un file di configurazione che può recuperare valori come stringhe, e supporta anche la trasformazione in istruzioni return digitati:

class config { 
    // throws key_not_found 
    string get(string const & key); 
    template <typename T> T get_as(string const & key) { 
     return boost::lexical_cast<T>(get(key)); 
    } 
}; 

Ora il problema è come si fa a gestire se non è stata trovata la chiave. Se si utilizzano i codici di ritorno (ad esempio nel percorso), il problema è che get_as deve gestire il codice di errore da get e agire di conseguenza. Poiché in realtà non sa cosa fare, l'unica cosa sensata è manualmente propagare l'errore a monte:

class config2 { 
    pair<string,bool> get(string const & key); 
    template <typename T> pair<T,bool> get_as(string const & key) { 
     pair<string,bool> res = get(key); 
     if (!res.second) { 
      try { 
      T tmp = boost::lexical_cast<T>(res.first); 
      } catch (boost::bad_lexical_cast const &) { 
      return make_pair(T(), false); // not convertible 
      } 
      return make_pair(boost::lexical_cast<T>(res.first), true); 
     } else { 
      return make_pair(T(), false); // error condition 
     } 
    } 
} 

L'implementatore della classe deve aggiungere il codice aggiuntivo per inoltrare gli errori, e che il codice viene mescolati con la vera logica del problema. In C++ questo è probabilmente più oneroso che in un linguaggio progettato per assegnazioni multiple (a,b=4,5) ma ancora, se la logica dipende dal possibile errore (in questo caso chiamare lexical_cast dovrebbe essere eseguito solo se abbiamo una stringa effettiva) allora dovrete cache valori in variabili comunque.

+0

Ho familiarità con le eccezioni e le difficoltà. "Eccezionale C++" di Herb Sutter è un buon libro. Mi hai traviato - "i valori di ritorno non sono eccezioni". Detto questo, "config2" sembra abbastanza generico per un modello "oggetto valido e codice di successo rispetto a oggetto predefinito e codice di errore". – reechard

+0

Ho dimenticato di aggiungere un fermo alla funzione 'lexical_cast'. Se le eccezioni sono state gettate via, 'lexical_cast' (abbastanza semplice) potrebbe essere implementato con lo stesso modello valore/stato, ma il codice non sarebbe molto più bello di quello che è con try/catch, a meno che il valore di ritorno del cast modificato era esattamente uguale a quello del metodo 'get_as' e poteva quindi essere inoltrato direttamente. –

+1

In ogni caso, il codice diventa molto più complesso in ogni nuova fase, è necessario aggiungere codice per controllare le dichiarazioni di flusso e di ritorno in ogni possibile punto di errore. –

2

Questa domanda è un po 'complicata per rispondere obiettivamente, e le opinioni sulle eccezioni possono differire parecchio.

Ma se dovessi speculare, penso che il motivo principale per cui le eccezioni non sono incluse in Go è perché complica il compilatore e può portare a implicazioni non banali quando si scrivono le librerie. Le eccezioni sono difficili da ottenere e hanno la priorità nel far funzionare qualcosa.

La differenza principale tra errori di gestione tramite valori di ritorno ed eccezioni è che le eccezioni costringono il programmatore a gestire condizioni insolite. Non puoi mai avere un "errore silenzioso" a meno che non rilevi esplicitamente un'eccezione e non faccia nulla nel blocco catch. D'altra parte, si ottengono punti di ritorno impliciti ovunque all'interno delle funzioni che possono portare ad altri tipi di bug. Ciò è particolarmente diffuso con C++ in cui gestisci la memoria in modo esplicito e devi assicurarti di non perdere mai un puntatore a qualcosa che hai allocato.

Esempio di situazione pericolosa in C++:

struct Foo { 
    // If B's constructor throws, you leak the A object. 
    Foo() : a(new A()), b(new B()) {} 
    ~Foo() { delete a; delete b; } 

    A *a; 
    B *b; 
}; 

valori restituiti multiple facilita implementare la gestione degli errori valore restituito base, senza dover ricorrere a fuori argomenti alle funzioni, ma non cambia nulla fondamentalmente.

Alcune lingue hanno valori di ritorno multipli ed eccezioni (o meccanismi simili). Un esempio è Lua.

3

Non è Go, ma in Lua, il ritorno multiplo è un linguaggio estremamente comune per la gestione delle eccezioni.

se si ha una funzione come

function divide(top,bottom) 
    if bottom == 0 then 
     error("cannot divide by zero") 
    else 
     return top/bottom 
    end 
end 

Poi, quando bottom era 0, un'eccezione sarebbe alzato e l'esecuzione del programma potrebbe fermare, a meno che non avvolto la funzione divide in un pcall (or protected call).

pcall restituisce sempre due valori: il primo è risultato è un valore booleano che indica se la funzione è stata restituita correttamente e il secondo risultato è il valore restituito o il messaggio di errore.

Il seguente (forzato) Lua frammento mostra questo in uso:

local top, bottom = get_numbers_from_user() 
local status, retval = pcall(divide, top, bottom) 
if not status then 
    show_message(retval) 
else 
    show_message(top .. " divided by " .. bottom .. " is " .. retval) 
end 

Naturalmente, non è necessario utilizzare pcall, se la funzione che stai chiamando già ritorna sotto forma di status, value_or_error.

Il ritorno multiplo è stato abbastanza buono per Lua per diversi anni, quindi mentre quello non è assicurare che è abbastanza buono per Go, è di supporto all'idea.

+0

'pcall' è più vicino a una chiamata' catch' in stile erlang qui. l'errore si accumula svolgendosi e salterà i fotogrammi passati che non stanno facendo il rilevamento degli errori a uno che è. Questo è abbastanza diverso. – Dustin

1

Ecco un esempio di come più valori di ritorno potrebbero funzionare in C++. Non scriverei questo codice da solo, ma non penso che sia del tutto fuori discussione utilizzare un simile approccio.

#include <iostream> 
#include <fstream> 
#include <string> 
using namespace std; 

// return value type 
template <typename T> 
struct RV { 
    int mStatus; 
    T mValue; 

    RV(int status, const T & rv) 
     : mStatus(status), mValue(rv) {} 
    int Status() const { return mStatus; } 
    const T & Value() const {return mValue; } 
}; 

// example of possible use 
RV <string> ReadFirstLine(const string & fname) { 
    ifstream ifs(fname.c_str()); 
    string line; 
    if (! ifs) { 
     return RV <string>(-1, ""); 
    } 
    else if (getline(ifs, line)) { 
     return RV <string>(0, line); 
    } 
    else { 
     return RV <string>(-2, ""); 
    } 
} 

// in use 
int main() { 
    RV <string> r = ReadFirstLine("stuff.txt"); 
    if (r.Status() == 0) { 
     cout << "Read: " << r.Value() << endl; 
    } 
    else { 
     cout << "Error: " << r.Status() << endl; 
    } 
} 
+0

:) Questo mi ricorda un codice proprietario che ho letto un paio di mesi fa, in cui hanno usato questo tipo di approccio per garantire che i valori di ritorno venissero sempre elaborati. La classe dovrebbe tenere traccia del fatto che sia stato chiamato 'Status()' e ** throw ** ed eccezione (nelle build di debug) durante la distruzione, se non fosse stato. Le eccezioni sono state proibite da qualsiasi altra parte del codice, quindi non ci sarebbe alcun 'try/catch 'in qualsiasi punto dello stack di chiamate, l'eccezione avrebbe ucciso l'applicazione. Mi chiedo come siano arrivati ​​a quel codice ... –

-2

Se avete bisogno del senso C++ di fare un "annullabile" l'uso oggetto boost :: opzionale < T>. Lo si verifica come booleano e se si valuta vero, si denomina a un T. valido

+1

si prega di fare riferimento alla domanda posta. Non chiede nulla su C++ e non ho bisogno di nulla "nullable". Se questa è una risposta alla risposta sopra, lasciala nei commenti. – reechard