¿Cómo deben las implementaciones de interfaz manejar excepciones internas inesperadas de tipos que los llamadores pueden esperar?

Si una clase implementa una interfaz, ¿cómo debería manejar las situaciones en las que

  1. En la ejecución de un método o propiedad, se produce un error interno que es de un tipo que una persona que llama podría razonablemente tratar de manejar, pero que tal vez una persona que llama no debería. Por ejemplo, IDictionary.Add hace algo internamente que produce una excepción ArgumentException en circunstancias que implicarían que el diccionario está dañado, pero que no implicaría nada malo en el rest del sistema. ¿O implican que algo está corrompido más allá del diccionario? Una persona que llama puede estar esperando capturar y manejar el hecho de que existe una clave duplicada en el diccionario, ya que en algunos casos la excepción puede ser Vexing (por ejemplo, el mismo código se puede usar para un Diccionario al que no tienen acceso otros hilos y para un ConcurrentDictionary que es, y la semántica sería viable si un bash de agregar un registro duplicado causó un error de limpieza). Dejar que se filtre una excepción ArgumentException podría hacer que la persona que llama crea que el diccionario se encuentra en el mismo estado que si nunca hubiera ocurrido el agregado, lo que podría ser peligroso, pero lanzar algún otro tipo de excepción podría parecer confuso.
  2. En la ejecución de un método o propiedad, se produce una excepción que la persona que llama tal vez debería o no debería manejar, y la definición de la interfaz no proporciona ningún indicio de que pueda producirse una excepción similar de forma remota. Por ejemplo, supongamos que algo sale mal en la evaluación de IEnumerator, ya sea implicando (1) que el enumerador se corrompió (posiblemente por una acción inesperada en otro hilo) pero al reintentar la enumeración podría tener éxito; (2) el objeto enumerable en sí probablemente esté corrompido o sea inutilizable, pero todo lo demás en el sistema probablemente esté bien (por ejemplo, una rutina de análisis de archivos evaluado de forma perezosa llega a un registro no válido); (3) algo más allá del objeto enumerable ha sido corrompido. IEnumerable solo tiene una excepción definida que puede lanzar, pero la persona que llama puede querer variar su acción según la “gravedad” de la excepción.

Si tuviera mis druthers, el sistema definiría algunos tipos como RetryableFailureException, CleanFailureException, ObjectCorruptFailureException, y la mayoría de las interfaces tendrían permitido lanzar esos o derivados. Dado que ese no es el caso, ¿cómo debería uno manejar adecuadamente las interfaces, ya sea desde la vista de la interfaz o desde la persona que llama?

Por cierto, un patrón que no he visto implementado, pero parecería útil, sería que los métodos acepten que se ejecute un parámetro delegado en caso de que el método falle de una manera que cause que un método “try” devuelva falso. Un delegado de este tipo, proporcionado por la persona que llama, podría no solo lanzar una excepción que el destinatario sabría buscar, sino que también podría establecer una bandera que de otra manera estaría disponible solo para la persona que llama. Por lo tanto, la persona que llama puede saber que la excepción que se detectó fue la esperada.

El trabajo de una interfaz es definir los miembros (y sus firmas) que debe tener una clase, no cómo deben implementarse. Por lo tanto, diría que deje que la excepción suba en la stack. Si realmente desea definir el contrato y controlar parte de la implementación (como el manejo de errores), debe crear una clase base (me inclinaría hacia una clase MustInherit / Abstract en esta situación) con los métodos MustOverride que la clase base llama sus métodos (en su situación en un Try Catch para que pueda hacer su manejo especial de errores).