Rust
Objektorienterad rost
Sök…
Introduktion
Rost är objektorienterad genom att dess algebraiska datatyper kan ha tillhörande metoder, vilket gör dem till objekt i betydelse av data lagrade tillsammans med kod som vet hur man arbetar med den.
Rost stöder emellertid inte arv, vilket föredrar komposition med egenskaper. Detta betyder att många OO-mönster inte fungerar som de är och måste ändras. Vissa är helt irrelevanta.
Arv med drag
I Rust finns det inget begrepp att "ärva" egenskaperna hos en struktur. Istället, när du utformar förhållandet mellan objekt, gör det på ett sätt som ens funktionalitet definieras av ett gränssnitt (ett drag i Rust). Detta främjar komposition över arv , vilket anses vara mer användbart och lättare att utvidga till större projekt.
Här är ett exempel med ett exempel på arv i 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'
För att översätta detta till Rust måste vi ta ut vad som utgör ett djur och sätta den funktionen i egenskaper.
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();
}
Lägg märke till hur vi delade den abstrakta förälderklassen i två separata komponenter: den del som definierar strukturen som ett djur och den del som låter den tala.
Stygga läsare kommer att märka att detta inte är riktigt en till en, eftersom varje implementatör måste implementera logiken igen för att skriva ut en sträng i formen "The {animal} said {noise}". Du kan göra detta med en liten omformning av gränssnittet där vi implementerar Speak
for Animal
:
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();
}
Lägg märke till att djuret ljuder och talar helt enkelt nu har en implementering för allt som är ett djur. Detta är mycket mer flexibelt än både det tidigare sättet och Python-arvet. Om du till exempel vill lägga till en Human
som har ett annat ljud kan vi istället bara ha en annan implementering av speak
för något 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());
}
}
Besökarmönster
Det typiska besöksexemplet i Java skulle vara:
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();
}
Detta kan lätt översättas till Rust på två sätt.
Det första sättet använder polymorfism under drift:
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
}
Det andra sättet använder kompileringstids-polymorfism istället, bara skillnaderna visas här:
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
}