Suche…


Einführung

Rust ist objektorientiert, da seinen algebraischen Datentypen zugehörige Methoden zugeordnet werden können, wodurch sie Objekte im Sinne von Daten werden, die zusammen mit Code gespeichert werden, der mit ihm arbeiten kann.

Rust unterstützt jedoch nicht die Vererbung, sondern bevorzugt die Komposition mit Traits. Dies bedeutet, dass viele OO-Muster nicht so wie sie sind und modifiziert werden müssen. Einige sind völlig irrelevant.

Vererbung mit Merkmalen

In Rust gibt es kein Konzept, die Eigenschaften einer Struktur zu "erben". Wenn Sie die Beziehung zwischen Objekten entwerfen, tun Sie dies auf eine Art und Weise, in der die eigene Funktionalität durch eine Schnittstelle (ein Merkmal in Rust) definiert wird. Dies fördert die Komposition über die Vererbung , was als nützlicher erachtet wird und sich leichter auf größere Projekte ausweiten lässt.

Hier ein Beispiel, in dem einige Beispiele für die Vererbung in Python verwendet werden:

class Animal:
    def speak(self):
        print("The " + self.animal_type + " said " + self.noise)

class Dog(Animal):
    def __init__(self):
        self.animal_type = 'dog'
        self.noise = 'woof'

Um dies in Rust umzusetzen, müssen wir herausfinden, was ein Tier ausmacht, und diese Funktionalität in Merkmale einbringen.

trait Speaks {
     fn speak(&self);

     fn noise(&self) -> &str;
}

trait Animal {
    fn animal_type(&self) -> &str;
}

struct Dog {}

impl Animal for Dog {
    fn animal_type(&self) -> &str {
        "dog"
    }
}  

impl Speaks for Dog {
    fn speak(&self) {
        println!("The dog said {}", self.noise());
    }

    fn noise(&self) -> &str {
        "woof"
    }
}

fn main() {
    let dog = Dog {};
    dog.speak();
}

Beachten Sie, wie wir diese abstrakte übergeordnete Klasse in zwei separate Komponenten aufgeteilt haben: den Teil, der die Struktur als Tier definiert, und den Teil, der es erlaubt, zu sprechen.

Eifrige Leser werden feststellen, dass dies nicht ganz eins zu eins ist, da jeder Implementierer die Logik erneut implementieren muss, um eine Zeichenfolge in der Form "Das Tier" zu drucken. Sie können dies mit einer leichten Neugestaltung der Benutzeroberfläche tun, in der wir Speak for Animal implementieren:

trait Speaks {
     fn speak(&self);
}

trait Animal {
    fn animal_type(&self) -> &str;
    fn noise(&self) -> &str;
}

impl<T> Speaks for T where T: Animal {
    fn speak(&self) {
        println!("The {} said {}", self.animal_type(), self.noise());
    }
}

struct Dog {}
struct Cat {}

impl Animal for Dog {
    fn animal_type(&self) -> &str {
        "dog"
    }
    
    fn noise(&self) -> &str {
        "woof"
    }
}

impl Animal for Cat {
    fn animal_type(&self) -> &str {
        "cat"
    }

    fn noise(&self) -> &str {
        "meow"
    }
}

fn main() {
    let dog = Dog {};
    let cat = Cat {};
    dog.speak();
    cat.speak();
}

Beachten Sie jetzt, dass das Tier ein Geräusch macht und einfach nur spricht, jetzt hat es eine Implementierung für alles, was ein Tier ist. Dies ist viel flexibler als der vorherige Weg und die Python-Vererbung. Wenn Sie beispielsweise einen Human hinzufügen möchten, der einen anderen Sound hat, können Sie stattdessen einfach eine andere Implementierung von speak für etwas Human speak :

trait Human {
    fn name(&self) -> &str;
    fn sentence(&self) -> &str;
}

struct Person {}

impl<T> Speaks for T where T: Human {
    fn speak(&self) {
        println!("{} said {}", self.name(), self.sentence());
    }
}

Besuchermuster

Das typische Besucherbeispiel in Java wäre:

interface ShapeVisitor {
    void visit(Circle c);
    void visit(Rectangle r);
}

interface Shape {
    void accept(ShapeVisitor sv);
}

class Circle implements Shape {
    private Point center;
    private double radius;

    public Circle(Point center, double radius) {
        this.center = center;
        this.radius = radius;
    }

    public Point getCenter() { return center; }
    public double getRadius() { return radius; }

    @Override
    public void accept(ShapeVisitor sv) {
        sv.visit(this);
    }
}

class Rectangle implements Shape {
    private Point lowerLeftCorner;
    private Point upperRightCorner;

    public Rectangle(Point lowerLeftCorner, Point upperRightCorner) {
        this.lowerLeftCorner = lowerLeftCorner;
        this.upperRightCorner = upperRightCorner;
    }

    public double length() { ... }
    public double width() { ... }

    @Override
    public void accept(ShapeVisitor sv) {
        sv.visit(this);
    }
}

class AreaCalculator implements ShapeVisitor {
    private double area = 0.0;

    public double getArea() { return area; }

    public void visit(Circle c) {
        area = Math.PI * c.radius() * c.radius();
    }

    public void visit(Rectangle r) {
         area = r.length() * r.width();
    }
}

double computeArea(Shape s) {
    AreaCalculator ac = new AreaCalculator();
    s.accept(ac);
    return ac.getArea();
}

Dies kann auf zwei Arten leicht in Rust übersetzt werden.

Der erste Weg verwendet den Laufzeit-Polymorphismus:

trait ShapeVisitor {
    fn visit_circle(&mut self, c: &Circle);
    fn visit_rectangle(&mut self, r: &Rectangle);
}

trait Shape {
    fn accept(&self, sv: &mut ShapeVisitor);
}

struct Circle {
    center: Point,
    radius: f64,
}

struct Rectangle {
    lowerLeftCorner: Point,
    upperRightCorner: Point,
}

impl Shape for Circle {
    fn accept(&self, sv: &mut ShapeVisitor) {
        sv.visit_circle(self);
    }
}

impl Rectangle {
    fn length() -> double { ... }
    fn width() -> double { ... }
}

impl Shape for Rectangle {
    fn accept(&self, sv: &mut ShapeVisitor) {
        sv.visit_rectangle(self);
    }
}

fn computeArea(s: &Shape) -> f64 {
    struct AreaCalculator {
        area: f64,
    }

    impl ShapeVisitor for AreaCalculator {
        fn visit_circle(&mut self, c: &Circle) {
            self.area = std::f64::consts::PI * c.radius * c.radius;
        }
        fn visit_rectangle(&mut self, r: &Rectangle) {
            self.area = r.length() * r.width();
        }
    }
    
    let mut ac = AreaCalculator { area: 0.0 };
    s.accept(&mut ac);
    ac.area
}

Der zweite Weg verwendet stattdessen den Kompilierungszeit-Polymorphismus. Hier werden nur die Unterschiede angezeigt:

trait Shape {
    fn accept<V: ShapeVisitor>(&self, sv: &mut V);
}

impl Shape for Circle {
    fn accept<V: ShapeVisitor>(&self, sv: &mut V) {
        // same body
    }
}

impl Shape for Rectangle {
    fn accept<V: ShapeVisitor>(&self, sv: &mut V) {
        // same body
    }
}

fn computeArea<S: Shape>(s: &S) -> f64 {
    // same body
}


Modified text is an extract of the original Stack Overflow Documentation
Lizenziert unter CC BY-SA 3.0
Nicht angeschlossen an Stack Overflow