Continuamos con la introducción al lenguaje Pascal Moderno.

Esta entrada es una traducción al español, del texto original escrito por Michalis Kamburelis.

Clases

Lo esencial

En el nivel básico, una clase es solo un contenedor para:

  • campos (que es un nombre elegante para «una variable dentro de una clase»)
  • métodos (que es un nombre elegante para «un procedimiento o función dentro de una clase»)
  • Propiedades (que es una sintaxis elegante para algo que parece un campo, pero en realidad es un par de métodos para obtener y establecer algo; más en Propiedades)

La herencia

Tenemos herencia y métodos virtuales.

Por defecto los métodos no son virtuales, decláralos con virtual para hacerlos. Las anulaciones deben estar marcadas con override, de lo contrario recibirá una advertencia. Para ocultar un método sin anularlo (por lo general, no desea hacer esto, a menos que sepa lo que está haciendo), use reintroduce.

Para probar la clase de una instancia en tiempo de ejecución, use el operador is. Para encasillar la instancia en una clase específica, use el operador as.

En lugar de usar X como TMyClass, también puede usar TMyClass(X) encasillado sin marcar. Esto es más rápido, pero da como resultado un comportamiento indefinido si la X no es, de hecho, un descendiente de TMyClass. Por lo tanto, no use el tipo de conversión TMyClass(X), o utilícelo solo en un código donde es absolutamente obvio que es correcto, por ejemplo, justo después de probar con is:

Propiedades

Las propiedades son un muy buen «azúcar de sintaxis» para:

  1. Cree algo que parezca un campo (que se pueda leer y configurar) pero que debajo se realice llamando a métodos getter y setter. El uso típico es realizar algún efecto secundario (por ejemplo, volver a dibujar la pantalla) cada vez que cambia algún valo
  2. Cree algo que parezca un campo, pero que sea de solo lectura. En efecto, es como una función constante o sin parámetros

Tenga en cuenta que en lugar de especificar un método, también puede especificar un campo (normalmente un campo privado) para obtener o establecer directamente. En el ejemplo anterior, la propiedad Color utiliza un método establecido SetColor. Pero para obtener el valor, la propiedad Color se refiere directamente al campo privado FColor. Hacer referencia directa a un campo es más rápido que implementar métodos getter o setter triviales (más rápido para usted y más rápido en la ejecución).

Al declarar una propiedad se especifica:

  1. Si se puede leer y cómo (leyendo directamente un campo o usando un método «captador»).
  2. Y, de manera similar, si se puede establecer y cómo (escribiendo directamente en un campo designado o llamando a un método de «setter»).

El compilador verifica que los tipos y parámetros de los campos y métodos indicados coincidan con el tipo de propiedad. Por ejemplo, para leer una propiedad de número entero, debe proporcionar un campo de número entero o un método sin parámetros que devuelva un número entero.

Técnicamente, para el compilador, los métodos «getter» y «setter» son simplemente métodos normales y pueden hacer absolutamente cualquier cosa (incluidos los efectos secundarios o la aleatorización). Pero es una buena convención diseñar propiedades para que se comporten más o menos como campos:

  • La función getter no debería tener efectos secundarios visibles (por ejemplo, no debería leer alguna entrada del archivo/teclado). Debe ser determinista (sin aleatorización, ni siquiera pseudoaleatorización :). Leer una propiedad muchas veces debería ser válido y devolver el mismo valor, si nada cambió en el medio.

Tenga en cuenta que está bien que getter tenga algún efecto secundario invisible, por ejemplo, almacenar en caché un valor de algún cálculo (conocido por producir los mismos resultados para una instancia determinada), para devolverlo más rápido la próxima vez.

  • De hecho, esta es una de las interesantes posibilidades de una función «captador». La función setter siempre debe establecer el valor solicitado, de modo que llamar al getter lo devuelva. No rechace valores inválidos en silencio en el «setter» (provoque una excepción si es necesario). No convierta ni escale el valor solicitado. La idea es que después de MyClass.MyProperty := 123; el programador puede esperar que MyClass.MyProperty = 123.
  • Las propiedades de solo lectura a menudo se usan para hacer que algunos campos sean de solo lectura desde el exterior. Nuevamente, la buena convención es hacer que se comporte como una constante, al menos constante para esta instancia de objeto con este estado. El valor de la propiedad no debe cambiar inesperadamente. Conviértalo en una función, no en una propiedad, si su uso tiene un efecto secundario o devuelve algo aleatorio.
  • El campo de «respaldo» de una propiedad es casi siempre privado, ya que la idea de una propiedad es encapsular todo acceso externo a ella.
  • Es técnicamente posible crear propiedades de solo conjunto, pero aún no he visto un buen ejemplo de tal cosa 🙂

Las propiedades también se pueden definir fuera de la clase, a nivel de unidad. Entonces, tienen un propósito análogo: parecen una variable global, pero están respaldados por rutinas getter y setter.

Serializacion de propiedades

Las propiedades publicadas son la base de una serialización (también conocida como componentes de transmisión) en Pascal. La serialización significa que los datos de la instancia se registran en un flujo (como un archivo), desde el cual se pueden restaurar más tarde.

La serialización es lo que sucede cuando Lazarus lee (o escribe) el estado del componente desde un archivo xxx.lfm. (En Delphi, el archivo equivalente tiene la extensión .dfm). También puede usar este mecanismo explícitamente, usando rutinas como ReadComponentFromTextStream de la unidad LResources. También puede usar otros algoritmos de serialización, por ejemplo la Unidad FpJsonRtti (serializar a JSON).

En Castle Game Engine: use la unidad CastleComponentSerialize (basada en FpJsonRtti) para serializar nuestras jerarquías de interfaz de usuario y componentes de transformación.

En cada propiedad, puede declarar algunas cosas adicionales que serán útiles para cualquier algoritmo de serialización:

Puede especificar el valor predeterminado de la propiedad (usando la palabra clave predeterminada).

  • Tenga en cuenta que aún debe inicializar la propiedad en el constructor a este valor predeterminado exacto (no se hace automáticamente). La declaración predeterminada es simplemente una información para el algoritmo de serialización: «cuando finaliza el constructor, la propiedad dada tiene el valor dado».
  • Si la propiedad debe almacenarse en absoluto (usando la palabra clave almacenada).

Excepciones. Un ejemplo rápido

Tenemos excepciones. Se pueden atrapar con cláusulas try…except…​ end, y finalmente tenemos secciones como try…finally…end.

Tenga en cuenta que la cláusula finalmente se ejecuta incluso si sale del bloque usando Exit (desde la función/procedimiento/método) o Break o Continue (desde el cuerpo del bucle).

Consulte el capítulo Excepciones para obtener una descripción más detallada de las excepciones.

Especificadores de visibilidad

Como en la mayoría de los lenguajes orientados a objetos, tenemos especificadores de visibilidad para ocultar campos/métodos/propiedades.

Los niveles básicos de visibilidad son:

Public

todos pueden acceder a él, incluido el código en otras unidades.

Private

sólo accesible en esta clase.

Protected

solo accesible en esta clase y descendientes.

La explicación anterior de visibilidad privada y protegida no es precisamente cierta. El código en la misma unidad puede superar sus límites y acceder libremente a las cosas privadas y protegidas. A veces, esta es una buena característica, le permite implementar clases estrechamente conectadas. Use estricta privacidad o estricta protección para asegurar sus clases de manera más estricta. Ver el privado y privado estricto.

De forma predeterminada, si no especifica la visibilidad, la visibilidad de las cosas declaradas es pública. La excepción son las clases compiladas con {$M+} o los descendientes de las clases compiladas con {$M+}, que incluye todos los descendientes de TPersistent, que también incluye todos los descendientes de TComponent (ya que TComponent desciende de TPersistent). Para ellos, se publica el especificador de visibilidad predeterminado, que es como público, pero además el sistema de transmisión sabe manejar esto.

No todos los campos y tipos de propiedades están permitidos en la sección publicada (no todos los tipos se pueden transmitir y solo las clases se pueden transmitir desde campos simples). Solo use público si no le importa la transmisión pero quiere algo disponible para todos los usuarios.

Antecesor por defecto

Si no declara el tipo antepasado, cada clase hereda de TObject.

Self

La palabra clave especial Self se puede usar dentro de la implementación de la clase para referirse explícitamente a su propia instancia. Es equivalente a esto de C++, Java y lenguajes similares.

Llamando al método heredado

Dentro de la implementación de un método, si llama a otro método, por defecto llama al método de su propia clase. En el código de ejemplo a continuación, TMyClass2.MyOtherMethod llama a MyMethod, que termina llamando a TMyClass2.MyMethod.

Si el método no está definido en una clase determinada, llama al método de una clase antepasada. En efecto, cuando llama a MyMethod en una instancia de TMyClass2, entonces:

  • El compilador busca TMyClass2.MyMethod
  • Si no lo encuentra, busca TMyClass1.MyMethod.
  • Si no lo encuentra, busca TObject.MyMethod.
  • si no se encuentra, la compilación falla.

Puede probarlo comentando la definición de TMyClass2.MyMethod en el ejemplo anterior. En efecto, TMyClass1.MyMethod será llamado por TMyClass2.MyOtherMethod.

A veces, no desea llamar al método de su propia clase. Desea llamar al método de un antepasado (o al antepasado de un antepasado, etc.). Para hacer esto, agregue la palabra clave heredada antes de la llamada a MyMethod, así:

De esta forma, fuerza al compilador a comenzar a buscar desde una clase antecesora. En nuestro ejemplo, significa que el compilador está buscando MyMethod dentro de TMyClass1.MyMethod, luego TObject.MyMethod y luego se da por vencido. Ni siquiera considera usar la implementación de TMyClass2.MyMethod.

TMyClass2.MyOtherMethod above to use inherited MyMethod, and see the difference in the output.139 / 5.000

Consejo: a delante, cambie la implementación de TMyClass2.MyOtherMethod anterior para usar MyMethod heredado y vea la diferencia en el resultado.

La llamada heredada se usa a menudo para llamar al método antecesor del mismo nombre. De esta forma, los descendientes pueden mejorar a los ancestros (manteniendo la funcionalidad del ancestro, en lugar de reemplazar la funcionalidad del ancestro). Como en el ejemplo de abajo.

Dado que el uso inherited para llamar a un método con el mismo nombre, con los mismos argumentos, es un caso muy frecuente, hay un atajo especial para ello: puede escribir heredado; (palabra clave heredada seguida inmediatamente por un punto y coma, en lugar de un nombre de método). Esto significa «llamar a un método heredado con el mismo nombre, pasándole los mismos argumentos que el método actual».

Consejo: En el ejemplo anterior, todos los inherited…​; las llamadas podrían ser reemplazadas por un simple inherited.

Nota 2: por lo general, desea que MyMethod sea virtual cuando muchas clases (a lo largo de la «cadena de herencia») lo definen. Más sobre los métodos virtuales en la sección a continuación. Pero la palabra clave heredada funciona independientemente de si el método es virtual o no. El heredado siempre significa que el compilador comienza a buscar el método en un ancestro, y tiene sentido tanto para los métodos virtuales como para los no virtuales.

Métodos virtuales, anular y reintroducir

Por defecto, los métodos no son virtuales. Esto es similar a C++ y diferente a Java.

Cuando un método no es virtual, el compilador determina a qué método llamar según el tipo de clase declarado actualmente, no según el tipo de clase realmente creado. La diferencia parece sutil, pero es importante cuando se declara que su variable tiene una clase como TFruit, pero de hecho puede ser una clase descendiente como TApple.

La idea de la programación orientada a objetos es que la clase descendiente siempre es tan buena como el antepasado, por lo que el compilador permite usar una clase descendiente siempre que se espera el antepasado. Cuando tu método no es virtual, esto puede tener consecuencias no deseadas. Considere el siguiente ejemplo:

Este ejemplo mostrará

We have a fruit with class TApple

We eat it:

Eating a fruit

En efecto, la llamada Fruit.Eat llamó a la implementación TFruit.Eat y nada llamó a la implementación TApple.Eat.

Si piensa en cómo funciona el compilador, esto es natural: cuando escribió Fruit.Eat, se declaró que la variable Fruit contenía una clase TFruit. Entonces, el compilador estaba buscando el método llamado Eat dentro de la clase TFruit. Si la clase TFruit no contuviera dicho método, el compilador buscaría dentro de un ancestro (TObject en este caso). Pero el compilador no puede buscar dentro de los descendientes (como TApple), ya que no sabe si la clase real de Fruit es TApple, TFruit o algún otro descendiente de TFruit (como TOrange, que no se muestra en el ejemplo anterior).

En otras palabras, el método a llamar se determina en tiempo de compilación.

El uso de los métodos virtuales cambia este comportamiento. Si el método Eat fuera virtual (a continuación se muestra un ejemplo), la implementación real que se llamará se determina en tiempo de ejecución. Si la variable Fruit contiene una instancia de la clase TAple (incluso si se declara como TFruit), el método Eat se buscará primero dentro de la clase TAple.

En Object Pascal, para definir un método como virtual, necesita:

  • Marque su primera definición (en el ancestro más alto) con la palabra clave virtual.
  • Marque todas las demás definiciones (en los descendientes) con la palabra clave override. Todas las versiones anuladas deben tener exactamente los mismos parámetros (y devolver los mismos tipos, en el caso de las funciones).

Este ejemplo mostrará

We have a fruit with class

TApple We eat it:

Eating an apple

Internamente, los métodos virtuales funcionan al tener la llamada tabla de métodos virtuales asociada con cada clase. Esta tabla es una lista de punteros a las implementaciones de métodos virtuales para esta clase. Al llamar al método Eat, el compilador busca en una tabla de método virtual asociada con la clase real de Fruit y usa un puntero a la implementación de Eat almacenada allí.

Si no usa la palabra clave override, el compilador le advertirá que está ocultando (oscureciendo) el método virtual de un ancestro con una definición no virtual. Si está seguro de que esto es lo que desea, puede agregar una palabra clave de reintroducción. Pero en la mayoría de los casos, preferirá mantener el método virtual y agregar la palabra clave anular, asegurándose así de que siempre se invoque correctamente.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio esta protegido por reCAPTCHA y laPolítica de privacidady losTérminos del servicio de Googlese aplican.

El periodo de verificación de reCAPTCHA ha caducado. Por favor, recarga la página.