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 récord [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ámica mente 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 Compile. 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 des habilitar 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.
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.
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.
Tipos de registro (record)
Los tipos récord definen colecciones fijas de elementos de distintos tipos. Cada elemento, o campo, tiene su propio tipo. La definición de un tipo récord 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 récord, 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;
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.
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
No hay comentarios:
Publicar un comentario