Stranger Than Usual

Wir hoffen, dass Sie Ihre Energie nun darauf verwenden, Ihr Sicherheitsniveau an die gängigen Standards dieses Jahrhunderts anzuheben.

Linus Neumann (CCC) an d-trust, Januar 2025

Rollenspielszenen: Sprengstoffspürhund

Rollenspielszene. Wir sind immer noch in einer besetzten Stadt und dringen gerade in ein Militärlager ein. Lilian, ein Teenager aus unserer Gruppe (Warum nehmen wir das Kind mit? Weil sie sonst ohne uns mitkommen würde.) hat den Sprengstoff, den Ariadne (eine Bastlerin) eigentlich beschlagnahmt hatte, entwendet um Bomben damit zu bauen und die auf die Mission mitzunehmen.

Wir haben uns zu nächtlicher Zeit erfolgreich in das Lager eingeschlichen (während überall in der Stadt Aufstände ausbrechen). Wir begegnen einem Hund, der sich gerade an einer Mülltonne vollfrisst. Als er uns bemerkt, schnüffelt er kurz und geht dann gezielt auf Lilian los. Alle versuche in abzulenken, scheitern zunächst, der Hund schnüffelt aufgeregt an Lilians Tasche, bevor er Alarm geben kann, wird er dann doch mit Essen abgelenkt.

Ariadne: „Lilian, was hast du da in deiner Tasche?“. Ein Blick in die Tasche bestätigt: Fünf Bomben, eine davon mit einer gefährlich kurzen Zündschnur. Ariadne lässt Lilian die Bomben, was soll man auch jetzt noch machen? Aber wir reden noch mal drüber! Und die mit der kurzen Zündschnur wird nicht benutzt!

Die irreführende Fahrradampel

Ein nennenswerter Teil meiner Mobilität besteht aus Fahrradfahren. So fahre ich zum Beispiel zum Einkaufen oft mit dem Fahrrad. Besonders, wenn der Supermarkt, zu dem ich will, etwas weiter weg liegt. Radfahren ist aber nicht immer ohne Probleme. Eins davon ist die Kreuzung Ardeystraße / Johannisstraße in Witten:

Eine Kreuzung zweier mehrspuriger Straßen. Der Beobachter steht auf dem Bürgersteig an einer Ecke der Kreuzung. Auf der Kreuzung sind in Rot Radwege eingezeichnet. Für jede Richtung gibt es eine Einbuchtung für indirektes Linksabbiegen.

Diese Kreuzung legt den Radfahrer_innen indirektes Linksabbiegen nahe. Das Konzept kenne ich schon länger, den Begriff habe ich erst durch Recherchen zu diesem Artikel kennengelernt. Indirektes Linksabbiegen bedeutet, dass man an der Kreuzung zunächst geradeausfährt und sich dann in die Spur, die von rechts kommt, einsortiert. Der Vorteil ist, dass man ein direktes Linksabbiegen vermeiden kann, was je nach Kreuzung und Verkehrslage wirklich gruselig sein kann. Der Nachteil ist, dass man zwei Ampelphasen warten muss.

In diesem Fall kümmern mich aber weder der Vorteil noch der Nachteil, sondern die konkrete Umsetzung. Ich hätte hier eine Luftaufnahme, aber die ist von Google Maps. Google erlaubt zwar die nicht-kommerzielle Nutzung unter korrekter Quellenangabe, aber das Internet™ gibt unklare Auskünfte darüber, wie es mit Luftaufnahmen aussieht, die Google von Drittparteien lizenziert hat. Also stattdessen eine Art abgepaustes SVG der Kreuzung. Die Zahlen und Pfeile werden weiter unten erklärt:

Abstrakte Skizze der Kreuzung aus dem Foto oben. Die Kreuzung ist X-förmig. Rot eingezeichnet sind die auf dem Foto oben zu sehenden Fahrradstreifen. Ein grüner Pfeil zieht sich durchs Bild. Er beginnt an einer Straße unten links (markiert mit einer 2), führt über die Kreuzung zu einer der Einbuchtungen zum indirekten Linksabbiegen (markiert mit einer 3) und zeigt am Ende in die Straße links oben. Die Straßenecke rechts ist markiert mit einer 1, die Straßenecke oben ist markiert mit einer 4.

Das Foto oben ist (wie die anderen Fotos in diesem Post) von der mit einer grünen 1 markierten Stelle aufgenommen. Der grüne Pfeil zeigt einen Weg, den man zum Linksabbiegen nehmen kann. Ich fahre dort also erst einmal von Position 2 geradeaus und reihe mich dann entsprechend dem Abbiegepfeil bei 3 zum Linksabbiegen ein. Ich will, sobald meine Ampel grün wird, nach obenLinks fahren, also richte ich mich entsprechend aus. Nun ist die eigentliche Ampel für diese Kreuzung hinter mir, ich kann also nicht sehen, wann ich losfahren darf. Aber kein Problem, denn bei 4 ist eine Fahrradampel (hier fotografiert vom Bürgersteig, nicht von der Straße, als Abbieger mit dem Fahrrad stehe ich links des Bildausschnittes.):

Ein Fußgängerüberweg an einer Kreuzung. In der Mitte des Bildes, auf der gegenüberliegenden Straßenseite, steht eine Fahrradampel.

Also warte ich darauf, dass die Ampel grün wird. Es wird grün für die Fußgängerampel. Es wird grün für die Autofahrer hinter mir. Es wird grün für die Abbieger. Nur für mich wird es nicht grün, obwohl alle anderen, die in die gleiche Richtung wollten, schon grün hatten. Endlich wird sie grün! Ich fahre los. Komme über die Kreuzung, muss aber hart bremsen, weil die Fußgänger quer zu mir gleichzeitig grün haben. Und nicht nur das. Nur Momente danach braust hinter mir der Querverkehr vorbei.

Ich steige ab und rette mich mit dem Fahrrad auf den Gehweg. Während ich noch über die komische Ampelschaltung schimpfe, merkt ein Passant an, dass diese Kreuzung dafür sogar schon mal in der Zeitung stand.

Die richtige Ampel für mich war nämlich nicht die Ampel bei 4. Es wäre eine Ampel bei 1 gewesen:

Eine Fahrradampel in Nachaufnahme. Die Ampel ist rot. Über dem roten Ampellist ist ein weiteres Ampelelement mit einem blass weiß leuchtenden Pfeil.

Wenn man genau hinschaut, sieht man sogar den Pfeil, der nach links zeigt. Die Ampel hat nur ein Problem: Wenn ich micht korrekt eingeordnet habe, ist die Ampel rechts hinter mir, ungefähr auf 4 Uhr. Da sehe ich sie nicht. Stattdessen sehe ich die Ampel gegenüber, die genau in meine Richtung ausgerichtet ist. Den blassen weißen Pfeil nach links kann man schnell übersehen. Immerhin sind die einzigen anderen weißen Lichtsignale im Straßenverkehr ausschließlich für öffentliche Verkehrsmittel.

Keine Ahnung, wer das für eine gute Idee gehalten hat. Die Kreuzung, wie sie jetzt ist, ist noch relativ neu. Die WAZ hat sie „Wirrwarrkreuzung“ genannt, in einem Artikel, den ich nicht verlinke, weil man den Artikel nicht lesen kann ohne seine Seele zu verkaufen.

Ein Fahrradblogger aus der Nachbarstadt Bochum hingegen meint in einem Blogpost, diese Kreuzung in Witten sei korrekt nach Empfehlungen angelegt worden. In dem Artikel beklagt er sich über Kreuzungen mit indirektem Linksabbiegen, in denen es für Radfahrer_innen extrem schwierig gemacht wird, das richtig zu machen.

Aber Empfehlungen hin oder her, ich finde diese Ampeln extrem verwirrend und wäre beinahe überfahren worden, weil die falsche Ampel so aussah, als ob sie die Ampel für mich sei. Meiner Meinung nach muss das anders gemacht werden.

Entengrütze

Heute war ich mal wieder im botanischen Garten der RUB. Da gibt es auch einige Anlagen für Wasserpflanzen. Die meisten sind gezielt bepflanzt, andere werden momentan anscheinend nicht aktiv genutzt. Das hier ist eine davon:

Ein rechteckiger Teich, der komplett mit Entengrütze bedeckt ist. Im Vordergrund ein Schild: „Offene Wasserfläche VORSICHT“.

Der Wasserspiegel ist fast auf einer Höhe mit dem Boden drumherum, das Ding ist komplett bedeckt mit Entengrütze (auch als Wasserlinsen bekannt). Auf einen flüchtigen Blick erkennt man sie also nicht als Wasserflächen, deswegen die Warnschilder.

Ein paar Minuten, nachdem ich an diesem Teich vorbeigekommen bin, hörte ich Stimmen und Geräusche:

„Halt ihn fest, das ist Wasser!“ (lautes, platschendes Geräusch) „Hohl in raus, HOHL IHN RAUS!“ (weiter platschendes Geräusch) „Er ist ganz nass!“.

Ich habe die Szene nicht gesehen, aber ich habe ein ziemlich genaues Bild davon, was dort passiert ist :)

Cloning Blues

Als ich einem Kollegen meine Idee für den kürzlich veröffentlichtem Blogpost über Vergleichsoperationen in verschiedenen Programmiersprachen erzählt habe, meinte er, ein Post über tiefe Kopien in verschiedenen Sprachen fände er viel interessanter. Ich fand die Idee auch interessant, und hatte mir ein paar Pläne gemacht.

Nun hat der Vergleichsoperations-Blogpost so viel Zeit gekostet, dass ich keine Motivation habe, das zu wiederholen. Also hier eine Kurzabhandlung.

Tiefe Kopien von Datenstrukturen sind Kopien, in denen wirklich von allem in der Datenstruktur eine Kopie gemacht wird, rekursiv die Hierarchie hinab. Im Gegensatz zu einer flachen Kopie, wo nur auf oberster Ebene die Referenzen bzw. Pointer kopiert werden. Im zweiten Fall sollte man dan tunlichst nichts an den Daten in der Kopie verändern, wenn man will, dass das Original unverändert bleibt. Also genau das, was man vermutlich machen möchte, wenn man eine Kopie erzeugt.

Wie gehen verschiedene Sprachen das an? Für C muss man zum Beispiel alles Kopieren von Daten hinter Pointern von Hand machen. Als C-Entwickler_in ist man so etwas aber gewöhnt. C++ hat Standardkopieoperationen für Klassen, die je nachdem, was wie sich die Datentypen in der Klasse kopieren, tiefe oder flache Kopien machen. Will man gesichert eine tiefe Kopie, muss man den Zuweisungsoperator überschreiben. Und den Kopierkonstruktor. Und vermutlich auch den Dekonstruktor. Und dann ist es recht einfach, aus Versehen eine teure tiefe Kopie zu machen, wenn man eigentlich nur ein Objekt weiterreichen möchte.

In rust ist die Sache natürlich wieder schön gelöst. Standardmäßig keine tiefen Kopien, oder überhaupt Kopien. Entweder man gibt das Eigentum an einer Variable weiter, oder man verleiht die Variable. Wenn man kopieren will, muss man das Clone-trait implementieren. Ähnlich wie beim Gleichheitsoperator kann man das aber auch automatisch machen lassen, wenn alle unterelemente des Types Clone implementieren. clone() muss explizit aufgerufen werden, also keine teuren, tiefen Kopien aus Versehen. Bonuspunkte für Copy, das man implementieren kann, wenn die flache Kopie identisch mit der tiefen Kopie ist, so dass man leichter Sachen kopieren kann anstatt das Eigentum weiterzugeben.

Die meisten heutigen Sprachen arbeiten aber grundsätzlich erst einmal mit Referenzen. Spezielle Kopieroprationen muss man den ganzen Baum hinab manuell implementieren. Java insbesondere hat es fies gemacht: Jedes Objekt hat die clone()-Funktion, die keine Typsicherheit bietet und immer eine Exception wirft, wenn man nicht das Cloneable-Interface implementiert, das lustigerweise keine Funktionen beinhaltet, auch nicht clone(). Ich glaube, es ist in Java Konsens, clone() nicht zu benutzen.

Ist aber auch egal. Ich würde für solche Sprachen (mit garbage collector und mit der zwanghaften Neigung, ausschließlich Referenzen herumzureichen) folgendes Vorschlagen: Macht alle Klassen immutable (unveränderlich, read-only). Dann macht es keinen Unterschied mehr zwischen flacher und tiefer Kopie, weil niemand in den Daten herumpfuschen kann.

Genau genommen würde ich diesen Ansatz auch für andere Sprachen empfehlen. Es gibt da noch ein paar Optimierungsmöglichkeiten (z.B. Copy-On-Write für manche Anwendungsfälle, Pestistent data structures für andere), aber insgesamt ist man mit unveränderbaren Objekten auf der sicheren Seite. Nur in rust gibt der Compiler dir Rückendeckung im Umgang mit veränderbaren Daten. Das ist aber mit einigen Einschränkungen verbunden, deswegen ist es am Ende doch wieder leichter, nur unveränderliche Daten herumzureichen.

Kurz: Macht eure Daten immutable, dann braucht ihr sie nicht zu klonen. Wenn eure Sprache das nicht zulässt (ich schaue dich an, Go), dann habt ihr Pech gehabt.

Pacman contribution graph für GitHub und GitLab

Wer mit GitHub oder GitLab arbeitet kennt diesen contribution graph: Das ist eine Matrix aus Feldern, mit einem Feld pro Tag des Jahres, in dem jedes Feld einen Farbton abhängig von der Anzahl commits hat, die ein Benutzer an diesem Tag gemacht hat.

Ein Arbeitskollege von mir hat ein Programm geschrieben, dass diesen Graphen in ein animiertes Pacman-Spiel umwandelt. Man kann es nicht spielen (es spielt sich von alleine) aber es ist animiert. Die Animation wird entweder auf ein canvas gezeichnet oder (eigentlich die coolere Variante) als animiertes SVG ausgegeben.

Der Kollege hat in einem Vortrag auch kurz erzählt, wie er das gemacht hat. So hat er zum Beispiel mit ein paar Tricks die Dateigröße der ausgegebenen SVG von über 5 MiB auf etwa 250 kiB verringert. Jemand anderes hat ihm außerdem ein paar Patches geschrieben, die das Verhalten der unterschiedlichen Geister mehr an das Originalspiel gebracht haben.

Ich würde hier gerne ein Beispiel zeigen, aber 250 kiB sind immer noch eine Menge, aber vor allem ziehen animierte SVG einiges an Rechenleiistung. Ich möchte nicht, dass jemand, der meine Seite aufruft, alleine deswegen 50% und mehr Auslastung auf einem CPU-Kern hat.

Trotzdem: ich liebe solche Projekte. Der Code ist auf GitHub, da kann man sich auch eine Beispielanimation anschauen.

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.

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”

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

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.

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.