Contratto contrattuale quando si implementa un metodo che restituisce un’attività

Esiste una “best practice” o un accordo contrattuale degli Stati membri quando si implementa un metodo che restituisce un’attività per quanto riguarda il lancio di eccezioni? Questo è emerso quando ho scritto i test unitari e stavo cercando di capire se avrei dovuto testare / gestire questa condizione (riconosco che la risposta potrebbe essere “codifica difensiva”, ma non voglio che questa sia la risposta).

vale a dire

  1. Il metodo deve sempre restituire un’attività, che dovrebbe contenere l’eccezione generata.

  2. Il metodo deve sempre restituire un’attività, tranne quando il metodo fornisce argomenti non validi (ad es. ArgumentException).

  3. Il metodo deve sempre restituire un compito, tranne quando lo sviluppatore si arrende e fa ciò che desidera (jk).

Task Foo1Async(string id){ if(id == null){ throw new ArgumentNullException(); } // do stuff } Task Foo2Async(string id){ if(id == null){ var source = new TaskCompletionSource(); source.SetException(new ArgumentNullException()); return source.Task; } // do stuff } Task Bar(string id){ // argument checking if(id == null) throw new ArgumentNullException("id") try{ return this.SomeService.GetAsync(id).ContinueWith(t => { // checking for Fault state here // pass exception through. }) }catch(Exception ex){ // handling more Fault state here. // defensive code. // return Task with Exception. var source = new TaskCompletionSource(); source.SetException(ex); return source.Task; } } 

Ho fatto una domanda un po ‘simile recentemente:

Gestione delle eccezioni dalla parte sincrona del metodo asincrono .

Se il metodo ha una firma async , non importa se passi dalla parte sincrona o asincrona del metodo. In entrambi i casi, l’eccezione verrà memorizzata all’interno Task . L’unica differenza è che l’object Task risultante verrà immediatamente completato (con errori) nel primo caso.

Se il metodo non ha la firma async , l’eccezione può essere generata sullo stack frame del chiamante.

IMO, in entrambi i casi il chiamante non dovrebbe fare alcuna ipotesi sul fatto che l’eccezione sia stata lanciata dalla parte sincrona o asincrona, o se il metodo abbia la firma async , a tutti.

Se è davvero necessario sapere se l’attività è stata completata in modo sincrono, è sempre ansible controllarne lo stato Task.Completed / Faulted / Cancelled o Task.Exception , senza attendere:

 try { var task = Foo1Async(id); // check if completed synchronously with any error // other than OperationCanceledException if (task.IsFaulted) { // you have three options here: // 1) Inspect task.Exception // 2) re-throw with await, if the caller is an async method await task; // 3) re-throw by checking task.Result // or calling task.Wait(), the latter works for both Task and Task } } catch (Exception e) { // handle exceptions from synchronous part of Foo1Async, // if it doesn't have `async` signature Debug.Print(e.ToString()) throw; } 

Tuttavia, normalmente è necessario await il result , senza preoccuparsi se l’attività è stata completata in modo sincrono o asincrono e quale parte è stata probabilmente generata. Qualsiasi eccezione verrà nuovamente generata nel contesto del chiamante:

 try { var result = await Foo1Async(id); } catch (Exception ex) { // handle it Debug.Print(ex.ToString()); } 

Questo funziona anche per il test delle unità, purché il metodo async restituisca un’attività (il motore Test unitario non supporta il metodo di async void , AFAIK, che ha senso: non esiste alcuna Task da tenere traccia e await ).

Tornando al tuo codice, lo direi in questo modo:

 Task Foo1Async(string id){ if(id == null) { throw new ArgumentNullException(); } // do stuff } Task Foo2Async(string id) { if(id == null){ throw new ArgumentNullException(); } // do stuff } Task Bar(string id) { // argument checking if(id == null) throw new ArgumentNullException("id") return this.SomeService.GetAsync(id); } 

Lascia che il chiamante di Foo1Async , Foo2Async , Bar Foo2Async le eccezioni, invece di catturarle e propagarle manualmente.

So che Jon Skeet è un fan del controllo dei precondizionamenti in un metodo sincrono separato in modo che vengano lanciati direttamente.

Tuttavia, la mia opinione è “non importa”. Prendi in considerazione la tassonomia delle eccezioni di Eric Lippert. Siamo tutti d’accordo sul fatto che le eccezioni esogene dovrebbero essere posizionate sull’attività restituita (non generate direttamente sullo stack frame del chiamante). Le eccezioni fastidiose dovrebbero essere completamente evitate. Gli unici tipi di eccezioni in questione sono eccezioni boneheaded (ad esempio, eccezioni argomento).

La mia tesi è che non importa come vengono lanciati perché non si dovrebbe scrivere codice di produzione che li cattura . I tuoi test unitari sono l’unico codice che dovrebbe catturare ArgumentException e gli amici, e se usi await allora non importa quando vengono lanciati.

Il caso generale in cui i metodi restituiscono le attività è perché sono metodi asincroni. In questi casi è comune che un’eccezione nella parte sincrona del metodo sia lanciata come in qualsiasi altro metodo, e nella parte asincrona dovrebbe essere memorizzata all’interno dell’attività restituita (automaticamente chiamando un metodo async o un delegato anonimo).

Quindi, nei casi semplici come i parametri non validi è sufficiente lanciare un’eccezione come in Foo1Async . Nel caso più complesso relativo all’operazione asincrona, imposta un’eccezione sull’attività restituita come in Foo2Async

Questa risposta presuppone che tu stia facendo riferimento ai metodi di restituzione dell’attività che non sono contrassegnati con async . In quelli che non hai il controllo sull’attività che si sta creando, e qualsiasi eccezione verrebbe automaticamente salvata in quell’attività (quindi la domanda sarebbe irrilevante).