tenequm/rust-dev

Guide de première journée pour bien construire en Rust - ownership, les erreurs comme valeurs, String vs &str, Box/Rc/Arc, anyhow vs thiserror, et une sélection de crates (tokio, serde, axum, sqlx). À utiliser au démarrage d'un projet Rust, en luttant contre le borrow checker, ou en choisissant des crates.

Qu'est-ce que rust-dev ?

Ce skill fournit une base pratique pour bien écrire des applications Rust dès le premier commit, en se concentrant sur les différences avec les autres langages, les décisions de première journée qui influencent tout le reste, et un petit ensemble de crates qui couvrent la plupart des applications réelles. Il couvre cinq idées clés du modèle mental de Rust (propriété, aliasage vs mutabilité, erreurs comme valeurs, traits, et le *borrow checker* comme oracle de conception), un tableau de décision complet pour les choix quotidiens (String vs &str, Box vs Rc vs Arc, anyhow vs thiserror, etc.), et des idiomes essentiels à intérioriser tôt. Le guide inclut également des conseils spécifiques pour les développeurs venant de Python, JavaScript, Go, Java/C# et C++, ainsi qu'une courte liste de crates comme serde, tokio, anyhow, thiserror, clap, axum, reqwest, sqlx, tracing et regex. Une section de références rapides couvre les opérations courantes comme la lecture/écriture de fichiers, l’appel HTTP, les requêtes SQL asynchrones, les helpers Option/Result, les tests et la sérialisation JSON. Ce skill est utile pour tout développeur débutant un projet Rust, qu'il s'agisse d'une CLI, d'un service ou d'une bibliothèque, et qui souhaite éviter les pièges courants et adopter les bonnes pratiques dès le départ.

Compatible avec~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/tenequm/skills/tree/HEAD/skills/rust-dev

Installed? Explore more Développement et programmation skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

Développement Rust - Première Journée

Une base pratique pour bien écrire des applications Rust dès le premier commit. Pas un manuel. Se concentre sur les différences avec les autres langages, les décisions de première journée qui influencent tout le reste, et le petit ensemble de crates qui couvrent la plupart des applications réelles.

Quand l'utiliser

  • Démarrer un nouveau projet Rust (CLI, service, bibliothèque)
  • Venir à Rust depuis Python, JavaScript, Go, Java/C# ou C++
  • Choisir entre types possédés/empruntés, pointeurs intelligents, objets traits vs génériques
  • Choisir une stratégie de gestion des erreurs (anyhow vs thiserror)
  • Décider quelles crates utiliser
  • Configurer un Cargo.toml minimal mais opiné, clippy et rustfmt

Configuration de première journée

# 1. Installer la toolchain (rustup est le gestionnaire de toolchain)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# 2. Confirmer les composants (rustfmt et clippy sont livrés avec stable, rust-src active les fonctionnalités IDE)
rustup component add rustfmt clippy rust-src

# 3. Créer un projet
cargo new my-app          # binaire (src/main.rs)
cargo new --lib my-lib    # bibliothèque (src/lib.rs)

# 4. La boucle de développement (mémorisez ces quatre)
cargo check     # vérification rapide des types, sans génération de code
cargo run       # construire et exécuter (binaire)
cargo test      # construire et exécuter les tests (y compris doctests)
cargo clippy --all-targets   # lint, y compris tests/ et benches/ (à exécuter avant un push)
cargo fmt       # formater

# 5. Gérer les dépendances sans éditer Cargo.toml à la main
cargo add tokio --features full
cargo remove tokio
cargo update                 # recalculer Cargo.lock dans les plages semver existantes

cargo update ne se déplace que dans les plages de versions déjà dans Cargo.toml. Passer une version majeure (1.x à 2.0) nécessite une modification de Cargo.toml ou cargo add <crate>@2.

rust-analyzer est obligatoire. C'est le serveur de langage que chaque éditeur utilise (VS Code, Zed, Neovim, Helix, RustRover utilise son propre moteur mais est comparable). Dans VS Code, installez l'extension rust-analyzer et définissez rust-analyzer.check.command à "clippy" pour obtenir un retour de lint à la sauvegarde.

Vous voulez un observateur de fichiers plus tard ? cargo install --locked bacon, puis exécutez bacon dans votre projet. Pas nécessaire le premier jour.

Le modèle mental de Rust en 5 idées

Rust échange deux choses que vous tenez pour acquises dans la plupart des langages (un ramasse-miettes et des exceptions) contre des garanties à la compilation concernant la mémoire, les accès concurrents et la gestion des erreurs. La forme du langage découle de cet échange.

1. La propriété : chaque valeur a exactement un propriétaire

Pensez aux valeurs comme à des objets physiques. Un livre, un fichier, une connexion réseau. À tout moment, une variable la possède. Vous pouvez :

  • La déplacer : let b = a; donne la propriété à b. a n'existe plus.
  • L'emprunter de manière immuable : &a permet à d'autres de la regarder. Plusieurs lecteurs autorisés.
  • L'emprunter de manière mutable : &mut a permet à une personne de la modifier. Accès exclusif.
  • La cloner : a.clone() fait une copie profonde. Les deux gardent la leur.

Quand le propriétaire sort de la portée, la valeur est libérée (mémoire libérée, fichier fermé, verrou relâché). Pas de GC, pas de free manuel. C'est le RAII, imposé par le compilateur.

2. Aliasage XOR mutabilité

À tout moment, une donnée a soit :

  • une référence mutable (&mut T), ou
  • un nombre quelconque de références immuables (&T),

jamais les deux en même temps. Cette seule règle élimine les accès concurrents et la plupart des bugs d'utilisation après libération. Le borrow checker l'applique. Quand il se plaint, il vous dit que votre histoire de propriété des données n'est pas claire, pas que le langage est difficile.

3. Les erreurs sont des valeurs, pas des exceptions

Il n'y a pas de try/catch. Les fonctions qui peuvent échouer retournent Result<T, E>. Les fonctions qui ne retournent rien d'utile retournent Option<T>. Le compilateur vous force à gérer les deux. L'opérateur ? propage les erreurs dans la pile d'appels avec un seul caractère :

fn read_config() -> Result<Config, anyhow::Error> {
    let text = std::fs::read_to_string("config.toml")?;  // ? = retour anticipé en cas d'Err
    let config = toml::from_str(&text)?;
    Ok(config)
}

Il n'y a pas de null. Option<T> est None ou Some(value). Le compilateur ne vous laissera pas oublier le cas None.

4. Les traits ne sont pas des interfaces Java

Un trait définit un comportement. Les types implémentent les traits. Jusque là, familier. Les différences :

  • La dispatch statique est la valeur par défaut. Quand vous écrivez fn f<T: Display>(x: T), le compilateur génère une copie séparée de f pour chaque T concret avec lequel vous l'appelez (monomorphisation, comme les templates C++). Surcharge à l'exécution nulle.
  • La dispatch dynamique est optionnelle via dyn Trait (généralement Box<dyn Trait> ou &dyn Trait). Une consultation de vtable par appel.
  • Pas d'héritage. Les traits composent. Si vous vous surprenez à utiliser Deref pour "étendre" un type, arrêtez-vous et utilisez la composition ou une énumération.
  • Règle de l'orphelin : vous pouvez impl VotreTrait pour TypeDUnAutre ou impl TraitDeUnAutre pour VotreType, mais pas les deux en même temps. Cela maintient la résolution des dépendances saine.

5. Le borrow checker est un oracle de conception

L'erreur la plus courante chez les nouveaux venus est de traiter les erreurs du compilateur comme des obstacles à faire taire. Ce n'est pas le cas. Presque chaque erreur de borrow check révèle un vrai problème de qui possède quoi. Quand vous êtes bloqué, la question est rarement "comment faire compiler ça" et presque toujours "quelle est la relation de propriété réelle que je veux ici ?" Lisez l'erreur. Le compilateur est inhabituellement instructif.

Les 3 questions pour chaque signature de fonction

Avant d'écrire une fonction, demandez-vous : doit-elle posséder, lire ou modifier l'entrée ?

fn consume(s: String)        // possède :   la fonction prend la responsabilité, l'appelant la perd
fn read(s: &str)             // lit :       la fonction regarde, l'appelant garde
fn modify(s: &mut String)    // modifie :   la fonction change sur place

Valeurs par défaut qui fonctionnent 90% du temps :

  • Paramètres de fonction : préférer &str à String, &[T] à Vec<T> (ce sont des tranches, acceptent les appelants possédés et empruntés).
  • Retours de fonction : retourner des types possédés (String, Vec<T>). Retourner des références implique des durées de vie ; évitez jusqu'à en avoir besoin.
  • Champs de structure : préférer les types possédés (String, Vec<T>). Stocker &str dans une structure est le piège le plus courant pour les débutants et il cascade des annotations de durée de vie dans chaque type qui contient votre structure.

Tableau de décision de première journée

Réponses en une ligne aux choix qui se présentent en premier.

DécisionPar défautQuand choisir l'autre
String vs &str (champ de structure)StringPresque jamais &str jusqu'à ce que vous ayez une vraie raison et compreniez les durées de vie
String vs &str (paramètre de fonction)&strUtilisez String seulement si vous devez posséder/stocker à l'intérieur
Vec<T> vs &[T] (paramètre)&[T]Vec<T> seulement si vous devez posséder
Box<T> vs Rc<T> vs Arc<T>Box<T> (propriétaire unique, tas)Arc<T> pour la propriété partagée entre threads. Évitez Rc<T> par défaut ; utilisez Arc<T> pour ne pas avoir à refactoriser quand vous passez en asynchrone
RefCell<T> vs Mutex<T>Mutex<T> (ou RwLock<T>)Même raison : fonctionne en asynchrone/threads, alors que RefCell non
Option<T> vs Result<T, E>Option<T> pour "pas de valeur", Result<T, E> pour "a échoué pour une raison"Si l'absence a une signification que l'appelant doit gérer, Result
dyn Trait vs impl Trait / <T: Trait>Générique (<T: Trait> ou impl Trait) - dispatch statiqueBox<dyn Trait> quand vous avez besoin d'une collection hétérogène (Vec<Box<dyn Animal>>)
Erreurs dans le code applicatifanyhow::Result<T> partout-
Erreurs dans le code de bibliothèqueEnumération dérivée de thiserrorJamais Box<dyn Error> dans les API de bibliothèque publiques - force les appelants à faire un downcast
&self vs &mut self vs self&self pour les getters, &mut self pour les setters, self pour les builders/opérations consommatrices-
Disposition des modulesModules en ligne jusqu'à ce qu'un fichier devienne long, puis diviserUn module = un fichier est un instinct Java/C#, pas un instinct Rust

Idiomes à intérioriser tôt

Ceux-ci apparaissent dans presque tous les programmes Rust. Apprenez-les en première semaine.

? pour la propagation des erreurs. Remplace neuf lignes de match par un caractère.

let body = reqwest::get(url).await?.text().await?;

Chaînes d'itérateurs plutôt que des boucles manuelles. Compilent dans le même code machine que des boucles écrites à la main (LLVM inline les fermetures). Le Rust idiomatique est de style fonctionnel.

let active_emails: Vec<String> = users
    .iter()
    .filter(|u| u.active)
    .map(|u| u.email.clone())
    .collect();

Exhaustivité de match. Ajoutez une nouvelle variante à une énumération et chaque match qui ne la gère pas devient une erreur de compilation. Utilisez cela. C'est l'un des outils de refactorisation les plus puissants de tous les langages.

Laissez le compilateur piloter les refactorisations. La même astuce fonctionne avec les structures : ajoutez un champ obligatoire sans Default et chaque constructeur existant devient une erreur de compilation - une liste de travail exhaustive et gratuite de tous les sites à mettre à jour.

if let et let else pour le cas courant de branche unique.

if let Some(name) = user.name { println!("salut {name}"); }

let Some(name) = user.name else { return Err(anyhow!("pas de nom")); };
// `name` est dans la portée à partir d'ici, pas d'imbrication

From / Into pour les conversions de types. Implémentez From, obtenez Into gratuitement. ? utilise From pour convertir automatiquement les types d'erreurs.

Combinateurs sur Option / Result. Utilisez .map, .and_then, .unwrap_or, .unwrap_or_else, .ok_or avant d'utiliser match.

Macros de dérivation. #[derive(Debug, Clone, PartialEq)] vous donne 80% du code standard gratuitement. Ajoutez #[derive(Serialize, Deserialize)] pour le JSON.

Sugar récent. Fonctionnalités stabilisées qui valent la peine d'être connues, les plus récentes en premier : bool::ok_or et ok_or_else (1.98) transforment une condition directement en Result (ready.ok_or(Error::NotReady)?). cfg_select! est un match à la compilation sur des prédicats cfg, remplaçant la plupart des utilisations de la crate cfg-if, et les gardes if let fonctionnent sur les branches match (match x { Some(v) if let Ok(n) = v.parse::<i32>() => ... }) - tous les deux en 1.95. assert_matches! et debug_assert_matches! (1.96) affirment sur un motif plutôt que sur l'égalité, ce qui est l'assertion naturelle pour une énumération. Deux plus anciens que vous verrez constamment et ne devez pas prendre pour exotiques : let-chains (if let Some(x) = a && x > 3 { ... }), stable dans l'édition 2024 depuis 1.88, et fermetures asynchrones (async || { ... }, avec la famille de limites AsyncFn), stables depuis 1.85.

Venant de X, voici ce qui vous mord

De Python ou JavaScript :

  • let b = a; pour une valeur sur le tas (comme String, Vec) déplace la valeur. a n'est plus utilisable. Utilisez &a pour emprunter ou a.clone() pour copier.
  • Pas de null. Option<T> est imposé.
  • Pas d'exceptions. Result<T, E> et ?. Le compilateur ne vous laissera pas ignorer les erreurs.
  • Pas d'héritage. Composition + traits + énumérations.
  • Les variables sont immuables par défaut. Ajoutez mut pour muter. Pareil pour les références : & vs &mut.
  • Redéclarer un nom est autorisé et idiomatique : let input = input.trim(); occulte l'ancienne liaison par une nouvelle, éventuellement d'un type différent. Ce n'est pas une mutation, et c'est ainsi que le code Rust convertit une valeur sur place sans inventer input2.
  • Les types d'entiers sont explicites et l'indexation nécessite usize. Ils débordent différemment selon la construction : debug "inclut des vérifications de débordement d'entier qui font paniquer votre programme à l'exécution", tandis que --release les supprime et effectue un "complément à deux avec enveloppement". Une suite de tests qui passe peut encore envelopper en production, donc utilisez checked_*/saturating_*/wrapping_* là où l'arithmétique peut vraiment atteindre la limite, plutôt que de compter sur la panique en debug pour la trouver. strict_add et ses amis (stables depuis 1.91) paniquent en cas de débordement dans toutes les constructions, et overflow-checks = true sous [profile.release] conserve le comportement de debug en production. Et as entre types d'entiers tronque silencieusement (300_u32 as u8 donne 44) - utilisez u8::try_from(x)? partout où la valeur pourrait ne pas tenir.
  • Une String n'est pas indexable. s[0] ne compile pas - voir references/ownership-and-types.md pour quoi utiliser à la place.

De Go :

  • Les erreurs comme valeurs - même instinct, mais utilisez ? au lieu de if err != nil.
  • Pas de nil. Option<T>.
  • Pas de GC et pas de goroutines : propriété + emprunt, async/await avec tokio. Le modèle asynchrone est coopératif (await est un point d'abandon explicite), pas préemptif.
  • interface{} devient des traits. Par défaut, des génériques pour la dispatch statique ; Box<dyn Trait> seulement quand vous en avez besoin.
  • La liaison statique est la valeur par défaut. Les binaires sont plus gros mais autonomes.
  • panic! doit être réservé aux bugs irrécupérables dans le code applicatif ; ne l'utilisez pas comme un "log et continue" à la Go.

De Java ou C# :

  • Les traits ne sont pas des interfaces avec dispatch virtuel par défaut. <T: Trait> est monomorphisé. dyn Trait est la version dynamique optionnelle.
  • Pas de références nulles. Option<T>.
  • Pas d'exceptions. Result<T, E> et ?.
  • Pas d'héritage de classe. Utilisez des énumérations pour les types sommes, des traits pour le comportement partagé.
  • Pas de GC : la propriété et l'emprunt décident des durées de vie. Arc<T> est ce qu'il y a de plus proche d'une référence Java.
  • Les génériques sont monomorphisés, pas effacés par type.

De C++ :

  • Comme le RAII, mais le borrow checker l'applique à la compilation.
  • Pas de constructeurs de copie/déplacement. Clone est explicite et Copy est un trait marqueur pour les copies binaires bon marché.
  • Pas de comportement indéfini dans le code sûr (en théorie).
  • & est un emprunt vérifié à la compilation, pas un pointeur brut. Les pointeurs bruts existent (*const T, *mut T) mais nécessitent unsafe pour être déréférencés.
  • Les pointeurs intelligents sont Box<T> (unique_ptr), Rc<T> (shared_ptr, mono-thread), Arc<T> (shared_ptr, thread-safe).
  • Les macros sont hygiéniques. Les macros procédurales (derive, attribut, fonctionnelles) sont comment serde, tokio::main, etc. fonctionnent.

La courte liste de crates

Celles-ci couvrent la plupart des applications réelles. Ajoutez-les selon les besoins ; elles ne sont pas toutes obligatoires.

CrateCe qu'elle vous apporte
serde + serde_jsonSérialisation. #[derive(Serialize, Deserialize)] et vous avez terminé
tokioExécution asynchrone. #[tokio::main], tokio::spawn, E/S asynchrones
anyhowType d'erreur applicatif. anyhow::Result<T>, bail!, context()
thiserrorÉnumérations d'erreurs de bibliothèque. #[derive(thiserror::Error)]
clapAnalyse des arguments en ligne de commande. #[derive(Parser)]
axumServeur Web. Basé sur tokio + tower. Exprime la composition comme des types
reqwestClient HTTP. Bloquant et async
sqlxAccès à la base de données. Vérifié à la compilation contre le schéma réel. PostgreSQL, MySQL, SQLite
tracingLog structuré et propagation de spans pour les contextes async tokio
regexExpressions régulières. Compilées. Regex::new(r"...")
chronoTypes de date/heure : DateTime<Utc>, NaiveDate, Duration
uuidIdentifiants uniques universels : Uuid::new_v4(), serde features
tokio-streamAdapte les récepteurs tokio et autres sources en flux Stream
futuresUtilitaires Stream (StreamExt) et extensions combinatories
randGénération de nombres aléatoires
csvÉcriture/lecture de CSV, avec support de la sérialisation

Références rapides

Lire un fichier

use std::fs;

// Lossy - remplace les octets non UTF-8 par U+FFFD
let text = fs::read_to_string("path")?;

// Binaire
let bytes = fs::read("path")?;

// Ligne par ligne
for line in fs::read_to_string("path")?.lines() {
    // ...
}

Écrire un fichier

use std::fs;

// Écrase le fichier existant
fs::write("output.json", &json_string)?;

// Écriture bufferisée (performante pour les écritures multiples)
use std::io::Write;
let mut file = fs::File::create("output.txt")?;
file.write_all(b"hello\n")?;

Si vous écrivez fréquemment des lignes, enveloppez le fichier dans std::io::BufWriter :

use std::io::{BufWriter, Write}
let file = fs::File::create("large.log")?;
let mut writer = BufWriter::new(file);
writer.write_all(b"ligne\n")?;

Appel HTTP (reqwest)

// GET
let resp = reqwest::get("https://api.example.com/data").await?;
let body = resp.text().await?;

// POST avec JSON
let client = reqwest::Client::new();
let resp = client
    .post("https://api.example.com/submit")
    .json(&payload)
    .send()
    .await?;

SQL avec sqlx

use sqlx::postgres::PgPoolOptions;

let pool = PgPoolOptions::new()
    .max_connections(5)
    .connect("postgres://user:pass@localhost/db")
    .await?;

// Récupérer un enregistrement
let user: (i64, String) = sqlx::query_as("SELECT id, name FROM users WHERE id = $1")
    .bind(user_id)
    .fetch_one(&pool)
    .await?;

// Plusieurs enregistrements dans un struct
#[derive(sqlx::FromRow)]
struct User { id: i64, name: String, email: String }

let users: Vec<User> = sqlx::query_as::<_, User>("SELECT id, name, email FROM users")
    .fetch_all(&pool)
    .await?;

Helpers Option/Result

// Transformer Option en Result avec une erreur personnalisée
let val = opt.ok_or(anyhow!("valeur manquante"))?;
let val = opt.ok_or_else(|| anyhow!("valeur manquante"))?;  // paresseux

// Valeur par défaut
let val = opt.unwrap_or(default);
let val = opt.unwrap_or_else(|| compute_default());

// Carte de conversion dans Option/Result
let val = opt.map(|x| x * 2);
let val = res.map_err(|e| format!("échec : {e}"));

// and_then - chaînage des opérations qui retournent Option/Result
let val = opt.and_then(|x| x.parse::<i32>().ok());

// Iter sur l'intérieur
for item in opt.iter() { /* s'exécute seulement si Some */ }

Tester avec des assertions de motifs

use std::assert_matches::assert_matches;

let res: Result<i32, _> = Ok(42);
assert_matches!(res, Ok(42) | Err(_));

Surcharge d'opérateurs (modèles courants)

use std::ops::{Add, Mul};

#[derive(Debug, Clone, PartialEq)]
pub struct Point {
    x: f64,
    y: f64,
}

impl Add for Point {
    type Output = Self;
    fn add(self, rhs: Self) -> Self::Output {
        Self { x: self.x + rhs.x, y: self.y + rhs.y }
    }
}

impl Mul<f64> for Point {
    type Output = Self;
    fn mul(self, rhs: f64) -> Self::Output {
        Self { x: self.x * rhs, y: self.y * rhs }
    }
}

Les fonctions de trait se prennent par valeur. Si vous devez aussi supporter &Point + Point, Point + &Point, etc., implémentez les combinaisons explicitement ou utilisez le paquet impl_ops.

Ajouter un middleware à axum

use axum::{
    Router,
    middleware::{self, Next},
    response::Response,
    http::Request,
};

async fn log_request(req: Request<axum::body::Body>, next: Next) -> Result<Response, axum::response::Response> {
    println!("{} {}", req.method(), req.uri());
    let res = next.run(req).await;
    println!("status: {}", res.status());
    Ok(res)
}

let app = Router::new()
    .route("/", get(handler))
    .layer(middleware::from_fn(log_request));

#[tokio::test] pour les tests asynchrones

#[cfg(test)]
mod tests {
    use super::*;

    #[tokio::test]
    async fn test_fetch_user() {
        let pool = test_db().await;
        let user = get_user(&pool, 1).await.unwrap();
        assert_eq!(user.name, "Alice");
    }

    async fn test_db() -> PgPool {
        // utilisez testcontainers ou une URL de BD de test
    }
}

Ajouter sérialisation à un struct

use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
    pub host: String,
    pub port: u16,
    #[serde(default = "default_timeout")]
    pub timeout_secs: u64,
}

fn default_timeout() -> u64 { 30 }

// Vers fichier JSON
fs::write("config.json", serde_json::to_string_pretty(&config)?)?;

// Depuis fichier JSON
let config: Config = serde_json::from_str(&fs::read_to_string("config.json")?)?;

Marquer les tests d'intégration

#[cfg(test)]
mod tests {
    use super::*;

    // Les tests coûteux/externes obtiennent leur propre tag
    #[test]
    #[ignore = "nécessite une base de données en cours d'exécution"]
    fn test_full_pipeline() {
        // ...
    }

    // Alternative : utiliser un module avec #![cfg(feature = "integration_tests")]
    // et exécuter avec cargo test --features integration_tests
}

Modèle de configuration d'application lire-depuis-env

#[derive(serde::Deserialize)]
pub struct AppConfig {
    pub database_url: String,
    pub host: String,
    pub port: u16,
    pub log_level: String,
}

impl AppConfig {
    pub fn from_env() -> Result<Self, config::ConfigError> {
        // Utilisez la crate `config` pour les fichiers .env, toml, etc.
        config::Config::builder()
            .add_source(config::Environment::default())
            .build()?
            .try_deserialize()
    }
}

Présence de & dans les signatures de fonction

Si une fonction prend &self, elle lit l'instance. Vous pouvez appeler la méthode sur n'importe quel propriétaire ou emprunt :

struct Counter { count: i32 }
impl Counter {
    fn current(&self) -> i32 { self.count }
    fn inc(&mut self) { self.count += 1 }
}

let mut c = Counter { count: 0 };
let r = &c;
r.current();     // ✅ &self fonctionne sur &Counter
// r.inc();      // ❌ &mut self nécessite &mut Counter
c.inc();         // ✅ appel sur le propriétaire

De manière générale : la présence de & ne détermine pas si l'appelant passe une référence ou une valeur. Ce qui compte est si le type que vous passez à la fonction est une référence (&T) ou une valeur (T).

Individual skills in this repo

This repo contains 4 individual skills — each has its own dedicated page.

Skills associés