Rust Style Guide (Google)
Rust wordt op veel plaatsen gebruikt, waaronder google3, Android, ChromeOS, Fuchsia en Chrome. We streven ernaar dat het gebruik van Rust binnen en tussen deze codebases consistent is. Naast consistentie helpt deze stijlgids ook bij het vaststellen van een kwaliteitsstandaard.
Achtergrond
Reikwijdte
De huidige autoriteit van de regels in deze gids is als volgt: In Android, Chromium en andere codebases is deze stijlgids ontwikkeld in overleg met andere teams en is het de bedoeling deze toe te passen. Deze codebases kunnen hun eigen regels hebben over hoe strikt dit moet gebeuren. Verdere informatie volgt naarmate deze codebases de gids adopteren.
Doelen van de stijlgids
Over het algemeen is dit document opgesteld om dezelfde redenen als de C++ stijlgids. De meeste regels vloeien voort uit de volgende doelen:
- Prioriteit voor leesbaarheid en onderhoudbaarheid boven schrijfgemak: We verwachten dat onze code langdurig relevant blijft. Een beetje extra inspanning tijdens het schrijven is op de lange termijn waardevol als dit de leesbaarheid verbetert.
- Bevorderen van betere automatisering: Grootschalige wijzigingen (Large-Scale Changes, LSC's) komen vaak voor bij Google. Consistentie maakt het uitvoeren van LSC's eenvoudiger.
- Consistentie met het bredere Rust-ecosysteem: Waar mogelijk leunen we op de bestaande consistentie in het Rust-ecosysteem om verrassingen te verminderen en de leercurve voor Google's Rust-stijl te verkleinen.
- Regels moeten hun waarde bewijzen: Omdat elke Rust-ontwikkelaar en reviewer bij Google deze regels in het achterhoofd moet houden, voegen we alleen regels toe die een tastbaar voordeel bieden.
- Voorkomen van kleine meningsverschillen over stijl: Er zijn gevallen waarin redelijke mensen van mening kunnen verschillen over stijl. Een vaste regel voorkomt dat dit bij elke Change List (CL) opnieuw besproken moet worden. Regels in deze categorie worden tot een minimum beperkt.
Publieke Rust-stijlregels volgen
Volg de officiële publieke Rust-stijlregels, tenzij deze in strijd zijn met deze stijlgids.
- Rust Style Guide: De meeste opmaakconventies worden automatisch afgedwongen door
rustfmt. - Rust API Guidelines: Een "conflict" is een situatie waarin deze gids een patroon suggereert dat wordt ontmoedigd of tegenstrijdig is aan het primaire patroon in de publieke Rust-stijlgids.
Waarom wijkt deze gids af van de reguliere Rust-stijl?
De meeste stijlgidsen richten zich op consistentie, leesbaarheid en goed API-ontwerp. Deze gids heeft echter een aanvullend doel: het vergemakkelijken van grootschalige wijzigingen (LSC's). In grote codebases zijn refactorings die door tooling worden ondersteund essentieel. Consistentie en bewuste, refactor-vriendelijke patronen zorgen ervoor dat tooling gemakkelijker met de code kan werken.
API-ontwerpregels in context toepassen
Bij het toepassen van regels voor API-ontwerp moet er onderscheid worden gemaakt tussen twee soorten code:
- Bibliotheekcode (Library code): Bedoeld voor gebruik door andere projecten (applicaties of andere bibliotheken). Lost een relatief algemeen probleem op.
- Applicatiecode (Application code): Bedoeld voor gebruik in één enkele applicatie. Lost een specifieker probleem op.
Voor bibliotheekcode zijn de regels voor API-ontwerp bindend. Voor applicatiecode zijn deze regels een sterke aanbeveling. Applicatiecode moet over het algemeen eenvoud en directheid verkiezen boven brede herbruikbaarheid. Vermijd voortijdige generalisatie en abstractie.
Codeorganisatie
Terminologie
- Crate: Een compileereenheid. Een crate bevat altijd een naamloze Rust-module, de "root module".
- Rust-module: Een eenheid van codeorganisatie binnen een crate, gebruikt om de zichtbaarheid van namen te controleren. Modules kunnen genest zijn en worden gebruikt om code over bestanden en mappen te verdelen.
Preferentie voor kleine crates [chrome]
In Chromium gaat de voorkeur uit naar kleine crates; idealiter bestaat een crate uit één enkel bestand (gericht op één abstractie). Splits code op in kleine crates indien mogelijk.
Een crate komt overeen met een enkele ruststaticlibrary (chrome) regel, die één of meer .rs bestanden kan bevatten.
Aanvullende overwegingen:
- De root module mag publieke API's re-exporteren uit leaf-submodules, waardoor submodules een implementatiedetail van de crate worden.
- Evalueer eerst of API's in dezelfde crate moeten zitten, en overweeg daarna pas of submodules helpen bij de organisatie.
- Grotere crates zijn toegestaan wanneer
trait implcoherentie-regels vereisen dat meerdere typen en traits in één bibliotheek zitten, of bij het bouwen van een grote fundamentele bibliotheek.
Rationele: Fijnmazige library targets worden in Chromium aanbevolen vanwege de enorme omvang van de codebase, de niet-lokale effecten van afhankelijkheden in de build graph, en om de prestaties van parallelle compilatie en build caching te verbeteren.
Gebruik van flat file layout voor rust_library bronnen [chrome]
In Chromium moeten .rs bronbestanden die tot één rust_library (één Rust crate) behoren, in één enkele map staan. Gebruik geen submappen om bestanden van een enkele crate te organiseren. Als dit toch nodig voelt, is dat een sterk teken dat de crate te groot is geworden en gesplitst moet worden.
Dit wijkt af van de conventionele Cargo-mapstructuur. Uitzondering: Rust-code in //third_party die ook met Cargo wordt gebouwd, moet de conventionele Cargo-structuur volgen.
Rationele:
- De voorkeur voor kleine crates betekent dat de meeste
rust_librarytargets slechts één bronbestand bevatten. - Het aanmaken van een map voor elk target zou leiden tot veel mappen met slechts één bestand.
- Multi-file targets mogen geen dedicated mappen voor submodules maken om consistentie te waarborgen.
Naamgeving van bronbestanden en leaf-modules
Wanneer een leaf-module een primair door de gebruiker gedefinieerd item heeft (bijv. Foo), moet de module naar dat item worden benoemd (bijv. mod foo en foo.rs). Leaf-modules die niet primair over één specifieke abstractie gaan, kunnen naar eigen inzicht worden benoemd.
Rationele: Dit maakt het eenvoudig om in één oogopslag de inhoud/het doel van een bronbestand of leaf-module te identificeren.
Implementeer API's niet in non-leaf modules
De enige items in non-leaf modules mogen module-declaraties (niet definities) en re-exports zijn. Let op dat de definitie van leaf-modules de volgende sub-modules toestaat: mod test en cross-language binding modules zoals #[cxx::bridge].
Uitzonderingen: Een kleine hoeveelheid kerncode die zaken "samenbindt" in een non-leaf module is acceptabel. Het doel is om te voorkomen dat er veel code in deze modules komt.
Rationele: Dit maakt het makkelijker om zaken te vinden en voorkomt het mengen van structuur en inhoud.
Gebruik geen glob imports
Gebruik geen glob imports, behalve in de volgende situaties (naar eigen discretionair vermogen):
use super::*voor inline modules.use enum::*in method-bodies van enums, en functies die primair met die enum werken.pub use submodule::*voor non-leaf modules.- C++ interop: Omdat dezelfde namespace (in C++) een submodule is in verschillende Rust-crates, mag je namespaces samenvoegen in een private kopie via glob imports. Bijvoorbeeld:
``rust mod absl { pub use status::absl::; pub use strings::absl::; } `` Dit mag alleen voor het samenvoegen van C++ namespaces, en de resulterende gemengde module mag niet publiek zijn.
Als third-party crates een "prelude" aanmoedigen, is het aan de reviewer om te bepalen of dit is toegestaan. Het definiëren van eigen preludes moet worden vermeden.
Rationele: Glob imports verhogen de werklast voor code-onderhoud bij LSC's en maken het moeilijker om te bepalen waar een type vandaan komt in contexten zonder cross-references (xrefs).
Een use-statement mag items uit slechts één module importeren
Deze regel komt overeen met de rustfmt instelling imports_granularity = Module. Er mogen in principe geen :: staan binnen het {..} blok van een use-statement.
Fout:
use foo::{Foo, bar::Bar};
Goed:
use foo::Foo;
use foo::bar::Bar;
use foo::bar::Baz;
Geef de voorkeur aan locally-scoped trait imports
Importeer traits binnen functiebodies, tenzij de methoden van de trait uitgebreid in het hele bestand worden gebruikt.
fn foo(x: u8) {
// Importeer de trait binnen een functie, niet op module-niveau.
use std::str::FromStr;
let string: String = x.parse();
}
Kwalificeer aanroepen van top-level functies met een modulenaam
Om top-level functies uit andere modules of crates te gebruiken, importeer je hun module en roep je ze aan als modulenaam::functienaam().
Goed:
use std::mem;
let x = mem::transmute(y);
Fout:
use std::mem::transmute;
let x = transmute(y);
Uitzonderingen:
- Bij functies met voldoende onderscheidende namen, bijv. functies uit C++.
- In Chromium, bij functies uit kleine, single-purpose first-party crates.
- Wanneer de modulenaam zelf gevoelig is voor collisies (bijv. C++ namespace namen).
Rationele: Veel Rust-codebases hanteren de stijl dat niet-gekwalificeerde lowercase namen als lokaal worden beschouwd. Dit voorkomt discussies tijdens reviews en verbetert de leesbaarheid zonder xrefs.
Typenamen moeten worden geïmporteerd en ongekwalificeerd worden gebruikt
Standaard moeten typen en traits in de code worden gereferenced zonder hun modulepad. Gebruik Regex in plaats van regex::Regex, met een use regex::Regex bovenaan het bestand of in de relevante functie.
Uitzonderingen:
- Bij ambiguïteit is het acceptabel om direct te refereren naar typen met veelvoorkomende namen zoals
io::Error, of om ze te hernoemen (use std::io::Error as IoError). - Bij grote groepen typen (bijv. de
hirmodule inrustc). - In macro's en gegenereerde code.
- In code met zeer veel afhankelijkheden, waar een enorme import-sectie weinig voegt.
Rationele: Deze regel is er primair om meningsverschillen tussen reviewers te voorkomen.
Gebruik het juiste importmechanisme [chrome]
Chrome: Gebruik chromium::import!().
Codeopmaak en esthetiek
Gebruik de juiste opmaaktool
Gebruik de voorgeschreven tool (bijv. git cl format in Chromium).
Configuratie (voor auteurs van de stijlgids): Een codebase kan besluiten de volgende rustfmt opties niet toe te passen als ze op stable Rust bouwen en de optie daar niet beschikbaar is:
- Maximum 100 kolommen per regel:
max_width = 100 - Gebruik van lowercase letters in hex-literalen:
hexliteralcase = Lower - Macro matcher opmaak:
formatmacromatchers = true - Gebruik van escapes voor onzichtbare tekens in literalen (bijv.
\u{200D}).
Afzien van automatische opmaak
Je mag automatische opmaak uitsluiten met #[rustfmt::skip], maar alleen in deze situaties:
- Handmatige uitlijning: Als er een specifieke reden is om code op een bepaalde manier uit te lijnen voor de leesbaarheid (bijv. een matrix).
- Gegenereerde code: (Gebruik idealiter een
genrule).
Belangrijk: Opmaak mag niet worden overgeslagen vanwege persoonlijke stijlkeuzes.
Eigendom en lening (Ownership and borrowing)
Vermijd Rc en Arc
Geef de voorkeur aan uniek eigendom en lening boven Rc en Arc voor geheugenbeheer.
Uitzondering: Gebruik Rc en Arc wanneer het alternatieve eigendoms- en leenstructuur onredelijk complex zou worden om te schrijven, te begrijpen of te onderhouden. Dit is een inschatting die tijdens de code review besproken moet worden.
Rationele: Rust maakt het mogelijk om objectgrafen te representeren zonder shared ownership pointers. Deze pointers zorgen voor runtime overhead en extra complexiteit. Ontwikkelaars uit andere talen neigen er vaak toe Rc en Arc te snel in te zetten.
Voorkeur voor lenen boven deep copies
Gebruik .clone() alleen wanneer je echt een deep copy van een object nodig hebt. Vermijd .clone() als vervanging voor lening. Ontwerp code die natuurlijk werkt met de ownership- en borrowing-regels van Rust. Als leenrelaties echter excessief complex worden, kan .clone() een pragmatische keuze zijn. Houd rekening met de prestatiebehoeften.
Let op: Dit advies geldt niet voor het klonen van Rc of Arc pointers. Het klonen hiervan creëert geen deep copy en is de bedoelde manier om ze te gebruiken.
Rationele: Deep copies kunnen duur zijn vanwege geheugenallocatie en data-kopiëren.
Functie-inputs en -outputs
Flexibele referenties accepteren voor onveranderlijke data
Wanneer functies geleende, onveranderlijke data accepteren, gebruik dan het referentietype dat de meeste flexibiliteit biedt. Geef referenties naar de data zelf in plaats van naar smart pointers, containers of wrappers.
| Gebruik | In plaats van | Waarom? |
|---|---|---|
&T | &Box<T> | Leen de data binnen de box |
&[T] | &Vec<T> | Accepteer elke slice in plaats van specifieke containers |
&str | &String | Accepteer een string slice in plaats van references naar owned strings |
Option<&T> | &Option<T> | Leen de data binnen de Option |
Uitzonderingen: Bij niet-lineaire karakterdata zoals Rope/Cord. Zie ook Clippy lints borrowedbox en ptrarg.
Rationele: Door typen te gebruiken die geen specifieke smart pointers of containers noemen, worden functies veelzijdiger en performanter; aanroepers kunnen data in diverse vormen doorgeven zonder deze eerst te kopiëren.
Iterators versus owned collections in return types
Geef waar mogelijk de voorkeur aan het teruggeven van iterators (concrete typen of impl Iterator) boven het teruggeven van Vec<T>.
Opmerking: In applicatiecode is het teruggeven van een Vec<T> acceptabel als dit de code vereenvoudigt en de voordelen van iterators niet duidelijk nodig zijn.
Uitzonderingen:
- De meeste aanroepers moeten een owned vector construeren.
- De implementatie van de functie construeert natuurlijk een vector.
- De teruggegeven waarde moet waarschijnlijk willekeurig geïndexeerd worden.
- Het algoritme werkt beter als een
for-loop en de geforceerde allocatie is minder problematisch. - Het doel van de functie is het creëren van een nieuwe owned collection.
- De functie geeft een reeds bestaande vector terug.
Rationele: Herbruikbare bibliotheekcode moet streven naar flexibele API's. Een iterator stelt de aanroeper in staat te kiezen hoe de data geconsumeerd wordt (collecten in een vector, verwerken in een loop, etc.), wat onnodige allocaties voorkomt. In applicatiecode zijn gebruikspatronen voorspelbaarder; als prestaties geen rol spelen, is een vector een simpelere API.
Iterators versus slices in functieparameters
Herbruikbare bibliotheekcode moet iterators of impl IntoIterator accepteren als er alleen over de input geïtereerd hoeft te worden.
Opmerking: In applicatiecode is het accepteren van een slice &[T] prima als dit de code vereenvoudigt.
Rationele: Functies die iterators accepteren bieden meer flexibiliteit (data kan uit elke collectie komen), efficiëntie (geen kopie naar contigu geheugen nodig) en composabiliteit (werkt beter met iterator chains).
Accepteren van Into
Bij het ontwerpen van bibliotheek-API's die een T accepteren, maar waarbij gebruikers waarschijnlijk diverse typen hebben, overweeg dan impl Into<T> in plaats van T voor betere ergonomie.
Tip: Balanceer dit met zorgen over code bloat; het is geen vereiste om overal Into<T> te gebruiken. Opmerking: Applicatiecode zou bijna nooit impl Into<T> parameters moeten gebruiken.
Rationele: impl Into<T> staat toe dat een functie direct verschillende compatibele typen accepteert (bijv. een String of een &str letteraal). De afweging is het gemak voor de aanroeper versus de kosten van generics (complexere signature, grotere code size, mogelijke problemen met type-inferentie).
Voorkeur voor enum boven bool in API's
Gebruik enum-parameters in plaats van bool in gevallen waar het doel van de parameter niet overduidelijk is. Als je toch bool gebruikt, overweeg dan een inline comment om het doel te documenteren.
Fouten rapporteren
Kies het mechanisme dat past bij je API-contract: Option, Result, of panic!.
Option: Gebruik dit voor eenvoudige fouten waarbij de oorzaak duidelijk is en de aanroeper geen baat heeft bij details (bijv. een ontbrekende waarde of parsing mismatch).- Voorbeeld:
fn checked_add(a: u32, b: u32) -> Option<u32> Result: Gebruik dit voor herstelbare fouten waarbij de aanroeper context nodig heeft om de fout af te handelen. Dit zijn vaak "onvermijdelijke" fouten door externe factoren (I/O, netwerk).- Voorbeeld:
fn open_file(path: &Path) -> Result<File, io::Error> panic!: Gebruik dit om bugs en geschonden invarianten te rapporteren. Deze fouten zijn vermijdbaar: de programmeur is een check vergeten of heeft een preconditie niet vervuld. Deze defecten moeten in de code worden opgelost, niet tijdens runtime.- Voorbeeld: Panieken als een index buiten de grenzen van een vector valt.
Opmerking: Sommige codebases vermijden panics volledig en vereisen dat zelfs geschonden invarianten via Result worden gerapporteerd.
Uitzonderingen:
- Developer-facing binaries mogen panics gebruiken voor niet-fatale fouten.
- Tests mogen panics gebruiken voor assertions.
Typen
Newtypes versus type-aliassen
Gebruik een newtype in plaats van een type-alias in de volgende gevallen:
- Wanneer het type wezenlijk semantische verschillen heeft.
- Voor typen die een eigen API rechtvaardigen.
- Wanneer je de flexibiliteit wilt om het onderliggende type later te wijzigen.
- Voor typesafety, om te voorkomen dat typen per ongeluk worden gemengd (bijv.
Day,Month,Yearals aparte newtypes in plaats vanu32).
Voorkeur voor benoemde velden boven tuples
Geef de voorkeur aan typen met benoemde velden boven tuples, aangekopen tuple-indices (.0, .1, ...) worden snel verwarrend.
Uitzonderingen:
- Newtype tuple structs (met slechts één veld).
- In zeer lokale contexten waar verwarring onwaarschijnlijk is.
Traits
Voeg trait-implementaties toe wanneer deze nodig zijn
Als algemene regel: low-level bibliotheekcode moet proactief anticiperen op de behoeften van gebruikers, terwijl applicatiecode trait-impls toevoegt wanneer dat nodig is.
Implementeer bekende standard library traits in plaats van custom methoden
Als een methode overeenkomt met het doel en gedrag van een standaard library trait, implementeer dan die trait. Gebruik bijvoorbeeld impl FromStr for Foo in plaats van fn from_str(&str) -> Self. Het is acceptabel om beide te hebben voor betere ontdekbaarheid.
Gebruik #[derive()] indien mogelijk
Handmatige implementaties moeten worden vermeden als ze kunnen worden uitgedrukt als een #[derive()].
Generics versus dyn Trait
In bibliotheken preferer je generics (T: Trait of impl Trait) in functieparameters boven dyn Trait.
Uitzonderingen: Gevallen waarin code-omvang (binary size) kritiek is.
Rationele:
- Efficiëntie:
dyn Traitvereist een "fat pointer" en een vtable-lookup bij elke aanroep. - Optimalisatie: De compiler kan bij generics optimalisaties uitvoeren (zoals inlining) die bij
dyn Traitonmogelijk zijn. - Compatibiliteit: Sommige traits zijn niet
dyncompatibel (bijv. traits met generieke functies of functies dieSelfteruggeven).
Wanneer een trait definiëren versus een inherente implementatie
Definieer een trait alleen voor de volgende doelen:
- Om generiek programmeren mogelijk te maken (
T: Trait). - Om een
impl Traitofdyn Traitte accepteren of terug te geven. - Om een extension trait te definiëren.
In alle andere gevallen gebruik je een inherente implementatie. Gebruik traits niet enkel om code te organiseren.
Extension traits
Een extension trait voegt methoden toe aan een type uit een andere crate. Het enige doel is het toevoegen van methoden; het is niet bedoeld voor generics of dynamische dispatch. De naam van een extension trait moet eindigen op ~Ext (bijv. ForeignTypeExt).
In het algemeen moet het team dat het originele type beheert, ook de extension trait beheren.
Uitzonderingen: Extension methoden die verwacht worden zeer bekend te worden en aanzienlijke waarde bieden in fundamentele libraries/frameworks.
Rationele: Wanneer hetzelfde team zowel de extension trait als het type beheert, worden naamconflicten voorkomen.
Specifieke richtlijnen voor belangrijke traits
- Typen met publieke velden: Overweeg traits te implementeren die door alle subvelden zijn geïmplementeerd.
- Error types: Implementeer de
Copytrait waar mogelijk. Copy: Implementeer dit wanneer het redelijkerwijs duidelijk is dat het type in de toekomstCopyzal blijven.Clone: De meeste typen moetenCloneimplementeren, tenzij ze een externe resource representeren.Default: Implementeer dit als het type eennew()heeft of een standaardstatus kent.PartialEq/Eq: PrefererPartialEqbovenEq. Let bijEqop de vereiste reflexiviteit.PartialOrd/Ord: PrefererPartialOrdbovenOrd. Let bijOrdop de vereiste totale ordening.- Map keys: Als een type waarschijnlijk als map key wordt gebruikt, implementeer dan
HashenEq. Indien er een totale ordening is, implementeer ookOrd.
Combineer belangrijke From impls met expliciete constructormethoden
Als een type een belangrijke From impl heeft die een primaire manier van constructie is, voeg dan een inherente ::new(), ::newfromfoo() of ::from_foo() methode toe.
Options structs moeten Default implementeren
Structs met volledig publieke velden die primair dienen als een "zak met opties" voor een functie, moeten Default implementeren. Dit maakt het gebruik van de struct-update syntax mogelijk: Struct { a: 0, ..Default::default() }.
Concurrency (Gelijktijdigheid)
Omgaan met poison
Ga nooit "in" een poisoned mutex om bij de inhoud te komen, hoe verleidelijk het scenario ook lijkt.
Gebruik geen async / .await [chrome]
Gebruik geen async / .await in Chromium.
Start geen nieuwe threads via de Rust standard library [chrome]
In Chromium gaat de voorkeur uit naar het gebruik van sequences in plaats van threads.
Gebruik fuchsia_sync::Mutex [Fuchsia]
Gebruik Fuchsia's mutex type, in plaats van die uit std::sync of parking_lot.
Macros
Bij het ontwerpen van een API is de voorkeursvolgorde:
- Reguliere Rust-functies, typen, traits, objecten en modules. (Vermijd macros en metaprogrammering).
macro_rules!-stijl macros.- Andere codegeneratiestrategieën (bijv.
genrule). proc_macromacros.
Overwegingen bij macros:
- IDE's en Code Search werken minder goed met macros.
- Macros kunnen "dialecten" van de taal creëren, wat samenwerking bemoeilijkt.
- LSC's zijn moeilijker en duurder om toe te passen.
Rationele: Macros zijn moeilijker correct te schrijven dan reguliere Rust-code. Gebruik macro_rules! voor eenvoudige generatie omdat deze op de AST opereren en minder fragiel zijn dan tekstuele generatie.
Naamgeving
- Methoden: Volg de stijl van de Rust standard library. Gebruik prefixen zoals
as...(),to...(), ofinto_...()conform deselfconventies. - Lifetimes: Gebruik beschrijvende namen, vooral voor type-lifetimes.
- Uitzonderingen: Zeer generieke abstracties zoals
Cowof anonieme lifetimes ('_). Enkele-karakter lifetimes zijn oké voor lokale relaties tussen input en output (max. twee). - Generieke parameters: Gebruik korte namen (één of meer karakters) die gerelateerd zijn aan wat ze representeren.
TofUzijn prima in ongebonden contexten (bijv.Vec<T>). - Extension traits: Gebruik het suffix
~Ext(bijv.ForeignTypeExt). - Initialisatiefuncties:
- Constructors:
new()ofnewwithfoo(). Gebruiktry_new()als het eenResultteruggeeft. - Waarde-naar-waarde conversies:
from_foo()met een bijbehorendeFromimpl.
Vermijd afkortingen
Gebruik geen afkortingen (vooral acroniemen) die iemand buiten het project waarschijnlijk niet kent. Kort niet woorden af door letters te verwijderen.
Uitzonderingen: Veelvoorkomende afkortingen uit Wikipedia of termen die onwaarschijnlijk verwarrend zijn (bijv. IpAddr).
Documentatie
Voeg documentatie-comments toe aan publieke API's. Gebruik eventueel de missing_doc lint.
Best practices:
- Voorbeelden: Niet elke functie heeft ze nodig, maar zorg dat er genoeg voorbeelden zijn om het gebruik van de crate te begrijpen.
- Panics: Documenteer altijd gevallen waarin een functie kan panieken door foutieve input.
- Unsafe: Documenteer duidelijk de invarianten voor
unsafefuncties en traits. - Stijl: Gebruik hoofdletters en interpunctie. Leg de basis van de programmeertaal niet uit.
- Inzicht: Comments moeten iets toevoegen dat de code niet al zegt. Vermijd triviale comments zoals
/// Verwijdert een elementbovenpub fn remove_element().
Formattering van documentatie-comments
- Gebruik
///voor items (functies, structs, etc.). - Gebruik
//!voor de module of bibliotheek zelf (bovenaan het bestand). - Gebruik Markdown syntax.
- Gebruik gewone comments (
//) voor implementatiedetails, TODO's of uitgecommentteerde code.
Rationele: Rust heeft een strikt onderscheid tussen doc comments en implementatie-comments. Doc comments worden door tooling (rustdoc, IDE's) geconsumeerd en zijn onderdeel van de AST, wat ze robuuster maakt.
Overige Rust-functies
Unsafe code
Schrijf geen unsafe code tenzij er geen andere manier is om de functionaliteit te implementeren.
Indien unsafe noodzakelijk is:
- Encapsuleer de code in een safe abstractie. Isoleer het zoveel mogelijk van de business logica (bijv. in een aparte crate).
- Voeg een safety comment toe die begint met
SAFETY:en uitlegt waarom de code correct is. - Voeg een technische rechtvaardiging toe: Waarom is dit niet in safe code geschreven? Zijn er benchmarks als prestaties de reden zijn?
Geen onstabiele functies
Gebruik geen onstabiele features.
Uitzonderingen: Android Embedded programming kan dit vereisen (na review). Chrome Style arbiters kunnen een kleine lijst met features goedkeuren die een duidelijk pad naar stabilisatie hebben.
Getters en setters
Hanteer de volgende naamgeving:
foo(): getter die eenTbij waarde teruggeeft.foo_mut(): getter die een&mut Tteruggeeft.take_foo(): "cleart" de waarde en geeft de oude waarde terug.set_foo(): setter die eenTbij waarde accepteert.withfoo()/withmutfoo(): accessor die via een closure werkt (object.withfoo(|foo| frob(foo))).replace_foo(): setter die de oude waarde teruggeeft.
Over accessors met closures: Deze zijn een goede keuze wanneer:
- De eigenschap in een interior mutability wrapper zit (bijv.
MutexofRefCell). - Er validatie of vervolgwerk nodig is na een wijziging, maar
set_foo()niet kan worden gebruikt omdat het type te duur is om te klonen.
Implementeer verwachte API-sets samen
Als je len() implementeert, implementeer dan ook is_empty().
Toegang tot #[doc(hidden)]
Behandel items gemarkeerd met #[doc(hidden)] als private. Gebruik ze niet, tenzij je de eigenaar van de code bent.
Gebruik const functies verantwoord
Markeer een publieke functie alleen als const als je verwacht dat deze dat in de toekomst blijft. Het wijzigen van een const functie naar een reguliere functie is een breaking change.
Vermijd overloading voor niet-mathematische doeleinden
Gebruik operator overloads alleen voor hun oorspronkelijke mathematische/logische doel, niet enkel als "syntax sugar" om code beknopter te maken.
Pas #[non_exhaustive] toe op publieke enums
Markeer enums die deel uitmaken van een publieke API voor andere teams als #[nonexhaustive]. Dit dwingt gebruikers om een wildcard-arm ( => ...) in match-statements te gebruiken, zodat de code blijft compileren wanneer er nieuwe varianten worden toegevoegd.
Uitzonderingen: De enum zal nooit veranderen (bijv. Color { Red, Green, Blue }) of de enum heeft slechts twee staten die booleans hadden kunnen zijn.
Pas #[non_exhaustive] toe op publieke structs met publieke velden
Structs met publieke velden in een publieke API moeten #[non_exhaustive] zijn. Dit voorkomt dat gebruikers de struct-literal syntax gebruiken, waardoor er later private velden kunnen worden toegevoegd zonder breaking changes.
Uitzonderingen: Velden zullen waarschijnlijk niet veranderen (bijv. een Rectangle type) of wanneer je functional record update (FRU) wilt toestaan.
#[must_use] op functies
Gebruik #[must_use] wanneer:
- De aanroeper de functie niet correct kan gebruiken als de returnwaarde wordt genegeerd (bijv. pure functies zoals
HashMap::get()). - De returnwaarde een fout of kritieke informatie bevat (tenzij het type zelf, zoals
Result, al#[must_use]is).
#[must_use] op typen
Gebruik dit op typen die worden teruggegeven door functies die #[must_use] zouden zijn.
- Voorbeelden: Fout-typen (vergelijkbaar met
Result) of typen die een lui berekende (lazily evaluated) computation representeren.
Voer geen code uit vóór main()
Zorg ervoor dat er geen code wordt uitgevoerd vóór de main() functie (vermijd bijvoorbeeld de ctor crate).
Implementatie-overwegingen
Overgebruik van type-inferentie vermijden
Voeg voldoende expliciete typen toe zodat iemand die bekend is met de codebase kan begrijpen wat de geïnferreerde typen zijn. Voeg typen bij voorkeur toe aan variabelen in plaats van via turbofishes.
Voeg typen toe in situaties zoals:
- Bij het aanroepen van methoden met een generiek return type (bijv.
.into()of.parse()). - Voor het resultaat van complexe iterator chains.
Preferentie voor pattern matching boven check-then-unwrap
Vermijd operaties die kunnen panieken wanneer de input garandeert dat dit niet gebeurt. Gebruik pattern matching om correctheid aan de compiler te bewijzen.
Goed:
if let Some(value) = option {
use_value(value);
}
Fout:
if option.is_some() {
use_value(option.unwrap());
}
Preferentie voor expect boven unwrap
Als een panic noodzakelijk is omdat een invariant niet in het typesysteem kan worden uitgedrukt, gebruik dan expect om de reden te documenteren.
Geen patterns weglaten bij non_exhaustive enums
Lijst bij voorkeur alle enum-varianten op in match-statements over non_exhaustive enums. Gebruik een wildcard-arm voor varianten waar je niet om geeft, of gebruik if let / let...else.
Gebruik context-strings voor assertions
Gebruik de optionele format-string van assert! om uit te leggen waarom een assertion moet gelden, of wat de oorzaak van de schending is.
Gebruik dit vooral wanneer:
- Je overweegt een comment bij de assertion te zetten (de string verschijnt in logs).
- Foutieve assertions door eindgebruikers gezien worden.
- Het loggen van een waarde helpt begrijpen waarom de assertion faalde.
- Je wilt verduidelijken of de client de API verkeerd gebruikt heeft, of dat er een bug in de implementatie zit.
Fout: Gebruik de string niet enkel om de conditie te herhalen (assert!(!sorted, "should be sorted")).
Geen iterator combinator chains gebruiken als directe iteratie duidelijker is
Vermijd combinator chains die de leesbaarheid schaden:
- Vermijd lange en complexe chains.
- Bouw geen chains in de header van een
for-loop; wijs de chain eerst toe aan een variabele. - Vermijd
Iterator::for_eachtenzij er een zeer goede reden is; eenfor-loop is meestal leesbaarder.
Wanneer is een combinator chain wél te prefereren? Bij korte chains of wanneer de combinator zelf veel logica bevat die anders handmatig geschreven moet worden (bijv. .any()).
Roep panic::catch_unwind niet aan
Het gebruik van panic::catch_unwind is verboden in first-party Rust code.
Uitzondering: Als "defense in depth" mechanisme, mits er niet op vertrouwd wordt voor functionele eigenschappen. Dit geldt alleen voor code die met panic=unwind kan draaien (niet van toepassing op Chrome, Android, Fuchsia).
Rationele: panic::catch_unwind werkt alleen in omgevingen met panic=unwind. Chrome, Android en Fuchsia vereisen panic=abort.
Preferentie voor format! / write! voor concatenatie en substitutie
Gebruik format!() en write!() in de volgende situaties:
- Om meer dan twee strings samen te voegen (gebruik
format!("{a}{b}{c}")in plaats vana + b + c). - Voor substituties in een letterale string (in plaats van
str::replace).
Rationele: format! / write! vereisen vaak minder allocaties dan concatenatie met +.
Gebruik het juiste test-framework per codebase
De meeste codebases gebruiken hun eigen framework in plaats van #[test]:
- Chrome: Gebruik
rustgtestinteropom Rust-tests te integreren in C++ test-binaries. - Fuchsia: Gebruik
#[fuchsia::test].
Groetjes,