 Ja, willkommen zurück zu Programmierparadigmen. Wir sind immer noch im Modul Typ-Systeme und hier im 5. Teil dieses Moduls soll es um die Frage gehen, wann Typen denn eigentlich miteinander kompatibel sind und wie wir sehen werden, ist es nicht dasselbe wie das Typen equivalent zueinander sind, sondern kann noch ein bisschen mehr bedeuten. Was bedeutet Typ-Kompatibilität? Also im Prinzip bedeutet das, dass wir überprüfen, ob es valide ist, ob es legal ist, zwei Werte miteinander zu kombinieren in der Programmiersprache. Was heißt jetzt kombinieren? Da gibt es im Prinzip drei Varianten, was das heißen kann in den üblichen Programmiersprachen. Das eine ist in Assignments. Also wenn ich den Wert eine Variable, eine andere Variable zuweise, dann wird überprüft, ob die linke Seite und die rechte Seite dieser Zuweisung tatsächlich miteinander kompatibel sind, denn nur dann soll die Zuweisung tatsächlich stattfinden, denn nur dann ist sie auch typkorekt. Die zweite Szenarie, wo das eine Rolle spielt, ist für Operatoren. Also wenn ich zum Beispiel eine binäre Operation habe, dann möchte ich überprüfen, ob die beiden Operanten, die ich in dieser Operation verbinde, tatsächlich auch kompatibel sind und ob deren Typen kompatibel sind und genau das kann ich über die Typ-Kompatibilität dann machen. Der dritte Fall, wo das eine Rolle spielt, ist bei Funktionsaufrufen, denn da übergebe ich ja, wenn ich Argumente übergebe, diese Argumente an die formalen Parameter und hier ist auch wieder die Frage, sind denn diese Argumente, die ich da übergebe mit den formalen Parametern tatsächlich kompatibel und insbesondere sind die Typkompatibel und auch diese Frage wird hoffentlich in diesem Teil der Vorlesung beantwortet. Was ganz wichtig ist, ist, dass man versteht, dass kompatibel nicht dasselbe ist wie gleich und auch nicht dasselbe wie equivalent. In den meisten Sprachen, die es so gibt, sind nämlich Typen oft kompatibel, auch wenn sie eben nicht gleich und auch nicht vollständig equivalent sind. Hier ist mal ein Beispiel aus C, wo wir einfach mit so verschiedenen Zahlen ein bisschen spielen. Was wir da zunächst machen ist, eine Double Variable zu definieren und deklarieren. Das heißt, diese Variable speichert einfach Floating Point Zahlen in Double Größe ab. Anschließend benutzen wir diese Double Variable, um einen Wert zu berechnen, indem wir sie einfach mit zwei multiplizieren, speichern das Ganze dann aber in eine Float Variable. Das heißt, in auch wieder Floating Point, aber mit weniger Bits und anschließend speichern wir das Ganze hier noch in ein Integer, wandeln also diese Floating Point Variable in eine Ganzzahl um und all das ist in C legal, denn diese verschiedenen Typen sind eben miteinander kompatibel. Und schlussendlich geben wir das Ganze dann hier aus. Wer will, kann ja einfach mal kurz drüber nachdenken, was das Ganze dann ausgibt und vielleicht auch einfach mal kurz in C aufschreiben, kompilieren und schauen, was dabei dann rauskommt. Ob solche Zuweisungen und Operationen, die wir gerade eben in dem Beispiel gesehen haben, jetzt tatsächlich in der bestimmten Sprache legal sind, hängt von der Sprache ab und jede Programmiersprache definiert im Prinzip ihren eigenen Satz von Regeln, die definieren, welche Typen dann miteinander kompatibel sind. Ich habe hier mal ein paar Beispiele für solche Regeln, die dann natürlich in bestimmten Sprachen gelten oder eben auch nicht gelten können. Also diese Regeln gelten nicht in allen Sprachen, sondern sind einfach nur Beispiele. Eine solche Regel ist, dass man in einem Assignment immer ein Wert eines Subtypes an eine Variable, eine Supertypes zuweisen kann. Das heißt, wenn ich zum Beispiel zwei Klassen habe, A und dann eine Subklasse von A namens B und habe eine Variable vom Typ A auf der linken Seite des Assignments, dann könnte ich auf der rechten Seite des Assignments eine Instanz vom Typ B haben. Und das wäre legal, weil B ja dann an den Supertypen A zugewiesen wird und in vielen Sprachen zumindest die Subtypen mit den Supertypen kompatibel sind in Assignments. Eine andere Beispiel für so eine Regel ist, dass verschiedene Typen um Zahlen zu repräsentieren miteinander kompatibel sind. Und das Beispiel, was wir gerade eben in C gesehen haben, hat das glaube ich ganz gut illustriert, so dass man einfach Zahlen hin und her konvertieren kann, ohne da jedes Mal einen Typ Fehler zu bekommen. Noch ein anderes Beispiel ist in Sprachen, wo wir Collections haben, also zum Beispiel Listen von Ding, dann sind in der Regel die Collections die Elemente des selben Types enthalten, zum Beispiel zwei Disten von Strings miteinander kompatibel, selbst wenn sie zum Beispiel unterschiedliche Längen haben. Also eigentlich, wenn man die Struktur jetzt nur anschauen würde, vielleicht doch ein bisschen unterschiedlich repräsentiert sind, aber da es eben doch Collections des selben Types sind, sind die eben oft kompatibel. So, jetzt haben wir gesagt, Typen sind kompatibel, auch wenn sie nicht gleich sind. Es ist aber die Frage, wenn sie denn nicht gleich sind, wie kann man sie dann zum Beispiel zueinander zuweisen oder wie kann man sie dann in derselben Operation verwenden? Und die Antwort hier ist, dass die Typen konvertiert werden müssen, um sie dann eben wirklich auch im Speicher kompatibel zu haben. Um das zu machen, gibt es im Prinzip zwei Varianten, die wir im letzten Teil der Verlösung ja auch schon mal kurz angesprochen haben. Variante eins ist das Cast, beziehungsweise die explizite Typkonvertierung, wo der Programmierer explizit angibt, dass ein Wert vom Typ T1 zu einem Wert vom Typ T2 umgewandelt werden soll. Und dadurch, dass das explizit angegeben wird, ist das natürlich auch im Source Code dann sehr gut ersichtlich. Die zweite Variante, die manchmal ein bisschen weniger gut ersichtlich ist, ist die implizite Typkonvertierung, beziehungsweise die Type Coercion. Was hier passiert, ist, dass die Programmiersprache es erlaubt, ein Wert vom Typ T1 in einer Situation zu verwenden, wo eigentlich ein Wert vom Typ 2 erwartet wird. Und was die Programmiersprache Implementierung dann für uns macht, ist diese Typkonvertierung implizit, ohne es wirklich sichtbar zu machen, anzuwenden. Das kann gut sein, denn es erspart einiges an Schreibarbeit, aber kann manchmal auch ein bisschen verwirren sein und wir schauen gleich noch ein paar Beispiele dazu an. Was wichtig ist, dass in beiden Fällen die tatsächliche Konvertierung des Typs zur Laufzeit stattfindet. Das heißt, in beiden Fällen findet eine Konvertierung statt, nur das eine mal steht es explizit im Code drin und das andere mal eben nicht. Zum Thema explizite Typkonvertierung haben wir am letzten Teil der Vorlesung schon ein bisschen was gesehen, deswegen schauen wir uns jetzt hier mal die impliziten Konvertierung beziehungsweise die Type Coercions ein bisschen genauer an. Eine Sprache, wo das recht häufig auftritt, ist in C, deswegen fangen wir mal mit der an. Was hier passiert ist, dass die meisten primitiven Typen, die diese Sprache anbietet, implizit konvertiert werden, wann immer das nötig ist. Also wenn wir als Programmierer einfach mal einen primitiven Typen statt dem anderen verwenden, dann findet oft eine implizite Konvertierung statt. Diese impliziten Konvertierung oder Coercions können unter Umständen Informationen verlieren. Also wenn ich zum Beispiel eine Variable vom Typ float habe und die dann in einen Int umwandle, dann wird eben die Nachkommastellen, die werden dann abgeschnitten und ich verliere die sozusagen, also da geht Informationen verloren. Ähnlich ist es, wenn ich Typen habe, die eigentlich gleich repräsentiert werden, aber in unterschiedlichen Größen gespeichert werden. Also zum Beispiel, wenn ich von Int, was 32 bits hat, zu charg, was nur 8 bits hat, dann wird dieser Wert oder Umständen ja ein Overflow verursachen. Und ich bekomme dann unter Umständen in dieser Variable, die dann ein char ist, nicht das raus, was ich eigentlich erwarte. Je nach Compiler gibt es da oft Warnungen, die einem vor solchen Überraschungen vielleicht ein bisschen warnen, aber die muss man dann erstens auch ernst nehmen. Und zweitens sind das eben nur Warnungen und keine Typfehler. Das heißt, ich kann den Code natürlich trotzdem kompilieren. Hier ist mal so eine kleine Übersicht von primitiven Typen in C und wie die konvertiert werden können. Also im Prinzip kann man all diese Typen ineinander überführen, implizit. Und so die Grundregel ist, dass wenn ich von unten nach oben konvertiere, also hin zu den kleineren Typen, es oft zu irgendeiner Form von Verlust kommt. Also das sind eigentlich die Konvertierungen, bei denen man ein bisschen aufpassen muss und gegen die Konvertierung von oben nach unten in der Regel zu dem führt, was man erwartet, weil eben kein Informationsverlust stattfindet. Schauen wir uns das Ganze mal anhand von einem konkreten Beispiel an, und zwar ein paar Zahlen C Code, die wir hier sehen. Also was wir hier haben, sind zwei Variablen int n und character c. Und wir initialisieren dieses Integer einfach mal, indem wir 2 hoch 10 plus 2 da reinschreiben. Jetzt muss man wissen, dass Integer 32 bits hat. Das heißt, diese Zahl, die daraus kommt 1026, passt da auf jeden Fall rein. Character hat als Typ jetzt allerdings nur 8 bits. Das heißt, wenn wir diesen Wert, der hier in n steht, dann an C zuweisen, dann ist das typtechnisch erstmal okay. Das ist, weil diese Typen eben kompatibel sind, typkorekt. Allerdings findet eben diese implizierte Konvertierung statt. Das heißt, wir können die 1026, die wir in n haben, eben nicht so in die Variable c speichern, weil die einfach nicht reinpasst, sondern es findet dann ein Overflow statt. Und wir bekommen einen Wert, den wir vielleicht nicht erwarten. Hier unten geben wir die zwei Werte mal aus und um einfach zu zeigen, was da passiert, kompiliere ich das jetzt mal. Und wenn wir es dann ausführen, dann sehen wir eben, aha, okay, in n haben wir tatsächlich immer noch diese 1026 drin. Also das, was man erwartet, weil das haben wir reingeschrieben. Aber in der Character Variable c haben wir eben nur den Wert 2. Ganz einfach, weil die 1026 eben nicht in die 8 bits reinpasst, sondern wir dann den Overflow haben und eben nur die zwei übrig bleibt. Das heißt, man muss aufpassen, wenn man diese impliziten Konvertierung hat, weil manche Compiler, wie meiner jetzt zum Beispiel hier, dann noch nicht mal eine Warnung ausgeben, sondern der Wert dann halt ganz einfach nicht mehr der Wert ist, den wir vielleicht vorher mal reingeschrieben haben. Eine andere Programmiersprache, wo Typ Konvertierung implizit sehr, sehr häufig stattfinden, ist JavaScript. Also wir hatten ja ganz am Anfang dieses Modules zum Thema Typ Systeme schon ein kleines Beispiel, wo ich das ein bisschen motiviert habe. In JavaScript ist es so, dass eigentlich fast alle Typen ineinander konvertiert werden können implizit. Das heißt, wann auch immer ich vielleicht den falschen Typen verwende, passiert eigentlich erstmal nichts offensichtlich Falsches. Also insbesondere wird das Programm nicht abstürzen, sondern der Wert wird irgendwie konvertiert in den Zieltyp, den ich da brauche. Der Gedanke dahinter ist, dass JavaScript da viel auf Webseiten verwendet wird und Webseiten möglichst nicht abstürzen sollen, sondern selbst wenn da vielleicht mal irgendwo was schief geht, dann ist wahrscheinlich der Rest der Seite trotzdem irgendwie noch nutzbar. Und deswegen hat JavaScript diese No Crash Philosophie. Also es läuft fast immer einfach weiter und stürzt eben nicht ab. Viele dieser impliziten Konvertierungen, die da stattfinden, machen auch sehr viel Sinn und erleichtern eigentlich das Programmieren. Also dieses Beispiel hier unten, wo ich ein String benutze und den dann mit Plus verbinde mit einer Zahl. Da bekommt ganz einfach der String raus, wo dann die Zahl hinten dran gehangen wurde. Und das ist genau das, was man als Programmierer in der Situation eigentlich auch erwartet. Und es ist gut, dass man da vielleicht nicht unbedingt einen expliziten Cast hinschreiben muss, um diese Zahl 3 dann tatsächlich erst in diesen String umzuwandeln, sondern das einfach so aufschreiben kann. Es gibt allerdings auch Beispiele, wo es verhalten alles andere als intuitive ist. Also wenn ich zum Beispiel so ein Array aus zwei Zahlen 1 und 2 nehme und dann ein Left-Shift da drauf anwende, mit diesem String in dem 2 drin steht, dann kommt, ja wer hätte es gedacht, null raus. Die Frage ist, warum genau ist das so? Das ist ja wohl definiert. Also die JavaScript Sprachspezifikation schreibt genau vor, was in diesem Fall hier zu geschehen hat. Allerdings ist es nicht das, was man als Programmierer jetzt vielleicht erwarten würde, weil semantisch macht es eigentlich wenig Sinn, einen Array mit einem String zu Left-Shiften, weil das einfach keinen Sinn macht. Aber die Sprache hat es zumindest definiert, so dass falls jemand mal sowas aufschreibt, dass tatsächlich trotzdem ausgeführt wird, ohne dass das Programm dann abstürzt. Wer sich dann noch mehr für interessiert, wir hatten da vor ein paar Jahren mal ein Paper zu, wo wir geschaut haben, inwiefern diese impliziten Typkonvertierung auf echten Webseiten dann auftreten. Und was wir gesehen haben, ist, dass tatsächlich viele, viele von diesen impliziten Konvertierungen stattfinden, aber die meisten doch tatsächlich eher, wie die hier oben sind, also sagen wir mal harmlose Konvertierung, die man auch versteht und relativ wenige von diesen doch sehr unintuitiven Konvertierungen stattfinden. Das heißt, obwohl diese Sprache da sehr, sehr liberal ist, passieren eigentlich relativ wenige schlechte Sachen deswegen. Ja, das soll es auch schon wieder gewesen sein zum Thema Typkompatibilität und inwiefern, die dann erreicht wird, indem wir manchmal die Typen implizit oder explizit ineinander konvertieren. Ich hoffe, Sie haben was gelernt. Vielen Dank für's Zuhören und dann bis zum 6. und letzten Teil dieses Moduls zum Thema Typsysteme.