Stranger Than Usual

String encoding in Go

(Usually I write stuff in German, but this could be interesting to some people I know who do not speak German, so English it is.)

As frequent readers (do I have those?) know, text encoding is a small hobby / pet peeve of mine. A lot of things would be easier if we just used UTF-8 everywhere. But a consistent text encoding is only useful if it is used correctly and its validity is enforced. Most languages do a bad job at enforcing valid encoding. For example:

  • C does not have strings, C has null-terminated char-arrays. Encoding optional
  • C++ has strings that do not enforce encoding, newer versions have some questionable unicode strings
  • Java encodes strings in UTF-16 and does not check for unpaired surrogates
  • Javascript works similar to Java in that regard

Python and rust enforce valid unicode. Except that python allows to encode surrogate code points (but at least it does fail when it tries to encode that string as UTF-8). However, I'm not here to talk about any of these languages. I'm here to talk about Go, since I'm probably going to use it professionally again next week.

Strings in Go are just gloriefied immutable byte arrays. They have no inherent guarantee to contain a valid string of any encoding. Since Go is such a young language, this is a shame. The worst part: Developers don't expect it. They take a modern language, read strings from external sources, modify them, write them to external sinks and are oblivious about the fact that they are just one step away from splitting a code point in the middle.

I talked about the strengths and weaknesses of Go in another blogpost. Here, I'm going to ask the question: How bad is the situation? What can go wrong, where is easy to make mistakes and where is it hard to do so? Let's start with the basics.

Thing I already knew

Source files in Go are strictly UTF-8. So in theory, string literals are also strictly UTF-8. Unless they are not because you can use escape sequences to do stuff like this:

var s = "foo\xffbar"

There, an invalid byte in the middle of the string. Go has the unicode/utf8 package in its standard library, so we can at least check if the string is valid:

utf8.ValidString(s) // evaluates to false for the string above

You can also just create a string from a byte slice without validation. I have seen many developers doing exactly this, without even thinking about any problems:

c := []byte{0x66, 0x6f, 0x6f, 0xff}
s := string(c)

It should come to no surprise that you can just split a string in the middle of a code point:

s := "🐍"
s1 := s[:2] // s1 now contains an incomplete code point

Just as with casting a byte array to a string, this is something I have seen in the wild several times. Barely anyone thinks about it.

This situation is bad. It is far too easy to get invalidly encoded strings. And Golang is used primarily for web servers. Those get untrusted input all the time. So maybe at least the interfaces that are commonly used are protected? I have never tried this before, so let's do it!

Unicode validation at the border of golang web services

I have written a small test web server for a few test cases. I want to test three interfaces (or two, depending on how you count):

  • URL-parameters for GET requests
  • HTML form data from a POST request
  • JSON data (in this case from POST request, but it does not really matter)

The first two can count more or less as one, because the interface in Golang is the same: A http.Request-object has a field Form (this field needs to be populated by a call to ParseForm(), but that detail does not really matter here). Form is basically a map from parameter name to parameter value(s). So this is what I did:

if err := req.ParseForm(); err != nil {
    response.WriteHeader(http.StatusBadRequest)
    fmt.Fprintln(response, "bad form data")
    return
}

query, ok := req.Form["q"]
// [I left out some validation code here]
if utf8.ValidString(query[0]) {
    log.Printf("query string '%s' is valid utf-8", query[0])
} else {
    log.Println("query string is not valid utf-8")
}
response.WriteHeader(http.StatusOK)
fmt.Fprintln(response, "looks good")

So basically: I try to parse the form data and return an error if any problem occurs. Then I get the string from the form map and log whether it is valid UTF-8. Then I return with a success status code. Ideally, ParseForm() should fail for invalid UTF-8. Let's see what happens:

$ curl "http://localhost:8090/form?q=foo%ff"
looks good

Needless to say, there is a query string is not valid utf-8 message in the server log. And that was the most simple case. POST-body form data has basically the same result, no matter whether I use %ff to escape the byte or use a raw 0xff-byte in the body.

So that's form data. What about JSON? After all, we all write bloated single-page javascript apps with a server that is basically a JSON-API. Does Go's JSON parsing do it better? As a matter of fact, it does! Assume I have this struct

type Input struct {
	Query string `json:"q"`
}

and then handle it like this:

body, err := io.ReadAll(req.Body)
// [omitted error handling]

var input Input
err = json.Unmarshal(body, &input)
// [omitted error handling]

if utf8.ValidString(input.Query) {
    if strings.ContainsRune(input.Query, '�') {
        log.Printf("query string '%s' is valid utf-8 but contains a replacement character", input.Query)
    } else {
        log.Printf("query string '%s' is valid utf-8", input.Query)
    }
} else {
    log.Println("query string is not valid utf-8")
}
response.WriteHeader(http.StatusOK)
fmt.Fprintln(response, "looks good")

No matter what I threw at it, it was always handles gracefully. An escaped surrogate code point? Replaced with the replacement character. An unescaped surrogate code point? Replaced with three replacement characters. An unescaped 0xff-byte? Replaced with the replacement character. An overlong encoding of "}? Replaced with eight replacement characters.

So while I'm a bit unhappy that it does this silently and I have not found an option to let the parsing fail instead of replacing unexpected bytes, this is at least valid behaviour and leads to valid encoding.

Conclusions

Always check strings from untrusted sources for valid encoding. I cannot stress this enough: Most standard library functions will ignore encoding! You have to check manually. json.Unmarshal may have your back, but unless you know for certain that this is the case, always check (in addition to other security measures you should take with untrusted input).

Also: do not split strings in arbitrary places, do not cast byte arrays to strings without checking for valid encoding and be very careful with escape sequences in string literals.

Bahncard-Tortur

Ich sollte mal eine Statistik aufstellen, welche Tags in diesem Blog am häufigsten zusammen vorkommen. Ich würde wetten, es sind „rant“ und „deutsche bahn“.

Was ist es diesmal? Mal wieder der Onlineauftritt, in Kombination mit ihrem halbgaren Digitalzwang. Ich wollte nur eine Bahncard kaufen, weil ich demnächst häufiger mal nach Hamburg fahren muss. Mein Ärger fing schon beim Login an.

Passwortrichtlinien

Die DB hat ihre Passwortrichtlinien geändert. An sich kein Problem. Bisher wurden definitiv zu kurze Passworte erlaubt. Aber anstatt nach dem Stand der Wissenschaft zu gehen, sehe ich das hier:

For your own security, please choose a password that you have never used before. At least 12 characters, contains uppercase letters, contains lowercase letters, contains numbers, contains special characters, not your e-mail address

7 Regeln, nur drei davon sind gut: Das Passwort muss neu sein, mindestens 12 Zeichen lang, nicht die E-Mail-Adresse.

Ich habe mich dazu schon öfters ausgelassen, deswegen spare ich mir das jetzt. Siehe Umzug nach Nijmegen, Sparkasse pushTAN und das Passwort und Furchtbare Passwortrichtlinien bei der Bundesagentur für Arbeit. Manchmal frage ich mich, wer im falschen Paralleluniversum lebt. Bin ich es, und bilde mir nur ein, dass so komplizierte Passwortregeln als kontraproduktiv erwiesen wurden? Oder die Erfinder solcher Vorgaben, die die letzten zehn Jahre oder mehr verschlafen haben?

Dann war ich endlich eingelogged. Ich habe mir die Bahncard herausgesucht, „Lastschrift“ als Zahlart ausgewählt und wollte die Bestellung abschließen. Ich muss nur noch einen kleinen Schritt durchführen, sagt die Bahn-Website.

Identifizierung durch Verimi

Ich muss nur noch meine Identität bestätigen. Wo kämen wir denn hin, wenn ich für jemand anderen eine Bahncard kaufen könnte? Zur Identitätsbestimmung gibt es mehrere Möglichkeiten.

Als Erstes die E-Perso-App, oder wie die heißt. Ich habe zwar gerade einen neuen Perso bekommen, habe das aber noch nicht eingerichtet, außerdem fehlt mir der Chipkartenleser. Also fällt das erst einmal weg.

Als Zweites kann ich Verimi (oder einem anderen Drittanbieter) meine Logindaten für mein Online-Banking geben, dann können die sicher sein, dass ich ich bin. Meine Logindaten. Für Online-Banking.

Kermit der Frosch am Telefon: JA, DIE HABEN ALLE LACK GESOFFEN. NEIN, ICH WEIß NICHT WIE VIEL

Das ganze hat bei mir Flashbacks ausgelöst. Damit musste ich Silvester 2023 auch schon kämpfen, als ich mir ein Deutschlandticket besorgen wollte.

Die dritte Option war, meinen Perso zu Fotografieren und denen gleich auch noch ein Selfie zu schicken. Auch nicht meine Lieblingsvariante, insbesondere nach dieser Geschichte neulich, bei der eine Hotelkette Ausweisdaten gesammelt und nicht gesichert hat. Aber ich brauche diese Bahncard. Nach einigem Herumprobieren (Verimi war pingelig, was die fotografische Qualität meines Ausweises anging) war ich dann identifiziert.

Aber wenigstens kann ich dann endlich mit Lastschrift zahlen, oder?

Die Zahlart

„For the better, right?“ meme. Vier Panels. Panel 1: Anakin: „Du musst dich ausweisen.“ Panel 2: Padme (fröhlich): „Dann kann ich per Lastschrift zahlen, richtig?“. Panel 3: Beat Panel, Anakin schweigt. Panel 4: Padme (zweifelnd): „Richtig?“

Hahaha. Nein.

Die Zahlung konnte nicht durchgeführt werden. Die Zahlung konnte mit dem gewählten Zahlungsmittel nicht durchgeführt werden. Bitte wählen Sie ein anderes Zahlungsmittel.

Ok, dann also Paypal. Ich lese immer wieder von Leuten die schreiben, man solle Paypal nicht benutzen, weil Peter Thiel ein Arschloch ist. Ist er, aber irgendwie muss ich ja meine Bahntickets bezahlen.

Immerhin habe ich jetzt das Bahnticket. Das wird natürlich automatisch verlängert, man kann es, ähnlich wie das Deutschlandticket, nur im Abo kaufen. Zwei Mal bin ich schon darauf hereingefallen, dass ich vergessen habe, die Bahncard zu kündigen, dann habe ich eine Rechnung bekommen, die in den ganzen Ihr-Zug-kommt-zu-spät-oh-nein-doch-nicht-oh-warte-doch-ach-ne-er-fällt-komplett-aus-Spam-Emails untergegangen ist, und statt einer Mahnung haben sie mir dann direkt ein Inkassounternehmen auf den Hals gehetzt, dass eine saftige Strafgebühr verlangt hat. Vielleicht lassen sie mich deswegen nicht per Lastschrift zahlen. Ihr wisst, schon, dann würde das ja automatisch abgebucht und sie bekämen die Strafgebühr nicht.

Dass ich verdammte vier Wochen vor Ablauf kündigen muss, halte ich auch für eine Frechheit. Immerhin werde ich darauf hingewiesen. Und als weiteren Beweis, dass die Bahn unfähig ist, eine Website zu bauen, haben sie ihr HTML auch einmal zu viel escaped und zeigen statt einem Link den rohen HTML-Code an:

Eine Infobox, die die Kündigungsbedingungen der Bahncard erläutert. Darin ist HTML-Code, der wohl ein Link zur Seite mit der Widerrufsbelehrung werden sollte.

Gut, wann schicken die mir meine Bahncard zu?

Digitalzwang

Das war eine Fangfrage. Natürlich schicken die mir keine Bahncard mehr zu. Physische Bahncards haben ausgedient, das hatte ich schon vorher mitgekriegt. Digitalcourage hatte das immer mal wieder angesprochen.

Ich brauche also die DB-Navigator-App. Eine App, die wegen Datenschutz- und Sicherheitsproblemen immer mal wieder in den Schlagzeilen war. Ist aber auch irrelevant, ich kann die App nicht installieren, weil ich den Playstore nicht auf meinem Phone habe. Immerhin kann man mittlerweile ein Ersatz-PDF herunterladen, dass man dann ausdrucken oder auf einem Gerät vorzeigen kann. Aber wo finde ich das?

Es gibt eine Website mit einer laaangen Anleitung, wie man die DB-Navigator-App benutzt. Darunter ein Video, wie man die PDF-Bahncard herunterladen kann. Darunter noch eine „barrierefreie“ Option. Diese Option ist eine PDF-Datei mit vielen Bildern. 9 Seiten lang. 8 Seiten davon eine Anleitung, warum man doch lieber die App verwenden sollte. Auf Seite neun dann fünf kurze Punkte, wo man die Bahncard-PDF-Datei findet (sie ist gut versteckt). Fünf Punkte auf einer Strichpunktliste. This PDF could have been a website.

Naja, jetzt habe ich die Bahncard wenigstens. Und habe schon wieder zwei Stunden verbracht, diesen Blogpost zu schreiben. Ich hatte heute noch etwas anderes vor.

Keilschriftuhr

Eigentlich wollte ich ja gestern nichts über die Bahn schreiben, sondern über etwas Schönes. Ich habe mich ja vor Kurzem über das GRSSK-Phänomen und den Missbrauch von nicht-lateinischen Schriften ausgelassen.

Im Gegensatz dazu kann man Keilschrift aber auch korrekt einsetzen. Da hat zum Beispiel jemand eine Website gebaut, die die Uhrzeit in Keilschrift anzeigt. Bonuspunkte für den Pfadnamen sumertime.

Diese Uhr benutzt die echten Babylonischen Zahlendarstellungen. Die gehen von 1 bis 59 (und ein Leerzeichen für die 0) und sind damit perfekt geeignet, um Minuten und Sekunden darzustellen. Tatsächlich lässt sich die 60 Minuten/Stunde, 60 Sekunden/Minute schlussendlich auf das Sexagesimalsystem der Sumerer und Babylonier zurückführen, auch wenn die eigentlich Aufteilung laut Wikipedia zum ersten Mal vor etwa tausend Jahren verwendet wurde.

Auch die Doppelpunkte, um Stunden, Minuten und Sekunden zu trennen sind wahrscheinlich ein Anachronismus. Trotzdem eine schöne Uhr.

Neue PNG-Spezifikation

Vor ein paar Tagen kam die Meldung, dass es eine aktualisiert PNG-Spezifikation gibt. Darin enthalten: verschiedene Standardisierungen, oft von ohnehin schon so benutzten Features, wie z.B. Exif-Metadaten oder dem APNG-Format.

Letzteres habe ich hier auch schon verwendet, in dem Blogpost über eine Barber's Pole als Farbschichtschema für Fordite. Ich wollte eine Animation haben, aber eine GIF-Datei wollte ich nicht benutzen, also habe ich mir angeschaut, was es sonst so gibt. Es gibt wohl das MNG-Format, das von niemandem unterstützt wird und die Konkurrenz in form von APNG, die bis vor ein paar Tagen nicht standardisiert war.

Ich wollte dann direkt mal nachschauen, ob meine APNG-Dateien eigentlich standardkonform sind. Bei der Suche nach einem Tool bin ich dann aber abgelenkt worden und auf apngopt gestoßen. Wer dieses Blog verfolgt, weiß, dass ich nahezu zwanghaft alle Bilder optimal komprimieren möchte. Momentan benutze ich für die meisten Bilder in diesem Blog das WebP-Format (je nach Anwendung in verlustfreier oder verlustbehafteter Version). Verlustfrei komprimiert WebPs sind in den meisten relevanten Fällen kleiner als vergleichbare PNGs. PNGs sind aber immer noch ein Ding, nicht zuletzt wegen der Animationen.

Also musste ich das tool natürlich ausprobieren. Und siehe da: es kriegt meine APNG auch ein bisschen kleiner. Die eine Datei schrumpfte von 13546 byte zu 12206 byte, also um mehr als ein kilobyte oder um fast zehn Prozent. Die andere Datei schrumpfte von 46743 auf 43840 byte also um knapp drei kilobyte oder gut 6%.

Was mich an diesen ganzen Bildoptimierungsprogrammen irritiert ist, wie unterschiedlich das Benutzerinterface ist. Beispiele

  • optipng, jpegoptim und svgo ersetzen per default die Originaldatei mit der kleineren Version (nur, wenn die wirklich echt kleiner ist), lassen sich aber dazu überreden, eine andere Datei als Zieldatei zu verwenden.
  • zopflipng braucht explizit eine Ausgabedatei. Ist das dieselbe wie die Originaldatei, wird nachgefragt. Die Nachfrage kann man mit einem Flag unterdrücken
  • apngopt legt neben der Originaldatei eine zweite Datei an, die so heißt wie das Original, nur das (vor der Dateiendung) noch In _opt angefügt wurde. Es lässt sich nicht dazu bringen, es anders zu tun.

Immerhin kann ich bei apngopt zwischen verschiedenen Kompressionsbackends wählen, unter Anderem auch zopfli.

Eine kleine Anekdote noch: PNG, geschaffen für das Internet, wurde in den Neunzigerjahren als Alternative für GIF entwickelt, weil GIF von Patenten belastet war (die 2006 ausgelaufen sind). Herausgekommen ist ein Format, das Bilder kleiner und in besserer Qualität als GIF ausliefert.

Nur Microsoft hatte es lange nicht hingekriegt, PNGs vollständig zu unterstützen. Im berüchtigten Internet Explorer 6 zum Beispiel wurden keine transparenten PNGs unterstützt. Anstatt das zu beheben empfahl Microsoft, eine proprietäre Erweiterung im HTML-Code zu verwenden. Es gab auch Javascript von Drittparteien, die das Problem gelöst haben. Und wie das nun einmal so ist, hat man einen solchen Workaround einmal eingebaut, bleibt er lange. In einem Projekt, in dem ich gearbeitet habe, war bis 2018 noch Code, der Javascript enthielt, um dem zu diesem Zeitpunkt schon zum Teufel gejagten Internet Explorer 6 auf die Sprünge zu helfen.

Jetzt muss ich C# lernen

In der Erwartung, dass ich bald beruflich wieder Go benutzen werde, habe ich Anfang der Woche herumexperimentiert, wie gut Go einen davor schützt, falsches Encoding in string-Variablen zu haben.

Zwei Tage später hatte ich dann ein Gespräch mit zukünftigen Kollegen und musste erfahren: Es wird nicht Go. Der Kunde besteht auf C# und lässt sich durch nichts davon abbringen. 75% des Teams kann zwar kein C#, aber das sind alles intelligente Leute, die werden das lernen.

Ich habe noch nie C# benutzt. Aber ich habe meine Vorurteile. Meinen Vorurteilen zufolge hat C# (zusammen mit Mono, bzw. .net) die gleichen Probleme, mit denen ich mich bei Java jahrelang herumplagen musste und froh war, davon wegzukommen. Insbesondere, dass es zwanghaft Klassen verwendet, auch für statische Funktionen, und dass es die Art von Entwickler anzieht, die es für eine gute Idee halten, auf Teufel komm raus alle Design-Pattern, die die objektorientierte Programmierung zu bieten hat, in den Code zu quetschen, egal, ob es nun sinnvoll ist oder nicht.

Ich erwarte Dependency Injection, Inversion of Control, zu viele Interfaces, Dinge wie die AbstractSingletonProxyFactoryBean in Java Spring, ORMs, das ganze Programm. Alles Dinge, die sich jemand ausgedacht hat um Sachen einfacher und flexibler zu machen, die am Ende aber alles komplexer machen, Flexibilität schaffen, die man nicht braucht und es dafür an anderer Stelle unmöglich machen, eigentlich triviale Dinge überhaupt zu machen.

Ich war in meiner Anfangszeit als Programmierer ein großer Fan von objektorientierter Programmierung, habe aber mit der Zeit ihre Schwächen gelernt und picke mir jetzt nur noch die Teile davon heraus, die ich für gut befinde. Das geht in Go und rust sehr gut, in Java eher nicht. Wie es bei C# aussieht bleibt abzusehen.

Jedenfalls kann ich meine Vorurteile demnächst prüfen. Vielleicht werden sie ja widerlegt. Ich erwarte das aber nicht wirklich.

Zwei positive Seiten hat es. Zum Einen lerne ich mal wieder eine neue Programmiersprache. Nur schade, dass ich nie gezwungen werde, Sprachen wie Lisp oder Haskell zu lernen (und alleine bringe ich nicht genug Motivation auf, diese Sprachen zu lernen, weil ich mit rust so bequem geworden bin).

Zum Anderen kann ich die Experimente zum String-encoding, die ich neulich mit Go gemacht habe, in C# wiederholen. Ich erwarte nicht wirklich viel, denn erste Experimente haben schon gezeigt, dass es sehr einfach ist, einen string in c# mitten im (UTF-16-codierten) codepoint durchzuschneiden:

const string snek = "🐍";

string snekstats = string.Format("length: {0}", snek.Length);
Console.WriteLine(snekstats); // ergibt "2"

char foo = snek[0]; // char ist 16 bit
Console.WriteLine(string.Format("{0}", foo)); // ungültiges encoding

Moderne Bahnreisen

Wie schön doch die Digitalisierung ist! Dank E-Mail-Benachrichtigung weiß man schon eineinhalb Stunden vorher, dass der Zug Verspätung hat. Bringt aber auch nichts. Ich muss nämlich trotzdem zur selben Zeit los, darf dann aber am Umsteigebahnhof bei hochsommerlichen Mittagstemperaturen eine halbe Stunde extra warten.

Rollenspielszenen: Kinder manipulieren

Ich spiele regelmäßig Pen & Paper-Rollenspiele, und habe mir mal vorgenommen, öfter was dazu zu schreiben. Angefangen damit, (wenn möglich) nach jeder Runde eine markante Szene aufzuschreiben und zu posten. Hier kommt die erste Szene, aus einer World Tree-Runde, die ich leite:

Die Gruppe ist in einem Orren-Dorf und muss einen See überqueren. Das einzige Boot des Dorfes wurde aber von einer Bastlerin versenkt:

Orren: „Fizzel wollte ein Schnellboot daraus machen. Das hat auch so halb funktioniert“

Rassimel (PC): „Wie kann das halb funktioniert haben?“

Orren: „Die Hälfte, die funktioniert hat, liegt da drüben am Ufer. Die andere Hälfte ist am Steg versunken.

Die Zeit drängte, und zum Reparieren wird jede Hand gebraucht. Wie kriegt man einen Haufen fauler Orren dazu, mit anzupacken?

Die Lösung war ein Orrenkind. Schon am Dorfeingang musste die Gruppe das Kind abwimmeln, und verriet ihm, dass sie in „geheimer Mission“ unterwegs seien. Später entdeckten sie, wie das Kind ihnen hinterherspioniert. Sie nutzten die Gelegenheit und unterhielten sich lautstark über ihren „geheimen“ Auftrag:

„ES WÄRE WIRKLICH SCHLIMM, WENN WIR NICHT RECHTZEITIG ÜBER DEN SEE KÄMEN!“

„GENAU! WIR BRAUCHEN DRINGEND DIESEN HEILZAUBER, UM DIE MHEROBUMP ZU RETTEN!“

„WENN WIR DOCH NUR GENUG LEUTE HÄTTEN, DIE UNS BEIM REPARIEREN HELFEN!“

Kurze Zeit später rannte das Kind überall herum, scheuchte Leute auf und bettelte seine Mutter an, mit anzupacken. Voller Erfolg!

Verspätungs-Doomscrolling

Habe ich gesagt, dass mir die Ankündigung der 30 Minuten Verspätung nicht hilft, weil ich trotzdem zum gleichen Zeitpunkt losfahren muss, um rechtzeitig am Umsteigebahnhof zu sein?

Zwischendurch hatte sich die Verspätung auf 41 Minuten (warum geben die überhaupt minutengenaue Angaben, wenn die das sowieso nicht genau wissen?) erhöht. Also dachte ich mir, ich nehme einen Zug später.

Etwa vierzehn Minuten, bevor mein ursprünglicher Zug losgefahren ist, kriege ich die Meldung, dass der Anschlusszug nur noch 22 Minuten Verspätung hat. Ich muss also doch den ursprünglich geplanten Zug erwischen! Die Zeit ist knapp, also muss ich schnell aufbrechen um dann bei 27 °C im Schatten mit schwerem Gepäck durch die Stadt zu rennen.

Am Umsteigebahnhof angekommen, kriegte ich direkt eine Mail, dass der Zug doch wieder 33 Minuten Verspätung hat. Kurz darauf eine Durchsage am Bahnsteig, dass die Verspätung 35 Minuten beträgt.

In Zukunft werde ich diese Mails ignorieren. Sie helfen mir nicht, weil ich mich nicht auf sie verlassen kann. Kennt ihr das Konzept des „Doomscrollings“? Grob gesagt ist das, wenn man sich in social media durch die schlechten Nachrichten liest. Dank Endlosscrolling kommen immer mehr und mehr schlechte Nachrichten herein, was den Gemütszustand immer weiter verschlechtert. Man kann nicht aufhören, aber etwas tun kann man auch nicht.

Die Verspätungsmails der DB-Reisebegleitung sind das Doomscrolling des Bahnverkehrs.

Rollenspielszenen: Die Stimmung ist zu positiv

Rollenspielszene. Wir sind in einer besetzten Stadt. Wir suchen eine befreundete (und berüchtigte) Ætherwissenschaftlerin, Dr. Sjöstroem. Wir haben ein verschwörerisches Treffen mit Olive, einer Journalistin, und Jaro, einem Angestellten der Besatzerarmee.

Die beiden erzählen uns, dass Dr. Sjöstroem in einem Sperrgebiet im Sperrgebiet verschollen ist, bei dem Versuch, die dort stattfindenden Entwicklungen gefährlicher Ætherwaffen (inklusive Tests an lebenden Menschen) offenzulegen. Wir entscheiden uns, in das Sperrgebiet einzudringen und sie zu retten. Allerdings brauchen wir dafür eine Ablenkung. Wir entscheiden uns, der örtlichen Journalisten dazu einzuspannen, für einen Aufruhr zu sorgen (im Austausch für eine Druckerpresse, denn deren eigene Pressen wurden von den Besatzern zerstört).

Wie spricht man so ein Thema an? Ganz einfach:

Ich dachte, die Stimmung in der Stadt ist ein bisschen zu positiv. Ich finde, das sollte geändert werden.

Eintausend 🎉

Das hier ist der tausendste Post auf diesem Blog! Naja, je nachdem, wie man zählt. Ich habe alle Posts auf meinem alten Blog, die ich mal importiert habe mitgezählt. Bis auf den einen, den ich nicht importiert habe, weil er nur ein Verweis auf dieses Blog hier ist.

Jedenfalls sind es nur noch 24 Posts bis zu einer schönen, runden Zahl! Ich feiere das Jubiläum trotzdem jetzt schon, denn ich bin nur durch Zufall darauf gestoßen, dass wir kurz vor der 1000 sind und bis zur 1024 hätte ich das schon wieder vergessen. Jubiläen vergessen hat zwar eine gewisse Tradition hier, aber man muss ja nicht jede Tradition durchziehen.

Es gibt auch ein paar kleine Änderungen: innerhalb der letzten Monate habe ich sukzessive für alle alten Blogposts eine Zusammenfassung hinzugefügt. Alles Handarbeit, keine LLMs oder andere Generatoren. War eine Menge Arbeit, aber nebenbei konnte ich einige alte Posts noch taggen oder kaputte Links austauschen und ein paar Typos und schlechte Typografie korrigieren.

Diese Zusammenfassungen tauchten bisher nur in meta-Tags auf und waren im Blog nicht sichtbar. Das habe ich nun geändert. Auf den Tag-Seiten und auf den Kategorie-Seiten wird jetzt unter dem Titel auch jeweils die Zusammenfassung (und das Veröffentlichungsdatum) angezeigt. Ich bin noch nicht sicher, ob ich das Markup so lasse, aber ich probiere es erst einmal so. Vielleicht muss ich dort auch noch paging einbauen, die Seiten werden mittlerweile ziemlich groß.

Die Version meines Generators habe ich jetzt auf 1.0.0 hochgezogen. Das macht eigentlich keinen Unterschied, aber eine Blogsoftware, die seit fast fünf Jahren ihre Arbeit macht und nur noch kleinere Verbesserungen bekommt, hat eine stabile Versionsnummer verdient.

Gleich ungleich Gleich

Zwei Werte auf Gleichheit prüfen ist etwas, dass man beim Programmieren an jeder Ecke braucht. Die meisten Programmiersprachen verhalten sich hier zumindest bei einfachen Datentypen wie z.B. Ganzzahlen ganz so, wie man es erwarten würde (über Fließkommazahlen werde ich in diesem Artikel nicht viel reden, die sind in dieser Hinsicht kompliziert.

Was komplexe Datentypen angeht (Arrays, Strings oder zusammengesetzte Typen wie Structs oder Klassen) verhalten sich die verschiedenen Programmiersprachen allerdings sehr unterschiedlich. Es fängt schon damit an, dass verschiedene Sprachen verschiedene Typen als „einfache“ Typen (oder wie immer das im Jargon der jeweiligen Sprache heißt, das geht von „primitive types“ über „basic types“ bis zu „built-in types“) bezeichnen. Für manche sind Strings zum Beispiel primitive Typen die man einfach vergleichen kann. Für andere sind Strings Klassen, die man aber wie einfache Typen auch vergleichen kann. Für wieder andere kann man nicht mehr den Gleichheitsoperator benutzen sondern muss eine besondere Funktion aufrufen.

Letztere Sprachen sind häufig die, die recht willkürlich zwischen „reference types“ und „primitive types“ unterscheiden. Für die einen kann man den Gleichheitoperator (meist ==) verwenden, für die anderen eine equals-Funktion in irgendeiner Form.

Sowohl der Gleichheitsoperator als auch die equals-Funktion können in verschiedenen Sprachen einen Default haben. Dann kann man die Operation zwar verwenden, oft vergleicht sie aber nur die Identität zweier Variablen (ist es dieselbe Variable, nicht der gleiche Wert). Manche Programmiersprachen haben auch keinen einheitlichen Standard für Gleichheitsoperationen. Man kann natürlich eine selbst definierte Funktion zum Vergleichen schreiben, die arbeitet dann aber nicht zwingend mit anderen Funktionen oder Datenstrukturen zusammen, die eine Gleichheitsoperation benötigen.

Ich möchte hier einmal eine Hand voll Programmiersprachen vergleichen (hah!). Ich stelle mir insbesondere folgende Fragen

  • Für welche Datentypen ist ein echter Vergleich Teil der Sprache?
  • Wird ein Unterschied zwischen Typen gemacht, bei denen man den Gleichheitsoperator verwenden kann und jenen die eine Equals-Funktion brauchen?
  • Gibt es Defaultimplementierungen, die unerwarteter Weise auf Identität, nicht auf Gleichheit überprüfen?
  • Ist es möglich, und wenn ja wie einfach ist es, eigene Gleichheitsoperationen zu definieren?

Eine Anmerkung noch: Üblicherweise erfordern Gleichheitsoperationen drei Eigenschaften: Sie müssen symmetrisch sein: a = b ⇔ b = a, transitiv: a = b ∧ b = c ⇒ a = c und reflexiv: a = a. Beim Programmieren ist das vor allem wichtig, wenn sich andere Codeteile auf diese Eigenschaften verlassen, weil es sonst zu Logikfehlern kommen kann. Insbesondere Gleitkommazahlen, heutzutage üblicherweise nach dem Standard IEEE 754 implementiert, verletzen die letzte Eigenschaft: den NaN („Not a Number“) ist ungleich NaN. Aber Fließkommazahlen sollte man wegen Rundungsfehlern sowieso nie direkt vergleichen.

Außerdem gibt es noch einige weitere Ansprüche, die manche Sprachen stellen. Ungleichheit zum Beispiel muss konsistent mit Gleicheit sein: a ≠ b ⇔ ¬(a = b). Oft wird auch gefordert, dass eine Hashfunktion für einen Wert konsistent mit der Gleichheitsoperation ist: a = b ⇒ hash(a) = hash(b). Letzteres ist insbesondere in Hashmaps wichtig.

Aber genug Gerede, schauen wir uns mal ein paar Sprachen an. Fangen wir an mit C, denn C ist recht low-level und recht alt.

C

Ich fange bei jeder Sprache mit derselben Baseline an: Vergleichen von Ganzzahlen:

int a = 1;
int b = 2;
int c = 1;
printf("a == b: %d, a == c: %d\n", a == b, a == c);

Der Output sieht so aus (in C ist 0 unwahr und alles andere wahr):

a == b: 0, a == c: 1

So weit keine Überraschungen. C hat auch Pointer, also Variablen, die eine Adresse auf einen Speicherbereich haben. Auch die kann man vergleichen, und es ist so etwas wie ein Identitätsvergleich (ich packe jetzt mal den output direkt im Kommentar unter den Codeblock, um es übersichtlicher zu halten):

int *ap = &a;
int *cp = &c;
printf("ap == ap: %d, ap == cp: %d\n", ap == ap, ap == cp);
printf("*ap == *cp: %d\n", *ap == *cp);
// ap == ap: 1, ap == cp: 0

ap zeigt auf a, cp zeigt auf c. Obwohl der Wert von a und c gleich ist, sind ap und cp verschieden. Auch hier keine Überraschungen. Aber schauen wir uns mal Arrays an:

int d[] = {1,2};
int e[] = {1,2};
printf("d == e: %d, memcmp(d, e) == 0: %d\n", d == e, memcmp(d, e, 2 * sizeof(int)) == 0);
// d == e: 0, memcmp(d, e) == 0: 1

Array-Variablen sind im Prinzip nur ein Pointer auf das erste (nullte) Element des Arrays. Deswegen funktioniert der direkte Vergleich nicht. Man muss eine Funktion benutzen, um sie zu vergleichen, wie hier memcmp aus der Standardbibliothek. memcmp liefert 0 zurück, wenn die Speicherbereiche gleich sind. Bei memcmp muss man aber vorsichtig sein, dass man die Größe des Arrays korrekt angibt.

Auch hier keine große Überraschung, C ist halt recht nah an der Maschine dran. Aber es fängt schnell an, aufwändig zu werden, mit diesen Datentypen zu hantieren. Wie sieht es mit Strings aus?

char f[] = "foobar";
char g[] = "foobar";
printf("f == g: %d, strcmp(f, g) == 0: %d\n", f == g, strcmp(f, g) == 0);
// f == g: 0, strcmp(f, g) == 0: 1

Strings in C sind nichts Anderes als char-Arrays, die ein 0-Byte am Ende haben. strcmp vergleicht Strings, braucht aber keine Längenangabe, weil es sich auf das Nullbyte verlässt (aber wehe, wenn das nicht da ist).

Aber wie sieht es mit komplexeren Datentypen aus? C hat hier hauptsächlich struct zu bieten (und union, aber wir reden nicht über union):

typedef struct {
    int a;
} Foo;

// … irgendwo später:

Foo h = { .a = 42 };
Foo i = { .a = 42 };
printf("h == i: %d\n", h == i);

Nope, hier beschwert sich der Compiler: error: invalid operands to binary == (have ‘Foo’ and ‘Foo’). Geht also nicht. Ich sehe das Positiv: anstatt hier einfach irgendwas zu machen sagt C hier: „nein, das geht nicht“. memcmp kann man hier aber auch verwenden:

printf("memcmp(h, i) == 0: %d\n", memcmp(&h, &i, sizeof(Foo)) == 0);
// memcmp(h, i) == 0: 1

Prima. Aber macht das auch einen tiefen Vergleich?

typedef struct  {
    int a;
    int *b;
} Foopar;

// irgendwo später:

Foopar l = {.a = 42, .b = &a};
Foopar m = {.a = 42, .b = &b};
Foopar n = {.a = 42, .b = &c};
printf("memcmp(l, m) == 0: %d, memcmp(l, n) == 0: %d\n", memcmp(&l, &m, sizeof(Foopar)) == 0, memcmp(&l, &n, sizeof(Foopar)) == 0);
// memcmp(l, m) == 0: 0, memcmp(l, n) == 0: 0

Nein, natürlich nicht. Aber auch das sollte keine Überraschung sein. memcmp vergleicht Speicherbereiche und folgt dabei natürlich keinen Pointern. Dafür müsste man sich selber eine Funktion schreiben.

Fazit

C ist simpel aufgestellt, bietet keine Komfortfunktionen, leitet hier aber auch nicht in die Irre. Alles ist umständlich, aber angesichts der Maschinennähe nicht verwunderlich. Positiv muss ich hervorheben, dass zumindest mein Compiler (gcc) einige Warnungen herausgehauen hat, als ich versucht habe, Variablen mit sich selbst zu vergleichen oder Arrays mit dem ==-Operator. Ich nehme C jetzt mal als Baseline, um andere Sprachen zu bewerten. Zum Beispiel C++.

C++

C++ ist eine auf C basierende, objektorientierte Sprache. C++ übernimmt viel von C, setzt eine Menge oben drauf, hat aber auch ein paar Unterschiede. Aber wir sind für eine Sache hier: Vergleichsoperatoren. Die einfachen Typen verhalten sich hier wir in C, deswegen fangen wir gleich mal mit Strings an:

string a = "foo";
string b = "bar";
string c = "foo";

cout << "a == a: " << (a == a) << ", a == b: " << (a == b) << ", a == c: " << (a == c) << endl;
// a == a: 1, a == b: 0, a == c: 1

Das funktioniert ja viel besser als in C. string ist hier übrigens kein eingebauter Typ, sondern eine Klasse (so etwas wie ein struct aus C, aber auf Steroiden) aus der Standardbibliothek. Man kann ja viel über objektorientierte Programmierung streiten, aber manche Features, die Klassen in C++ bieten, sind schon sehr hilfreich. Probieren wir es mal aus:

class Unfoo {
    private:
        const int a;
    public:
        Unfoo(int a): a(a) {}
};

// …weiter unten:
vector<Unfoo> o = {Unfoo(1)};
cout << "o == o: " << (o == o) << endl;

Ups… das geht nicht Das ist nicht einmal mit sich selbst vergleichbar. Das gibt einen Compilerfehler:

error: no match for ‘operator==’ (operand types are ‘const Unfoo’ and ‘const Unfoo’)

Dazu eine lange Liste von Hinweisen, die wohl hilfreich sein sollen, aber erst einmal verwirren. Aber immerhin: Es wird nicht einfach irgendwie verglichen, wir müssen das selber definieren.

class Foo {
    private:
        const int a;
    public:
        Foo(int a) : a(a) {}
        bool operator==(const Foo& right) const {
            return this->a == right.a;
        }
        int get_a() const {
            return this->a;
        }
};

// … weiter unten

Foo d(42);
Foo e(9001);
Foo f(42);
cout << "d == d: " << (d == d) << ", d == e: " << (d == e) << ", d == f: " << (d == f) << endl;
// d == d: 1, d == e: 0, d == f: 1

Wunderbar! Es funktioniert. Man muss ein bisschen vorsichtig sein: Wenn man Foo später um weitere Membervariablen erweitert, dann muss man auch die operator==-Methode anpassen.

Man kann natürlich auch tiefe Vergleiche machen (wenn man sie entsprechend implementiert) oder unterschiedliche Typen vergleichen (sofern das Sinn ergibt):

class Foop {
    private:
        const int *const a;
        Foop& operator=(Foop& original) = delete;
    public:
        Foop(int a) : a(new int(a)) {}
        bool operator==(const Foop& right) const {
            return *this->a == *right.a;
        }
        Foop(const Foop& original) : a(new int(*a)) {}
        ~Foop() {
            delete this->a;
        }
        int get_a() const {
            return *this-> a;
        }
};

bool operator==(const Foop& left, const Foo& right) {
    return left.get_a() == right.get_a();
}

bool operator==(const Foo& left, const Foop& right) {
    return left.get_a() == right.get_a();
}

// …weiter unten

Foop g(42);
Foop h(9001);
Foop i(42);
cout << "g == g: " << (g == g) << ", g == h: " << (g == h) << ", g == i: " << (g == i) << endl;
cout <<"d == g: " << (d == g) << ", g == d: " << (g == d) << endl;
// g == g: 1, g == h: 0, g == i: 1
// d == g: 1, g == d: 1

Funktioniert, ist aber eine Menge Handarbeit. Ich musste den Vergleich zwischen Foo und Foop zwei Mal implementieren, damit der auch symmetrisch ist. Dafür hat man eine Gleichheitsoperation, die angenehm zu benutzen ist und überall gleich aussieht. Die Standardbibliothek nutzt das viel. Zum Beispiel mit Containertypen wie vector, einer Klasse für dynamisch wachsende Arrays. Wichtig ist nur, dass die Typen im Container den Operator implementiert haben:

vector<Foo> k = {Foo(1)};
vector<Foo> l = {Foo(1), Foo(2)};
vector<Foo> m = {Foo(1)};
cout << "k == l: " << (k == l) << ", k == m: " << (k == m) << endl;
// k == l: 0, k == m: 1

Fazit

C++ bietet die Möglichkeit, für selbstdefinierte Typen den Gleichheitsoperator zu implementieren. Das ist sehr hilfreich, aber einiges an Handarbeit. C++ bietet keine automatische Implementierung, aber auch keine Default-Implementierung, die einen vielleicht überrascht. Warum ich so auf dieser Default-Implementierung herumhacke? Schauen wir uns das mal am Beispiel von Python an.

Python

Vergleichen wir erst einmal wieder Ganzzahlen, als Baseline:

a = 42
b = 11
c = 42

print(f"a is a: {a is a}, a is b: {a is b}, a is c: {a is c}")
print(f"a == a: {a == a}, a == b: {a == b}, a == c: {a == c}")

// a is a: True, a is b: False, a is c: True
// a == a: True, a == b: False, a == c: True

Python kennt nur Referenztypen. Das is-Keyword vergleicht hier die Identität, der Gleichheitsoperator == den Wert. Aber warum ist a is c wahr? das sind doch zwei verschiedene Variablen? Nun, Python ist recht ineffizient, und um die Effizienz wenigstens ein bisschen zu steigern, versucht es an manchen Stellen, Platz zu sparen und legt, wenn möglich, nur ein Objekt anstelle von zweien an. Die Zahl selber ist unveränderlich, also ist das auch kein Problem, nur halt nervig für eine Demonstration. Für den Vergleich von Strings habe ich also ein Kommandozeilenparameter verwendet, dass ich beim Aufruf des Skripts mit foo gefüttert habe:

d = "foo"
e = "bar"
f = "foo"
g = sys.argv[1]

print(f"d is e: {d is e}, d is f: {d is f}, d is g: {d is g}");
print(f"d == e: {d == e}, d == f: {d == f}, d == g: {d == g}");

// d is e: False, d is f: True, d is g: False
// d == e: False, d == f: True, d == g: True

Strings werden also korrekt verglichen, egal ob sie dasselbe Objekt sind oder nicht. Das ist ja schön. Mit Arrays (oder „Listen“ im Python-Jargon) und Wörterbüchern sieht es genauso aus (dieser Post wird lang, also spare ich mir mal den Codeschnipsel). Aber Python hat auch Klassen. Wie sieht das da aus?

class Foo:
    def __init__(self, a):
        self.a = a

l = Foo(42)
m = Foo(11)
n = Foo(42)

print(f"l is l: {l is l}, l is m: {l is m}, l is n: {l is n}")
print(f"l == l: {l == l}, l == m: {l == m}, l == n: {l == n}")
// l is l: True, l is m: False, l is n: False
// l == l: True, l == m: False, l == n: False

Mist. Es gibt hier eine Standardimplementierung von ==, und es ist die Identität. Das kann zu fiesen Fehlern führen. Aber immerhin können wir diese Implementierung überschreiben:

class Bar:
    def __init__(self, a):
        self.a = a

    def __eq__(self, other):
        return self.a == other.a

o = Bar(42)
p = Bar(11)
q = Bar(42)

print(f"o is p: {o is p}, o is q: {o is q}")
print(f"o == p: {o == p}, o == q: {o == q}")

print(f"o == l: {o == l}, l == o: {l == o}")

// o is p: False, o is q: False
// o == p: False, o == q: True
// o == l: True, l == o: True

Prima. Dann geht das. Wir können auch Bar mit Foo vergleichen, dank Duck-Typing. Ich bin nicht sicher, ob das gut ist. Jedenfalls ist es symmetrisch, obwohl ich nicht speziell darauf geachtet habe.

Fazit

Python bietet für viele eingebaute Typen eine Vergleichsfunktion an und ermöglicht es auch, die Vergleichsoperation zu überschreiben. Gefährlich wird es, wenn man das nicht tut und die Standardimplementierung (Identitätsvergleich) verwendet wird.

Zu dem Identitätsvergleich aus Versehen komme ich später noch einmal. Aber da wir gerade bei Python waren, bleiben wir mal bei Skriptsprachen und nehmen eine Sprache, die deutlich schlechter designed ist, aber trotzdem in jedem gängigen Browser läuft: Javascript

Javascript

Zunächst einmal: Javascript hat zwei Vergleichsoperatoren: == und ===. Ich weiß nicht, warum. Sicher ist: Ich will hier nicht viel über == sprechen, denn der Operator hat einige ungewöhnliche Verhaltensweisen, wenn man zwischen verschiedenen Typen vergleicht:

console.log(`"0" == false: ${"0" == false}`);
console.log(`"" == false: ${"" == false}`);
console.log(`"" == "0": ${"" == "0"}`);
// "0" == false: true
// "" == false: true
// "" == "0": false

Ups. "0" ist false. ""istfalse. Aber ""ist offensichtlich nicht"0". Nicht transitiv. Also gehen wir lieber über zu ===`, denn hier wird immer auch der Typ überprüft:

const a = 42;
const b = 11;
const c = 42;

console.log(`a === a: ${a === a}, a === b: ${a === b}, a === c: ${a === c}`);
// a === a: true, a === b: false, a === c: true

const d = "foo";
const e = "bar";
const f = "foo";

console.log(`d === d: ${d === d}, d === e: ${d === e}, d === f: ${d === f}`);
// d === d: true, d === e: false, d === f: true

const g = [1,2,3];
const h = [1,2];
const i = [1,2,3];

console.log(`g === g: ${g === g}, g === h: ${g === h}, g === i: ${g === i}`);
// g === g: true, g === h: false, g === i: false

const k = {foo: 42};
const l = {foo: 11};
const m = {foo: 42};

console.log(`k === k: ${k === k}, k === l: ${k === l}, k === m: ${k === m}`);
// k === k: true, k === l: false, k === m: false

Uff. Strings und Zahlen kann man noch gut vergleichen (Achtung: Technisch gesehen hat Javascript keine Ganzzahlen, nur Fließkommazahlen. Vergleiche zwischen effektiven Ganzzahlen funktionieren trotzdem gut). Schon bei Arrays hört es aber auf. Objekte (Wörtberbücher/Maps) machen auch nicht mit. Hier wird jeweils nur die Identität überprüft.

Eigene Gleichheitsoperatoren, die man dann mit === benutzen kann, kann man nicht definieren. Man kann natürlich Vergleichsfunktionen schreiben. Über Klassen reden wir nicht. Klassen in Javascript sind weird und niemand verwendet sie.

Fazit

Javascript vergleicht bei Arrays und Objects nur die Identität. Dieses Verhalten kann man nicht überschreiben. Man kann überhaupt keinen eigenen Vergleichsoperator definieren. Wenn der Identitätsvergleich nicht wäre, dann wäre es immerhin simpel, wenn auch unbequem. So ist es aber auch noch eine Falle. Gehen wir also weiter zum Namensvetter von Javascript, der außer dem Namen nichts mit Javascript zu tun hat: Java

Java

int a = 42; 
int b = 11; 
int c = 42; 

System.out.println(String.format("a == a: %b, a == b: %b, a == c: %b", a == a, a == b, a == c));
// a == a: true, a == b: false, a == c: true

Die Baseline funktioniert. Wie sieht es mit Arrays aus?

int[] d = {1,2,3};
int[] e = {1,2};
int[] f = {1,2,3};

System.out.println(String.format("d == d: %b, d == e: %b, d == f: %b", d == d, d == e, d == f));
System.out.println(String.format("compare(d, e) == 0: %b, compare(d, f) == 0: %b", Arrays.compare(d, e) == 0, Arrays.compare(d, f) == 0));
// d == d: true, d == e: false, d == f: false
// compare(d, e) == 0: false, compare(d, f) == 0: true

Na toll. Bei Arrays wird auch wieder nur die Identität verglichen. Warum? Keine Ahnung. Vermutlich, weil man sich so verhalten wollte wie C. Arrays sind hier immer nur Referenzen, aber keine Klassenobjekte. Also müssen wir eine andere Vergleichsfunktion nehmen als wir für Klassen nehmen würden. Maximal unpraktisch.

Aber Strings sind Klassen. Klassen haben doch sicher eine schöne Vergleichsoperation, oder?

String g = "foo";
String h = "bar";
String i = "foo";
String k = args[1];
System.out.println(String.format("g == h: %b, g == i: %b, g == k: %b", g == h, g == i, g == k));
System.out.println(String.format("g.equals(h): %b, g.equals(i): %b, g.equals(k): %b", g.equals(h), g.equals(i), g.equals(k)));
// g == h: false, g == i: true, g == k: false
// g.equals(h): false, g.equals(i): true, g.equals(k): true

Ich habe hier denselben Trick wie bei Python angewandt, um das Caching von statischen Strings zu umgehen: einen Kommandozeilenparameter, den ich mit foo gefüllt habe.

Also was sehen wir hier? Der ==-Operator vergleicht hier immer nur die Identität. Die Entwickler von Java haben hier eine Dreiklassengesellschaft (no pun intendet) eingeführt. Klassenobjekte, primitive Typen und Arrays. Nur primitive Typen werden als Wert behandelt, alles andere als Referenz. Für Arrays gibt es keine Methoden, also muss man das die Arrays.compare()-Funktion nehmen. Klassenobjekte vergleicht man mit der equals()-Methode. Aber wer verwendet schon Arrays in Java? Es gibt doch die praktische ArrayList-Klasse!

List<Integer> l = Arrays.asList(Integer.valueOf(1), Integer.valueOf(2));
List<Integer> m = Arrays.asList(Integer.valueOf(1));
List<Integer> o = Arrays.asList(Integer.valueOf(1), Integer.valueOf(2));
System.out.println(String.format("l == l: %b, l == m: %b, l == o: %b", l == l, l == m, l == o));
System.out.println(String.format("l.equals(l): %b, l.equals(m): %b, l.equals(o): %b", l.equals(l), l.equals(m), l.equals(o)));
// l == l: true, l == m: false, l == o: false
// l.equals(l): true, l.equals(m): false, l.equals(o): true

Immerhin. Die kann Vergleiche. Hier ist übrigens auch ein guter Grund, Arrays zu verwenden: Primitive Typen muss man in Klassen Wrappen, um sie in generischen Containerklassen zu verwenden. Es gibt einen Vorschlag für Generics über primitive Typen, aber da hat sich meines Wissens seit 2017 nicht viel getan. Aber ich schweife ab. Wie sieht es denn mit selbst definierten Klassen aus?

// Foo.java
public class Foo {
    final int a;

    public Foo(int a) {
        this.a = a;
    }
}

// in der main-Funktion:
Foo p = new Foo(42);
Foo q = new Foo(42);
System.out.println(String.format("p.equals(p): %b, p.equals(q): %b", p.equals(p), p.equals(q)));
// p.equals(p): true, p.equals(q): false

Selbes Problem wie in Python: Wenn nicht selbst implementiert, wird die equals-Funktion von Object geerbt. Jede Klasse in Java erbt von Object. Ich habe das immer für komisch gehalten, aber hier ist es regelrecht nervig. Also definieren wir die Funktion mal selbst:

// Foo2.java
public class Foo2 {
    final int a;

    public Foo2(int a) {
        this.a = a;
    }

    public boolean equals(Object other) {
        if (other == null) {
            return false;
        } else if (other.getClass() != this.getClass()) {
            return false;
        }
        Foo2 foo = (Foo2) other;

        return this.a == foo.a;
    }
}

// in der main-Funktion
Foo2 r = new Foo2(42);
Foo2 s = new Foo2(11);
Foo2 t = new Foo2(42);
System.out.println(String.format("r.equals(s): %b, r.equals(t): %b, r.equals(42): %b", r.equals(s), r.equals(t), r.equals(42)));
// r.equals(s): false, r.equals(t): true, r.equals(42): false

Das Positive zuerst: Es funktioniert. Das Negative: Weil wir equals von Object erben und überschreiben, muss die überschriebene Funktion natürlich auch Object als Parameter nehmen. Wir müssen also zur Laufzeit noch einen Typ-Check vornehmen, um sicherzustellen, dass wir das Object zu Foo2 casten können. Immerhin können wir hier auch mit anderen Typen vergleichen. Mit aktivierten Warnungen warnt der Compiler auch, dass wir hier hashCode überschreiben sollten, damit beide konsistent sind. Immerhin.

Fazit

Java ist kompliziert was Gleichheit angeht, hat je nach Metatyp unterschiedliche Wege, wie man vergleicht, vergleicht bei Referenztypen standardmäßig nur die Identität und macht es fummelig, den Gleichheitsoperator zu überschreiben. Ich bin froh, dass ich nicht mehr in dieser Sprache entwickeln muss. Stattdessen muss ich aber in Javas Schwestersprache entwickeln: C#

C#

Zunächst einmal: Ich programmiere erst seit knapp zwei Wochen in C#, deswegen würde ich alle Beispiele hier mit ein bisschen Vorsicht betrachten.

C# ist auf demselben Mist gewachsen wie Java und hat an vielen Stellen dieselben Fehler gemacht. An manchen Stellen aber auch andere Fehler. Gemein haben beide Sprachen eine Liebe für objektorientierte Programmierung und Boilerplate. Also: Fangen wir an.

int a = 42;
int b = 11;
int c = 42;
Console.WriteLine(string.Format("a == a: {0}, a == b: {1}, a == c: {2}", a == a, a == b, a == c));
// a == a: True, a == b: False, a == c: True

Keine Überraschungen. Strings?

string d = "foo";
string e = "bar";
string f = args[1];
Console.WriteLine(string.Format("d == d: {0}, d == e: {1}, d == f: {2}", d == d, d == e, d == f));
Console.WriteLine(string.Format("ReferenceEquals(d, d): {0}, ReferenceEquals(d, e): {1}, ReferenceEquals(d, f): {2}", Object.ReferenceEquals(d, d), Object.ReferenceEquals(d, e), Object.ReferenceEquals(d, f)));
// d == d: True, d == e: False, d == f: True
// ReferenceEquals(d, d): True, ReferenceEquals(d, e): False, ReferenceEquals(d, f): False

Ich habe hier wieder ein Kommandozeilenparameter (mit foo gefüllt) genommen. Aber immerhin: Strings kann man einfach mit dem Gleichheitsoperator vergleichen. Besser als Java. ReferenceEquals vergleicht explizit die Identität.

Arrays habe ich mal ausgelassen (bin nicht einmal sicher, in welcher Form die in dieser Sprache existieren) und bin direkt zu den Listenklassen gegangen:

List<int> g = new List<int>() { 1, 2, 3 };
List<int> h = new List<int>() { 1, 2 };
List<int> i = new List<int>() { 1, 2, 3 };
Console.WriteLine(string.Format("g == g: {0}, g == h {1}, g == i {2}", g == g, g == h, g == i));
Console.WriteLine(string.Format("g.Equals(g): {0}, g.Equals(h) {1}, g.Equals(i) {2}", g.Equals(g), g.Equals(h), g.Equals(i)));
// g == g: True, g == h False, g == i False
// g.Equals(g): True, g.Equals(h) False, g.Equals(i) False

…und da haben wir es wieder: Listen kann man nicht mit == vergleichen, das gibt wieder nur die Identität. Im Gegensatz zu Java funktioniert hier aber auch die Equals()-Funktion nicht. Also definieren wir mal selber eine Klasse und schauen, wie man die implementiert. Nein halt, erst einmal keine Klasse. C# unterscheidet zwischen class und struct. class wird auf dem Heap allokiert, struct auf dem Stack, oder kriegt einen Speicherbereich direkt und ohne Referenz in anderen Klassen und Structs. Warum man das am Typ festmachen soll und nicht dort, wo man den Typ verwendet, ist mir ein Rätsel, aber so ist es halt.

public struct Foos
{
    public Foos(int a)
    {
        A = a;
    }
    public int A { get; }
}

// woanders

Foos j = new Foos(42);
Foos k = new Foos(11);
Foos l = new Foos(42);
Console.WriteLine(string.Format("j == k: {0}, j == l: {1}", j == k, j == l));

Halt, nein, das gibt einen Compilerfehler: error CS0019: Operator '==' cannot be applied to operands of type 'Foos' and 'Foos'. Immerhin. Liegt aber leider daran, dass das hier kein Referenztyp ist. Equals hingegen vergleicht ordentlich, auch ohne zu überschreiben:

Console.WriteLine(string.Format("j.Equals(k): {0}, j.Equals(l): {1}", j.Equals(k), j.Equals(l)));
// j.Equals(k): False, j.Equals(l): True

Bei Klassen sieht das leider schon wieder anders aus:

public class Fooc
{
    public Fooc(int a)
    {
        A = a;
    }
    public int A { get; }
}

// woanders

Fooc m = new Fooc(42);
Fooc n = new Fooc(11);
Fooc o = new Fooc(42);
Console.WriteLine(string.Format("m == m: {0}, m == n: {1}, m == o: {2}", m == m, m == n, m == o));
Console.WriteLine(string.Format("m.Equals(m): {0}, m.Equals(n): {1}, m.Equals(o): {2}", m.Equals(m), m.Equals(n), m.Equals(o)));
// m == m: True, m == n: False, m == o: False
// m.Equals(m): True, m.Equals(n): False, m.Equals(o): False

Keine Überraschung, sieht so aus wie bei List.

public class Fooc2
{
    public Fooc2(int a)
    {
        A = a;
    }
    public int A { get; }
    public override bool Equals(object? other)
    {
        if (other == null || !(other is Fooc2))
        {
            return false;
        }
        Fooc2 o = (Fooc2)other;
        return this.A == o.A;
    }
}

// später

Fooc2 p = new Fooc2(42);
Fooc2 q = new Fooc2(11);
Fooc2 r = new Fooc2(42);
Console.WriteLine(string.Format("p == q: {0}, p == r: {1}", p == q, p == r));
Console.WriteLine(string.Format("p.Equals(q): {0}, p.Equals(r): {1}", p.Equals(q), p.Equals(r)));
// p == q: False, p == r: False
// p.Equals(q): False, p.Equals(r): True

Auch hier dasselbe Spiel wie bei Java: nimmt nur object? entgegen, man muss also manuell den Typ überprüfen. Aber die Sprache entwickelt sich weiter. Es gibt ein Interface, mit dem man Equals typsicher implementieren kann. Und man kann auch den ==-Operator überschreiben. Letzteres geht erst seit Kurzem Ich hatte hier zunächst den Mono-Compiler genommen, aber der kommt nur bis C#-6. Jetzt habe ich dotnet installiert und kann das Feature nutzen:

public class Fooc3 : IEquatable<Fooc3>
{
    public Fooc3(int a)
    {
        A = a;
    }
    public int A { get; }
    public bool Equals(Fooc3? other)
    {
        return other != null && this.A == other.A;
    }

    public static bool operator ==(Fooc3? left, Fooc3? right)
    {
        if (left is null)
        {
            return right is null;
        }
        return left.Equals(right);
    }

    public static bool operator !=(Fooc3? left, Fooc3? right)
    {
        if (left is null)
        {
            return right is not null;
        }
        return !left.Equals(right);
    }

    public override bool Equals(object obj)
    {
        if (ReferenceEquals(this, obj))
        {
            return true;
        }

        if (ReferenceEquals(obj, null))
        {
            return false;
        }

        throw new NotImplementedException();
    }
}

// woanders

Fooc3 s = new Fooc3(42);
Fooc3 t = new Fooc3(1);
Fooc3 u = new Fooc3(42);
Console.WriteLine(string.Format("s == t: {0}, s == u: {1}", s == t, s == u));
Console.WriteLine(string.Format("s.Equals(t): {0}, s.Equals(u): {1}", s.Equals(t), s.Equals(u)));
// s == t: False, s == u: True
// s.Equals(t): False, s.Equals(u): True

Erfolg! Es ist nur eine unglaubliche Menge Code. operator!= muss man implementieren, auch wenn der abgeleitet werden könnte. Ach ja, und as alte Equals wollte ich eigentlich überhaupt nicht implementieren. Das hat mir der Code-Formatter dotnet format mit eingeschleust. Vermutlich aus Gründen der Rückwärtskompatibilität. Aber nicht, was ich von einem Code-Formatter erwarten würde. An anderen Stellen hat er mir auch die GetHashCode()-Funktion eingeschmuggelt. Ohne Implementierung, und ohne mir Bescheid zu geben:

public override int GetHashCode()
{
    throw new NotImplementedException();
}

Immerhin weiß ich jetzt, warum dotnet format um eine paar Größenordnung langsamer ist als andere code formatter. Für dieses Mini-Projekt braucht es schon über 7 Sekunden (!). Zum Vergleich: cargo fmt braucht für meinen deutlich größeren Bloggenerator gerade mal etwa 470 Millisekunden. Um noch Bonus-Negativpunkte einzusammeln: Man muss dotnet format mehrmals laufen lassen, um alles hinzuzufügen. Beim ersten Durchlauf erkennt es, dass es eine Equals-Funktion hinzufügen muss, weil IEquatable implementiert ist (oder weil == implementiert ist? Keine Ahnung). Beim zweiten Durchlauf erkennt es dann, dass GetHashCode implementiert werden muss, weil Equals implementiert ist. Aber ich schweife schon wieder ab.

Compilerwarnung bekomme ich eine ganze Menge: Vergleich einer Variable mit sich selbst, fehlende Hashfunktion, fehlendes Equals. Immerhin ein Lichtblick.

Fazit

C# ist genau so ein Chaos wie Java, aber an anderen Stellen. Immerhin versucht es, besser zu werden schafft es aber nicht, sich dabei Altlasten zu entledigen. Wenn man die Altlasten wenigstens automatisch von den neueren Funktionen ableiten könnte… aber selbst dotnet format gibt nur eine mangelhafte Implementierung vor.

Jetzt hatten wir zwei alte, maschinennahe Sprachen, zwei Skriptsprachen, zwei von großen Konzernen gehypte und furchtbare Sprachen. Es folgen: Zwei moderne Sprachen, die ich beide lieber habe alle alle anderen Sprachen hier (mit Ausnahme vielleicht von Python, aber das kommt auf den Anwendungsfall an). Beginnen wir mit Go

Go

var a uint32 = 42
var b uint32 = 11
var c uint32 = 42
fmt.Printf("a == a: %t, a == b: %t, a == c: %t\n", a == a, a == b, a == c)
// a == a: true, a == b: false, a == c: true

Bisher hat mich hier noch keine Sprache überrascht, und das ist gut.

d := "foo"
e := "bar"
f := "foo"
fmt.Printf("d == e: %t, d == f: %t\n", d == e, d == f)
// d == e: false, d == f: true

Strings lassen sich vergleichen, gut.

var g [2]uint32 = [2]uint32{1, 2}
// var h [3]uint32 = [3]uint32{1,2,3};
var i [2]uint32 = [2]uint32{1, 2}
// g == h: type mismatch (differently sized arrays)
fmt.Printf("/*g == h: ?*/, g == i: %t\n" /*g == h,*/, g == i)
// /*g == h: ?*/, g == i: true

Arrays lassen sich vergleichen, sofern sie dieselbe Länge haben. Ähnlich wie in rust erkennt Go Arrays unterschiedlicher Länge als unterschiedliche Typen an. Gleichtypige Arrays lassen sich vergleichen.

//var k []uint32 = []uint32{1, 2}
//var l []uint32 = []uint32{1, 2, 3}
//var m []uint32 = []uint32{1, 2}
// Compiler error: `invalid operation: k == l (slice can only be compared to nil)`
//fmt.Printf("k == l: %t, k == m: %t\n", k == l, k == m);

Slices lassen sich nicht direkt vergleichen, man muss das manuell machen. Genau so sieht es für map aus.

type Foo struct {
    a uint32
}

// woanders

n := Foo{a: 42}
o := Foo{a: 11}
p := Foo{a: 42}
fmt.Printf("n == o: %t, n == p: %t\n", n == o, n == p)
// n == o: false, n == p: true

Structs kann man vergleichen, wenn alle Typen darin vergleichbar sind. Vorsicht: Bei Pointer werden die Pointer verglichen, nicht die Werte, auf die sie zeigen:

type Foop struct {
    a *uint32
}

// woanders
q := Foop{a: &a}
s := Foop{a: &b}
t := Foop{a: &c}
fmt.Printf("q == q: %t, q == s: %t, q == t: %t\n", q == q, q == s, q == t)
// q == q: true, q == s: false, q == t: false

Selber überschreiben kann man Gleichheitsoperatoren nicht. Es gibt ein Pseudo-Interface comparable, das mehr oder weniger zeitgleich mit den lang ersehnten Generics eingeführt wurde (die hatte Go, ähnlich wie Java, lange Zeit nicht). Was vergleichbar ist und was nicht ist relativ übersichtlich.

Fazit

Go hält es einfach. Einfach ist gut in der Programmierung. Eine Menge Probleme entstehen, weil Entwickler eine Komplexitätssucht entwickeln. Aber Go übertreibt es meiner Meinung nach mit der Einfachheit, so dass wiederum Probleme entstehen, die man mit ein bisschen mehr Komplexität nicht hätte. Dadurch wird es unbeabsichtlicht wieder komplizierter. Und ein paar Fallstricke gibt es trotzdem. Dennoch, obwohl man Gleichheitsoperationen nicht überschreiben kann, ist Go bisher mein Favorit. Aber mein persönlicher Goldstandard kommt erst noch:

Rust

Ich bin natürlich voreingenommen. Rust ist halt meine Lieblingssprache. Aber ich habe gute Argumente, warum rust sich hier am besten schlägt. Meine Voreingenommenheit zeigt sich also nicht darin, dass ich rust übermäßig bevorteile, sondern daher, dass ich das Problem so gestellt habe, dass rust gut darin abschneidet.

let a: u32 = 42;
let b: u32 = 11;
let c: u32 = 42;
println!("a == a: {}, a == b: {}, a == c: {}", a == a, a == b, a == c);

Langweilig! Die nächsten Beispiele bitte im Schnelldurchlauf, ich will langsam ins Bett!

let d: &str = "foo";
let e: &str = "bar";
let f: &str = "foo";
let g: String = "foo".to_string();
println!("d == e: {}, d == f: {}, d == g: {}", d == e, d == f, d == g); 
// d == e: false, d == f: true, d == g: true

let h: &[u32] = &[1, 2, 3]; 
let i: &[u32] = &[1, 2]; 
let j: &[u32] = &[1, 2, 3]; 
println!("h == i: {}, h == j: {}", h == i, h == j); 
// h == i: false, h == j: true

let k: [u32; 3] = [1, 2, 3]; 
let l: Vec<u32> = vec![1, 2, 3]; 
println!("h == k: {}, h == l: {}", h == k, h == l);
// h == k: true, h == l: true

Strings kann man vergleichen. Egal ob string slice oder String. Auch untereinander. Man kann slices miteinander vergleichen (take that, Go!). Man kann Slices mit Arrays vergleichen. Man kann Slices mit dem Vec-struct vergleichen. Alles funktioniert, solange der Typ innerhalb des Containers vergleichbar ist.

struct Foo(u32);

// woanders:

let m = Foo(42);
let n = Foo(42);
println!("m == n: {}", m == n);

Nope, das gibt einen Compilerfehler: „binary operation == cannot be applied to type Foo“. Wie gesagt, ich halte das für gut. Nicht alles kann vergleichbar sein. Aber man kann den das implementieren:

struct Fooman(u32);

impl PartialEq for Fooman {
    fn eq(&self, other: &Self) -> bool {
        self.0 == other.0
    }
}

// woanders

let m = Fooman(42);
let n = Fooman(11);
let o = Fooman(42);
println!("m == n: {}, m == o: {}", m == n, m == o);
// m == n: false, m == o: true

Schön. Aber das ist doch aufwändig. In den meisten Fällen sind zwei struct-Instanzen doch gleich, wenn alle Felder gleich sind. Dafür gibt es das derive-Makro:

#[derive(PartialEq, Eq)]
struct Fooder(u32);

// woanders
let p = Fooder(42);
let q = Fooder(11);
let r = Fooder(42);
println!("p == q: {}, p == r: {}", p == q, p == r);
// p == q: false, p == r: true

Erstellt die PartialEq-Implementierung zur Compilezeit. Funktioniert wunderbar. Und das Beste: Wenn man jetzt weitere Felder zum Struct hinzufügt, werden die automatisch mit in die Vergleichsfunktion aufgenommen. Weniger Spielraum für Fehler. Nur, wenn ein Feld in dem Struct nicht PartialEq implementiert, muss man manuell Hand anlegen.

PartialEq stellt dabei auch direkt noch den !=-Operator bereit. Den muss man also nicht manuell implementieren (könnte es aber, muss dann aber auf Konsistenz achten).

Eine Notiz noch am Rande: Es gibt hier zwei Traits (vergleichbar zu Interfaces in anderen Sprachen): PartialEq und Eq. PartialEq muss nicht reflexiv sein. Eq ist nur ein Marker ohne Funktionen, mit dem der Entwickler garantiert, dass die Operation auch reflexiv ist. Das heißt Gleitkommatypen (f32, f64) implementieren PartialEq, aber nicht Eq. Die meisten anderen Typen, die PartialEq implementieren, implementieren auch Eq.

Fazit

Rust kann alles, was die anderen Sprachen auch können, und vieles besser. Programmiererfehler werden abgefangen. Boilerplate wird reduziert. Trotzdem kann man überall Gleichheitsfunktionen haben.

Zusammenfassung

Meine Güte, ich habe jetzt schon wieder drei Stunden an diesem Artikel geschrieben. Und das schließt nicht die Zeit ein, die ich beim Implementieren der Codebeispiele verwendet habe. Die vollständigen Codebeispiele habe ich auf Gitlab hochgeladen. Fassen wir also zusammen:

  • C bietet kaum Hilfe, geht aber offen damit um
  • C++ bietet Operatorüberladung und macht es ganz ordentlich, man muss aber viel selber machen
  • Python hat den fiesen Identitäts-Default, man kann den Operator aber überladen und es ist einfach
  • Javascript ist Javascript
  • Java hat drei verschiedene Arten von Typen, die verglichen werden können, alle unterschiedlich. Objekte haben beim Vergleich den Identitäts-Default.
  • C# ist so ähnlich wie Java, an einzelnen Stellen schlechter, aber mit ein paar neuen Features besser, allerdings ohne den alten Krams loszuwerden
  • Go hält es simpel, aber vielleicht ein bisschen zu simpel. Man kann trotzdem gut damit arbeiten
  • rust bietet Sicherheit vor Programmiererfehlern, Solide Auto-Implementierungen, keine Überraschungen, es kostet nur ein bisschen Komplexität im Verhältnis zu Go

Rollenspielszenen: Löcher

Rollenspielszene. Wir sind immer noch in einer besetzten Stadt. Der Plan, in ein Sperrgebiet im Sperrgebiet einzudringen wird dadurch erschwert, dass eine Freundin eines Gruppenmitglieds von ihrer ehemaligen Gang (alles Teenager) als Geisel gehalten wird, bis sich unser Gruppenmitglied stellt und sich von der Gang umbringen lässt.

Wir versuchen, zu verhandeln (Spielleiter: „Mit Teenagern?“). Die Szene spielt auf einem Schrottplatz. Unsere Verhandlunsgführerin betritt den Schrottplatz, achtet dabei auf Fallen, patzt, und landet in einem Loch.

Eine andere Gruppe von uns versucht sich (das war nicht Teil des Plans) auf den Schrottplatz zu schleichen (sie zerschneiden dabei einen Zaun). Einer der beiden patzt bei seinem Stealth-Check und fällt ebenfalls in ein Loch. Ein Gangmitglied wird auf ihn aufmerksam. Er gibt sich als Kunde des Schrottplatzes aus, der, äh… Paletten kaufen will, genau! Der Teen sieht eine Gelegenheit für Geld, und verlangt erst einmal 500 Drachmen für den kaputten Zaun.

Das mit dem Zaun kostet 500 Drachmen. Es ist keine gute Zeit in der Zaunbranche.

Gruppenmitglied bietet an, den Zaun zu reparieren:

Ne, du musst mir jetzt die 500 Drachmen geben, sonst muss ich dich zusammenschlagen.

Es kommt zur Rangelei. Drittes Gruppenmitglied versucht, den Gang-Teen in das Loch zu stoßen. Patzt, landet selber im Loch.

Teen kriegt ein Messer an den Hals gehalten, besteht aber auf seinen 500 Drachmen. Unsere Verhandlungsführerin bekommt mit, was passiert, geht zu den beiden mit vibes einer „pissed-off mom, trying to separate the kids“. Will alle nach Hause schicken.

Sie schafft es am Ende die Geisel freizukriegen. Auf dem Rückweg fällt der Typ mit dem gepatzten Stealth-Check noch einmal in ein Loch. Spielleiter:

We'll call this junkyard “the trauma dump”

Lehrerstellen in Baden-Württemberg durch Softwarefehler unbesetzt

Schlagzeile: „1.440 Lehrerjobs in BW jahrelang unbesetzt - Kultusministerin will Stellen schnell besetzen“. Warum?

Der Grund sollen Programmierfehler im Personal- und Stellenprogramm der Kultusverwaltung im Jahr 2005 gewesen sein, heißt es in einer gemeinsamen Mitteilung des Kultus- und Finanzministeriums. "Diese Fehler blieben seither unbemerkt."

Aha. Softwarefehler halt. Kann man nichts machen (wenn fefe nicht gerade krank wäre, würde er sich mit begeisterter Schadenfreude auf diesen Fall stürzen). Was genau der Fehler war? Weiß man noch nicht:

Die Ministerien wollen nun untersuchen, wie es zu der Fehlerkette kommen konnte. Die Vermutung: Bei der Übertragung der Daten zehntausender Lehrerinnen und Lehrer im Landesamt für Besoldung und Versorgung in ein neues Computersystem habe es einen Programmierfehler gegeben.

Wäre vielleicht etwas für den Digitale Anomalien-Podcast. Wenn denn die Ursache einmal feststeht. Als Informatiker habe ich selbst natürlich auch ein professionelles Interesse daran: Wie konnte das, technisch im Detail betrachtet, passieren, und wieso ist es nicht früher aufgefallen?

Lehrerverbände sind jetzt natürlich stinkig. Das Finanzministerium gibt aber Entwarnung:

Das Finanzministerium versicherte, durch die Panne sei kein finanzieller Schaden entstanden. Da die Stellen nicht besetzt wurden, sei auch kein Geld abgeflossen.

Kein finanzieller Schaden. Das ist ja gut. Naja, außer bei den ganzen Lehrkräften, die nicht eingestellt wurden. Oder bei den Schüler_innen, die durch schlechtere Ausbildung weniger Geld verdienen. Oder für die Wirtschaft insgesamt, weil die Leute weniger qualifiziert sind. Aber das zählt ja nicht, hauptsache die Staatskasse wurde nicht belastet.

In der Regierung sei man sich nun einig, dass die 1.440 Stellen vollständig nachbesetzt werden sollen.

Ja immerhin.