Kotlin: tiefer graben. Konstruktoren und Initialisierer



    Bereits im Mai 2017 gab Google bekannt, dass Kotlin zur offiziellen Sprache für die Android-Entwicklung geworden ist. Jemand hörte dann den Namen dieser Sprache zum ersten Mal, jemand schrieb lange darauf, aber von diesem Moment an wurde klar, dass jeder, der sich mit Android-Entwicklung auskennt, nun einfach dazu verpflichtet ist, sie kennenzulernen. Darauf folgten die beiden begeisterten Antworten „Endlich!“ Und die schreckliche Empörung „Warum brauchen wir eine neue Sprache? Dann hat Java nicht gefallen? ", Etc. usw.

    Seitdem ist genug Zeit vergangen, und obwohl die Debatte darüber, ob ein guter Kotlin oder ein schlechter Kotlin noch nicht nachgelassen hat, immer mehr Android-Code darauf geschrieben wird. Und auch ganz konservative Entwickler wechseln dazu. Darüber hinaus kann das Netzwerk auf Informationen stolpern, dass die Entwicklungsgeschwindigkeit nach dem Beherrschen dieser Sprache um 30% im Vergleich zu Java erhöht wird.

    Heute hat sich Kotlin bereits von mehreren Kinderkrankheiten erholt, hat viele Fragen und Antworten auf den Stack Overflow erhalten. Seine Vor- und Nachteile wurden mit bloßem Auge sichtbar.

    Auf dieser Welle hatte ich die Idee, die einzelnen Elemente einer jungen, aber beliebten Sprache detailliert zu analysieren. Achten Sie auf komplexe Probleme und vergleichen Sie sie mit Java, um die Übersichtlichkeit und das Verständnis zu verbessern. Verstehen Sie die Frage etwas tiefer, als dies durch das Lesen der Dokumentation geschehen kann. Wenn dieser Artikel von Interesse ist, wird er höchstwahrscheinlich einen ganzen Artikelzyklus initiieren. In der Zwischenzeit beginne ich mit ziemlich grundlegenden Dingen, die jedoch viele Fallstricke verbergen. Sprechen Sie über Konstruktoren und Initialisierer in Kotlin.

    Wie in Java erfolgt in Kotlin die Erstellung neuer Objekte - Entitäten eines bestimmten Typs - durch Aufruf des Klassenkonstruktors. Argumente können auch an den Konstruktor übergeben werden, und es können mehrere Konstruktoren vorhanden sein. Wenn Sie diesen Prozess wie von außen betrachten, besteht der einzige Unterschied zu Java darin, dass das Schlüsselwort new beim Aufruf des Konstruktors fehlt. Schauen wir uns jetzt genauer an, was in der Klasse passiert.

    Eine Klasse kann primäre (primäre) und zusätzliche (sekundäre) Konstruktoren haben.
    Der Konstruktor wird mit dem Schlüsselwort "Konstruktor" deklariert. Wenn der primäre Konstruktor keine Zugriffsmodifizierer und Anmerkungen enthält, kann das Schlüsselwort weggelassen werden.
    Eine Klasse hat möglicherweise keine expliziten Konstruktoren. In diesem Fall gibt es nach der Klassendeklaration keine Konstruktionen. Wir gehen sofort zum Hauptteil der Klasse. Wenn wir eine Analogie zu Java ziehen, entspricht dies dem Fehlen einer expliziten Deklaration von Konstruktoren. Dies führt dazu, dass der Standardkonstruktor (ohne Parameter) automatisch bei der Kompilierung generiert wird. Es sieht erwartungsgemäß so aus:

    classMyClassA

    Dies entspricht dem folgenden Datensatz:

    class  MyClassA constructor()

    Wenn Sie jedoch so schreiben, werden Sie höflich aufgefordert, den primären Konstruktor ohne Parameter zu entfernen.

    Der primäre Konstruktor ist derjenige, der immer aufgerufen wird, wenn ein Objekt erstellt wird (sofern vorhanden). Vorläufig nehmen wir dies zur Kenntnis, werden es jedoch später genauer analysieren, wenn wir zu den sekundären Konstruktoren übergehen. Dementsprechend erinnern wir uns daran, dass, wenn es überhaupt keine Designer gibt, es tatsächlich einen (primären) gibt, aber wir sehen es nicht.

    Wenn wir möchten, dass der primäre Designer ohne Parameter keinen öffentlichen Zugriff hat, müssen Sie ihn zusammen mit der Änderung privatebereits explizit mit dem Schlüsselwort deklarieren constructor.

    Das Hauptmerkmal des primären Konstruktors besteht darin, dass er keinen Körper hat, d. H. kann keinen ausführbaren Code enthalten. Es übernimmt einfach die Parameter und gibt sie zur weiteren Verwendung tief in die Klasse ein. Auf der Syntaxebene sieht das so aus:

    class  MyClassA constructor(param1: String, param2: Int, param3: Boolean){
      // some code
    }

    Auf diese Weise übergebene Parameter können für verschiedene Initialisierungen verwendet werden, jedoch nicht mehr. In seiner reinen Form können wir diese Argumente nicht im Code der Arbeitsklasse verwenden. Wir können jedoch die Klassenfelder hier direkt initialisieren. Es sieht so aus:

    class  MyClassA constructor(val param1: String, var param2: Int, param3: Boolean){
      // some code
    }

    Hier param1und param2Sie können den Code als Klasse - Feld verwenden, die den folgenden entsprechen:

    class  MyClassA constructor(p1: String, p2: Int, param3: Boolean){
      val param1 = p1
      var param2 = p2
      // some code
    }

    Wenn wir es mit Java vergleichen, würde es so aussehen (und übrigens können wir anhand dieses Beispiels abschätzen, wie viel Kotlin die Menge an Code reduzieren kann):

    publicclassMyClassAJava{
      privatefinal String param1;
      private Integer param2;
      public MyClassAJava(String p1, Integer p2, Boolean param3) {
         this.param1 = p1;
         this.param2 = p2;
      }
      public String getParam1() {
         return param1;
      }
      public Integer getParam2() {
         return param2;
      }
      public void setParam2(final Integer param2) {
         this.param2 = param2;
      }
      // some code
    }
    

    Sprechen wir über zusätzliche Konstrukteure. Sie ähneln eher regulären Konstruktoren in Java: Sie nehmen Parameter an und können einen ausführbaren Block haben. Bei der Deklaration zusätzlicher Konstruktoren ist das Schlüsselwort constructorerforderlich. Wie bereits erwähnt, muss trotz der Möglichkeit, ein Objekt durch einen Aufruf an einen zusätzlichen Konstruktor zu erstellen, der primäre Konstruktor (falls vorhanden) auch mithilfe eines Schlüsselworts aufgerufen werden this. Auf der Syntaxebene ist es folgendermaßen organisiert:

    classMyClassA(val p1: String) {
      constructor(p1: String, p2: Int, p3: Boolean) : this(p1) {
         // some code
      }
      // some code
    }
    

    Ie der zusätzliche Konstruktor ist sozusagen der Nachfolger des primären.
    Wenn wir nun ein Objekt durch Aufrufen eines zusätzlichen Konstruktors erstellen, wird Folgendes angezeigt: ein

    Aufruf an einen zusätzlichen Konstruktor;
    den Hauptkonstruktor aufrufen;
    Initialisierung des Klassenfeldes p1im Hauptkonstruktor;
    Codeausführung im Rumpf eines zusätzlichen Konstruktors.

    Dies ist ähnlich in Java:

    classMyClassAJava{
      privatefinal String param1;
      publicMyClassAJava(String p1){
         param1 = p1;
      }
      publicMyClassAJava(String p1, Integer p2, Boolean param3){
         this(p1);
         // some code
      }
      // some code
    }
    

    Es sei daran erinnert, dass wir in Java einen Konstruktor von einem anderen mit einem Schlüsselwort thisnur am Anfang des Konstruktorkörpers aufrufen können . In Kotlin wurde diese Frage hauptsächlich entschieden - sie machten einen solchen Aufruf als Teil der Konstruktorsignatur. Nur für den Fall stelle ich fest, dass es verboten ist, einen (primären oder zusätzlichen) Konstruktor direkt vom zusätzlichen Body aus aufzurufen.

    Ein zusätzlicher Konstruktor sollte immer auf den Hauptkonstruktor verweisen (sofern verfügbar), kann dies jedoch indirekt tun, indem er sich auf einen anderen zusätzlichen Konstruktor bezieht. Die Quintessenz ist, dass wir am Ende der Kette immer noch zur Hauptsache gelangen. Das Auslösen der Konstruktoren erfolgt offensichtlich in umgekehrter Reihenfolge der Konstrukteure, die sich zueinander drehen:

    class MyClassA(p1: String) {
      constructor(p1: String, p2: Int, p3: Boolean) : this(p1) {
         // some code
      }constructor(p1: String, p2: Int, p3: Boolean, p4: String) : this(p1, p2, p3) {
         // some code
      }// some code
    }
    

    Nun ist die Reihenfolge:

    • einen zusätzlichen Konstruktor mit 4 Parametern aufrufen;
    • einen zusätzlichen Konstruktor mit 3 Parametern aufrufen;
    • den primären Konstruktor aufrufen;
    • Initialisierung des p1-Klassenfelds im primären Konstruktor;
    • Codeausführung im Konstruktorkörper mit 3 Parametern;
    • Codeausführung im Rumpf des Konstruktors mit 4 Parametern.

    In jedem Fall wird der Compiler niemals vergessen, zum primären Konstruktor zu gelangen.

    Es kommt vor, dass eine Klasse keinen primären Konstruktor hat und möglicherweise einen oder mehrere zusätzliche. Dann müssen zusätzliche Konstruktoren nicht auf jemanden verweisen, sie können jedoch auf andere zusätzliche Konstruktoren dieser Klasse verweisen. Zuvor haben wir herausgefunden, dass der nicht explizit angegebene Hauptkonstruktor automatisch generiert wird. Dies gilt jedoch für Fälle, in denen überhaupt keine Konstruktoren in der Klasse vorhanden sind. Wenn es mindestens einen zusätzlichen Konstruktor gibt, wird der primäre Konstruktor ohne Parameter nicht erstellt:

    classMyClassA{
    // some code
    } 
    

    Wir können ein Klassenobjekt erstellen, indem wir Folgendes aufrufen:

    val myClassA = MyClassA()

    In diesem Fall:

    classMyClassA{
      constructor(p1: String, p2: Int, p3: Boolean)  {
         // some code
      }
      // some code
    }
    

    Wir können ein Objekt nur mit einem solchen Aufruf erstellen:

    val myClassA = MyClassA(“some string”, 10, True)

    In dieser Hinsicht gibt es in Kotlin im Vergleich zu Java nichts Neues.

    Übrigens kann ein zusätzlicher Konstrukt wie der primäre Konstruktor keinen Körper haben, wenn er lediglich Parameter an andere Konstruktoren übergeben soll.

    classMyClassA{
      constructor(p1: String, p2: Int, p3: Boolean) : this(p1, p2, p3, "")
      constructor(p1: String, p2: Int, p3: Boolean, p4: String) {
         // some code
      }
      // some code
    }
    

    Sie sollten auch darauf achten, dass im Gegensatz zum primären Konstruktor die Initialisierung der Klassenfelder in der Liste der Argumente des zusätzlichen Konstruktors verboten ist.
    Ie Ein solcher Datensatz ist ungültig:

    classMyClassA{
      constructor(val p1: String, var p2: Int, p3: Boolean){
         // some code
      }
      // some code
    }
    

    Unabhängig davon ist es erwähnenswert, dass der zusätzliche Konstruktor ebenso wie der primäre Konstruktor ohne Parameter sein kann:

    classMyClassA{
      constructor(){
         // some code
      }
      // some code
    }
    

    Apropos Konstruktoren: Man kann nur eine der praktischen Funktionen von Kotlin erwähnen - die Möglichkeit, den Argumenten Standardwerte zuzuweisen.

    Nehmen wir an, wir haben eine Klasse mit mehreren Konstruktoren, die eine unterschiedliche Anzahl von Argumenten haben. Ich werde ein Beispiel in Java geben:

    publicclassMyClassAJava{
      private String param1;
      private Integer param2;
      privateboolean param3;
      privateint param4;
      publicMyClassAJava(String p1){
         this (p1, 5);
      }
      publicMyClassAJava(String p1, Integer p2){
         this (p1, p2, true);
      }
      publicMyClassAJava(String p1, Integer p2, boolean p3){
         this(p1, p2, p3, 20);
      }
      publicMyClassAJava(String p1, Integer p2, boolean p3, int p4){
         this.param1 = p1;
         this.param2 = p2;
         this.param3 = p3;
         this.param4 = p4;
      }
    // some code
    }
    

    Wie die Praxis zeigt, sind solche Designs durchaus üblich. Mal sehen, wie man auf Kotlin dasselbe schreiben kann:

    classMyClassA(var p1: String, var p2: Int = 5, var p3: Boolean = true, var p4: Int = 20){
      // some code 
    }
    

    Lassen Sie uns Kotlin nun zusammenfassen, wie viel er den Code geschnitten hat. Neben der Verringerung der Anzahl der Zeilen erhalten wir übrigens mehr Ordnung. Denken Sie daran, sicher haben Sie so etwas gesehen:

    publicMyClassAJava(String p1, Integer p2, boolean p3){
         this(p3, p1, p2, 20);
      }
      publicMyClassAJava(boolean p1, String p2, Integer p3, int p4){
      // some code 
      }
    

    Wenn Sie das sehen, möchten Sie die Person finden, die das Dokument geschrieben hat. Nehmen Sie es an der Schaltfläche, führen Sie zum Bildschirm und fragen Sie mit trauriger Stimme: „Warum?“
    Obwohl Sie dies auf Kotlin wiederholen können, ist dies nicht der Fall.

    Es gibt jedoch ein Detail, das bei einem solchen abgekürzten Datensatz in Kotlin berücksichtigt werden sollte: Wenn wir einen Konstruktor mit Standardwerten von Java aus aufrufen möchten, müssen wir ihm eine Anmerkung hinzufügen @JvmOverloads:

    class MyClassA @JvmOverloads constructor(var p1: String, var p2: Int = 5, var p3: Boolean = true, var p4: Int = 20){
      // some code
    }

    Ansonsten erhalten wir einen Fehler.

    Nun reden wir über Initialisierer .

    Ein Initialisierer ist ein mit einem Schlüsselwort markierter Codeblock init. In diesem Block können Sie eine gewisse Logik bei der Initialisierung von Klassenelementen ausführen, einschließlich der Werte der Argumente, die in den primären Konstruktor übernommen wurden. Wir können auch Funktionen aus diesem Block aufrufen.

    Java hat auch Initialisierungsblöcke, aber das ist nicht dasselbe. In ihnen können wir nicht wie in Kotlin den Wert von außen übergeben (die Argumente des primären Konstruktors). Der Initialisierer ist dem Hauptteil des primären Konstruktors sehr ähnlich und wird in einem separaten Block gerendert. Aber es ist auf einen Blick. In der Tat ist das nicht ganz richtig. Lass es uns herausfinden.

    Ein Initialisierer kann auch vorhanden sein, wenn kein primärer Konstruktor vorhanden ist. Wenn ja, wird der Code wie alle Initialisierungsprozesse vor dem Code des zusätzlichen Konstruktors ausgeführt. Es kann mehr als einen Initialisierer geben. In diesem Fall stimmt die Reihenfolge ihres Anrufs mit der Reihenfolge ihres Standorts im Code überein. Beachten Sie auch, dass die Initialisierung von Klassenfeldern außerhalb von Blöcken erfolgen kann init. In diesem Fall erfolgt die Initialisierung auch entsprechend der Anordnung der Elemente im Code, und dies muss beim Aufruf von Methoden aus dem Initialisierungsblock berücksichtigt werden. Wenn Sie dies sorglos einnehmen, besteht die Möglichkeit, dass Sie auf einen Fehler stoßen.

    Ich werde einige interessante Fälle der Arbeit mit Initialisierern anführen.

    classMyClassB{
      init {
         testParam = "some string"
         showTestParam()
      }
      init {
         testParam = "new string"
      }
      var testParam: String = "after"constructor(){
         Log.i("wow", "in constructor testParam = $testParam")
      }
      funshowTestParam(){
         Log.i("wow", "in showTestParam testParam = $testParam")
      }
    }
    

    Dieser Code ist durchaus gültig, wenn auch nicht ganz offensichtlich. Wenn Sie es betrachten, können Sie sehen, dass die Zuordnung eines Werts zu einem Feld testParamim Initialisierungsblock erfolgt, bevor der Parameter deklariert wird. Das funktioniert übrigens nur, wenn wir einen zusätzlichen Konstruktor in der Klasse haben, aber keinen primären Konstruktor (wenn wir die Felddeklaration testParamoberhalb des Blocks anheben init, funktioniert das auch ohne den Konstruktor). Wenn wir Bytecode dieser Klasse in Java dekompilieren, erhalten Sie Folgendes:

    publicclassMyClassB{
      @NotNullprivate String testParam = "some string";
      @NotNullpublicfinal String getTestParam() {
         returnthis.testParam;
      }
      publicfinal void setTestParam(@NotNull String var1) {
         Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
         this.testParam = var1;
      }
      publicfinal void showTestParam() {
         Log.i("wow", "in showTestParam testParam = " + this.testParam);
      }
      public MyClassB() {
         this.showTestParam();
         this.testParam = "new string";
         this.testParam = "after";
         Log.i("wow", "in constructor testParam = " + this.testParam);
      }
    }
    

    Hier sehen wir, dass der erste Aufruf eines Feldes während der Initialisierung (in einem Block initoder außerhalb davon) seiner üblichen Initialisierung in Java entspricht. Alle anderen Aktionen, die sich auf die Zuweisung eines Werts im Initialisierungsprozess beziehen, mit Ausnahme der ersten (die erste Zuweisung des Werts wird mit der Deklaration des Felds kombiniert) werden an den Konstruktor übergeben.
    Wenn Sie Experimente mit Dekompilierung durchführen, stellt sich heraus, dass, wenn es keinen Konstruktor gibt, der primäre Konstruktor generiert wird und die gesamte Magie darin stattfindet. Wenn es mehrere zusätzliche Konstruktoren gibt, die sich nicht aufeinander beziehen, und es keinen primären Konstruktor gibt, werden im Java-Code dieser Klasse alle nachfolgenden Zuordnungen des testParamFeldwerts in allen zusätzlichen Konstruktoren dupliziert. Wenn der primäre Konstruktor vorhanden ist, dann nur im primären. Puh ...

    Und für einen Snack das interessanteste: Ändern Sie die Signatur testParamvon varbis val:

    classMyClassB{
      init {
         testParam = "some string"
         showTestParam()
      }
      init {
         testParam = "new string"
      }
      val testParam: String = "after"constructor(){
         Log.i("wow", "in constructor testParam = $testParam")
      }
      funshowTestParam(){
         Log.i("wow", "in showTestParam testParam = $testParam")
      }
    }
    

    Und irgendwo im Code nennen wir:

    MyClassB myClassB = new MyClassB();
    

    Alles wurde ohne Fehler kompiliert, gestartet und hier sehen wir die Ausgabe der Protokolle:

    in showTestParam testParam = irgendein String
    im Konstruktor testParam = after

    Es stellt sich heraus, dass das deklarierte Feld valden Wert im Prozess der Codeausführung geändert hat. Warum so? Ich denke, dass dies ein Manko des Kotlin-Compilers ist, und in der Zukunft wird dies vielleicht nicht kompiliert werden, aber heute ist alles so, wie es ist.

    Wenn Sie aus den oben genannten Fällen Rückschlüsse ziehen, können Sie nur raten, keine Initialisierungsblöcke zu erstellen und nicht in der Klasse zu streuen, eine Neuzuweisung von Werten im Initialisierungsprozess zu vermeiden, nur reine Funktionen aus den Init-Blöcken aufzurufen. All dies wird getan, um mögliche Verwirrung zu vermeiden.

    SoInitialisierer sind eine Art Codeblock, der zwingend ausgeführt wird, wenn ein Objekt erstellt wird, unabhängig davon, mit welchem ​​Konstruktor dieses Objekt erstellt wird.

    Es scheint zu verstehen. Betrachten Sie das Zusammenspiel von Konstruktoren und Initialisierern. Innerhalb einer Klasse ist alles ganz einfach, aber Sie müssen sich daran erinnern:

    • einen zusätzlichen Konstruktor aufrufen;
    • den primären Konstruktor aufrufen;
    • Initialisierung von Klassenfeldern und Initialisierungsblöcken in der Reihenfolge ihrer Position im Code;
    • Codeausführung im Rumpf eines zusätzlichen Konstruktors.

    Interessanter sind die Fälle mit Vererbung.

    Es ist erwähnenswert, dass Object als Basis für alle Klassen in Java gilt, also Any in Kotlin. Any und Object sind jedoch nicht dasselbe.

    Für einen Anfang wie Vererbung auftritt. Eine untergeordnete Klasse kann wie die übergeordnete Klasse einen primären Konstruktor haben oder nicht, muss sich jedoch auf einen bestimmten Konstruktor der übergeordneten Klasse beziehen.

    Wenn eine untergeordnete Klasse über einen primären Konstruktor verfügt, muss dieser Konstruktor auf einen bestimmten Konstruktor der Basisklasse zeigen. Gleichzeitig müssen alle zusätzlichen Konstruktoren der Erbenklasse auf den Hauptkonstruktor ihrer Klasse verweisen.

    classMyClassC(p1: String): MyClassA(p1) {
      constructor(p1: String, p2: Int): this(p1) {
         //some code
      }
      //some code
    }
    

    Wenn eine untergeordnete Klasse keinen primären Konstruktor hat, muss jeder der zusätzlichen Konstruktoren mithilfe eines Schlüsselworts auf den Konstruktor der übergeordneten Klasse verweisen super. Gleichzeitig können verschiedene zusätzliche Konstruktoren der Erbenklasse auf andere Konstruktoren der Elternklasse verweisen:

    class MyClassC : MyClassA {
      constructor(p1: String): super(p1) {
         //some code
      }constructor(p1: String, p2: Int): super(p1, p2) {
         //some code
      }//some code
    }
    

    Vergessen Sie auch nicht die Möglichkeit, den Konstruktor der übergeordneten Klasse über andere Konstruktoren der Erbenklasse indirekt aufzurufen:

    class MyClassC : MyClassA{
      constructor(p1: String): super(p1){
         //some code
      }constructor(p1: String, p2: Int): this (p1){
         //some code
      }//some code
    }
    

    Wenn die Erbenklasse keine Konstruktoren enthält, fügen Sie nach dem Erbenklassennamen einfach einen Aufruf zum Konstruktor der übergeordneten Klasse hinzu:

    classMyClassC: MyClassA(“some string”) {
      //some code
    }
    

    Es gibt jedoch immer noch eine Variante mit Vererbung, bei der kein Verweis auf den Konstruktor der übergeordneten Klasse erforderlich ist. Ein solcher Datensatz ist gültig:

    class MyClassC : MyClassB {
      constructor(){
         //some code
      }constructor(p1: String){
      }//some code
    }
    

    Dies gilt jedoch nur, wenn die übergeordnete Klasse über einen Konstruktor ohne Parameter verfügt. Dies ist der Standardkonstruktor (primär oder optional - nicht wichtig).

    Betrachten Sie nun die Reihenfolge des Aufrufens von Initialisierern und Konstruktoren während der Vererbung:

    • den zusätzlichen Erbauer des Erben nennen;
    • den primären Erbauer des Erben anrufen;
    • einen zusätzlichen übergeordneten Konstruktor aufrufen;
    • Aufruf des primären Konstruktors des übergeordneten Elements;
    • Ausführung von initübergeordneten Blöcken ;
    • Ausführung des Bodycodes des zusätzlichen Konstruktors des übergeordneten Elements;
    • Ausführung des initErbenblocks;
    • Ausführung des Bodycodes des zusätzlichen Erstellers

    Lassen Sie uns mehr über den Vergleich mit Java sprechen, bei dem es tatsächlich kein Analogon zum primären Konstruktor von Kotlin gibt. In Java sind alle Designer gleich und können voneinander aufgerufen werden. In Java und Kotlin gibt es einen Standardkonstruktor, er ist auch ein Konstruktor ohne Parameter, er erhält jedoch nur während der Vererbung einen besonderen Status. Hier sollten Sie Folgendes beachten: Beim Erben in Kotlin müssen wir der Erbenklasse explizit mitteilen, welcher übergeordnete Klassenkonstruktor verwendet werden soll - der Compiler lässt uns das nicht vergessen. In Java können wir dies nicht explizit angeben. Seien Sie vorsichtig: In diesem Fall wird der Standardkonstruktor der übergeordneten Klasse (falls vorhanden) aufgerufen.

    An dieser Stelle gehen wir davon aus, dass wir die Designer und Initialisierer gründlich studiert haben und jetzt fast alles über sie wissen. Lass uns ein wenig ausruhen und in die andere Richtung graben!

    Jetzt auch beliebt: