Rust
Ruggine orientata agli oggetti
Ricerca…
introduzione
Rust è orientato agli oggetti in quanto i suoi tipi di dati algebrici possono avere metodi associati, rendendoli oggetti nel senso di dati memorizzati insieme a codice che sa come lavorarci.
La ruggine, tuttavia, non supporta l'ereditarietà, favorendo la composizione con i tratti. Ciò significa che molti modelli OO non funzionano così come sono e devono essere modificati. Alcuni sono totalmente irrilevanti.
Eredità con i tratti
In Rust, non esiste il concetto di "ereditare" le proprietà di una struttura. Invece, quando si progetta la relazione tra gli oggetti, si fa in modo che la propria funzionalità sia definita da un'interfaccia (un tratto in Rust). Questo promuove la composizione sull'ereditarietà , che è considerata più utile e più facile da estendere a progetti più grandi.
Ecco un esempio che utilizza alcuni esempi di ereditarietà 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'
Per tradurre questo in Rust, dobbiamo prendere ciò che costituisce un animale e mettere quella funzionalità in tratti.
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();
}
Nota come abbiamo rotto quella classe genitore astratta in due componenti separate: la parte che definisce la struttura come un animale e la parte che le consente di parlare.
I lettori più attenti noteranno che questo non è del tutto personale, poiché ogni implementatore deve reimplementare la logica per stampare una stringa nel formato "The {animal} said {noise}". Puoi farlo con una leggera riprogettazione dell'interfaccia in cui implementiamo 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();
}
Notate ora che l'animale fa rumore e parla semplicemente ora ha un'implementazione per tutto ciò che è un animale. Questo è molto più flessibile sia del modo precedente che dell'ereditarietà di Python. Ad esempio, se vuoi aggiungere un Human
con un suono diverso, possiamo invece avere un'altra implementazione di speak
per qualcosa di 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());
}
}
Modello di visitatore
L'esempio tipico di Visitor in Java potrebbe essere:
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();
}
Questo può essere facilmente tradotto in Rust, in due modi.
Il primo modo utilizza il polimorfismo di runtime:
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
}
Il secondo modo utilizza invece il polimorfismo in fase di compilazione, qui vengono mostrate solo le differenze:
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
}