Ponga una clase en su vida
segunda entrega

 

Fecha: 9/Feb/99 (??/Dic/98)
Autor: Luis Sanz, Hospital "Reina Sofía" hrst@ctv.es


 

Siempre ha habido clases

Volvemos al árido tema de las clases y de los objetos. A ver si podemos avanzar un poco más sin dormirnos.

Vimos en el capítulo anterior algunos de los aspectos más sencillos de los módulos de clases, y sus ventajas respecto a los módulos estándar.

Empezamos a construir un proyecto de gestión de amigos, empezando por un sistema "clásico", con formularios que se comunican por variables públicas, y como poco a poco hemos ido modificándolo hasta convertirlo en algo un poco (por ahora, poco) más robusto. Pero apenas nombramos algunas de las posibilidades de esta nueva herramienta de VB.

Clases, objetos, instancias y referencias

Hubo un aspecto que no sé si quedó muy claro en el capítulo anterior. De vez en cuando, hablábamos de "módulos de clases", en otros casos, de "instancias", en otros casos, de "referencias". Vamos a ver si aclaramos un poco el tema.

Un módulo de clases no es un objeto de VB, sino el esquema a base del cual se construirán otros: no es sino la receta con las que VB "cocinará" un objeto en tiempo de ejecución.

Una instancia es el objeto (pido disculpas ante los puristas) que VB construye, a partir de la clase, en tiempo de ejecución. La relación entre el módulo de clases y la instancia es la misma que entre el molde de una tarta y la tarta (el ejemplo está sacado del libro de Harold Davis "Secretos de Visual Basic": un libro de nivel intermedio que no es ninguna maravilla, pero se deja leer).

Pero una vez cocinada la tarta (creado el módulo de clases), tendremos que poder acceder a ella: supongamos que queremos comernos un trozo, pero la habitación está sin luz (ya sabéis que el funcionamiento de Windows es un poco oscuro): si cogemos un cuchillo, y empezamos a dar tajos, lo menos que nos puede pasar es que nos quedemos sin mano (y que se bloquee el sistema operativo). Para poder partirla, necesitamos saber dónde está "en el borde de la mesa que está a la derecha". La ubicación en memoria de la instancia es la referencia.

Lo del modelo, la instancia y la referencia no se aplica sólo a los módulos de clases: afecta a todo tipo de objetos de VB. Por ejemplo

Option Explicit

Private Sub Form_Click()

Dim FLForm As Form1

Set FLForm = New Form1

FLForm.Show

Set FLForm = Nothing

End Sub

Vamos a revisar cada línea:

Dim FLForm As Form1

En esa línea no se ha creado aún la instancia. Simplemente se ha reservado una posición en memoria para la referencia a ésta.

Set FLForm = New Form1

Ahora se "da la orden" de crear una copia en memoria del objeto (del formulario), aunque este todavía no se ha iniciado.

FLForm.Show

Ahora se ha dado a la copia del formulario (a la instancia) la orden de que se muestre (y como buen formulario, se carga solito en cuanto nos referimos a él). La orden no ha sido dada a Form1, sino a la nueva copia en memoria. Para localizarla, lo hacemos mediante su "dirección" (su referencia) que habíamos almacenado en la variable FLForm.

Set FLForm = Nothing

Ahora lo que hacemos es destruir (poner a cero) la variable que contenía la referencia de la instancia del formulario. Pero la instancia no se ha destruido. Sigue presente en memoria (y en la pantalla) hasta que reciba una orden específica (hasta que cerremos manualmente el formulario). Ahora bien, hemos "perdido" la dirección de la instancia y, en principio, ya no podemos localizarla (no es así realmente: VB almacena por su cuenta la referencia en la colección Forms, que podríamos localizar).

Es importante pues ver que el ciclo de vida de las instancias no tiene por qué ser el mismo que las referencias. Por una parte, podemos destruir la referencia mientras la instancia sigue cargada (como en el caso anterior). Y lo contrario: podríamos descargar el objeto mientras la referencia sigue existiendo:

Dim FLForm As frmPrueba

Set FLForm = New frmPrueba

FLForm.Show

Unload FLForm ‘Se descarga

FLForm.Caption = "Hola"

En estos casos, al hacer nueva referencia al objeto, se crea una nueva instancia de éste (por eso a veces no se consigue cerrar aplicaciones mal diseñadas).

En VB, las instancias de las clases y sus referencias están relacionadas: la instancia se destruye cuando se destruye su referencia (con Set… = Nothing), o cuando su referencia cae fuera de ámbito.

En un aspecto los formularios se comportan de una forma diferente a las clases: si tenemos un formulario (frmPrueba) y ponemos:

Private Sub Form_Click()

frmPrueba.Show

frmPrueba.Print "Hola"

End Sub

Lo que ocurre al pulsar es que directamente se accede a la copia actual de frmPrueba, y se imprime "Hola" (completamente diferente a lo que ocurría antes). Es como si hubiese una "referencia automática" a frmPrueba.

Esto se debe a que los formularios tienen una "instancia por defecto" a la que se accede si se usa su nombre. Es como si, por cada formulario que incluyésemos, hubiésemos puesto una línea (invisible) que pusiese:

Global frmPrueba As New frmPrueba

Recordaréis del capítulo anterior las dos formas de declarar un módulo de clases. Ahora, frmPrueba es una instancia a una copia del formulario, pero podemos usarla también para crear nuevas copias:

Dim FLPrueba = New frmPrueba

En el ejemplo anterior, cuando cogemos un molde de la tarta frmPrueba, vemos que ya está lleno (con la instancia por defecto frmPrueba). Pero nadie nos obliga a usarla, sino que podemos crear nuevas copias (nuevas instancias) como vimos antes. Es más, suele ser ventajoso usar copias (ya que así evitamos el uso de una variable global, y restringimos el acceso a nuestras copias de los formularios). A cambio de una pequeña penalización en recursos, hacemos más sólida nuestra aplicación.

Parámetros por valor y por referencia

Esto se relaciona con un aspecto (que no es de las clases; es mucho más básico) que es el paso de los parámetros por valor y por referencia. Por ejemplo, vamos a tener una función que mostrará una cadena sin su última letra. Para ello tenemos un formulario con un TextBox, y este código:

 

Private Sub txtPrueba_Click()

Dim sLCadena As String

sLCadena = txtPrueba.Text

Cortacadena sLCadena

txtPrueba.Text = sLCadena

End Sub

Private Sub Cortacadena(ByRef Cadena As String)

If Len(Cadena) Then

Cadena = Left$(Cadena, Len(Cadena) - 1)

Print Cadena

End If

End sub

Vemos cómo no sólo muestra la cadena cortada, sino que también corta la cadena original. Si sustituímos ByRef por ByVal:

Private Sub Cortacadena(ByVal Cadena As String)

Ahora la función se porta mejor, y "no mete la zarpa" dónde no debe.

La causa es que a las funciones no se les envía las variables, sino la dirección de memoria en la cual se encuentran. Cuando se pasa un parámetro por referencia, le indicamos a la función la ubicación de la variable, y le dejamos que haga con ella lo que quiera. Cuando pasamos un parámetro por valor, lo que hacemos es crear una copia temporal de la variable, e indicar a la función la dirección de la copia. La copia se destruirá cuando salga de ámbito, o sea, al acabar la función.

Por defecto, los procedimientos de VB reciben los parámetros por referencia. Este sistema puede ser marginalmente más rápido, ya que no se requiere la creación de copias locales, pero es inseguro (le damos al procedimiento la llave de casa).

En ocasiones desearemos enviar parámetros por referencia: por ejemplo, podemos querer una función que revise una cadena, quite los espacios, y devuelva por un lado la cadena sin los espacios, y por otra el número de espacios que había (por cierto, para hacer esto VB6 nos ahorrará mucho trabajo, con sus nuevas funciones como Replace). Para hacer eso, pasaremos a la función la cadena por referencia. La función modificará la cadena "directamente", y el valor devuelto será el número de espacios que había.

Pero ¿qué ocurre con los objetos? Habitualmente, lo que manejamos no son los objetos, sino referencias. Así, en este código:

Private Sub Form_Click()

Dim FLForm As frmPrueba

Set FLForm = New frmPrueba

FLForm.Show

Prueba FLForm

FLForm.Print "Otra"

End Sub

Private Sub Prueba(ByVal F As Form)

F.Print "Hola"

End Sub

Cada vez que pulsemos, obtendremos una ventana así:

A pesar de haber pasado el parámetro (el formulario) por valor, lo ha modificado. Se debe a que el parámetro enviado no es una copia del formulario, sino una copia de la referencia al formulario (y por tanto, el procedimiento sigue sabiendo donde encontrarlo). Si en lugar de pasarlo por valor, lo hacemos por referencia, parece que el efecto es el mismo. Pero las diferencias surgen cuando destruimos la referencia:

Private Sub Prueba(ByVal F As Form)

F.Print "Hola"

Set F = Nothing

End Sub

Seguirá funcionando, porque no se ha destruído la referencia almacenada, sino la copia local. Si sustituimos por ByRef:

Private Sub Prueba(ByRef F As Form)

F.Print "Hola"

Set F = Nothing

End Sub

Cuando vayamos a imprimir la línea "Otra", obtenemos un error "Se requiere un objeto": ahora no se ha enviado una copia de la referencia, sino la dirección de esta. Por eso ha podido destruirse.

Por tanto, un seguro contra errores y contra manipulaciones malintencionadas es enviar todos los parámetros por valor, incluyendo los objetos, salvo en los casos en los que necesitemos que la función manipule directamente el parámetro.

Jerarquía de clases

Una posibilidad de las clases, que no tienen los formularios, es la posibilidad de construir objetos complejos a base de otros objetos anteriores. Empezaremos por el principio, y veremos que ocurre con los TDUs.

Tipos Definidos por el Usuario anidados

Los Tipos Definidos por el Usuario (TDUs) pueden construirse a base de otros TDUs:

Public Type TipoDireccion

Direccion As String

Ciudad As String

Pais As String

CódigoPostal As String

Telefono As String

End Type

Public Type TipoAmigo

Nombre As String

Apellido1 As String

Apellido2 As String

Edad As Integer

SexoVaron As Boolean

Direccion As TipoDireccion

End Type

Public Sub Prueba()

Dim tLAmigo As TipoAmigo

tLAmigo.Direccion.Ciudad = "Zaragoza"

End Sub

Así podremos construir una variable "a trozos", usando tipos anteriores, tipos definidos por el API, de alguna librería de tipos... Nos permite organizar mejor los datos que si todas las variables están en el "tipo base", organizando una especie de "jerarquía de tipos".

Además, los TDUs pueden incluir matrices, que pueden ser dinámicas. Por ejemplo, los amigos del ejemplo anterior pueden tener varias direcciones (la del trabajo, la del pueblo, la de sus padres), y varios teléfonos (también se han comprado móvil, que plaga):

Public Type TipoDireccion

Direccion As String

Ciudad As String

Pais As String

CódigoPostal As String

Telefono() As String

End Type

Public Type TipoAmigo

Nombre As String

Apellido1 As String

Apellido2 As String

Edad As Integer

SexoVaron As Boolean

Direccion() As TipoDireccion

End Type

Public Sub Prueba()

Dim tLAmigo As TipoAmigo

ReDim tLAmigo.Direccion(2)

ReDim tLAmigo.Direccion(0).Telefono(1)

tLAmigo.Direccion(0).Ciudad = "Zaragoza"

tLAmigo.Direccion(0).Telefono(0) = "976 123456"

tLAmigo.Direccion(0).Telefono(1) = "609 654321"

End Sub

Como vemos, podemos redimensionar cada matriz, y redimensionar cada matriz "hija" por separado. Esto puede ser una solución para tener matrices dinámicas, redimensionables en dos dimensiones (con ReDim sólo podemos redimensionar la última).

Y, si no sabemos que vamos a guardar (en un caso la dirección del amigo, con clsDireccion; en otro caso, su trabajo, con clsEsclavitud) podemos declarar el elemento del TDU como Variant. Podremos guardar cualquier cosa: números, textos, matrices…

Así podemos construir TDUs complejos que admitan cualquier tipo de datos, incluso relaciones complejas, listas de longitud variable, listas de elementos de distintos tipos... Pero este sistema de organización es ineficaz.

Clases en piezas

En primer lugar, los TDUs tipos complejos siguen siendo TDUs, con sus limitaciones: no pueden ser parámetros de procedimientos públicos (últimas noticias: en Visual Basic 6 pueden serlo: en las páginas del Guille hay más información), no filtran los datos introducidos sin un procedimiento externo (y si además algún elemento es Variant, no digamos), etc.

Pero las clases salen al quite: las podemos construir usando como componentes otras clases: así por ejemplo, podemos escribir una clase clsDireccion:

Option Explicit

Public Direccion As String

Public Ciudad As String

Public Pais As String

Public CódigoPostal As String

Public Telefono As String

E incluirla en clsAmigo, usando un procedimiento Property Set:

Private cMDireccion As clsDireccion

Public Property Set Direccion(NuevaDireccion As clsDireccion)

Set cMDireccion = NuevaDireccion

End Property

Public Property Get Direccion() As clsDireccion

Set Direccion = cMDireccion

End Property

También podría haber sido una variable pública:

Public Direccion As clsDireccion

Aunque ya sabéis que un procedimiento de propiedad nos da más control, como vimos en el capítulo anterior. Para acceder, lo haríamos siguiendo la cadena de clases (de referencias a las instancias de las clases, claro), con:

CGAmigo(0).Direccion.Ciudad = "Zaragoza"

Los elementos de clsDireccion pueden ser también matrices. Pero no es tan sencillo implementarlo como en el caso de los TDUs. Si escribimos:

Public Telefono() As String

Nos dará un error: las matrices no pueden ser miembros públicos de un módulo de objeto (de un formulario o un módulo de clases). Pero podremos hacerlo con una matriz privada a la que accedamos mediante un procedimiento Property:

Private sMTelefono() As String

Public Property Let Telefonos(Indice As Integer, NuevoTelefono As String)

On Error Resume Next

sMTelefono(Indice) = NuevoTelefono

If Err Then

Err.Clear

ReDim Preserve sMTelefono(Indice)

sMTelefono(Indice) = NuevoTelefono

End If

End Property

Public Property Get Telefonos(Indice As Integer) As String

If Indice > UBound(sMTelefono) Then

Err.Raise 13003, , "Este amigo no tiene tantos teléfonos"

Else

Telefonos = sMTelefono(Indice)

End If

End Property

Este procedimiento es más complejo de lo esperado: no basta con asignar directamente el valor de la propiedad, ya que obtendríamos un error del tipo "Índice fuera de rango". Es preciso tener una rutina que redimensione la matriz.

Otro tanto puede hacerse en clsAmigo con clsDireccion: basta con poner:

Private cMDireccion() As clsDireccion

Y luego escribir una rutina similar a la anterior, con la precaución de iniciar los objetos (Set cMDireccion(Indice) = New clsDireccion). Así parece que podemos guardar datos bastante complejos ¿no? Pero, por desgracia, el sistema hace aguas por todas partes:

En primer lugar, no hay ningún sistema de control que evite que se dejen espacios vacíos en la matriz: nos permitiría asignar:

CGAmigo(0).Direccion(0).Telefonos(2) = "976 123456"

CGAmigo(0).Direccion(3).Telefonos(7) = "909 654321"

Pero si accedemos a un "agujero"

MsgBox CGAmigo(0).Direccion(0).Telefonos(5)

Y nos daría una cadena vacía. Pero no es lo peor. Si ponemos:

MsgBox CGAmigo(0).Direccion(2).Telefonos(5)

conseguiremos un error. Por tanto, hemos de hacer aún más complejo el procedimiento de añadir datos: se debe revisar que no haya espacios vacíos en la matriz.

Hay otro problema que aún no habíamos abordado ¿y si nos peleamos con un amigo? Lo querremos borrar de la lista. Pero ¿cómo? Lo mismo, si queremos quitar una dirección, o lo que sea. No nos basta con eliminar la referencia:

Set cGAmigo(NumeroDeMalAmigo) = Nothing

Set cGAmigo(2).Direccion(1) = Nothing

Porque entonces dejaremos huecos dentro de la matriz de amigos (o de direcciones), que serán trampas esperando a algún bucle For…Next, o algo así. Con todo esto, aún se complica más el asunto: necesitamos algún procedimiento que "mantenga" la matriz coherente, del tipo:

Private Function BorraElemento(ByRef Matriz As Variant, ByVal Borrar As Integer) As Boolean

Dim vLProv() As Variant

Dim iLMax As Integer

Dim i As Integer

If IsArray(Matriz) Then

If Not (Borrar < LBound(Matriz) Or Borrar > _

UBound(Matriz)) Then

If UBound(Matriz) = 0 Then

Erase Matriz

Else

iLMax = UBound(Matriz)

ReDim vLProv(iLMax - 1)

For i = 0 To iLMax

If i < Borrar Then

If IsObject(Matriz(i)) Then

Set vLProv(i) = Matriz(i)

Else

vLProv(i) = Matriz(i)

End If

ElseIf i > Borrar Then

If IsObject(Matriz(i)) Then

Set vLProv(i - 1) = Matriz(i)

Else

vLProv(i - 1) = Matriz(i)

End If

End If

Next

ReDim Matriz(iLMax - 1)

For i = 0 To iLMax - 1

If IsObject(vLProv(i)) Then

Set Matriz(i) = vLProv(i)

Else

Matriz(i) = vLProv(i)

End If

Next

End If

BorraElemento = True

End If

End If

Erase vLProv

End Function

Y lo llamaríamos así:

If BorraElemento(cGAmigo, NumeroDeMalAmigo) Then…

Vemos como este procedimiento devuelve DOS resultados: por un lado, la matriz (que se le envía por referencia y que, por tanto, puede modificar), y por otro, si ha funcionado.

Con esto hemos conseguido mantener la "coherencia" de la matriz, pero a costa de desordenarla: los índices de los elementos no son los primitivamente asignados. Y ni siquiera hemos acabado: hemos que proporcionar métodos que indiquen el tamaño de la matriz, claves para recuperar elementos, procedimientos para buscar duplicados… Vamos, un lío gordo.

Afortunadamente, Visual Basic proporciona una nueva herramienta que hace todo esto (y más) ella solita: las colecciones.

Coleccionar clases

Las colecciones es una de las incorporaciones de VB4, que ha sido mejorada en VB5.

Si estuviésemos trabajando en C (cosa que, afortunadamente, no precisamos) tendríamos que decidir como guardar los datos: en arboles de elementos, en listas enlazadas, etc. Un sistema sencillo y eficaz es una lista enlazada, en la cual cada elemento guarda una referencia (un puntero) al siguiente. Y si queremos eliminar un elemento del medio, basta con "conectar" el anterior al siguiente.

Como hemos visto, las matrices no son capaces de hacerlo: son veloces, fáciles de usar, pero son muy rígidas, y para poder eliminar un elemento del centro necesitamos una matriz intermedia.

Una colección es un objeto de VB que funciona como una lista enlazada. Permite hacer de forma transparente al usuario esas operaciones que son tan complejas con una matriz. En VB hay matrices predeterminadas, como Forms, Controls, TableDefs, etcétera. Pero la gran ventaja es que podemos crear nuestras propias colecciones.

Para agregar una colección a nuestro proyecto, basta con escribir en él:

Private Amigos As New Collection

ó

Dim Amigos As Collection

Set Amigos = New Collection

Habitualmente, las colecciones se nombran con el plural del elemento contenido: una colección de Control se llama Controls, de Form Forms, y de elementos clsAmigo, Amigos (que raro: un sistema de nomenclatura lógico).

Una colección tiene sus propios procedimientos. Tiene una única propiedad, Count, que devuelve el número de elementos de la colección. A diferencia de las matrices, las colecciones se numeran desde 1:

Amigos.Count = 0 ‘colección vacía

Amigos.Count = 3 ‘hay tres elementos

Las colecciones tienen tres métodos:

Colección.Add elemento, [clave], [antes], [después]

El único parámetro requerido es el elemento que se añade, que puede ser de cualquier tipo (una variable, un objeto, lo que sea). Si no se suministra una clave, VB asignará una por su cuenta. Pero debe ser única: si se intenta añadir un elemento con una clave ya existente, se produce un error interceptable (que nos permite comprobar si un elemento ya estaba en la colección). Por defecto, los elementos se añaden al final, aunque podemos especificar el elemento anterior o posterior dónde queramos añadirlo (a costa de un descenso considerable en la eficiencia).

Colección.Remove Indice

Donde índice puede ser la clave del elemento, o un valor numérico que indica la posición. Esto último es peligroso, ya que, a diferencia de las matrices, en una colección no conocemos la posición de sus elementos si no la comprobamos antes.

Colección.Item(Indice)

Que devuelve una referencia a un miembro de la colección. Índice puede ser un valor numérico (la posición) o la clave de un elemento. El método Item es el método por defecto de las colecciones, por lo que podemos poner, indiferentemente:

Debug.Print Amigos.Item(ClaveAmigo).Nombre

Debug.Print Amigos(ClaveAmigo).Nombre

Para recorrer la clase, podemos hacerlo con un bucle For…Next

For i = 1 to Amigos.Count

Debug.Print Amigos(i).Nombre

Next

Teniendo en cuenta que las colecciones se numeran desde uno. Pero este sistema es demasiado rígido: si queremos eliminar un elemento de la matriz, podemos tener problemas, al cambiar la posición de los demás. Con el código:

For i = 1 to Amigos.Count

If Amigos(i).Nombre = "Pepe" Then Amigos.Remove i

Next

Obtendríamos un error. Se podría evitar con un bucle inverso:

For i = Amigos.Count to 1 Step –1

Pero es más sencillo con un nuevo tipo de bucle con el que podemos recorrer las colecciones: el bucle For Each…Next, que recorre la colección hasta que no quede ningún elemento:

Dim Amigo As clsAmigo

For Each Amigo In Amigos

If Amigo.Nombre = "Pepe" Then Amigos.Remove Amigo

Next

Set Amigo = Nothing

Insisto en que las colecciones, a diferencia de las matrices, no están ordenadas (sobre todo si ya hemos añadido y eliminado elementos): es conveniente hacer comprobaciones antes de manipular los elementos de la colección.

El proyecto PAmig2.vbp muestra como sustituir la matriz por una colección. Si se revisa el código, podemos ver que:

Matrices y colecciones

Después de todo lo que hemos visto, parece que las matrices son rudimentarias, y las colecciones una bendición del cielo ¿o no? Pues como siempre, depende de lo que queramos hacer.

Lo primero que vamos a hacer es una comparativa de velocidad. Para ello vamos a usar un proyecto sencillo, usando la función del API GetTickCount (podéis buscar en las páginas del Guille para ver cómo hacerlo).

En el proyecto vamos a comparar la velocidad de matrices y colecciones. Como no todas las matrices son iguales, ni todos los elementos, compararemos varios:

ReDim L(lnLMax)

For i = 0 To lnLMax

L(i) = ctL

Next

For i = 0 To lnLMax

ReDim Preserve L(i)

L(i) = ctL

Next

Para los vagos, por si no queréis escribir el código (que está en PTick.vbp), os he calculado los resultados (con mi ordenador: un P120 con 32 MB de RAM: no está mal, pero acepto subvenciones para pasar a un PII). De todas formas, tened en cuenta que los resultados pueden variar mucho, dependiendo del equipo, la memoria RAM, etc. (lo probé con un P233, y no había color).

Algunos de los resultados eran de esperar: como se ve, la matriz de objetos Long es muchísimo más rápida que cualquiera de las otras, sobre todo si se redimensiona de una vez. Por el contrario, las cadenas (String) son lentas, tirando a desesperantes, y más aún si la matriz dónde se almacena es Variant (al fin y al cabo, para guardar una cadena VB tiene que hacer varios malabarismos con la memoria: no es como un Long, que siempre tiene un tamaño fijo. De hecho, usando cadenas de longitud fija (Dim S() As String * 20) casi se duplica la velocidad (no lo hagáis: desperdiciáis espacio de pila).

Sin embargo, las matrices de objetos son más rápidas de lo esperado: al fin y al cabo no se guarda una copia del objeto, sino una referencia (que no es sino una especie de puntero: un número de 32 bits, Long).

En todos los casos, las colecciones se han quedado por detrás de las matrices. En algunos casos, las diferencias son muy significativas: con variables de "longitud fija": números, cadenas de longitud fija, referencias a objetos… Se comportan bastante peor con las de dimensión variable (String y Variant), dónde son sólo marginalmente más rápidas que las colecciones. De todas formas, hay que tener en cuenta que a pesar del gran número de iteraciones (30000), apenas tardan 1,7 segundos (la más lenta). Con un P233, no llegaban a 0,3 segundos.

Por tanto, tenemos una primera conclusión: matrices y colecciones son, en principio, suficientemente rápidas para nuestros fines. Pero si la velocidad es crítica, y para grandes números, son preferibles las matrices, sobre todo para referencias. Si se van a almacenar cadenas, u objetos "raros" (otras matrices, etc.), apenas hay diferencia. Pero, de todas formas, la velocidad no es criterio suficiente para decidirse.

Otra cuestión será la utilidad. En algunos casos, las matrices tendrán ventajas: cuando sólo se añadan elementos (y no se eliminen), cuando queramos estar seguros de la posición de cada uno. Si se van a extraer elementos, y no importa demasiado la pequeña pérdida de tiempo, lo nuestro serán las colecciones.

Para tareas sencillas (como un cálculo dentro de un procedimiento), es más lógico usar matrices, que son más sencillas de depurar y más rápidas de desarrollar. Tened en cuenta que es importante la velocidad de los programas, pero lo realmente caro es el tiempo del programador (a veces, es más barato actualizar el ordenador que actualizar el programa).

En tareas complejas, con objetos anidados, cuando queramos guardar elementos de varios tipos (las matrices Variant son lentas), cuando necesitemos la funcionalidad de un bucle For Each…Next, será preferible una colección.

En resumen: de lo visto hasta ahora, las matrices son para tareas sencillitas, y las colecciones se tienen que enfrentar con las tareas "de verdad". Y eso, sin tener en cuenta la ventaja que vamos a ver ahora:

La casa de paja, la casa de piedra, y otros ejemplos de Microsoft

Si alguna vez os leéis los manuales de Visual Basic (cosa bastante recomendable, si es que los tenéis, claro) os habréis encontrado, en el apartado de las colecciones con unos ejemplo. Como los tenéis en el libro (y el código, en el CD-ROM) os los voy a ahorrar. Pero indican un problemilla que se nos había "olvidado" en el programa de "Gestión de amigos".

En el capítulo anterior habíamos hablado de la seguridad en las variables, y vimos como las variables, "a secas", pueden admitir datos absurdos (como amigos con una edad de –324,544 años). Y que en ocasiones, puede ser preferible el gasto de recursos de sustituir la variable por una clase que "filtre" los datos introducidos.

Pues con las colecciones, lo mismo. Vamos a mirar el ejemplo anterior, y vamos a añadirle una "bomba": añadimos un botón que añada a la colección una referencia a un control:

Private Sub cmdError_Click()

Amigos.Add cmdError

End Sub

 

Si pulsamos el botón, parece que no pasa nada. Hasta que elegimos "Modificar datos": al recorrer la colección con un bucle For Each…Next, de repente encuentra un elemento que no es del tipo clsAmigo, y se produce un error: hemos añadido a la colección un elemento diferente a los anteriores, sin que se queje. Y el error ha quedado "escondido", preparado para saltar en cualquier otro procedimiento (y para que nos rompamos la cabeza depurando).

Esto se debe a una "desventaja" (o ventaja) de las colecciones: se puede guardar cualquier cosa (bueno, todo no; el pescado se estropea fuera de la nevera). Si declaramos una matriz:

Dim iLNumeros() As Long

Dim cGAmigos() as clsAmigos

Sólo podemos guardar datos del tipo declarado: o números, o referencias a instancias de la clase clsAmigos. Sin embargo, en la colección podríamos haber metido cualquier cosa, números, referencias a objetos, cadenas... Es casi como si hubiésemos declarado la matriz así:

Dim vMCualquierCosa() As Variant

Y metiésemos cualquier cosa.

Como siempre, podríamos haber incorporado filtros en el programa: por ejemplo, una instrucción como:

On Error Resume Next

If TypeOf(Elemento) Is clsAmigo

Que filtrase los datos a añadir, a leer… Pero tendríamos el mismo problema que vimos el capítulo anterior con las variables y los TDUs: necesitamos hacer comprobaciones en el código cada vez que fuésemos a usar la colección. Tendríamos que revisar el tipo de cada elemento; ni siquiera nos sería útil la propiedad Count (ya que si hay elementos equivocados, tampoco deberían contarse). Si vamos a tener una colección tan frágil ¿para qué complicarnos la vida? Una matriz, y a correr. Pero hay una solución:

Igual que podíamos tener una variable "envuelta" en una clase, con propiedades, valores por defecto, límites... podemos hacer lo mismo con una colección: envolverla dentro de un módulo de clases, que filtre las acciones que vayamos a realizar.

Colecciones con filtro

Para eso, debemos construir una clase que impida el acceso directo a la colección, pero que proporcione sus funcionalidades. Para ello, abrimos un nuevo módulo de clases (colAmigos), en el que ponemos una colección privada. Este módulo incluye los métodos y propiedades de la colección (Add, Item, Remove y Count), pero con diferencias:

Option Explicit

Private colMAmigos As Collection

Public Property Get Item(Clave As Variant) As clsAmigo

Set Item = colMAmigos(Clave)

End Property

Public Property Get Count() As Long

Count = colMAmigos.Count

End Property

Public Sub Remove(Clave As Variant)

colMAmigos.Remove Clave

End Sub

Public Function Add(ByVal Nombre As String, _

ByVal Apellido1 As String, _

ByVal Apellido2 As String, _

ByVal FechaNacimiento As Date, _

ByVal SexoVaron As Boolean, _

ByVal Clave As String) As clsAmigo

Dim cLAmigo As clsAmigo

Set cLAmigo = New clsAmigo

With cLAmigo

.Nombre = Nombre

.Apellido1 = Apellido1

.Apellido2 = Apellido2

.FechaNacimiento = FechaNacimiento

.SexoVaron = SexoVaron

.Clave = Clave

‘Set .Direccion = Direccion

End With

colMAmigos.Add cLAmigo, Clave

Set Add = cLAmigo

Set cLAmigo = Nothing

End Function

En este caso, como puede verse, no se han incluido todas las propiedades de clsAmigo. Por una parte, aún no hemos implementado lo de la clase anidada Direccion. Por otra, la propiedad Edad es de sólo lectura, por lo que nos la hemos saltado.

Hay un nuevo procedimiento extraño:

Public Property Get NewEnum() As IUnknown

Set NewEnum = colMAmigos.[_NewEnum]

End Property

Este procedimiento se ha incluido para que se pueda recorrer la colección con un bucle For Each…Next (es una de las diferencias entre VB4 y VB5: en VB5 las colecciones desarrolladas por nosotros pueden recorrerse igual que las predeterminadas).

Con todo esto, la clase-colección aún no está completa: son precisos algunos ajustes, pero que no podemos hacer desde la ventana de código. Necesitamos usar el cuadro de diálogo "Atributos del procedimiento", que está en "Herramientas". Pulsando "Avanzadas", podemos acceder a varias características especiales:

Necesitamos modificar dos procedimientos:

Amigos("2").Nombre = "Pepe"

Amigos.Item("2") = "Pepe"

El proyecto PAmig3.vbp muestra las modificaciones realizadas. Como antes, no han variado clsAmigo ni frmAmigo. Sí se ha modificado frmGestion, pero ahora los métodos para añadir, etc., son mucho más sencillos. En las versiones anteriores, los elementos se creaban dentro de frmGestion. Ahora, la gestión (creación, comprobación, etc.) se ha trasladado a colAmigos, por lo que frmAmigos es mucho más sencillo.

Además, vamos a refinar todavía más el proyecto. Teníamos la colección "envuelta" dentro de una clase, con los mismos procedimientos que la colección. Pero nadie nos impide añadirle más. Así, en el proyecto PAmig4.vbp, podemos ver como hacer algunas mejoras.

En primer lugar, sería conveniente que la colección nos informase si se producen cambios en ella (si se añaden o si se quitan elementos). Aunque tiene la propiedad Count, esta sólo nos informa cuando la llamamos. Queremos algo que nos informe siempre. Para ello, añadimos un evento a colAmigos:

Public Event CambioTamaño(ByVal Tamaño As Long)

Que llamamos así (en Add y en Remove):

RaiseEvent CambioTamaño(colMAmigos.Count)

Para que frmGestion pueda recibir ese evento, es preciso que la declaración de colAmigos se haga con la palabra clave WithEvents:

Private WithEvents Amigos As colAmigos

Sin que se nos olvide iniciar la colección (en Form_Load). Ahora no puede hacerse con:

Private WithEvents Amigos As New colAmigos

Ya que VB no nos lo permite. Ahora, en el editor de código, si editamos frmGestion, vemos que en la ventana de objetos hay disponible uno nuevo, el objeto Amigos, con un solo evento: Cambiotamaño.

Private Sub Amigos_CambioTamaño(ByVal Tamaño As Long)

Donde podemos escribir código.

Otra cosa que se puede echar en falta en una colección es algún método que permita eliminar todos los miembros de esta, más o menos la instrucción Erase de las matrices dinámicas, o el método Clear de los ListBox. Como nos gustaría tenerlo, lo añadimos:

Public Sub Clear()

Dim cLAmigo As clsAmigo

For Each cLAmigo In colMAmigos

colMAmigos.Remove cLAmigo.Clave

Next

Set cLAmigo = Nothing

RaiseEvent CambioTamaño(colMAmigos.Count)

End Sub

Ahora podemos incluir un nuevo botón en frmGestion, que elimina todos los amigos de la lista. Como antes, no se han modificado (salvo por algún error detectado) frmAmigos, clsAmigos ni clsDireccion.

Hay clases y clases

Si revisamos el proyecto, podemos ver como la estructura del mismo nos está quedando curiosa:

En el esquema puede verse como unos objetos "dependen" de otros. Tenemos un formulario principal (frmGestion), del cual depende frmAmigos (porque no puede presentarse independientemente, y se le llama desde un procedimiento interno). Se comunican mediante instancias de la clase clsAmigo (que a su vez, contiene la clase clsDireccion). Por otra parte, frmGestion almacena sus datos en colAmigos, que es una colecciónde instancias de clsAmigos (que contienen, a su vez instancias de clsDireccion).

Vemos como el proyecto está estructurado con una jerarquía de objetos, donde unos contienen a los otros (aunque algunos objetos están repetidos dentro del esquema).

Las clases son particularmente adecuadas para esta finalidad. Podemos construir con facilidad objetos complejos, de orden superior (con mucha clase), que se "dividen" en objetos más sencillos, más fáciles de usar, y más fácilmente reutilizables (como lo que hacen los ricos con la clase baja; aunque en VB no haya lucha de clases).

Por ejemplo, ahora vamos a construir un objeto "Coche" de esta forma:

La estructura está muy simplificada, claro. Pero vemos como la clase Coche contiene varios elementos: Chasis, Motor, Habitáculo, y la colección Accesorios (que no hemos desarrollado, para no cansar). Tiene además unas propiedades no relacionadas con los objetos contenidos, como pueden ser Nombre, Precio, Marca…

Cada uno de los objetos contenidos, a su ves, contiene otros: el motor está hecho de una colección de cilindros, tiene una transmisión, y tendrá propiedades como Nombre, Consumo… Y lo mismo: el objeto Cilindro está compuesto por una colección de Válvulas (de elementos Válvula), por un elemento Bujía, otro elemento Pistón (que contiene la colección Segmentos…).

Además, los elementos (con sus elementos dependientes) pueden ser usados en otros proyectos: se podrían tener otras instancias de Rueda (con valores diferentes de sus propiedades Marca, Tamaño, Presión…) en los objetos Camión, Bicicleta, Avión, CochecitoDeNiño…

Vemos cómo podemos convertir un proyecto desorganizado, una simple lista de archivos usados de cualquier forma (como se ven en el Explorador de Proyecto), en un proyecto estructurado, con dependencias precisas, que es algo más complejo de programar, pero mucho más sencillo de depurar, mantener y reutilizar. Pueden verse árboles de objetos de este tipo en los objetos de acceso a datos (podéis verlo mirando la ayuda sobre el motor de base de datos Jet).

Sin embargo, estos árboles de clases no son siempre convenientes, pues si son muy complejos pueden complicar más que ayudar. Como regla, si mantener un objeto complejo comienza a llevar demasiado trabajo, hay que fraccionarlo en un árbol. Y si tener una jerarquía de objetos sencillos se convierte en un dolor de cabeza, hay que juntarlos en uno solo.

Trabajar con asistente

Si me encargasen que diseñase la clase clsCoche, tal como la hemos visto, creo que me daría un soponcio. Me imagino escribiendo uno a uno los métodos y propiedades de cada clase, de cada colección, con sus parámetros, con sus atributos de procedimiento… Y al final, seguro que se me colaban tropecientos errores.

Gracias al Cielo, Don Guillermo Puertas es un alma caritativa que no puede dormir sin pensar en los pobres desarrolladores de VB. Y, pensando en ellos (y en los de C, y en los de Java, que también son de Dios) ha incluido en VB una herramienta que automatiza la construcción de clases y de estructuras complejas: la utilidad de construcción de clases (Class Builder Utility, en la lengua del imperio). Lo tenéis en el menú Add-Ins (y si no está, podéis agregarlo con el Add-In Manager).

Vamos a abrirlo. El proyecto PAmig4, visto con él, queda así:

En la ventana de la izquierda, podemos ver la estructura del proyecto, muy similar a la que vimos antes, con jerarquía. En la derecha tenemos las propiedades, métodos y eventos de la clase que seleccionemos. Además, la barra de herramientas (o el menú) nos permite añadir nuevos elementos. Los botones de la izquierda permiten añadir nuevas clases y nuevas colecciones, los del centro, nuevos métodos, eventos y propiedades.

Cuando añadimos cualquier cosa, se nos presentan diferentes opciones, dependiendo de lo añadido. Para ilustrarlo, vamos a construir la clase Coche que veíamos antes:

Para eso, iniciamos un nuevo proyecto (EXE estándar) y abrimos el asistente, que estará vacío. Añadimos una nueva clase, y se nos presenta esta ventana:

Como veíamos antes, Coche tiene varios componentes: Chasis, Motor, Habitáculo y Accesorios, y varias propiedades. Añadimos las propiedades:

Y, luego, añadimos los componentes. Para eso, seleccionando la clase Coche, pulsamos "Nueva clase", y añadimos las clases constituyentes.

Para añadir la colección Accesorios, pulsamos "Nueva colección" e indicamos los elementos contenidos (Accesorio: crea otra clase automáticamente).

Vamos creando todo el árbol, y en unos minutos tendremos ya:

Cuando cerramos el asistente, nos pregunta si actualizar el proyecto con los cambios. Si aceptamos, se crearán los módulos con las propiedades que hayamos indicado.

No son estas las únicas opciones del asistente. Podíamos haber creado clases basadas en otras clases ya existentes (una forma primitiva de herencia). También nos permite añadir código de control de errores, e instrucciones que escriben en la ventana de depuración los cambios que se produzcan. También permite arrastrar y soltar elementos (colecciones, clases, procedimientos, etcétera).

Por tanto, este asistente es una herramienta francamente útil, y es la que he usado para el desarrollo de las clases de los proyectos de ejemplo (¿o creíais que lo hacía a mano?). Pero, como buena herramienta de Microsoft, no es perfecta.

Private mvarPrecio As String 'local copy

Private mvarNombre As String 'local copy

Private mvarMarca As String 'local copy

Private mvarChasis As Chasis

Private mvarHabitáculo As Habitáculo

Private mvarMotor As Motor

Private mvarAccesorios As Accesorios

Vamos, que con el asistente pueden construirse proyectos complejos, pero es necesario dar luego un buen repaso para que quede bien.

Aunque no sea una herramienta perfecta, os aconsejo que la uséis (y que aprendáis manoseándola). De todas formas, una advertencia: si alguien me presenta un proyecto con un objeto como Coche, le doy un capón. Puede quedar muy bonito como ejemplo, pero también me parece un ejemplo de cómo no hacer las cosas: un proyecto innecesariamente complejo que, salvo que vayamos a diseñar un coche ¿con Visual Basic? Sólo acaba siendo una fuente de problemas.

Volver a empezar

Y para despedir esta entrega, una sugerencia: hemos visto que las colecciones tienen varias ventajas sobre las matrices, pero no todas. Muchas veces, es más cómodo usar una matriz para recorrerla, sin "agujeros", con un bucle For…Next (que resulta más "ordenado" que un bucle For Each…Next) ¿por qué no tomar lo mejor de ambos mundos? Os propongo, como ejercicio, diseñar una clase que contenga una matriz, y que simule una colección. Hay que implementar los métodos Add, Remove, Item y la propiedad Count (no sé como hacer el método NewEnum; si a alguien se le ocurre…). Van dos pistas:

Private vMMatriz() As Variant

Public Function Count() As Long

On Error Resume Next

Count = UBound(vMMatriz) - LBound(vMMatriz)

End Function

Aunque espero que os penséis la razón del control de errores.

En la próxima entrega, hablaremos (supongo) de clases y programación orientada a objetos. Hasta la próxima.


ir al índice

Pulsa este link para ir a la página de clases en Visual Basic

Este otro te llevará a la entrega anterior