GNU/Linux >> LINUX-Kenntnisse >  >> Linux

Gewusst wie:Objektorientierte Programmierung – Konstruktoren und Vererbung

Früher

Wir haben fast das Ende unserer Reise durch diese Bonusrunde unserer Einführung in die objektorientierte Programmierung erreicht! In unserem letzten Artikel haben wir unser Streichelzoo-Beispiel mit einem Überblick über Klassen, Objekte und Methoden begonnen. Wenn Sie den Anfang dieser Serie verpasst haben, können Sie uns hier nachholen. Ansonsten tauchen wir wieder ein!


.

Konstruktoren

Wenn Sie die Übung mit zwei Dog gemacht haben Objekte, es war ein bisschen langweilig, oder? Schließlich haben wir nichts, um die Hunde voneinander zu trennen, und können ohne Blick auf den Quellcode nicht wissen, welcher Hund welches Bellen erzeugt hat.

Im vorherigen Artikel habe ich erwähnt, dass Sie beim Erstellen von Objekten eine spezielle Methode namens Konstruktor aufrufen . Der Konstruktor sieht aus wie der als Methode geschriebene Klassenname. Zum Beispiel für einen Dog Klasse würde der Konstruktor Dog() heißen .

Das Besondere an Konstruktoren ist, dass sie der Pfad zu jedem neuen Objekt sind und sich daher hervorragend zum Aufrufen von Code eignen, der ein Objekt mit Standardwerten initialisiert. Außerdem ist der Rückgabewert einer Konstruktormethode immer ein Objekt der Klasse selbst, weshalb wir den Rückgabewert des Konstruktors einer Variablen des von uns erstellten Klassentyps zuweisen können.

Bisher haben wir jedoch noch keinen Konstruktor erstellt, wie kommt es also, dass wir diese Methode immer noch aufrufen können?

In vielen Sprachen, einschließlich C#, bietet Ihnen die Sprache einen kostenlosen und leeren Konstruktor, ohne dass Sie etwas tun müssen. Es wird vorausgesetzt, dass Sie einen Konstruktor benötigen; Andernfalls gäbe es keine Möglichkeit, die Klasse für irgendetwas zu verwenden, also gehen die Sprachen einfach davon aus, dass Sie eine geschrieben haben.

Dieser unsichtbare und freie Konstruktor wird Standardkonstruktor genannt , und in unserem Beispiel sieht es so aus:

public Dog(){ }

Beachten Sie, dass diese Syntax sehr ähnlich zu Speak() ist -Methode, die wir zuvor erstellt haben, außer dass wir weder explizit einen Wert zurückgeben noch den Rückgabetyp der Methode deklarieren. Wie ich bereits erwähnt habe, gibt ein Konstruktor immer eine Instanz der Klasse zurück, zu der er gehört.

In diesem Fall ist das die Klasse Dog , und deshalb schreiben wir Dog myDog = new Dog() , können wir das neue Objekt einer Variablen namens myDog zuweisen welches vom Typ Dog ist .

Also fügen wir unserem Dog den Standardkonstruktor hinzu Klasse. Sie können entweder die obige Zeile kopieren oder in Visual Studio eine Verknüpfung verwenden:Geben Sie ctor ein und drücken Sie Tab zweimal. Es sollte den Standardkonstruktor für Sie generieren, wie in Abbildung 9 gezeigt:

Abbildung 9:Hinzufügen eines Konstruktors mit „ctor“

Der Standardkonstruktor gibt uns eigentlich nichts Neues, weil er jetzt explizit tut, was zuvor implizit getan wurde. Da es sich jedoch um eine Methode handelt, können wir jetzt Inhalte in die Klammern einfügen, die ausgeführt werden, wenn wir diesen Konstruktor aufrufen. Und da der Konstruktor als allererstes bei der Konstruktion eines Objekts ausgeführt wird, ist er ein perfekter Ort, um Initialisierungscode hinzuzufügen.

Zum Beispiel könnten wir den Name setzen Eigenschaft unserer Objekte zu etwas hinzufügen, indem Sie Code wie diesen hinzufügen:

public Dog()
{
    this.Name = "Snoopy";
}

In diesem Beispiel wird der Name festgelegt Eigenschaft aller neuen Objekte an „Snoopy“.

Das ist natürlich nicht sehr nützlich, da nicht alle Hunde „Snoopy“ heißen, also ändern wir stattdessen die Methodensignatur des Konstruktors so, dass sie einen Parameter akzeptiert.

Die Klammern von Methoden sind nicht nur da, um hübsch auszusehen; Sie dienen dazu, Parameter zu enthalten, mit denen wir Werte an eine Methode übergeben können. Diese Funktion gilt für alle Methoden, nicht nur für Konstruktoren, aber machen wir es zuerst für einen Konstruktor.

Ändern Sie die Standardkonstruktorsignatur wie folgt:

public Dog(string dogName)

Dieser Zusatz ermöglicht es uns, einen string zu senden Parameter in den Konstruktor, und wenn wir das tun, können wir auf diesen Parameter mit dem Namen dogName verweisen .

Fügen Sie dann dem Methodenblock die folgende Zeile hinzu:

this.Name = dogName;

Diese Zeile legt die Eigenschaft Name dieses Objekts fest zu dem Parameter, den wir an den Konstruktor gesendet haben.

Beachten Sie, dass Sie, wenn Sie die Signatur des Konstruktors ändern, einen Fall von roten Schnörkeln in Ihrer Datei „Program.cs“ erhalten, wie in Abbildung 10 gezeigt.

Abbildung 10:Ein Fall der roten Squigglies von unserem neuen Konstruktor

Wenn wir unsere eigenen expliziten Konstruktoren hinzufügen, erstellen C# und .NET nicht implizit einen Standardkonstruktor für uns. In unserer Datei Program.cs erstellen wir immer noch den Dog Objekte, die den parameterlosen Standardkonstruktor verwenden, der jetzt nicht mehr existiert.

Um dieses Problem zu beheben, müssen wir unserem Konstruktoraufruf in Program.cs einen Parameter hinzufügen. Wir können beispielsweise unsere Objektkonstruktionslinie wie folgt aktualisieren:

Hund mein Hund =neuer Hund ("Snoopy");

Dadurch werden die roten Schnörkel entfernt und Sie können den Code erneut ausführen. Wenn Sie Ihren Haltepunkt nach der letzten Codezeile verlassen oder setzen, können Sie sich das Locals-Bedienfeld ansehen und überprüfen, ob Ihr Objekt den Name hat -Eigenschaft wurde tatsächlich festgelegt, wie in Abbildung 11 gezeigt.

Abbildung 11:Korrektes Anzeigen unseres Name-Property-Sets

Ich habs? Gut! Wir haben jetzt die Möglichkeit, unserem Hund einen Namen zu geben, aber es ist kein wirklich nützliches Programm, wenn die Leute es debuggen müssen, um zu sehen, was wir tun. Lassen Sie uns also den Code ein wenig durcheinander bringen, um sicherzustellen, dass wir den Namen des Hundes anzeigen, der bellt.

Aktualisieren Sie Ihren Dog Objekte und ändern Sie das Speak() Methode als solche:

public void Speak() { Console.WriteLine(this.Name + " says: Woof"); }

Die von uns vorgenommene Änderung teilt die WriteLine nun mit -Methode, um den Namen dieses Objekts mit der wörtlichen Zeichenfolge „says:Woof“ zu verketten, was uns eine Ausgabe liefern sollte, die unsere Bemühungen besser darstellt. Versuchen Sie jetzt, das Programm auszuführen, und Sie sollten etwas sehen, das Abbildung 12 ähnelt.

Abbildung 12:Snoopy sagt:Woof

Gute Arbeit! Sie können jetzt viele Hunde mit unterschiedlichen Namen erstellen, die alle auf Ihren Befehl hin bellen.

Natürlich ist ein Streichelzoo nur mit Hunden etwas langweilig. Ich meine, ich liebe Hunde, aber vielleicht könnten wir ein paar zusätzliche Tiere hinzufügen, um das Erlebnis ein wenig aufzupeppen?
.

Vererbung

Beginnen wir mit dem Hinzufügen einer neuen Cat class in unsere Animals.cs-Datei. Fügen Sie den folgenden Code neben der Dog-Klasse hinzu, aber immer noch innerhalb der PettingZoo-Namespace-Klammern:

class Cat
{
    public Cat(string catName)
    {
        this.Name = catName;
    }
    string Name;
    public void Speak() { Console.WriteLine(this.Name + " says: Meow!"); }
}

Dann erstellen wir eine Cat Objekt in Program.cs und lassen Sie es sprechen:

Cat myCat = new Cat("Garfield");
myCat.Speak();

Wenn Sie dieses Programm jetzt ausführen, sollten Sie eine Ausgabe wie in Abbildung 13 erhalten.

Abbildung 13:Garfield sagt:Miau

Unser Programm ist vorhersehbar und funktioniert, aber ist Ihnen aufgefallen, wie ähnlich sich die beiden Klassen wirklich sind? Sehen Sie, wie viel Code wir in den beiden Klassen dupliziert haben? Sollte uns die Objektorientierung nicht davor bewahren, Code mehrfach zu schreiben?

Die Antwort ist ja. Wir können – und sollten – diesen Code vereinfachen, um die Menge an Duplikaten zu reduzieren. Worauf wir gleich eingehen werden, wird ein Feature von OO demonstrieren, das Vererbung genannt wird .

Die Vererbung in Objektorientierung ermöglicht es Ihnen, eine Klassenhierarchie zu erstellen und jede Klasse die Eigenschaften und Methoden einer übergeordneten Klasse erben zu lassen. Für unseren Streichelzoo können wir beispielsweise ein übergeordnetes Animal erstellen Klasse und haben Dog und Cat von dieser Klasse erben. Dann können wir Teile unseres Codes in das Animal verschieben Klasse, sodass sowohl der Dog und Cat Klassen erben diesen Code.

Wenn wir jedoch die doppelten Teile unseres Codes zu diesem Animal verschieben Klasse, dann der Dog und Cat Klassen würden jeweils die gleichen Eigenschaften und Methoden erben, wodurch die untergeordneten Klassen zu Klonen der übergeordneten Klassen würden. Diese Organisation ist nicht sehr nützlich, weil wir unterschiedliche Tierarten haben wollen. Es wäre furchtbar langweilig, wenn Katzen, Hunde und alle anderen Tiere gleich wären. Tatsächlich wäre es für die Zwecke unseres Programms nicht sinnvoll, sie überhaupt anders zu nennen.

Wenn nun Vererbung bedeutete, dass wir nur untergeordnete Klassen erstellen könnten, die mit ihren übergeordneten Klassen identisch sind, würde es nicht viel Sinn machen, diesen ganzen Aufwand zu betreiben. Während wir also alle Teile einer übergeordneten Klasse erben, können wir auch bestimmte Teile einer übergeordneten Klasse in den untergeordneten Klassen überschreiben, um die untergeordneten Klassen unterschiedlich zu machen.

Lassen Sie uns das durchgehen und sehen, wie es funktioniert, sollen wir?
.

Übergeordnete und untergeordnete Klassen erstellen

Wir gehen ein paar Schritte zurück und erstellen den Dog neu und Cat Klassen besser. Zu Beginn benötigen wir eine übergeordnete Klasse, die die gemeinsamen Eigenschaften und Methoden der untergeordneten Klassen definiert.

Entfernen Sie in Ihrer Animals.cs-Datei den Dog und Cat Klassen und fügen Sie die folgende Klassendefinition hinzu:

class Animal
{
    public string Name;
    public string Sound;
    public void Speak() { Console.WriteLine(this.Name + " says " + this.Sound); }
}

Diese Klasse sieht wenig überraschend wie der vorherige Dog aus und Cat Klassen, mit den erwähnten Ausnahmen, dass wir alle Eigenschaften und Methoden veröffentlicht haben und dass wir eine neue Eigenschaft namens Sound eingeführt haben vom Typ string . Schließlich haben wir Speak aktualisiert -Methode, um den Sound zu verwenden Variable.

An diesem Punkt funktioniert Ihre Program.cs nicht und wenn Sie auf diese Registerkarte klicken, sollten Sie mehrere Fehlermeldungen sehen, was nur natürlich ist, weil wir die Cat entfernt haben und Dog Klassen. Lassen Sie uns sie neu erstellen und dies mithilfe der Vererbung tun. In Ihrer Animals.cs-Datei direkt nach Animal Klasse, fügen Sie den folgenden Code hinzu:

class Dog : Animal { }
class Cat : Animal { }

Diese neuen Klassen sind viel kürzer als zuvor und replizieren keinen Code. Die neue Syntax hier ist ein Doppelpunkt gefolgt vom Klassennamen Animal , was C# mitteilt, dass wir beide Dog wollen und Cat vom Animal zu erben Klasse. Tatsächlich Dog und Cat Unterklassen von Animal werden wie im Diagramm in Abbildung 14 dargestellt.

Abbildung 14:Diagramm der Tierklassenvererbung

Hinweis:Ich habe den Begriff Eigentum verwendet bisher, was etwas ungenau ist, da der korrekte Begriff Feld ist wie Sie im Diagramm in Abbildung 14 sehen können. Field ist jedoch außerhalb des Bereichs der Programmierung weniger klar, daher habe ich stattdessen Property verwendet. Technisch gesehen ist eine Eigenschaft ein Wrapper um ein Feld in C#.

.
Aufgrund des von uns erstellten benutzerdefinierten Konstruktors haben wir jedoch immer noch ein Problem mit unserer Datei Program.cs. Wenn Sie versuchen, unser Programm auszuführen oder zu debuggen, sollten Sie eine Fehlermeldung sehen, die besagt, dass „‚PettingZoo.Cat‘ keinen Konstruktor enthält, der 1 Argumente akzeptiert“ und eine ähnliche für „PettingZoo.Dog“.

Um diesen Fehler zu beheben, müssen wir die Konstruktoren wieder hinzufügen. Dieses Mal verwenden wir jedoch Vererbung, um den Konstruktor zu erben, und zeigen, wie Sie den Konstruktor erweitern können, um die Funktionalität einer übergeordneten Klasse zu ändern.

Zuerst müssen wir einen Basiskonstruktor für Animal erstellen , die den zuvor erstellten Konstruktoren ähneln. Fügen Sie Ihrem Animal den folgenden Code hinzu Klasse:

public Animal(string animalName)
{
    this.Name = animalName;
}

Wie zuvor setzt unser Konstruktor den Namen des Tieres auf das, was wir ihm übergeben. Dies reicht jedoch nicht aus, da wir auch einen Konstruktor zu beiden Dog hinzufügen müssen und Cat Klassen. Wir werden dies verwenden, um zu definieren, welches Geräusch jedes Tier macht.

class Dog : Animal
{
    public Dog(string dogName) : base(dogName)
    {
        this.Sound = "Woof";
    }
}
class Cat : Animal
{
    public Cat(string catName) : base(catName)
    {
        this.Sound = "Meow";
    }
}

Für beide Klassen fügen wir einen Konstruktor hinzu, der einen Name akzeptiert Parameter. Wir legen auch den Sound dieses Objekts fest Eigenschaft zu dem Geräusch, das das Tier machen soll.

Im Gegensatz zu früher rufen wir jetzt jedoch den übergeordneten Klassenkonstruktor auf und übergeben den Name Parameter aus dem übergeordneten Konstruktor. Dieser Aufruf erfolgt über : base() -Methode und durch Hinzufügen des empfangenen dogName oder catName Parameter in Klammern.

Hinweis:Dieser : base() Syntax ist einzigartig für Konstruktoren. Wir werden später andere Möglichkeiten zum Ändern oder Erweitern der Fallfunktionalität sehen.

.
Mit diesen Aktualisierungen unseres Codes können wir unser Programm jetzt erneut ausführen und sehen ein Ergebnis ähnlich dem in Abbildung 15.

Abbildung 15:Tiere sprechen mit Vererbung

Beachten Sie, dass wir weder einen Sound haben noch ein Name entweder im Dog definierte Eigenschaft oder Cat Klassen. Wir haben auch kein Speak() Methode. Stattdessen befinden sie sich im übergeordneten Animal Klasse und der Dog und Cat Klassen erben diese Eigenschaften und Methoden.

Wir können jetzt auf ähnliche Weise andere Klassen für jede Art von Tier erstellen und müssen nur das Muster des Dog replizieren und Cat Klassen. Zum Beispiel könnten wir diese Klassen erstellen:

class Parrot : Animal
{
    public Parrot(string parrotName)
        : base(parrotName)
    {
        this.Sound = "I want a cracker!";
    }
}
class Pig : Animal
{
    public Pig(string pigName)
        : base(pigName)
    {
        this.Sound = "Oink";
    }
}

Dann können wir diese Klassen in Program.cs instanziieren:

Parrot myParrot = new Parrot("Polly");
myParrot.Speak();

Pig myPig = new Pig("Bacon");
myPig.Speak();

Jetzt haben wir einen wahren Zoo vor uns, wenn wir das Programm ausführen, wie in Abbildung 16 gezeigt.

Abbildung 16:Ein Zoo mit vier sprechenden Tieren

Sie können auch weitere untergeordnete Klassen bestehender untergeordneter Klassen erstellen. Beispielsweise könnten Sie den Dog trennen Klasse in Beagle und Pointer oder den Parrot haben Klasse von einem übergeordneten Bird erben Klasse, die wiederum von Animal erbt . Das Erstellen dieser Art von Hierarchie würde jedoch den Rahmen dieses Artikels sprengen. Lassen Sie uns also weitermachen und uns schließlich ansehen, wie wir die Funktionalität von übergeordneten Klassen in untergeordneten Klassen überschreiben, ändern oder erweitern können.
.

Vererbung ändern

Wir können die Methode einer übergeordneten Klasse ändern, indem wir eine Technik namens Überschreiben verwenden . Die Syntax zum Überschreiben kann sich zwischen den Sprachen unterscheiden, aber das Prinzip, die Methode des Elternteils in der Kindklasse zu ändern, bleibt gleich.

In C# fügen wir das Schlüsselwort override hinzu gefolgt von der Methodensignatur, die Sie überschreiben möchten. Wir müssen auch in der übergeordneten Klasse deklarieren, dass wir Kindern erlauben, die Methoden zu überschreiben, indem wir den virtual hinzufügen Schlüsselwort für die Methodensignatur in der übergeordneten Klasse. (Andere Sprachen erfordern diese zusätzliche Angabe möglicherweise nicht.)

In unserem Programm müssen wir Speak() aktualisieren Methode im Animal Klasse.

public virtual void Speak() { Console.WriteLine(this.Name + " says " + this.Sound); }

Jetzt können wir unsere Überschreibung des Speak() einfügen Methode im Dog class am Anfang unseres Codeblocks für diese Klasse.

class Dog : Animal
{
    public override void Speak()
    {
        base.Speak();
        Console.WriteLine("...and then runs around, chasing his tail");
    }
[remaining code omitted]

Dieser Code sagt uns, dass wir überschreiben wollen, was in der übergeordneten Methode Speak() passiert . Wir werden die übergeordnete Methode weiterhin mit base.Speak() ausführen aufrufen, bevor wir direkt danach eine zusätzliche Zeile ausgeben.

Warum würden Sie das tun wollen? Nun, vielleicht möchten wir, dass unsere Hunde so enthusiastisch sind, wie sie persönlich sein können. Führen Sie das Programm aus und überzeugen Sie sich selbst:

Abbildung 17:Unser Hund überschreibt seine Klassenmethode

Zuerst bellt Snoopy ganz normal:das base.Speak() -Methode ruft Speak() auf Methode vom übergeordneten Animal Klasse. Dann gibt der Rest des Codes eine zweite Zeile an die Konsole aus. Effektiv erweitern wir die übergeordnete Klasse, indem wir nur der untergeordneten Klasse neue Funktionen hinzufügen. Sehen Sie, wie die Cat Klasse ist nicht betroffen.

Natürlich ist es notorisch schwierig, Katzen nach Belieben zu machen, also spiegeln wir das im Code wider. Aktualisieren Sie die Cat Klasse und fügen Sie die folgende Methodenüberschreibung hinzu, ähnlich wie bei Dog Klasse.

class Cat : Animal
{
    public override void Speak()
    {
        Console.WriteLine(Name + " doesn't speak but just sits there wondering when you will feed it.");
    }
[remaining code omitted]

Anders als bei Dog außer Kraft setzen, wir rufen base.Speak() nicht auf Methode diesmal. Ohne diesen Aufruf führen wir niemals Animal aus Speak() Methode und ändern Sie vollständig, was in dieser Methode passiert. Überzeugen Sie sich selbst:

Abbildung 18:Unsere Katze überschreibt auch seine Klassenmethode

Beachten Sie auch, dass wir bei der Neuerstellung unserer Klassen den Program.cs-Code nicht geändert haben. Diese Änderungen sind ein gutes Beispiel dafür, warum die Objektorientierung eine sehr mächtige Technik ist; Wir haben den zugrunde liegenden Code vollständig geändert, ohne das Programm zu berühren, das diesen Code verwendet. [Die Objektorientierung hat es uns ermöglicht, viele der Implementierungsdetails zu isolieren und einfach einen kleinen Satz von Schnittstellen bereitzustellen, mit denen das Programm interagieren muss.]

Wo wir die Regeln verbogen haben

Bevor wir zum Abschluss kommen, möchte ich auf einige Dinge hinweisen, hauptsächlich auf einige „schlechte“ Dinge, die wir getan haben.

Zuerst rufen wir in unseren Klassen Console.WriteLine() auf Methode direkt. Diese Verwendung ist keine gute Designentscheidung, da Klassen unabhängig davon sein sollten, wie wir die Ergebnisse der Klassen ausgeben.

Ein besserer Ansatz wäre, stattdessen Daten aus den Klassen zurückzugeben und diese dann als Teil des Programms auszugeben. Wir würden es dann dem Programm überlassen, zu entscheiden, was mit der Ausgabe und nicht mit den Klassen geschehen soll, was es uns ermöglicht, dieselben Klassen für viele verschiedene Programmtypen zu verwenden, egal ob es sich um Webseiten, Windows Forms, QT oder Konsolenanwendungen handelt.

Zweitens waren in unseren späteren Beispielen alle Eigenschaften public . Die Veröffentlichung dieser Eigenschaften schwächt den Sicherheitsaspekt der Objektorientierung, bei der wir versuchen, die Daten in den Klassen vor Manipulation von außen zu schützen. Der Schutz von Eigenschaften und Methoden wird jedoch schnell komplex, wenn es um Vererbung geht, daher wollte ich diese Komplikation zumindest in dieser Einführungsphase vermeiden.

Schließlich macht es wirklich keinen Sinn, Sound zu haben überhaupt als Teil des untergeordneten Konstruktors festlegen, weil wir auch diesen Code dupliziert haben. Es wäre ein besseres Design, einen Konstruktor in der übergeordneten Klasse zu erstellen, der sowohl einen Namen als auch einen Sound akzeptiert, und diesen dann einfach als Teil des untergeordneten Konstruktors aufzurufen. Der Einfachheit halber wollte ich an dieser Stelle jedoch keine unterschiedlichen Konstruktorsignaturen einführen.

Obwohl wir in diesem Bonus-Tutorial viel gelernt haben, können Sie sehen, dass es noch mehr zu lernen gibt. Machen Sie sich darüber jedoch noch nicht zu viele Gedanken. Sie können sich einen Moment Zeit nehmen, um sich selbst zu gratulieren, dass Sie den ganzen Weg mitverfolgt haben. Wenn Sie alle Schritte befolgt haben, haben Sie gelernt:

  • Warum wir die Objektorientierung verwenden
  • So erstellen Sie Klassen
  • Erstellen von Eigenschaften und Methoden
  • Wie man Konstruktoren erstellt und was sie tun
  • Wie Sie die Vererbung nutzen und warum sie ein leistungsstarkes Merkmal der Objektorientierung ist

Ich hoffe, Ihnen hat diese zusätzliche Lektion gefallen und Sie sind daran interessiert, mehr zu entdecken. Besuchen Sie uns auf jeden Fall für neue Inhalte im Atlantic.Net-Blog und ziehen Sie einen unserer branchenführenden virtuellen privaten Hosting-Server in Betracht.
.


Linux
  1. So booten Sie Linux und Windows dual

  2. So installieren Sie Elasticsearch und Kibana unter Linux

  3. So verwalten und listen Sie Dienste in Linux auf

  4. Gewusst wie:DRBD-Replikation und -Konfiguration

  5. Gewusst wie:Socket-Programmierung in Python

So installieren und verwenden Sie die Programmiersprache R in Ubuntu 20.04 LTS

So installieren und verwenden Sie die Programmiersprache „R“ unter Ubuntu

So installieren und konfigurieren Sie Grafana

Gewusst wie:Einführung in die Programmierung – Variablen, Typen und Datenbearbeitung

Gewusst wie:Objektorientierte Programmierung – Mehr mit Klassen und Objekten

Wie vernetzt man Ubuntu und Windows 10?