Parametric types beziehen sich auf jeden type der einen parameter annehmen kann. Du kannst parametrische Typen in Verse verwenden, um verallgemeinerte Datenstrukturen und Operationen zu definieren. Es gibt zwei Möglichkeiten, parametrische Typen als Argumente zu verwenden: entweder in functions als explizites oder implizites Typ- arguments oder in classes als explizites Typ-Argument.
Events sind ein gängiges Beispiel für parametrische Typen und werden in UEFN ausgiebig in allen Geräten verwendet. Das Schaltflächengerät verfügt beispielsweise über das InteractedWithEvent, das immer dann auftritt, wenn ein Spieler mit der Schaltfläche interagiert. Um einen parametrischen Typ in Aktion zu sehen, schau dir das CountdownEndedEvent aus dem Tutorial für den benutzerdefinierten Countdown-Timer an.
Explizite Typargumente
Erwäge eine box, die zwei Argumente benötigt. Der first_item initialisiert einen ItemOne, und der second_item initialisiert einen ItemTwo, beide vom Typ type. Sowohl first_item als auch second_item sind Beispiele für parametrische Typen, die explizite Argumente für eine Klasse sind.
box(first_item:type, second_item:type) := class:
ItemOne:first_item
ItemTwo:second_itemDa type das Typargument für first_item und second_item ist, kann die Klasse box mit zwei beliebigen Typen erstellt werden. Du könntest eine Box mit zwei Werten string, eine Box mit zwei Werten int, einem string und einem int oder sogar eine Box mit zwei Boxen haben!
Ein weiteres Beispiel ist die Funktion MakeOption(), die einen beliebigen Typ annimmt und eine option dieses Typs zurückgibt.
MakeOption(t:type):?t = false
IntOption := MakeOption(int)
FloatOption := MakeOption(float)
StringOption := MakeOption(string)Du könntest die Funktion MakeOption() so ändern, dass sie stattdessen einen anderen Containertyp zurückgibt, wie z.B. ein array oder eine map.
Implizite Typargumente
Implizite Typargumente für Funktionen werden mit dem Schlüsselwort where eingeführt. Nehmen wir zum Beispiel eine Funktion ReturnItem(), die einfach einen Parameter nimmt und ihn zurückgibt:
ReturnItem(Item:t where t:type):t = ItemHier ist t ein impliziter Typparameter der Funktion ReturnItem(), die ein Argument vom Typ type annimmt und es sofort zurückgibt. Der Typ von t schränkt ein, welchen Typ von Item wir an diese Funktion übergeben können. Da t in diesem Fall vom Typ type ist, können wir ReturnItem() mit einem beliebigen Typ aufrufen. Der Grund für die Verwendung impliziter parametrischer Typen mit Funktionen ist, dass du damit Code schreiben kannst, der unabhängig vom Typ funktioniert, der ihm übergeben wird.
Zum Beispiel, anstatt zu schreiben:
ReturnInt(Item:int):int = Item
ReturnFloat(Item:float):float = ItemStattdessen könnte die einzelne Funktion geschrieben werden.
ReturnItem(Item:t where t:type):t = ItemDamit wird garantiert, dass ReturnItem() nicht wissen muss, welcher bestimmte Typ das t ist – welche Operation es auch immer durchführt, sie funktioniert unabhängig vom Typ des t.
Der tatsächlich für t zu verwendende Typ hängt davon ab, wie ReturnItem() verwendet wird. Wenn beispielsweise ReturnItem() mit dem Argument 0,0 aufgerufen wird, ist t vom Typ float.
ReturnItem("t") # t is a string
ReturnItem(0.0) # t is a floatHier sind "hello" und 0.0 die expliziten Argumente (das Item), die an ReturnItem() übergeben werden. Beides funktioniert, weil der implizite Typ von Item t ist, der ein beliebiger type sein kann.
Ein weiteres Beispiel für einen parametrischen Typ als implizites Argument für eine Funktion ist die folgende Funktion MakeBox(), die mit der Klasse box arbeitet.
box(first_item:type, second_item:type) := class:
ItemOne:first_item
ItemTwo:second_item
MakeBox(ItemOneVal:ValOne, SecondItemVal:ValTwo where ValOne:type, ValTwo:type):box(ValOne, ValTwo) =
box(ValOne, ValTwo){ItemOne := ItemOneVal, ItemTwo := SecondItemVal}
Main():void =
MakeBox("A", "B")
MakeBox(1, "B")
Hier nimmt die Funktion MakeBox() zwei Argumente, FirstItemVal und SecondItemVal, beide vom Typ type, und gibt eine Box vom Typ (type, type) zurück. Die Verwendung von type bedeutet hier, dass wir MakeBox mitteilen, dass die zurückgegebene Box aus zwei beliebigen Objekten bestehen könnte; es könnte ein Array, ein String, eine Funktion usw. sein. Die Funktion MakeBox() übergibt beide Argumente an Box, und verwendet sie, um eine Box zu erstellen, und gibt sie zurück. Beachte, dass sowohl box als auch MakeBox() die gleiche Syntax wie ein call der Funktion verwenden.
Ein integriertes Beispiel hierfür ist die nachstehend beschriebene Funktion für den Containertyp Map.
Map(F(:t) : u, X : []t) : []u =
for (Y : X):
F(Y)Typ Beschränkungen
Du kannst eine Beschränkung für den Typ eines Ausdrucks angeben. Die einzige derzeit unterstützte Einschränkung ist Subtyp, und zwar nur für implizite Typparameter. Zum Beispiel:
int_box := class:
Item:int
MakeSubclassOfIntBox(NewBox:subtype_box where subtype_box:(subtype(int_box))) : tuple(subtype_box, int) = (NewBox, NewBox.Item)In diesem Beispiel wird MakeSubclassOfIntBox() nur kompiliert, wenn eine Klasse übergeben wird, die eine Subklasse von IntBox ist, da SubtypeBox den Typ (subtype(IntBox)) hat. Beachte, dass type als Kurzform für subtype(any) angesehen werden kann. Mit anderen Worten, diese Funktion akzeptiert jeden Subtyp von any, d. h. jeden Typ.
Kovarianz und Kontravarianz
Kovarianz und Kontravarianz beziehen sich auf die Beziehung zwischen zwei Typen, wenn die Typen in zusammengesetzten Typen oder Funktionen verwendet werden. Zwei Typen, die in irgendeiner Weise miteinander verwandt sind, z. B. wenn einer von ihnen eine Subklasse des anderen bildet, sind entweder kovariant oder kontravariant zueinander, je nachdem, wie sie in einem bestimmten Stück Code verwendet werden.
Kovariant: Verwendung eines spezifischeren Typs, wenn der Code etwas Allgemeineres erwartet.
Kontravariant: Verwendung eines allgemeineren Typs, wenn der Code etwas Spezifisches erwartet.
Wenn wir zum Beispiel eine int in einer Situation verwenden könnten, in der jedes comparable akzeptiert werden würde (wie ein float), würde unser int wie ein kovariant handeln, da wir einen spezifischeren Typ verwenden, während ein allgemeinerer erwartet wird. Umgekehrt, wenn wir ein beliebiges comparable verwenden könnten, wo normalerweise ein int verwendet werden würde, würde unser comparable kontravariant handeln, da wir einen allgemeineren Typ verwenden, wenn ein spezifischerer erwartet wird.
Ein Beispiel für Kovarianz und Kontravarianz in einem parametrischen Typ könnte wie folgt aussehen:
MyFunction(Input:t where t:type):logic = trueHier wird t kontravariant als Eingabe für die Funktion und logic kovariant als Ausgabe für die Funktion verwendet.
Es ist wichtig zu beachten, dass die beiden Typen nicht von Natur aus kovariant oder kontravariant zueinander sind. Ob sie kovariant oder kontravariant wirken, hängt vielmehr davon ab, wie sie im Code verwendet werden.
Kovariant
Kovarianz bedeutet, etwas Spezifischeres zu verwenden, wenn etwas Generisches erwartet wird. Normalerweise ist dies für den Output einer Funktion. Alle Typverwendungen, die keine Eingaben für Funktionen sind, sind kovariante Verwendungen.
Bei dem nachstehenden Beispiel eines allgemeinen parametrischen Typs wirkt payload kovariant.
DoSomething():int =
payload:int = 0Nehmen wir zum Beispiel an, wir haben eine Klasse animal und eine Klasse cat, die die Subklasse von animal ist. Wir haben auch eine Klasse pet_sanctuary, die mit der Funktion AdoptPet() Haustiere adoptiert. Da wir nicht wissen, welche Art von Haustier wir bekommen werden, gibt AdoptPet() ein allgemeines animal zurück.
animal := class:
cat := class(animal):
pet_sanctuary := class:
AdoptPet():animal = animal{}Nehmen wir an, wir haben ein anderes Tierheim, das sich nur mit Katzen beschäftigt. Diese Klasse, cat_sanctuary, ist eine Subklasse von pet_sanctuary. Da dies eine Katzenzuflucht ist, override wir AdoptPet(), um nur eine cat anstelle eines animal zurückzugeben.
cat_sanctuary := class(pet_sanctuary):
AdoptPet<override>():cat = cat{}In diesem Fall ist der Rückgabetyp cat von AdoptPet() kovariant zu animal. Wir verwenden einen spezifischeren Typ, während das Original einen allgemeineren verwendet hat.
Dies kann auch für zusammengesetzte Typen gelten. Wenn wir ein Array von cat haben, können wir ein Array von animal mit dem Cat-Array initialisieren. Das Gegenteil funktioniert nicht, da animal nicht in seine Subklasse cat umgewandelt werden kann. Das Array von cat ist kovariant zu dem Array von animal, da wir einen engeren Typ als einen allgemeineren Typ behandeln.
CatArray:[]cat = array{}
AnimalArray:[]animal = CatArrayInputs in Funktionen können nicht kovariant verwendet werden. Der folgende Code schlägt fehl, weil die Zuweisung von AnimalExample() an CatExample() vom Typ cat ist, der zu spezifisch ist, um der Rückgabetyp von AnimalExample() zu sein. Die Umkehrung dieser Reihenfolge durch Zuweisung von CatExample() zu AnimalExample würde aufgrund der Subtypisierung von cat zu animal funktionieren.
CatExample:type{CatFunction(MyCat:cat):void} = …
AnimalExample:type{AnimalFunction(MyAnimal:animal):void} = CatExampleEs folgt ein weiteres Beispiel, bei dem die Variable t nur kovariant verwendet wird.
# The line below will fail because t is used only covariantly.
MyFunction(:logic where t:type):?t = falseKontravariant
Kontravarianz ist das Gegenteil von Kovarianz und bedeutet, dass man etwas Allgemeineres verwendet, wenn etwas Spezifisches erwartet wird. Dies ist in der Regel der Input für eine Funktion. Bei dem nachstehenden Beispiel eines allgemeinen parametrischen Typs wirkt payload kontravariant.
DoSomething(Payload:payload where payload:type):voidAngenommen, unser Tierheim hat ein spezielles Verfahren für den Umgang mit neuen Katzen. Wir fügen eine neue Methode zu pet_sanctuary mit dem Namen RegisterCat() hinzu.
pet_sanctuary := class:
AdoptPet():animal = animal{}
RegisterCat(NewAnimal:cat):void = {}Für unser cat_sanctuary werden wir diese Methode überschreiben, um ein animal als Typparameter zu akzeptieren, weil wir bereits wissen, dass jede cat ein animal ist.
cat_sanctuary := class(pet_sanctuary):
AdoptPet<override>():cat = cat{}
RegisterCat<override>(NewAnimal:animal):void = {}Hier ist animal kontravariant zu cat, da wir etwas allgemeineres verwenden, wenn etwas spezifischeres funktionieren würde.
Die Verwendung eines impliziten Typs, der durch eine Klausel where eingeführt wurde, führt zu einem Fehler. Zum Beispiel wird payload hier kontravariant verwendet, schlägt aber fehl, weil es nicht als Argument definiert ist.
DoSomething(:logic where payload:type) : ?payload = falseUm dies zu beheben, könnte dies umgeschrieben werden, um einen Typparameter auszuschließen:
DoSomething(:logic) : ?false = falseDie ausschließliche Verwendung von Kontravariant führt nicht zu einem Fehler, kann aber unter Verwendung von any anstelle von false umgeschrieben werden. Zum Beispiel:
ReturnFirst(First:first_item, :second_item where first_item:type, second_item:type) : first_item = FirstDa second_item vom Typ type war und nicht zurückgegeben wurde, können wir es im zweiten Beispiel durch any ersetzen und eine Typüberprüfung vermeiden.
ReturnFirst(First:first_item, :any where first_item:type) : first_item = FirstIndem der Typ first_item entweder durch any oder false ersetzt wird, geht die Genauigkeit verloren. Zum Beispiel kann der folgende Code nicht kompiliert werden:
ReturnFirst(First:any, :any) :any = First
Main() : void =
FirstInt:int = ReturnFirst(1, "ignored")Bekannte Einschränkungen
Explizite Typparameter für Datentypen dürfen nur mit Klassen verwendet werden, nicht mit Interfaces oder structs. Inheritance in Bezug auf parametrische Typen ist ebenfalls unzulässig. | Verse |
Parametrische Typen können auf sich selbst recursively referenzieren, solange die Rekursion direkt ist. Parametrische Typen können nicht rekursiv auf andere parametrische Typen verweisen. | Verse |
Derzeit unterstützen Klassen nur unveränderliche parametrische Daten. Zum Beispiel würde dieser Code nicht kompiliert werden, weil | Verse |
Explizite Typparameter können frei mit einer Klasse kombiniert werden, genauso wie implizite Typparameter mit einer Funktion kombiniert werden können. | Verse |