A nyelv lehetőséget ad a rekordok és uniók osztálytagokkal való bővítésére. Ezek a tagok lehetnek az objektumpéldányhoz (instance) kötött és osztályhoz kötött statikus függvények, változók vagy property-k. Az objektumpéldányhoz kötött tagfüggvényeket a következő szintaxissal definiálhatjuk: member objname.methodname(x) = utasítások. A legtöbb objektumorientált nyelvtől eltérően az objektumpéldányra, amin végrehajtjuk a műveletet, nem a this vagy a self kulcsszóval hivatkozunk, hanem a definícióban tetszőlegesen választott objname névvel. A statikus tagfüggvényeket a static member methodname(x) = utasítások szintaxissal definiálhatjuk, itt ki kell hagyni a objname azonosítót. A statikus osztálytagokra anélkül is hivatkozhatunk, hogy példányosítanánk a típusból. Általában arra használják őket, hogy értékeket rendeljenek egy típushoz, nem pedig annak egy adott objektumához.
Az osztálytagokat lehet úgy definiálni, hogy valamilyen értéket állítanak be, vagy adnak vissza. Ezek az osztálymetódusok lesznek a get és set módosítószval ellátva.
Alapértelmezésben az osztálytagok public hozzáféréssel rendelkeznek. Természetesen hozzáférési módosítóval is el lehet látni őket. Ahhoz, hogy hozzáadjunk egy hozzáférési módosítót az osztálytaghoz, írjuk a módosítószót (public, private, internal; lejjebb a Láthatóságnál olvashatunk róluk) az osztálytag neve elé. Ha a getre és setre egyaránt vonatkozik a módosítószó, akkor kell rögtön a név elé írni, viszont ha külön láthatóság vonatkozik rájuk, akkor a get vagy set elé kell írni a módosítószót.
Az osztályok az F# objektumorientáltságának egyik legfontosabb építőelemei. Az osztályok a C# osztályokhoz hasonló tulajdonságokkal rendelkeznek: elrejthetik a belső állapotukat, származhatnak ősosztályokból, lehet konstruktoruk és megvalósíthatnak interfészeket is. Az osztályok szintaxisa nagyon hasonlít a tagváltozókat tartalmazó rekordokéhoz, azonban itt kötelező egy konstruktor megadása. Az osztályok definícióját a következő szintaxissal kell megadni:
Az osztály definíciójában két helyen is megadhatunk hozzáférési módosítószót. Az elsőt a type kulcsszó után (az osztály neve előtt), amivel az osztályra vonatkozó megszorítást adhatjuk meg. Ez alapértelmezetten public. A másodikat pedig a típus paraméterek után lehet megadni (a típus paraméterek a konstruktor paraméterek előtt szerepelnek, de az osztály neve után), ez pedig az elsődleges konstruktorra vonatkozik. A konstruktor paramétereknél a típus paraméterek nevei szerepelnek és/vagy megszorítások lehetnek, amiket < és > jelek közé írhatunk.
Az azonosítónév, melyet az as kulcsszóval adunk meg egy nevet ad a példányváltozónak vagy a self azonosítónak, amit arra használhatunk, hogy a típus definíciójában (osztály leírásában) hivatkozzunk az aktuális típuspéldányra.
Természetesen az osztályoknál lehetőség van az öröklődésre is, erről lejjebb olvashatunk.
Mezőket és függvényeket, amik lokálisak az osztályra nézve a let kötéseknél definiáljuk és a let kötésekre vonatkozó szabályokat kell követni ilyenkor. A do-kötések pedig olyan kódot tartalmaznak, amelyeknek az objektum létrehozásakor kell lefutnia.
A tag-lista újabb konstruktorokat, példány és statikus eljárás deklarációkat, interface deklarációkat, absztrakt kötéseket, tulajdonság és esemény deklarációkat tartalmazhat.
Ezeken felül lehetőség van arra, hogy kölcsönösen rekurzív típusokat is létrehozzunk, amik egymásra tartalmaznak hivatkozásokat. Ezeknek a definícióit az and kulcsszóval kötjük össze, ahogyan a kölcsönösen rekurzív függvényeknél is.
A class és end, amik az osztálydefiníció elejét ill. végét jelzik, opcionálisak.
Az első sorban a Vector2D-t egy konstrukciós kifejezéssel definiáljuk, ezután az x és y konstruktor paraméterek az osztály összes nem statikus tagjában használható. A második sorban egy privát adattagot adtunk meg, amit az F# mindig csak az osztály egy példányának létrehozásakor számol ki. A példa utolsó sorában pedig a konstruktor egy explicit túlterhelését adtuk meg, ami csak egy float paramétert igényel.
Az osztályok member kifejezéssel megadott tagjaihoz explicit rendelhetünk láthatóságokat. Az F#-ban háromféle láthatóság van: private, internal és public. A private tagokat csak a definiáló osztály érheti el. Az internal tagokat a definiáló osztály és az osztállyal egy .Net assemblyben lévő osztályok érhetik el. A public tagokat pedig bármely osztály elérheti. A member-el definiált tagok alapértelmezés szerint public elérésűek. A let-el definiált tagok mindig private elérésűek.
A struct olyan kompakt szerkezet, ami az osztályoknál hatékonyabb megoldást nyújthat olyan típusok megvalósítására, amik kevés adatot tartalmaznak és egyszerű működéssel bírnak.
A struktúrák érték típusúak ami annyit jelent, hogy közvetlenül a stack-en tárolódnak vagy, ha mezőként vagy tömb elemként használjuk, inline módon a szülőben.
Az osztályokkal és a rekordokkal ellentétben, a struct érték szerinti átadással rendelkezik! A struct nem öröklődhet, nem tartalmazhat let és do kötéseket és nem tartalmazhatja a saját típusát.
(Bár tartalmazhat olyan referenciát, ami a saját típusát hivatkozza meg.) Mivel nem használható a let, így minden mezőt a val kulcsszóval kell deklarálnunk, továbbá megadhatunk saját konstruktorokat.
A nevesített argumentumok egy egyszerű koncepcióra épülnek: bármely tagfüggvény hívásakor lehetőségünk van, hogy a paraméterek megnevezésével adjuk át az értékeket és ne a paraméterek sorrendjének függvényében. Az opcionális paramétereket a paraméter neve elé rakott ? jellel deklarálhatjuk. Ezeknek a típusa mindig egy option<_> típus lesz, például egy int opcionális paraméterként option<int> típusú lesz.
Példa:
Az objektumok létrehozásakor lehetőségünk van a konstruktor paraméterében értéket adni nevesített propertyknek. Az F# fordító a paraméterben átadott nevek közül kiválasztja azokat, amik nem egyeznek meg a konstruktor paramétereinek neveivel és megpróbál a kiválasztott nevű property-knek értéket adni. Például:
Az F# a legtöbb funkcionális nyelvhez hasonlóan lehetőséget a felhasználónak saját operátorok definiálására, akár osztályok között végzett műveletekhez is. A felhasználó által definiált operátorok szimbólumai a !$%&*+-./<=>?@^|~: jelekből állhatnak, azonban nem kezdődhetnek : jellel. Például a 2 dimenziós vektort a következőképpen egészíthetjük ki a skaláris szorzás operátorral:
A típusainkban a .Net-es operátorokat is megvalósíthatjuk, és így a többi .Net nyelv által is használható operátorokat kapunk. Ennek a szintaktikája a következő:
Az F#-ban lehetőség van a metódusok túlterhelésére, habár ezt az opcionális paraméterek és property beállítók mellett ritkán szokás használni.
Az F# fordító 1.9.2.9-es verziója még nem megfelelően kezeli a túlterheléseket, ezért a különböző értékű OverloadID attribútumokkal segíteni kell a fordítónak.
Típuskonverziónak egy adott változó statikus típusának megváltoztatását nevezzük. Az osztályok hierarchiája az obj (System.Object) típusnál kezdődik, ha ebben hierarchiában felfele lépve tesszük, akkor azt upcast-olásnak, ha a hierarchiában lefelé lépve, akkor pedig downcast-olásnak nevezzük a típuskonverziót.
Az upcast-olás biztonságos művelet, mivel fordítási időben eldönthető, hogy az adott változónak szerepel-e az ősei között a típus, amire konvertálni akarunk. Az upcast-olást a :> operátorral végezhetjük el.
Az upcast-olást általában akkor használjuk, ha különböző típusokat akarunk egy tárolóba helyezni, majd ezeket egy közös felülettel kezelni.
Upcast-olás során bármely .Net Framework-ös érték típus automatikusan box-olódik. Az érték típusok a programveremben tárolódnak, a box-olás során azonban ezek átkerülnek a managed heap-re, ahol már referenciaként hivatkozhatunk rájuk.
Downcast-olás során az objektumot egy leszármazott típusára konvertáljuk. Ez a művelet nem biztonságos, mivel fordítási időben általában nem dönthető el, hogy az adott változónak a dinamikus típusa kompatibilis-e a céltípussal. Ha nem kompatibilisek, akkor System.InvalidCastException kivételt fog kiváltani a típuskonverzió. A downcast-olást a :?> operátorral végezhetjük el.
Sokszor hasznos lehet egy változó dinamikus típusát lekérdezése. Ezt F# -ban a :? operátorral tehetjük meg.
Az F# típusrendszere szigorúbb, mint a más objektumorientált nyelvekben megszokott (pl.: C#) típusrendszerek. Ha egy típus annotációval rögzítjük egy kifejezés típusát, akkor csak a rögzített típusú változókat használhatjuk és azok leszármazottjait már nem.
A fenti példaprogram nem fog lefordulni, mivel a PrintPreviewDialog statikus típusa nem Form, hanem annak egy leszármazottja.
Egy megoldás erre a problémára az lenne, hogy a kódban mindenhol a megfelelő upcast operátort használjuk. Azonban az F#-ban egy sokkal elegánsabb megoldás is létezik a problémára, ez a flexibilis típus annotáció. Egy típus annotációt a típusnév elé írt # jellel tehetünk flexibilissé. A flexibilis annotációk a megadott típust és összes leszármazottját is elfogadják, ahogy azt más objektumorientált nyelvek típusainál megszoktuk.
A showform paraméterének módosításával már lefordul a kódunk. A .Net Framework-ből származó kódok automatikusan flexibilis típus annotációval kerülnek importálásra az F# -ban.
Mint minden objektumorientált nyelv, az F# is lehetőséget ad osztályhierarchiák kialakítására. Öröklődés során a már meglévő osztályainkat kiegészíthetjük plusz funkcionalitással. Ezt az inherit kulcsszóval tehetjük meg.
Az öröklődés egy ritkán használt technika az F# nyelvben, mivel az öröklődés komplexebbé teszi az objketumokat, a funkcionális programozás során pedig arra törekszünk, hogy egyszerű objektumok kompozíciójával érjük el a célunkat.
Az F# interface-k a többi .Net Framework interface-hez hasonlóan csak publikus absztrakt metódusokat tartalmazhatnak. Az interface-k segítségével szétválaszthatjuk a definíciót felhasználó osztályokat az implementációk részleteitől. Egy osztálynak csak egy ősosztálya lehet, azonban egyszerre tetszőleges számú interface-t valósíthat meg.
Egy interface implementációját struct-ok és osztályok tartalmazhatják. Egy interface implementálásához először interface név with sorral kezdjük az implementációt. Majd member x.függvényneve = … szintaktikával tagfüggvényekként megadjuk az interface összes implementálandó függvényét, az implementációt végül az end kulcsszóval zárjuk.
Az objetum kifejezések az interfacek és absztrakt objektumok implementálásához adnak egy egyszerűen használható módszert. Egy anonim rekordokhoz hasonló, anonim osztállyal implementáljuk a szükséges függvényeket.
A delegate-ek úgy reprezentálnak egy függvényhívást, mint egy objektumot. Az F#-ban általában a függvény értékeket arra használjuk, hogy elsőrendű értékekként reprezentáljuk a függvényeket. Ezzel szemben a delegáltakat a .NET keretrendszerben használjuk, és akkor van rájuk szükség, amikor interoprációt hajtunk végre API-k között, amik számítanak erre. Akkor is használhatjuk őket, amikor olyan könyvtárakat engedélyezünk, amelyeket más .NET keretrendszer nyelveken íródtak.
A type1 reprezentálja az argumentum típust vagy típusokat és type2 adja meg a visszatérési típust. A type1 által reprezentált argumentum típusok automatikusan curry-zve vannak. Az automatikus curry-zés eltávolít zárójeleket, így olyan állapotba kerül a paraméterlista, ami megfelel a céleljárásnak.
Az Invoke metódus a delegált típusokon szolgál arra, hogy meghívja a közrezárt függvényt. Valamint a delegáltakat átadhatjuk függvény értékekként is, úgy, hogy az Invoke eljárás nevére hivatkozunk a zárójelek nélkül.Példa:
A reflection szó alatt azt a folyamatot értjük, amikor egy program futás közben megfigyelheti, és módosíthatja a saját felépítését és viselkedését. Reflection használatával olyan osztályok adattagjait, és metódusait is elérhetjük és hívhatjuk, amelyekről nem rendelkezünk típusinformációval (pl.: Egy külső modul osztályai). Ezenkívül attribútumokat definiálhatunk az osztályoknak, metódusoknak, adattagoknak, melynek segítségével metaadatokkal láthatjuk el ezeket a kódrészeket.
Ha valamely kódrészt attribútummal szeretnénk ellátni, használhatjuk a beépített attribútumokat, vagy saját magunk is definiálhatunk egyet. Egy attribútumot a [< és >] közé téve helyezhetjük el valamely programrészben.
Ezután az attribútumhoz reflection segítségével férhetünk hozzá.
Attribútumokat metódusokhoz, rekordokhoz, mezőkhöz, vagy kivételekhez is kapcsolhatunk ugyanúgy, mint osztályokhoz
Egy objektumhoz akár több attribútumot is kapcsolhatunk, ekkor pontosvesszővel választjuk el az attribútumokat a [< >] jelek között.
Reflection segítségével megvizsgálhatjuk az egyes típusokat, és futási időben hívhatjuk meg az objektumok metódusait, kérdezhetjük le a tulajdonságait, akár a privát adattagjait is megváltoztathatjuk anélkül, hogy az objektum típusát fordítási időben ismernénk.
Többféle lehetőségünk is van az objektumok típusinformációinak lekérdezésére. A tradicionális megoldás a System.Object osztálytól örökölt GetType() metódus hívása bármely nem null értékű objektumra:
Objektumpéldány nélkül is lekérdezhetjük egy osztály típusinformációját a beépített typeof metódussal.
Mind az Object.GetType(), mind a typeof metódus egy System.Type példánnyal tér vissza, amelynek tulajdonságain keresztül érhetjük el a típusinformációkat az osztályunkról.
Az InvokeMember metódussal meghívhatjuk az objektum egy megadott nevű, azon metódusát, amelyre az adott argumentumlista megfelel.
Reflection segítségével lekérdezhetjük az egyes példányok tulajdonságait, és azok értékét. Az alábbi program kiírja a paraméterként átadott objektum tulajdonságait:
A fenti program kimenete a következő:
A reflectiont számos további esetben használhatjuk, akár egy objektum privát (és immutable) mezőinek értékét is megváltoztathatjuk:
A reflection F# típusokra történő kiegészítése a Microsoft.FSharp.Reflection névtérben található, itt a beépített típusokra, mint: Unió, Tuple, stb. találhatóak reflection metódusok.