Eigenschaften von objektorientierten Sprachen

RustRustBeginner
Jetzt üben

This tutorial is from open-source community. Access the source code

💡 Dieser Artikel wurde von AI-Assistenten übersetzt. Um die englische Version anzuzeigen, können Sie hier klicken

Einführung

Willkommen zu Eigenschaften von objektorientierten Sprachen. Dieser Lab ist ein Teil des Rust Buches. Du kannst deine Rust-Fähigkeiten in LabEx üben.

In diesem Lab werden wir die Eigenschaften von objektorientierten Sprachen, einschließlich Objekte, Kapselung und Vererbung, erkunden und untersuchen, ob Rust diese Funktionen unterstützt.

Eigenschaften von objektorientierten Sprachen

Es besteht keine Übereinstimmung in der Programmiergemeinschaft darüber, welche Funktionen eine Sprache haben muss, um als objektorientiert angesehen zu werden. Rust wird von vielen Programmierparadigmen beeinflusst, einschließlich OOP; beispielsweise haben wir in Kapitel 13 die Funktionen untersucht, die aus der funktionalen Programmierung stammen. Streitig ist, dass objektorientierte Sprachen bestimmte gemeinsame Merkmale aufweisen, nämlich Objekte, Kapselung und Vererbung. Schauen wir uns an, was jeder dieser Eigenschaften bedeutet und ob Rust sie unterstützt.

Objekte enthalten Daten und Verhalten

Das Buch Design Patterns: Elements of Reusable Object-Oriented Software von Erich Gamma, Richard Helm, Ralph Johnson und John Vlissides (Addison-Wesley, 1994), das üblicherweise als The Gang of Four -Buch bezeichnet wird, ist ein Katalog von objektorientierten Designmustern. Es definiert die OOP auf diese Weise:

Objektorientierte Programme bestehen aus Objekten. Ein Objekt verpackt sowohl Daten als auch die Prozeduren, die auf diesen Daten operieren. Die Prozeduren werden typischerweise als Methoden oder Operationen bezeichnet.

Unter Verwendung dieser Definition ist Rust objektorientiert: Structs und Enums haben Daten, und impl -Blöcke bieten Methoden für Structs und Enums. Auch wenn Structs und Enums mit Methoden nicht als Objekte bezeichnet werden, bieten sie die gleiche Funktionalität gemäß der Definition von Objekten durch die Gruppe von Vier.

Kapselung, die Implementierungsdetails versteckt

Ein weiteres Aspekt, das häufig mit der OOP assoziiert wird, ist das Konzept der Kapselung, was bedeutet, dass die Implementierungsdetails eines Objekts nicht für den Code zugänglich sind, der dieses Objekt verwendet. Daher ist die einzige Möglichkeit, mit einem Objekt zu interagieren, über seine öffentliche Schnittstelle; der Code, der das Objekt verwendet, sollte nicht in das Innere des Objekts eindringen und Daten oder Verhalten direkt ändern können. Dies ermöglicht es dem Programmierer, die internen Details eines Objekts zu ändern und umzuschreiben, ohne dass der Code, der das Objekt verwendet, geändert werden muss.

Wir haben in Kapitel 7 diskutiert, wie die Kapselung kontrolliert werden kann: Wir können das Schlüsselwort pub verwenden, um zu bestimmen, welche Module, Typen, Funktionen und Methoden in unserem Code öffentlich sein sollten, und standardmäßig ist alles andere privat. Beispielsweise können wir eine Struktur AveragedCollection definieren, die ein Feld enthält, das einen Vektor von i32 -Werten enthält. Die Struktur kann auch ein Feld haben, das den Durchschnitt der Werte im Vektor enthält, was bedeutet, dass der Durchschnitt nicht jedes Mal berechnet werden muss, wenn jemand ihn benötigt. Mit anderen Worten, AveragedCollection wird den berechneten Durchschnitt für uns zwischenspeichern. Listing 17-1 zeigt die Definition der AveragedCollection -Struktur.

Dateiname: src/lib.rs

pub struct AveragedCollection {
    list: Vec<i32>,
    average: f64,
}

Listing 17-1: Eine AveragedCollection -Struktur, die eine Liste von ganzen Zahlen und den Durchschnitt der Elemente in der Sammlung aufbewahrt

Die Struktur ist als pub markiert, sodass anderer Code sie verwenden kann, aber die Felder innerhalb der Struktur bleiben privat. Dies ist in diesem Fall wichtig, weil wir sicherstellen möchten, dass jederzeit, wenn ein Wert zur Liste hinzugefügt oder entfernt wird, auch der Durchschnitt aktualisiert wird. Wir tun dies, indem wir add, remove und average -Methoden auf der Struktur implementieren, wie in Listing 17-2 gezeigt.

Dateiname: src/lib.rs

impl AveragedCollection {
    pub fn add(&mut self, value: i32) {
        self.list.push(value);
        self.update_average();
    }

    pub fn remove(&mut self) -> Option<i32> {
        let result = self.list.pop();
        match result {
            Some(value) => {
                self.update_average();
                Some(value)
            }
            None => None,
        }
    }

    pub fn average(&self) -> f64 {
        self.average
    }

    fn update_average(&mut self) {
        let total: i32 = self.list.iter().sum();
        self.average = total as f64 / self.list.len() as f64;
    }
}

Listing 17-2: Implementierungen der öffentlichen Methoden add, remove und average auf AveragedCollection

Die öffentlichen Methoden add, remove und average sind die einzigen Wege, um auf Daten in einer Instanz von AveragedCollection zuzugreifen oder zu modifizieren. Wenn ein Element der list mit der add -Methode hinzugefügt oder mit der remove -Methode entfernt wird, rufen die Implementierungen jeder die private update_average -Methode auf, die auch die Aktualisierung des average -Felds übernimmt.

Wir lassen die Felder list und average privat, sodass es keinem externen Code möglich ist, direkt Elemente zur list -Liste hinzuzufügen oder zu entfernen; andernfalls könnte das average -Feld möglicherweise unzugleich bleiben, wenn sich die list ändert. Die average -Methode gibt den Wert im average -Feld zurück, was es externen Code ermöglicht, den average zu lesen, aber nicht zu modifizieren.

Da wir die Implementierungsdetails der Struktur AveragedCollection kapselt haben, können wir in Zukunft leicht Aspekte wie die Datenstruktur ändern. Beispielsweise könnten wir für das list -Feld ein HashSet<i32> anstelle eines Vec<i32> verwenden. Solange die Signaturen der öffentlichen Methoden add, remove und average gleich blieben, müsste der Code, der AveragedCollection verwendet, nicht geändert werden. Wenn wir list stattdessen als öffentlich gemacht hätten, wäre dies nicht unbedingt der Fall: HashSet<i32> und Vec<i32> haben unterschiedliche Methoden zum Hinzufügen und Entfernen von Elementen, sodass der externe Code wahrscheinlich geändert werden müsste, wenn er direkt list modifizierte.

Wenn die Kapselung ein erforderlicher Aspekt für eine Sprache ist, um als objektorientiert angesehen zu werden, dann erfüllt Rust diese Anforderung. Die Möglichkeit, pub für verschiedene Teile des Codes zu verwenden oder nicht, ermöglicht die Kapselung von Implementierungsdetails.

Vererbung als Typsystem und als Code - Wiederverwendung

Vererbung ist ein Mechanismus, durch den ein Objekt Elemente aus der Definition eines anderen Objekts erben kann und somit die Daten und das Verhalten des Elternobjekts erhält, ohne dass Sie es erneut definieren müssen.

Wenn eine Sprache Vererbung haben muss, um objektorientiert zu sein, dann ist Rust keine solche Sprache. Es gibt keine Möglichkeit, eine Struktur zu definieren, die die Felder und Methodenimplementierungen der Elternstruktur erbt, ohne Makros zu verwenden.

Wenn Sie jedoch in Ihrem Programmierungswerkzeugkasten an Vererbung gewöhnt sind, können Sie in Rust andere Lösungen verwenden, je nachdem, warum Sie ursprünglich auf Vererbung zurückgegriffen haben.

Es gibt zwei Hauptgründe, warum Sie Vererbung wählen würden. Ein Grund ist die Wiederverwendung von Code: Sie können ein bestimmtes Verhalten für einen Typ implementieren, und die Vererbung ermöglicht es Ihnen, diese Implementierung für einen anderen Typ zu verwenden. Sie können dies in begrenztem Maße in Rust -Code tun, indem Sie Standard -Trait -Methodenimplementierungen verwenden, wie Sie es in Listing 10-14 gesehen haben, als wir eine Standardimplementierung der summarize -Methode auf dem Summary -Trait hinzufügten. Jeder Typ, der das Summary -Trait implementiert, hätte die summarize -Methode verfügbar, ohne weitere Codezeilen. Dies ähnelt der Situation, in der eine Elternklasse eine Implementierung einer Methode hat und eine erbende Kindklasse ebenfalls die Implementierung der Methode hat. Wir können auch die Standardimplementierung der summarize -Methode überschreiben, wenn wir das Summary -Trait implementieren, was der Situation ähnelt, in der eine Kindklasse die Implementierung einer von der Elternklasse geerbten Methode überschreibt.

Der andere Grund, Vererbung zu verwenden, bezieht sich auf das Typsystem: um es zu ermöglichen, dass ein Kindtyp an den gleichen Stellen wie der Elterntyp verwendet werden kann. Dies wird auch als Polymorphismus bezeichnet, was bedeutet, dass Sie zur Laufzeit mehrere Objekte für einander austauschen können, wenn sie bestimmte Merkmale teilen.

Polymorphismus

Für viele Menschen ist Polymorphismus synonym mit Vererbung. Tatsächlich ist es jedoch ein allgemeineres Konzept, das sich auf Code bezieht, der mit Daten mehrerer Typen arbeiten kann. Bei der Vererbung sind diese Typen im Allgemeinen Unterklassen.

Rust verwendet stattdessen Generics, um über verschiedene mögliche Typen abzustrahlen, und Trait -Bedingungen, um Beschränkungen auf das zu setzen, was diese Typen liefern müssen. Dies wird manchmal als beschränkter parametrischer Polymorphismus bezeichnet.

In letzter Zeit ist die Vererbung als Lösung für das Programmierungsdesign in vielen Programmiersprachen weniger beliebt geworden, weil es oft das Risiko birgt, mehr Code zu teilen, als notwendig ist. Unterklassen sollten nicht immer alle Merkmale ihrer Elternklasse teilen, aber dies passiert bei der Vererbung. Dies kann die Flexibilität des Programmdesigns verringern. Es führt auch zur Möglichkeit, Methoden auf Unterklassen aufzurufen, die keinen Sinn ergeben oder die Fehler verursachen, weil die Methoden nicht auf die Unterklasse zutreffen. Darüber hinaus erlauben einige Sprachen nur die einfache Vererbung (d.h. eine Unterklasse kann nur von einer Klasse erben), was die Flexibilität des Programmdesigns weiter einschränkt.

Aus diesen Gründen nimmt Rust den anderen Ansatz, Trait -Objekte anstelle von Vererbung zu verwenden. Schauen wir uns an, wie Trait -Objekte Polymorphismus in Rust ermöglichen.

Zusammenfassung

Herzlichen Glückwunsch! Sie haben das Labor zu den Eigenschaften von objektorientierten Sprachen abgeschlossen. Sie können in LabEx weitere Labore absolvieren, um Ihre Fähigkeiten zu verbessern.