2009-05-07 15 views
5

Supponiamo che io sono un metodo che modifica lo stato di un oggetto, e genera un evento per notificare gli ascoltatori di questo cambiamento di stato:Posso avere una forte eccezione di sicurezza ed eventi?

public class Example 
{ 
    public int Counter { get; private set; } 

    public void IncreaseCounter() 
    { 
     this.Counter = this.Counter + 1; 
     OnCounterChanged(EventArgs.Empty); 
    } 

    protected virtual void OnCounterChanged(EventArgs args) 
    { 
     if (CounterChanged != null) 
     CounterChanged(this,args); 
    } 

    public event EventHandler CounterChanged; 
} 

I gestori di eventi possono generare un'eccezione anche se IncreaseCounter completato con successo il cambiamento di stato. Quindi non abbiamo forte exception safety qui:

Il forte garanzia: che l'operazione ha sia completata con successo o generato un'eccezione, lasciando lo Stato programma esattamente come era prima l'operazione iniziata.

È possibile avere una forte protezione dalle eccezioni quando è necessario generare eventi?

risposta

3

Per evitare un'eccezione in un gestore di propagarsi al generatore di eventi, la risposta è di richiamare manualmente ogni elemento nel Delegato MultiCast (cioè il gestore di eventi) all'interno di un try-catch

Tutti i gestori vengono chiamati e l'eccezione w non propagarsi.

public EventHandler<EventArgs> SomeEvent; 

protected void OnSomeEvent(EventArgs args) 
{ 
    var handler = SomeEvent; 
    if (handler != null) 
    { 
     foreach (EventHandler<EventArgs> item in handler.GetInvocationList()) 
     { 
      try 
      { 
       item(this, args); 
      } 
      catch (Exception e) 
      { 
       // handle/report/ignore exception 
      } 
     }     
    } 
} 

ciò che rimane è per voi di implementare la logica per Cosa fare quando uno o più destinatari di eventi tiri e gli altri non fanno. Il catch() potrebbe anche rilevare un'eccezione specifica e annullare eventuali modifiche se questo è ciò che ha senso, consentendo al destinatario dell'evento di segnalare alla sorgente di eventi che si è verificata una situazione eccezionale.

Come altri sottolineano, l'utilizzo di eccezioni come flusso di controllo non è raccomandato. Se è davvero una circostanza eccezionale, allora usa sempre un'eccezione. Se stai ricevendo molte eccezioni, probabilmente vorrai usare qualcos'altro.

+0

In altre parole, ottenere una forte sicurezza delle eccezioni nei metodi che generano eventi ha il costo di dover inghiottire/registrare eccezioni. Questa è quasi sempre una brutta cosa, quindi ho concluso che il raggiungimento di una forte sicurezza delle eccezioni probabilmente non ne vale la pena. –

0

Non si scambierà semplicemente l'ordine delle due linee in IncreaseCounter per far rispettare la sicurezza delle eccezioni?

O se necessità incrementare Counter prima di generare l'evento:

public void IncreaseCounter() 
{ 
    this.Counter += 1; 

    try 
    { 
     OnCounterChanged(EventArgs.Empty); 
    } 
    catch 
    { 
     this.Counter -= 1; 

     throw; 
    } 
} 

In una situazione in cui si hanno variazioni di stato più complessi (effetti collaterali) in corso, allora diventa un po 'più complicato (potrebbe essere necessario clonare determinati oggetti, ad esempio), ma in questo esempio sembrerebbe abbastanza facile.

+0

l'unico problema con questo è che il valore del contatore sarà come era prima che fosse incrementato (che non sarebbe il valore previsto per la maggior parte degli sviluppatori) –

+0

@Alan: Sì, l'ho appena considerato. Vedi la mia versione modificata. – Noldorin

+0

Inoltre, non dimenticare di propagare l'eccezione. Attualmente questo non è stato completato correttamente o genera un errore a un chiamante. – workmad3

2

Lo schema generale si vedrà per il framework è:

public class Example 
{ 
    public int Counter { get; private set; } 

    public void IncreaseCounter() 
    { 
     OnCounterChanging(EventArgs.Empty); 
     this.Counter = this.Counter + 1; 
     OnCounterChanged(EventArgs.Empty); 
    } 

    protected virtual void OnCounterChanged(EventArgs args) 
    { 
     if (CounterChanged != null) 
     CounterChanged(this, args); 
    } 

    protected virtual void OnCounterChanging(EventArgs args) 
    { 
     if (CounterChanging != null) 
     CounterChanging(this, args); 
    } 

    public event EventHandler<EventArgs> CounterChanging; 
    public event EventHandler<EventArgs> CounterChanged; 
} 

Se un utente desidera un'eccezione per impedire la modifica del valore allora che dovrebbero fare nel OnCounterChanging() evento invece di OnCounterChanged(). Per definizione del nome (passato, suffisso di -ed) che implica che il valore è stato modificato.

Edit:

Si noti che in genere si desidera stare lontano da una copiosa quantità di try..catch extra/blocchi finally come gestori di eccezioni (tra cui try..finally) sono costosi a seconda della implementazione del linguaggio. cioè il modello con frame stack win32 o il modello di eccezione mappato al PC hanno entrambi i loro pro e contro, tuttavia se ci sono troppi di questi frame saranno entrambi costosi (nello spazio o nella velocità di esecuzione). Solo un'altra cosa da tenere a mente quando si crea un framework.

+2

Questo non sarebbe stato migliore per garantire una sicurezza delle eccezioni forte di una convenzione che diceva "non lanciare eccezioni nei gestori di eventi". In realtà non impedisce a qualcuno di rompere il sistema lanciando eccezioni dove non dovrebbero. Se questo è davvero un problema o meno è diverso :) – workmad3

+0

Le eccezioni sono un meccanismo utile, e il costo di try/catch è minimo quando non ci sono eccezioni. Le eccezioni non dovrebbero mai essere usate per il normale flusso di controllo, ma non penso che questo sia ciò che l'OP sta chiedendo. –

+0

try..catch costo è minimo per le eccezioni basate su frame stack quando non ci sono eccezioni. Le eccezioni associate al PC hanno un costo di dimensione associato a tali quantità così copiose di questi tipi di gestori di eccezioni potrebbero iniziare a gonfiare il tuo DS. –

0

In questo caso, penso che dovresti prendere spunto da Workflow Foundation. Nel metodo OnCounterChanged, racchiudi la chiamata al delegato con un blocco try-catch generico. Nel gestore delle catture, dovrai eseguire un'azione compensativa in modo efficace: arretrando il contatore.

1

Beh, si potrebbe compensare, se è stato fondamentale:

public void IncreaseCounter() 
{ 
    int oldCounter = this.Counter; 
    try { 
     this.Counter = this.Counter + 1; 
     OnCounterChanged(EventArgs.Empty); 
    } catch { 
     this.Counter = oldCounter; 
     throw; 
    } 
} 

Ovviamente questo sarebbe meglio se si può parlare con il campo (dal momento che l'assegnazione di un campo non sarà errore).Si potrebbe anche avvolgere questo in bolierplate:

void SetField<T>(ref T field, T value, EventHandler handler) { 
    T oldValue = field; 
    try { 
     field = value; 
     if(handler != null) handler(this, EventArgs.Empty); 
    } catch { 
     field = oldField; 
     throw; 
    } 
} 

e chiamare:

SetField(ref counter, counter + 1, CounterChanged); 

o qualcosa di simile ...

+0

Ho considerato la soluzione di rollback, ma se ci sono gestori di eventi che hanno già gestito con successo la modifica, dovrebbero sapere che è stato eseguito il rollback. Quindi dovrei sollevare nuovamente l'evento per il rollback, ma poi la stessa eccezione potrebbe essere generata nuovamente ecc. –