Rust Methodensyntax Übung

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 Methoden-Syntax. Dieser Lab ist ein Teil des Rust Buchs. Du kannst deine Rust-Fähigkeiten in LabEx üben.

In diesem Lab werden Methoden mit dem fn-Schlüsselwort und einem Namen deklariert, können Parameter und einen Rückgabewert haben und werden innerhalb des Kontexts einer Struktur definiert, wobei der erste Parameter immer self ist, um die Instanz der aufgerufenen Struktur darzustellen.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/DataStructuresandEnumsGroup(["Data Structures and Enums"]) rust(("Rust")) -.-> rust/AdvancedTopicsGroup(["Advanced Topics"]) rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/DataTypesGroup -.-> rust/integer_types("Integer Types") rust/DataTypesGroup -.-> rust/boolean_type("Boolean Type") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") rust/DataStructuresandEnumsGroup -.-> rust/method_syntax("Method Syntax") rust/AdvancedTopicsGroup -.-> rust/traits("Traits") subgraph Lab Skills rust/variable_declarations -.-> lab-100397{{"Rust Methodensyntax Übung"}} rust/integer_types -.-> lab-100397{{"Rust Methodensyntax Übung"}} rust/boolean_type -.-> lab-100397{{"Rust Methodensyntax Übung"}} rust/function_syntax -.-> lab-100397{{"Rust Methodensyntax Übung"}} rust/expressions_statements -.-> lab-100397{{"Rust Methodensyntax Übung"}} rust/method_syntax -.-> lab-100397{{"Rust Methodensyntax Übung"}} rust/traits -.-> lab-100397{{"Rust Methodensyntax Übung"}} end

Methoden-Syntax

Methoden ähneln Funktionen: Wir deklarieren sie mit dem fn-Schlüsselwort und einem Namen, sie können Parameter und einen Rückgabewert haben und enthalten einige Codezeilen, die ausgeführt werden, wenn die Methode von irgendwo anders aufgerufen wird. Anders als Funktionen werden Methoden innerhalb des Kontexts einer Struktur (oder eines Enums oder eines Trait-Objekts, die wir in Kapitel 6 und Kapitel 17 behandeln) definiert, und ihr erster Parameter ist immer self, der die Instanz der Struktur darstellt, auf der die Methode aufgerufen wird.

Methoden definieren

Ändern wir die area-Funktion, die eine Rectangle-Instanz als Parameter hat, und definieren stattdessen eine area-Methode für die Rectangle-Struktur, wie in Listing 5-13 gezeigt.

Dateiname: src/main.rs

#[derive(Debug)]
struct Rectangle {
    width: u32,
    height: u32,
}

1 impl Rectangle {
  2 fn area(&self) -> u32 {
        self.width * self.height
    }
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    println!(
        "The area of the rectangle is {} square pixels.",
      3 rect1.area()
    );
}

Listing 5-13: Definieren einer area-Methode für die Rectangle-Struktur

Um die Funktion innerhalb des Kontexts von Rectangle zu definieren, starten wir einen impl (Implementierungs)-Block für Rectangle [1]. Alles innerhalb dieses impl-Blocks wird mit dem Rectangle-Typ assoziiert. Dann bewegen wir die area-Funktion innerhalb der geschweiften Klammern des impl [2] und ändern den ersten (und in diesem Fall einzigen) Parameter in der Signatur und überall im Körper in self. In main, wo wir die area-Funktion aufgerufen und rect1 als Argument übergeben haben, können wir stattdessen die Methodensyntax verwenden, um die area-Methode auf unserer Rectangle-Instanz aufzurufen [3]. Die Methodensyntax folgt einer Instanz: Wir fügen einen Punkt hinzu, gefolgt vom Methodennamen, Klammern und beliebigen Argumenten.

In der Signatur für area verwenden wir &self anstelle von rectangle: &Rectangle. &self ist eigentlich die Abkürzung für self: &Self. Innerhalb eines impl-Blocks ist der Typ Self ein Alias für den Typ, für den der impl-Block bestimmt ist. Methoden müssen einen Parameter namens self vom Typ Self als ersten Parameter haben, daher lässt Rust Ihnen diese mit nur dem Namen self im ersten Parameterplatz abkürzen. Beachten Sie, dass wir immer noch das & vor der self-Abkürzung verwenden müssen, um anzuzeigen, dass diese Methode die Self-Instanz baut, genauso wie wir es in rectangle: &Rectangle getan haben. Methoden können die Eigentumsgewalt an self übernehmen, self unveränderlich bauen, wie wir hier getan haben, oder self veränderlich bauen, genauso wie sie auch jeden anderen Parameter können.

Wir haben hier &self gewählt, aus dem gleichen Grund, warum wir in der Funktionsversion &Rectangle verwendet haben: Wir möchten keine Eigentumsgewalt übernehmen und nur die Daten in der Struktur lesen, nicht darauf schreiben. Wenn wir die Instanz, auf der wir die Methode aufgerufen haben, als Teil dessen ändern möchten, was die Methode tut, würden wir &mut self als ersten Parameter verwenden. Es ist selten, dass eine Methode die Eigentumsgewalt an der Instanz übernimmt, indem sie nur self als ersten Parameter verwendet; diese Technik wird normalerweise verwendet, wenn die Methode self in etwas anderes umwandelt und Sie möchten, dass der Aufrufer die ursprüngliche Instanz nach der Transformation nicht mehr verwenden kann.

Der Hauptgrund für die Verwendung von Methoden statt Funktionen, neben der Bereitstellung der Methodensyntax und der Tatsache, dass man nicht den Typ von self in jeder Methodensignatur wiederholen muss, ist die Organisation. Wir haben alles, was wir mit einer Instanz eines Typs tun können, in einem impl-Block zusammengefasst, anstatt zukünftige Benutzer unseres Codes dazu zu bringen, in verschiedenen Teilen der Bibliothek, die wir zur Verfügung stellen, nach den Fähigkeiten von Rectangle zu suchen.

Beachten Sie, dass wir die Möglichkeit haben, einer Methode denselben Namen wie einem der Felder der Struktur zu geben. Beispielsweise können wir eine Methode auf Rectangle definieren, die auch width heißt:

Dateiname: src/main.rs

impl Rectangle {
    fn width(&self) -> bool {
        self.width > 0
    }
}

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };

    if rect1.width() {
        println!(
            "The rectangle has a nonzero width; it is {}",
            rect1.width
        );
    }
}

Hier wählen wir aus, dass die width-Methode true zurückgibt, wenn der Wert im width-Feld der Instanz größer als 0 ist, und false, wenn der Wert 0 ist: Wir können ein Feld innerhalb einer Methode mit demselben Namen zu jedem Zweck verwenden. In main, wenn wir rect1.width mit Klammern folgen, weiß Rust, dass wir die Methode width meinen. Wenn wir keine Klammern verwenden, weiß Rust, dass wir das Feld width meinen.

Oft, aber nicht immer, wenn wir Methoden mit demselben Namen wie einem Feld geben, möchten wir, dass sie nur den Wert im Feld zurückgeben und nichts anderes tun. Methoden wie diese werden Getter genannt, und Rust implementiert sie nicht automatisch für Strukturfelder wie einige andere Sprachen. Getter sind nützlich, weil Sie das Feld privat und die Methode öffentlich machen können und so einen schreibgeschützten Zugang zu diesem Feld als Teil der öffentlichen API des Typs ermöglichen. Wir werden in Kapitel 7 diskutieren, was öffentlich und privat sind und wie man ein Feld oder eine Methode als öffentlich oder privat markiert.

Wo ist der -> Operator?

In C und C++ werden zwei verschiedene Operatoren zum Aufrufen von Methoden verwendet: Sie verwenden ., wenn Sie direkt auf dem Objekt eine Methode aufrufen, und ->, wenn Sie die Methode auf einem Zeiger auf das Objekt aufrufen und den Zeiger zuerst aufgelöst haben müssen. Mit anderen Worten, wenn object ein Zeiger ist, ist object->something() ähnlich zu (*object).something().

Rust hat keinen Äquivalent zum ->-Operator; stattdessen hat Rust ein Feature namens automatisches Referenzieren und Dereferenzieren. Methodenaufrufe sind eine der wenigen Stellen in Rust, an denen dieses Verhalten existiert.

So funktioniert es: Wenn Sie eine Methode mit object.something() aufrufen, fügt Rust automatisch &, &mut oder * hinzu, sodass object der Signatur der Methode entspricht. Mit anderen Worten, die folgenden beiden Zeilen sind gleichwertig:

p1.distance(&p2);
(&p1).distance(&p2);

Die erste sieht viel sauberer aus. Dieses automatische Referenzieren funktioniert, weil Methoden einen eindeutigen Empfänger haben - den Typ von self. Ausgehend vom Empfänger und dem Namen einer Methode kann Rust eindeutig feststellen, ob die Methode das Objekt liest (&self), mutiert (&mut self) oder konsumiert (self). Die Tatsache, dass Rust das Entlehnung implizit für Methodenempfänger macht, ist ein wichtiger Teil der Tatsache, dass die Eigentumsgewalt in der Praxis komfortabel zu handhaben ist.

Methoden mit mehr Parametern

Üben wir das Verwenden von Methoden, indem wir eine zweite Methode auf der Rectangle-Struktur implementieren. Diesmal möchten wir, dass eine Rectangle-Instanz eine andere Rectangle-Instanz annimmt und true zurückgibt, wenn das zweite Rectangle vollständig innerhalb von self (der ersten Rectangle-Instanz) passt; andernfalls sollte es false zurückgeben. Das heißt, nachdem wir die can_hold-Methode definiert haben, möchten wir das Programm wie in Listing 5-14 schreiben können.

Dateiname: src/main.rs

fn main() {
    let rect1 = Rectangle {
        width: 30,
        height: 50,
    };
    let rect2 = Rectangle {
        width: 10,
        height: 40,
    };
    let rect3 = Rectangle {
        width: 60,
        height: 45,
    };

    println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
    println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}

Listing 5-14: Verwenden der noch nicht implementierten can_hold-Methode

Die erwartete Ausgabe würde wie folgt aussehen, da beide Dimensionen von rect2 kleiner als die Dimensionen von rect1 sind, aber rect3 breiter als rect1 ist:

Can rect1 hold rect2? true
Can rect1 hold rect3? false

Wir wissen, dass wir eine Methode definieren möchten, daher wird sie innerhalb des impl Rectangle-Blocks sein. Der Methodenname wird can_hold sein, und er wird eine unveränderliche Referenz auf eine andere Rectangle als Parameter akzeptieren. Wir können den Typ des Parameters bestimmen, indem wir uns den Code ansehen, der die Methode aufruft: rect1.can_hold(&rect2) übergibt &rect2, was eine unveränderliche Referenz auf rect2, eine Rectangle-Instanz, ist. Dies ergibt Sinn, da wir nur rect2 lesen müssen (anstatt schreiben, was bedeuten würde, dass wir eine veränderliche Referenz benötigen), und wir möchten, dass main die Eigentumsgewalt an rect2 behält, damit wir sie nach dem Aufruf der can_hold-Methode erneut verwenden können. Der Rückgabewert von can_hold wird ein Boolean sein, und die Implementierung wird überprüfen, ob die Breite und Höhe von self größer als die Breite und Höhe der anderen Rectangle sind, respektive. Fügen wir die neue can_hold-Methode zum impl-Block aus Listing 5-13 hinzu, wie in Listing 5-15 gezeigt.

Dateiname: src/main.rs

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }

    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

Listing 5-15: Implementieren der can_hold-Methode auf Rectangle, die eine andere Rectangle-Instanz als Parameter annimmt

Wenn wir diesen Code mit der main-Funktion aus Listing 5-14 ausführen, erhalten wir die gewünschte Ausgabe. Methoden können mehrere Parameter akzeptieren, die wir der Signatur nach dem self-Parameter hinzufügen, und diese Parameter funktionieren genauso wie Parameter in Funktionen.

Assoziierte Funktionen

Alle Funktionen, die innerhalb eines impl-Blocks definiert werden, werden als assoziierte Funktionen bezeichnet, weil sie mit dem Typ assoziiert sind, der nach dem impl benannt ist. Wir können assoziierte Funktionen definieren, die self nicht als ersten Parameter haben (und daher keine Methoden sind), weil sie keine Instanz des Typs benötigen, um zu funktionieren. Wir haben bereits eine solche Funktion verwendet: die String::from-Funktion, die auf dem String-Typ definiert ist.

Assoziierte Funktionen, die keine Methoden sind, werden oft für Konstruktoren verwendet, die eine neue Instanz der Struktur zurückgeben. Diese werden oft new genannt, aber new ist kein spezieller Name und ist nicht in die Sprache eingebaut. Beispielsweise könnten wir eine assoziierte Funktion namens square bereitstellen, die einen Dimensionenparameter hätte und diesen als Breite und Höhe verwenden würde, was es somit einfacher macht, ein quadratisches Rectangle zu erstellen, anstatt den gleichen Wert zweimal angeben zu müssen:

Dateiname: src/main.rs

impl Rectangle {
    fn square(size: u32) -> 1 Self  {
      2 Self  {
            width: size,
            height: size,
        }
    }
}

Die Self-Schlüsselwörter im Rückgabetyp [1] und im Funktionskörper [2] sind Aliase für den Typ, der nach dem impl-Schlüsselwort erscheint, was in diesem Fall Rectangle ist.

Um diese assoziierte Funktion aufzurufen, verwenden wir die ::-Syntax mit dem Struktur-Namen; let sq = Rectangle::square(3); ist ein Beispiel. Diese Funktion ist in der Struktur namenspezifiziert: Die ::-Syntax wird sowohl für assoziierte Funktionen als auch für durch Module erstellte Namensräume verwendet. Wir werden in Kapitel 7 über Module sprechen.

Mehrere impl-Blöcke

Jeder Struktur ist es erlaubt, mehrere impl-Blöcke zu haben. Beispielsweise ist Listing 5-15 äquivalent zum in Listing 5-16 gezeigten Code, bei dem jede Methode in ihrem eigenen impl-Block steht.

impl Rectangle {
    fn area(&self) -> u32 {
        self.width * self.height
    }
}

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.width > other.width && self.height > other.height
    }
}

Listing 5-16: Umformulierung von Listing 5-15 unter Verwendung mehrerer impl-Blöcke

Es gibt keinen Grund, diese Methoden hier in mehrere impl-Blöcke aufzuteilen, aber dies ist gültige Syntax. Wir werden in Kapitel 10, in dem wir generische Typen und Traits diskutieren, einen Fall sehen, in dem mehrere impl-Blöcke nützlich sind.

Zusammenfassung

Herzlichen Glückwunsch! Sie haben das Lab zu Methodensyntax abgeschlossen. Sie können in LabEx weitere Labs absolvieren, um Ihre Fähigkeiten zu verbessern.