Advertisement
  1. Code
  2. Kotlin

Kotlin von Grund auf neu: Erweiterte Eigenschaften und Klassen

by
Read Time:13 minsLanguages:
This post is part of a series called Kotlin From Scratch.
Kotlin From Scratch: Classes and Objects
Kotlin From Scratch: Abstract Classes, Interfaces, Inheritance, and Type Alias

German (Deutsch) translation by Władysław Łucyszyn (you can also view the original English article)

Kotlin ist eine moderne Programmiersprache, die zu Java-Bytecode kompiliert wird. Es ist kostenlos und Open Source und verspricht, das Programmieren für Android noch mehr Spaß zu machen.

Im vorherigen Artikel haben Sie etwas über Klassen und Objekte in Kotlin erfahren. In diesem Tutorial werden wir weiterhin mehr über Eigenschaften erfahren und uns auch mit fortgeschrittenen Klassentypen in Kotlin befassen, indem wir Folgendes untersuchen:

  • spät initialisierte Eigenschaften
  • Inline-Eigenschaften
  • Erweiterungseigenschaften
  • data-, enum-, nested- und sealed-Klassen

1. Spätinitialisierte Eigenschaften

Wir können eine Eigenschaft, die nicht null ist, in Kotlin als spät initialisiert deklarieren. Dies bedeutet, dass eine Nicht-Null-Eigenschaft zum Zeitpunkt der Deklaration nicht mit einem Wert initialisiert wird – die eigentliche Initialisierung erfolgt nicht über einen Konstruktor – sondern stattdessen durch eine Methode oder eine Abhängigkeitsinjektion spät initialisiert wird.

Sehen wir uns ein Beispiel an, um diesen einzigartigen Eigenschaftsmodifikator zu verstehen.

Im obigen Code haben wir innerhalb der Klasse Presenter eine veränderbare, nullable repository-Eigenschaft vom Typ Repository deklariert und diese Eigenschaft dann während der Deklaration auf null initialisiert. Wir haben eine Methode initRepository() in der Presenter-Klasse, die diese Eigenschaft später mit einer tatsächlichen Repository-Instanz reinitialisiert. Beachten Sie, dass dieser Eigenschaft auch mit einem Abhängigkeitsinjektor wie Dagger ein Wert zugewiesen werden kann.

Damit wir nun Methoden oder Eigenschaften für diese repository-Eigenschaft aufrufen können, müssen wir eine Nullprüfung durchführen oder den Safe-Call-Operator verwenden. Warum? Weil die repository-Eigenschaft vom Typ NULL ist (Repository?). (Wenn Sie eine Auffrischung zum Thema NULL-Zulässigkeit in Kotlin benötigen, besuchen Sie bitte NULL-Zulässigkeit, Schleifen und Bedingungen).

Um zu vermeiden, dass jedes Mal, wenn wir die Methode einer Eigenschaft aufrufen müssen, Nullprüfungen durchgeführt werden müssen, können wir diese Eigenschaft mit dem lateinit-Modifikator markieren. Dies bedeutet, dass wir diese Eigenschaft (die eine Instanz einer anderen Klasse ist) als spät initialisiert deklariert haben (bedeutet, dass die Eigenschaft später initialisiert wird).

Solange wir warten, bis der Eigenschaft ein Wert zugewiesen wurde, können wir ohne Nullprüfungen auf die Methoden der Eigenschaft zugreifen. Die Eigenschaftsinitialisierung kann entweder in einer Setter-Methode oder durch Abhängigkeitsinjektion erfolgen.

Beachten Sie, dass wir, wenn wir versuchen, auf Methoden der Eigenschaft zuzugreifen, bevor sie initialisiert wurde, eine kotlin.UninitializedPropertyAccessException anstelle einer NullPointerException erhalten. In diesem Fall lautet die Ausnahmemeldung "lateinit-Eigenschafts-Repository wurde nicht initialisiert".

Beachten Sie auch die folgenden Einschränkungen beim Verzögern einer Eigenschaftsinitialisierung mit lateinit:

  • Es muss veränderlich sein (mit var deklariert).
  • Der Eigenschaftstyp darf kein primitiver Typ sein, z. B. Int, Double, Float usw.
  • Die Eigenschaft kann keinen benutzerdefinierten Getter oder Setter haben.

2. Inline-Eigenschaften

In Erweiterte Funktionen habe ich den inline-Modifier für Funktionen höherer Ordnung eingeführt – dies hilft, alle Funktionen höherer Ordnung zu optimieren, die ein Lambda als Parameter akzeptieren.

In Kotlin können wir diesen inline-Modifikator auch für Eigenschaften verwenden. Die Verwendung dieses Modifikators optimiert den Zugriff auf die Eigenschaft.

Sehen wir uns ein praktisches Beispiel an.

Im obigen Code haben wir eine normale Eigenschaft, nickName, die nicht über den inline-Modifizierer verfügt. Wenn wir das Code-Snippet mit der Funktion Kotlin-Bytecode anzeigen dekompilieren (wenn Sie sich in IntelliJ IDEA oder Android Studio befinden, verwenden Sie Tools  > Kotlin > Kotlin-Bytecode anzeigen), sehen wir den folgenden Java-Code:

Im oben generierten Java-Code (einige Elemente des generierten Codes wurden der Kürze halber entfernt) können Sie sehen, dass der Compiler innerhalb der main()-Methode ein Student-Objekt erstellt hat, das die getNickName()-Methode aufgerufen und dann seinen Rückgabewert ausgegeben hat .

Lassen Sie uns nun stattdessen die Eigenschaft als inline angeben und den generierten Bytecode vergleichen.

Wir fügen einfach den inline-Modifizierer vor dem Variablen-Modifizierer ein: var oder val. Hier ist der für diese Inline-Eigenschaft generierte Bytecode:

Wieder wurde etwas Code entfernt, aber das Wichtigste ist die main()-Methode. Der Compiler hat den Funktionsrumpf der Eigenschaft get() kopiert und in die Aufrufsite eingefügt (dieser Mechanismus ähnelt Inline-Funktionen).

Unser Code wurde optimiert, da kein Objekt erstellt und die Eigenschafts-Getter-Methode aufgerufen werden muss. Aber wie im Beitrag zu den Inline-Funktionen besprochen, hätten wir einen größeren Bytecode als zuvor – also mit Vorsicht verwenden.

Beachten Sie auch, dass dieser Mechanismus für Eigenschaften funktioniert, die kein Unterstützungsfeld haben (denken Sie daran, ein Unterstützungsfeld ist nur ein Feld, das von Eigenschaften verwendet wird, wenn Sie diese Felddaten ändern oder verwenden möchten).

3. Erweiterungseigenschaften

In Erweiterte Funktionen habe ich auch Erweiterungsfunktionen besprochen – diese geben uns die Möglichkeit, eine Klasse um neue Funktionen zu erweitern, ohne von dieser Klasse erben zu müssen. Kotlin bietet auch einen ähnlichen Mechanismus für Eigenschaften, genannt Erweiterungseigenschaften.

Im Post Erweiterte Funktionen haben wir eine uppercaseFirstLetter()-Erweiterungsfunktion mit dem Empfängertyp String definiert. Hier haben wir sie stattdessen in eine Erweiterungseigenschaft der obersten Ebene umgewandelt. Beachten Sie, dass Sie eine Getter-Methode für Ihre Eigenschaft definieren müssen, damit dies funktioniert.

Mit diesem neuen Wissen über Erweiterungseigenschaften wissen Sie also, dass Sie, wenn Sie sich jemals gewünscht haben, dass eine Klasse eine Eigenschaft haben soll, die nicht verfügbar war, eine Erweiterungseigenschaft dieser Klasse erstellen können.

4. Data-Klassen

Beginnen wir mit einer typischen Java-Klasse oder POJO (Plain Old Java Object).

Wie Sie sehen, müssen wir die Klasseneigenschafts-Accessoren explizit codieren: Getter und Setter sowie hashcode-, equals- und toString-Methoden (obwohl IntelliJ IDEA, Android Studio oder die AutoValue-Bibliothek uns bei der Generierung helfen können). Wir sehen diese Art von Boilerplate-Code meistens in der Datenschicht eines typischen Java-Projekts. (Ich habe die Feld-Accessoren und den Konstruktor der Kürze halber entfernt).

Das Coole ist, dass das Kotlin-Team uns den data-Modifikator für Klassen zur Verfügung gestellt hat, um das Schreiben dieser Boilerplates zu vermeiden.

Lassen Sie uns jetzt stattdessen den vorherigen Code in Kotlin schreiben.

Genial! Wir geben einfach den data-Modifikator vor dem Schlüsselwort class an, um eine Datenklasse zu erstellen – genau wie in unserer BlogPost-Kotlin-Klasse oben. Jetzt werden die Methoden equals, hashcode, toString, copy und multiple component für uns unter der Haube erstellt. Beachten Sie, dass eine Datenklasse andere Klassen erweitern kann (dies ist eine neue Funktion von Kotlin 1.1).

Die equals-Methode

Diese Methode vergleicht zwei Objekte auf Gleichheit und gibt true zurück, wenn sie gleich sind, oder ansonsten false. Mit anderen Worten, es vergleicht, ob die beiden Klasseninstanzen dieselben Daten enthalten.

In Kotlin wird mit dem Gleichheitsoperator == die equals-Methode im Hintergrund aufgerufen.

Die hashCode-Methode

Diese Methode gibt einen ganzzahligen Wert zurück, der zum schnellen Speichern und Abrufen von Daten verwendet wird, die in einer Hash-basierten Sammlungsdatenstruktur gespeichert sind, beispielsweise in den Sammlungstypen HashMap und HashSet.

Die toString-Methode

Diese Methode gibt eine String-Darstellung eines Objekts zurück.

Durch einfaches Aufrufen der Klasseninstanz erhalten wir ein String-Objekt, das an uns zurückgegeben wird. Kotlin ruft das Objekt für uns unter der Haube toString() auf. Aber wenn wir das data-Schlüsselwort nicht einfügen, sehen Sie sich an, wie unsere Objektstring-Darstellung aussehen würde:

Viel weniger informativ!

Die copy-Methode

Mit dieser Methode können wir eine neue Instanz eines Objekts mit denselben Eigenschaftswerten erstellen. Mit anderen Worten, es erstellt eine Kopie des Objekts.

Eine coole Sache an der copy-Methode in Kotlin ist die Möglichkeit, Eigenschaften während des Kopierens zu ändern.

Wenn Sie ein Java-Programmierer sind, ähnelt diese Methode der Methode clone(), mit der Sie bereits vertraut sind. Die Kotlin copy-Methode bietet jedoch leistungsstärkere Funktionen.

Zerstörerische Erklärung

In der Person-Klasse haben wir auch zwei Methoden, die der Compiler aufgrund des in der Klasse platzierten Schlüsselworts data automatisch für uns generiert. Diesen beiden Methoden wird "Komponente" vorangestellt, gefolgt von einem Nummernsuffix: component1(), component2(). Jede dieser Methoden repräsentiert die einzelnen Eigenschaften des Typs. Beachten Sie, dass das Suffix der Reihenfolge der im primären Konstruktor deklarierten Eigenschaften entspricht.

In unserem Beispiel liefert der Aufruf von component1() den Vornamen und der Aufruf von component2() den Nachnamen.

Das Aufrufen der Eigenschaften mit diesem Stil ist jedoch schwer zu verstehen und zu lesen, daher ist es viel besser, den Eigenschaftsnamen explizit aufzurufen. Diese implizit erzeugten Eigenschaften haben jedoch einen sehr nützlichen Zweck: Sie ermöglichen uns eine destrukturierende Deklaration, in der wir jede Komponente einer lokalen Variablen zuweisen können.

Wir haben hier die ersten und zweiten Eigenschaften (firstName und lastName) des Typs Person den Variablen firstName bzw. lastName direkt zugewiesen. Ich habe auch diesen Mechanismus, der als Destrukturierungsdeklaration bekannt ist, im letzten Abschnitt des Beitrags Pakete und grundlegende Funktionen erörtert.

5. Nested-Klassen

Im Beitrag Mehr Spaß mit Funktionen habe ich Ihnen gesagt, dass Kotlin lokale oder verschachtelte Funktionen unterstützt – eine Funktion, die in einer anderen Funktion deklariert wird. Nun, Kotlin unterstützt in ähnlicher Weise auch nested-Klassen – eine Klasse, die innerhalb einer anderen Klasse erstellt wurde.

Wir rufen sogar die öffentlichen Funktionen der verschachtelten Klasse auf, wie unten gezeigt – eine verschachtelte Klasse in Kotlin entspricht einer static verschachtelten Klasse in Java. Beachten Sie, dass verschachtelte Klassen keinen Verweis auf ihre äußere Klasse speichern können.

Es steht uns auch frei, die geschachtelte Klasse als privat festzulegen – das bedeutet, dass wir nur eine Instanz der NestedClass innerhalb des Geltungsbereichs der OuterClass erstellen können.

Inner-Klasse

Inner-Klassen hingegen können auf die äußere Klasse verweisen, in der sie deklariert wurden. Um eine innere Klasse zu erstellen, platzieren wir das Schlüsselwort inner vor dem Schlüsselwort class in einer verschachtelten Klasse.

Hier verweisen wir auf die OuterClass aus der InnerClass, indem wir this@OuterClass verwenden.

6. Enum-Klassen

Ein Aufzählungstyp deklariert einen Satz von Konstanten, die durch Bezeichner dargestellt werden. Diese spezielle Art von Klasse wird durch das Schlüsselwort enum erstellt, das vor dem Schlüsselwort class angegeben wird.

Um einen Enum-Wert basierend auf seinem Namen abzurufen (genau wie in Java), tun wir Folgendes:

Oder wir können die Hilfsmethode Kotlin enumValueOf<T>() verwenden, um generisch auf Konstanten zuzugreifen:

Außerdem können wir alle Werte (wie bei einer Java-Enumeration) wie folgt abrufen:

Schließlich können wir die Kotlin enumValues<T>()-Hilfsmethode verwenden, um alle Enum-Einträge auf generische Weise abzurufen:

Dies gibt ein Array zurück, das die Enum-Einträge enthält.

Enum-Konstruktoren

Genau wie eine normale Klasse kann der enum typ einen eigenen Konstruktor mit Eigenschaften haben, die jeder Aufzählungskonstante zugeordnet sind.

Im primären Konstruktor des Aufzählungstyps Country haben wir die unveränderliche Eigenschaft callingCodes für jede Aufzählungskonstante definiert. In jeder der Konstanten haben wir dem Konstruktor ein Argument übergeben.

Wir können dann wie folgt auf die Eigenschaft constants zugreifen:

7. Sealed-Klassen 

Eine sealed-Klasse in Kotlin ist eine abstrakte Klasse (Sie haben nie die Absicht, daraus Objekte zu erstellen), die andere Klassen erweitern können. Diese Unterklassen werden innerhalb des sealed-Klassenkörpers definiert – in derselben Datei. Da alle diese Unterklassen innerhalb des versiegelten Klassenkörpers definiert sind, können wir alle möglichen Unterklassen kennen, indem wir einfach die Datei anzeigen.

Sehen wir uns ein praktisches Beispiel an.

Um eine Klasse als versiegelt zu deklarieren, fügen wir den sealed Modifizierer vor dem class-Modifizierer in den Klassendeklarationsheader ein – in unserem Fall haben wir die Shape-Klasse als sealed deklariert. Eine versiegelte Klasse ist ohne ihre Unterklassen unvollständig – genau wie eine typische abstrakte Klasse –, also müssen wir die einzelnen Unterklassen in derselben Datei (in diesem Fall shape.kt) deklarieren. Beachten Sie, dass Sie keine Unterklasse einer versiegelten Klasse aus einer anderen Datei definieren können.

In unserem obigen Code haben wir festgelegt, dass die Shape-Klasse nur um die Klassen Circle, Triangle und Rectangle erweitert werden kann.

Für versiegelte Klassen in Kotlin gelten die folgenden zusätzlichen Regeln:

  • Wir können den Modifier abstract zu einer versiegelten Klasse hinzufügen, dies ist jedoch überflüssig, da versiegelte Klassen standardmäßig abstrakt sind.
  • Versiegelte Klassen können den open oder final Modifikator nicht haben.
  • Es steht uns auch frei, Datenklassen und Objekte als Unterklassen zu einer versiegelten Klasse zu deklarieren (sie müssen immer noch in derselben Datei deklariert werden).
  • Versiegelte Klassen dürfen keine öffentlichen Konstruktoren haben - ihre Konstruktoren sind standardmäßig privat.

Klassen, die Unterklassen einer versiegelten Klasse erweitern, können entweder in derselben Datei oder in einer anderen Datei abgelegt werden. Die Unterklasse der versiegelten Klasse muss mit dem open-Modifikator markiert werden (mehr über die Vererbung in Kotlin erfahren Sie im nächsten Beitrag).

Eine versiegelte Klasse und ihre Unterklassen sind in einem when-Ausdruck sehr praktisch. Beispielsweise:

Hier ist der Compiler schlau, um sicherzustellen, dass wir alle möglichen when abdecken. Das bedeutet, dass die else-Klausel nicht hinzugefügt werden muss.

Wenn wir stattdessen Folgendes tun würden:

Der Code würde nicht kompiliert, da wir nicht alle möglichen Fälle berücksichtigt haben. Wir hätten folgenden Fehler:

Wir könnten also entweder den Fall is Rectangle einschließen oder die else-Klausel einfügen, um den when-Ausdruck zu vervollständigen.

Abschluss

In diesem Tutorial haben Sie mehr über Klassen in Kotlin erfahren. Wir haben Folgendes über Klasseneigenschaften behandelt:

  • späte Initialisierung
  • Inline-Eigenschaften
  • Erweiterungseigenschaften

Außerdem haben Sie einige coole und erweiterte Klassen wie data, enum, verschachtelte und versiegelte Klassen kennengelernt. Im nächsten Tutorial der Kotlin von Grund auf neu-Reihe werden Sie mit Schnittstellen und Vererbung in Kotlin vertraut gemacht. Bis bald!

Um mehr über die Kotlin-Sprache zu erfahren, empfehle ich den Besuch der Kotlin-Dokumentation. Oder sieh dir einige unserer anderen Beiträge zur Entwicklung von Android-Apps hier auf Envato Tuts an!

Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.