Hay pocos cambios en el tratamiento de excepciones de Freya, tanto en comparación con Delphi como con otros lenguajes .NET. Existe un consenso tácito alrededor de la técnica de "excepciones estructuradas" que se ve reforzado por el propio soporte del Common Language Runtime para éstas. Y no seremos nosotros quienes lo rompamos.

SI NO ESTA ROTO, NO LO ARREGLES

Freya ofrece las tres instrucciones básicas y casi universales para señalar y tratar excepciones:

  1. raise: para lanzar excepciones cuando no podemos cumplir con un contrato.
  2. try/finally: para garantizar la devolución de recursos cuando se produce una excepción.
  3. try/except: para probar algoritmos alternativos, cuando existen.

La instrucción peor utilizada de estas tres, suele ser try/except. En una aplicación, menos del 10% de los try debe ser un try/except. La explicación está en que muy pocas veces tenemos un plan B para cuando nos falla el primer intento de resolver un contrato. Más aún, de ese 10% de frecuencia de uso, la mayoría de los ejemplos seguirán el siguiente patrón:

try
    ...
except
    ...
    raise;
end;

Es decir: utilizamos realmente la cláusula except para llevar el programa a un estado estable... y repetir la excepción original, de modo que siga su propagación natural. Uno de los principios más importantes de la buena programación es dejar bien claras nuestras intenciones en todo momento. Un try/except puede confundir al programador que intenta comprender el listado... al menos, hasta que encuentre la instrucción final raise. Peor aún: ésta puede haber sido olvidada por el propio autor del código.

Por estas razones, Freya añade otra instrucción al repertorio de instrucciones para el tratamiento de excepciones:

try
    // Intentamos cumplir con el contrato
fault
    // Pero se produce un fallo, y entramos en esta sección
    // Intentamos devolver recursos, manteniendo el modo de excepción
end;
// Sólo se llega aquí si todo ha ido bien

Si todo funciona correctamente, se ejecutan las instrucciones de la sección try, no se ejecutan las de la sección fault, y la ejecución continua en la instrucción que sigue al try/fault completo. Por el contrario, si se produce un error durante el procesamiento de la sección try, la ejecución saltaría dentro de la sección fault, como si se tratase de un try/except. Sin embargo, al terminar la ejecución de las instrucciones fault, la excepción sigue propagándose, como si se tratase de un try/finally, o como si hubiésemos añadido un raise al final de esta sección.

¿Cuándo es útil try/fault? Regresemos un momento a try/finally. A veces explico la necesidad de esta instrucción con un símil: suponga que hemos abierto un "paréntesis" en mitad del código. Evidentemente, tenemos que garantizar su cierre, ¿no? Esa garantía es la que aporta la cláusula finally. Para try/fault funciona una variante de esta explicación: suponga ahora que podemos cerrar el "paréntesis" de dos formas diferentes. Una de ellas, cuando todo funcione a la perfección; la otra forma de cierre debe activarse cuando se produzca un error. Es fácil reconocer en esta "teoría de los dos tipos de paréntesis" algoritmos y patrones de programación muy comunes, como la ejecución de instrucciones SQL dentro de una transacción explícita:

// Freya, por supuesto...
var
    Transaccion: SqlTransaction;
begin
    Transaccion := SqlConnection1.BeginTransaction;
    try
        // ... operaciones atómicas ...
        Transaccion.Commit;
    fault
        Transaccion.Rollback;
    end;
end;

Si todas las instrucciones se ejecutan sin sobresaltos, cerramos el "paréntesis" abierto por BeginTransaction con una llamada a Commit. En caso de fallo, el paréntesis se cierra con una llamada a Rollback... y la excepción sigue propagándose.

NOTA
Tenga muy presente que aquí no se trata de "ahorrar" una instrucción, sino de que la intención del programador sea lo más evidente posible. Pero debo reconocer que también hay trampa: el lenguaje intermedio de .NET soporta directamente el caso de los bloques fault. La inclusión de esta variante de instrucción ha sido sugerida por la presencia de este soporte a la implementación.

PROTECCION DE RECURSOS

Lo que viene ahora es más polémico, y es probable que haya cambios en la especificación final. Eche un vistazo a este fragmento de Delphi "clásico", que se repite en muchas aplicaciones:

// Delphi
with TMyForm.Create(nil) do
try
    Result := IsPositiveResult(ShowModal);
finally
    Free;
end;

¿Cómo se haría esto mismo en Freya? En .NET la destrucción de objetos es no determinista, y tiene lugar gracias a la recolección de basura. Por lo tanto, no podemos escribir un equivalente exacto para Free:

// Freya
with new TMyForm(nil) do
begin
    Result := IsPositiveResult(ShowModal);
end;

¿Y cómo se haría C#? Bueno, no se trata solamente de C#, sino de todos los lenguajes que pretendan ser compatibles con .NET. En esta plataforma, cuando un objeto necesita liberar recursos importantes de manera determinista, como sucede con un formulario, la clase a la que pertenece debe implementar la interfaz IDisposable. No voy a explicar aquí cómo se debe implementar el correspondiente patrón, pero sí quiero mostrar cómo se aprovecharía una vez implementado por una clase. En notación Delphi:

// Delphi.NET
F := TMyForm.Create(nil);
try
    Result := IsPositiveResult(ShowModal);
finally
    if F <> nil then
        (F as IDisposable).Dispose();
end;

Como esto es demasiado código, C# ofrece una instrucción que logra el mismo efecto: la instrucción using. Esta instrucción permite eliminar el bloque try/finally y la ejecución explícita de Dispose:

// C#, suponiendo disponibles las mismas clases
// IsPositiveResult, por ejemplo, debería ser un método static
using (TMyForm f = new TMyForm(null))
    return TMyForm.IsPositiveResult(f.ShowModal());

Sin embargo, Delphi.NET no implementa una instrucción using equivalente. La alternativa que propone, aunque está muy mal documentada, consiste en que el compilador traduce el destructor automáticamente en una implementación de IDisposable, por lo que valdría el patrón mostrado inicialmente. La solución es aceptable si lo que más importa es la portabilidad con el código ya existente (un problema que no comparte Freya). De todos modos:

  1. Aunque se recomienda el uso de Dispose siempre que sea posible, esta decisión del compilador quita control al programador.
  2. Seguimos luchando con el bloque try/finally explícito.

La alternativa propuesta por Freya es la siguiente:

// Freya
with new TMyForm(nil) do
    Result := IsPositiveResult(ShowModal);

¡No me he equivocado, sino que he repetido el fragmento de código original a propósito! La instrucción with de Freya garantiza la llamada del método IDisposable.Dispose cuando la expresión utilizada en la cabecera pertenece a una clase que implementa este tipo de interfaz. Incluso va más allá del propósito original de using, porque define un nuevo ámbito para la resolución de nombres de recursos, como en el Pascal original y en Delphi.

VARIABLES DECLARADAS EN INSTRUCCIONES WITH

La introducción de una variante de with como la anterior introduce varios peligros. ¿En qué casos, por ejemplo, tendría with que "desechar" la referencia de su cabecera? En el ejemplo que he mostrado, queda muy claro que estamos creando un nuevo objeto:

with new TMyForm(nil) do ...

Pero, ¿qué habría que hacer en un caso como éste?

with SqlConnection.BeginTransaction do ...

El compilador no tiene forma alguna de saber si BeginTransaction devuelve un nuevo objeto, al que puede aplicarse el método Dispose sin remordimientos, o si está devolviendo un objeto compartido. La primera consecuencia es muy clara:

  • Si aceptamos el with asociado a IDisposable, debemos aceptar que siempre llame a IDisposable al terminar. El uso tradicional que se le daba en Pascal/Delphi queda descartado.

Y hay más problemas. Observe el siguiente ejemplo:

// Freya: esto puede ser peligroso
var
    X: MyClass;
begin
    ...
    X := new MyClass;
    with X do
    begin
        // Cuando with termine, X será "desechada"
    end;
    // X apunta a partir de ahora a una instancia “zombie”
    ...
end;

En el ejemplo anterior, X es una variable cuyo único objetivo es ser utilizada por el bloque with. Pero al estar declarada para todo el método, la zona de código que sigue a la instrucción with es peligrosa, porque seguimos teniendo acceso a X, aunque ya hemos llamado a Dispose sobre el objeto asociado.

En estos casos es mejor imitar a C++/C#, y permitir una declaración de variable que sólo sea válida dentro del bloque delimitado por la instrucción with:

begin
    ...
    with X: MyClass := new MyClass do
    begin
        // Cuando with termine, X será "desechada"
    end;
    // X no disponible en este punto
    ...
end;

Detengámonos un momento y veamos por qué hemos llegado a este punto:

  • Necesitábamos un equivalente del using de C#. Recuerde que uno de los objetivos de Freya es evitar el tecleo excesivo.
  • Delphi/Pascal ofrece una instrucción relacionada: with, que viene de perlas cuando se crea un objeto anónimo temporal. El único problema de with: hay que añadir un try/finally para garantizar la destrucción del objeto anónimo.
  • ¿Por qué no añadimos la llamada automática a IDisposable.Dispose automáticamente con with? No es un equivalente exacto de using: es más útil, porque nos permite evitar una variable innecesaria, en algunos casos.
  • Lamentablemente, si with debe destruir los objetos anónimos, debe destruir cualquier referencia que utilicemos en su cabecera.
  • Para evitar problemas con variables "desechadas" con un tiempo de vida superior al with, añadimos una variante que permite declarar una variable local con el mismo tiempo de vida de esta instrucción. Esta sí que es una réplica exacta del using de C#.

Además, seamos sinceros: no tenemos la presión de la compatibilidad con el pasado que sí tiene Delphi.NET. Una instrucción with limitada a la funcionalidad clásica de Pascal no sirve de mucho. Siempre puede simularse el mismo efecto con una variable temporal. Es más, el abuso de with, incluso olvidando las ambigüedades que puede provocar, puede conducir a código "malo". En definitiva, si tenemos nuestro código salpicado de with's, probablemente estemos expandiendo en línea código que debería estar agrupado dentro de métodos. ¿Cuántas veces habrá visto código como éste en Delphi clásico?:

// Delphi clásico
procedure TForm1.DBGrid1TitleClick(Column: TColumn);
var
  S: string;
begin
  S := Column.FieldName;
  with DataModule2.ClientDataSet1 do
    if IndexFieldNames <> S then
      IndexFieldNames := S
    else
    begin
      AddIndex(S, S, [], S);
      IndexName := S;
    end;
end;

Estamos en la clase TForm1 pero movemos nuestros hilos saltándonos la pobre encapsulación de TDataModule2 y de la propia TClientDataSet. Sería demasiado engorro redefinir esta última clase sólo para este propósito, pero quizás deberíamos haber creado la operación dentro del módulo de datos. Esta es una presunta violación de una ley que no lo es: la falsa "Ley de Demeter".

INSTRUCCIONES WITH ANIDADAS

Al igual que sucede en Delphi, la instrucción with puede anidarse dentro de otros with, y existe incluso una forma de abreviatura que coloca en una misma lista, separadas por comas, las expresiones de los distintos with. Estos dos ejemplos son equivalentes:

 
with A do
    with B do
        C;
      
with A, B do
    C;
 

Claro, si las expresiones A y B pertenecen al mismo tipo, sería imposible sacar partido a este anidamiento directo, porque los campos asociados al contexto de B esconderían los campos correspondientes de A. Recuerde, no obstante, que la variante de with que incluye una declaración local de variable no presenta este problema.

En la práctica el anidamiento de instrucciones with se realizará en algoritmos que proceden por solicitud sucesiva de recursos, como en el siguiente ejemplo, en el que hemos mezclado las dos variantes de with que ofrece Freya:

with conn: SqlConnection := new SqlConnection(connStr),
     new SqlCommand(commText, conn),
     rd: SqlReader := ExecuteReader do
    while rd.Read do
    begin
        ...
    end;

Si tuviéramos que escribir "correctamente" este mismo código sin utilizar el with de Freya, obtendríamos lo siguiente:

var
    conn: SqlConnection;
    cmd: SqlCommand;
    rd: SqlReader;
begin
    conn := new SqlConnection(connStr);
    try
        cmd := new SqlCommand(commText, conn);
        try
            rd := cmd.ExecuteReader;
            try
                while rd.Read do
                begin
                    ...
                end;
            finally
                IDisposable(rd).Dispose;
            end;
        finally
            IDisposable(cmd).Dispose;
        end;
    finally
        IDisposable(conn).Dispose;
    end;
end;

Espero que la longitud del listado alternativo termine de vencer cualquier posible escrúpulo sobre la conveniencia de añadir a Freya un recurso que parece provenir de C++/C#. Una vez abierta la veda sobre las variables declaradas junto con instrucciones, no le costará demasiado aceptar también nuestras extensiones a la instrucción for:

for N: TreeNode in ATree.PreOrder do
begin
    ...
end;

for I: Integer := 0 to Count – 1 do
begin
    ...
end;

La primera variante es en realidad un equivalente exacto del foreach de C# que tanto se echa de menos en Delphi.NET.


Vea también:

Iteradores en Freya Propiedades en Freya
Métodos en Freya Constructores en Freya
Interfaces en Freya