Perché progettare un linguaggio moderno senza un meccanismo di gestione delle eccezioni?

44

Molte lingue moderne forniscono una ricca gestione delle eccezioni , ma il linguaggio di programmazione Swift di Apple non fornisce un meccanismo di gestione delle eccezioni .

Nonostante le eccezioni, ho difficoltà a capire cosa significhi. Swift ha asserzioni e, naturalmente, valori di ritorno; ma ho difficoltà a immaginare come il mio modo di pensare basato sull'eccezione sia mappato a un mondo senza eccezioni (e, per questo, perché un tale mondo è desiderabile ). Ci sono cose che non posso fare in una lingua come Swift che potrei fare con le eccezioni? Guadagno qualcosa perdendo le eccezioni?

Ad esempio, potrei esprimere al meglio qualcosa come

try:
    operation_that_can_throw_ioerror()
except IOError:
    handle_the_exception_somehow()
else:
     # we don't want to catch the IOError if it's raised
    another_operation_that_can_throw_ioerror()
finally:
    something_we_always_need_to_do()

in una lingua (Swift, ad esempio) che manca di gestione delle eccezioni?

    
posta orome 03.10.2014 - 20:47
fonte

8 risposte

33

Nella programmazione incorporata, le eccezioni non erano consentite tradizionalmente, perché il sovraccarico dello stack che devi svolgere era considerato una variabilità inaccettabile quando si cercava di mantenere le prestazioni in tempo reale. Mentre gli smartphone potrebbero tecnicamente essere considerati piattaforme in tempo reale, sono abbastanza potenti ora dove le vecchie limitazioni dei sistemi incorporati non si applicano più realmente. Lo faccio solo per ragioni di completezza.

Le eccezioni sono spesso supportate nei linguaggi di programmazione funzionale, ma sono usate così raramente che potrebbero anche non esserlo. Una ragione è la valutazione pigra, che viene eseguita occasionalmente anche in lingue che non sono pigre per impostazione predefinita. Avere una funzione che viene eseguita con uno stack diverso rispetto al posto in cui è stata accodata per eseguire rende difficile determinare dove inserire il gestore delle eccezioni.

L'altro motivo è che le funzioni di prima classe consentono costrutti come opzioni e futures che offrono i vantaggi sintattici delle eccezioni con maggiore flessibilità. In altre parole, il resto della lingua è abbastanza espressivo che le eccezioni non ti comprano nulla.

Non ho familiarità con Swift, ma il poco che ho letto sulla sua gestione degli errori suggerisce che intendevano la gestione degli errori per seguire più schemi di stile funzionale. Ho visto esempi di codice con blocchi success e failure che assomigliano molto ai futures.

Ecco un esempio che utilizza un Future da questo tutorial di Scala :

val f: Future[List[String]] = future {
  session.getRecentPosts
}
f onFailure {
  case t => println("An error has occured: " + t.getMessage)
}
f onSuccess {
  case posts => for (post <- posts) println(post)
}

Puoi vedere che ha approssimativamente la stessa struttura del tuo esempio usando le eccezioni. Il blocco future è come un try . Il blocco onFailure è come un gestore di eccezioni. In Scala, come nella maggior parte dei linguaggi funzionali, Future è implementato completamente utilizzando il linguaggio stesso. Non richiede alcuna sintassi speciale come fanno le eccezioni. Ciò significa che puoi definire i tuoi stessi costrutti simili. Forse aggiungi un blocco timeout , ad esempio, o la funzionalità di registrazione.

Inoltre, è possibile passare il futuro in giro, restituirlo dalla funzione, memorizzarlo in una struttura dati o qualsiasi altra cosa. È un valore di prima classe. Non sei limitato come eccezioni che devono essere propagate direttamente nello stack.

Le opzioni risolvono il problema di gestione degli errori in un modo leggermente diverso, che funziona meglio per alcuni casi d'uso. Non sei bloccato con un solo metodo.

Queste sono le cose che "guadagni perdendo le eccezioni".

    
risposta data 03.10.2014 - 23:00
fonte
18

Le eccezioni possono rendere il codice più difficile da ragionare. Sebbene non siano così potenti come gotos, possono causare molti degli stessi problemi a causa della loro natura non locale. Ad esempio, supponiamo di avere un pezzo di codice imperativo come questo:

cleanMug();
brewCoffee();
pourCoffee();
drinkCoffee();

Non si capisce a colpo d'occhio se una di queste procedure può generare un'eccezione. Devi leggere la documentazione di ciascuna di queste procedure per capirlo. (Alcune lingue rendono questo leggermente più semplice aumentando la firma del tipo con queste informazioni.) Il codice sopra verrà compilato bene indipendentemente dal fatto che una qualsiasi delle procedure passi, rendendo davvero facile dimenticare di gestire un'eccezione.

Inoltre, anche se l'intento è quello di propagare l'eccezione al chiamante, spesso è necessario aggiungere altro codice per evitare che le cose vengano lasciate in uno stato incoerente (ad esempio se il tuo caffè si rompe, devi comunque pulire pasticciare e restituire la tazza!). Pertanto, in molti casi il codice che utilizza le eccezioni sembrerebbe tanto complesso quanto il codice che non lo richiedeva a causa della pulizia extra richiesta.

Le eccezioni possono essere emulate con un sistema di tipi sufficientemente potente. molte delle lingue che evitano le eccezioni utilizzano valori di ritorno per ottenere lo stesso comportamento. È simile a come è fatto in C, ma i sistemi di tipo moderno di solito lo rendono più elegante e anche più difficile da dimenticare per gestire la condizione di errore. Possono anche fornire zucchero sintattico per rendere le cose meno ingombranti, a volte quasi pulite come con le eccezioni.

In particolare, incorporando la gestione degli errori nel sistema dei tipi piuttosto che implementarli come funzionalità separata consente di utilizzare "eccezioni" per altre cose che non sono nemmeno correlate agli errori. (È noto che la gestione delle eccezioni è in realtà una proprietà delle monadi).

    
risposta data 04.10.2014 - 06:06
fonte
9

Ci sono alcune grandi risposte qui, ma penso che una ragione importante non sia stata enfatizzata abbastanza: Quando si verificano eccezioni, gli oggetti possono essere lasciati in stati non validi. Se è possibile "catturare" un'eccezione, il codice del gestore delle eccezioni sarà in grado di accedere e utilizzare quegli oggetti non validi. Ciò andrà in modo orribilmente sbagliato a meno che il codice per quegli oggetti sia stato scritto perfettamente, il che è molto, molto difficile da fare.

Ad esempio, immagina di implementare Vector. Se qualcuno crea un'istanza del tuo vettore con un insieme di oggetti, ma durante l'inizializzazione si verifica un'eccezione (forse, per esempio, durante la copia degli oggetti nella memoria appena assegnata), è molto difficile codificare correttamente l'implementazione Vector in modo tale che la memoria è trapelata. Questo breve documento di Stroustroup copre l'esempio Vettoriale .

E quella è solo la punta dell'iceberg. Cosa succede se, ad esempio, hai copiato alcuni degli elementi, ma non tutti gli elementi? Per implementare correttamente un contenitore come Vector, devi quasi rendere reversibile ogni azione che esegui, quindi l'intera operazione è atomica (come una transazione di database). Questo è complicato e molte applicazioni sbagliano. E anche quando è fatto correttamente, complica notevolmente il processo di implementazione del contenitore.

Quindi alcune lingue moderne hanno deciso che non ne vale la pena. Ad esempio, Rust ha delle eccezioni, ma non possono essere "catturate", quindi non c'è modo per il codice di interagire con gli oggetti in uno stato non valido.

    
risposta data 12.02.2015 - 21:31
fonte
6

Una cosa che inizialmente mi ha sorpreso del linguaggio Rust è che non supporta le eccezioni di cattura. Puoi lanciare eccezioni, ma solo il runtime può catturarle quando un task (il thread di pensiero, ma non sempre un thread del SO separato) muore; se inizi tu stesso un'attività, allora puoi chiedere se è uscito normalmente o se è fail!() ed.

In quanto tale, non è unidirezionale a fail molto spesso. I pochi casi in cui si verificano, ad esempio, sono nel cablaggio di test (che non sa come è il codice utente), come il livello principale di un compilatore (la maggior parte dei compilatori invece), o quando si richiama un callback sull'input dell'utente.

Invece, l'idioma comune è usare il Result template in modo esplicito passare gli errori che dovrebbero essere gestiti. Ciò è reso significativamente più facile dalla macro try! , che può essere racchiusa in qualsiasi espressione che produce un risultato e produce il braccio riuscito se ce n'è uno, o restituisce in anticipo dalla funzione.

use std::io::IoResult;
use std::io::File;

fn load_file(name: &Path) -> IoResult<String>
{
    let mut file = try!(File::open(name));
    let s = try!(file.read_to_string());
    return Ok(s);
}

fn main()
{
    print!("{}", load_file(&Path::new("/tmp/hello")).unwrap());
}
    
risposta data 04.10.2014 - 06:53
fonte
5

A mio parere, le eccezioni sono uno strumento essenziale per rilevare errori di codice in fase di esecuzione. Sia nei test che nella produzione. Rendi i loro messaggi abbastanza dettagliati così in combinazione con una traccia dello stack puoi capire cosa è successo da un log.

Le eccezioni sono principalmente uno strumento di sviluppo e un modo per ottenere rapporti di errore ragionevoli dalla produzione in casi imprevisti.

A prescindere dalla separazione delle preoccupazioni (percorso felice con solo errori attesi vs. caduta fino a raggiungere qualche gestore generico per errori imprevisti) essendo una buona cosa, rendendo il tuo codice più leggibile e mantenibile, è infatti impossibile preparare il codice per tutti i possibili casi imprevisti, anche ingigantendoli con codice di gestione degli errori per completare l'illeggibilità.

Questo è in realtà il significato di "inaspettato".

Btw., cosa è previsto e cosa no è una decisione che può essere presa solo nel sito di chiamata. Ecco perché le eccezioni controllate in Java non hanno funzionato: la decisione è presa al momento dello sviluppo di un'API, quando non è affatto chiaro cosa è previsto o inaspettato.

Semplice esempio: l'API di una mappa hash può avere due metodi:

Value get(Key)

e

Option<Value> getOption(key)

il primo lancio di un'eccezione se non trovato, quest'ultimo che ti dà un valore opzionale. In alcuni casi, quest'ultimo ha più senso, ma in altri il tuo codice deve aspettarsi che ci sia un valore per una determinata chiave, quindi se non ce n'è uno, questo è un errore che questo codice non può correggere perché un l'ipotesi è fallita. In questo caso è in realtà il comportamento desiderato a cadere fuori dal percorso del codice e verso il basso per alcuni gestori generici nel caso in cui la chiamata fallisce.

Il codice non dovrebbe mai provare a gestire ipotesi di base fallite.

Tranne controllandoli e lanciando eccezioni ben leggibili, naturalmente.

Lanciare le eccezioni non è malvagio, ma catturarle potrebbe essere. Non cercare di correggere errori imprevisti. Cattura le eccezioni in alcuni punti in cui desideri continuare un ciclo o un'operazione, registrali, magari segnala un errore sconosciuto, e il gioco è fatto.

I blocchi di cattura dappertutto sono una pessima idea.

Progetta le tue API in modo che sia facile esprimere la tua intenzione, ovvero dichiarare se ti aspetti un determinato caso, come la chiave non trovata o meno. Gli utenti della tua API possono quindi scegliere la chiamata di lancio solo per casi realmente imprevisti.

Suppongo che la ragione per cui le persone si risentono delle eccezioni e spingono troppo lontano omettendo questo strumento cruciale per l'automazione della gestione degli errori e una migliore separazione delle preoccupazioni dalle nuove lingue sono esperienze negative.

Questo e alcuni fraintendimenti su ciò per cui sono effettivamente utili.

Simularli eseguendo TUTTO attraverso il binding monadico rende il tuo codice meno leggibile e manutenibile, e finisci senza una traccia stack, il che rende questo approccio WAY peggio.

La gestione degli errori di stile funzionale è ottima per i casi di errore previsti.

Lascia che la gestione delle eccezioni si occupi automaticamente di tutto il resto, ecco a cosa serve:)

    
risposta data 31.05.2016 - 09:38
fonte
3

Swift usa gli stessi principi qui come Objective-C, solo più di conseguenza. In Objective-C, le eccezioni indicano errori di programmazione. Non vengono gestiti se non dagli strumenti di segnalazione degli arresti anomali. La "gestione delle eccezioni" viene eseguita correggendo il codice. (Ci sono alcune eccezioni, ad esempio nelle comunicazioni tra processi, ma questo è abbastanza raro e molte persone non ci riescono mai, e oggettivo-C ha effettivamente provato / cattura / finalmente / lancia, ma raramente li usi). Swift ha appena rimosso la possibilità di catturare le eccezioni.

Swift ha una caratteristica che assomiglia alla gestione delle eccezioni ma è solo una gestione forzata degli errori. Storicamente, Objective-C aveva un pattern di gestione degli errori abbastanza pervasivo: un metodo restituiva un BOOL (YES per successo) o un riferimento oggetto (nil per errore, non nil per successo) e ha un parametro "puntatore a NSError *" che verrebbe utilizzato per memorizzare un riferimento NSError. Swift converte automagicamente le chiamate a tale metodo in qualcosa che assomiglia alla gestione delle eccezioni.

In generale, le funzioni di Swift possono facilmente restituire alternative, come un risultato se una funzione ha funzionato bene e un errore se non ha funzionato; ciò rende la gestione degli errori molto più semplice. Ma la risposta alla domanda iniziale: i designer di Swift sentivano chiaramente che creare un linguaggio sicuro e scrivere codice di successo in tale linguaggio è più facile se la lingua non ha eccezioni.

    
risposta data 29.08.2016 - 18:43
fonte
2
 int result;
 if((result = operation_that_can_throw_ioerror()) == IOError)
 {
  handle_the_exception_somehow();
 }
 else
 {
   # we don't want to catch the IOError if it's raised
   result = another_operation_that_can_throw_ioerror();
 }
 result |= something_we_always_need_to_do();
 return result;

In C si finirebbe con qualcosa di simile a quanto sopra.

Are there things I can't do in Swift that I could do with exceptions?

No, non c'è niente. Finisci per gestire i codici risultato invece delle eccezioni.
Le eccezioni ti permettono di riorganizzare il tuo codice in modo che la gestione degli errori sia separata dal tuo felice codice di percorso, ma questo è tutto.

    
risposta data 03.10.2014 - 21:27
fonte
1

Oltre alla risposta di Charlie:

Questi esempi di gestione delle eccezioni dichiarate che vedi in molti manuali e libri, sembrano molto intelligenti solo su esempi molto piccoli.

Anche se metti da parte l'argomento sullo stato dell'oggetto non valido, causano sempre un enorme dolore quando si ha a che fare con una grande app.

Ad esempio, quando si ha a che fare con l'IO, usando della crittografia, si possono avere 20 tipi di eccezioni che possono essere scartate da 50 metodi. Immagina la quantità di codice di gestione delle eccezioni di cui avrai bisogno. La gestione delle eccezioni richiederà molte volte più codice rispetto al codice stesso.

In realtà sai quando l'eccezione non può apparire e non hai mai bisogno di scrivere tanta gestione delle eccezioni, quindi devi solo usare qualche soluzione alternativa per ignorare le eccezioni dichiarate. Nella mia pratica, solo il 5% circa delle eccezioni dichiarate devono essere gestite in codice per avere un'app affidabile.

    
risposta data 28.08.2016 - 14:51
fonte