Auf Wiedersehen Typ-Aliase? / Sudo Null IT-Nachrichten

Mit der Veröffentlichung von Kotlin 1.5.0 Wertklassen (früher bekannt als in der Reihe Klassen) sind endlich stabil und wurden von der @OptIn-Annotation befreit. Es gab viele neue Sachen in der Version, die auch viel Verwirrung stifteten, da uns jetzt drei sehr ähnliche Werkzeuge zur Verfügung stehen: Typ-Aliase, Datenklassen und Wertklassen. Welche sollten wir jetzt verwenden? Ist es möglich, Typaliase und Datenklassen auf einmal wegzuwerfen und durch Wertklassen zu ersetzen?

Problem

Klassen Kotlin löst zwei Probleme:

  1. Sie vermitteln Bedeutung durch ihren Namen und erleichtern das Verständnis, um was für einen Gegenstand es sich handelt.

  2. Sie erzwingen Typsicherheit, indem sie argumentieren, dass ein Objekt der Klasse A nicht an eine Funktion übergeben werden kann, die ein Objekt der Klasse B als Eingabeparameter erwartet. Dies verhindert schwerwiegende Fehler zur Kompilierzeit.

Primitive Typen wie Int, Boolean, Double erzwingen ebenfalls die Typsicherheit (Sie können ein Double nicht übergeben, wo ein Boolean erwartet wird), aber sie vermitteln keine Bedeutung (naja, außer dass es eine Zahl ist).

Eine doppelte Zahl kann fast alles sein: Temperatur in Grad Celsius, Gewicht in Kilogramm oder die Helligkeit Ihres Bildschirms in Prozent. Klar ist nur, dass wir es mit einer Gleitkommazahl doppelter Genauigkeit (64 Bit) zu tun haben, was uns aber nicht sagt, was diese Zahl ist. Aus diesem Grund, semantisch Typsicherheit verletzt:

Wenn wir eine Funktion haben, um die Helligkeit unseres Displays einzustellen:

fun setDisplayBrightness(newDisplayBrightness: Double) { … }

Wir können diese Funktion mit jedem beliebigen Double-Wert aufrufen, und wir könnten versehentlich eine Zahl mit einer völlig anderen Bedeutung übergeben:

Val-Gewicht: Double = 85,4 setDisplayBrightness(weight) // 💥

Der Compiler wird dies übersehen, aber es ist ein Programmierfehler, der das Programm “verwerfen” oder, noch schlimmer, zu unerwartetem Verhalten führen kann.

Lösung

Es gibt mehrere Ansätze, um die obigen zwei Probleme zu lösen. Es ist möglich, einen primitiven Typ einfach mit einer Klasse zu umhüllen, aber das bringt viel Overhead mit sich. Sehen wir uns also an, wie wir diese Probleme überwinden können mit:

  • Datenklasse;

  • Typ-Alias;

  • und Wertklasse.

und finden Sie heraus, welche dieser Methoden am besten geeignet ist.

Versuch Nr. 1: Datenklassen

Der einfachste Weg (der in Kotlin nativ ist) ist die Verwendung einer Datenklasse:

Datenklasse DisplayBrightness(val value: Double) fun setDisplayBrightness(newDisplayBrightness: DisplayBrightness) { … }✅ Benefits

DisplayBrightness hier – der Typ selbst enthält ein Double, aber Zuordnung nicht kompatibel mit Double (z. B. setDisplayBrightness(DisplayBrightness(0.5)) funktioniert, aber setDisplayBrightness(0.5) gibt einen Kompilierfehler). Auch mit dieser Lösung können Sie dies tun: setDisplayBrightness(DisplayBrightness(person.weight)) . Es ist dass die Entscheidung – solcher zu sich offensichtlich.

⛔️ Nachteile

Es gibt jedoch einen großen Nachteil: Das Instanziieren von Datenklassen ist sehr teuer. Primitive Werte können auf den Stack gepusht werden, was schneller und effizienter ist. Instanzen von Datenklassen werden auf den Heap geschrieben, was mehr Zeit und Speicher verbraucht.
Wie lange noch, fragst du? Testen wir:

Datenklasse DisplayBrightnessDataClass(val value: Double) @OptIn(ExperimentalTime::class) fun main(){ val dataClassTime = measureTime { repeat(1000000000) { DisplayBrightnessDataClass(0.5) } } println(“Datenklassen nahmen ${dataClassTime.toDouble( MILLISEKUNDEN)} ms”) val primitiveTime = measureTime { repeat(1000000000) { var helligkeit = 0,5 } } println(“Primitive Typen haben ${primitiveTime.toDouble(MILLISECONDS)} ms” gebraucht) }

…gibt die Ausgabe:

Datenklassen benötigten 9,898582 ms. Primitive Typen benötigten 2,812561 ms

Und das, obwohl dieser Performance-Hit für die heutige Zeit sehr sehr schnell unbedeutend erscheint elektronische Computerkleine Verbesserungen wie diese sind in leistungsintensiven Anwendungen sehr hilfreich.

Versuch Nr. 2: Geben Sie Aliase ein

Ein Typalias gibt einem Typ einen zweiten Namen. Zum Beispiel:

typealias DisplayBrightness = Double fun setDisplayBrightness(newDisplayBrightness: DisplayBrightness) { … }✅ Vorteile

Unter der Haube sind Double und DisplayBrightness zu Synonymen geworden.
Wenn der Compiler jetzt DisplayBrightness sieht, ersetzt er es einfach durch ein Double und fährt fort. Dementsprechend ist der neue DisplayBrightness-Alias ​​so schnell wie Double – er verwendet die gleichen Optimierungen wie Double.
Wenn wir den obigen Test erweitern, können wir sehen, dass der Test sowohl für das Synonym als auch für den primitiven Typ ungefähr gleich lange dauert:

Datenklassen benötigten 7,743406 ms. Primitive Typen benötigten 2,77597 ms. Typaliase benötigten 2,688276 ms

Da DisplayBrightness gleichbedeutend mit Double ist, funktionieren alle Operationen, die auf Double funktionieren, auch auf DisplayBrightness:

val first: DisplayBrightness = 0,5 val second: DisplayBrightness = 0,1 val sumdBrightness = first + second // 0,6 first.isNaN() // false⛔️

Der Haken dabei ist, dass DisplayBrightness und Double jetzt sind Zuordnung kompatibelwas bedeutet, dass der Compiler dies gerne akzeptiert:

typealias DisplayBrightness = Double typealias Weight = Double fun setDisplayBrightness(newDisplayBrightness: DisplayBrightness) { … } fun callingFunction() { val weight: Weight = 85.4 setDisplayBrightness(weight) }

Haben wir das Problem also tatsächlich gelöst? Nun, teilweise. Während Typaliase Funktionssignaturen ausdrucksstärker und viel schneller als Datenklassen machen, lässt die Tatsache, dass DisplayBrightness und Double zuweisungskompatibel sind, das Problem der Typsicherheit ungelöst.

Versuch Nr. 3: Wertklassen

Wertklassen sind auf den ersten Blick Datenklassen sehr ähnlich. Ihre Signatur sieht genauso aus, aber anstelle von data class wird das Schlüsselwort value class verwendet:

@JvmInline-Wertklasse DisplayBrightness(val value: Double) fun setDisplayBrightness(newDisplayBrightness: DisplayBrightness) { … }

Außerdem fällt Ihnen möglicherweise die Annotation @JvmInline auf. KEEP über Wertklassen erklärt dies sowie den Grund, warum Wertklassen derzeit nur 1 Feld haben können.

Warum @JvmInline benötigt wird

Kurz gesagt, während Kotlin/Native- und Kotlin/JS-Backends Wertklassen mit mehr als einem Feld technisch unterstützen können, kann Kotlin/JVM dies derzeit nicht. Dies liegt daran, dass die JVM nur ihre integrierten primitiven Typen unterstützt. Es gibt jedoch Pläne und ein Valhalla-Projekt (vgl entsprechenden JEP), die benutzerdefinierte primitive Typen zulassen. Der aktuelle Stand der Dinge ist, dass das Kotlin-Team glaubt, dass das Valhalla-Projekt die beste Kompilierungsstrategie für Wertklassen ist. Das Valhalla-Projekt ist jedoch noch nicht stabil und sie mussten eine temporäre Kompilierungsstrategie finden, auf die sie sich verlassen konnten. Um dies explizit zu machen, ist @JvmInline derzeit eine notwendige Maßnahme.

✅ Vorteile

Hinter den Kulissen behandelt der Compiler Wertklassen als Typaliase, aber mit einem großen Unterschied:

Die Wertklassen sind zuweisungsinkompatibel, was bedeutet, dass der folgende Code nicht kompiliert werden kann:

@JvmInline-Wert Klasse DisplayBrightness(val value: Double) fun setDisplayBrightness(newDisplayBrightness: DisplayBrightness) { … } fun callingFunction() { val weight: Double = 85.4 setDisplayBrightness(weight) // 💥 }

Wenn wir den obigen Test erweitern, können wir sehen, dass Wertklassen die gleiche hohe Leistung wie primitive Typen und damit als Typaliase haben:

Datenklassen benötigten 7,268809 ms. Primitive Typen benötigten 2,799518 ms. Typaliase benötigten 2,627111 ms. Wertklassen benötigten 2,883411 ms

Sollte ich immer Wertklassen verwenden?

Es sieht also so aus, als hätten die Wertklassen alle Kästchen angekreuzt, oder? Sie sind…

  • Machen Sie Variablendeklarationen und Funktionssignaturen ausdrucksstärker, ✅

  • Bewahren Sie die Leistung primitiver Typen, ✅

  • Zuweisung inkompatibel mit ihrem zugrunde liegenden Typ, wodurch der Benutzer daran gehindert wird, dumme Dinge zu tun, ✅

  • und unterstützen viele Funktionen von Datenklassen, wie Konstruktoren, Init, Methoden und sogar zusätzliche Eigenschaften (jedoch nur durch Getter). ✅

Die einzige verbleibende Verwendung für den Unterricht Daten ist, wenn Sie mehrere Parameter umschließen müssen. Wertklassen sind im Moment auf einen Parameter in ihrem Konstruktor beschränkt.

Ebenso haben Typaliase immer noch Verwendungen, die nicht durch Wertklassen abgedeckt werden können (oder gegen ihren beabsichtigten Zweck verstoßen):

  1. Kürzen langer generischer Typsignaturen:

typealias Restaurant = Organisation<(Währung, Coupon?) -> Verpflegung

  1. Parameter von Funktionen höherer Ordnung:

typealias ListReducer = (List, List) -> List

Abgesehen von diesen Ausnahmen sind Wertklassen in den meisten Fällen tatsächlich die beste Lösung. (Aus diesem Grund verschieben wir unsere Designs jetzt in Werteklassen.)

Mach weiter

Es gibt zwei Dokumente, die uns wirklich helfen zu verstehen, wie Wertklassen funktionieren und welche technischen Ideen während des Designprozesses entstanden sind:

KEEP spricht auch über mögliche zukünftige Entwicklungen und Designideen. Dieser Artikel auf typealias.com erklärt, wie Typaliase funktionieren und wie sie verwendet werden sollten – empfohlene Lektüre.

Wenn Sie sich allgemein für die Entwicklung der Sprache Kotlin interessieren, könnte Ihnen der Artikel gefallen Kotlins versiegelte Schnittstellen und das Loch in der Versiegelung. Vielen Dank für Ihre Aufmerksamkeit!

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *