Il codice seguente è abbastanza autoesplicativo: copia e incolla tutto in un modulo ed eseguilo , fornisce alcuni casi duso e molti commenti esplicativi nel testo. (Funziona ma mi interessa sapere cosa ne pensano gli altri e per eventuali suggerimenti che potresti dare.)
I fatti più importanti da realizzare sono:
-
Quando si usa in caso di errore goto Label1 la procedura entra in uno stato di “Sto gestendo un errore” poiché è stata sollevata uneccezione. Quando è in questo stato, se viene eseguita unaltra istruzione label2 “On Error Goto” NON andrà a label2, ma solleva un errore che viene passato al codice che ha chiamato la procedura.
-
Puoi impedire che una procedura si trovi nello stato “Io” sto gestendo un errore “cancellando leccezione (impostando err su niente in modo che la proprietà err.number diventi 0) utilizzando
Err.clear or On Error Goto -1 " Which I think is less clear!
(NOTA che On Error Goto 0
è diverso da quanto sopra)
È anche importante notare che Err.Clear lo reimposta a zero ma in realtà è equivalente a:
On Error Goto -1 On Error Goto 0
cioè Err.Clear rimuove un “On Error Goto” che è attualmente in uso. Quindi, quindi, è meglio utilizzare:
On Error Goto -1
poiché si utilizza Err.clear Spesso è necessario write
Err.Clear On Error Goto MyErrorHandlerLabel
Uso le tecniche precedenti con varie etichette per simulare le funzionalità a volte utili fornite dai blocchi TRY CATCH di Visual basic, che penso abbiano il loro posto nella scrittura codice leggibile.
È vero che questa tecnica crea poche righe di codice in più rispetto a una bella istruzione VB try catch, ma non è troppo complicata e abbastanza facile da ottenere r testa intorno.
PS. Anche di interesse potrebbe essere la procedura ManageErrSource che fa in modo che la proprietà Err.Source memorizzi la procedura in cui si è verificato lerrore.
Option Compare Database Option Explicit Dim RememberErrNumber As Long Dim RememberErrDescription As String Dim RememberErrSource As String Dim RememberErrLine As Integer Private Sub RememberThenClearTheErrorObject() On Error Resume Next " For demo purposes Debug.Print "ERROR RAISED" Debug.Print Err.Number Debug.Print Err.Description Debug.Print Err.Source Debug.Print " " " This function has to be declared in the same scope as the variables it refers to RememberErrNumber = Err.Number RememberErrDescription = Err.Description RememberErrSource = Err.Source RememberErrLine = Erl() " Note that the next line will reset the error object to 0, the variables above are used to remember the values " so that the same error can be re-raised Err.Clear " Err.Clear is used to clear the raised exception and set the err object to nothing (ie err.number to 0) " If Err.Clear has not be used, then the next "On Error GoTo ALabel" that is used in this or the procedure that called it " will actually NOT pass execution to the ALabel: label BUT the error is paseed to the procedure that called this procedure. " Using Err.Clear (or "On Error GoTo -1 ") gets around this and facilitates the whole TRY CATCH block scenario I am using there. " For demo purposes Debug.Print "ERROR RAISED is now 0 " Debug.Print Err.Number Debug.Print Err.Description Debug.Print Err.Source Debug.Print " " " For demo purposes Debug.Print "REMEMBERED AS" Debug.Print RememberErrNumber Debug.Print RememberErrDescription Debug.Print RememberErrSource Debug.Print " " End Sub Private Sub ClearRememberedErrorObjectValues() " This function has to be declared in the same scope as the variables it refers to RememberErrNumber = 0 RememberErrDescription = "" RememberErrSource = "" RememberErrLine = 0 End Sub Sub ExampleOfTryCatchBlockInVBA() On Error GoTo HandleError " ----------------------------------------------------- " SubProcedure1 has the example of a multiple line TRY block with a block of code executed in the event of an error SubProcedure1 Exit Sub HandleError: Select Case Err.Number Case 0 " This shold never happen as this code is an error handler! " However if it does still allow the Err.raise to execute below. (In this case Err.raise will fail " and itself will raise an error "Invalid procedure call or argument" indicating that 0 cannot be used to raise and error! Case 111111 " You might want to do special error handling for some predicted error numbers " perhaps resulting in a exit sub with no error or " perhaps using the Err.raise below Case Else " Just the Err.raise below is used for all other errors End Select " " I include the procedure ManageErrSource as an exmple of how Err.Source can be used to maintain a call stack of procedure names " and store the name of the procedure that FIRST raised the error. " Err.Raise Err.Number _ , ManageErrSource("MyModuleName", Err.Source, Erl(), "tsub1_RaisesProcedureNotFoundError") _ , Err.Number & "-" & Err.Description " Note the next line never gets excuted, but I like to have resume in the code for when I am debugging. " (When a break is active, by moving the next executable line onto it, and using step over, it moves the exection point to the line that actually raised the error) Resume End Sub Sub SubProcedure1() " ----------------------------------------------------- " Example of a multiple line TRY block with a Case statement used to CATCH the error " " It is sometimes better to NOT use this technique but to put the code in it"s own procedure " (ie I refer to the code below that is surrounded by the tag #OWNSUB) . " However,sometimes using this technique makes code more readable or simpler! " Dim i As Integer " This line puts in place the defualt error handler found at the very foot of the procedure On Error GoTo HandleError " " Perhaps lots of statements and code here " " First an example with comments " ----------------------------------------------------- " TRY BLOCK START " This next line causes execution to "jump" to the "catch" block in the event an error is detected. On Error GoTo CatchBlock1_Start " #OWNSUB tsub_WillNotRaiseError_JustPrintsOk If vbYes = MsgBox("1. Do you want to raise an error in the try block? - (PRESS CTRL+BREAK now then choose YES, try no later.)", vbYesNo) Then i = 100 / 0 End If " " Perhaps lots of statements and code here " " #OWNSUB " TRY BLOCK END " ----------------------------------------------------- " ----------------------------------------------------- " CATCH BLOCK START CatchBlock1_Start: If Err.Number = 0 Then On Error GoTo HandleError " Re-instates the procedure"s generic error handler " This is also done later, but I think putting it here reduces the likelyhood of a coder accidentally removing it. Else " WARNING: BE VERY CAREFUL with any code that is written here as " the "On Error GoTo CatchBlock1_Start" is still in effect and therefore any errors that get raised could goto this label " and cause and infinite loop. " NOTE that a replacement "On Error Goto" cannot be executed until Err.clear is used, otherwise the "On Error Goto" " will itself raise and error. " THEREFORE KEEP THE CODE HERE VERY SIMPLE! " RememberThenClearTheErrorObject should be the only code executed and this called procedure must be tight! " This saves the details of the error in variables so that the "On Error GoTo HandleError" can be used " to determine how the next Err.Raise used below is handled (and also how any unexpected implicitly raised errors are handled) RememberThenClearTheErrorObject On Error GoTo HandleError "#THISLINE# If vbYes = MsgBox("2. Do you want to raise an error in the erro handler? - (PRESS CTRL+BREAK now then try both YES and NO )", vbYesNo) Then i = 100 / 0 End If Select Case RememberErrNumber Case 0: " No Error, do Nothing Case 2517 Debug.Print "The coder has decided to just give a Warning: Procedure not found " & Err.Number & " - " & Err.Description ClearRememberedErrorObjectValues " Not essential, but might save confusion if coding errors are made Case Else " An unexepected error or perhaps an (user) error that needs re-raising occurred and should to be re-raised " NOTE this is giving an example of what woudl happen if the CatchBlock1_ErrorElse is not used below If vbYes = MsgBox("3. Do you want to raise an error in the ELSE error handler? CatchBlock1_ErrorElse *HAS NOT* been used? - (PRESS CTRL+BREAK now then try both YES and NO )", vbYesNo) Then i = 100 / 0 End If On Error GoTo CatchBlock1_ErrorElse " SOME COMPLEX ERROR HANDLING CODE - typically error logging, email, text file, messages etc.. " Because the error objects values have been stored in variables, you can use " code here that might itself raise an error and CHANGE the values of the error object. " You might want to surround the code with the commented out CatchBlock1_ErrorElse lines " to ignore these errors and raise the remembered error. (or if calling a error handling module " just use on error resume next). " Without the CatchBlock1_ErrorElse lines any error raised in this "complex code" will be handled by the " active error handler which was set by the "On Error GoTo HandleError" tagged as "#THISLINE#" above. If vbYes = MsgBox("4. Do you want to raise an error in the ELSE error handler when CatchBlock1_ErrorElse HAS been used? - (PRESS CTRL+BREAK now then try both YES and NO )", vbYesNo) Then i = 100 / 0 End If CatchBlock1_ErrorElse: On Error GoTo HandleError " This line must be preceeded by an new "On error goto" for obvious reasons Err.Raise RememberErrNumber, RememberErrSource, RememberErrDescription End Select On Error GoTo HandleError End If " CATCH BLOCK END " ----------------------------------------------------- On Error GoTo HandleError " Unnecessary but used to delimt the catch block " " lots of code here perhaps " " ----------------------------------------------------- " Example 2 " " In this example goto statements are used instead of the IF statement used in example 1 " and no explanitory comments are given (so you can see how simple it can look) " " ----------------------------------------------------- " TRY BLOCK START On Error GoTo CatchBlock2_Start tsub_WillNotRaiseError_JustPrintsOk If vbYes = MsgBox("Do you want to raise an error? - (PRESS CTRL+BREAK now then choose YES)", vbYesNo) Then i = 100 / 0 End If " " Perhaps lots of statements and code here " " TRY BLOCK END " ----------------------------------------------------- GoTo CatchBlock2_End: CatchBlock2_Start: RememberThenClearTheErrorObject On Error GoTo HandleError Select Case RememberErrNumber Case 0: " No Error, do Nothing Case 2517 Debug.Print "The coder has decided to just give a Warning: Procedure not found " & Err.Number & " - " & Err.Description ClearRememberedErrorObjectValues " Not essential, but might save confusion if coding errors are made Case Else " An unexepected error or perhaps an (user) error that needs re-raising occurred and should to be re-raised " In this case the unexpecetd erro will be handled by teh code that called this procedure " This line must be preceeded by an new "On error goto" for obvious reasons Err.Raise RememberErrNumber, RememberErrSource, RememberErrDescription End Select On Error GoTo HandleError End If CatchBlock2_End: " CATCH BLOCK END " ----------------------------------------------------- On Error GoTo HandleError " Unnecessary but used to delimt the catch block " " Here you could add lots of lines of vba statements that use the generic error handling that is after the HandleError: label " " " " You could of course, alway add more TRY CATCH blocks like the above " " Exit Sub HandleError: Select Case Err.Number Case 0 " This shold never happen as this code isan error handler! " However if it does still allow the Err.raise to execute below. (In this case Err.raise will fail " and itself will raise an error "Invalid procedure call or argument" indicating that 0 cannot be used to raise and error! Case 111111 " You might watch to do special error handling for some predicted error numbers " perhaps exit sub " Perhaps using the Err.raise below End Select " ie Otherwise " " Note that I use the Err.Source to maintain a call stack of procedure names " Err.Raise Err.Number _ , ManageErrSource("MyModuleName", Err.Source, Erl(), "tsub1_RaisesProcedureNotFoundError") _ , Err.Number & "-" & Err.Description " Note the next line never gets excuted, but I like to have resume in the code for when I am debugging. " (By moving the next executable line onto it, and using step over, it moves the exection point to the line that actually raised the error) Resume End Sub Sub tsub_WillNotRaiseError_JustPrintsOk() Static i As Integer i = i + 1 Debug.Print "OK " & i End Sub Public Function ManageErrSource(MyClassName As String, ErrSource As String, ErrLine As Integer, ProcedureName As String) As String " This function would normally be in a global error handling module " On Error GoTo err_ManageErrSource Const cnstblnRecordCallStack As Boolean = True Select Case ErrSource Case Application.VBE.ActiveVBProject.Name " Err.Source is set to this value when a VB statement raises and error. eg In Access by defualt it is set to "Database" ManageErrSource = Application.VBE.ActiveVBProject.Name & " " & MyClassName & "." & ProcedureName & ":" & ErrLine Case "" " When writing code ouside of the error handling code, the coder can raise an error explicitly, often using a user error number. " ie by using err.raise MyUserErrorNumber, "", "My Error descirption". " The error raised by the coder will be handled by an error handler (typically at the foot of a procedure where it was raised), and " it is this handler that calls the ManageErrSource function changing the Err.Source from "" to a meaningful value. ManageErrSource = Application.VBE.ActiveVBProject.Name & " " & MyClassName & "." & ProcedureName & ":" & ErrLine Case Else " This code is executed when ManageErrSource has already been called. The Err.Source will already have been set to hold the " Details of where the error occurred. " This option can be used to show the call stack, ie the names of the procdures that resulted in the prcedure with the error being called. If cnstblnRecordCallStack Then If InStr(1, ErrSource, ";") = 0 Then ManageErrSource = ErrSource & ":: Called By: " End If ManageErrSource = ErrSource & ";" & ProcedureName & ":" & ErrLine Else ManageErrSource = ErrSource End If End Select Exit Function err_ManageErrSource: Err.Raise Err.Number, "MyModuleName.err_ManageErrSource", Err.Description Resume End Function
Commenti
- Solo due commenti: 1 Perché diavolo useresti questo? 2
On Error Goto -1
- Try / catch può anche essere emulato inserendo il codice pertinente tra
On Error Resume Next
eOn Error GoTo 0
e selezionandoErr.Number
. Quanto sopra è un po difficile da seguire, ha un po dispaghetti
struttura .. - Grazie Rory, I ‘ lho cambiato. Lo useresti o lo stesso motivo per cui chiunque userebbe unistruzione TRY CATCH in VB o SQL Server. cioè ti permette di strutturare il tuo codice in modo diverso. cioè puoi usare lo stesso gestore degli errori per molte righe di codice senza doverle inserire nella loro procedura.
- Loannis. sì, ‘ lho fatto in passato, per singole righe di codice che richiedono un gestore degli errori. Cioè una riga di codice ha un gestore degli errori. TRY CATCH consente di incorporare un blocco di codice (con molte righe) in una procedura con il suo gestore di errori ‘. Uso molto TRY CATCH in SQL Server e poiché ‘ è disponibile anche in VB, deve servire a scopi generalmente utili. Certo, questa versione è un po confusa.
- @Loannis E se vuoi saltare più righe quando ricevi un errore. Vedi la mia risposta per un esempio semplificato. Ovviamente puoi farlo anche con la normale gestione degli errori.
Answer
Il problema è che gli errori di runtime in VBA non sono eccezioni e la gestione degli errori in VBA ha molto poco in comune con la gestione delle eccezioni.
RememberErrLine = Erl()
Il Erl
la funzione è un membro nascosto del modulo VBA.Information
per un motivo: restituisce 0 a meno che lerrore non si sia verificato su una riga numerata. E se stai usando i numeri di riga in VBA, vivi in una caverna da 25 anni e probabilmente stai usando le istruzioni GoSub
invece di scrivere procedure. I numeri di riga sono supportati per motivi legacy / retrocompatibilità , perché il codice scritto negli anni 80 li richiedeva .
Mi piace come lhai detto tu stesso:
" THEREFORE KEEP THE CODE HERE VERY SIMPLE!
..ma perché non è quello applicato al resto del codice? Senza offesa, ma questa è logica spaghetti, scritta in procedure che violano chiaramente e senza vergogna il Principio di responsabilità unica . Nessun codice conforme a SRP avrebbe mai bisogno di due di questi ” try-catch ” blocchi.
Questo odora:
Case 0: " No Error, do Nothing
Significa una delle due cose: o hai un codice di gestione degli errori che viene eseguito in contesti non di errore, o hai un codice morto che dovrebbe essere eliminato.
Questo puzza puzza :
GoTo CatchBlock2_End: CatchBlock2_Start:
In primo luogo, ac olon (:
) che non specifica una etichetta di riga , è un separatore di istruzioni .Risulta che una nuova riga è anche un ” separatore di istruzioni “, quindi i due punti alla fine di GoTo CatchBlock2_End
è completamente inutile e confuso, soprattutto dato il livello di rientro dellistruzione GoTo .
A proposito di GoTo
…
Neil Stephenson pensa che sia carino chiamare le sue etichette” dengo “
Non mi piace come devo saltare tra le etichette per seguire il codice. IMO è disordinato e inutilmente spaghettato.
Bene, smartypants . Allora, come si gestiscono in modo pulito gli errori in VBA?
1. Scrivi un codice pulito in primo luogo.
Rispetta le migliori pratiche e scrivi piccole procedure che fanno una cosa e la fanno bene.
2. Scrivi codice orientato agli oggetti.
Astrazione e incapsulamento sono due dei 4 pilastri dellOOP , e sono completamente supportati in VBA. Anche il polimorfismo è in qualche modo unopzione; è esclusa solo l ereditarietà appropriata, ma ciò non impedisce di astrarre concetti in moduli di classe e istanziare oggetti specializzati.
Il codice procedurale scritto in moduli standard (.bas) dovrebbe essere un metodo pubblico minuscolo (macro ” hook “) che creano gli oggetti richiesti per eseguire la funzionalità.
Quindi, come si relaziona anche in remoto alla corretta gestione degli errori?
3. Abbraccia la gestione idiomatica degli errori, non combatterla.
Dato il codice che aderisce ai punti precedenti, non cè motivo di non implementare la gestione degli errori lidiomatico VBA-way.
Public Sub DoSomething() On Error GoTo CleanFail "method body CleanExit: "cleanup code goes here. runs regardless of error state. Exit Sub CleanFail: "handle runtime error(s) here. "Raise Err.Number ""rethrow" / "bubble up" Resume CleanExit Resume "for debugging - break above and jump to the error-raising statement End Sub
Questo modello è analogo a un ” try-catch-latest ” nel modo seguente:
- Il corpo è ” try “, che fa quello che dice il nome del metodo e nientaltro
-
CleanFail
è ” catch ” parte, che viene eseguita solo se viene generato un errore -
CleanExit
è la parte ” finalmente “, che viene eseguita indipendentemente dal fatto che sia stato generato o meno un errore … a meno che tu non “stia rilanciando . Ma se hai bisogno di far apparire un errore per il codice chiamante da gestire, non dovresti fare molta pulizia c ode da eseguire, e dovresti avere unottima ragione per farlo.
Se la tua subroutine di gestione degli errori può generare un errore, allora “non stai aderendo a SRP. Ad esempio, scrivere in un file di registro è una preoccupazione di per sé, che dovrebbe essere astratto in qualche oggetto Logger
che vive per gestire i problemi di registrazione ed espone metodi che gestiscono i propri errori . Il codice della subroutine di gestione degli errori dovrebbe essere banale.
Commenti
Risposta
Ascolta Mat “sMug , ma non ha trattato la situazione in cui si sa effettivamente come risolvere un errore. Per completezza, vorrei spiegarlo.
Diamo prima unocchiata a come faremmo qualcosa di simile in VB.Net.
Try foo = SomeMethodLikelyToThrowAnException Catch e As SomeException foo = someDefaultValue End Try " some more code
Il modo idiomatico per farlo in VB6 è ResumeNext
. Scrivilo, perché “è solo volta che” lo dirò mai ” diritto a ResumeNext
.
On Error Goto ErrHandler foo = SomeMethodLikelyToRaiseAnError " some more code CleanExit: " clean up resources Exit Sub ErrHandler: If Err.Number = ConstantValueForErrorWeExpected Then foo = someDefaultValue Resume Next End If Resume CleanExit "or re-raise error Exit Sub
Il modo alternativo è incorporare questa logica, che penso sia un po più pulita e più vicino al Try...Catch
idioma, ma può diventare brutto rapidamente se abusarne.
On Error Resume Next foo = SomeMethodLikelyToRaiseAnError If Err.Number = ConstantValueForErrorWeExpected Then foo = someDefaultValue End If On Error Goto 0
O è un modo idiomatico di affrontare gli errori previsti, ma qualunque cosa tu faccia. Non preoccuparti di Resume Next
finché non avrai completamente compreso cosa fa e quando è appropriato . (Più un avvertimento per i futuri lettori che per te. Sembra che tu comprenda a fondo la gestione degli errori in VB6. Forse un po troppo bene per il tuo bene.)
Commenti
- Grazie @RubberDuck per i tuoi commenti utili. Ad essere sincero, mi ritrovo a utilizzare ” In caso di errore riprendi dopo ” prima parecchie chiamate di procedura dopo r che in genere è un SELECT CASE che risponde a qualsiasi errore sollevato. Il grosso errore che mi rendo conto di fare è che sollevo uneccezione definita dallutente nella procedura secondaria per contrassegnare le situazioni che si verificano (come lutente che richiede di annullare lelaborazione). Credo che dovrei usare di più le funzioni. Questa è unindicazione che la mia struttura generale del codice è ” non ideale ” / scarsa e penso e ho bisogno di affrontare questo problema. Grazie.
- ‘ hai raggiunto un ottimo punto @HarveyFrench. Le eccezioni riguardano il comportamento eccezionale , non il flusso di controllo. Benvenuto in CR.
- ‘ sarei molto interessato alle tue opinioni su questa domanda SO: stackoverflow. com / questions / 31007009 / …
- Il codice che ho ricevuto da fmsinc.com risolve molti dei problemi che ‘ che ho avuto. ‘ dò valore alla tua opinione. Vedi qui codereview.stackexchange.com/questions/94498/…
Risposta
Questa risposta ha lo scopo di semplificare il pattern Try / Catch in modo che sia facilmente comprensibile.
Questo non è molto diverso da regolare gestione degli errori in linea tranne per il fatto che può saltare più righe contemporaneamente, gestire un errore e quindi riprendere lesecuzione regolare. Questo è un modello strutturato in modo molto pulito per la gestione di un errore. Il flusso si muove in modo molto pulito dallalto verso il basso; nessun codice spaghetti qui.
Tradizionalmente il gestore degli errori è posizionato in fondo. Ma il costrutto Try / Catch è così elegante. È un modo molto strutturato di gestire gli errori ed è molto facile da seguire. Questo modello tenta di riprodurlo in modo molto chiaro e conciso. Il flusso è molto coerente e non salta da un posto allaltro.
Sub InLineErrorHandling() "code without error handling BeginTry1: "activate inline error handler On Error GoTo ErrHandler1 "code block that may result in an error Dim a As String: a = "Abc" Dim c As Integer: c = a "type mismatch ErrHandler1: "handle the error If Err.Number <> 0 Then "the error handler is now active Debug.Print (Err.Description) End If "disable previous error handler (VERY IMPORTANT) On Error GoTo 0 "exit the error handler Resume EndTry1 EndTry1: "more code with or without error handling End Sub
Fonti:
- Pearson Error Handling in VBA
- Procedura: gestire gli errori di runtime in VBA
- Gestione corretta Errori in VBA (Excel)
- Personale: come eseguire un blocco di gestione degli errori in linea come Try / Catch
Se gestito correttamente, funziona abbastanza bene. È uno schema molto pulito che è riproducibile ovunque sia necessario.
Commenti
- @D_Bester, Grazie per i collegamenti e il semplice esempio. ‘ sto ancora imparando e ho trovato utile il tuo feedback, tuttavia dovrai aggiungere un ” In caso di errore Vai a 0 ” dopo ” sullerrore goto -1 “. Inoltre, riflettendo, penso che sia meglio usare Err.Clear invece di ” On Error Goto -1 ” in quanto mostra più chiaramente cosa sta succedendo. ‘ trovo che lintera gestione degli errori in VBA sia un po unarte nera.
- @D_Bester. Riflettendoci, il codice va bene se tutto quello che vuoi dare allutente un messaggio quando si verifica un errore, ma cosa succede se vuoi sollevare nuovamente lerrore? Che sarà uno scenario molto comune. Prendere in considerazione.Se il codice stava tentando di cercare i dettagli di un cliente ‘ e non è stato possibile ‘ ottenerli per un motivo IMPREVISTO. Dovresti sollevare nuovamente lerrore e lasciare che il codice che sta utilizzando il tuo codice per fare la ricerca decida cosa fare.
- @HarveyFrench Se vuoi ripetere lerrore usa ‘ Err.Raise ‘. Nessun problema, supponendo che il codice sia ben strutturato e che la gestione degli errori sia abilitata nel codice chiamante.
- @HarveyFrench
Err.Clear
eOn Error Goto -1
NON sono equivalenti. Vedi stackoverflow.com/a/30994055/2559297 - Tu ‘ hai ragione non lo stesso mi dispiace. Ma penso che il codice sopra abbia ancora bisogno di On Error GoTo -1 sostituito con Err.Clear altrimenti ” ‘ altro codice senza gestione degli errori ” passerà a ErrHandler1 se si verifica un errore.
Risposta
Riguardo a “CleanExit” e allargomento “Finalmente”.
Mat “s Mug ha scritto:
CleanExit è il ” finalmente ” parte, che viene eseguita indipendentemente dal fatto che sia stato sollevato o meno un errore … a meno che tu non “rilanci di nuovo.
Tale situazione potrebbe verificarsi ad esempio in questo codice procedurale:
Approccio procedurale
Public Sub DoSomething() On Error GoTo CleanFail " Open any resource " Use the resource CleanExit: " Close/cleanup the resource Exit Sub CleanFail: Raise Err.Number Resume CleanExit End Sub
Problema qui : se si verifica un errore nel corpo del metodo che deve essere rieseguito in CleanFail, CleanExit non verrà eseguito a t tutto e quindi la risorsa non può essere chiusa correttamente.
Certo, potresti chiudere la risorsa anche nel gestore degli errori stesso, ma ciò potrebbe portare ad avere più frammenti di codice dove la gestione delle risorse sarà / ha da fare.
Il mio suggerimento è di utilizzare un oggetto personalizzato per ogni necessità di associazione di risorse:
AnyResourceBindingClass
Private Sub Class_Initialize() "Or even use Mats "Create method" approach here instead. "Open/acquire the resource here End Sub Private Sub Class_Terminate() On Error GoTo CleanFail "Close/clean up the resource here properly CleanExit: Exit Sub CleanFail: MsgBox Err.Source & " : " & Err.Number & " : " & Err.Description Resume CleanExit End Sub Public Sub UseResource() "Do something with the resource End Sub
Approccio orientato agli oggetti
Public Sub DoSomething() On Error GoTo CleanFail " Use custom object which acquires the resource With New AnyResourceBindingClass .UseResource End With CleanExit: Exit Sub CleanFail: Raise Err.Number Resume CleanExit End Sub
Opportunità : Poiché loggetto personalizzato sarà fuori ambito dopo che è stato generato lerrore, il suo metodo Terminate verrà eseguito automaticamente, il che fa sì che la risorsa acquisita venga chiusa / ripulita correttamente.
Una necessità in meno per un blocco “finalmente”.
Gestione degli errori nel metodo Terminate
Secondo me dipende dal contesto come verrà gestito un errore nel metodo Terminate della classe personalizzata. Forse dovrebbe essere registrato da qualche parte in aggiunta o addirittura inghiottito del tutto?
Sicuramente questo è discutibile.
Ma è essenziale per abilitare un gestore degli errori in questo metodo, perché, per quanto ne so, qualsiasi errore non gestito in questo metodo farà sì che VBA interrompa lesecuzione e visualizzerà il suo messaggio di errore di runtime standard.
Risposta
Per chiarire il mio post precedente, la seguente riga dal codice di HarveyFrench:
RememberErrLine = Erl()
non funzionerà a meno che i numeri di riga non siano stati aggiunti a ogni riga di codice. Invece di digitare manualmente i numeri di riga, il che è troppo noioso, puoi utilizzare uno strumento per aggiungere automaticamente i numeri di riga. ci sono alcuni strumenti là fuori che possono farlo, io ne uso uno chiamato CodeLiner.
Ecco il codice con i numeri di riga, che permetterà a Erl()
di funzionare correttamente :
Option Compare Database Option Explicit Dim RememberErrNumber As Long Dim RememberErrDescription As String Dim RememberErrSource As String Dim RememberErrLine As Integer Private Sub RememberThenClearTheErrorObject() 10 11 On Error Resume Next 12 " For demo purposes 14 Debug.Print "ERROR RAISED" 15 Debug.Print Err.Number 16 Debug.Print Err.Description 17 Debug.Print Err.Source 18 Debug.Print " " 19 20 " This function has to be declared in the same scope as the variables it refers to 22 RememberErrNumber = Err.Number 23 RememberErrDescription = Err.Description 24 RememberErrSource = Err.Source 25 RememberErrLine = Erl() 26 " Note that the next line will reset the error object to 0, the variables above are used to remember the values " so that the same error can be re-raised 29 Err.Clear 30 " Err.Clear is used to clear the raised exception and set the err object to nothing (ie err.number to 0) " If Err.Clear has not be used, then the next "On Error GoTo ALabel" that is used in this or the procedure that called it " will actually NOT pass execution to the ALabel: label BUT the error is paseed to the procedure that called this procedure. " Using Err.Clear (or "On Error GoTo -1 ") gets around this and facilitates the whole TRY CATCH block scenario I am using there. 35 36 " For demo purposes 38 Debug.Print "ERROR RAISED is now 0 " 39 Debug.Print Err.Number 40 Debug.Print Err.Description 41 Debug.Print Err.Source 42 Debug.Print " " 43 " For demo purposes 45 Debug.Print "REMEMBERED AS" 46 Debug.Print RememberErrNumber 47 Debug.Print RememberErrDescription 48 Debug.Print RememberErrSource 49 Debug.Print " " 50 End Sub Private Sub ClearRememberedErrorObjectValues() 54 " This function has to be declared in the same scope as the variables it refers to 56 RememberErrNumber = 0 57 RememberErrDescription = "" 58 RememberErrSource = "" 59 RememberErrLine = 0 60 End Sub Sub ExampleOfTryCatchBlockInVBA() 67 68 On Error GoTo HandleError 69 70 " ----------------------------------------------------- " SubProcedure1 has the example of a multiple line TRY block with a block of code executed in the event of an error 73 74 SubProcedure1 75 76 77 78 Exit Sub 79 HandleError: 80 81 Select Case Err.Number 82 Case 0 " This shold never happen as this code is an error handler! " However if it does still allow the Err.raise to execute below. (In this case Err.raise will fail " and itself will raise an error "Invalid procedure call or argument" indicating that 0 cannot be used to raise and error! 86 87 Case 111111 " You might want to do special error handling for some predicted error numbers " perhaps resulting in a exit sub with no error or " perhaps using the Err.raise below 91 92 Case Else " Just the Err.raise below is used for all other errors 94 95 End Select 96 " " I include the procedure ManageErrSource as an exmple of how Err.Source can be used to maintain a call stack of procedure names " and store the name of the procedure that FIRST raised the error. " 101 Err.Raise Err.Number _ , ManageErrSource("MyModuleName", Err.Source, Erl(), "tsub1_RaisesProcedureNotFoundError") _ , Err.Number & "-" & Err.Description 104 " Note the next line never gets excuted, but I like to have resume in the code for when I am debugging. " (When a break is active, by moving the next executable line onto it, and using step over, it moves the exection point to the line that actually raised the error) 107 Resume 108 End Sub Sub SubProcedure1() 112 " ----------------------------------------------------- " Example of a multiple line TRY block with a Case statement used to CATCH the error 115 " " It is sometimes better to NOT use this technique but to put the code in it"s own procedure " (ie I refer to the code below that is surrounded by the tag #OWNSUB) . " However,sometimes using this technique makes code more readable or simpler! " 121 122 Dim i As Integer 123 " This line puts in place the defualt error handler found at the very foot of the procedure 125 On Error GoTo HandleError 126 127 " " Perhaps lots of statements and code here " 131 132 " First an example with comments 134 135 " ----------------------------------------------------- " TRY BLOCK START 138 " This next line causes execution to "jump" to the "catch" block in the event an error is detected. 140 On Error GoTo CatchBlock1_Start 141 " #OWNSUB 143 144 tsub_WillNotRaiseError_JustPrintsOk 145 146 If vbYes = MsgBox("1. Do you want to raise an error in the try block? - (PRESS CTRL+BREAK now then choose YES, try no later.)", vbYesNo) Then 147 i = 100 / 0 148 End If 149 " " Perhaps lots of statements and code here " 153 " #OWNSUB 155 " TRY BLOCK END " ----------------------------------------------------- 158 159 " ----------------------------------------------------- " CATCH BLOCK START 162 CatchBlock1_Start: 163 164 If Err.Number = 0 Then 165 On Error GoTo HandleError " Re-instates the procedure"s generic error handler " This is also done later, but I think putting it here reduces the likelyhood of a coder accidentally removing it. 168 169 Else 170 " WARNING: BE VERY CAREFUL with any code that is written here as " the "On Error GoTo CatchBlock1_Start" is still in effect and therefore any errors that get raised could goto this label " and cause and infinite loop. " NOTE that a replacement "On Error Goto" cannot be executed until Err.clear is used, otherwise the "On Error Goto" " will itself raise and error. " THEREFORE KEEP THE CODE HERE VERY SIMPLE! " RememberThenClearTheErrorObject should be the only code executed and this called procedure must be tight! 178 " This saves the details of the error in variables so that the "On Error GoTo HandleError" can be used " to determine how the next Err.Raise used below is handled (and also how any unexpected implicitly raised errors are handled) 181 RememberThenClearTheErrorObject 182 183 On Error GoTo HandleError "#THISLINE# 184 185 If vbYes = MsgBox("2. Do you want to raise an error in the erro handler? - (PRESS CTRL+BREAK now then try both YES and NO )", vbYesNo) Then 186 i = 100 / 0 187 End If 188 189 Select Case RememberErrNumber 190 Case 0: " No Error, do Nothing 191 192 Case 2517 193 Debug.Print "The coder has decided to just give a Warning: Procedure not found " & Err.Number & " - " & Err.Description 194 ClearRememberedErrorObjectValues " Not essential, but might save confusion if coding errors are made 195 196 Case Else " An unexepected error or perhaps an (user) error that needs re-raising occurred and should to be re-raised 198 " NOTE this is giving an example of what woudl happen if the CatchBlock1_ErrorElse is not used below 200 If vbYes = MsgBox("3. Do you want to raise an error in the ELSE error handler? CatchBlock1_ErrorElse *HAS NOT* been used? - (PRESS CTRL+BREAK now then try both YES and NO )", vbYesNo) Then 201 i = 100 / 0 202 End If 203 204 On Error GoTo CatchBlock1_ErrorElse 205 206 " SOME COMPLEX ERROR HANDLING CODE - typically error logging, email, text file, messages etc.. " Because the error objects values have been stored in variables, you can use " code here that might itself raise an error and CHANGE the values of the error object. " You might want to surround the code with the commented out CatchBlock1_ErrorElse lines " to ignore these errors and raise the remembered error. (or if calling a error handling module " just use on error resume next). " Without the CatchBlock1_ErrorElse lines any error raised in this "complex code" will be handled by the " active error handler which was set by the "On Error GoTo HandleError" tagged as "#THISLINE#" above. 215 216 If vbYes = MsgBox("4. Do you want to raise an error in the ELSE error handler when CatchBlock1_ErrorElse HAS been used? - (PRESS CTRL+BREAK now then try both YES and NO )", vbYesNo) Then 217 i = 100 / 0 218 End If 219 220 CatchBlock1_ErrorElse: 221 On Error GoTo HandleError " This line must be preceeded by an new "On error goto" for obvious reasons 223 Err.Raise RememberErrNumber, RememberErrSource, RememberErrDescription 224 225 End Select 226 227 On Error GoTo HandleError 228 229 End If " CATCH BLOCK END " ----------------------------------------------------- 232 On Error GoTo HandleError " Unnecessary but used to delimt the catch block 233 234 235 236 " " lots of code here perhaps " 240 241 242 243 " ----------------------------------------------------- " Example 2 " " In this example goto statements are used instead of the IF statement used in example 1 " and no explanitory comments are given (so you can see how simple it can look) " 250 " ----------------------------------------------------- " TRY BLOCK START 253 254 On Error GoTo CatchBlock2_Start 255 256 tsub_WillNotRaiseError_JustPrintsOk 257 258 If vbYes = MsgBox("Do you want to raise an error? - (PRESS CTRL+BREAK now then choose YES)", vbYesNo) Then 259 i = 100 / 0 260 End If 261 " " Perhaps lots of statements and code here " 265 " TRY BLOCK END " ----------------------------------------------------- 268 269 270 GoTo CatchBlock2_End: 271 CatchBlock2_Start: 272 273 RememberThenClearTheErrorObject 274 275 On Error GoTo HandleError 276 277 Select Case RememberErrNumber 278 Case 0: " No Error, do Nothing 279 280 Case 2517 281 Debug.Print "The coder has decided to just give a Warning: Procedure not found " & Err.Number & " - " & Err.Description 282 ClearRememberedErrorObjectValues " Not essential, but might save confusion if coding errors are made 283 284 Case Else " An unexepected error or perhaps an (user) error that needs re-raising occurred and should to be re-raised " In this case the unexpecetd erro will be handled by teh code that called this procedure " This line must be preceeded by an new "On error goto" for obvious reasons 288 Err.Raise RememberErrNumber, RememberErrSource, RememberErrDescription 289 290 End Select 291 292 On Error GoTo HandleError 293 294 End If 295 296 CatchBlock2_End: " CATCH BLOCK END " ----------------------------------------------------- 299 On Error GoTo HandleError " Unnecessary but used to delimt the catch block 300 301 302 303 " " Here you could add lots of lines of vba statements that use the generic error handling that is after the HandleError: label " " 308 " " You could of course, alway add more TRY CATCH blocks like the above " " 313 314 315 316 Exit Sub 317 HandleError: 318 319 Select Case Err.Number 320 Case 0 " This shold never happen as this code isan error handler! " However if it does still allow the Err.raise to execute below. (In this case Err.raise will fail " and itself will raise an error "Invalid procedure call or argument" indicating that 0 cannot be used to raise and error! 324 325 Case 111111 " You might watch to do special error handling for some predicted error numbers " perhaps exit sub " Perhaps using the Err.raise below 329 End Select 330 " ie Otherwise " " Note that I use the Err.Source to maintain a call stack of procedure names " 335 Err.Raise Err.Number _ , ManageErrSource("MyModuleName", Err.Source, Erl(), "tsub1_RaisesProcedureNotFoundError") _ , Err.Number & "-" & Err.Description 338 " Note the next line never gets excuted, but I like to have resume in the code for when I am debugging. " (By moving the next executable line onto it, and using step over, it moves the exection point to the line that actually raised the error) 341 Resume 342 End Sub Sub tsub_WillNotRaiseError_JustPrintsOk() 348 349 Static i As Integer 350 351 i = i + 1 352 353 Debug.Print "OK " & i 354 End Sub Public Function ManageErrSource(MyClassName As String, ErrSource As String, ErrLine As Integer, ProcedureName As String) As String 360 " This function would normally be in a global error handling module 362 " On Error GoTo err_ManageErrSource 364 365 Const cnstblnRecordCallStack As Boolean = True 366 367 Select Case ErrSource 368 369 Case Application.VBE.ActiveVBProject.Name 370 " Err.Source is set to this value when a VB statement raises and error. eg In Access by defualt it is set to "Database" 372 373 ManageErrSource = Application.VBE.ActiveVBProject.Name & " " & MyClassName & "." & ProcedureName & ":" & ErrLine 374 375 Case "" 376 " When writing code ouside of the error handling code, the coder can raise an error explicitly, often using a user error number. " ie by using err.raise MyUserErrorNumber, "", "My Error descirption". " The error raised by the coder will be handled by an error handler (typically at the foot of a procedure where it was raised), and " it is this handler that calls the ManageErrSource function changing the Err.Source from "" to a meaningful value. 381 382 ManageErrSource = Application.VBE.ActiveVBProject.Name & " " & MyClassName & "." & ProcedureName & ":" & ErrLine 383 384 Case Else 385 " This code is executed when ManageErrSource has already been called. The Err.Source will already have been set to hold the " Details of where the error occurred. " This option can be used to show the call stack, ie the names of the procdures that resulted in the prcedure with the error being called. 389 390 If cnstblnRecordCallStack Then 391 392 If InStr(1, ErrSource, ";") = 0 Then 393 ManageErrSource = ErrSource & ":: Called By: " 394 End If 395 ManageErrSource = ErrSource & ";" & ProcedureName & ":" & ErrLine 396 397 Else 398 ManageErrSource = ErrSource 399 400 End If 401 402 End Select 403 404 Exit Function 405 err_ManageErrSource: 406 Err.Raise Err.Number, "MyModuleName.err_ManageErrSource", Err.Description 407 Resume 408 End Function
Commenti
- Ciao! Benvenuto in Code Review. Aggiungi più contesto alla tua risposta: spiega perché il tuo suggerimento migliorerà il codice dellOP ‘, o forse vai più in dettaglio su ciò che stai cercando di dire.
TypeName(Me)
come fonte per errori personalizzati nei moduli di classe e lunico modo per un errore di sapere quale procedura si è verificato, è quello di codificare il nome della procedura in unaconst
locale, idealmente non troppo lontano dalla firma del metodo ‘. Mi piace lidea dello stack di chiamate, ma uno svantaggio è che devi ” push ” e ” pop ” ogni volta che entri / esci da una procedura, altrimenti diventa una bugia.