Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Learning Path and Next Steps
学习路线与下一步

What you’ll learn: A structured roadmap covering weeks 1-2 through month 3+, recommended books and resources, common pitfalls for C# developers, and a practical comparison of tracing with ILogger and Serilog.
本章将学习: 一条从第 1 到 2 周延伸到第 3 个月之后的学习路线、推荐书籍与资源、C# 开发者常见误区,以及 tracingILogger、Serilog 的实践对照。

Difficulty: 🟢 Beginner
难度: 🟢 入门

Immediate Next Steps (Week 1-2)
近期安排(第 1 到 2 周)

  1. Set up your environment
    先把开发环境搭起来。

    • Install Rust via rustup.rs
      通过 rustup.rs 安装 Rust。
    • Configure VS Code with the rust-analyzer extension
      在 VS Code 里配置 rust-analyzer 扩展。
    • Create the first cargo new hello_world project
      创建第一个 cargo new hello_world 项目。
  2. Master the basics
    把基础动作练熟。

    • Practice ownership through small exercises
      通过小练习熟悉所有权。
    • Write functions with &strString&mut 这些不同参数形式
      多写几组分别接收 &strString&mut 的函数。
    • Implement basic structs and methods
      实现基础结构体和方法。
  3. Error handling practice
    开始练错误处理。

    • Convert C# try-catch code into Result-based Rust patterns
      把 C# 的 try-catch 代码改写成 Rust 的 Result 模式。
    • Practice ? and match repeatedly
      反复练习 ? 运算符和 match
    • Implement custom error types
      动手写几个自定义错误类型。

Intermediate Goals (Month 1-2)
中期目标(第 1 到 2 个月)

  1. Collections and iterators
    集合与迭代器。

    • Master Vec<T>HashMap<K,V>HashSet<T>
      Vec<T>HashMap<K,V>HashSet<T> 用顺手。
    • Learn mapfiltercollectfold 这些常用迭代器方法
      掌握 mapfiltercollectfold 这些核心迭代器方法。
    • Compare for loops with iterator chains in real examples
      在真实例子里体会 for 循环和迭代器链的差异。
  2. Traits and generics
    Trait 与泛型。

    • Implement common traits such as DebugClonePartialEq
      实现或派生 DebugClonePartialEq 这类常见 trait。
    • Write generic functions and structs
      编写泛型函数和泛型结构体。
    • Understand trait bounds and where clauses
      吃透 trait bound 和 where 子句。
  3. Project structure
    项目结构。

    • Organize code into modules
      学会把代码拆进模块。
    • Understand pub visibility
      理解 pub 可见性。
    • Work with external crates from crates.io
      开始使用 crates.io 上的第三方 crate。

Advanced Topics (Month 3+)
高级主题(第 3 个月之后)

  1. Concurrency
    并发。

    • Learn Send and Sync thoroughly
      SendSync 真正弄明白。
    • Use std::thread for basic parallelism
      std::thread 练基础并行。
    • Explore async programming with tokio
      进一步研究基于 tokio 的异步编程。
  2. Memory management
    内存管理。

    • Understand Rc<T> and Arc<T> for shared ownership
      理解 Rc<T>Arc<T> 在共享所有权中的定位。
    • Learn when Box<T> is appropriate for heap allocation
      掌握什么时候该用 Box<T> 做堆分配。
    • Master lifetimes in more complex scenarios
      把生命周期推进到更复杂的场景里练熟。
  3. Real-world projects
    真实项目。

    • Build a CLI tool with clap
      clap 做一个命令行工具。
    • Create a web API with axum or warp
      axumwarp 写一个 Web API。
    • Publish a reusable library to crates.io
      写一个库并发布到 crates.io。

Books
书籍

  • “The Rust Programming Language” — the official free book.
    《The Rust Programming Language》:官方免费教材,最适合作为主线参考。
  • “Rust by Example” — hands-on examples that are easy to follow.
    《Rust by Example》:例子驱动,适合边学边敲。
  • “Programming Rust” by Jim Blandy — deeper technical coverage.
    《Programming Rust》:技术深度更强,适合进阶阶段系统补课。

Online Resources
在线资源

Practice Projects
练手项目

  1. Command-line calculator — practice enums and pattern matching.
    命令行计算器:练枚举和模式匹配。
  2. File organizer — work with filesystem APIs and error handling.
    文件整理器:练文件系统操作和错误处理。
  3. JSON processor — learn serde and data transformation.
    JSON 处理工具:练 serde 和数据转换。
  4. HTTP server — understand networking and async basics.
    HTTP 服务器:练网络编程和异步基础。
  5. Database library — explore traits, generics, and error modeling.
    数据库访问库:练 trait、泛型和错误建模。

Common Pitfalls for C# Developers
C# 开发者常见误区

Ownership Confusion
对所有权概念发懵

#![allow(unused)]
fn main() {
// DON'T: Trying to use moved values
fn wrong_way() {
    let s = String::from("hello");
    takes_ownership(s);
    // println!("{}", s); // ERROR: s was moved
}

// DO: Use references or clone when needed
fn right_way() {
    let s = String::from("hello");
    borrows_string(&s);
    println!("{}", s); // OK: s is still owned here
}

fn takes_ownership(s: String) { /* s is moved here */ }
fn borrows_string(s: &str) { /* s is borrowed here */ }
}

The key point is that C# developers are used to references moving freely in a GC world, while Rust makes a sharp distinction between borrowing and moving ownership. Once that boundary becomes clear, many diagnostics stop feeling mysterious.
这里的关键在于:C# 开发者更熟悉 GC 体系下的引用传递,而 Rust 会把“借用”和“所有权转移”分得非常明确。只要把这条线看清,大量错误都会变得可以预期。

Fighting the Borrow Checker
总想和借用检查器对着干

#![allow(unused)]
fn main() {
// DON'T: Multiple mutable references
fn wrong_borrowing() {
    let mut v = vec![1, 2, 3];
    let r1 = &mut v;
    // let r2 = &mut v; // ERROR: cannot borrow as mutable more than once
}

// DO: Limit scope of mutable borrows
fn right_borrowing() {
    let mut v = vec![1, 2, 3];
    {
        let r1 = &mut v;
        r1.push(4);
    } // r1 goes out of scope here

    let r2 = &mut v; // OK: no other mutable borrows exist
    r2.push(5);
}
}

Mutability in Rust is intentionally narrow in scope. Once the mutable borrow ends, the next one becomes valid. This explicit scoping is what keeps aliasing and mutation from colliding.
Rust 对可变借用的作用域卡得很细。前一个可变借用结束后,后一个才会成立。正是这种显式边界,把“别名”和“修改”冲突拦在了编译期。

Expecting Null Values
下意识等着 null 出现

#![allow(unused)]
fn main() {
// DON'T: Expecting null-like behavior
fn no_null_in_rust() {
    // let s: String = null; // NO null in Rust!
}

// DO: Use Option<T> explicitly
fn use_option_instead() {
    let maybe_string: Option<String> = None;

    match maybe_string {
        Some(s) => println!("Got string: {}", s),
        None => println!("No string available"),
    }
}
}

Rust chooses explicit absence over ambient null. That design removes a huge class of late crashes at the type level.
Rust 选择用显式的“可能为空”表达,而不是让 null 到处游走。这个设计直接把一大类延迟到运行时的空值崩溃前移到了类型系统里。

Final Tips
最后几条建议

  1. Embrace the compiler — compiler diagnostics are part of the learning process.
    接受编译器。 编译器提示本身就是学习材料。
  2. Start small — begin with simple programs and increase complexity gradually.
    从小程序开始。 先解决清楚简单问题,再一点点增加复杂度。
  3. Read other people’s code — popular crates are excellent study material.
    多读别人的代码。 优秀 crate 的源码就是高质量教材。
  4. Ask for help — the Rust community is generally welcoming and practical.
    遇到问题就查资料、看讨论。 Rust 社区整体上比较务实,也乐于解答具体问题。
  5. Practice regularly — concepts that feel awkward early on become natural through repetition.
    规律练习。 前期觉得拧巴的概念,通常都是练熟以后才顺。

Rust has a learning curve, but the reward is substantial: memory safety, predictable performance, and concurrency without fear. The ownership system that initially feels restrictive often becomes the very thing that keeps large codebases reliable.
Rust 的学习曲线确实存在,但回报也很扎实:内存安全、可预测性能,以及更可靠的并发模型。刚开始显得很“紧”的所有权系统,往往正是后期保持大型代码稳定的关键。


Congratulations! This foundation is enough to start the transition from C# to Rust. From this point forward, steady practice through small projects, source reading, and careful attention to compiler diagnostics will deepen understanding step by step.
恭喜。 这部分内容已经足够支撑从 C# 往 Rust 迁移的起步阶段。接下来只要持续做小项目、持续读源码、持续把错误信息看懂,理解就会稳步加深。

Structured Observability: tracing vs ILogger and Serilog
结构化可观测性:tracingILogger、Serilog 的对照

C# developers are used to structured logging through ILogger、Serilog、NLog 这类工具,日志消息里通常会带有类型化的键值字段。Rust 的 log crate 只能提供基础的分级日志,而 tracing 才是生产场景下更完整的结构化可观测性方案,它支持 span、异步上下文,以及分布式追踪。
C# 开发者通常早就习惯了 ILogger、Serilog、NLog 这类结构化日志工具,日志记录里会天然带有键值字段。Rust 里的 log crate 只覆盖基础分级日志,真正适合生产环境的结构化可观测性方案通常是 tracing,因为它支持 span、异步上下文与分布式追踪。

Why tracing Over log
为什么更推荐 tracing 而不是 log

Feature
特性
log cratetracing crateC# Equivalent
C# 对应概念
Leveled messages
分级日志
info!()error!()info!()error!()ILogger.LogInformation()
Structured fields
结构化字段
❌ String interpolation only
基本只能拼字符串
✅ Typed key-value fields
类型化键值字段
Serilog Log.Information("{User}", user)
Spans (scoped context)
Span 与作用域上下文
#[instrument]span!()ILogger.BeginScope()
Async-aware
感知异步上下文
❌ Loses context across .await
.await 容易丢上下文
✅ Spans follow across .await
Span 会跟着异步流程走
Activity / DiagnosticSource
Distributed tracing
分布式追踪
✅ OpenTelemetry integration
可接 OpenTelemetry
System.Diagnostics.Activity
Multiple output formats
多种输出格式
Basic
比较基础
JSON、pretty、compact、OTLPSerilog sinks

Getting Started
起步依赖

# Cargo.toml
[dependencies]
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }

Basic Usage: Structured Logging
基础用法:结构化日志

// C# Serilog
Log.Information("Processing order {OrderId} for {Customer}, total {Total:C}",
    orderId, customer.Name, order.Total);
// Output: Processing order 12345 for Alice, total $99.95
// JSON:  {"OrderId": 12345, "Customer": "Alice", "Total": 99.95, ...}
#![allow(unused)]
fn main() {
use tracing::{info, warn, error, debug, instrument};

// Structured fields — typed, not string-interpolated
info!(order_id = 12345, customer = "Alice", total = 99.95,
      "Processing order");
// Output: INFO Processing order order_id=12345 customer="Alice" total=99.95
// JSON:  {"order_id": 12345, "customer": "Alice", "total": 99.95, ...}

// Dynamic values
let order_id = 12345;
info!(order_id, "Order received");  // field name = variable name shorthand

// Conditional fields
if let Some(promo) = promo_code {
    info!(order_id, promo_code = %promo, "Promo applied");
    //                        ^ % means use Display formatting
    //                        ? would use Debug formatting
}
}

The important shift is that fields are first-class structured data rather than formatted text fragments. That makes downstream search, filtering, and aggregation much stronger.
这里最重要的变化在于:字段不再只是拼进日志字符串里的文字,而是独立存在的结构化数据。这样一来,后续检索、过滤、聚合都会更强。

Spans: The Killer Feature for Async Code
Span:异步代码里最关键的能力

Spans are scoped contexts that keep fields alive across function calls and .await points. It is similar in spirit to ILogger.BeginScope(),但它对异步场景更自然。
Span 是带作用域的上下文,它能让字段跨函数调用和 .await 保持存在。这个概念和 ILogger.BeginScope() 很接近,但在异步场景里更自然。

// C# — Activity / BeginScope
using var activity = new Activity("ProcessOrder").Start();
activity.SetTag("order_id", orderId);

using (_logger.BeginScope(new Dictionary<string, object> { ["OrderId"] = orderId }))
{
    _logger.LogInformation("Starting processing");
    await ProcessPaymentAsync();
    _logger.LogInformation("Payment complete");  // OrderId still in scope
}
#![allow(unused)]
fn main() {
use tracing::{info, instrument, Instrument};

// #[instrument] automatically creates a span with function args as fields
#[instrument(skip(db), fields(customer_name))]
async fn process_order(order_id: u64, db: &Database) -> Result<(), AppError> {
    let order = db.get_order(order_id).await?;

    // Add a field to the current span dynamically
    tracing::Span::current().record("customer_name", &order.customer_name.as_str());

    info!("Starting processing");
    process_payment(&order).await?;        // span context preserved across .await!
    info!(items = order.items.len(), "Payment complete");
    Ok(())
}
// Every log message inside this function automatically includes:
//   order_id=12345 customer_name="Alice"
// Even in nested async calls!

// Manual span creation (like BeginScope)
async fn batch_process(orders: Vec<u64>, db: &Database) {
    for order_id in orders {
        let span = tracing::info_span!("process_order", order_id);

        // .instrument(span) attaches the span to the future
        process_order(order_id, db)
            .instrument(span)
            .await
            .unwrap_or_else(|e| error!("Failed: {e}"));
    }
}
}

Once spans are in place, logs stop being isolated lines and become connected traces of work. That is especially valuable in async services where execution jumps across .await boundaries.
一旦把 span 用起来,日志就不再是孤零零的一行一行,而是可以串成完整工作过程的上下文轨迹。这在跨越多个 .await 的异步服务里尤其有价值。

Subscriber Configuration (Like Serilog Sinks)
Subscriber 配置(相当于 Serilog 的 sink 配置)

#![allow(unused)]
fn main() {
use tracing_subscriber::{fmt, EnvFilter, layer::SubscriberExt, util::SubscriberInitExt};

fn init_tracing() {
    // Development: human-readable, colored output
    tracing_subscriber::registry()
        .with(EnvFilter::try_from_default_env()
            .unwrap_or_else(|_| "my_app=debug,tower_http=info".into()))
        .with(fmt::layer().pretty())  // Colored, indented spans
        .init();
}

fn init_tracing_production() {
    // Production: JSON output for log aggregation (like Serilog JSON sink)
    tracing_subscriber::registry()
        .with(EnvFilter::new("my_app=info"))
        .with(fmt::layer().json())  // Structured JSON
        .init();
    // Output: {"timestamp":"...","level":"INFO","fields":{"order_id":123},...}
}
}
# Control log levels via environment variable (like Serilog MinimumLevel)
RUST_LOG=my_app=debug,hyper=warn cargo run
RUST_LOG=trace cargo run  # everything

Development usually prefers readable pretty output, while production systems often prefer JSON for centralized collection. EnvFilter then acts much like runtime log-level configuration in other ecosystems.
开发阶段通常更适合可读性高的 pretty 输出,生产环境则往往更偏向 JSON,方便集中采集。EnvFilter 的角色则很像其他生态里的运行时日志级别配置。

Serilog → tracing Migration Cheat Sheet
Serilog → tracing 迁移速查

Serilog / ILoggertracingNotes
说明
Log.Information("{Key}", val)info!(key = val, "message")Fields are typed, not interpolated
字段是类型化数据,不只是字符串插值
Log.ForContext("Key", val)span.record("key", val)Add fields to current span
向当前 span 追加字段
using BeginScope(...)#[instrument] or info_span!()Automatic with #[instrument]
#[instrument] 可自动生成
.WriteTo.Console()fmt::layer()Human-readable
可读性高
.WriteTo.Seq() / .File()fmt::layer().json() + file redirectOr use tracing-appender
也可结合 tracing-appender
.Enrich.WithProperty()span!(Level::INFO, "name", key = val)Span fields
通过 span 字段补充上下文
LogEventLevel.Debugtracing::Level::DEBUGSame concept
概念一致
{@Object} destructuringfield = ?value or %value? uses Debug% uses Display
? 使用 Debug% 使用 Display

OpenTelemetry Integration
OpenTelemetry 集成

# For distributed tracing (like System.Diagnostics + OTLP exporter)
[dependencies]
tracing-opentelemetry = "0.22"
opentelemetry = "0.21"
opentelemetry-otlp = "0.14"
#![allow(unused)]
fn main() {
// Add OpenTelemetry layer alongside console output
use tracing_opentelemetry::OpenTelemetryLayer;

fn init_otel() {
    let tracer = opentelemetry_otlp::new_pipeline()
        .tracing()
        .with_exporter(opentelemetry_otlp::new_exporter().tonic())
        .install_batch(opentelemetry_sdk::runtime::Tokio)
        .expect("Failed to create OTLP tracer");

    tracing_subscriber::registry()
        .with(OpenTelemetryLayer::new(tracer))  // Send spans to Jaeger/Tempo
        .with(fmt::layer())                      // Also print to console
        .init();
}
// Now #[instrument] spans automatically become distributed traces!
}

With this layer in place, spans from tracing can flow into Jaeger、Tempo、OpenTelemetry collectors 这类系统。对于需要跨服务排查问题的后端系统,这一步的价值非常高。
接入这一层之后,tracing 里的 span 就能被送到 Jaeger、Tempo、OpenTelemetry Collector 等系统。对于需要跨服务定位问题的后端系统,这一步非常重要。