AgentSkillsCN

Rust

当用户提出“编写Rust代码”“创建Rust模块”“实现结构体”“定义trait”“添加错误处理”“修复这段Rust代码”“重构这段Rust代码”“在Rust中建模这种类型”“使用async/await”“编写异步函数”“处理生命周期”“组织Rust模块”“搭建Rust项目”“设置Cargo工作空间”“使用serde”“配置tokio”“编写Rust测试”或任何涉及Rust代码编写、审查或优化的任务时,应使用此技能。它提供了2024年版的最佳实践指南,涵盖类型设计、错误处理、异步模式、Trait设计、生命周期管理、模块组织以及测试方法。

SKILL.md
--- frontmatter
name: Rust
version: 0.1.0
description: >-
  This skill should be used when the user asks to "write Rust code",
  "create a Rust module", "implement a struct", "define a trait",
  "add error handling", "fix this Rust code", "refactor this Rust",
  "model this type in Rust", "use async/await", "write an async function",
  "handle lifetimes", "organize Rust modules",
  "set up a Rust project", "set up a Cargo workspace",
  "use serde", "configure tokio", "write Rust tests",
  or any task involving
  writing, reviewing, or improving Rust code. Provides best practices
  for the 2024 edition including type design, error handling, async
  patterns, trait design, lifetimes, module organization, and testing.

Rust

Best practices for writing idiomatic Rust targeting the 2024 edition (Rust 1.85+). Covers type design, error handling, async patterns, trait design, ownership and lifetimes, module organization, and testing.

Edition and Project Setup

Cargo.toml

Target the 2024 edition in new projects:

toml
[package]
edition = "2024"
rust-version = "1.85"

Key 2024 Edition Changes

Changes that affect how code is written day-to-day:

ChangeImpact
RPIT lifetime captureimpl Trait return types capture all in-scope lifetime params by default. Use use<'a> to opt out.
Unsafe extern blocksextern "C" { ... } items are implicitly unsafe. Wrap in unsafe extern or add safe keyword to individual items.
unsafe_op_in_unsafe_fn warningUnsafe operations inside unsafe fn now warn without an inner unsafe block.
gen keyword reservedRename any identifiers named gen (use r#gen as escape hatch).
Prelude additionsFuture and IntoFuture are in the prelude — no import needed.
Temporary scope changesif let and tail expression temporaries drop before local variables.
Cargo MSRV-aware resolverCargo prefers dependency versions compatible with declared rust-version.

Migration

Run cargo fix --edition before updating edition = "2024" in Cargo.toml. For large codebases, enable rust-2024-compatibility lints incrementally. Update code-generating crates (bindgen, cxx, proc-macros) first.

Type Design

Newtype Pattern

Wrap primitive types to create domain-specific types with zero runtime cost:

rust
struct UserId(u64);
struct EmailAddress(String);

impl UserId {
    fn new(id: u64) -> Self { Self(id) }
    fn as_u64(&self) -> u64 { self.0 }
}

Newtypes prevent accidental interchange (UserId vs bare u64), enable trait implementations on foreign types (orphan rule workaround), and hide internal representation.

Discriminated Enums

Model variants with data as enums, not stringly-typed fields:

rust
enum ConnectionState {
    Disconnected,
    Connecting { attempt: u32 },
    Connected { session_id: String },
    Failed { error: String, retries: u32 },
}

Match exhaustively. The compiler enforces handling of every variant.

Typestate Pattern

Encode state transitions in the type system to make invalid states unrepresentable:

rust
struct Unvalidated;
struct Validated;

struct Form<State> {
    data: FormData,
    _state: std::marker::PhantomData<State>,
}

impl Form<Unvalidated> {
    fn validate(self) -> Result<Form<Validated>, ValidationError> { /* ... */ }
}

impl Form<Validated> {
    fn submit(self) -> Result<Response, SubmitError> { /* ... */ }
}

submit() is only callable on Form<Validated> — calling it on an unvalidated form is a compile error. Use sparingly; the complexity cost is justified when invalid state transitions cause serious bugs.

Builder Pattern

Use the builder pattern for types with many optional fields:

rust
struct ServerConfig {
    host: String,
    port: u16,
    max_connections: Option<usize>,
    timeout: Option<Duration>,
}

impl ServerConfig {
    fn builder(host: impl Into<String>, port: u16) -> ServerConfigBuilder {
        ServerConfigBuilder {
            host: host.into(),
            port,
            max_connections: None,
            timeout: None,
        }
    }
}

impl ServerConfigBuilder {
    fn max_connections(mut self, n: usize) -> Self {
        self.max_connections = Some(n);
        self
    }

    fn timeout(mut self, duration: Duration) -> Self {
        self.timeout = Some(duration);
        self
    }

    fn build(self) -> ServerConfig {
        ServerConfig {
            host: self.host,
            port: self.port,
            max_connections: self.max_connections,
            timeout: self.timeout,
        }
    }
}

Required parameters go in the builder constructor. Optional parameters are set via chained methods consuming and returning Self. Call .build() to produce the final type. For critical state ordering, combine with the typestate pattern.

For detailed type patterns including generic newtypes, sealed traits, and advanced typestate, consult references/type-patterns.md.

Error Handling

Library Errors: thiserror

Define typed errors at library/crate boundaries:

rust
use thiserror::Error;

#[derive(Debug, Error)]
enum StorageError {
    #[error("record not found: {id}")]
    NotFound { id: String },
    #[error("connection failed")]
    Connection(#[from] std::io::Error),
    #[error("deserialization failed")]
    Deserialize(#[source] serde_json::Error),
}

#[from] generates From impls for automatic ? conversion. #[source] preserves the error chain without generating From. Always derive Debug.

Application Errors: anyhow

Use anyhow at the application layer for ergonomic error aggregation:

rust
use anyhow::{Context, Result};

fn load_config(path: &Path) -> Result<Config> {
    let contents = std::fs::read_to_string(path)
        .context("failed to read config file")?;
    let config: Config = toml::from_str(&contents)
        .context("failed to parse config")?;
    Ok(config)
}

Always add .context() when propagating with ? — bare ? loses the "what was happening" information.

Guidelines

LayerCratePattern
Library / public APIthiserrorNamed error enum with #[from] / #[source]
Application / mainanyhowanyhow::Result<T> with .context()
Internal modulesEitherMatch the layer's convention

Do not over-engineer error types. If callers always handle variants the same way, fewer variants or #[error(transparent)] wrapping is better.

Async Patterns

async fn in Traits (Stable since 1.75)

rust
trait DataStore {
    async fn get(&self, key: &str) -> Result<Vec<u8>>;
    async fn put(&self, key: &str, value: &[u8]) -> Result<()>;
}

Caveat: async trait methods are not dyn-compatible. Use the async-trait crate or manual Pin<Box<dyn Future>> desugaring when trait objects are needed.

Async Closures (Stable since 1.85)

rust
let fetch = async |url: &str| {
    reqwest::get(url).await?.text().await
};

Async closures return futures when called. The compiler uses AsyncFn, AsyncFnMut, and AsyncFnOnce traits internally — pass async closures directly rather than naming these traits in bounds.

Cancellation and Timeouts

rust
use tokio::time::{timeout, Duration};

let result = timeout(Duration::from_secs(5), some_async_work()).await;
match result {
    Ok(inner) => { /* inner is the Result from some_async_work */ }
    Err(_) => { /* timed out */ }
}

Use tokio::select! for racing multiple futures. Use CancellationToken for structured cancellation across task trees.

Send + Sync Rules

tokio::spawn requires Future + Send + 'static. Common fixes for Send errors:

  • Don't hold MutexGuard across .await — drop the guard before awaiting.
  • Use tokio::sync::Mutex when a lock must span an .await point.
  • Use spawn_local for futures that cannot be Send.

Blocking Work

Never run CPU-intensive or blocking I/O on the async runtime. Use tokio::task::spawn_blocking for blocking operations and tokio::sync::mpsc channels to communicate results back.

For detailed tokio channel types, select! patterns, and task management, consult references/api-reference.md.

Trait Design

Associated Types vs. Generics

Use associated types when there is one natural type per implementation:

rust
trait Parser {
    type Output;
    fn parse(&self, input: &str) -> Result<Self::Output>;
}

Use generic parameters when a single type can implement the trait for multiple type arguments:

rust
trait Convert<T> {
    fn convert(&self) -> T;
}

Sealed Traits

Seal traits when implementations should only exist inside the current crate:

rust
mod private { pub trait Sealed {} }

pub trait DatabaseDriver: private::Sealed {
    fn connect(&self, url: &str) -> Result<Connection>;
}

This preserves the ability to add methods in non-breaking releases. Document that the trait is sealed.

Extension Traits

Add methods to foreign types via extension traits:

rust
pub trait IteratorExt: Iterator {
    fn count_where<P: Fn(&Self::Item) -> bool>(self, predicate: P) -> usize
    where Self: Sized {
        self.filter(predicate).count()
    }
}

impl<I: Iterator> IteratorExt for I {}

#[diagnostic::do_not_recommend] (1.85+)

Annotate trait impls that produce confusing error messages when they fail to match:

rust
#[diagnostic::do_not_recommend]
impl<T: ToString> From<T> for MyError {
    fn from(value: T) -> Self {
        MyError::Other(value.to_string())
    }
}

The compiler will skip this impl in diagnostics and show a more helpful suggestion instead.

Ownership and Lifetimes

Elision Rules

The compiler infers lifetimes in most function signatures:

  1. Each input reference gets a distinct lifetime.
  2. If there is exactly one input lifetime, it applies to all output references.
  3. If one input is &self or &mut self, its lifetime applies to all output references.
  4. Otherwise, annotate explicitly.

Practical Guidelines

  • Let elision work. Do not annotate lifetimes that the compiler can infer. Unnecessary annotations like &'a mut self on methods can over-constrain borrowing.
  • Scope borrows narrowly. Extract the borrow into a block or helper to release it before other operations on the same data.
  • Prefer owned data at API boundaries. Accept String / Vec<u8> rather than &str / &[u8] when the callee needs to store the data. Use impl Into<String> for ergonomic conversion.
  • Use Cow<'_, str> when a function sometimes borrows and sometimes allocates. See references/type-patterns.md for detailed Cow patterns.
  • Understand 'static. It means the data lives for the entire program (string literals, leaked allocations, const values). Trait bounds like T: 'static mean the type contains no non-static references — it can still be a heap-allocated owned type.

Module Organization

Single Crate Layout

code
my_project/
├── Cargo.toml
└── src/
    ├── main.rs          # binary entry point
    ├── lib.rs           # library root (optional, for testable public API)
    ├── config.rs
    ├── models/
    │   ├── mod.rs
    │   └── user.rs
    └── services/
        ├── mod.rs
        └── auth.rs

Expose a public API from lib.rs and keep main.rs thin — call into the library.

Workspace Layout

For multi-crate projects:

code
my_project/
├── Cargo.toml           # [workspace] definition
├── Cargo.lock
└── crates/
    ├── core/            # domain logic
    ├── cli/             # CLI binary
    └── api/             # HTTP API binary

Define shared dependencies in [workspace.dependencies] and reference them with dep.workspace = true in member crates.

Visibility

  • Default: private to the module.
  • pub: public to all.
  • pub(crate): visible within the crate only.
  • pub(super): visible to the parent module.

Prefer pub(crate) for internal APIs that need cross-module access but should not be part of the public API.

Preludes

For crates with many commonly-used types, provide a prelude module:

rust
// src/prelude.rs
pub use crate::config::Config;
pub use crate::error::{AppError, Result};
pub use crate::models::User;

Consumers import with use my_crate::prelude::*;.

Testing

Unit Tests

Keep unit tests in the same file as the code under test:

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

    #[test]
    fn parses_valid_input() {
        let result = parse("valid input");
        assert_eq!(result, Ok(Expected));
    }
}

#[cfg(test)] compiles only under cargo test. Tests within the module can access private items.

Integration Tests

Place integration tests in a top-level tests/ directory. Each file compiles as a separate crate and can only use the public API:

code
tests/
├── api_tests.rs
└── common/
    └── mod.rs          # shared test helpers

Testing Async Code

rust
#[tokio::test]
async fn fetches_data() {
    let result = fetch_data("test-key").await;
    assert!(result.is_ok());
}

Property Testing

Use proptest for input-space exploration:

rust
use proptest::prelude::*;

proptest! {
    #[test]
    fn roundtrip_serialization(input in ".*") {
        let encoded = encode(&input);
        let decoded = decode(&encoded).unwrap();
        assert_eq!(input, decoded);
    }
}

Test Runner

Consider cargo-nextest for faster parallel execution and cleaner output: cargo nextest run.

Recent Stable Features

Key features stabilized in the 1.75–1.85 range:

VersionFeature
1.75async fn in traits, RPITIT
1.77C-string literals, async recursive calls
1.79Inline const blocks, bounds on associated types in bounds
1.80LazyCell / LazyLock (replaces lazy_static / once_cell), exclusive range patterns
1.81#[expect] lint level, Error trait in core
1.82Apple ARM tier 1, inline asm const
1.84Cargo MSRV-aware resolver, strict pointer provenance
1.852024 edition, async closures, #[diagnostic::do_not_recommend]

Prefer LazyLock over the lazy_static or once_cell crates. Prefer #[expect(lint)] over #[allow(lint)] — the former warns when the suppression becomes unnecessary.

Additional Resources

  • references/type-patterns.md — Advanced type design: generic newtypes, typestate with enums, sealed trait implementations, builder validation, Cow patterns, and zero-cost abstraction techniques.
  • references/api-reference.md — Quick reference for recently stabilized std library APIs, common crate APIs (tokio, serde, thiserror, anyhow), and 2024 edition patterns with code examples.