Abschnitt I: Grundlagen der Programmierung

There was something amazingly enticing about programmingVint Cerf

Dieser Abschnitt ist für Leser gedacht, die gerade das Programmieren lernen. Wenn Sie ein erfahrener Programmierer sind, überspringen Sie diesen Abschnitt und fahren Sie mit Zusammenfassung 1 und Zusammenfassung 2 fort.

Einführung

Dieses Buch ist für engagierte Anfänger und erfahrene Programmierer.

Sie sind ein Anfänger, wenn Sie keine Vorkenntnisse im Programmieren haben, aber “engagiert”, weil wir Ihnen gerade genug geben, um es selbst herauszufinden. Wenn Sie fertig sind, haben Sie eine solide Grundlage in der Programmierung und in Kotlin.

Wenn Sie ein erfahrener Programmierer sind, überspringen Sie zu Zusammenfassung 1 und Zusammenfassung 2 und fahren Sie von dort aus fort.

Der “Atomare” Teil des Buchtitels bezieht sich auf Atome als die kleinsten unteilbaren Einheiten. In diesem Buch versuchen wir, nur ein Konzept pro Kapitel einzuführen, sodass die Kapitel nicht weiter unterteilt werden können — daher nennen wir sie Atome.

Konzepte

Alle Programmiersprachen bestehen aus Funktionen. Sie wenden diese Funktionen an, um Ergebnisse zu erzielen. Kotlin ist mächtig — es hat nicht nur eine reiche Menge an Funktionen, sondern man kann diese Funktionen normalerweise auf verschiedene Arten ausdrücken.

Wenn alles zu schnell auf Sie einprasselt, könnten Sie denken, Kotlin sei “zu kompliziert”.

Dieses Buch versucht, Überforderung zu verhindern. Wir bringen Ihnen die Sprache sorgfältig und gezielt bei, unter Anwendung der folgenden Prinzipien:

  1. Babyschritte und kleine Erfolge. Wir werfen die Tyrannei des Kapitels ab. Stattdessen präsentieren wir jeden kleinen Schritt als ein omares Konzept oder einfach Atom, das wie ein winziges Kapitel aussieht. Wir versuchen, pro Atom nur ein neues Konzept vorzustellen. Ein typisches Atom enthält ein oder mehrere kleine, ausführbare Codebeispiele und die erzeugte Ausgabe.
  2. Keine Vorwärtsverweise. Soweit möglich, vermeiden wir es zu sagen: “Diese Funktionen werden in einem späteren Atom erklärt.”
  3. Keine Verweise auf andere Programmiersprachen. Wir tun dies nur, wenn es notwendig ist. Ein Vergleich mit einer Funktion in einer Sprache, die Sie nicht verstehen, ist nicht hilfreich.
  4. Zeigen, nicht erzählen. Anstatt eine Funktion verbal zu beschreiben, bevorzugen wir Beispiele und Ausgaben. Es ist besser, eine Funktion im Code zu sehen.
  5. Praxis vor Theorie. Wir versuchen, zuerst die Mechanik der Sprache zu zeigen und dann zu erklären, warum diese Funktionen existieren. Das ist umgekehrt zur “traditionellen” Lehre, scheint aber oft besser zu funktionieren.

Wenn Sie die Funktionen kennen, können Sie die Bedeutung herausfinden. Es ist in der Regel einfacher, eine einzelne Seite Kotlin zu verstehen als den entsprechenden Code in einer anderen Sprache.

Wo ist der Index?

Dieses Buch ist in Markdown geschrieben und mit Leanpub produziert. Leider unterstützen weder Markdown noch Leanpub Indizes. Indem wir jedoch die kleinstmöglichen Kapitel (Atome) schaffen, die aus einem einzigen Thema pro Atom bestehen, fungiert das Inhaltsverzeichnis als eine Art Index. Darüber hinaus ermöglichen die eBook-Versionen elektronisches Suchen im gesamten Buch.

Querverweise

Ein Verweis auf ein Atom im Buch sieht so aus: Einführung, was in diesem Fall auf das aktuelle Atom verweist. In den verschiedenen eBook-Formaten erzeugt dies einen Hyperlink zu diesem Atom.

Formatierung

In diesem Buch:

  • Kursiv führt einen neuen Begriff oder ein Konzept ein und betont manchmal eine Idee.
  • Schrift mit fester Breite zeigt Programmschlüsselwörter, Bezeichner und Dateinamen an. Die Codebeispiele sind ebenfalls in dieser Schriftart und in den eBook-Versionen des Buches farblich hervorgehoben.
  • Im Fließtext folgt auf einen Funktionsnamen leere Klammern, wie in func(). Dies erinnert den Leser daran, dass er eine Funktion betrachtet.
  • Um das eBook auf allen Geräten leicht lesbar zu machen und dem Benutzer zu ermöglichen, die Schriftgröße zu erhöhen, begrenzen wir die Breite unserer Code-Listings auf 47 Zeichen. Dies erfordert manchmal Kompromisse, aber wir glauben, dass die Ergebnisse es wert sind. Um diese Breiten zu erreichen, entfernen wir möglicherweise Leerzeichen, die in vielen Formatierungsstilen ansonsten enthalten wären — insbesondere verwenden wir Einrückungen von zwei Leerzeichen anstelle der standardmäßigen vier Leerzeichen.

“Pause”

Gelegentlich sehen Sie:

  • -

Dies zeigt eine Pause oder eine Art kleinen Reset an. In diesem Buch erscheint es oft vor einer kurzen Zusammenfassung des aktuellen Unterabschnitts, wo ein “Zusammenfassung”-Untertitel übertrieben wäre. Einige Bücher verwenden einen Mechanismus wie diesen, um anzuzeigen, dass eine Idee abgeschlossen ist und wir etwas Neues beginnen, das jedoch noch im gleichen Thema liegt und nicht groß genug ist, um einen Unterabschnitt oder einen neuen Abschnitt zu rechtfertigen. Das Markdown in Leanpub ist ziemlich begrenzt, und die Verwendung von einem oder mehreren Punkten (mein ursprünglicher Versuch) ist nicht möglich. Zwei Striche im Markdown zu setzen, erzeugt einen Punkt und einen Strich. Es könnte eine bessere Möglichkeit geben, dies zu tun, aber ich habe sie nicht gefunden, also habe ich mich darauf festgelegt.

Probieren Sie das Buch aus

Wir bieten eine kostenlose Probe des elektronischen Buches auf AtomicKotlin.com an. Die Probe enthält die ersten beiden Abschnitte in voller Länge sowie mehrere nachfolgende Atome. So können Sie das Buch ausprobieren und entscheiden, ob es für Sie geeignet ist.

Das vollständige Buch ist sowohl als Druckversion als auch als eBook erhältlich. Wenn Ihnen gefällt, was wir in der kostenlosen Probe gemacht haben, unterstützen Sie uns bitte und helfen Sie uns, unsere Arbeit fortzusetzen, indem Sie für das bezahlen, was Sie nutzen. Wir hoffen, das Buch hilft Ihnen, und wir schätzen Ihre Unterstützung.

Im Zeitalter des Internets scheint es unmöglich, irgendein Stück Information zu kontrollieren. Sie werden wahrscheinlich die elektronische Version dieses Buches an zahlreichen Orten finden. Wenn Sie im Moment nicht für das Buch zahlen können und es von einer dieser Seiten herunterladen, „geben Sie es bitte weiter“. Helfen Sie beispielsweise jemand anderem, die Sprache zu lernen, sobald Sie sie beherrschen. Oder helfen Sie jemandem auf irgendeine Weise, wie er es braucht. Vielleicht geht es Ihnen in Zukunft besser, und dann können Sie für das Buch bezahlen.

Übungen und Lösungen

Die meisten Atome in Atomic Kotlin werden von einer Handvoll kleiner Übungen begleitet. Um Ihr Verständnis zu verbessern, empfehlen wir, die Übungen unmittelbar nach dem Lesen des Atoms zu lösen. Die meisten Übungen werden automatisch von der JetBrains IntelliJ IDEA integrierten Entwicklungsumgebung (IDE) überprüft, sodass Sie Ihren Fortschritt sehen und Hinweise erhalten können, wenn Sie feststecken.

Sie finden die folgenden Links unter http://AtomicKotlin.com/exercises/.

Um die Übungen zu lösen, installieren Sie IntelliJ IDEA mit dem EduTools-Plugin, indem Sie diesen Tutorials folgen:

  1. Installieren Sie IntelliJ IDEA und das EduTools-Plugin.
  2. Öffnen Sie den Atomic Kotlin-Kurs und lösen Sie die Übungen.

Im Kurs finden Sie Lösungen für alle Übungen. Wenn Sie bei einer Übung feststecken, schauen Sie nach Hinweisen oder werfen Sie einen Blick auf die Lösung. Wir empfehlen dennoch, sie selbst zu implementieren.

Wenn Sie Probleme bei der Einrichtung und Ausführung des Kurses haben, lesen Sie bitte den Leitfaden zur Fehlerbehebung. Wenn das Ihr Problem nicht löst, wenden Sie sich bitte an das Support-Team, wie im Leitfaden angegeben.

Wenn Sie einen Fehler im Kursinhalt finden (zum Beispiel ein Test für eine Aufgabe liefert das falsche Ergebnis), nutzen Sie bitte unser Issue-Tracker, um das Problem mit diesem vorausgefüllten Formular zu melden. Beachten Sie, dass Sie sich bei YouTrack anmelden müssen. Wir schätzen Ihre Zeit, um den Kurs zu verbessern!

Seminare

Informationen zu Live-Seminaren und anderen Lernwerkzeugen finden Sie auf AtomicKotlin.com.

Konferenzen

Bruce organisiert Open-Spaces-Konferenzen wie das Winter Tech Forum. Treten Sie der Mailingliste auf AtomicKotlin.com bei, um über unsere Aktivitäten und Vorträge informiert zu bleiben.

Unterstützen Sie uns

Dies war ein großes Projekt. Es hat Zeit und Mühe gekostet, dieses Buch und die begleitenden Unterstützungsmaterialien zu erstellen. Wenn Ihnen dieses Buch gefällt und Sie mehr davon sehen möchten, unterstützen Sie uns bitte:

  • Bloggen, tweeten Sie, usw. und erzählen Sie Ihren Freunden davon. Dies ist eine Graswurzel-Marketing-Bemühung, daher hilft alles, was Sie tun.
  • Kaufen Sie eine eBook- oder Druckversion dieses Buches auf AtomicKotlin.com.
  • Besuchen Sie AtomicKotlin.com für andere Unterstützungsprodukte oder Veranstaltungen.

Über uns

Bruce Eckel ist der Autor der mehrfach ausgezeichneten Bücher Thinking in Java und Thinking in C++ sowie einer Reihe weiterer Bücher über Computerprogrammierung, darunter Atomic Scala. Er hat weltweit Hunderte von Präsentationen gehalten und alternative Konferenzen und Veranstaltungen wie das Winter Tech Forum und Entwickler-Retreats organisiert. Bruce hat einen BS in angewandter Physik und einen MS in Computertechnik. Sein Blog befindet sich auf www.BruceEckel.com und sein Beratungs-, Trainings- und Konferenzunternehmen ist Mindview LLC.

Svetlana Isakova begann als Mitglied des Kotlin-Compiler-Teams und ist nun eine Entwickler-Botschafterin für JetBrains. Sie unterrichtet Kotlin und spricht auf Konferenzen weltweit und ist Mitautorin des Buches Kotlin in Action.

Danksagungen

  • Das Kotlin-Sprachdesign-Team und die Mitwirkenden.
  • Die Entwickler von Leanpub, die das Veröffentlichen dieses Buches so viel einfacher gemacht haben.
  • James Ward für die Umwandlung des Gradle-Builds in Kotlin und dafür, dass er im Allgemeinen großartig ist.

Widmungen

Für meinen geliebten Vater, E. Wayne Eckel. 1. April 1924—23. November 2016. Du hast mir zuerst etwas über Maschinen, Werkzeuge und Design beigebracht.

Für meinen Vater, Sergey Lvovich Isakov, der so früh von uns gegangen ist und den wir immer vermissen werden.

Über das Cover

Daniel Will-Harris gestaltete das Cover basierend auf dem Kotlin-Logo.

Warum Kotlin?

Programme sollen so geschrieben werden, dass Menschen sie lesen können, und erst in zweiter Linie für Maschinen, die sie ausführen.Harold Abelson, Mitautor, Structure and Interpretation of Computer Programs.

Dieses Kapitel bietet einen Überblick über die historische Entwicklung von Programmiersprachen, damit Sie verstehen, wo Kotlin einzuordnen ist und warum Sie es lernen möchten. Wir führen einige Themen ein, die, wenn Sie ein Anfänger sind, momentan zu kompliziert erscheinen mögen. Fühlen Sie sich frei, dieses Kapitel zu überspringen und später darauf zurückzukommen, nachdem Sie mehr vom Buch gelesen haben.

Die Gestaltung von Programmiersprachen ist ein evolutionärer Weg, der von der Erfüllung der Bedürfnisse der Maschine zur Erfüllung der Bedürfnisse des Programmierers führt.

Eine Programmiersprache wird von einem Sprachdesigner erfunden und als eines oder mehrere Programme implementiert, die als Werkzeuge zur Nutzung der Sprache dienen. Der Implementierer ist in der Regel der Sprachdesigner, zumindest anfangs.

Frühe Sprachen konzentrierten sich auf Hardwarebeschränkungen. Mit zunehmender Rechenleistung der Computer verlagerten sich neuere Sprachen hin zu anspruchsvollerer Programmierung mit einem Schwerpunkt auf Zuverlässigkeit. Diese Sprachen können Merkmale basierend auf der Psychologie des Programmierens wählen.

Jede Programmiersprache ist eine Sammlung von Experimenten. Historisch gesehen war das Design von Programmiersprachen eine Abfolge von Vermutungen und Annahmen darüber, was Programmierer produktiver machen könnte. Einige dieser Experimente scheitern, einige sind mäßig erfolgreich und einige sind sehr erfolgreich.

Wir lernen aus den Experimenten jeder neuen Sprache. Einige Sprachen befassen sich mit Problemen, die sich als nebensächlich statt wesentlich erweisen, oder die Umgebung ändert sich (schnellere Prozessoren, günstigere Speicher, neues Verständnis von Programmierung und Sprachen) und dieses Problem wird weniger wichtig oder sogar unbedeutend. Wenn diese Ideen veraltet sind und sich die Sprache nicht weiterentwickelt, verschwindet sie aus der Verwendung.

Die ursprünglichen Programmierer arbeiteten direkt mit Zahlen, die Prozessor-Maschinenbefehle darstellten. Dieser Ansatz führte zu zahlreichen Fehlern, und Assemblersprache wurde geschaffen, um die Zahlen durch mnemonische Opcodes—Wörter, die sich Programmierer leichter merken und lesen konnten, zusammen mit anderen hilfreichen Werkzeugen zu ersetzen. Es gab jedoch immer noch eine Eins-zu-eins-Entsprechung zwischen Assemblerbefehlen und Maschinenbefehlen, und Programmierer mussten jede Zeile Assemblercode schreiben. Darüber hinaus verwendete jeder Computerprozessor seine eigene spezifische Assemblersprache.

Das Entwickeln von Programmen in Assemblersprache ist äußerst kostspielig. Höhere Programmiersprachen helfen, dieses Problem zu lösen, indem sie eine Abstraktionsebene von den niedrigeren Assemblersprachen schaffen.

Compiler und Interpreter

Die Anweisungen einer interpretierten Sprache werden direkt von einem Programm namens Interpreter ausgeführt. Kotlin wird kompiliert statt interpretiert. Der Quellcode einer kompilierten Sprache wird in eine andere Darstellung umgewandelt, die als eigenes Programm läuft, entweder direkt auf einem Hardwareprozessor oder auf einer virtuellen Maschine, die einen Prozessor emuliert:

Sprachen wie C, C++, Go und Rust werden in Maschinencode kompiliert, der direkt auf der zugrunde liegenden Hardware-Zentralverarbeitungseinheit (CPU) läuft. Sprachen wie Java und Kotlin werden in Bytecode kompiliert, der ein Zwischenformat ist, das nicht direkt auf der Hardware-CPU läuft, sondern auf einer virtuellen Maschine, einem Programm, das Bytecode-Anweisungen ausführt. Programme, die von der JVM-Version von Kotlin erzeugt werden, laufen auf der Java Virtual Machine (JVM).

Die Portabilität ist ein wichtiger Vorteil einer virtuellen Maschine. Der gleiche Bytecode kann auf jedem Computer laufen, der eine virtuelle Maschine hat. Virtuelle Maschinen können für spezielle Hardware optimiert werden und Geschwindigkeitsprobleme lösen. Die JVM enthält viele Jahre solcher Optimierungen und wurde auf vielen Plattformen implementiert.

Zur Kompilierungszeit wird der Code vom Compiler überprüft, um Kompilierungsfehler zu entdecken. (IntelliJ IDEA und andere Entwicklungsumgebungen heben diese Fehler hervor, wenn Sie den Code eingeben, sodass Sie schnell Probleme entdecken und beheben können). Wenn es keine Kompilierungsfehler gibt, wird der Quellcode in Bytecode kompiliert.

Ein Laufzeitfehler kann zur Kompilierungszeit nicht entdeckt werden, daher tritt er erst auf, wenn Sie das Programm ausführen. Typischerweise sind Laufzeitfehler schwieriger zu entdecken und teurer zu beheben. Statisch typisierte Sprachen wie Kotlin entdecken so viele Fehler wie möglich zur Kompilierungszeit, während dynamische Sprachen ihre Sicherheitsprüfungen zur Laufzeit durchführen (einige dynamische Sprachen führen nicht so viele Sicherheitsprüfungen durch, wie sie könnten).

Sprachen, die Kotlin beeinflusst haben

Kotlin zieht seine Ideen und Merkmale aus vielen Sprachen, und diese Sprachen wurden von früheren Sprachen beeinflusst. Es ist hilfreich, etwas über die Geschichte der Programmiersprachen zu wissen, um Perspektiven zu gewinnen, wie wir zu Kotlin gekommen sind. Die hier beschriebenen Sprachen wurden wegen ihres Einflusses auf die nachfolgenden Sprachen ausgewählt. All diese Sprachen inspirierten letztendlich das Design von Kotlin, manchmal indem sie ein Beispiel dafür waren, was man nicht tun sollte.

FORTRAN: FORmula TRANslation (1957)

Entwickelt für den Einsatz durch Wissenschaftler und Ingenieure, war das Ziel von Fortran, das Codieren von Gleichungen zu erleichtern. Fein abgestimmte und getestete Fortran-Bibliotheken sind noch heute im Einsatz, werden jedoch typischerweise “umwickelt”, um sie von anderen Sprachen aus aufrufbar zu machen.

LISP: LISt Processor (1958)

Anstatt anwendungsspezifisch zu sein, verkörperte LISP wesentliche Programmierkonzepte; es war die Sprache der Informatiker und die erste funktionale Programmiersprache (Sie werden in diesem Buch über funktionale Programmierung lernen). Der Kompromiss für seine Macht und Flexibilität war die Effizienz: LISP war typischerweise zu teuer, um auf frühen Maschinen ausgeführt zu werden, und erst in den letzten Jahrzehnten wurden Maschinen schnell genug, um eine Wiederbelebung der Nutzung von LISP zu ermöglichen. Zum Beispiel ist der GNU Emacs-Editor vollständig in LISP geschrieben und kann mit LISP erweitert werden.

ALGOL: ALGOrithmic Language (1958)

Wahrscheinlich die einflussreichste der Sprachen der 1950er Jahre, da sie eine Syntax einführte, die in vielen nachfolgenden Sprachen Bestand hatte. Zum Beispiel sind C und seine Derivate “ALGOL-ähnliche” Sprachen.

COBOL: COmmon Business-Oriented Language (1959)

Entwickelt für Geschäft, Finanzen und administrative Datenverarbeitung. Es hat eine englischartige Syntax und sollte selbstdokumentierend und sehr lesbar sein. Obwohl diese Absicht im Allgemeinen scheiterte—COBOL ist berüchtigt für Fehler, die durch ein fehlplatziertes Punktzeichen eingeführt wurden—zwang das US-Verteidigungsministerium die weitverbreitete Einführung auf Großrechnern, und Systeme laufen (und erfordern Wartung) noch heute.

BASIC: Beginners’ All-purpose Symbolic Instruction Code (1964)

BASIC war einer der frühen Versuche, Programmieren zugänglich zu machen. Obwohl sehr erfolgreich, waren seine Funktionen und Syntax begrenzt, sodass es nur teilweise hilfreich für Menschen war, die anspruchsvollere Sprachen lernen mussten. Es ist überwiegend eine interpretierte Sprache, was bedeutet, dass man den ursprünglichen Code für das Programm benötigt, um es auszuführen. Trotzdem wurden viele nützliche Programme in BASIC geschrieben, insbesondere als Skriptsprache für Microsofts “Office”-Produkte. BASIC könnte sogar als die erste “offene” Programmiersprache betrachtet werden, da zahlreiche Variationen davon erstellt wurden.

Simula 67, die ursprüngliche objektorientierte Sprache (1967)

Eine Simulation beinhaltet typischerweise viele “Objekte”, die miteinander interagieren. Verschiedene Objekte haben unterschiedliche Eigenschaften und Verhaltensweisen. Die zu der Zeit existierenden Sprachen waren unhandlich für Simulationen zu verwenden, daher wurde Simula (eine weitere “ALGOL-ähnliche” Sprache) entwickelt, um direkte Unterstützung für die Erstellung von Simulationsobjekten zu bieten. Es stellt sich heraus, dass diese Ideen auch für allgemeine Programmierung nützlich sind, und dies war der Ursprung der objektorientierten (OO) Sprachen.

Pascal (1970)

Pascal erhöhte die Kompilierungsgeschwindigkeit, indem es die Sprache so einschränkte, dass sie als Einzelpass-Compiler implementiert werden konnte. Die Sprache zwang den Programmierer, ihren Code auf eine bestimmte Weise zu strukturieren und legte etwas umständliche und weniger lesbare Einschränkungen für die Programmorganisation auf. Da Prozessoren schneller wurden, Speicher billiger und die Compiler-Technologie besser, wurden die Auswirkungen dieser Einschränkungen zu kostspielig.

Eine Implementierung von Pascal, Turbo Pascal von Borland, arbeitete zunächst auf CP/M-Maschinen und machte dann den Sprung zu frühen MS-DOS (Vorläufer von Windows), später entwickelte es sich zur Delphi-Sprache für Windows. Indem alles im Speicher untergebracht wurde, kompilierte Turbo Pascal in atemberaubender Geschwindigkeit auf sehr leistungsschwachen Maschinen, was das Programmerlebnis dramatisch verbesserte. Sein Schöpfer, Anders Hejlsberg, entwarf später sowohl C# als auch TypeScript.

Niklaus Wirth, der Erfinder von Pascal, schuf nachfolgende Sprachen: Modula, Modula-2 und Oberon. Wie der Name schon sagt, konzentrierte sich Modula auf die Aufteilung von Programmen in Module, für bessere Organisation und schnellere Kompilierung. Die meisten modernen Sprachen unterstützen separate Kompilierung und eine Form von Modulsystem.

C (1972)

Trotz der zunehmenden Zahl von Hochsprachen schrieben Programmierer immer noch in Assemblersprache. Dies wird oft als Systemprogrammierung bezeichnet, da es auf Ebene des Betriebssystems erfolgt, umfasst aber auch eingebettete Programmierung für spezielle physische Geräte. Dies ist nicht nur mühsam und teuer (Bruce begann seine Karriere mit dem Schreiben von Assemblersprache für eingebettete Systeme), sondern es ist auch nicht portabel—Assemblersprache kann nur auf dem Prozessor laufen, für den sie geschrieben wurde. C wurde als “hochlevelige Assemblersprache” entworfen, die dennoch nah genug an der Hardware ist, dass man selten Assemblersprache schreiben muss. Noch wichtiger ist, dass ein C-Programm auf jedem Prozessor mit einem C-Compiler läuft. C entkoppelte das Programm vom Prozessor, was ein großes und teures Problem löste. Als Ergebnis konnten ehemalige Assemblersprachen-Programmierer in C weitaus produktiver sein. C war so effektiv, dass neuere Sprachen (insbesondere Go und Rust) immer noch versuchen, es für die Systemprogrammierung abzulösen.

Smalltalk (1972)

Von Anfang an als rein objektorientiert konzipiert, hat Smalltalk die OO- und Sprachtheorie erheblich vorangebracht, indem es eine Plattform für Experimente war und die schnelle Anwendungsentwicklung demonstrierte. Es wurde jedoch in einer Zeit entwickelt, als Sprachen noch proprietär waren, und der Einstiegspreis für ein Smalltalk-System konnte in die Tausende gehen. Es war interpretiert, sodass man eine Smalltalk-Umgebung benötigte, um Programme auszuführen. Open-Source-Smalltalk-Implementierungen erschienen erst, nachdem die Programmierwelt sich weiterentwickelt hatte. Smalltalk-Programmierer haben großartige Einblicke geliefert, die späteren OO-Sprachen wie C++ und Java zugutekamen.

C++: Ein besseres C mit Objekten (1983)

Bjarne Stroustrup schuf C++, weil er ein besseres C wollte und Unterstützung für die objektorientierten Konstrukte, die er bei der Verwendung von Simula-67 erlebt hatte. Bruce war acht Jahre lang Mitglied des C++-Normungsausschusses und schrieb drei Bücher über C++, darunter Thinking in C++.

Rückwärtskompatibilität mit C war ein grundlegendes Prinzip des C++-Designs, sodass C-Code in C++ mit praktisch keinen Änderungen kompiliert werden kann. Dies bot einen einfachen Migrationspfad - Programmierer konnten weiterhin in C programmieren, die Vorteile von C++ nutzen und langsam mit C++-Funktionen experimentieren, während sie produktiv blieben. Die meisten Kritiken an C++ lassen sich auf die Einschränkung der Rückwärtskompatibilität mit C zurückführen.

Eines der Probleme bei C war das Thema Speicherverwaltung. Der Programmierer muss zuerst Speicher erwerben, dann eine Operation mit diesem Speicher ausführen und dann den Speicher freigeben. Das Vergessen, Speicher freizugeben, wird als Speicherleck bezeichnet und kann dazu führen, dass der verfügbare Speicher aufgebraucht wird und der Prozess abstürzt. Die anfängliche Version von C++ machte einige Innovationen in diesem Bereich, zusammen mit Konstruktoren, um eine ordnungsgemäße Initialisierung sicherzustellen. Spätere Versionen der Sprache haben bedeutende Verbesserungen in der Speicherverwaltung vorgenommen.

Python: Freundlich und flexibel (1990)

Der Designer von Python, Guido Van Rossum, schuf die Sprache basierend auf seiner Inspiration des „Programmieren für alle“. Seine Pflege der Python-Community hat ihr den Ruf verliehen, die freundlichste und unterstützendste Community in der Programmierwelt zu sein. Python war eine der ersten Open-Source-Sprachen, was zu Implementierungen auf praktisch jeder Plattform führte, einschließlich eingebetteter Systeme und maschinellem Lernen. Seine Dynamik und Benutzerfreundlichkeit machen es ideal für die Automatisierung kleiner, sich wiederholender Aufgaben, aber seine Funktionen unterstützen auch die Erstellung großer, komplexer Programme.

Python ist eine echte “Grassroots”-Sprache; es hatte nie ein Unternehmen, das es förderte, und die Einstellung seiner Fans war, die Sprache niemals zu pushen, sondern einfach jedem zu helfen, der sie lernen möchte. Die Sprache verbessert sich stetig, und in den letzten Jahren ist ihre Popularität explodiert.

Python könnte die erste Mainstream-Sprache gewesen sein, die funktionale und OO-Programmierung kombinierte. Es war Java voraus mit automatischer Speicherverwaltung durch Müllabfuhr (normalerweise müssen Sie selbst nie Speicher zuweisen oder freigeben) und der Fähigkeit, Programme auf mehreren Plattformen auszuführen.

Haskell: Reine funktionale Programmierung (1990)

Inspiriert von Miranda (1985), einer proprietären Sprache, wurde Haskell als offener Standard für die Forschung zur reinen funktionalen Programmierung geschaffen, obwohl es auch für Produkte verwendet wurde. Syntax und Ideen von Haskell haben eine Reihe nachfolgender Sprachen beeinflusst, darunter Kotlin.

Java: Virtuelle Maschinen und Müllabfuhr (1995)

James Gosling und sein Team erhielten die Aufgabe, Code für eine TV-Set-Top-Box zu schreiben. Sie entschieden, dass sie C++ nicht mochten und anstatt die Box zu erstellen, entwickelten sie die Java-Sprache. Das Unternehmen, Sun Microsystems, setzte einen enormen Marketingdruck hinter die kostenlose Sprache (damals eine neue Idee), um die aufkommende Internetlandschaft zu dominieren.

Dieses wahrgenommene Zeitfenster für die Internet-Dominanz setzte das Java-Sprachdesign unter erheblichen Druck, was zu einer beträchtlichen Anzahl von Mängeln führte (Das Buch Thinking in Java beleuchtet diese Mängel, damit die Leser darauf vorbereitet sind, mit ihnen umzugehen). Brian Goetz bei Oracle, der derzeitige leitende Entwickler von Java, hat bemerkenswerte und überraschende Verbesserungen an Java vorgenommen, trotz der Einschränkungen, die er geerbt hat. Obwohl Java bemerkenswert erfolgreich war, ist ein wichtiges Kotlin-Designziel, die Mängel von Java zu beheben, damit Programmierer produktiver sein können.

Der Erfolg von Java beruht auf zwei innovativen Funktionen: einer virtuellen Maschine und Müllabfuhr. Diese waren in anderen Sprachen verfügbar - zum Beispiel haben LISP, Smalltalk und Python Müllabfuhr, und UCSD Pascal lief auf einer virtuellen Maschine -, aber sie wurden nie als praktikabel für Mainstream-Sprachen angesehen. Java änderte das und machte Programmierer dadurch erheblich produktiver.

Eine virtuelle Maschine ist eine Zwischenebene zwischen der Sprache und der Hardware. Die Sprache muss keinen Maschinencode für einen bestimmten Prozessor erzeugen; sie muss nur eine Zwischen-Sprache (Bytecode) erzeugen, die auf der virtuellen Maschine läuft. Virtuelle Maschinen erfordern Rechenleistung und wurden vor Java als unpraktisch angesehen. Die Java Virtual Machine (JVM) führte zu Javas Slogan “write once, run everywhere.” Darüber hinaus können andere Sprachen leichter entwickelt werden, indem sie die JVM anvisieren; Beispiele umfassen Groovy, eine Java-ähnliche Skriptsprache, und Clojure, eine Version von LISP.

Die Müllabfuhr löst das Problem, das Freigeben von Speicher zu vergessen, oder wenn es schwierig ist, zu wissen, wann ein Speicherplatz nicht mehr genutzt wird. Projekte wurden erheblich verzögert oder sogar abgebrochen wegen Speicherlecks. Obwohl die Müllabfuhr in einigen früheren Sprachen vorkommt, galt sie als inakzeptabel aufwändig, bis Java ihre Praktikabilität demonstrierte.

JavaScript: Nur dem Namen nach Java (1995)

Der ursprüngliche Webbrowser kopierte und zeigte einfach Seiten von einem Webserver an. Webbrowser vervielfältigten sich und wurden zu einer neuen Programmierplattform, die Sprachunterstützung benötigte. Java wollte diese Sprache sein, war aber zu umständlich für den Job. JavaScript begann als LiveScript und wurde in NetScape Navigator integriert, einen der ersten Webbrowser. Die Umbenennung in JavaScript war ein Marketingtrick von NetScape, da die Sprache nur eine vage Ähnlichkeit mit Java hat.

Als das Web aufblühte, wurde JavaScript enorm wichtig. Das Verhalten von JavaScript war jedoch so unvorhersehbar, dass Douglas Crockford ein Buch mit dem ironischen Titel JavaScript, the Good Parts schrieb, in dem er alle Probleme mit der Sprache aufzeigte, damit Programmierer sie vermeiden können. Nachfolgende Verbesserungen durch das ECMAScript-Komitee haben JavaScript für einen ursprünglichen JavaScript-Programmierer unkenntlich gemacht. Es wird jetzt als stabile und ausgereifte Sprache betrachtet.

Web-Assembly (WASM) wurde von JavaScript abgeleitet, um eine Art Bytecode für Webbrowser zu sein. Es läuft oft viel schneller als JavaScript und kann von anderen Sprachen generiert werden. Zum Zeitpunkt des Schreibens arbeitet das Kotlin-Team daran, WASM als Ziel hinzuzufügen.

C#: Java für .NET (2000)

C# wurde entwickelt, um einige der wichtigen Fähigkeiten von Java auf der .NET (Windows) Plattform bereitzustellen, während es den Designern freistellte, sich nicht an die Java-Sprache zu halten. Das Ergebnis beinhaltete zahlreiche Verbesserungen gegenüber Java. Zum Beispiel entwickelte C# das Konzept der Erweiterungsfunktionen, die in Kotlin stark genutzt werden. C# wurde auch deutlich funktionaler als Java. Viele C#-Funktionen haben offensichtlich das Design von Kotlin beeinflusst.

Scala: SCALAble (2003)

Martin Odersky schuf Scala, um auf der Java Virtual Machine zu laufen: Um auf der Arbeit auf der JVM aufzubauen, um mit Java-Programmen zu interagieren und möglicherweise mit der Idee, dass es Java verdrängen könnte. Als Forscher nutzten Odersky und sein Team Scala als Plattform, um mit Sprachmerkmalen zu experimentieren, insbesondere solchen, die nicht in Java enthalten sind.

Diese Experimente waren erhellend, und eine Reihe davon fand in modifizierter Form ihren Weg nach Kotlin. Zum Beispiel wird die Fähigkeit, Operatoren wie + für spezielle Fälle neu zu definieren, als Operatorüberladung bezeichnet. Dies war in C++ enthalten, aber nicht in Java. Scala fügte die Operatorüberladung hinzu, erlaubt aber auch die Erfindung neuer Operatoren durch Kombination beliebiger Zeichenfolgen. Dies führt oft zu verwirrenderen Code. Eine begrenzte Form der Operatorüberladung ist in Kotlin enthalten, aber man kann nur Operatoren überladen, die bereits existieren.

Scala ist auch ein objekt-funktionales Hybrid, ähnlich wie Python, aber mit einem Fokus auf reine Funktionen und strenge Objekte. Dies inspirierte die Entscheidung von Kotlin, ebenfalls ein objekt-funktionales Hybrid zu sein.

Wie Scala läuft Kotlin auf der JVM, interagiert jedoch viel einfacher mit Java als Scala (siehe Anhang B). Darüber hinaus zielt Kotlin auf JavaScript, das Android-Betriebssystem und erzeugt nativen Code für andere Plattformen.

Atomic Kotlin entwickelte sich aus den Ideen und Materialien von Atomic Scala.

Groovy: Eine dynamische JVM-Sprache (2007)

Dynamische Sprachen sind ansprechend, weil sie interaktiver und prägnanter sind als statische Sprachen. Es gab zahlreiche Versuche, ein dynamischeres Programmiererlebnis auf der JVM zu erreichen, darunter Jython (Python) und Clojure (ein Dialekt von Lisp). Groovy war die erste, die breite Akzeptanz erreichte.

Auf den ersten Blick erscheint Groovy als bereinigte Version von Java, die ein angenehmeres Programmiererlebnis bietet. Der meiste Java-Code läuft unverändert in Groovy, sodass Java-Programmierer schnell produktiv sein können und später die anspruchsvolleren Funktionen erlernen können, die bemerkenswerte Programmierverbesserungen gegenüber Java bieten.

Die Kotlin-Operatoren ?. und ?:, die sich mit dem Problem der Leere beschäftigen, erschienen zuerst in Groovy.

Es gibt zahlreiche Groovy-Funktionen, die in Kotlin erkennbar sind. Einige dieser Funktionen erscheinen auch in anderen Sprachen, was wahrscheinlich stärker dafür drängte, dass sie in Kotlin aufgenommen wurden.

Warum Kotlin? (Eingeführt 2011, Version 1.0: 2016)

Genauso wie C++ ursprünglich als “ein besseres C” gedacht war, war Kotlin zunächst darauf ausgerichtet, “ein besseres Java” zu sein. Es hat sich seitdem erheblich über dieses Ziel hinaus entwickelt.

Kotlin wählt pragmatisch nur die erfolgreichsten und hilfreichsten Funktionen aus anderen Programmiersprachen aus—nachdem diese Funktionen in der Praxis getestet und als besonders wertvoll erwiesen wurden.

Wenn Sie also von einer anderen Sprache kommen, könnten Sie einige Funktionen dieser Sprache in Kotlin wiedererkennen. Dies ist beabsichtigt: Kotlin maximiert die Produktivität, indem es bewährte Konzepte nutzt.

Lesbarkeit

Lesbarkeit ist ein Hauptziel bei der Gestaltung der Sprache. Die Kotlin-Syntax ist prägnant—sie erfordert in den meisten Szenarien keine Förmlichkeit, kann aber dennoch komplexe Ideen ausdrücken.

Werkzeuge

Kotlin stammt von JetBrains, einem Unternehmen, das sich auf Entwicklerwerkzeuge spezialisiert hat. Es bietet erstklassige Unterstützung für Werkzeuge, und viele Sprachmerkmale wurden mit Blick auf Werkzeuge entwickelt.

Multi-Paradigma

Kotlin unterstützt mehrere Programmierparadigmen, die in diesem Buch sanft eingeführt werden:

  • Imperatives Programmieren
  • Funktionales Programmieren
  • Objektorientiertes Programmieren

Multi-Plattform

Kotlin-Quellcode kann in verschiedene Zielplattformen kompiliert werden:

  • JVM. Der Quellcode wird in JVM-Bytecode (.class Dateien) kompiliert, der dann auf jeder Java Virtual Machine (JVM) ausgeführt werden kann.
  • Android. Android hat seine eigene Laufzeitumgebung namens ART (der Vorgänger hieß Dalvik). Der Kotlin-Quellcode wird in das Dalvik Executable Format (.dex Dateien) kompiliert.
  • JavaScript, um innerhalb eines Webbrowsers ausgeführt zu werden.
  • Native Binaries durch die Generierung von Maschinencode für spezifische Plattformen und CPUs.

Dieses Buch konzentriert sich auf die Sprache selbst, wobei die JVM als einzige Zielplattform verwendet wird. Sobald Sie die Sprache beherrschen, können Sie Kotlin auf verschiedene Anwendungen und Zielplattformen anwenden.

Zwei Kotlin-Funktionen

Dieses Atom setzt nicht voraus, dass Sie ein Programmierer sind, was es schwierig macht, die meisten Vorteile von Kotlin gegenüber Alternativen zu erklären. Es gibt jedoch zwei Themen, die sehr wirkungsvoll sind und zu diesem frühen Zeitpunkt erklärt werden können: Java-Interoperabilität und das Problem, “keinen Wert” anzuzeigen.

Mühelose Java-Interoperabilität

Um “ein besseres C” zu sein, muss C++ rückwärtskompatibel mit der Syntax von C sein, aber Kotlin muss nicht rückwärtskompatibel mit der Syntax von Java sein—es muss nur mit der JVM arbeiten. Dies befreit die Kotlin-Designer, eine viel sauberere und leistungsfähigere Syntax zu schaffen, ohne das visuelle Rauschen und die Komplikationen, die Java überladen.

Damit Kotlin “ein besseres Java” ist, muss das Erlebnis, es auszuprobieren, angenehm und reibungslos sein, sodass Kotlin eine mühelose Integration mit bestehenden Java-Projekten ermöglicht. Sie können ein kleines Stück Kotlin-Funktionalität schreiben und es mitten in Ihren bestehenden Java-Code einfügen. Der Java-Code merkt nicht einmal, dass der Kotlin-Code da ist—er sieht einfach wie weiterer Java-Code aus.

Unternehmen untersuchen oft eine neue Sprache, indem sie ein eigenständiges Programm mit dieser Sprache erstellen. Idealerweise ist dieses Programm nützlich, aber nicht essentiell, sodass es bei einem Scheitern des Projekts mit minimalem Schaden beendet werden kann. Nicht jedes Unternehmen möchte die Ressourcen aufwenden, die für diese Art von Experimenten erforderlich sind. Da Kotlin sich nahtlos in ein bestehendes Java-System integriert (und von dessen Tests profitiert), wird es sehr billig oder sogar kostenlos, Kotlin auszuprobieren, um zu sehen, ob es passt.

Darüber hinaus bietet JetBrains, das Unternehmen, das Kotlin erstellt, IntelliJ IDEA in einer “Community” (kostenlosen) Version an, die Unterstützung sowohl für Java als auch für Kotlin beinhaltet und die Möglichkeit bietet, die beiden einfach zu integrieren. Es gibt sogar ein Tool, das Java-Code nimmt und ihn (größtenteils) in Kotlin umschreibt.

Anhang B behandelt Java-Interoperabilität.

Darstellung von Leere

Ein besonders vorteilhaftes Kotlin-Feature ist seine Lösung für ein herausforderndes Programmierproblem.

Was tun Sie, wenn Ihnen jemand ein Wörterbuch in die Hand drückt und Sie bittet, ein Wort nachzuschlagen, das nicht existiert? Sie könnten Ergebnisse garantieren, indem Sie Definitionen für unbekannte Wörter erfinden. Ein nützlicherer Ansatz ist einfach zu sagen: “Es gibt keine Definition für dieses Wort.” Dies zeigt ein erhebliches Problem in der Programmierung: Wie zeigt man “keinen Wert” für einen Speicherplatz an, der nicht initialisiert ist, oder für das Ergebnis einer Operation?

Die Null-Referenz wurde 1965 für ALGOL von Tony Hoare erfunden, der sie später als “meinen Milliarden-Dollar-Fehler” bezeichnete. Ein Problem war, dass sie zu einfach war—manchmal reicht es nicht aus, zu wissen, dass ein Raum leer ist. Man muss vielleicht wissen, warum er leer ist. Dies führt zum zweiten Problem: der Implementierung. Aus Effizienzgründen war es typischerweise nur ein spezieller Wert, der in eine kleine Menge Speicher passte, und was war besser als der Speicher, der bereits für diese Information bereitgestellt war?

Die ursprüngliche C-Sprache initialisierte den Speicher nicht automatisch, was zahlreiche Probleme verursachte. C++ verbesserte die Situation, indem neu zugewiesener Speicher auf null gesetzt wurde. Wenn also ein numerischer Wert nicht initialisiert ist, ist er einfach eine numerische Null. Das schien nicht so schlimm zu sein, aber es ermöglichte es, dass nicht initialisierte Werte unbemerkt durchrutschten (neuere C- und C++-Compiler warnen oft davor). Schlimmer noch, wenn ein Speicherstück ein Zeiger war—verwendet, um auf ein anderes Speicherstück zu verweisen—würde ein Nullzeiger auf die Speicheradresse null zeigen, was fast sicher nicht das ist, was man will.

Java verhindert Zugriffe auf nicht initialisierte Werte, indem es solche Fehler zur Laufzeit meldet. Obwohl dies nicht initialisierte Werte entdeckt, löst es das Problem nicht, denn die einzige Möglichkeit, zu überprüfen, ob Ihr Programm nicht abstürzt, besteht darin, es auszuführen. Es gibt Schwärme dieser Art von Fehlern im Java-Code, und Programmierer verschwenden enorme Mengen an Zeit, um sie zu finden.

Kotlin löst dieses Problem, indem es Operationen verhindert, die Nullfehler verursachen könnten, zur Kompilierzeit, bevor das Programm ausgeführt werden kann. Dies ist das am meisten gefeierte Merkmal von Java-Programmierern, die Kotlin übernehmen. Diese eine Funktion kann Java’s Nullfehler minimieren oder eliminieren, was Ihrem Projekt erhebliche Mengen an Zeit und Geld spart.

Eine Fülle von Vorteilen

Die beiden Funktionen, die wir hier erklären konnten (ohne mehr Programmierkenntnisse zu erfordern), machen einen großen Unterschied, unabhängig davon, ob Sie ein Java-Programmierer sind oder nicht. Wenn Kotlin Ihre erste Sprache ist und Sie an einem Projekt arbeiten, das mehr Programmierer benötigt, ist es viel einfacher, einen der vielen existierenden Java-Programmierer für Kotlin zu gewinnen.

Kotlin hat viele weitere Vorteile, die wir erst erklären können, wenn Sie mehr über das Programmieren wissen. Dafür ist der Rest des Buches da.

  • -

Sprachen werden oft aus Leidenschaft gewählt, nicht aus Vernunft… Ich versuche, Kotlin zu einer Sprache zu machen, die aus einem Grund geliebt wird.—Andrey Breslav, Kotlin Lead Language Designer.

Hallo, Welt!

“Hello, world!” ist ein Programm, das häufig verwendet wird, um die grundlegende Syntax von Programmiersprachen zu demonstrieren.

Wir entwickeln dieses Programm in mehreren Schritten, damit Sie seine Teile verstehen.

Zuerst lassen Sie uns ein leeres Programm untersuchen, das überhaupt nichts tut:

// HelloWorld/EmptyProgram.kt

fun main() {
  // Program code here ...
}

Das Beispiel beginnt mit einem Kommentar, der ein erläuternder Text ist, der von Kotlin ignoriert wird. // (zwei Schrägstriche) leitet einen Kommentar ein, der bis zum Ende der aktuellen Zeile geht:

// Single-line comment

Kotlin ignoriert das // und alles danach bis zum Ende der Zeile. In der folgenden Zeile wird es wieder beachtet.

Die erste Zeile jedes Beispiels in diesem Buch ist ein Kommentar, der mit dem Namen des Unterverzeichnisses beginnt, das die Quellcodedatei enthält (hier HelloWorld), gefolgt vom Namen der Datei: EmptyProgram.kt. Das Beispiel-Unterverzeichnis für jedes Atom entspricht dem Namen dieses Atoms.

Schlüsselwörter sind von der Sprache reserviert und haben eine spezielle Bedeutung. Das Schlüsselwort fun steht für Funktion. Eine Funktion ist eine Sammlung von Code, die unter Verwendung des Namens dieser Funktion ausgeführt werden kann (wir verbringen viel Zeit im Buch mit Funktionen). Der Name der Funktion folgt dem fun Schlüsselwort, in diesem Fall ist es main() (im Fließtext folgen wir dem Funktionsnamen mit Klammern).

main() ist tatsächlich ein spezieller Name für eine Funktion; es zeigt den “Einstiegspunkt” für ein Kotlin-Programm an. Ein Kotlin-Programm kann viele Funktionen mit verschiedenen Namen haben, aber main() ist diejenige, die automatisch aufgerufen wird, wenn Sie das Programm ausführen.

Die Parameterliste folgt dem Funktionsnamen und ist in Klammern eingeschlossen. Hier übergeben wir nichts an main(), daher ist die Parameterliste leer.

Der Funktionskörper erscheint nach der Parameterliste. Er beginnt mit einer öffnenden Klammer ({) und endet mit einer schließenden Klammer (}). Der Funktionskörper enthält Anweisungen und Ausdrücke. Eine Anweisung erzeugt eine Wirkung, und ein Ausdruck liefert ein Ergebnis.

EmptyProgram.kt enthält keine Anweisungen oder Ausdrücke im Körper, nur einen Kommentar.

Lassen Sie das Programm “Hello, world!” anzeigen, indem Sie eine Zeile im main()-Körper hinzufügen:

// HelloWorld/HelloWorld.kt

fun main() {
  println("Hello, world!")
}
/* Output:
Hello, world!
*/

Die Zeile, die die Begrüßung anzeigt, beginnt mit println(). Wie main(), ist println() eine Funktion. Diese Zeile ruft die Funktion auf, die dann ihren Körper ausführt. Man gibt den Funktionsnamen an, gefolgt von Klammern, die einen oder mehrere Parameter enthalten. In diesem Buch fügen wir beim Bezug auf eine Funktion in der Prosa nach dem Namen Klammern hinzu, um daran zu erinnern, dass es sich um eine Funktion handelt. Hier sagen wir println().

println() nimmt einen einzelnen Parameter, der ein String ist. Ein String wird definiert, indem man Zeichen in Anführungszeichen setzt.

println() bewegt den Cursor nach der Anzeige seines Parameters in eine neue Zeile, sodass nachfolgende Ausgaben in der nächsten Zeile erscheinen. Man kann stattdessen print() verwenden, das den Cursor in derselben Zeile belässt.

Im Gegensatz zu einigen Sprachen benötigt man in Kotlin kein Semikolon am Ende eines Ausdrucks. Es ist nur notwendig, wenn man mehr als einen Ausdruck auf eine einzelne Zeile setzt (was nicht empfohlen wird).

Für einige Beispiele im Buch zeigen wir die Ausgabe am Ende der Auflistung in einem mehrzeiligen Kommentar. Ein mehrzeiliger Kommentar beginnt mit einem /* (einem Schrägstrich gefolgt von einem Sternchen) und setzt sich fort – einschließlich Zeilenumbrüchen (die wir neue Zeilen nennen) – bis ein */ (ein Sternchen gefolgt von einem Schrägstrich) den Kommentar beendet:

/* A multiline comment
Doesn't care
about newlines */

Es ist möglich, Code auf derselben Zeile nach dem schließenden */ eines Kommentars hinzuzufügen, aber das ist verwirrend, daher tun es die Leute normalerweise nicht.

Kommentare fügen Informationen hinzu, die nicht offensichtlich aus dem Code hervorgehen. Wenn Kommentare nur wiederholen, was der Code sagt, werden sie lästig und die Leute beginnen, sie zu ignorieren. Wenn sich der Code ändert, vergessen Programmierer oft, die Kommentare zu aktualisieren, daher ist es eine gute Praxis, Kommentare sparsam zu verwenden, hauptsächlich um knifflige Aspekte Ihres Codes hervorzuheben.

Übungen und Lösungen finden Sie auf www.AtomicKotlin.com.

var & val

Wenn ein Bezeichner Daten enthält, müssen Sie entscheiden, ob er neu zugewiesen werden kann.

Sie erstellen Bezeichner, um auf Elemente in Ihrem Programm zu verweisen. Die grundlegendste Entscheidung für einen Datenbezeichner ist, ob er seinen Inhalt während der Programmausführung ändern kann oder ob er nur einmal zugewiesen werden kann. Dies wird durch zwei Schlüsselwörter gesteuert:

  • var, kurz für Variable, was bedeutet, dass Sie seinen Inhalt neu zuweisen können.
  • val, kurz für Wert, was bedeutet, dass Sie ihn nur initialisieren können; Sie können ihn nicht neu zuweisen.

Sie definieren ein var so:

var identifier = initialization

Das Schlüsselwort var wird gefolgt von dem Bezeichner, einem Gleichheitszeichen und dann dem Initialisierungswert. Der Bezeichner beginnt mit einem Buchstaben oder einem Unterstrich, gefolgt von Buchstaben, Zahlen und Unterstrichen. Groß- und Kleinschreibung werden unterschieden (also sind thisvalue und thisValue unterschiedlich).

Hier sind einige var Definitionen:

// VarAndVal/Vars.kt

fun main() {
  var whole = 11              // [1]
  var fractional = 1.4        // [2]
  var words = "Twas Brillig"  // [3]
  println(whole)
  println(fractional)
  println(words)
}
/* Output:
11
1.4
Twas Brillig
*/

In diesem Buch versehen wir Zeilen mit kommentierten Nummern in eckigen Klammern, damit wir im Text auf sie verweisen können, wie folgt:

  • [1] Erstellen Sie eine var namens whole und speichern Sie 11 darin.
  • [2] Speichern Sie die “Bruchzahl” 1.4 in der var fractional.
  • [3] Speichern Sie etwas Text (einen String) in der var words.

Beachten Sie, dass println() jeden einzelnen Wert als Argument annehmen kann.

Wie der Name Variable impliziert, kann eine var variieren. Das heißt, Sie können die in einer var gespeicherten Daten ändern. Wir sagen, dass eine var veränderlich ist:

// VarAndVal/AVarIsMutable.kt

fun main() {
  var sum = 1
  sum = sum + 2
  sum += 3
  println(sum)
}
/* Output:
6
*/

Die Zuweisung sum = sum + 2 nimmt den aktuellen Wert von sum, addiert zwei und weist das Ergebnis zurück in sum zu.

Die Zuweisung sum += 3 bedeutet dasselbe wie sum = sum + 3. Der += Operator nimmt den vorher gespeicherten Wert in sum und erhöht ihn um 3, dann weist er dieses neue Ergebnis zurück in sum zu.

Den in einer var gespeicherten Wert zu ändern, ist eine nützliche Methode, um Änderungen auszudrücken. Wenn jedoch die Komplexität eines Programms zunimmt, ist Ihr Code klarer, sicherer und leichter zu verstehen, wenn die Werte, die durch Ihre Bezeichner dargestellt werden, sich nicht ändern können—das heißt, sie können nicht neu zugewiesen werden. Wir spezifizieren einen unveränderlichen Bezeichner, indem wir statt var das Schlüsselwort val verwenden. Ein val kann nur einmal zugewiesen werden, wenn es erstellt wird:

val identifier = initialization

Das Schlüsselwort val stammt von Wert und deutet auf etwas hin, das sich nicht ändern kann—es ist unveränderlich. Wählen Sie wann immer möglich val anstelle von var. Das Vars.kt-Beispiel am Anfang dieses Abschnitts kann unter Verwendung von vals umgeschrieben werden:

// VarAndVal/Vals.kt

fun main() {
  val whole = 11
  // whole = 15 // Error   // [1]
  val fractional = 1.4
  val words = "Twas Brillig"
  println(whole)
  println(fractional)
  println(words)
}
/* Output:
11
1.4
Twas Brillig
*/
  • [1] Sobald Sie ein val initialisieren, können Sie es nicht neu zuweisen. Wenn wir versuchen, whole eine andere Zahl zuzuweisen, meldet sich Kotlin mit der Nachricht “Val kann nicht neu zugewiesen werden.”

Beschreibende Namen für Ihre Bezeichner zu wählen, macht Ihren Code leichter verständlich und reduziert oft die Notwendigkeit für Kommentare. In Vals.kt haben Sie keine Ahnung, was whole repräsentiert. Wenn Ihr Programm die Zahl 11 speichert, um die Tageszeit darzustellen, zu der Sie Kaffee trinken, ist es offensichtlicher für andere, wenn Sie es coffeetime nennen, und leichter zu lesen, wenn es coffeeTime ist (gemäß dem Kotlin-Stil, bei dem wir den ersten Buchstaben klein schreiben).

  • -

vars sind nützlich, wenn sich Daten während der Ausführung des Programms ändern müssen. Dies klingt nach einer häufigen Anforderung, stellt sich jedoch in der Praxis als vermeidbar heraus. Im Allgemeinen sind Ihre Programme leichter zu erweitern und zu pflegen, wenn Sie vals verwenden. In seltenen Fällen ist es jedoch zu komplex, ein Problem nur mit vals zu lösen. Aus diesem Grund bietet Ihnen Kotlin die Flexibilität von vars. Je mehr Zeit Sie jedoch mit vals verbringen, desto mehr werden Sie entdecken, dass Sie vars fast nie benötigen und dass Ihre Programme ohne sie sicherer und zuverlässiger sind.

Übungen und Lösungen finden Sie unter www.AtomicKotlin.com.

Datentypen

Daten können unterschiedliche Typen haben.

Um ein mathematisches Problem zu lösen, schreibt man einen Ausdruck:

5.9 + 6

Du weißt, dass das Addieren dieser Zahlen eine weitere Zahl ergibt. Kotlin weiß das auch. Du weißt, dass eine davon eine Dezimalzahl (5.9) ist, die Kotlin Double nennt, und die andere eine ganze Zahl (6), die Kotlin Int nennt. Du weißt, dass das Ergebnis eine Dezimalzahl ist.

Ein Typ (auch Datentyp genannt) sagt Kotlin, wie du diese Daten verwenden möchtest. Ein Typ definiert die Menge der Werte, die ein Ausdruck dieses Typs erzeugen kann. Ein Typ definiert auch die Operationen, die auf den Daten durchgeführt werden können, die Bedeutung der Daten und wie Werte dieses Typs gespeichert werden können.

Kotlin verwendet Typen, um zu überprüfen, ob deine Ausdrücke korrekt sind. Im obigen Ausdruck erstellt Kotlin einen neuen Wert des Typs Double, um das Ergebnis zu speichern.

Kotlin versucht, sich an deine Bedürfnisse anzupassen. Wenn du es bittest, etwas zu tun, das die Typregeln verletzt, erzeugt es eine Fehlermeldung. Zum Beispiel, versuche, einen String und eine Zahl zu addieren:

// DataTypes/StringPlusNumber.kt

fun main() {
  println("Sally" + 5.9)
}
/* Output:
Sally5.9
*/

Typen sagen Kotlin, wie sie korrekt verwendet werden. In diesem Fall sagen die Typregeln Kotlin, wie man eine Zahl zu einem String hinzufügt: indem die beiden Werte angehängt werden und ein String erstellt wird, um das Ergebnis zu halten.

Versuchen Sie nun, einen String und ein Double zu multiplizieren, indem Sie das + in StringPlusNumber.kt durch ein * ersetzen:

"Sally" * 5.9

Das Kombinieren von Typen auf diese Weise ergibt für Kotlin keinen Sinn, daher gibt es einen Fehler aus.

In var & val haben wir verschiedene Typen gespeichert. Kotlin hat die Typen für uns ermittelt, basierend darauf, wie wir sie verwendet haben. Dies wird type inference genannt.

Wir können ausführlicher sein und den Typ angeben:

val identifier: Type = initialization

Du beginnst mit dem Schlüsselwort val oder var, gefolgt vom Bezeichner, einem Doppelpunkt, dem Typ, einem =, und dem Initialisierungswert. Anstatt also zu sagen:

val n = 1
var p = 1.2

Du kannst sagen:

val n: Int = 1
var p: Double = 1.2

Wir haben Kotlin gesagt, dass n ein Int und p ein Double ist, anstatt es den Typ ableiten zu lassen.

Hier sind einige von Kotlins grundlegenden Typen:

// DataTypes/Types.kt

fun main() {
  val whole: Int = 11              // [1]
  val fractional: Double = 1.4     // [2]
  val trueOrFalse: Boolean = true  // [3]
  val words: String = "A value"    // [4]
  val character: Char = 'z'        // [5]
  val lines: String = """Triple quotes let
you have many lines
in your string"""                  // [6]
  println(whole)
  println(fractional)
  println(trueOrFalse)
  println(words)
  println(character)
  println(lines)
}
/* Output:
11
1.4
true
A value
z
Triple quotes let
you have many lines
in your string
*/
  • [1] Der Int-Datentyp ist ein Ganzzahltyp, was bedeutet, dass er nur ganze Zahlen speichert.
  • [2] Um Bruchzahlen zu speichern, verwenden Sie einen Double.
  • [3] Ein Boolean-Datentyp speichert nur die beiden speziellen Werte true und false.
  • [4] Ein String speichert eine Zeichenfolge. Sie weisen einen Wert mit einem doppelt-umrahmten String zu.
  • [5] Ein Char speichert ein Zeichen.
  • [6] Wenn Sie viele Zeilen und/oder Sonderzeichen haben, umgeben Sie diese mit dreifachen Anführungszeichen (dies ist ein dreifach-umrahmter String).

Kotlin verwendet Typinferenz, um die Bedeutung gemischter Typen zu bestimmen. Beim Mischen von Int und Double während der Addition entscheidet Kotlin den Typ für den resultierenden Wert:

// DataTypes/Inference.kt

fun main() {
  val n = 1 + 1.2
  println(n)
}
/* Output:
2.2
*/

Wenn Sie einen Int zu einem Double mit Typinferenz hinzufügen, bestimmt Kotlin, dass das Ergebnis n ein Double ist und stellt sicher, dass es alle Regeln für Doubles einhält.

Die Typinferenz von Kotlin ist Teil seiner Strategie, Arbeit für den Programmierer zu übernehmen. Wenn Sie die Typdeklaration weglassen, kann Kotlin sie normalerweise ableiten.

Übungen und Lösungen finden Sie unter www.AtomicKotlin.com.

Funktionen

Eine Funktion ist wie ein kleines Programm, das einen eigenen Namen hat und durch das Aufrufen dieses Namens aus einer anderen Funktion ausgeführt (aufgerufen) werden kann.

Eine Funktion fasst eine Gruppe von Aktivitäten zusammen und ist die grundlegendste Methode, um Ihre Programme zu organisieren und Code wiederzuverwenden.

Sie übergeben Informationen an eine Funktion, und die Funktion verwendet diese Informationen, um ein Ergebnis zu berechnen und zu erzeugen. Die Grundform einer Funktion ist:

fun functionName(p1: Type1, p2: Type2, ...): ReturnType {
  lines of code
  return result
}

p1 und p2 sind die Parameter: die Informationen, die Sie in die Funktion übergeben. Jeder Parameter hat einen Bezeichnernamen (p1, p2) gefolgt von einem Doppelpunkt und dem Typ dieses Parameters. Die schließende Klammer der Parameterliste wird von einem Doppelpunkt und dem Typ des von der Funktion erzeugten Ergebnisses gefolgt. Die Codezeilen im Funktionskörper sind in geschweifte Klammern eingeschlossen. Der Ausdruck nach dem Schlüsselwort return ist das Ergebnis, das die Funktion erzeugt, wenn sie abgeschlossen ist.

Ein Parameter definiert, was in eine Funktion übergeben wird — er ist der Platzhalter. Ein Argument ist der tatsächliche Wert, den Sie in die Funktion übergeben.

Die Kombination aus Name, Parametern und Rückgabetyp wird als Funktionssignatur bezeichnet.

Hier ist eine einfache Funktion namens multiplyByTwo():

// Functions/MultiplyByTwo.kt

fun multiplyByTwo(x: Int): Int {  // [1]
  println("Inside multiplyByTwo") // [2]
  return x * 2
}

fun main() {
  val r = multiplyByTwo(5)        // [3]
  println(r)
}
/* Output:
Inside multiplyByTwo
10
*/
  • [1] Beachten Sie das fun Schlüsselwort, den Funktionsnamen und die Parameterliste, die aus einem einzigen Parameter besteht. Diese Funktion nimmt einen Int Parameter und gibt einen Int zurück.
  • [2] Diese zwei Zeilen sind der Körper der Funktion. Die letzte Zeile gibt den Wert ihrer Berechnung x * 2 als Ergebnis der Funktion zurück.
  • [3] Diese Zeile ruft die Funktion mit einem geeigneten Argument auf und erfasst das Ergebnis in val r. Ein Funktionsaufruf imitiert die Form seiner Deklaration: den Funktionsnamen, gefolgt von Argumenten in Klammern.

Der Funktionscode wird durch Aufrufen der Funktion ausgeführt, wobei der Funktionsname multiplyByTwo() als Abkürzung für diesen Code dient. Aus diesem Grund sind Funktionen die grundlegendste Form der Vereinfachung und Wiederverwendung von Code in der Programmierung. Sie können auch an eine Funktion als Ausdruck mit austauschbaren Werten (den Parametern) denken.

println() ist ebenfalls ein Funktionsaufruf – er wird einfach von Kotlin bereitgestellt. Wir beziehen uns auf von Kotlin definierte Funktionen als Bibliotheksfunktionen.

Wenn die Funktion kein sinnvolles Ergebnis liefert, ist ihr Rückgabetyp Unit. Sie können Unit explizit angeben, wenn Sie möchten, aber Kotlin erlaubt es Ihnen, es wegzulassen:

// Functions/SayHello.kt

fun sayHello() {
  println("Hallo!")
}

fun sayGoodbye(): Unit {
  println("Auf Wiedersehen!")
}

fun main() {
  sayHello()
  sayGoodbye()
}
/* Output:
Hallo!
Auf Wiedersehen!
*/

Sowohl sayHello() als auch sayGoodbye() geben Unit zurück, aber sayHello() lässt die explizite Deklaration weg. Die main()-Funktion gibt ebenfalls Unit zurück.

Wenn eine Funktion nur einen einzigen Ausdruck enthält, können Sie die abgekürzte Syntax mit einem Gleichheitszeichen gefolgt von dem Ausdruck verwenden:

fun functionName(arg1: Type1, arg2: Type2, ...): ReturnType = expression

Ein Funktionskörper, der von geschweiften Klammern umgeben ist, wird als Blockkörper bezeichnet. Ein Funktionskörper, der die Gleichungssyntax verwendet, wird als Ausdruckskörper bezeichnet.

Hier verwendet multiplyByThree() einen Ausdruckskörper:

// Functions/MultiplyByThree.kt

fun multiplyByThree(x: Int): Int = x * 3

fun main() {
  println(multiplyByThree(5))
}
/* Output:
15
*/

Dies ist eine kurze Version, um return x * 3 innerhalb eines Blockkörpers zu sagen.

Kotlin leitet den Rückgabetyp einer Funktion ab, die einen Ausdruckskörper hat:

// Functions/MultiplyByFour.kt

fun multiplyByFour(x: Int) = x * 4

fun main() {
  val result: Int = multiplyByFour(5)
  println(result)
}
/* Output:
20
*/

Kotlin leitet ab, dass multiplyByFour() ein Int zurückgibt.

Kotlin kann Rückgabetypen nur für Ausdruckskörper ableiten. Wenn eine Funktion einen Blockkörper hat und Sie ihren Typ weglassen, gibt diese Funktion Unit zurück.

  • -

Beim Schreiben von Funktionen sollten Sie beschreibende Namen wählen. Dies macht den Code leichter lesbar und kann oft die Notwendigkeit für Codekommentare reduzieren. Wir können nicht immer so beschreibend sein, wie wir es uns wünschen würden, mit den Funktionsnamen in diesem Buch, weil wir durch die Zeilenbreiten eingeschränkt sind.

Übungen und Lösungen finden Sie unter www.AtomicKotlin.com.

if-Ausdrücke

Ein if-Ausdruck trifft eine Entscheidung.

Das if-Schlüsselwort prüft einen Ausdruck, um festzustellen, ob er wahr oder falsch ist, und führt basierend auf dem Ergebnis eine Aktion aus. Ein Wahr-oder-Falsch-Ausdruck wird als Boolean bezeichnet, nach dem Mathematiker George Boole, der die Logik hinter diesen Ausdrücken erfunden hat. Hier ist ein Beispiel mit den Symbolen > (größer als) und < (kleiner als):

// IfExpressions/If1.kt

fun main() {
  if (1 > 0)
    println("It's true!")
  if (10 < 11) {
    println("10 < 11")
    println("ten is less than eleven")
  }
}
/* Output:
It's true!
10 < 11
ten is less than eleven
*/

Der Ausdruck in den Klammern nach dem if muss zu true oder false ausgewertet werden. Wenn true, wird der folgende Ausdruck ausgeführt. Um mehrere Zeilen auszuführen, platziere sie in geschweiften Klammern.

Wir können einen booleschen Ausdruck an einer Stelle erstellen und an einer anderen verwenden:

// IfExpressions/If2.kt

fun main() {
  val x: Boolean = 1 >= 1
  if (x)
    println("It's true!")
}
/* Output:
It's true!
*/

Da x ein Boolean ist, kann der if-Operator es direkt testen, indem er if(x) sagt.

Der >=-Operator für Boolean gibt true zurück, wenn der Ausdruck auf der linken Seite des Operators größer oder gleich dem auf der rechten Seite ist. Ebenso gibt <= true zurück, wenn der Ausdruck auf der linken Seite kleiner oder gleich dem auf der rechten Seite ist.

Das Schlüsselwort else ermöglicht es Ihnen, sowohl true- als auch false-Pfade zu behandeln:

// IfExpressions/If3.kt

fun main() {
  val n: Int = -11
  if (n > 0)
    println("It's positive")
  else
    println("It's negative or zero")
}
/* Output:
It's negative or zero
*/

Das else-Schlüsselwort wird nur in Verbindung mit if verwendet. Sie sind nicht auf eine einzelne Prüfung beschränkt — Sie können mehrere Kombinationen testen, indem Sie else und if kombinieren:

// IfExpressions/If4.kt

fun main() {
  val n: Int = -11
  if (n > 0)
    println("It's positive")
  else if (n == 0)
    println("It's zero")
  else
    println("It's negative")
}
/* Output:
It's negative
*/

Hier verwenden wir ==, um zwei Zahlen auf Gleichheit zu prüfen. != testet auf Ungleichheit.

Das typische Muster beginnt mit if, gefolgt von so vielen else if-Klauseln, wie Sie benötigen, und endet mit einem abschließenden else für alles, was nicht zu den vorherigen Tests passt. Wenn ein if-Ausdruck eine bestimmte Größe und Komplexität erreicht, verwenden Sie wahrscheinlich stattdessen einen when-Ausdruck. when wird später im Buch beschrieben, in „when“ Ausdrücke.

Der „Nicht“-Operator ! testet das Gegenteil eines Booleschen Ausdrucks:

// IfExpressions/If5.kt

fun main() {
  val y: Boolean = false
  if (!y)
    println("!y is true")
}
/* Output:
!y is true
*/

Um if(!y) zu verbalisieren, sagt man “wenn nicht y.”

Das gesamte if ist ein Ausdruck, der ein Ergebnis liefern kann:

// IfExpressions/If6.kt

fun main() {
  val num = 10
  val result = if (num > 100) 4 else 42
  println(result)
}
/* Output:
42
*/

Hier speichern wir den Wert, der durch den gesamten if Ausdruck erzeugt wird, in einem Zwischenbezeichner namens result. Wenn die Bedingung erfüllt ist, erzeugt der erste Zweig result. Wenn nicht, wird der else Wert zu result.

Lassen Sie uns üben, Funktionen zu erstellen. Hier ist eine, die einen Booleschen Parameter nimmt:

// IfExpressions/TrueOrFalse.kt

fun trueOrFalse(exp: Boolean): String {
  if (exp)
    return "It's true!"          // [1]
  return "It's false"            // [2]
}

fun main() {
  val b = 1
  println(trueOrFalse(b < 3))
  println(trueOrFalse(b >= 3))
}
/* Output:
It's true!
It's false
*/

Der Boolean-Parameter exp wird an die Funktion trueOrFalse() übergeben. Wenn das Argument als Ausdruck übergeben wird, wie b < 3, wird dieser Ausdruck zuerst ausgewertet und das Ergebnis an die Funktion übergeben. trueOrFalse() testet exp und wenn das Ergebnis true ist, wird Zeile [1] ausgeführt, andernfalls wird Zeile [2] ausgeführt.

  • [1] return sagt: “Verlasse die Funktion und liefere diesen Wert als Ergebnis der Funktion.” Beachten Sie, dass return überall in einer Funktion erscheinen kann und nicht am Ende stehen muss.

Anstatt return wie im vorherigen Beispiel zu verwenden, können Sie das else-Schlüsselwort verwenden, um das Ergebnis als Ausdruck zu erzeugen:

// IfExpressions/OneOrTheOther.kt

fun oneOrTheOther(exp: Boolean): String =
  if (exp)
    "True!" // No 'return' necessary
  else
    "False"

fun main() {
  val x = 1
  println(oneOrTheOther(x == 1))
  println(oneOrTheOther(x == 2))
}
/* Output:
True!
False
*/

Anstelle von zwei Ausdrücken in trueOrFalse() ist oneOrTheOther() ein einzelner Ausdruck. Das Ergebnis dieses Ausdrucks ist das Ergebnis der Funktion, sodass der if-Ausdruck zum Funktionskörper wird.

Übungen und Lösungen finden Sie auf www.AtomicKotlin.com.

String-Vorlagen

Eine String-Vorlage ist eine programmatische Methode, um einen String zu erzeugen.

Wenn Sie ein $ vor einen Bezeichnernamen setzen, fügt die String-Vorlage den Inhalt dieses Bezeichners in den String ein:

// StringTemplates/StringTemplates.kt

fun main() {
  val answer = 42
  println("Found $answer!")     // [1]
  println("printing a $1")      // [2]
}
/* Output:
Found 42!
printing a $1
*/
  • [1] $answer ersetzt den Wert von answer.
  • [2] Wenn dem $ nichts folgt, das als Programmbezeichner erkennbar ist, passiert nichts Besonderes.

Man kann auch Werte in einen String einfügen, indem man die Verkettung (+) verwendet:

// StringTemplates/StringConcatenation.kt

fun main() {
  val s = "hi\n" // \n is a newline character
  val n = 11
  val d = 3.14
  println("first: " + s + "second: " +
    n + ", third: " + d)
}
/* Output:
first: hi
second: 11, third: 3.14
*/

Das Platzieren eines Ausdrucks innerhalb von ${} wertet ihn aus. Der Rückgabewert wird in einen String umgewandelt und in den resultierenden String eingefügt:

// StringTemplates/ExpressionInTemplate.kt

fun main() {
  val condition = true
  println(
    "${if (condition) 'a' else 'b'}")  // [1]
  val x = 11
  println("$x + 4 = ${x + 4}")
}
/* Output:
a
11 + 4 = 15
*/
  • [1] if(condition) 'a' else 'b' wird ausgewertet und das Ergebnis wird durch den gesamten ${} Ausdruck ersetzt.

Wenn eine Zeichenkette ein Sonderzeichen enthalten muss, wie zum Beispiel ein Anführungszeichen, können Sie entweder dieses Zeichen mit einem \ (Backslash) entkommen, oder Sie verwenden ein Zeichenkette Literal in dreifachen Anführungszeichen:

// StringTemplates/TripleQuotes.kt

fun main() {
  val s = "value"
  println("s = \"$s\".")
  println("""s = "$s".""")
}
/* Output:
s = "value".
s = "value".
*/

Mit dreifachen Anführungszeichen fügen Sie einen Wert eines Ausdrucks auf die gleiche Weise ein wie bei einem einfach-quotierten String.

Übungen und Lösungen finden Sie unter www.AtomicKotlin.com.

Zahlentypen

Verschiedene Zahlentypen werden auf unterschiedliche Weise gespeichert.

Wenn Sie einen Bezeichner erstellen und ihm einen Ganzzahlwert zuweisen, leitet Kotlin den Int-Typ ab:

// NumberTypes/InferInt.kt

fun main() {
  val million = 1_000_000  // Infers Int
  println(million)
}
/* Output:
1000000
*/

Für die Lesbarkeit erlaubt Kotlin Unterstriche innerhalb numerischer Werte.

Die grundlegenden mathematischen Operatoren für Zahlen sind diejenigen, die in den meisten Programmiersprachen verfügbar sind: Addition (+), Subtraktion (-), Division (/), Multiplikation (*) und Modulus (%), was den Rest aus der Ganzzahldivision ergibt:

// NumberTypes/Modulus.kt

fun main() {
  val numerator: Int = 19
  val denominator: Int = 10
  println(numerator % denominator)
}
/* Output:
9
*/

Ganzzahl-Division schneidet das Ergebnis ab:

// NumberTypes/IntDivisionTruncates.kt

fun main() {
  val numerator: Int = 19
  val denominator: Int = 10
  println(numerator / denominator)
}
/* Output:
1
*/

Wenn die Operation das Ergebnis gerundet hätte, wäre die Ausgabe 2.

Die Reihenfolge der Operationen folgt der grundlegenden Arithmetik:

// NumberTypes/OpOrder.kt

fun main() {
  println(45 + 5 * 6)
}
/* Output:
75
*/

Die Multiplikation 5 * 6 wird zuerst ausgeführt, gefolgt von der Addition 45 + 30.

Wenn Sie möchten, dass 45 + 5 zuerst erfolgt, verwenden Sie Klammern:

// NumberTypes/OpOrderParens.kt

fun main() {
  println((45 + 5) * 6)
}
/* Output:
300
*/

Nun berechnen wir den Body-Mass-Index (BMI), der das Gewicht in Kilogramm dividiert durch das Quadrat der Größe in Metern ist. Wenn Sie einen BMI von weniger als 18,5 haben, sind Sie untergewichtig. Zwischen 18,5 und 24,9 ist Normalgewicht. Ein BMI von 25 und höher ist Übergewicht. Dieses Beispiel zeigt auch den bevorzugten Formatierungsstil, wenn Sie die Parameter der Funktion nicht in eine einzige Zeile passen können:

// NumberTypes/BMIMetric.kt

fun bmiMetric(
  weight: Double,
  height: Double
): String {
  val bmi = weight / (height * height)  // [1]
  return if (bmi < 18.5) "Underweight"
    else if (bmi < 25) "Normal weight"
    else "Overweight"
}

fun main() {
  val weight = 72.57 // 160 lbs
  val height = 1.727 // 68 inches
  val status = bmiMetric(weight, height)
  println(status)
}
/* Output:
Normal weight
*/
  • [1] Wenn Sie die Klammern entfernen, teilen Sie weight durch height und multiplizieren dann dieses Ergebnis mit height. Das ergibt eine viel größere Zahl und ist die falsche Antwort.

bmiMetric() verwendet Doubles für das Gewicht und die Größe. Ein Double kann sehr große und sehr kleine Gleitkommazahlen aufnehmen.

Hier ist eine Version, die englische Einheiten verwendet, dargestellt durch Int-Parameter:

// NumberTypes/BMIEnglish.kt

fun bmiEnglish(
  weight: Int,
  height: Int
): String {
  val bmi =
    weight / (height * height) * 703.07 // [1]
  return if (bmi < 18.5) "Underweight"
    else if (bmi < 25) "Normal weight"
    else "Overweight"
}

fun main() {
  val weight = 160
  val height = 68
  val status = bmiEnglish(weight, height)
  println(status)
}
/* Output:
Underweight
*/

Warum unterscheidet sich das Ergebnis von bmiMetric(), das Doubles verwendet? Wenn Sie eine Ganzzahl durch eine andere Ganzzahl teilen, erzeugt Kotlin ein Ganzzahlergebnis. Die Standardmethode, um mit dem Rest während der ganzzahligen Division umzugehen, ist das Abschneiden, was bedeutet, “abschneiden und wegwerfen” (es gibt kein Runden). Wenn Sie also 5 durch 2 teilen, erhalten Sie 2, und 7/10 ist null. Wenn Kotlin bmi in Ausdruck [1] berechnet, teilt es 160 durch 68 * 68 und erhält null. Es multipliziert dann null mit 703.07, um null zu erhalten.

Um dieses Problem zu vermeiden, verschieben Sie 703.07 an den Anfang der Berechnung. Die Berechnungen werden dann gezwungen, Double zu sein:

val bmi = 703.07 * weight / (height * height)

Die Double Parameter in bmiMetric() verhindern dieses Problem. Rechnen Sie so früh wie möglich auf den gewünschten Typ um, um die Genauigkeit zu erhalten.

Alle Programmiersprachen haben Grenzen, was sie innerhalb einer Ganzzahl speichern können. Der Int Typ in Kotlin kann Werte zwischen -231 und +231-1 annehmen, eine Einschränkung der 32-Bit Darstellung von Int. Wenn Sie zwei Ints addieren oder multiplizieren, die groß genug sind, wird das Ergebnis überlaufen:

// NumberTypes/IntegerOverflow.kt

fun main() {
  val i: Int = Int.MAX_VALUE
  println(i + i)
}
/* Output:
-2
*/

Int.MAX_VALUE ist ein vordefinierter Wert, der die größte Zahl darstellt, die ein Int halten kann.

Der Überlauf erzeugt ein Ergebnis, das eindeutig falsch ist, da es sowohl negativ als auch viel kleiner ist, als wir erwarten. Kotlin gibt eine Warnung aus, wann immer es einen potenziellen Überlauf erkennt.

Es liegt in Ihrer Verantwortung als Entwickler, Überläufe zu verhindern. Kotlin kann nicht immer Überläufe während der Kompilierung erkennen, und es verhindert keine Überläufe, da dies eine untragbare Leistungseinbuße zur Folge hätte.

Wenn Ihr Programm große Zahlen enthält, können Sie Longs verwenden, die Werte von -263 bis +263-1 aufnehmen. Um ein val vom Typ Long zu definieren, können Sie den Typ explizit angeben oder ein L am Ende eines numerischen Literals hinzufügen, was Kotlin anweist, diesen Wert als Long zu behandeln:

// NumberTypes/LongConstants.kt

fun main() {
  val i = 0          // Infers Int
  val l1 = 0L        // L creates Long
  val l2: Long = 0   // Explicit type
  println("$l1 $l2")
}
/* Output:
0 0
*/

Durch die Verwendung von Longs verhindern wir den Überlauf in IntegerOverflow.kt:

// NumberTypes/UsingLongs.kt

fun main() {
  val i = Int.MAX_VALUE
  println(0L + i + i)              // [1]
  println(1_000_000 * 1_000_000L)  // [2]
}
/* Output:
4294967294
1000000000000
*/

Die Verwendung eines numerischen Literals in sowohl [1] als auch [2] erzwingt Long-Berechnungen und ergibt ebenfalls ein Ergebnis vom Typ Long. Der Ort, an dem das L erscheint, ist unwichtig. Wenn einer der Werte Long ist, ist der resultierende Ausdruck Long.

Obwohl sie viel größere Werte als Ints halten können, haben Longs immer noch Größenbeschränkungen:

// NumberTypes/BiggestLong.kt

fun main() {
  println(Long.MAX_VALUE)
}
/* Output:
9223372036854775807
*/

Long.MAX_VALUE ist der größte Wert, den ein Long halten kann.

Übungen und Lösungen finden Sie unter www.AtomicKotlin.com.

Boolesche Werte

if-Ausdrücke demonstrierten den Operator “nicht” !, der einen Boolescher Wert negiert. Dieses Kapitel führt mehr in die Boolesche Algebra ein.

Wir beginnen mit den Operatoren “und” und “oder”:

  • && (und): Erzeugt wahr nur, wenn der Boolescher Ausdruck links vom Operator und der rechts beide wahr sind.
  • || (oder): Erzeugt wahr, wenn entweder der Ausdruck links oder rechts vom Operator wahr ist, oder wenn beide wahr sind.

In diesem Beispiel bestimmen wir, ob ein Geschäft geöffnet oder geschlossen ist, basierend auf der Stunde:

// Booleans/Open1.kt

fun isOpen1(hour: Int) {
  val open = 9
  val closed = 20
  println("Operating hours: $open - $closed")
  val status =
    if (hour >= open && hour < closed) // [1]
      true
    else
      false
  println("Open: $status")
}

fun main() = isOpen1(6)
/* Output:
Operating hours: 9 - 20
Open: false
*/

main() ist ein einzelner Funktionsaufruf, daher können wir einen Ausdruckskörper verwenden, wie in Funktionen beschrieben.

Der if-Ausdruck in [1] prüft, ob hour zwischen der Öffnungszeit und der Schließzeit liegt, daher kombinieren wir die Ausdrücke mit dem Boolean && (und).

Der if-Ausdruck kann vereinfacht werden. Das Ergebnis des Ausdrucks if(cond) true else false ist einfach cond:

// Booleans/Open2.kt

fun isOpen2(hour: Int) {
  val open = 9
  val closed = 20
  println("Operating hours: $open - $closed")
  val status = hour >= open && hour < closed
  println("Open: $status")
}

fun main() = isOpen2(6)
/* Output:
Operating hours: 9 - 20
Open: false
*/

Lassen Sie uns die Logik umkehren und überprüfen, ob das Geschäft derzeit geschlossen ist. Der “or”-Operator || liefert true, wenn mindestens eine der Bedingungen erfüllt ist:

// Booleans/Closed.kt

fun isClosed(hour: Int) {
  val open = 9
  val closed = 20
  println("Operating hours: $open - $closed")
  val status = hour < open || hour >= closed
  println("Closed: $status")
}

fun main() = isClosed(6)
/* Output:
Operating hours: 9 - 20
Closed: true
*/

Boolean-Operatoren ermöglichen komplizierte Logik in kompakten Ausdrücken. Allerdings kann es leicht verwirrend werden. Streben Sie nach Lesbarkeit und spezifizieren Sie Ihre Absichten explizit.

Hier ist ein Beispiel für einen komplizierten Boolean-Ausdruck, bei dem unterschiedliche Auswertungsreihenfolgen zu unterschiedlichen Ergebnissen führen:

// Booleans/EvaluationOrder.kt

fun main() {
  val sunny = true
  val hoursSleep = 6
  val exercise = false
  val temp = 55

  // [1]:
  val happy1 = sunny && temp > 50 ||
    exercise && hoursSleep > 7
  println(happy1)

  // [2]:
  val sameHappy1 = (sunny && temp > 50) ||
    (exercise && hoursSleep > 7)
  println(sameHappy1)

  // [3]:
  val notSame =
    (sunny && temp > 50 || exercise) &&
      hoursSleep > 7
  println(notSame)
}
/* Output:
true
true
false
*/

Die Boolean-Ausdrücke sind sunny, temp > 50, exercise und hoursSleep > 7. Wir lesen happy1 als “Es ist sonnig und die Temperatur ist größer als 50 oder ich habe Sport getrieben und mehr als 7 Stunden geschlafen.” Aber hat && Vorrang vor || oder umgekehrt?

Der Ausdruck in [1] verwendet die Standardauswertungsreihenfolge von Kotlin. Dies ergibt dasselbe Ergebnis wie der Ausdruck in [2], da ohne Klammern die “und”-Operationen zuerst ausgewertet werden, dann das “oder”. Der Ausdruck in [3] verwendet Klammern, um ein anderes Ergebnis zu erzielen. In [3] sind wir nur glücklich, wenn wir mindestens 7 Stunden geschlafen haben.

Übungen und Lösungen finden Sie auf www.AtomicKotlin.com.

Wiederholung mit while

Computer sind ideal für sich wiederholende Aufgaben.

Die grundlegendste Form der Wiederholung verwendet das Schlüsselwort while. Dies wiederholt einen Block, solange der kontrollierende boolesche Ausdruck true ist:

while (Boolean-expression) {
  // Code to be repeated
}

Der boolesche Ausdruck wird einmal zu Beginn der Schleife und erneut vor jeder weiteren Iteration durch den Block ausgewertet.

// RepetitionWithWhile/WhileLoop.kt

fun condition(i: Int) = i < 100  // [1]

fun main() {
  var i = 0
  while (condition(i)) {         // [2]
    print(".")
    i += 10                      // [3]
  }
}
/* Output:
..........
*/
  • [1] Der Vergleichsoperator < liefert ein Boolescher Wert Ergebnis, daher leitet Kotlin Boolescher Wert als Ergebnistyp für condition() ab.
  • [2] Der bedingte Ausdruck für das while besagt: “Wiederhole die Anweisungen im Körper, solange condition() true zurückgibt.”
  • [3] Der += Operator addiert 10 zu i und weist das Ergebnis i in einem einzigen Vorgang zu (i muss eine var sein, damit dies funktioniert). Dies entspricht:
i = i + 10

Es gibt eine zweite Möglichkeit, while in Verbindung mit dem Schlüsselwort do zu verwenden:

do {
  // Code to be repeated
} while (Boolean-expression)

Das Umschreiben von WhileLoop.kt, um eine do-while-Schleife zu verwenden, ergibt:

// RepetitionWithWhile/DoWhileLoop.kt

fun main() {
  var i = 0
  do {
    print(".")
    i += 10
  } while (condition(i))
}
/* Output:
..........
*/

Der einzige Unterschied zwischen while und do-while besteht darin, dass der Körper von do-while immer mindestens einmal ausgeführt wird, selbst wenn der boolesche Ausdruck anfänglich false ergibt. Bei einem while wird der Körper nie ausgeführt, wenn die Bedingung beim ersten Mal false ist. In der Praxis ist do-while weniger verbreitet als while.

Die Kurzformen der Zuweisungsoperatoren sind für alle arithmetischen Operationen verfügbar: +=, -=, *=, /=, und %=. Hier werden -= und %= verwendet:

// RepetitionWithWhile/AssignmentOperators.kt

fun main() {
  var n = 10
  val d = 3
  print(n)
  while (n > d) {
    n -= d
    print(" - $d")
  }
  println(" = $n")

  var m = 10
  print(m)
  m %= d
  println(" % $d = $m")
}
/* Output:
10 - 3 - 3 - 3 = 1
10 % 3 = 1
*/

Um den Rest der ganzzahligen Division von zwei natürlichen Zahlen zu berechnen, beginnen wir mit einer while-Schleife und verwenden dann den Restoperator.

Das Hinzufügen und Subtrahieren von 1 zu einer Zahl ist so häufig, dass sie eigene Inkrement- und Dekrementoperatoren haben: ++ und --. Sie können i += 1 durch i++ ersetzen:

// RepetitionWithWhile/IncrementOperator.kt

fun main() {
  var i = 0
  while (i < 4) {
    print(".")
    i++
  }
}
/* Output:
....
*/

In der Praxis werden while-Schleifen nicht zum Iterieren über einen Bereich von Zahlen verwendet. Stattdessen wird die for-Schleife verwendet. Dies wird im nächsten Atom behandelt.

Übungen und Lösungen finden Sie unter www.AtomicKotlin.com.

Schleifen & Bereiche

Das Schlüsselwort for führt einen Codeblock für jeden Wert in einer Sequenz aus.

Die Menge der Werte kann ein Bereich von Ganzzahlen, ein String oder, wie Sie später im Buch sehen werden, eine Sammlung von Elementen sein. Das Schlüsselwort in zeigt an, dass Sie durch die Werte schreiten:

for (v in values) {
  // Do something with v
}

Jedes Mal, wenn die Schleife durchlaufen wird, erhält v das nächste Element in values.

Hier ist eine for-Schleife, die eine Aktion eine feste Anzahl von Malen wiederholt:

// LoopingAndRanges/RepeatThreeTimes.kt

fun main() {
  for (i in 1..3) {
    println("Hey $i!")
  }
}
/* Output:
Hey 1!
Hey 2!
Hey 3!
*/

Die Ausgabe zeigt, dass der index i jeden Wert im Bereich von 1 bis 3 erhält.

Ein range ist ein Intervall von Werten, das durch ein Paar von Endpunkten definiert wird. Es gibt zwei grundlegende Arten, ranges zu definieren:

// LoopingAndRanges/DefiningRanges.kt

fun main() {
  val range1 = 1..10         // [1]
  val range2 = 0 until 10    // [2]
  println(range1)
  println(range2)
}
/* Output:
1..10
0..9
*/
  • [1] Die Verwendung der ..-Syntax schließt beide Grenzen im resultierenden Bereich ein.
  • [2] until schließt das Ende aus. Die Ausgabe zeigt, dass 10 nicht Teil des Bereichs ist.

Die Anzeige eines Bereichs erzeugt ein lesbares Format.

Dies summiert die Zahlen von 10 bis 100:

// LoopingAndRanges/SumUsingRange.kt

fun main() {
  var sum = 0
  for (n in 10..100) {
    sum += n
  }
  println("sum = $sum")
}
/* Output:
sum = 5005
*/

Sie können über einen Bereich in umgekehrter Reihenfolge iterieren. Sie können auch einen Schritt-Wert verwenden, um das Intervall vom Standardwert 1 zu ändern:

// LoopingAndRanges/ForWithRanges.kt

fun showRange(r: IntProgression) {
  for (i in r) {
    print("$i ")
  }
  print("    // $r")
  println()
}

fun main() {
  showRange(1..5)
  showRange(0 until 5)
  showRange(5 downTo 1)          // [1]
  showRange(0..9 step 2)         // [2]
  showRange(0 until 10 step 3)   // [3]
  showRange(9 downTo 2 step 3)
}
/* Output:
1 2 3 4 5     // 1..5
0 1 2 3 4     // 0..4
5 4 3 2 1     // 5 downTo 1 step 1
0 2 4 6 8     // 0..8 step 2
0 3 6 9     // 0..9 step 3
9 6 3     // 9 downTo 3 step 3
*/
  • [1] downTo erzeugt einen absteigenden Bereich.
  • [2] step ändert das Intervall. Hier wird der Bereich in Schritten von zwei statt einem durchlaufen.
  • [3] until kann auch mit step verwendet werden. Beachten Sie, wie sich dies auf die Ausgabe auswirkt.

In jedem Fall bilden die Zahlenfolgen eine arithmetische Folge. showRange() akzeptiert einen IntProgression-Parameter, der ein eingebauter Typ ist, der Int-Bereiche beinhaltet. Beachten Sie, dass die String-Darstellung jeder IntProgression, wie sie im Ausgabekommentar für jede Zeile erscheint, oft anders ist als der Bereich, der in showRange() übergeben wird—der IntProgression übersetzt die Eingabe in eine gleichwertige gemeinsame Form.

Sie können auch einen Bereich von Zeichen erzeugen. Diese for-Schleife iteriert von a bis z:

// LoopingAndRanges/ForWithCharRange.kt

fun main() {
  for (c in 'a'..'z') {
    print(c)
  }
}
/* Output:
abcdefghijklmnopqrstuvwxyz
*/

Sie können über einen Bereich von Elementen iterieren, die ganze Mengen sind, wie Ganzzahlen und Zeichen, aber nicht Gleitkommawerte.

Eckige Klammern greifen über den Index auf Zeichen zu. Da wir mit dem Zählen der Zeichen in einem String bei Null beginnen, wählt s[0] das erste Zeichen des String s aus. Die Auswahl von s.lastIndex ergibt die letzte Indexnummer:

// LoopingAndRanges/IndexIntoString.kt

fun main() {
  val s = "abc"
  for (i in 0..s.lastIndex) {
    print(s[i] + 1)
  }
}
/* Output:
bcd
*/

Manchmal beschreiben Leute s[0] als “das nullte Zeichen.”

Zeichen werden als Zahlen gespeichert, die ihren Unicode-Werten entsprechen. Daher ergibt das Hinzufügen einer Ganzzahl zu einem Zeichen ein neues Zeichen, das dem neuen Codewert entspricht:

// LoopingAndRanges/AddingIntToChar.kt

fun main() {
  val ch: Char = 'a'
  println(ch + 25)
  println(ch < 'z')
}
/* Output:
z
true
*/

Die zweite println() zeigt, dass man Zeichencodes vergleichen kann.

Eine for-Schleife kann direkt über Strings iterieren:

// LoopingAndRanges/IterateOverString.kt

fun main() {
  for (ch in "Jnskhm ") {
    print(ch + 1)
  }
}
/* Output:
Kotlin!
*/

ch empfängt nacheinander jedes Zeichen.

Im folgenden Beispiel durchläuft die Funktion hasChar() den String s und prüft, ob er ein bestimmtes Zeichen ch enthält. Das return in der Mitte der Funktion stoppt die Funktion, sobald die Antwort gefunden wird:

// LoopingAndRanges/HasChar.kt

fun hasChar(s: String, ch: Char): Boolean {
  for (c in s) {
    if (c == ch) return true
  }
  return false
}

fun main() {
  println(hasChar("kotlin", 't'))
  println(hasChar("kotlin", 'a'))
}
/* Output:
true
false
*/

Das nächste Atom zeigt, dass hasChar() unnötig ist — Sie können stattdessen die eingebaute Syntax verwenden.

Wenn Sie einfach eine Aktion eine feste Anzahl von Malen wiederholen möchten, können Sie repeat() anstelle einer for-Schleife verwenden:

// LoopingAndRanges/RepeatHi.kt

fun main() {
  repeat(2) {
    println("hi!")
  }
}
/* Output:
hi!
hi!
*/

repeat() ist eine Standardbibliotheksfunktion, kein Schlüsselwort. Sie werden viel später im Buch sehen, wie sie erstellt wurde.

Übungen und Lösungen finden Sie unter www.AtomicKotlin.com.

Das in Schlüsselwort

Das in Schlüsselwort prüft, ob ein Wert innerhalb eines Bereichs liegt.

// InKeyword/MembershipInRange.kt

fun main() {
  val percent = 35
  println(percent in 1..100)
}
/* Output:
true
*/

In Booleans haben Sie gelernt, Grenzen explizit zu überprüfen:

// InKeyword/MembershipUsingBounds.kt

fun main() {
  val percent = 35
  println(0 <= percent && percent <= 100)
}
/* Output:
true
*/

0 <= x && x <= 100 ist logisch gleichwertig zu x in 0..100. IntelliJ IDEA schlägt vor, die erste Form automatisch durch die zweite zu ersetzen, da diese einfacher zu lesen und zu verstehen ist.

Das Schlüsselwort in wird sowohl für Iteration als auch für Mitgliedschaft verwendet. Ein in innerhalb des Steuerungsausdrucks einer for-Schleife bedeutet Iteration, andernfalls prüft in die Mitgliedschaft:

// InKeyword/IterationVsMembership.kt

fun main() {
  val values = 1..3
  for (v in values) {
    println("iteration $v")
  }
  val v = 2
  if (v in values)
    println("$v is a member of $values")
}
/* Output:
iteration 1
iteration 2
iteration 3
2 is a member of 1..3
*/

Das in Schlüsselwort ist nicht nur auf Bereiche beschränkt. Sie können auch überprüfen, ob ein Zeichen Teil eines String ist. Das folgende Beispiel verwendet in anstelle von hasChar() aus dem vorherigen Atom:

// InKeyword/InString.kt

fun main() {
  println('t' in "kotlin")
  println('a' in "kotlin")
}
/* Output:
true
false
*/

Später im Buch wirst du sehen, dass in auch mit anderen Typen funktioniert.

Hier prüft in, ob ein Zeichen zu einem Bereich von Zeichen gehört:

// InKeyword/CharRange.kt

fun isDigit(ch: Char) = ch in '0'..'9'

fun notDigit(ch: Char) =
  ch !in '0'..'9'               // [1]

fun main() {
  println(isDigit('a'))
  println(isDigit('5'))
  println(notDigit('z'))
}
/* Output:
false
true
true
*/
  • [1] !in prüft, dass ein Wert nicht zu einem Bereich gehört.

Sie können einen Double-Bereich erstellen, aber Sie können ihn nur verwenden, um die Zugehörigkeit zu überprüfen:

// InKeyword/FloatingPointRange.kt

fun inFloatRange(n: Double) {
  val r = 1.0..10.0
  println("$n in $r? ${n in r}")
}

fun main() {
  inFloatRange(0.999999)
  inFloatRange(5.0)
  inFloatRange(10.0)
  inFloatRange(10.0000001)
}
/* Output:
0.999999 in 1.0..10.0? false
5.0 in 1.0..10.0? true
10.0 in 1.0..10.0? true
10.0000001 in 1.0..10.0? false
*/

Sie können nur .. verwenden, um einen Gleitkomma-Bereich in Kotlin zu definieren.

Sie können überprüfen, ob ein String ein Mitglied eines Bereichs von Strings ist:

// InKeyword/StringRange.kt

fun main() {
  println("ab" in "aa".."az")
  println("ba" in "aa".."az")
}
/* Output:
true
false
*/

Hier verwendet Kotlin den alphabetischen Vergleich.

Übungen und Lösungen finden Sie unter www.AtomicKotlin.com.

Ausdrücke & Anweisungen

Anweisungen und Ausdrücke sind die kleinsten nützlichen Codefragmente in den meisten Programmiersprachen.

Es gibt einen grundlegenden Unterschied: Eine Anweisung hat eine Wirkung, erzeugt jedoch kein Ergebnis. Ein Ausdruck erzeugt immer ein Ergebnis.

Da eine Anweisung kein Ergebnis erzeugt, muss sie den Zustand ihrer Umgebung ändern, um nützlich zu sein. Anders gesagt: “Eine Anweisung wird wegen ihrer Seiteneffekte aufgerufen” (das heißt, was sie andere als ein Ergebnis zu erzeugen tut). Als Merkhilfe:

Eine Anweisung ändert den Zustand.

Eine Definition von “ausdrücken” ist “herauspressen”, wie in “den Saft aus einer Orange ausdrücken”. Also

Ein Ausdruck drückt aus.

Das heißt, er erzeugt ein Ergebnis.

Die for-Schleife ist eine Anweisung in Kotlin. Sie kann nicht zugewiesen werden, da es kein Ergebnis gibt:

// ExpressionsStatements/ForIsAStatement.kt

fun main() {
  // Can't do this:
  // val f = for(i in 1..10) {}
  // Compiler error message:
  // for is not an expression, and
  // only expressions are allowed here
}

Eine for-Schleife wird aufgrund ihrer Nebeneffekte verwendet.

Ein Ausdruck erzeugt einen Wert, der zugewiesen oder als Teil eines anderen Ausdrucks verwendet werden kann, während eine Anweisung immer ein Top-Level-Element ist.

Jeder Funktionsaufruf ist ein Ausdruck. Selbst wenn die Funktion Unit zurückgibt und nur aufgrund ihrer Nebeneffekte aufgerufen wird, kann das Ergebnis dennoch zugewiesen werden:

// ExpressionsStatements/UnitReturnType.kt

fun unitFun() = Unit

fun main() {
  println(unitFun())
  val u1: Unit = println(42)
  println(u1)
  val u2 = println(0) // Type inference
  println(u2)
}
/* Output:
kotlin.Unit
42
kotlin.Unit
0
kotlin.Unit
*/

Der Unit-Typ enthält einen einzelnen Wert namens Unit, den Sie direkt zurückgeben können, wie in unitFun() zu sehen ist. Der Aufruf von println() gibt ebenfalls Unit zurück. Das val u1 erfasst den Rückgabewert von println() und ist explizit als Unit deklariert, während u2 Typinferenz verwendet.

if erzeugt einen Ausdruck, sodass Sie dessen Ergebnis zuweisen können:

// ExpressionsStatements/AssigningAnIf.kt

fun main() {
  val result1 = if (11 > 42) 9 else 5

  val result2 = if (1 < 2) {
    val a = 11
    a + 42
  } else 42

  val result3 =
    if ('x' < 'y')
      println("x < y")
    else
      println("x > y")

  println(result1)
  println(result2)
  println(result3)
}
/* Output:
x < y
5
53
kotlin.Unit
*/

Die erste Ausgabelinie ist x < y, obwohl result3 erst am Ende von main() angezeigt wird. Dies geschieht, weil die Auswertung von result3 println() aufruft und die Auswertung erfolgt, wenn result3 definiert wird.

Beachten Sie, dass a innerhalb des Codeblocks für result2 definiert ist. Das Ergebnis des letzten Ausdrucks wird zum Ergebnis des if-Ausdrucks; hier ist es die Summe von 11 und 42. Aber was ist mit a? Sobald Sie den Codeblock verlassen (außerhalb der geschweiften Klammern gehen), können Sie nicht mehr auf a zugreifen. Es ist vorübergehend und wird verworfen, sobald Sie den Gültigkeitsbereich dieses Blocks verlassen.

Der Inkrementoperator i++ ist auch ein Ausdruck, selbst wenn er wie eine Anweisung aussieht. Kotlin folgt dem Ansatz von C-ähnlichen Sprachen und bietet zwei Versionen von Inkrement- und Dekrementoperatoren mit leicht unterschiedlichen Semantiken. Der Präfix-Operator erscheint vor dem Operanden, wie in ++i, und gibt den Wert zurück, nachdem das Inkrement erfolgt ist. Sie können es lesen als “zuerst das Inkrement durchführen, dann den resultierenden Wert zurückgeben”. Der Postfix-Operator wird nach dem Operanden platziert, wie in i++, und gibt den Wert von i zurück, bevor das Inkrement erfolgt. Sie können es lesen als “zuerst das Ergebnis erzeugen, dann das Inkrement durchführen”.

// ExpressionsStatements/PostfixVsPrefix.kt

fun main() {
  var i = 10
  println(i++)
  println(i)
  var j = 20
  println(++j)
  println(j)
}
/* Output:
10
11
21
21
*/

Der Dekrementoperator hat auch zwei Versionen: --i und i--. Die Verwendung von Inkrement- und Dekrementoperatoren innerhalb anderer Ausdrücke wird nicht empfohlen, da dies zu verwirrendem Code führen kann:

// ExpressionsStatements/Confusing.kt

fun main() {
  var i = 1
  println(i++ + ++i)
}

Versuchen Sie zu erraten, was die Ausgabe sein wird, und überprüfen Sie es dann.

Übungen und Lösungen finden Sie auf www.AtomicKotlin.com.

Zusammenfassung 1

Dieses Atom fasst die Atome in Abschnitt I zusammen und überprüft sie, beginnend bei Hallo, Welt! und endend mit Ausdrücke & Anweisungen.

Wenn Sie ein erfahrener Programmierer sind, sollte dies Ihr erstes Atom sein. Neue Programmierer sollten dieses Atom lesen und die Übungen als Überprüfung von Abschnitt I durchführen.

Wenn Ihnen etwas unklar ist, studieren Sie das zugehörige Atom zu diesem Thema (die Unterüberschriften entsprechen den Atomtiteln).

Hallo, Welt

Kotlin unterstützt sowohl // Einzelzeilenkommentare als auch /*-bis-*/ Mehrzeilenkommentare. Der Einstiegspunkt eines Programms ist die Funktion main():

// Summary1/Hello.kt

fun main() {
  println("Hello, world!")
}
/* Output:
Hello, world!
*/

Die erste Zeile jedes Beispiels in diesem Buch ist ein Kommentar, der den Namen des Unterverzeichnisses des Atoms enthält, gefolgt von einem / und dem Dateinamen. Alle extrahierten Code-Beispiele finden Sie unter AtomicKotlin.com.

println() ist eine Standardbibliotheksfunktion, die einen einzelnen String-Parameter (oder einen Parameter, der in einen String konvertiert werden kann) nimmt. println() bewegt den Cursor nach der Ausgabe seines Parameters in eine neue Zeile, während print() den Cursor in derselben Zeile lässt.

Kotlin erfordert kein Semikolon am Ende eines Ausdrucks oder einer Anweisung. Semikolons sind nur notwendig, um mehrere Ausdrücke oder Anweisungen in einer einzigen Zeile zu trennen.

var & val, Datentypen

Um einen unveränderlichen Bezeichner zu erstellen, verwenden Sie das Schlüsselwort val, gefolgt vom Bezeichnernamen, einem Doppelpunkt und dem Typ für diesen Wert. Fügen Sie dann ein Gleichheitszeichen und den Wert hinzu, der diesem val zugewiesen werden soll:

val identifier: Type = initialization

Sobald einem val ein Wert zugewiesen wurde, kann er nicht neu zugewiesen werden.

Kotlons Typinferenz kann normalerweise den Typ automatisch bestimmen, basierend auf dem Initialisierungswert. Dies führt zu einer einfacheren Definition:

val identifier = initialization

Beide der folgenden sind gültig:

val daysInFebruary = 28
val daysInMarch: Int = 31

Eine var (Variable) Definition sieht gleich aus, indem var anstelle von val verwendet wird:

var identifier1 = initialization
var identifier2: Type = initialization

Im Gegensatz zu einem val können Sie ein var ändern, daher ist Folgendes zulässig:

var hoursSpent = 20
hoursSpent = 25

Allerdings kann der Typ nicht geändert werden, sodass Sie einen Fehler erhalten, wenn Sie sagen:

hoursSpent = 30.5

Kotlin leitet den Int-Typ ab, wenn hoursSpent definiert wird, daher wird es die Änderung in einen Gleitkommawert nicht akzeptieren.

Funktionen

Funktionen sind benannte Unterprogramme:

fun functionName(arg1: Type1, arg2: Type2, ...): ReturnType {
  // Lines of code ...
  return result
}

Das Schlüsselwort fun wird gefolgt vom Funktionsnamen und der Parameterliste in Klammern. Jeder Parameter muss einen expliziten Typ haben, da Kotlin die Parameterarten nicht ableiten kann. Die Funktion selbst hat einen Typ, der auf die gleiche Weise definiert wird wie bei var oder val (ein Doppelpunkt gefolgt vom Typ). Der Typ der Funktion ist der Typ des zurückgegebenen Ergebnisses.

Die Funktionssignatur wird gefolgt vom Funktionskörper, der in geschweiften Klammern enthalten ist. Die return-Anweisung liefert den Rückgabewert der Funktion.

Sie können eine abgekürzte Syntax verwenden, wenn die Funktion aus einem einzelnen Ausdruck besteht:

fun functionName(arg1: Type1, arg2: Type2, ...): ReturnType = result

Diese Form wird als Ausdruckskörper bezeichnet. Anstelle einer öffnenden geschweiften Klammer verwenden Sie ein Gleichheitszeichen gefolgt vom Ausdruck. Sie können den Rückgabetyp weglassen, weil Kotlin ihn ableitet.

Hier ist eine Funktion, die den Würfel ihres Parameters produziert, und eine andere, die ein Ausrufezeichen zu einem String hinzufügt:

// Summary1/BasicFunctions.kt

fun cube(x: Int): Int {
  return x * x * x
}

fun bang(s: String) = s + "!"

fun main() {
  println(cube(3))
  println(bang("pop"))
}
/* Output:
27
pop!
*/

cube() hat einen Blockkörper mit einer expliziten return-Anweisung. bang() ist ein Ausdruckskörper, der den Rückgabewert der Funktion erzeugt. Kotlin leitet den Rückgabetyp von bang() als String ab.

Boolesche Werte

Für die Boolesche Algebra bietet Kotlin Operatoren wie:

  • ! (nicht) negiert den Wert logisch (wandelt true in false und umgekehrt).
  • && (und) gibt true nur zurück, wenn beide Bedingungen true sind.
  • || (oder) gibt true zurück, wenn mindestens eine der Bedingungen true ist.
// Summary1/Booleans.kt

fun main() {
  val opens = 9
  val closes = 20
  println("Operating hours: $opens - $closes")
  val hour = 6
  println("Current time: " + hour)

  val isOpen = hour >= opens && hour < closes
  println("Open: " + isOpen)
  println("Not open: " + !isOpen)

  val isClosed = hour < opens || hour >= closes
  println("Closed: " + isClosed)
}
/* Output:
Operating hours: 9 - 20
Current time: 6
Open: false
Not open: true
Closed: true
*/

Der Initialisierer von isOpen verwendet &&, um zu testen, ob beide Bedingungen true sind. Die erste Bedingung hour >= opens ist false, sodass das Ergebnis des gesamten Ausdrucks false wird. Der Initialisierer für isClosed verwendet ||, was true ergibt, wenn mindestens eine der Bedingungen true ist. Der Ausdruck hour < opens ist true, daher ist der gesamte Ausdruck true.

if-Ausdrücke

Da if ein Ausdruck ist, liefert es ein Ergebnis. Dieses Ergebnis kann einer var oder val zugewiesen werden. Hier sehen Sie auch die Verwendung des Schlüsselworts else:

// Summary1/IfResult.kt

fun main() {
  val result = if (99 < 100) 4 else 42
  println(result)
}
/* Output:
4
*/

Entweder Zweig eines if-Ausdrucks kann ein mehrzeiliger Codeblock sein, der von geschweiften Klammern umgeben ist:

// Summary1/IfExpression.kt

fun main() {
  val activity = "swimming"
  val hour = 10

  val isOpen = if (
    activity == "swimming" ||
    activity == "ice skating") {
    val opens = 9
    val closes = 20
    println("Operating hours: " +
      opens + " - " + closes)
    hour >= opens && hour < closes
  } else {
    false
  }
  println(isOpen)
}
/* Output:
Operating hours: 9 - 20
true
*/

Ein Wert, der innerhalb eines Codeblocks definiert ist, wie opens, ist außerhalb des Gültigkeitsbereichs dieses Blocks nicht zugänglich. Da sie global für den if-Ausdruck definiert sind, sind activity und hour innerhalb des if-Ausdrucks zugänglich.

Das Ergebnis eines if-Ausdrucks ist das Ergebnis des letzten Ausdrucks des gewählten Zweigs. Hier ist es hour >= opens && hour <= closes, was true ist.

String-Vorlagen

Sie können einen Wert innerhalb eines String mit Hilfe von String-Vorlagen einfügen. Verwenden Sie ein $ vor dem Bezeichnernamen:

// Summary1/StrTemplates.kt

fun main() {
  val answer = 42
  println("Found $answer!")            // [1]
  val condition = true
  println(
    "${if (condition) 'a' else 'b'}")  // [2]
  println("printing a $1")             // [3]
}
/* Output:
Found 42!
a
printing a $1
*/
  • [1] $answer ersetzt den Wert, der in answer enthalten ist.
  • [2] ${if(condition) 'a' else 'b'} wertet den Ausdruck innerhalb von ${} aus und ersetzt das Ergebnis.
  • [3] Wenn dem $ etwas folgt, das nicht als Programmkennzeichner erkennbar ist, passiert nichts Besonderes.

Verwenden Sie dreifach-angeführte Strings, um mehrzeiligen Text oder Text mit Sonderzeichen zu speichern:

// Summary1/ThreeQuotes.kt

fun json(q: String, a: Int) = """{
  "question" : "$q",
  "answer" : $a
}"""

fun main() {
  println(json("The Ultimate", 42))
}
/* Output:
{
  "question" : "The Ultimate",
  "answer" : 42
}
*/

Sie müssen keine Sonderzeichen wie " innerhalb eines dreifach-umrahmten String maskieren. (In einem regulären String schreiben Sie \", um ein Anführungszeichen einzufügen). Wie bei normalen Strings können Sie einen Bezeichner oder einen Ausdruck mit $ innerhalb eines dreifach-umrahmten String einfügen.

Zahlentypen

Kotlin bietet Ganzzahltypen (Int, Long) und Fließkommatypen (Double). Eine Ganzzahlenkonstante ist standardmäßig Int und Long, wenn Sie ein L anhängen. Eine Konstante ist Double, wenn sie einen Dezimalpunkt enthält:

// Summary1/NumberTypes.kt

fun main() {
  val n = 1000    // Int
  val l = 1000L   // Long
  val d = 1000.0  // Double
  println("$n $l $d")
}
/* Output:
1000 1000 1000.0
*/

Ein Int hält Werte zwischen -231 und +231-1. Ganzzahlenwerte können einen Überlauf verursachen; zum Beispiel verursacht das Hinzufügen von irgendetwas zu Int.MAX_VALUE einen Überlauf:

// Summary1/Overflow.kt

fun main() {
  println(Int.MAX_VALUE + 1)
  println(Int.MAX_VALUE + 1L)
}
/* Output:
-2147483648
2147483648
*/

Im zweiten println()-Statement fügen wir L zu 1 hinzu, wodurch der gesamte Ausdruck vom Typ Long wird, was den Überlauf verhindert. (Ein Long kann Werte zwischen -263 und +263-1 aufnehmen).

Wenn Sie ein Int durch ein anderes Int teilen, erzeugt Kotlin ein Int-Ergebnis, und ein verbleibender Rest wird abgeschnitten. Also ergibt 1/2 0. Wenn ein Double beteiligt ist, wird das Int vor der Operation zu Double hochgestuft, sodass 1.0/2 0.5 ergibt.

Man könnte erwarten, dass d1 im Folgenden 3.4 ergibt:

// Summary1/Truncation.kt

fun main() {
  val d1: Double = 3.0 + 2 / 5
  println(d1)
  val d2: Double = 3 + 2.0 / 5
  println(d2)
}
/* Output:
3.0
3.4
*/

Aufgrund der Auswertungsreihenfolge tut es das nicht. Kotlin teilt zuerst 2 durch 5, und Ganzzahlmathematik ergibt 0, was zu einem Ergebnis von 3.0 führt. Dieselbe Auswertungsreihenfolge erzeugt das erwartete Ergebnis für d2. Die Division von 2.0 durch 5 ergibt 0.4. Die 3 wird zu einem Double hochgestuft, weil wir es zu einem Double (0.4) addieren, was 3.4 ergibt.

Das Verständnis der Auswertungsreihenfolge hilft Ihnen zu entschlüsseln, was ein Programm macht, sowohl mit logischen Operationen (Boolean-Ausdrücken) als auch mit mathematischen Operationen. Wenn Sie sich über die Auswertungsreihenfolge unsicher sind, verwenden Sie Klammern, um Ihre Absicht zu erzwingen. Dies macht es auch für diejenigen, die Ihren Code lesen, klar.

Wiederholung mit while

Eine while-Schleife läuft weiter, solange der kontrollierende Boolean-Ausdruck true ergibt:

while (Boolean-expression) {
  // Code to be repeated
}

Der Boolean expression wird einmal zu Beginn der Schleife und erneut vor jeder weiteren Iteration ausgewertet.

// Summary1/While.kt

fun testCondition(i: Int) = i < 100

fun main() {
  var i = 0
  while (testCondition(i)) {
    print(".")
    i += 10
  }
}
/* Output:
..........
*/

Kotlin leitet Boolean als Ergebnistyp für testCondition() ab.

Die Kurzformen der Zuweisungsoperatoren sind für alle mathematischen Operationen verfügbar (+=, -=, *=, /=, %=). Kotlin unterstützt auch die Inkrement- und Dekrementoperatoren ++ und --, sowohl in Präfix- als auch in Postfix-Form.

while kann mit dem Schlüsselwort do verwendet werden:

do {
  // Code to be repeated
} while (Boolean-expression)

Umschreiben von While.kt:

// Summary1/DoWhile.kt

fun main() {
  var i = 0
  do {
    print(".")
    i += 10
  } while (testCondition(i))
}
/* Output:
..........
*/

Der einzige Unterschied zwischen while und do-while besteht darin, dass der Körper von do-while immer mindestens einmal ausgeführt wird, selbst wenn der boolesche Ausdruck beim ersten Mal false ergibt.

Schleifen & Bereiche

Viele Programmiersprachen greifen auf ein iterierbares Objekt zu, indem sie durch ganze Zahlen gehen. Kotlins for erlaubt es Ihnen, Elemente direkt aus iterierbaren Objekten wie Bereichen und Strings zu entnehmen. Zum Beispiel wählt diese for-Schleife jedes Zeichen in der Zeichenkette "Kotlin" aus:

// Summary1/StringIteration.kt

fun main() {
  for (c in "Kotlin") {
    print("$c ")
    // c += 1 // error:
    // val cannot be reassigned
  }
}
/* Output:
K o t l i n
*/

c kann nicht explizit als entweder var oder val definiert werden—Kotlin macht es automatisch zu einem val und leitet seinen Typ als Char ab (man kann den Typ explizit angeben, aber in der Praxis wird dies selten getan).

Sie können durch ganze Zahlenwerte mit Bereichen iterieren:

// Summary1/RangeOfInt.kt

fun main() {
  for (i in 1..10) {
    print("$i ")
  }
}
/* Output:
1 2 3 4 5 6 7 8 9 10
*/

Einen Bereich mit .. zu erstellen, schließt beide Grenzen ein, aber until schließt das obere Ende aus: 1 bis 10 ist dasselbe wie 1..9. Sie können einen Inkrementwert mit step angeben: 1..21 Schritt 3.

Das in Schlüsselwort

Dasselbe in, das die Iteration in for Schleifen ermöglicht, erlaubt es Ihnen auch, die Zugehörigkeit zu einem Bereich zu überprüfen. !in gibt true zurück, wenn der getestete Wert nicht im Bereich liegt:

// Summary1/Membership.kt

fun inNumRange(n: Int) = n in 50..100

fun notLowerCase(ch: Char) = ch !in 'a'..'z'

fun main() {
  val i1 = 11
  val i2 = 100
  val c1 = 'K'
  val c2 = 'k'
  println("$i1 ${inNumRange(i1)}")
  println("$i2 ${inNumRange(i2)}")
  println("$c1 ${notLowerCase(c1)}")
  println("$c2 ${notLowerCase(c2)}")
}
/* Output:
11 false
100 true
K true
k false
*/

in kann auch verwendet werden, um die Zugehörigkeit zu Gleitkomma-Bereichen zu testen, obwohl solche Bereiche nur mit .. und nicht mit until definiert werden können.

Ausdrücke & Statements

Das kleinste nützliche Codefragment in den meisten Programmiersprachen ist entweder ein Statement oder ein Ausdruck. Diese haben einen grundlegenden Unterschied:

  • Ein Statement ändert den Zustand.
  • Ein Ausdruck drückt aus.

Das heißt, ein Ausdruck liefert ein Ergebnis, während ein Statement das nicht tut. Weil es nichts zurückgibt, muss ein Statement den Zustand seiner Umgebung ändern (das heißt, einen Seiteneffekt erzeugen), um etwas Nützliches zu tun.

Fast alles in Kotlin ist ein Ausdruck:

val hours = 10
val minutesPerHour = 60
val minutes = hours * minutesPerHour

In jedem Fall ist alles rechts vom = ein Ausdruck, der ein Ergebnis liefert, das der Bezeichnung links zugewiesen wird.

Funktionen wie println() scheinen kein Ergebnis zu erzeugen, aber da sie immer noch Ausdrücke sind, müssen sie etwas zurückgeben. Kotlin hat dafür einen speziellen Typ namens Unit:

// Summary1/UnitReturn.kt

fun main() {
  val result = println("returns Unit")
  println(result)
}
/* Output:
returns Unit
kotlin.Unit
*/

Erfahrene Programmierer sollten nach dem Bearbeiten der Übungen für dieses Atom zu Zusammenfassung 2 gehen.

Übungen und Lösungen finden Sie unter www.AtomicKotlin.com.