Clases (F#)
Las clases son tipos que representan objetos que pueden tener propiedades, métodos y eventos.
// Class definition:
type [access-modifier] type-name [type-params] [access-modifier] ( parameter-list ) [ as identifier ] =
[ class ]
[ inherit base-type-name(base-constructor-args) ]
[ let-bindings ]
[ do-bindings ]
member-list
...
[ end ]
// Mutually recursive class definitions:
type [access-modifier] type-name1 ...
and [access-modifier] type-name2 ...
...
Comentarios
Las clases representan la descripción fundamental de los tipos de objeto de .NET; la clase es el concepto de tipo primario que respalda la programación orientada a objetos en F#.
En la sintaxis anterior, type-name es cualquier identificador válido. type-params describe parámetros de tipo genérico opcionales. Consta de los nombres y las restricciones de los parámetros de tipo entre corchetes angulares (< y >). Para obtener más información, vea Genéricos (F#) y Restricciones (F#). En parameter-list se describen los parámetros de constructor. El primer modificador de acceso pertenece al tipo; el segundo pertenece al constructor primario. En ambos casos, el valor predeterminado es public.
La clase base de una clase se especifica mediante la palabra clave inherit. Se deben proporcionar argumentos, entre paréntesis, para el constructor de la clase base.
Los valores de campos o funciones que son locales para la clase se declaran mediante enlaces let; se deben seguir las reglas generales para los enlaces let. En la sección do-bindings se incluye código que se ejecuta al realizar la construcción de objetos.
member-list está compuesto de constructores adicionales, declaraciones de métodos de instancia y estáticos, declaraciones de interfaz, enlaces abstractos y declaraciones de propiedades y eventos. Todos ellos se describen en Miembros (F#).
identifier se utiliza con la palabra clave as opcional para dar nombre a la variable de instancia, o autoidentificador, que se puede utilizar en la definición de tipo para hacer referencia a la instancia del tipo. Para obtener más información, vea la sección Autoidentificadores, más adelante en este mismo tema.
Las palabras clave class y end que marcan el inicio y el final de la definición son opcionales.
Los tipos mutuamente recursivos, que son aquellos que hacen referencia uno a otro, están unidos entre sí por la palabra clave and, al igual que sucede con las funciones mutuamente recursivas. Para obtener un ejemplo, vea la sección Tipos mutuamente recursivos.
Constructores
El constructor es código que crea una instancia del tipo de clase. Los constructores de las clases funcionan de un modo ligeramente distinto en F# que en otros lenguajes .NET. En una clase de F#, siempre hay un constructor primario cuyos argumentos se describen en parameter-list, que utiliza el nombre de tipo y cuyo cuerpo está compuesto de los enlaces let (y let rec) al principio de la declaración de clase y de los enlaces do que aparecen a continuación. Los argumentos del constructor primario permanecen en el ámbito durante toda la declaración de clase.
Se pueden agregar constructores adicionales; para ello, se utiliza la palabra clave new para agregar un miembro, como sigue:
new(argument-list) = constructor-body
El cuerpo del nuevo constructor debe invocar el constructor primario que se especifica en la parte superior de la declaración de clase.
En el ejemplo siguiente se muestra este concepto. En el código siguiente, MyClass tiene dos constructores, un constructor primario que toma dos argumentos y otro constructor que no toma ningún argumento.
type MyClass1(x: int, y: int) =
do printfn "%d %d" x y
new() = MyClass1(0, 0)
Para obtener más información, vea Constructores (F#).
Enlaces let y do
Los enlaces do y let de una definición de clase constituyen el cuerpo del constructor de clase primario y, por consiguiente, se ejecutan cada vez que se crea una instancia de clase. Si un enlace let es una función, se compilará en un miembro. Si el enlace let es un valor que no se utiliza en ninguna función ni miembro, se compilará en una variable local del constructor. De lo contrario, se compilará en un campo de la clase. Las expresiones do que figuran a continuación se compilan en el constructor primario y ejecutan el código de inicialización de cada instancia. Dado que todos los constructores adicionales llaman siempre al constructor primario, los enlaces let y do siempre se ejecutan, con independencia del constructor al que se llame.
Se puede tener acceso a los campos creados con enlaces let mediante los métodos y las propiedades de la clase; sin embargo, no se puede tener acceso a ellos desde métodos estáticos, aunque estos últimos tomen una variable de instancia como parámetro. Tampoco se puede tener acceso a ellos mediante el autoidentificador, si lo hay.
Autoidentificadores
Un autoidentificador es un nombre que representa la instancia actual. Los autoidentificadores se parecen la palabra clave this de C# o C++, o a Me en Visual Basic. Hay dos maneras distintas de definir un autoidentificador, según se desee que esté en el ámbito de la definición de clase completa o, simplemente, en el de un método individual.
Para definir un autoidentificador para toda la clase, se utiliza la palabra clave as después de los paréntesis de cierre de la lista de parámetros de constructor, y se especifica el nombre de identificador.
Para definir un autoidentificador para un solo método, se proporciona el autoidentificador en la declaración de miembro, justo antes del nombre del método, con un punto (.) como separador.
En el ejemplo de código siguiente se muestran las dos maneras de crear un autoidentificador. En la primera línea, se utiliza la palabra clave as para definir el autoidentificador. En la quinta línea, se utiliza el identificador this para definir un autoidentificador cuyo ámbito está restringido al método PrintMessage.
type MyClass2(dataIn) as self =
let data = dataIn
do
self.PrintMessage()
member this.PrintMessage() =
printf "Creating MyClass2 with Data %d" data
A diferencia de otros lenguajes .NET, se puede dar al autoidentificador cualquier nombre; no existe la restricción de usar nombres tales como self, Me o this.
El autoidentificador que se declara con la palabra clave as no se inicializa hasta después de haber ejecutado los enlaces let. Por consiguiente, no se puede utilizar en los enlaces let. Pero sí se puede utilizar en la sección de enlaces do.
Parámetros de tipo genérico
Los parámetros de tipo genérico se especifican entre corchetes angulares (< y >), con formato de una comilla sencilla seguida por un identificador. Los parámetros de tipo genérico van separados por comas. El parámetro de tipo genérico permanece en el ámbito durante toda la declaración. En el ejemplo de código siguiente se muestra cómo especificar los parámetros de tipo genérico.
type MyGenericClass<'a> (x: 'a) =
do printfn "%A" x
Cuando se utiliza el tipo, se realiza a inferencia de tipos de los argumentos de tipo. En el código siguiente, la inferencia de tipos da como resultado una secuencia de tuplas.
let g1 = MyGenericClass( seq { for i in 1 .. 10 -> (i, i*i) } )
Especificar la herencia
La cláusula inherit identifica la clase base directa, si la hay. En F#, únicamente se permite una clase base directa. Las interfaces que implementa una clase no se consideran clases base. Interfaces se explican en el tema Interfaces (F#).
Para tener acceso a los métodos y propiedades de la clase base desde la clase derivada, se utiliza la palabra clave de lenguaje base como identificador, seguida de un punto (.) y el nombre del miembro
Para obtener más información, vea Herencia (F#).
Sección de miembros
En esta sección se pueden definir métodos estáticos o de instancia, propiedades, implementaciones de interfaz, miembros abstractos, declaraciones de evento y constructores adicionales. En esta sección no pueden aparecer enlaces let ni do. Dado que los miembros se pueden agregar a gran variedad de tipos de F#, además de a las clases, se explican en un tema independiente, Miembros (F#).
Tipos mutuamente recursivos
Cuando se definen tipos que se hacen referencia entre sí de manera circular, se forma una cadena uniendo las definiciones de tipo mediante la palabra clave and. La palabra clave and reemplaza la palabra clave type en todas las definiciones excepto en la primera, como sigue.
open System.IO
type Folder(pathIn: string) =
let path = pathIn
let filenameArray : string array = Directory.GetFiles(path)
member this.FileArray = Array.map (fun elem -> new File(elem, this)) filenameArray
and File(filename: string, containingFolder: Folder) =
member this.Name = filename
member this.ContainingFolder = containingFolder
let folder1 = new Folder(".")
for file in folder1.FileArray do
printfn "%s" file.Name
La salida es una lista de todos los archivos del directorio actual.
Cuándo se deben utilizar las clases, las uniones, los registros y las estructuras
Teniendo en cuenta la variedad de tipos entre los que elegir, es necesario comprender bien para qué se ha diseñado cada tipo, a fin de seleccionar el adecuado para cada situación concreta. Las clases se han diseñado para usarlas en contextos de programación orientada a objetos. La programación orientada a objetos es el paradigma dominante que se utiliza en las aplicaciones escritas para .NET Framework. Si el código de F# tiene que funcionar en gran medida con .NET Framework o con alguna otra biblioteca orientada a objetos y, en especial, si se tiene que realizar una extensión a partir de un sistema de tipos orientado a objetos, como una biblioteca de IU, probablemente las clases resulten apropiadas.
Si no se interopera estrechamente con código orientado a objetos, o si se escribe código que es autónomo y, por consiguiente, está protegido contra la interacción frecuente con código orientado a objetos, puede ser más conveniente utilizar registros y uniones discriminadas. Con frecuencia se puede utilizar una sola unión discriminada bien pensada, combinada con el código de coincidencia de modelos adecuado, como alternativa más sencilla a la jerarquía de objetos. Para obtener más información sobre las uniones discriminadas, vea Uniones discriminadas (F#).
Los registros presentan la ventaja de ser más simples que las clases, pero no son adecuados cuando las exigencias de un tipo superan las ventajas que aporta su simplicidad. Básicamente, los registros son meros agregados de valores, sin constructores independientes que puedan realizar acciones personalizadas, sin campos ocultos, y sin implementaciones de interfaces ni herencia. Si bien es posible agregar a los registros algunos miembros, como las propiedades y los métodos, para hacer su comportamiento más complejo, los campos almacenados en un registro continúan siendo simples agregados de valores. Para obtener más información sobre los registros, vea Registros (F#).
Las estructuras también resultan útiles para pequeños agregados de datos, pero se diferencian de las clases y de los registros en que son tipos de valor de .NET. Las clases y los registros son tipos de referencia de .NET. Las semántica de los tipos de valor y de los tipos de referencia se diferencia en que los tipos de valor se pasan por valor. Esto significa que se copian bit a bit al pasarlos como parámetro o devolverlos de una función. También se almacenan en la pila o, si se utilizan como campo, se incrustan dentro del objeto primario en lugar de almacenarse en su propia ubicación independiente del montón. Por consiguiente, las estructuras son adecuadas para los datos a los que se tiene acceso con frecuencia cuando la sobrecarga de tener acceso al montón plantea un problema. Para obtener más información sobre las estructuras, vea Estructuras (F#).