Rust
Objectgeoriënteerde roest
Zoeken…
Invoering
Rust is objectgeoriënteerd omdat zijn algebraïsche gegevenstypen bijbehorende methoden kunnen hebben, waardoor ze objecten zijn in de zin van gegevens die zijn opgeslagen samen met code die weet hoe ermee te werken.
Roest ondersteunt echter geen erfenis, en bevordert de samenstelling met Traits. Dit betekent dat veel OO-patronen niet werken zoals ze zijn en moeten worden aangepast. Sommige zijn totaal irrelevant.
Overerving met eigenschappen
In Rust bestaat er geen concept om de eigenschappen van een struct te "erven". In plaats daarvan, wanneer u de relatie tussen objecten ontwerpt, doet u dit op een manier waarbij iemands functionaliteit wordt gedefinieerd door een interface (een eigenschap in Rust). Dit bevordert de samenstelling boven overerving , wat nuttiger wordt geacht en gemakkelijker kan worden uitgebreid naar grotere projecten.
Hier is een voorbeeld met een voorbeeld van overerving in Python:
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'
Om dit in Rust te vertalen, moeten we eruit halen wat een dier vormt en die functionaliteit in eigenschappen zetten.
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();
}
Merk op hoe we die abstracte ouderklasse in twee afzonderlijke componenten hebben onderverdeeld: het deel dat de struct als een dier definieert, en het deel dat het toestaat om te spreken.
Begrijpende lezers zullen opmerken dat dit niet helemaal één op één is, omdat elke implementator de logica opnieuw moet implementeren om een string af te drukken in de vorm "Het {dier} zei {noise}". U kunt dit doen met een licht herontwerp van de interface waarin we Speak
for Animal
implementeren:
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();
}
Merk nu op dat het dier geluid maakt en spreekt eenvoudig een implementatie heeft voor alles wat een dier is. Dit is veel flexibeler dan zowel de vorige manier als de erfenis van Python. Als u bijvoorbeeld een Human
wilt toevoegen die een ander geluid heeft, kunnen we in plaats daarvan gewoon een andere implementatie laten speak
voor iets Human
:
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());
}
}
Bezoekerspatroon
Het typische voorbeeld van een bezoeker in Java zou zijn:
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();
}
Dit kan eenvoudig op twee manieren worden vertaald naar Rust.
De eerste manier maakt gebruik van run-time polymorfisme:
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
}
De tweede manier gebruikt in plaats daarvan compilatie polymorfisme, alleen de verschillen worden hier getoond:
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
}