Le code Rust correspondant aux classes développées dans ce laboratoire est présenté ci-dessous :
filtres.rs
use crate::image::{Color, Image};
pub struct GrayFilter;
impl GrayFilter {
pub fn apply(
&self,
input: &Image,
) -> Image {
let width = input.width();
let height = input.height();
let name = input.name().to_string();
let mut result = Image::new(width, height, name);
for y in 0..height {
for x in 0..width {
let c = input.pixel(x, y);
let g = c.gray();
result.set_pixel(x, y, Color::new(g, g, g));
}
}
result
}
}
image_processor.rs
use crate::filtres::GrayFilter;
use crate::image::Image;
pub struct ImageProcessor {
name: String,
filter: GrayFilter,
}
impl ImageProcessor {
pub fn new(name: String) -> Self {
Self {
name,
filter: GrayFilter::new(),
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn process(
&self,
input: &Image,
) -> Image {
self.filter.apply(input)
}
}
main.rs
pub mod filtres;
pub mod image;
pub mod image_processor;
use filtres::GrayFilter;
use image::{Color, Image};
use image_processor::ImageProcessor;
fn test_gray_filter(
image: &Image,
filter: &GrayFilter,
) {
let result = filter.apply(image);
let path = format!("results/{}_gray.ppm", image.name());
result.save(&path);
}
fn test_image_processor(image: &Image) {
let processor = ImageProcessor::new("IP1".to_string());
let result = processor.process(image);
let path = format!("results/{}_{}.ppm", image.name(), processor.name());
result.save(&path);
}
fn main() {
let image = Image::from_file("data/Lena.ppm");
let filter = GrayFilter;
test_gray_filter(&image, &filter);
test_image_processor(&image);
}
Nous ne reviendrons pas sur l'implémentation complète de cette bibliothèque.
Nous nous intéresserons uniquement à la manière dont Rust exprime les deux relations
étudiées dans ce laboratoire :
- la dépendance entre GrayFilter et Image ;
- la composition entre ImageProcessor et GrayFilter.
Les autres éléments de la bibliothèque (
Image,
Color, lecture et écriture
des fichiers
PPM, etc.) restent inchangés et ne seront donc pas détaillés.
La première chose que l'on remarque est que l'architecture générale est identique
à celle obtenue en C#.
Les mêmes classes deviennent des structures (
struct), les mêmes responsabilités
sont conservées et les mêmes collaborations existent entre les objets.
Les principales différences concernent la manière dont Rust exprime la propriété
des objets et les emprunts.
La dépendance
En C#, la méthode
Apply() reçoit l'image à traiter en paramètre :
public Image Apply(Image input)
En Rust, la signature devient :
pub fn apply(
&self,
input: &Image,
) -> Image
La principale différence concerne le paramètre
input.
En C#, le type
Image est une classe. Les objets de ce type sont donc manipulés par référence.
L'appel à
Apply() ne copie pas l'image : le filtre reçoit simplement une référence vers cet objet.
Le fait que cette référence soit utilisée pour exprimer une dépendance est
implicite ;
il n'apparaît pas dans la signature de la méthode.
En Rust, les structures sont déplacées (
move) lorsqu'elles sont passées à une fonction.
Pour éviter de transférer la propriété de l'image au filtre, on passe donc une
référence empruntée :
input: &Image.
La présence de la référence partagée
&Image indique explicitement que le
filtre utilise l'image sans en prendre la propriété.
La dépendance entre les deux objets apparaît donc directement dans la signature
de la méthode.
Elle résulte ici d'un
choix explicite du concepteur.
La composition à multiplicité 1
En C#, la composition est mise en œuvre par un attribut de type
GrayFilter :
private GrayFilter filter;
En Rust, on retrouve exactement le même principe :
Dans les deux cas, un objet
ImageProcessor possède un objet
GrayFilter.
En C#,
GrayFilter est une classe. L'attribut
filter contient donc une référence
vers un objet créé sur le tas (
heap).
Rien n'empêche, en théorie, plusieurs processeurs de partager le même filtre.
Pour respecter les propriétés de la composition, le programmeur doit donc être
attentif à la manière dont les objets sont créés et utilisés.
Dans notre implémentation, le filtre est créé directement dans le constructeur
de
ImageProcessor.
Chaque processeur possède ainsi son propre filtre et les propriétés de la composition
sont respectées.
En Rust, la déclaration :
a une signification plus forte. Le filtre est possédé (
owned) par le processeur
d'image.
Il ne s'agit pas d'une référence, mais d'un objet contenu dans
ImageProcessor.
Le
non-partage et le
même cycle de vie découlent naturellement du principe d'
ownership :
tant que l'attribut est déclaré sous cette forme, le filtre
ne peut appartenir qu'à un seul processeur et sera détruit en même temps que lui.
Si l'on souhaitait partager un filtre entre plusieurs objets, il faudrait l'exprimer
explicitement en utilisant une référence ou un mécanisme de partage adapté.
Bien que le
concept de composition soit le même dans les deux langages,
sa mise en œuvre varie selon leur philosophie :
- C# : le partage est la situation naturelle ; le programmeur doit éviter le
partage lorsqu'il souhaite une composition.
- Rust : la possession exclusive est la situation naturelle ; le programmeur
doit demander explicitement le partage s'il le souhaite.
Dans les deux langages, c'est le concepteur qui choisit les collaborations entre les objets.
En revanche, Rust exprime plus explicitement ces choix et en garantit naturellement
certaines propriétés grâce au système d'
ownership.
Et dans les autres langages ?
En Java et en C#, les objets étant manipulés par référence, la dépendance est implicite.
En C++ et en Rust, le concepteur choisit explicitement de passer un objet par
référence lorsqu'il souhaite exprimer une dépendance.
En Java et en C#, ces propriétés doivent être assurées par le concepteur de la classe.
Dans notre implémentation, elles sont obtenues en créant le filtre directement
dans le constructeur de
ImageProcessor.
En C++ et en Rust, elles découlent naturellement du fait que le composant est
contenu dans le composite.