Unser Unternehmen setzt Kotlin seit mehr als zwei Jahren in der Produktion ein. Persönlich bin ich vor ungefähr einem Jahr auf diese Sprache gestoßen. Es gibt viele Themen, über die gesprochen werden muss, aber heute werden wir über die Fehlerbehandlung sprechen, auch in einem funktionalen Stil. Ich werde Ihnen sagen, wie das in Kotlin geht.
(Foto vom Treffen zu diesem Thema, das im Büro eines der Taganrog-Unternehmen stattfand. Alexey Shafranov, der Leiter der Arbeitsgruppe (Java) bei Maxilekt, sprach)Wie können Sie grundsätzlich mit Fehlern umgehen?
Ich habe verschiedene Wege gefunden:
- Sie können einen Rückgabewert als Zeiger auf die Tatsache verwenden, dass ein Fehler vorliegt.
- Sie können den Indikatorparameter für denselben Zweck verwenden.
- Geben Sie eine globale Variable ein
- Ausnahmen behandeln
- Verträge hinzufügen (DbC) .
Lassen Sie uns näher auf jede der Optionen eingehen.
Rückgabewert
Ein bestimmter "magischer" Wert wird zurückgegeben, wenn ein Fehler auftritt. Wenn Sie jemals Skriptsprachen verwendet haben, müssen Sie ähnliche Konstrukte gesehen haben.
Beispiel 1:
function sqrt(x) { if(x < 0) return -1; else return √x; }
Beispiel 2:
function getUser(id) { result = db.getUserById(id) if (result) return result as User else return “Can't find user ” + id }
Indikatorparameter
Ein bestimmter Parameter, der an die Funktion übergeben wird, wird verwendet. Nachdem Sie den Wert über den Parameter zurückgegeben haben, können Sie feststellen, ob in der Funktion ein Fehler aufgetreten ist.
Ein Beispiel:
function divide(x,y,out Success) { if (y == 0) Success = false else Success = true return x/y } divide(10, 11, Success) id (!Success) //handle error
Globale Variable
Die globale Variable funktioniert ungefähr genauso.
Ein Beispiel:
global Success = true function divide(x,y) { if (y == 0) Success = false else return x/y } divide(10, 11, Success) id (!Success) //handle error
Ausnahmen
Wir sind alle an Ausnahmen gewöhnt. Sie werden fast überall eingesetzt.
Ein Beispiel:
function divide(x,y) { if (y == 0) throw Exception() else return x/y } try{ divide(10, 0)} catch (e) {//handle exception}
Verträge (DbC)
Ehrlich gesagt habe ich diesen Ansatz noch nie live gesehen. Durch langes Googeln stellte ich fest, dass Kotlin 1.3 eine Bibliothek hat, die tatsächlich die Verwendung von Verträgen ermöglicht. Das heißt, Sie können Bedingungen für Variablen festlegen, die an die Funktion übergeben werden, Bedingungen für den Rückgabewert, die Anzahl der Aufrufe, von denen aus sie aufgerufen werden usw. Und wenn alle Bedingungen erfüllt sind, wird angenommen, dass die Funktion korrekt funktioniert hat.
Ein Beispiel:
function sqrt (x) pre-condition (x >= 0) post-condition (return >= 0) begin calculate sqrt from x end
Ehrlich gesagt hat diese Bibliothek eine schreckliche Syntax. Vielleicht habe ich so etwas deshalb nicht live gesehen.
Ausnahmen in Java
Kommen wir zu Java und wie alles von Anfang an funktioniert hat.

Beim Entwerfen einer Sprache wurden zwei Arten von Ausnahmen festgelegt:
- geprüft - geprüft;
- deaktiviert - deaktiviert.
Wofür sind geprüfte Ausnahmen? Theoretisch werden sie benötigt, damit die Leute nach Fehlern suchen müssen. Das heißt, Wenn eine bestimmte geprüfte Ausnahme möglich ist, muss sie später überprüft werden. Theoretisch hätte dieser Ansatz dazu führen müssen, dass keine unverarbeiteten Fehler auftreten und die Codequalität verbessert wird. In der Praxis ist dies jedoch nicht der Fall. Ich denke, jeder hat mindestens einmal in seinem Leben einen leeren Fangblock gesehen.
Warum kann das schlecht sein?
Hier ist ein klassisches Beispiel direkt aus der Kotlin-Dokumentation - eine Schnittstelle aus dem in StringBuilder implementierten JDK:
Appendable append(CharSequence csq) throws IOException; try { log.append(message) } catch (IOException e) { //Must be safe }
Ich bin sicher, Sie haben eine Menge Code getroffen, der in try-catch eingeschlossen ist, wobei catch ein leerer Block ist, da eine solche Situation laut Entwickler einfach nicht hätte passieren dürfen. In vielen Fällen wird die Behandlung geprüfter Ausnahmen folgendermaßen implementiert: Sie lösen einfach eine RuntimeException aus und fangen sie irgendwo darüber ab (oder fangen sie nicht ab ...).
try { // do something } catch (IOException e) { throw new RuntimeException(e); // - ...
Was ist in Kotlin möglich?
In Bezug auf Ausnahmen unterscheidet sich der Kotlin-Compiler darin, dass:
1. Unterscheidet nicht zwischen aktivierten und nicht aktivierten Ausnahmen. Alle Ausnahmen sind nur deaktiviert, und Sie entscheiden selbst, ob Sie sie abfangen und verarbeiten möchten.
2. Try kann als Ausdruck verwendet werden. Sie können den try-Block ausführen und entweder die letzte Zeile daraus oder die letzte Zeile aus dem catch-Block zurückgeben.
val value = try {Integer.parseInt(“lol”)} catch(e: NumberFormanException) { 4 } //
3. Sie können auch eine ähnliche Konstruktion verwenden, wenn Sie auf ein Objekt verweisen, das möglicherweise nullwertfähig ist:
val s = obj.money ?: throw IllegalArgumentException(“ , ”)
Java-Kompatibilität
Kotlin-Code kann in Java verwendet werden und umgekehrt. Wie gehe ich mit Ausnahmen um?
- Geprüfte Ausnahmen von Java in Kotlin können weder geprüft noch deklariert werden (da es in Kotlin keine geprüften Ausnahmen gibt).
- Mögliche geprüfte Ausnahmen von Kotlin (z. B. solche, die ursprünglich aus Java stammten) müssen in Java nicht geprüft werden.
- Wenn eine Überprüfung erforderlich ist, kann die Ausnahme mithilfe der Annotation @Throws in der Methode überprüft werden (es muss angegeben werden, welche Ausnahmen diese Methode auslösen kann). Die obige Anmerkung dient nur der Java-Kompatibilität. In der Praxis erklären viele Leute damit, dass eine solche Methode im Prinzip eine Ausnahme auslösen kann.
Alternative zum Try-Catch-Block
Der Try-Catch-Block hat einen erheblichen Nachteil. Wenn es angezeigt wird, wird ein Teil der Geschäftslogik innerhalb des Catch übertragen, und dies kann in einer der vielen oben genannten Methoden geschehen. Wenn die Geschäftslogik über Blöcke oder die gesamte Anrufkette verteilt ist, ist es schwieriger zu verstehen, wie die Anwendung funktioniert. Und die Lesbarkeitsblöcke selbst fügen keinen Code hinzu.
try { HttpService.SendNotification(endpointUrl); MarkNotificationAsSent(); } catch (e: UnableToConnectToServerException) { MarkNotificationAsNotSent(); }
Was sind die Alternativen?
Eine Option bietet uns einen funktionalen Ansatz für die Ausnahmebehandlung. Eine ähnliche Implementierung sieht folgendermaßen aus:
val result: Try<Result> = Try{HttpService.SendNotification(endpointUrl)} when(result) { is Success -> MarkNotificationAsSent() is Failure -> MarkNotificationAsNotSent() }
Wir haben die Möglichkeit, die Try-Monade zu verwenden. Im Wesentlichen ist dies ein Container, der einen bestimmten Wert speichert. flatMap ist eine Methode zum Arbeiten mit diesem Container, die zusammen mit dem aktuellen Wert eine Funktion übernehmen und erneut eine Monade zurückgeben kann.
In diesem Fall wird der Anruf in die Try-Monade eingeschlossen (wir geben Try zurück). Es kann an einem einzigen Ort verarbeitet werden - wo wir es brauchen. Wenn die Ausgabe einen Wert hat, führen wir die folgenden Aktionen damit aus. Wenn eine Ausnahme ausgelöst wird, verarbeiten wir sie ganz am Ende der Kette.
Funktionale Ausnahmebehandlung
Wo kann ich Try bekommen?
Erstens gibt es einige Community-Implementierungen der Klassen Try und Both. Sie können sie nehmen oder sogar selbst eine Implementierung schreiben. In einem der „Kampf“ -Projekte haben wir die selbst erstellte Try-Implementierung verwendet - wir haben es mit einer Klasse geschafft und hervorragende Arbeit geleistet.
Zweitens gibt es die Arrow-Bibliothek, die Kotlin im Prinzip viele Funktionen hinzufügt. Natürlich gibt es Try and Both.
Außerdem erschien die Ergebnisklasse in Kotlin 1.3, auf die ich später noch näher eingehen werde.
Versuchen Sie es mit der Pfeilbibliothek als Beispiel
Die Pfeilbibliothek gibt uns eine Try-Klasse. In der Tat kann es in zwei Zuständen sein: Erfolg oder Misserfolg:
- Erfolg bei erfolgreichem Rückzug behält unseren Wert,
- Fehler speichert eine Ausnahme, die während der Ausführung eines Codeblocks aufgetreten ist.
Der Anruf ist wie folgt. Natürlich ist es in einem regulären Try-Catch verpackt, aber dies wird irgendwo in unserem Code passieren.
sealed class Try<out A> { data class Success<out A>(val value: A) : Try<A>() data class Failure(val e: Throwable) : Try<Nothing>() companion object { operator fun <A> invoke(body: () -> A): Try<A> { return try { Success(body()) } catch (e: Exception) { Failure(e) } } }
Dieselbe Klasse sollte die flatMap-Methode implementieren, mit der Sie eine Funktion übergeben und unsere try-Monade zurückgeben können:
inline fun <B> map(f: (A) -> B): Try<B> = flatMap { Success(f(it)) } inline fun <B> flatMap(f: (A) -> TryOf<B>): Try<B> = when (this) { is Failure -> this is Success -> f(value) }
Wofür ist das? Um Fehler nicht für jedes der Ergebnisse zu verarbeiten, wenn wir mehrere davon haben. Zum Beispiel haben wir mehrere Werte von verschiedenen Diensten erhalten und möchten diese kombinieren. Tatsächlich können wir zwei Situationen haben: Entweder haben wir sie erfolgreich erhalten und kombiniert, oder es ist etwas gefallen. Daher können wir Folgendes tun:
val result1: Try<Int> = Try { 11 } val result2: Try<Int> = Try { 4 } val sum = result1.flatMap { one -> result2.map { two -> one + two } } println(sum) //Success(value=15)
Wenn beide Aufrufe erfolgreich waren und wir die Werte erhalten haben, führen wir die Funktion aus. Wenn sie nicht erfolgreich sind, wird Failure mit einer Ausnahme zurückgegeben.
So sieht es aus, wenn etwas herunterfällt:
val result1: Try<Int> = Try { 11 } val result2: Try<Int> = Try { throw RuntimeException(“Oh no!”) } val sum = result1.flatMap { one -> result2.map { two -> one + two } } println(sum) //Failure(exception=java.lang.RuntimeException: Oh no!
Wir haben dieselbe Funktion verwendet, aber die Ausgabe ist ein Fehler einer RuntimeException.
In der Pfeilbibliothek können Sie auch Konstrukte verwenden, bei denen es sich tatsächlich um syntaktischen Zucker handelt, insbesondere um Bindungen. Trotzdem kann es über eine serielle flatMap umgeschrieben werden, aber durch das Binden können Sie es lesbar machen.
val result1: Try<Int> = Try { 11 } val result2: Try<Int> = Try { 4 } val result3: Try<Int> = Try { throw RuntimeException(“Oh no, again!”) } val sum = binding { val (one) = result1 val (two) = result2 val (three) = result3 one + two + three } println(sum) //Failure(exception=java.lang.RuntimeException: Oh no, again!
Da eines der Ergebnisse gesunken ist, erhalten wir einen Fehler in der Ausgabe.
Eine ähnliche Monade kann für asynchrone Anrufe verwendet werden. Hier sind beispielsweise zwei Funktionen, die asynchron ausgeführt werden. Wir kombinieren ihre Ergebnisse auf die gleiche Weise, ohne ihren Status separat zu überprüfen:
fun funA(): Try<Int> { return Try { 1 } } fun funB(): Try<Int> { Thread.sleep(3000L) return Try { 2 } } val a = GlobalScope.async { funA() } val b = GlobalScope.async { funB() } val sum = runBlocking { a.await().flatMap { one -> b.await().map {two -> one + two } } }
Und hier ist ein Beispiel für einen „Kampf“. Wir haben eine Anfrage an den Server, wir verarbeiten sie, holen den Text von ihm und versuchen, ihn unserer Klasse zuzuordnen, von der wir bereits Daten zurückgeben.
fun makeRequest(request: Request): Try<List<ResponseData>> = Try { httpClient.newCall(request).execute() } .map { it.body() } .flatMap { Try { ObjectMapper().readValue(it, ParsedResponse::class.java) } } .map { it.data } fun main(args : Array<String>) { val response = makeRequest(RequestBody(args)) when(response) { is Try.Success -> response.data.toString() is Try.Failure -> response.exception.message } }
Try-Catch würde diesen Block viel weniger lesbar machen. In diesem Fall erhalten wir am Ausgang response.data, die wir je nach Ergebnis verarbeiten können.
Ergebnis von Kotlin 1.3
Kotlin 1.3 führte die Ergebnisklasse ein. In der Tat ist es etwas ähnliches wie Try, aber mit einer Reihe von Einschränkungen. Es ist ursprünglich für verschiedene asynchrone Operationen vorgesehen.
val result: Result<VeryImportantData> = Result.runCatching { makeRequest() } .mapCatching { parseResponse(it) } .mapCatching { prepareData(it) } result.fold{ { data -> println(“We have $data”) }, exception -> println(“There is no any data, but it's your exception $exception”) } )
Wenn nicht falsch, ist diese Klasse derzeit experimentell. Sprachentwickler können ihre Signatur und ihr Verhalten ändern oder ganz entfernen. Daher ist es derzeit verboten, sie als Rückgabewert von Methoden oder Variablen zu verwenden. Es kann jedoch als lokale (private) Variable verwendet werden. Das heißt, Tatsächlich kann es als Versuch aus dem Beispiel verwendet werden.
Schlussfolgerungen
Schlussfolgerungen, die ich für mich selbst gezogen habe:
- Die Behandlung von Funktionsfehlern in Kotlin ist einfach und bequem.
- niemand stört sich daran, sie durch Versuch im klassischen Stil zu verarbeiten (sowohl das als auch das hat das Recht auf Leben; sowohl das als auch das sind bequem);
- Das Fehlen geprüfter Ausnahmen bedeutet nicht, dass Fehler nicht behandelt werden können.
- Ungefangene Ausnahmen bei der Produktion führen zu traurigen Konsequenzen.
Artikelautor: Alexey Shafranov, Leiter der Arbeitsgruppe (Java), Maxilect
PS Wir veröffentlichen unsere Artikel auf mehreren Websites der Runet. Abonnieren Sie unsere Seiten auf
VK ,
FB oder
Telegramm-Kanal , um mehr über unsere Veröffentlichungen und andere Maxilect-Nachrichten zu erfahren.