lunes, 16 de agosto de 2021

Registros de estructura variable.

A diferencia de los registros de estructura fija, los registros de estructura variable tienen campos disponibles según el resultado del selector CASE OF. No es algo simple de comprender a primera vista por ende lo más recomendable es practicar (y mucho) para poder entender como funciona.
Es usado en programación avanzada, en el código fuente de Turbo Pascal, por ejemplo, y calculo que en Free Pascal también. Vale aclarar que no se recomienda su uso, además de por lo complejo, por tratarse Pascal de un lenguaje fuertemente tipado. Si bien existen al menos desde Turbo Pascal, sospecho que su implementación fue para tener algo parecido a “union” de otro lenguaje. Con Object Pascal, hay muy buenas alternativas, advanced records, objects y class (registros avanzados, objetos y clases).

En mi caso me costaba entender como hacía el compilador, ya que según el CASE luego podrían venir 10 campos más, o 2 o ninguno, porque supuestamente es una estructura variable, pero no es tan variable, es una unión, ahí está la clave, lo variable es como se lo utiliza, pero el record se arma con todos lo campos, si hay un CASE con un selector que define 3 campos y otro define 5 campos, entonces el registro tendrá 8 campos, no 3 u 8 según el valor del selector. Claro que algunos campos compartirán la misma área de memoria. La longitud (máxima) de los campos de la parte variante debe ser conocida por el compilador, por ejemplo, Byte, Integer, string[20] está bien, en cambio string, no.

Por ejemplo:

type
  Reg=record
    campo1:string[5];
    case algo: boolean of:
    true : (campo2: integer; campo3:Byte);
    false: (campo4:word);
  end;

Vendría internamente a ser algo así:

type
  Reg=record
    campo1:string[5];
    campo2: integer;
    algo:boolean;
    campo3:Byte;
    campo4:word;
  end;

Pero hay algo más complicado aún, todos los campos son accesibles independientemente del selector, es decir, el programador es el responsable de por ej., si algo es false entonces no utilizar el campo2 y el campo3, solo el campo4.

type TRec=record
       int:Integer;
       case Boolean of
       True : (s1:String[10]);
       False: (b1:Byte);
     end;

En este caso, no es posible establecer el valor de Boolean.
¿Qué dice la documentación oficial al respecto?
La parte variante debe ser la última en el registro. El identificador opcional en la instrucción case sirve para acceder al valor del campo de etiqueta, que de otro modo sería invisible para el programador. Se puede utilizar para ver qué variante está activa en un momento determinado (sin embargo, depende del programador mantener este campo). En efecto, introduce un nuevo campo en el registro.

Pero hay más, en alguno casos, como veremos en un ejemplo de consola, la parte variante para determinados identificadores o campos, comparten la misma área de memoria, por ende el valor es el mismo independientemente de valor del selector. Según alguien escribió en la wiki de FP, esto es algo extremadamente útil y cita un ejemplo con una declaración de registro y una breve explicación.
El ejemplo es el siguiente:

type
  TSpecialWord = record
    case Byte of
      0: (Word_value: Word);                      // 7  type Word = 0..65535;
      1: (Byte_low, Byte_high: Byte);             // 7, 0 type Byte = 0..255
      2: (Bits: bitpacked array [0..15] of 0..1); // 1, 1, 1, 0, 0, ...
  end;

Este registro tiene solo una parte variable y permite el acceso al valor de la palabra, los bytes individuales e incluso a los bits. Un identificador no es necesariamente necesario en la cláusula case, por lo que no ocupa ninguna memoria. El tamaño de este registro es de dos bytes. En el caso de Bits, esto solo es posible si se utiliza bitpacked. Observe el orden de los bytes, con el byte menos significativo (LSB) primero.

Veamos que pasa con ese ejemplo.
Declaramos una variable sw:TSpecialWord. No inicializamos la variable e imprimimos con writeln. El resultado serán todos ceros. Lo mismo sucede si incicializamos con Deflaut (sw:=Default(TSpecialWord);).
Ahora agregamos:  

sw.Word_value:=4;

Resultado:

Word_value = 4  //Lo asignamos.
Byte_low = 4  //Comparte la misma dirección de memoria, es correcto.
Byte_high = 0  //Es el valor por default.
Bits = 0010000000000000 //Es 4 en binario.

Probemos con:

sw.Word_value:=40000;

Resultado:

Word_value = 40000
Byte_low = 64  //40000/625=64 o 40000/156,25*4=64
Byte_high = 156 //40000/256=156,25
Bits = 0000001000111001 //569 en decimal.

Más pruebas:

sw.Bits[0]:=1;
sw.Bits[1]:=1;
sw.Bits[2]:=1;

Resultado:

Word_value = 7
Byte_low = 7
Byte_high = 0
Bits = 1110000000000000 //7

sw.Byte_low:=5;

Resultado:

Word_value = 5
Byte_low = 5
Byte_high = 0
Bits = 1010000000000000 //5

sw.Byte_high:=128;

Resultado:

Word_value = 32768
Byte_low = 0
Byte_high = 128
Bits = 0000000000000001

sw.Byte_low:=128;

Word_value = 128
Byte_low = 128
Byte_high = 0
Bits = 0000000100000000 //256

sw.Byte_high:=8;

Resultado:

Word_value = 2048
Byte_low = 0
Byte_high = 8
Bits = 0000000000010000 //16

sw.Bits[0]:=1;
sw.Bits[1]:=1;
sw.Bits[2]:=1;
sw.Bits[3]:=1;
sw.Bits[4]:=1;
sw.Bits[5]:=1;
sw.Bits[6]:=1;
sw.Bits[7]:=1;
sw.Bits[8]:=1;  

Resultado:

Word_value = 511
Byte_low = 255
Byte_high = 1
Bits = 1111111110000000 //511

Como vemos es bastante complejo cuando no se utiliza un identificador en el selector CASE.

El identificador del selector debe ser del tipo ordinal.

Los Case se pueden anidar.

Las partes variantes no pueden ser del tipo variant, string largos (string no, string[120] sí),  vectores dinámicos, datos estructurados que contengan lo anteriormente citado o interfaces, pero sí pueden ser punteros a esos tipos (types).

La mejor documentación que encontré es de Embarcadero: http://docwiki.embarcadero.com/RADStudio/Tokyo/en/Structured_Types_(Delphi)#Variant_Parts_in_Records

Como no encontré ningún ejemplo desarrollado completo elaboré uno.
 
Código fuente: RegistrosVariantes.7z
unit Unit1;

{$mode objfpc}{$H+}

interface

uses
  Classes, SysUtils, Forms, Controls, Graphics, Dialogs, StdCtrls,
  DateTimePicker;

type

  { TForm1 }

  TForm1 = class(TForm)
    BMostrarRegVariante: TButton;
    BCerrar: TButton;
    cbCasado: TCheckBox;
    DateTimePicker1: TDateTimePicker;
    Label1: TLabel;
    Label2: TLabel;
    Memo2: TMemo;
    procedure BMostrarRegVarianteClick(Sender: TObject);
    procedure BCerrarClick(Sender: TObject);
    procedure FormClose(Sender: TObject; var CloseAction: TCloseAction);
    procedure FormCreate(Sender: TObject);
  private

  public

  end;

  TVariantRec=record
    ID:Word;
    Nombre:String[50];
    case Casado : Boolean of
      True : (vrDate:TDate);
      False: (str2:String[10]);
  end;

var
  Form1: TForm1;
  VariantRec:TVariantRec;

implementation

{$R *.lfm}

{ TForm1 }

procedure TForm1.FormCreate(Sender: TObject);
begin
  VariantRec.ID:=27;
  VariantRec.Nombre:='Jorge';
  VariantRec.Casado:=True;
  VariantRec.vrDate:=EncodeDate(2010,12,25);
  VariantRec.str2:='Soltero';
end;

procedure TForm1.BMostrarRegVarianteClick(Sender: TObject);
begin
  VariantRec.Casado:=cbCasado.Checked;
  if cbCasado.Checked then
    VariantRec.vrDate:=DateTimePicker1.Date
  else
    VariantRec.str2:='Soltero';
  Memo2.Lines.Add(VariantRec.ID.ToString);
  Memo2.Lines.Add(VariantRec.Nombre);
  Memo2.Lines.Add(BoolToStr(VariantRec.Casado,'True','False'));
  Memo2.Lines.Add(DateToStr(VariantRec.vrDate));
  Memo2.Lines.Add(VariantRec.str2);
end;

procedure TForm1.BCerrarClick(Sender: TObject);
begin
  Close;
end;

procedure TForm1.FormClose(Sender: TObject; var CloseAction: TCloseAction);
begin
  CloseAction:=caFree;
end;

end.  

Más ejemplos, en este caso por consola:

program project1;

{$mode objfpc}{$H+}

uses
  {$IFDEF UNIX}{$IFDEF UseCThreads}
  cthreads,
  {$ENDIF}{$ENDIF}
  Classes
  { you can add units after this };

type TRec=record
       int:Integer;
       case Boolean of
       True : (s1:String[10]);
       False: (b1:Byte);
     end;
var
  rec:TRec;

begin
  rec.int:=2;
  rec.s1:='abx';
  rec.b1:=4;

  WriteLn(rec.int);
  WriteLn(rec.b1);
  WriteLn(rec.s1);
  ReadLn;
end.

Resultado:                   

2
4
abx

Todo, como un registro fijo.
 
-------------------------------------------------------------------------------------------------------------------------- 

type TRec=record
       int:Integer;
       case cond:Boolean of
       True : (s1:String[10]);
       False: (b1:Byte);
     end;
var
  rec:TRec;

begin
  rec.int:=2;
  rec.cond:=False;
  rec.s1:='abx';
  rec.b1:=4;

  WriteLn(rec.int);
  WriteLn(rec.cond);
  WriteLn(rec.b1);
  WriteLn(rec.s1);
  ReadLn;
end.       

Resultado:                   

2
FALSE
4
abx

De nuevo, todo, al compartir la misma dirección de memoria, entiendo que no debería suceder, o dar en s1 otro resultado que sea abx. Tampoco debí pedir el campo s1.
 
--------------------------------------------------------------------------------------------------------------------------

type TRec=record
       int:Integer;
       case cond:Boolean of
       True : (s1:String[10]);
       False: (s2:String[10]; b1:Byte);
     end;
var
  rec:TRec;

begin
  rec.int:=2;
  rec.cond:=False;
  rec.s1:='abx';
  rec.s2:='fgh';
  rec.b1:=4;

  WriteLn(rec.int);
  WriteLn(rec.cond);
  WriteLn(rec.s1);
  WriteLn(rec.s2);
  WriteLn(rec.b1);
  ReadLn;
end.   

Resultado:                   

2
FALSE
fgh
fgh
4

En este caso si se muestra el mismo valor en s1 y s2, mismo tipo, imprime el valor de s2 porque cond=flase. 
 
Documentación:

No hay comentarios:

Publicar un comentario