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 ARREGLESFreya ofrece las tres instrucciones básicas y casi universales para señalar y tratar excepciones:
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 RECURSOSLo 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:
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 WITHLa 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:
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:
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 ANIDADASAl 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:
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:
|