F# jest językiem głównie funkcyjnym, ale działa w oparciu o platformę .NET, która jest zorientowana obiektowo. Jeśli piszemy kod w F# do użycia w F# to nie potrzebujemy zbytnio klas i interfejsów, ale jeśli chcemy wykorzystać fsharpową bibliotekę w C# to musi ona udostępnić klasy. Więc powiemy dziś sobie o klasach.
Deklarowanie klas
Przejdziemy teraz krok po kroku przez deklarację klasy i jej atrybutów. Najpierw kawałek kodu
type MojaKlasa(x:int, y:int) =
let mutable para = x, y
let version = "1.0"
let suma () = (fst para) + (snd para)
do printfn "Nowa instancja mojej klasy."
member this.X
with get () = fst para
and set value = para <- (value, snd para)
member this.Y
with get () = snd para
and set value = para <- (fst para, value)
member this.Suma () = suma ()
member this.IsGreaterThan a b =
let c, d = para
c > a && d > b
Tyle wystarczy na początek. Idziemy po koleji:
type MojaKlasa(x:int, y:int) =
Mamy tutaj deklarację nowego typu o nazwie MojaKlasa
, którego domyślny konstruktor ma dwa parametry: x
i y
, oba typu int
.
let mutable para = x, y
let version = "1.0"
Następnie deklarujemy zmienną para
, której przypisujemy krotkę (x, y)
wartości z naszego konstruktora. Do tego mamy stałą version = "1.0"
. Są to deklaracje lokalne, więc będą one prywatnymi polami naszej klasy.
let suma () = (fst para) + (snd para)
Dalej mamy deklarację lokalnej (prywatenej) funkcji suma
, która zwraca sumę pierwszego i drugiego elementu krotki.
do printfn "Nowa instancja mojej klasy."
Teraz zauważmy, że nasz konstruktor przyjmuje dwa parametry, ale gdzie jest jego ciało? Otóż jest nim ciało klasy. Deklarowanie zmiennych i stałych jest również ich inicjalizowaniem w konstruktorze na odpowiednią wartość. Jeśli zaś chcemy wykonać kawałek kodu to użyjemy słowa kluczowego do
. W tym wypadku wypiszemy kawałek tekstu na konsolę.
Jeśli będziemy chcieli się odwołać do metod lub własności naszej klasy, które są deklarowane niżej, to również możemy to zrobić w sekcji do
, ale jeśli odwołujemy się do zmiennych/stałych/funkcji lokalnych to wymagana jest kolejność zapisu - to do czego się odwołujemy musiało być wcześniej zadeklarowane.
member this.X
with get () = fst para
and set value = para <- (value, snd para)
Tutaj widzimy publiczną własność X
wraz z funkcjami get
i set
. Deklaracje elementów instancyjnych wymagają słówka member
, a następnie #.Nazwa
, gdzie Nazwa jest nazwą własności lub metody, a #
to dowolne słowo (tutaj użyłem this
), ale musi być to to samo słowo w całej klasie.
Więc, jak widzimy, własność deklarujemy przez podanie nazwy, a następnie użycie with
z definicją metody get
, a potem and
- przedłużenie with
- z definicją metody set
.
member this.Suma () = suma ()
member this.IsGreaterThan a b =
let c, d = para
c > a && d > b
Podobnie jak własność deklarujemy metodę, która będzie miała argumenty i definicję po znaku =
.
Tworzenie i implementacja interfejsów
Czasem oprócz klas będziemy też tworzyli interfejsy, które w F# są dość proste:
type MojInterfejs =
abstract member Metoda: unit -> int
abstract member Własność: string
abstract member Własność: string with set
Czyli tworząc interfejs tworzymy typ, który posiada abstrakcyjnych członków (ang. member) o odpowiedniej sygnaturze typu. Wydaje mi się, że jest to dość proste.
Teraz będziemy chcieli ten interfejs zaimplementować:
type MojaImplementacja() =
let mutable a = ""
interface MojInterfejs with
member this.Metoda() = 10
member this.Własność
with get() = a
and set value = a <- value
Co ciekawe, to że implementacje interfejsów są w F# explicite. Tzn. można użyć metod i własności z interfejsu tylko po przerzutowaniu wartości na typ tego interfejsu
let a = MojaImplementacja()
let d = a.Metoda() // błąd
let b = a :> MojInterfejs // :> to operator rzutowania
let d = b.Metoda() // ok -> 10
Klasy abstrakcyjne
Klasy abstrakcyjne to klasy nie posiadające lub częściowo posiadające implementacje swoich metod. Nie można utworzyć instancji klasy abstrakcyjnej, a należy po niej dziedziczyć. Definujemy ją przez połączenie interfejsu i klasy oraz odpowiedni atrybut:
[<AbstractClass>]
type KlasaAbstrakcyjna() =
let a = 10
abstract member P: unit -> int
Kiedy dziedziczymy po dowolnej klasie używamy słowa kluczowego inherit
wraz z konstruktorem klasy bazowej, a następnie musimy zrobić override
wszystkich niezaimplementowanych metod.
type KlasaDziedziczaca() =
inherit KlasaAbstrakcyjna()
override this.P () = 1
Aby utworzyć funkcję wirtualną, czyli taką, która posiada implementację, ale pozwala na zmiany, użyjemy słowa kluczowego default
(można zamiennie użyć też override
):
type KlasaZWirtualnaMetoda() =
abstract member M: unit -> unit
default this.M() = printfn "Metoda"
Modyfikatory dostępu
Jak na razie mieliśmy prywatne zmienne i stałe oraz publiczne metody i własności. Możemy również mieć metody i własności prywatne lub wewnętrzne (internal):
type AccessibilityExample() =
member this.PublicValue = 1
member private this.PrivateValue = 2
member internal this.InternalValue = 3
Na razie nie ma możliwości tworzenia własności protected
aby były widoczne dla podklas.
Automatyczne własności
Widzieliśmy wcześniej jak rozpisać własność z get
i set
(jeśli chcemy tylko jedno to usuwamy drugie), a co jeśli chcemy mieć to zrobione automatycznie?
Do tego posłuży nam trochę inna składnia:
type Auto() =
// automatyczna własność {get;}
member val X = 1
// automatyczna własność {get; set;}
member val Y = "Ala" with get, set
Zauważmy, że musimy podać wartość początkową naszej automatycznej własności, tak jak musimy podać wartość kiedy piszemy let
. Dzieje się tak, ponieważ w F# nie ma czegoś takiego jak null
(albo nie chcemy żeby było) i wszystkie zmienne/stałe/własności muszą mieć wartość.
Więcej konstruktorów
Dotychczas klasy miały tylko jeden konstruktor, ten główny, podany przy nazwie. Mówię główny, bo taki właśnie jest. Każdy inny konstruktor będzie to specjalna funkcja new(...)
, która wywołuje na koniec inny konstruktor, stworzony lub główny. Może przykład to wyjaśni:
type Konstruktory(a, b:int) =
member val Prop = b with get, set
new() = Konstruktory("", 1)
new(a) = Konstruktory(a, 0)
new(w:bool) =
do printfn "Under construction..."
Konstruktory(w.ToString())
Jak widzimy new(w)
wywołuje konstruktor new(a)
, który wykonuje konstruktor główny.
Rozszerzanie rekordów i unii
Zauważmy, że deklarując unię albo rekord używamy słowa kluczowego type
. Dzieje się tak, ponieważ w rzeczywistości tworzymy nową klasę, która ma pewien wzorzec i jest według niego i naszej customizacji generowana przez kompilator. Dzięki słówku with
możemy więc rozszerzyć nasz typ o metody i własności:
type Name = {Name:string}
with
member this.Size() = this.Name.Length
type Tree = Leaf | Node of int * Tree
with
member this.Child =
match this with
| Node (x, t) -> t
| _ -> Leaf
Teraz po utworzeniu danego typu możemy korzystać również z jego członków (members).
Wideo
Na koniec wideo z poniedziałkowego spotkania Grupy .NET MIMUW, na którym te rzeczy omawialiśmy.