Logo

Marco Cantù
Pascal Esencial

Capítulo 4 Actualizar
Tipos de datos definidos
por el usuario

Junto con la noción de tipo, una de las grandes ideas introducidas por el lenguaje Pascal es la habilidad de definir nuevos tipos de datos en un programa. Los programadores pueden definir sus propios tipos de datos mediante constructores de tipos, como tipos de subrango, tipos de matrices, tipos de record [registro], tipos enumerados, tipos de puntero [pointer]. El tipo de datos definido por el usuario, más importante, es la clase, que es parte de las extensiones orientadas a objeto de Object Pascal, no cubiertas en este libro.

Si cree usted que los constructores de tipos son comunes en varios lenguajes de programación, tiene usted razón; pero Pascal fue el primer lenguaje que introdujo la idea de una manera formal y precisa. Aún hay pocos lenguajes que tengan tantos mecanismos para definir tipos nuevos.

Tipos con nombre y tipos sin nombre

A estos tipos se les puede dar un nombre para usarlos más tarde o se pueden aplicar directamente a una variable. Cuando usted le da un nombre a un tipo, debe dedicarle una sección del código específica, como la siguiente :

type
  // definición del subrango
  Uppercase = 'A'..'Z';

  // definición de la matriz
  Temperatures = array [1..24] of Integer;

  // definición del 'record'
  Date = record
    Month: Byte;
    Day: Byte;
    Year: Integer;
  end;

  // definición de tipo enumerado
  Colors = (Red, Yellow, Green, Cyan, Blue, Violet);

  // establecer la definición
  Letters = set of Char;

Construcciones de definición de tipo, similares, pueden ser utilizadas directamente para definir una variable sin un nombre de tipo específico, como en el siguiente código :

var
  DecemberTemperature: array [1..31] of Byte;
  ColorCode: array [Red..Violet] of Word;
  Palette: set of Colors;

Nota: En general, debería usted evitar usar tipos sin nombre, como en el código que aparece arriba, porque no puede usarlos como parámetros para rutinas o declarar otras variables del mismo tipo. La compatibilidad de tipos de Pascal, de hecho, está basada en nombres de tipo, no en la definición efectiva de los tipos. Dos variables de dos tipos idénticos aún no son compatibles, a no ser que sus tipos lleven exactamente el mismo nombre, y a los tipos sin nombre el compilador les da nombres internos. Acostúmbrese a definir un tipo de datos cada vez que necesite una variable compleja, y no se arrepentirá del tiempo que invirtió en hacerlo.

Pero ¿qué significan estas definiciones de tipo? Daré algunas descripciones para aquellos que no estén familiarizados con las construcciones de tipo de Pascal. También intentaré destacar las diferencias con las construcciones en otros lenguajes de programación, así que quizá le interese leer las secciones siguientes, incluso si conoce bien las definiciones de tipos como las de arriba. Finalmente, le mostraré algunos ejemplos en Delphi, e introduciré algunas herramientas que le permitirán acceder dinámicamente a la información de tipos.

Tipos de subrango

Un tipo de subrango define un rango de valores dentro del rango de otro tipo (de ahí el nombre subrango). Puede usted definir un subrango del tipo Integer, de 1 a 10 o de 100 a 1000, o puede usted definir un subrango del tipo Char[acter], como en :

type
  Ten = 1..10;
  OverHundred = 100..1000;
  Uppercase = 'A'..'Z';

En la definición de un subrango, no necesita usted especificar el nombre del tipo base. Sólo necesita proporcionar dos constantes de ese tipo. El tipo original debe ser ordinal, y el tipo resultante será otro tipo ordinal.

Cuando haya definido un subrango, puede asignarle un valor dentro de ese rango. El siguiente código es válido :

var
  UppLetter: UpperCase;
begin
  UppLetter := 'F';

But this one is not:

var
  UppLetter: UpperCase;
begin
  UppLetter := 'e'; // error durante la compilación

Un código como el de arriba resulta en un error durante la compilación : "Constant expression violates subrange bounds" [La expresión constante viola los límites del subrango]. Si, en vez de aquel, escribe el siguiente código ...

var
  UppLetter: Uppercase;
  Letter: Char;
begin
  Letter :='e';
  UppLetter := Letter;

... Delphi lo compilará. Durante la ejecución, si había usted habilitado la opción del compilador Range Checking [comprobación de rango] (en la ficha Compiler del cuadro de diálogo Project Options), obtendrá un mensaje de error Range check error.

Nota: Le sugiero activar esta opción del compilador mientras esté desarrollando un programa, con lo que sería más robusto y sencillo de depurar, ya que en caso de errores obtendrá un mensaje explícito, y no un comportamiento difícil de comprender. Al final, puede usted deshabilitar aquella opción para la versión definitiva del programa, para hacerlo un poco más rápido. De cualquier manera, la diferencia es realmente pequeña, y por esta razón le sugiero que deje activadas todas estas comprobaciones de tiempo de ejecución, incluso en un programa que vaya a distribuir. Lo mismo es válido para otras opciones de tiempo de ejecución, como las comprobaciones de desbordamiento [overflow].

Tipos enumerados

Los tipos enumerados constituyen otro tipo ordinal definido por el usuario. En vez de indicar un rango para un tipo existente, en una enumeración usted hace una lista de todos los valores posibles del tipo. En otras palabras, una enumeración es una lista de valores. Aquí hay algunos ejemplos :

type
  Colors = (Red, Yellow, Green, Cyan, Blue, Violet);
  Suit = (Club, Diamond, Heart, Spade);

Cada valor en la lista tiene una ordinalidad [número de orden] asociada, comenzando desde cero. Cuando aplica usted la función Ord a un valor de tipo enumerado, obtiene este valor. Por ejemplo, Ord (Diamond) devuelve 1.

Nota: Los tipos enumerados pueden tener distintas representaciones internas. Por defecto, Delphi usa una representación interna de 8 bits, a no ser que haya más de 256 valores diferentes, en cuyo caso usa la representación de 16 bits. También hay una representación de 32 bits, que podría ser útil para mantener la compatibilidad con bibliotecas de C o C++. De hecho, puede usted cambiar el comportamiento por defecto, pidiendo una representación de mayor tamaño, usando la directiva del compilador $Z.

La VCL (Visual Component Library, biblioteca de componentes visuales) de Delphi, usa tipos enumerados en muchas ocasiones. Por ejemplo, es estilo del borde de un formulario se define como sigue :

type
  TFormBorderStyle = (bsNone, bsSingle, bsSizeable,
    bsDialog, bsSizeToolWin, bsToolWindow);

Cuando el valor de una propiedad es una enumeración, normalmente podrá elegir de la lista de valores que se muestra en el Object Inspector, como se muestra en la figura 4.1.

Figura 4.1: Una propiedad de tipo enumerado en el Object Inspector

En la ayuda de Delphi se mencionan los posibles valores de una enumeración. Como alternativa, puede usted utilizar el programa OrdType, disponible en www.marcocantu.com, para ver la lista de valores en Delphi de cada enumeración, conjunto, subrango y cualquier otro tipo ordinal. Puede ver un ejemplo de la salida de este programa en la figura 4.2.

Figura 4.2: Información detallada sobre un tipo enumerado, como la muestra el programa OrdType (disponible en mi sitio en la Red).


Tipos de conjunto [set]

Tipos de conjunto indican un grupo de valores, donde la lista de valores disponibles se indica mediante el tipo ordinal en que se basa el conjunto. Los tipos suelen ser limitados, y a menudo se representan con una enumeración o un subrango. Si tomamos el subrango 1..3, los valores posible del conjunto basado en él incluyen sólo 1, sólo 2, sólo 3, tanto 1 como 2, tanto 1 como 3, 2 y 3, todos los tres valores, o ninguno de ellos.

Una variable normalmente contiene exactamente uno de los valores posibles para el rango de su tipo. Una variable de tipo conjunto, sin embargo, puede contener uno, dos o más valores del rango. Incluso puede incluirlos todos. He aquí un ejemplo de un Set :

type
  Letters = set of Uppercase;

Ahora podemos definir una variable de este tipo y asignarle algunos valores del tipo original. Para indicar algunos valores en un conjunto, se escribe una lista separada por comas, encerrada en corchetes. El siguiente código muestra la asignación de varios valores a una variable, de uno sólo, y del valor 'vacío' :


var
  Letters1, Letters2, Letters3: Letters;
begin
  Letters1 := ['A', 'B', 'C'];
  Letters2 := ['K'];
  Letters3 := [];

En Delphi, un conjunto se suele utilizar para indicar flags no exclusivos. Por ejemplo, las dos siguientes líneas de código (que son parte de la biblioteca Delphi) declaran una enumeración de posibles iconos para el borde de una ventana y el correspondiente tipo de conjunto.

type
  TBorderIcon = (biSystemMenu, biMinimize, biMaximize, biHelp);
  TBorderIcons = set of TBorderIcon;

De hecho, una ventana determinada podría no tener ninguno de estos iconos, uno de ellos, o más. Cuando trabaje con el Object Inspector (observe la figura 4.3), podrá proporcionarle los valores a un conjunto expandiendo la selección (haga doble clic en el nombre de la propiedad o clic en el signo '+' a su izquierda) activando y desactivando la presencia de cada valor.

Figura 4.3: Una propiedad de tipo de conjunto en el Object Inspector

Otra propiedad basada en un tipo de conjunto es el estilo de una fuente. Los valores posibles indican fuentes en negrita, cursiva, subrayadas o tachadas. Por supuesto, una misma fuente puede ser a la vez cursiva y negrita, no tener propiedades, o tenerlas todas. Por esta razón se declara como un conjunto. Puede usted asignar valores a este conjunto en el código de un programa, como sigue :

Font.Style := []; // no style
Font.Style := [fsBold]; // bold style only
Font.Style := [fsBold, fsItalic]; // two styles

También puede operar sobre un conjunto de muchas maneras distintas, incluyendo el añadir dos variables del mismo tipo de conjunto (o, para ser más preciso, calcular la unión de las dos variables de conjunto) :

Font.Style := OldStyle + [fsUnderline]; // two sets

Una vez más, puede utilizar los ejemplos OrdType incluidos en el directorio TOOLS del código fuente del libro para ver la lista de valores posibles de muchos conjuntos definidos por la biblioteca de componentes de Delphi.

Tipos de vector [array]

Los tipos de vector definen listas de un número fijo de elementos de un tipos específico. Normalmente se utiliza un índice entre corchetes para acceder a uno de los elementos del vector. Los corchetes también se utilizan para especificar los valores posibles del índice cuando el vector ha sido definido. Por ejemplo, puede definir un grupo de 24 enteros con este código :

type
  DayTemperatures = array [1..24] of Integer;

En la definición del vector, tiene usted que proporcionar un tipo de subrango entre corchetes, o definir un sugrango específico nuevo, utilizando dos constantes de un tipo ordinal. Este subrango especifica los índices válidos del vector. Como se especifica tanto el índice superior como el inferior del vector, los índices no tienen por qué comenzar por cero, como sí es necesario en C, C++, Java y otros lenguajes de programación.

Como los índices del vector están basado en subrangos, Delphi puede comprobar su rango, como ya hemos visto. Un subrango de constante no válido resulta en un error de compilación; y un índice fuera de rango utilizado durante la ejecución resulta en un error de ejecución si la correspondiente opción de compilador está activada.

Usando la definición de vector de arriba, puede usted establecer el valor de una variable DayTemp1 del tipo DayTemperatures, como sigue :

type
  DayTemperatures = array [1..24] of Integer;

var  
  DayTemp1: DayTemperatures;
  
procedure AssignTemp;  
begin  
  DayTemp1 [1] := 54;
  DayTemp1 [2] := 52;
  ...
  DayTemp1 [24] := 66;
  DayTemp1 [25] := 67; // compile-time error

Una matriz puede tener más de una dimensión, como en los siguientes ejemplos :

type
  MonthTemps = array [1..24, 1..31] of Integer;
  YearTemps = array [1..24, 1..31, Jan..Dec] of Integer;

Estos dos tipos de vector se construyen esencialmente sobre los mismos tipos. Así que puede declararlos utilizando los tipos de datos precedentes, como en el siguiente código :

type
  MonthTemps = array [1..31] of DayTemperatures;
  YearTemps = array [Jan..Dec] of MonthTemps;

Esta instrucción invierte el orden de los índices como se muestra arriba, pero también permite adjudicar bloques enteros entre las variables. Por ejemplo, la siguiente instrucción copia las temperaturas de enero a febrero :

var
  ThisYear: YearTemps;
begin
  ...
  ThisYear[Feb] := ThisYear[Jan];

También puede definir un vector que comience en el cero, donde el límite ordinal inferior es cero. Generalmente, el uso de límites más lógicos es una ventaja, ya que no tiene que usar el índice 2 para acceder al tercer elemento, y así sucesivamente. Sin embargo, Windows utiliza invariablemente vectores que comienzan en el cero (porque está basado en el lenguaje C), y la biblioteca de componentes de Delphi tiene a hacer lo mismo.

Si necesita trabajar en un vector, puede siempre comprobar cuáles son sus límites, utilizando las funciones normalizadas Low y High, que devuelven los límites inferior y superior. Le recomiendo encarecidamente utilizar Low y High al operar sobre un vector, especialmente en bucles, ya que hace al código independiente del rango de la matriz. Más tarde, podrá cambiar el rango declarado de los índices del vector, y el código que use Low y High seguirá funcionando. Si escribe usted un bucle fijando el rango de un vector, tendrá que actualizar el código del bucle cuando cambie el tamaño del vector. Low y High hacen su código más fácil de mantener y más fiable.

Nota: Por cierto, no se produce un aumento en el gasto de recursos durante la ejecución por usar Low y High con vectores. Son convertidos durante la compilación en expresiones constantes, no en llamadas a funciones. Esta conversión de expresiones y llamadas a funciones durante la compilación también ocurre con muchas otras funciones simples de sistema.

Delphi usa vectores principalmente en el formulario de propiedades de vectores. Ya hemos visto un ejemplo de tal propiedad en el ejemplo TimeNow, para acceder la propiedad Items de un componente ListBox. Le mostraré algunos ejemplos más de propiedades de vectores en el siguiente capítulo, cuando discuta los bucles Delphi.

Nota: Delphi 4 introdujo los vectores dinámicos en Object Pascal, esto es, vectores que se pueden cambiar de tamaño durante la ejecución, adjudicándoles la cantidad adecuada de memoria. Es fácil usar vectores dinámicos, pero para esta discusión de Pascal consideré que no era adecuado tratarlos. Puede encontrar una descripción de los vectores dinámicos de Delphi en el Capítulo 8.


Tipos de registro (record)

Los tipos record definen colecciones fijas de elementos de distintos tipos. Cada elemento, o campo, tiene su propio tipo. La definición de un tipo record incluye todos estos campos, dándole un nombre a cada uno, que se utiliza para acceder a él posteriormente.

Aquí aparece un pequeño listado con la definición de un tipo record, la instrucción de una variable de tal tipo, y algunas instrucciones en que se utiliza esta variable :

type
  Date = record
    Year: Integer;
    Month: Byte;
    Day: Byte;
  end;
  
var
  BirthDay: Date;
  
begin
  BirthDay.Year := 1997;
  BirthDay.Month := 2;
  BirthDay.Day := 14;

Las clases y los objetos se pueden considerar una extensión del tipo record. Las bibliotecas de Delphi tienden a usar tipos de clase en vez de de record, pero hay muchos tipos record definidos por el API de Windows.

Los tipos record también pueden tener una parte variante, esto es, campos múltiples pueden ser relacionados con la misma área de memoria, incluso si son de un tipo de datos distinto. (Esto equivale a una unión en el lenguaje C.) También puede utilizar estos campos variantes o grupos de campos para acceder al mismo lugar de la memoria dentro de un record, pero considerando esos valores desde perspectivas distintas. Los principales usos de este tipo eran almacenar datos similares, pero distintos, y obtener un efecto similar al de typecasting (algo menos útil ahora que éste también ha sido introducido en Pascal). El uso de tipos de record variantes ha sido reemplazado en gran parte por técnicas orientadas a objetos, y otras técnicas modernas, aunque Delphi los usa en algunos casos peculiares.

El uso de un tipo record variante no es a prueba de tipos , y no representa una práctica de programación recomendable, especialmente para principiantes. Los programadores expertos pueden, de hecho, utilizar tipos record variantes, y las bibliotecas básicas de Delphi los usan. En cualquier caso, no necesitará ocuparse en ellas hasta que sea realmente un programador experto.

Punteros [pointers]

Un tipo puntero define una variable que contiene la dirección de memoria de otra variable de un tipo de datos dado (o indefinido). Así, una variable de puntero apunta indirectamente a un valor. La definición de un tipo puntero no está basada en una palabra clave (keyword) específica, sino que usa un carácter especial, en vez de ello. Dicho símbolo es el acento circunflejo (^) :

type
  PointerToInt = ^Integer;

Una vez que haya definido una variable puntero, puede asignarla a la dirección de otra variable del mismo tipo, usando el operador @ :


var
  P: ^Integer;
  X: Integer;
begin
  P := @X;
  // change the value in two different ways
  X := 10;
  P^ := 20;  

Cuando tenga un puntero P, con la expresión P se referirá a la dirección de la posición de memoria a que apunta el puntero, y con la expresión P^ al contenido real de aquella dirección. Por esta razón, en el fragmento de código de arriba, ^P corresponde a X.

En vez de referirse a una posición de memoria existente, un puntero se puede referir a un nuevo bloque de memoria asignado dinámicamente (en el área de memoria heap) con el procedimiento New. En este caso, cuando deje de necesitar el puntero, deberá deshacerse de la memoria que adjudicó dinámicamente, haciendo una llamada al procedimiento Dispose.


var
  P: ^Integer;
begin
  // initialization
  New (P);
  // operations
  P^ := 20;
  ShowMessage (IntToStr (P^));
  // termination
  Dispose (P);
end;

Si un puntero no tiene valor, puede asignarle el valor nil (vacío). Entonces, puede comprobar si un puntero es nil para ver si actualmente apunta a algún valor. Esto se hace a menudo, porque desreferenciar un puntero inválido causa una infracción de acceso (también llamada fallo de protección general, GPF) :

procedure TFormGPF.BtnGpfClick(Sender: TObject);
var
  P: ^Integer;
begin
  P := nil;
  ShowMessage (IntToStr (P^));
end;

Puede usted ver un ejemplo del efecto de esta porción de código ejecutando el ejemplo GPF (u observando la figura correspondiente, 4.4). El ejemplo contiene también los fragmentos de código que aparecen arriba.

Figura 4.4: El error de sistema que resulta de acceder a un puntero nil , del ejemplo GPF.


En el mismo programa encontrará un ejemplo de acceso seguro a datos. En este segundo caso, el puntero es asignado a una variable local existente, y puede ser utilizado con seguridad, pero a pesar de ello he añadido una comprobación de seguridad :

procedure TFormGPF.BtnSafeClick(Sender: TObject);
var
  P: ^Integer;
  X: Integer;
begin
  P := @X;
  X := 100;
  if P  nil then
    ShowMessage (IntToStr (P^));
end;

Delphi también define un tipo de datos llamado Pointer, que indica punteros sin tipo (como el void* en el lenguaje C). Si necesita un puntero sin tipo, debería usar GetMem en vez de New. Se necesita el procedimiento GetMem cuando el tamaño de la variable de memoria a que se desea apuntar no esté definido.

El hecho de que los punteros son raramente necesarios en Delphi es una ventaja interesante de este entorno. Sin embargo, entender a los punteros es importante para programación avanzada y para una comprensión completa del modelo de objetos de Delphi, que usa punteros "tras el telón".

Nota: Aunque no se use punteros en Delphi muy a menudo, sí que se usa un tipo de construcción muy similar : las referencias. Cada ejemplo de objeto es, en realidad, un puntero implícito a los datos que contiene. De cualquier forma, esto es absolutamente transparente al programados, que usa variables de objeto como cualquier otro tipo de datos.

Tipos de archivo [file]

Otro constructor de tipos específico de Pascal es el tipo file. Los tipos de archivo representan archivos de disco físicos, lo cual es, ciertamente, una peculiaridad del lenguaje Pascal. Puede definir un nuevo tipo de archivo como sigue :

type
  IntFile = file of Integer;

Entonces puede abrir un archivo físico asociado a esta estructura y escribir valores enteros en él, o leer los valores del mismo.

Nota del autor : Ejemplos basados en archivos fueron parte de versiones anteriores a Mastering Delphi 5 [en español, Delphi 4]. Tengo previsto incluirlos también aquí.

El uso de los archivos en Pascal es bastante intuitivo, pero en Delphi hay también algunos componentes que son capaces de almacenar o cargar su contenido de o a un archivo. Hay alguna ayuda para hacer esto en serie, en forma de flujos (streams), y también se apoya el uso de bases de datos.

Conclusión

Este capítulo, que discute tipos de datos definidos por el usuario completan la información que damos sobre el sistema de tipos de Pascal. Ahora estamos preparados para investigar las instrucciones que proporciona este lenguaje para operar sobre las variables que hemos definido.

Capítulo siguiente: Instrucciones

© Copyright Marco Cantù, Wintech Italia Srl 1995-99
© Copyright de la traducción, Rafael Barranco-Droege, 2000