sábado 24 de octubre de 2009

Ser o no ser

Los saludo con gusto, nuevamente desde Morelia, México. Esta vez para exponer algunas curiosidades del no tan famoso pero sí muy útil operador Is, que el lector probablemente ha utilizado alguna vez en sus aplicaciones Delphi.

Como saben, el operador Is tiene como propósito permitirnos averiguar si una referencia de objeto apunta o no a una instancia de cierta clase o de clase descendiente de ésta. Como ejemplo, usamos el operador en este pequeño procedimiento llamado SetText, cuya finalidad hipotética es establecer el texto de un componente dado de forma genérica:

Procedure SetText (Const C :TComponent; Const Text :String);
Begin
  If C Is TButton Then
    TButton (C).Caption := Text
  Else
    If C Is TEdit Then
      TEdit (C).Text := Text
    Else
      // Otras clases de componentes
End;

El parámetro C es declarado de tipo TComponent para que el polimorfismo permita que se proporcione cualquier objeto de clase descendiente de TComponent. El primer If resultará verdadero si el componente dado es un TButton (o de clase descendiente de TButton). El segundo If resultará verdadero si C es de clase TEdit o descendiente.

Podemos entonces llamar al procedimiento SetText de esta forma:

SetText (Button1, 'Hola');
SetText (Edit1, 'a todos');

Por favor, no ahondemos en la finalidad de ese procedimiento y su implementación, es sólo un ejemplo de introducción al tema que quiero plantear. Tampoco pretendo hacer una exposición académica sobre el uso tradicional del operador Is, pues de eso hay ya mucho material publicado. Quiero enfocarme en un detalle curioso y poco documentado que convendría tomarse muy en cuenta.

Consideremos el siguiente formulario (admito que hasta hace un par de años les llamaba formas), en el que tenemos un componente TButton y un componente TEdit:


Hace tiempo, trabajando en no sé qué cosa, necesitaba obtener un componente por su nombre, conociendo de antemano que el componente en cuestión era de una clase específica. Sabemos que el método FindComponent sirve para obtener un componente por su nombre, y siempre devuelve éste como expresión de tipo TComponent, lo cual está muy bien.

Declaración del método TComponent.FindComponent:

function FindComponent(const AName: string): TComponent;

Pero conociendo no sólo el nombre, sino también la clase del componente que yo deseaba obtener, TButton, para fines de este ejemplo, me resultaba muy útil contar con una rutina que actuara como FindComponent, pero que, en lugar de regresar el objeto como TComponent, lo devolviese como TButton (así evitaría escribir un molde de tipo cada vez que obtuviera un TButton por su nombre). Por tal razón creé una rutina parecida a este método función:

Function TForm1.Boton (Const Nombre :String) :TButton;
Var
  C :TComponent;
Begin
  C := FindComponent (Nombre);

  If C Is TButton Then
    Result := TButton (C)
  Else
    Result := Nil;
End;

Hasta ahí todo estaba en armonía con el cosmos, hasta que mi espíritu perfeccionista me dijo al oído «ahórrate la variable C». Decidí entonces cambiar un poco el código, en principio para experimentar:

Function TForm1.Boton (Const Nombre :String) :TButton;
Begin
  Result := TButton (FindComponent (Nombre));

  If Result Is TButton Then
    Exit
  Else
    Result := Nil;
End;

En esta segunda versión de la rutina utilizo Result directamente, en lugar de la variable local C que ha sido desechada. El molde de tipo TButton en la llamada a FindComponent es necesario por obvias razones: el compilador no permitiría asignar una expresión TComponent a una variable cuyo tipo declarativo no fuese esa clase o alguno de sus ancestros (el tipo declarativo de Result es TButton, clase que no es ancestro, sino descendiente, de TComponent).

A simple vista podría parecer aceptable esta forma de simplificar el código, pero surge una puntual interrogante, ¿cómo evalúa Delphi la expresión Result Is TButton? es decir:

¿El operador Is trabaja todo el tiempo con la clase real de la instancia proporcionada o también considera el tipo declarativo de esa expresión objeto?

La respuesta a dicha pregunta puede ser respondida poniendo a prueba la nueva versión del método Boton, con el siguiente código:

procedure TForm1.FormShow(Sender: TObject);
Var
  B :TButton;
begin
  B := Boton ('Edit1');

  If B <> Nil Then
    ShowMessage ('Encontramos el botón ' + B.Name + '.');
end;

Cuando el formulario se abra, observarán en pantalla un bonito, revelador y algo desalentador mensaje como este:


Hagamos otro pequeño cambio al método Boton:

Function TForm1.Boton (Const Nombre :String) :TButton;
Begin
  Result := TButton (FindComponent (Nombre));

  If Result Is TButtonControl Then
    Exit
  Else
    Result := Nil;
End;

En lugar de TButton, he puesto su clase padre (TButtonControl) a la derecha de Is, y el resultado de la prueba fue el mismo. Esto me permite elaborar algunas conclusiones de cómo trabaja realmente el operador Is:

La ayuda de Delphi dice que Is verifica la clase real que en tiempo de ejecución tiene la instancia evaluada. Pero con las pruebas anteriores hemos descubierto que este operador considera en primer lugar el tipo de dato con el que fue declarada la expresión objeto que se evalúa. Si el tipo de dato declarativo de la expresión, a la izquierda del operador Is, es la clase indicada a la derecha de éste o una descendiente, el operador devuelve True en automático sin revisar ya la clase real de objeto. Así pues, la clase real del objeto es revisada solamente en los casos donde la expresión del lado izquierdo indica (por su declaración) una clase ancestro de la especificada en el lado derecho.

Lo anterior hace que este par de Ifs se cumplan, y sin que ocurra error alguno en el segundo:

procedure TForm1.Button1Click(Sender: TObject);
begin
  If TForm (Button1) Is TCustomForm Then
    ShowMessage ('!');

  If TComponent (5) Is TComponent Then
    ShowMessage ('!');
end;

Debo decir que este funcionamiento del operador Is me parece muy razonable, pues es poco visto que una expresión regrese un valor que no sea compatible con su tipo declarativo. De hecho, en la mayoría de los casos, eso es totalmente desaconsejable. El hecho de que Is no siempre verifique la clase real del objeto dado contribuye con toda seguridad a una ejecución más rápida de la operación.

Entonces, para ahorrarnos la variable C en el método Boton, haría falta un pequeño ajuste:

Function TForm1.Boton (Const Nombre :String) :TButton;
Begin
  Result := TButton (FindComponent (Nombre));

  If TComponent (Result) Is TButton Then
    Exit
  Else
    Result := Nil;
End;

Podemos recurrir a los métodos ClassType o InheritsFrom, en lugar del operador Is. Pero también podemos declarar la variable C con la directiva Absolute, para que ocupe el mismo espacio de memoria que Result (uno de mis trucos favoritos):

Function TForm1.Boton (Const Nombre :String) :TButton;
Var
  C :TComponent Absolute Result;
Begin
  C := FindComponent (Nombre);

  If C Is TButton Then
    Exit
  Else
    Result := Nil;
End;

Lo importante es tener presente que el operador Is toma en cuenta el tipo declarativo del objeto evaluado y no siempre revisa la clase de esa instancia.

Un verdadero abrazo.

Al González.