Classes (F#)

Classes são tipos que representam os objetos que podem ter propriedades, métodos e eventos.

Sintaxe

// 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 ...
...

Comentários

As classes representam a descrição fundamental dos tipos de objeto .NET; a classe é o conceito de tipo primário que dá suporte à programação orientada a objetos em F#.

Na sintaxe anterior, type-name é qualquer identificador válido. type-params descreve parâmetros de tipo genérico opcionais. Ele consiste em nomes de parâmetro de tipo e restrições entre colchetes angulares (< e >). Para saber mais, confira Genéricos e Restrições. parameter-list descreve os parâmetros do construtor. O primeiro modificador de acesso pertence ao tipo; o segundo pertence ao construtor primário. Nos dois casos, o padrão é public.

Especifique a classe base de uma classe usando a palavra-chave inherit. Você deve fornecer argumentos, entre parênteses, para o construtor de classe base.

Você declara campos ou valores de função que são locais à classe usando associações let, e deve seguir as regras gerais para associações let. A seção do-bindings inclui o código a ser executado na construção do objeto.

member-list é composto por construtores adicionais, declarações de método estático e de instância, declarações de interface, associações abstratas e declarações de propriedade e evento. Eles são descritos em Membros.

identifier, que é usado com a palavra-chave opcional as, fornece um nome para a variável de instância, ou autoidentificador, que pode ser usado na definição de tipo para se referir à instância do tipo. Para saber mais, confira a seção sobre Autoidentificadores mais adiante neste tópico.

As palavras-chave class e end que marcam o início e o fim da definição são opcionais.

Tipos mutuamente recursivos, que são tipos que fazem referência uns aos outros, são unidos com a palavra-chave and, assim como as funções mutuamente recursivas são. Para obter um exemplo, confira a seção Tipos mutuamente recursivos.

Construtores

O construtor é um código que cria uma instância do tipo de classe. Construtores para classes funcionam um pouco diferente em F# do que em outras linguagens .NET. Em uma classe F#, há sempre um construtor primário cujos argumentos são descritos no parameter-list, que segue o nome do tipo, e cujo corpo é composto por associações let (e let rec) no início da declaração de classe e das associações do seguintes. Os argumentos do construtor primário estão no escopo em toda a declaração de classe.

Você pode adicionar mais construtores usando a palavra-chave new para adicionar um membro, da seguinte maneira:

new(argument-list) = constructor-body

O corpo do novo construtor deve invocar o construtor primário especificado na parte superior da declaração de classe.

O exemplo a seguir ilustra esse conceito. No código a seguir, MyClass tem dois construtores, um construtor primário que usa dois argumentos e outro construtor que não usa argumentos.

type MyClass1(x: int, y: int) =
    do printfn "%d %d" x y
    new() = MyClass1(0, 0)

Associações let e do

As associações let e do em uma definição de classe formam o corpo do construtor de classe primária e, portanto, são executadas sempre que uma instância de classe é criada. Se uma associação let for uma função, ela será compilada em um membro. Se a associação let for um valor que não é usado em nenhuma função ou membro, ela será compilada em uma variável local para o construtor. Caso contrário, será compilada em um campo da classe. As expressões do a seguir são compiladas no construtor primário e executam o código de inicialização para cada instância. Como qualquer construtor adicional sempre chama o construtor primário, as associações let e do sempre são executadas independentemente de qual construtor é chamado.

Os campos criados pelas associações let podem ser acessados em todos os métodos e propriedades da classe; no entanto, não podem ser acessados a partir de métodos estáticos, mesmo que os métodos estáticos usem uma variável de instância como parâmetro. Eles não podem ser acessados usando o autoidentificador, se houver um.

Autoidentificadores

Um autoidentificador é um nome que representa a instância atual. Os autoidentificadores se assemelham à palavra-chave this em C# ou C++ ou Me no Visual Basic. Você pode definir um autoidentificador de duas maneiras diferentes, dependendo se deseja que o autoidentificador esteja no escopo de toda a definição de classe ou apenas para um método individual.

Para definir um autoidentificador para toda a classe, use a palavra-chave as após os parênteses de fechamento da lista de parâmetros do construtor e especifique o nome do identificador.

Para definir um autoidentificador para apenas um método, forneça o autoidentificador na declaração de membro, pouco antes do nome do método e de um ponto (.) como separador.

O exemplo de código a seguir ilustra as duas maneiras de criar um autoidentificador. Na primeira linha, a palavra-chave as é usada para definir o autoidentificador. Na quinta linha, o identificador this é usado para definir um autoidentificador cujo escopo é restrito ao método PrintMessage.

type MyClass2(dataIn) as self =
    let data = dataIn
    do
        self.PrintMessage()
    member this.PrintMessage() =
        printf "Creating MyClass2 with Data %d" data

Ao contrário de outras linguagens .NET, você pode nomear o autoidentificador como quiser; não há restrições a nomes como self, Me ou this.

O autoidentificador declarado com a palavra-chave as não é inicializado até depois do construtor base. Portanto, quando usado antes ou dentro do construtor base, System.InvalidOperationException: The initialization of an object or value resulted in an object or value being accessed recursively before it was fully initialized. será gerado durante o runtime. Você pode usar o autoidentificador livremente após o construtor base, como em associações let ou do.

Parâmetros de tipo genérico

Parâmetros de tipo genérico são especificados em colchetes angulares (< e >), na forma de uma única aspa seguida por um identificador. Vários parâmetros de tipo genérico são separados por vírgulas. O parâmetro de tipo genérico está no escopo durante toda a declaração. O exemplo de código a seguir mostra como especificar parâmetros de tipo genérico.

type MyGenericClass<'a>(x: 'a) =
    do printfn "%A" x

Os argumentos de tipo são inferidos quando o tipo é usado. No código a seguir, o tipo inferido é uma sequência de tuplas.

let g1 = MyGenericClass(seq { for i in 1..10 -> (i, i * i) })

Especificação de herança

A cláusula inherit identifica a classe base direta, se houver uma. Em F#, apenas uma classe base direta é permitida. As interfaces que uma classe implementa não são consideradas classes base. As interfaces são discutidas no tópico Interfaces.

Você pode acessar os métodos e as propriedades da classe base da classe derivada usando a palavra-chave de linguagem base como um identificador, seguido por um ponto (.) e o nome do membro.

Para obter mais informações, consulte Herança.

Seção Membros

Você pode definir métodos estáticos ou de instância, propriedades, implementações de interface, membros abstratos, declarações de evento e construtores adicionais nesta seção. As associações let e do não podem aparecer nessa seção. Como os membros podem ser adicionados a uma variedade de tipos F#, além de classes, eles são discutidos em um tópico separado, Membros.

Tipos mutuamente recursivos

Quando você define tipos que fazem referência entre si de forma circular, você agrupa as definições de tipo usando a palavra-chave and. A palavra-chave and substitui a palavra-chave type em todas, exceto na primeira definição, da seguinte maneira.

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

A saída é uma lista de todos os arquivos no diretório atual.

Quando usar classes, uniões, registros e estruturas

Considerando a variedade de tipos a escolher, você precisa ter uma boa compreensão do que cada tipo foi projetado, a fim de selecionar o tipo apropriado para uma situação específica. As classes são projetadas para uso em contextos de programação orientados a objetos. A programação orientada a objetos é o paradigma dominante usado em aplicativos escritos para o .NET Framework. Se o código F# precisar trabalhar em estreita colaboração com o .NET Framework ou com outra biblioteca orientada a objetos e, especialmente, se você precisar se estender de um sistema de tipo orientado a objeto, como uma biblioteca de interface do usuário, as classes provavelmente serão apropriadas.

Se você não estiver interoperando de perto com o código orientado a objeto ou se estiver escrevendo um código independente e, portanto, protegido contra interação frequente com código orientado a objeto, considere o uso de uma combinação de classes, registros e uniões discriminadas. Uma união discriminada única e bem pensada, juntamente com o código de correspondência de padrões apropriado, geralmente pode ser usada como uma alternativa mais simples a uma hierarquia de objetos. Para obter informações sobre uniões discriminadas, confira Uniões discriminadas.

Os registros têm a vantagem de serem mais simples do que as classes, mas não são apropriados quando as demandas de um tipo excedem o que pode ser feito com sua simplicidade. Os registros são basicamente agregações simples de valores, sem construtores separados que podem executar ações personalizadas, sem campos ocultos e sem implementações de herança ou interface. Embora membros como propriedades e métodos possam ser adicionados aos registros para tornar o comportamento deles mais complexo, os campos armazenados em um registro ainda são uma simples agregação de valores. Para saber mais sobre registros, confira Registros.

As estruturas também são úteis para pequenas agregações de dados, mas diferem de classes e registros, pois são tipos de valor .NET. Classes e registros são tipos de referência do .NET. A semântica de tipos de valor e tipos de referência são diferentes, pois os tipos de valor são passados por valor. Isso significa que são copiados bit por bit quando são passados como um parâmetro ou retornados de uma função. Eles também são armazenados na pilha ou, se forem usados como um campo, inseridos dentro do objeto pai em vez de armazenados em seu próprio local separado no heap. Portanto, as estruturas são apropriadas para dados acessados com frequência quando a sobrecarga de acesso ao heap é um problema. Para saber mais sobre structs, confira Structs.

Confira também