介绍

这是一本面向完全未接触过 Rust 的 C# 和 .NET 开发者的(不完全)指南。Rust 和 C# 中的一些概念和形式都可以很好地转换,仅仅是表达方式不同,也有一些是完全不同的,例如内存管理。本书通过一些简明的样例来简单对照和比较这些形式和概念。

本书的原作者1都是全新接触 Rust 的 C#/.NET 开发者。这本指南是这些作者在几个月的课程中编写 Rust 代码时的知识汇集,也正是作者们开始他们的 Rust 之旅时最期望看到的指南。也就是说,作者们同样鼓励你通过更多书籍和网络材料来深入 Rust,而不是仅仅透过 C# 和 .NET 观察。同时,本指南可以快速地解决一些疑问,例如:Rust是否支持继承,多线程,异步编程等?

本书假定:

  • 读者是一名经验丰富的 C#/.NET 开发者。
  • 读者对 Rust 是完全陌生的 。

本书目标:

  • 简要的对比 C#/.NET 的若干话题和它们对应的 Rust 版本。
  • 提供深入探究这些话题的 Rust 参考、书籍、文章链接2

非本书目标:

  • 对设计模式和架构的讨论
  • Rust 语言教程
  • 读者能在阅读后精通 Rust
  • 尽管有许多示例用于对比 C# 和 Rust,请注意本书并非是它们两者的编码用例指南。

1

本指南的原作者是(字母序): Atif Aziz, Bastian Burger, Daniele Antonio Maggio, Dariusz Parys, Patrick Schuler.

3

译者注①:此译本的贡献者为:artiga033

2

译者注②:为本地化考虑,此译本替换了部分链接为对应的中文版本,具体参见本地化外部引用索引

许可

Original Copy:

Copyright © Microsoft Corporation.
Portions Copyright © 2010 The Rust Project Developers

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE

This translation:

Copyright © zh-CN translators

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE

贡献指南

译者注:本节与原文不同,仅特定于此翻译版本。

如果您有关于原书内容的任何补充和见解,请提交至英文原版

欢迎参与翻译或修订/更新内容,请及时发 Issue 或 Pull Request 以便其他人知道您的工作,防止重复劳动。

本地化外部引用索引

英文原版中引用的外部文档有相当一部分已有社区或官方的中文翻译,为了方便中文读者,我们将一部分有中文对应版本的链接替换为了对应的中文版本,具体参见下表:

开始

Rust Playground

使用 Rust Playground 是最简单的一种不需要 本地安装就能体验 Rust 的方法。它是一个极简的开发前端,运行在浏览器中,并且可以编写和运行 Rust 代码。

Dev Container

Rust Playground 的执行环境有一些限制,例如总编译/执行时长,内存和网络等,还有另一个不需要安装 Rust 的方法是使用 dev container,例如 https://github.com/microsoft/vscode-remote-try-rust 这个仓库中提供的。就像 Rust Playground, dev container 可以直接在浏览器中运行,可以使用Github Codespaces使用本地的 Visual Studio Code

本地安装

要获取完整的 Rust 编译器和开发工具,参见 Rust 程序设计语言 一书中入门指南一章的安装小节,或是rust-lang.org上的安装页面

语言

本节对比 C# 和 Rust 的语言特性。

标量类型

下表展示了 Rust 中的原生类型(primitive types)和它们在 C# 和 .NET 中的等价物:

RustC#.NET备注
boolboolBoolean
charcharChar参见备注 1.
i8sbyteSByte
i16shortInt16
i32intInt32
i64longInt64
i128Int128
isizenintIntPtr
u8byteByte
u16ushortUInt16
u32uintUInt32
u64ulongUInt64
u128UInt128
usizenuintUIntPtr
f32floatSingle
f64doubleDouble
decimalDecimal
()voidVoidValueTuple参见备注 2 & 3.
objectObject参见备注 3.

备注:

  1. Rust中的 char 和 .NET 中的 Char 的定义不同。在Rust中,char 是一个四字节宽的Unicode 标量值,但是在.NET中,Char 是两字节宽的,它保存的该字符的UTF-16编码。更多信息参见Rust char 文档
  2. 尽管 unit 类型()(一个空元组)在Rust中是 可表达值(expressible value),C# 中最接近它的是表示“无”的 void。然而,仅当使用指针和 unsafe 代码时,void 才是 可表达值。.NET 中的 ValueTuple 是一个空元组,但是C#并没有像是 () 这种表示它的字面量语法。C# 中可以使用 ValueTuple,但这是很罕见的做法。不过,不同于 C# ,F# 拥有像 Rust 一样的 unit 类型
  3. 尽管 voidobject 不是标量类型(甚至在 .NET 类型系统中 int 这种标量也是 object 的子类),方便起见它们也被列入上表中。

另见:

字符串

Rust中有两种字符串类型:Stringstr,前者分配在堆上,后者则是到一个 String&str 的切片。

这些类型到 .NET 的映射关系如下:

Rust.NET备注
&mut strSpan<char>
&strReadOnlySpan<char>
Box<str>String参见备注 1.
StringString
String (mutable)StringBuilder参见备注 1.

在 Rust 和 .NET 中使用字符串的方式有所不同,不过可以从上面列出的大致的等价关系开始。其中一点是 Rust 字符串是 UTF-8 编码的,而 .NET 的则是 UTF-16 编码。此外,.NET 字符串是不可变的,但 Rust 字符串可以被声明为可变,例如 let s = &mut String::from("hello");

还有一些区别是由于所有权概念导致的。要了解 String 类型的所有权机制,参见 Rust Book.

备注:

  1. Rust 中的 Box<str> 类型和 .NET 的 String 类型是等价的。Rust中的 Box<str>String 类型的区别是前者保存指针和长度,而后者保存指针、长度、还有容量,使得 String 长度可以增长。如果 Rust 的 String 声明为可变的,那么它与 .NET 中的 StringBuilder 类型很相似。

C#:

ReadOnlySpan<char> span = "Hello, World!";
string str = "Hello, World!";
StringBuilder sb = new StringBuilder("Hello, World!");

Rust:

let span: &str = "Hello, World!";
let str = Box::new("Hello World!");
let mut sb = String::from("Hello World!");

字符串字面量

.NET 中的字符串字面量是分配在堆上的不可变的 String 类型。Rust 中则是 &'static str,它是不可变的,具有全局生命周期且并非分配在堆上,而是直接嵌入编译后的二进制文件。

C#

string str = "Hello, World!";

Rust

let str: &'static str = "Hello, World!";

C# 的逐字字符串字面量和 Rust 的原始字符串字面量等价。

string str = @"Hello, \World/!";

Rust

let str = r#"Hello, \World/!"#;

C# 的 UTF-8 字符串字面量和 Rust 的字节字符串字面量等价。

C#

string str = "hello"u8;

Rust

let str = b"hello";

字符串内插值

C# 有内置的字符串内插功能,允许你在字符串字面量内嵌入表达式。下面是 C# 字符串内插的示例。

string name = "John";
int age = 42;
string str = $"Person {{ Name: {name}, Age: {age} }}";

Rust 没有内置的字符串内插功能。作为替代,format! 宏被用来格式化字符串。下面是 Rust 字符串内插的示例。

let name = "John";
let age = 42;
let str = format!("Person {{ name: {name}, age: {age} }}");

在 C# 中,自定义类和结构体也可以被内插,因为所有类型都有继承自 objectToString()方法。

class Person
{
    public string Name { get; set; }
    public int Age { get; set; }

    public override string ToString() =>
        $"Person {{ Name: {Name}, Age: {Age} }}";
}

var person = new Person { Name = "John", Age = 42 };
Console.Writeline(person);

在 Rust 中,并非每一个类型都有默认的格式化实现/继承。相反,每一个需要转换到字符串的类型都必须实现 std::fmt::Display trait 。

use std::fmt::*;

struct Person {
    name: String,
    age: i32,
}

impl Display for Person {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
        write!(f, "Person {{ name: {}, age: {} }}", self.name, self.age)
    }
}

let person = Person {
    name: "John".to_owned(),
    age: 42,
};

println!("{person}");

另一种选择是使用 std::fmt::Debug trait,Debug trait 为所有标准类型实现,并且可以用于打印类型的内部表示。下面的例子展示了如何使用 derive 属性来使用 Debug trait 打印一个自定义结构体的内部表示。这种声明方式为 Person 结构体自动实现了 Debug trait:

#[derive(Debug)]
struct Person {
    name: String,
    age: i32,
}

let person = Person {
    name: "John".to_owned(),
    age: 42,
};

println!("{person:?}");

备注: 使用 :? 格式化说明符会使用 Debug trait 来打印结构体, 省略该符号则会使用 Display trait。

另见:

复合类型

.NET 中常用的对象和集合类型以及它们到 Rust 的映射

C#Rust
ArrayArray
ListVec
TupleTuple
DictionaryHashMap

数组

在 Rust 和 .NET 中,定长数组以同样的方式支持。

C#:

int[] someArray = new int[2] { 1, 2 };

Rust:

let someArray: [i32; 2] = [1,2];

列表

Rust 中 List<T> 的等价物是 Vec<T>。数组可以转换成 Vec,反之亦然。

C#:

var something = new List<string>
{
    "a",
    "b"
};

something.Add("c");

Rust:

let mut something = vec![
    "a".to_owned(),
    "b".to_owned()
];

something.push("c".to_owned());

元组

C#:

var something = (1, 2)
Console.WriteLine($"a = {something.Item1} b = {something.Item2}");

Rust:

let something = (1, 2);
println!("a = {} b = {}", something.0, something.1);

// deconstruction supported
let (a, b) = something;
println!("a = {} b = {}", a, b);

备注:Rust 元组的元素不像 C# 那般是可命名的。 唯一访问元组元素的办法就是使用该元素的索引,要么就解构元组。

字典

Rust 中 Dictionary<TKey,TValue> 的等价物是 Hashmap<K,V>

C#:

var something = new Dictionary<string, string>
{
    { "Foo", "Bar" },
    { "Baz", "Qux" }
};

something.Add("hi", "there");

Rust:

let mut something = HashMap::from([
    ("Foo".to_owned(), "Bar".to_owned()),
    ("Baz".to_owned(), "Qux".to_owned())
]);

something.insert("hi".to_owned(), "there".to_owned());

另见:

自定义类型

这些章节讨论开发自定义类型相关的主题。

类(Class)

Rust 没有类。 它只有 结构体(struct

记录(Record)

Rust 没有任何构造可以创建记录, 既没有像 C# 中的 record struct 也没有 record calss

结构(Struct)

Rust 和 C# 中的结构体有一些相同点:

  • 它们都通过 struct 关键字定义,但是 Rust 中,struct 只能定义数据/字段。函数和方法所表示的行为都单独定义在一个 impl块 中。

  • Rust 中结构可以实现多个 trait,就像 C# 中的可以实现多个接口。

  • 它们无法派生类。

  • 它们默认分配在栈上,除非:

    • .NET 中,装箱或转换为接口类型。
    • Rust 中,包裹在 BoxRc/Arc 等智能指针内。

在 C# 中,struct 是建立 .NET 值类型,的办法,它们通常是一些领域特定的原始值,或是需要值等价性语义的复合类型。在 Rust 中,struct 是主要的建立数据类型的方式。(另一个是 enum)。

C# 中的 struct (或 record struct)默认拥有按值复制和值等价性语义。但在 Rust 中,这需要额外的一步,使用 #derive 属性 并列出要实现的 trait。

#[derive(Clone, Copy, PartialEq, Eq, Hash)]
struct Point {
    x: i32,
    y: i32,
}

C#/.NET 中的值类型通常被开发者设计为不可变的。这被认为是符合语义的最佳实践,不过语言本身并不阻止设计一个具有破坏性和原地修改行为的 struct。Rust 中也一样。类型必须被开发者刻意地设计为可变的。

由于 Rust 没有类,因此也没有通过派生类实现的类型架构,共享行为通过 trait,泛型,以及通过使用 trait objects 动态分发实现的多态。

假设下面这个 C# struct 表示一个矩形:

struct Rectangle
{
    public Rectangle(int x1, int y1, int x2, int y2) =>
        (X1, Y1, X2, Y2) = (x1, y1, x2, y2);

    public int X1 { get; }
    public int Y1 { get; }
    public int X2 { get; }
    public int Y2 { get; }

    public int Length => Y2 - Y1;
    public int Width => X2 - X1;

    public (int, int) TopLeft => (X1, Y1);
    public (int, int) BottomRight => (X2, Y2);

    public int Area => Length * Width;
    public bool IsSquare => Width == Length;

    public override string ToString() => $"({X1}, {Y1}), ({X2}, {Y2})";
}

Rust 的等价实现:

#![allow(dead_code)]

struct Rectangle {
    x1: i32, y1: i32,
    x2: i32, y2: i32,
}

impl Rectangle {
    pub fn new(x1: i32, y1: i32, x2: i32, y2: i32) -> Self {
        Self { x1, y1, x2, y2 }
    }

    pub fn x1(&self) -> i32 { self.x1 }
    pub fn y1(&self) -> i32 { self.y1 }
    pub fn x2(&self) -> i32 { self.x2 }
    pub fn y2(&self) -> i32 { self.y2 }

    pub fn length(&self) -> i32 {
        self.y2 - self.y1
    }

    pub fn width(&self)  -> i32 {
        self.x2 - self.x1
    }

    pub fn top_left(&self) -> (i32, i32) {
        (self.x1, self.y1)
    }

    pub fn bottom_right(&self) -> (i32, i32) {
        (self.x2, self.y2)
    }

    pub fn area(&self)  -> i32 {
        self.length() * self.width()
    }

    pub fn is_square(&self)  -> bool {
        self.width() == self.length()
    }
}

use std::fmt::*;

impl Display for Rectangle {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
        write!(f, "({}, {}), ({}, {})", self.x1, self.y2, self.x2, self.y2)
    }
}

需要注意 C# 的 struct 继承了来自 ObjectToString 方法,因此,它可以通过 重写 基类实现来提供自定义的字符串表示。由于 Rust 中没有继承,类型对一些 格式化 表示的支持是通过实现 Display trait。这样该结构的实例就可以参与格式化,例如被 println! 打印。如下:

fn main() {
    let rect = Rectangle::new(12, 34, 56, 78);
    println!("Rectangle = {rect}");
}

接口(Interfaces)

Rust 没有 C#/.NET 中的接口。作为替代,它使用 trait。和接口类似,一个 trait 表示一种抽象,同时形成一种约束:当类型实现 trait 时,必须实现它的所有成员。

就像 C#/.NET 接口可以有默认方法(默认实现的方法体作为方法定义的一部分),Rust 中的 trait 也一样。实现接口/trait 的类型也可以随后提供一个更合适的或优化过的实现。

C#/.NET 的接口可以拥有所有类型的成员,属性、索引器、事件、方法等,既可以是静态的也可以是实例的。类似地,Rust trait 可以有(实例)方法、关联函数(当作是 C#/.NET 中的静态方法)、常量。

除了类架构,接口是通过动态分发实现多态的核心概念,这创造了横向的抽象。通过接口,可以编写针对接口的通用代码,而无需关心实现它的具体类型。Rust 的 trait 也可以有限地取得同样的效果。trait 对象实际上是一张 v-table (虚表),它通过 dyn 关键字后跟上 trait 名来标识,例如 dyn Shape(其中 Shape 是trait名)。trait 对象总是隐藏在指针之后,通过引用(例如 &dyn Shape)或堆上的 Box(例如 Box<dyn Shape>。这与 .NET 有些类似,接口是引用类型,因此值类型转换成接口会被自动装箱到托管堆上。前文提到过 trait 对有些限制,那就是原始的实现类型无法被恢复。也就是说,虽然转换或者测试一个接口是否是某个实例是非常常见的场景,但在 Rust 中是不可能的(如果没有一些额外的工作和支持)。

枚举(Enum)

C# 中,枚举是一种值类型,它将符号映射到整数值。

enum DayOfWeek
{
    Sunday = 0,
    Monday = 1,
    Tuesday = 2,
    Wednesday = 3,
    Thursday = 4,
    Friday = 5,
    Saturday = 6,
}

Rust 有着 完全一致 语法做同样的事:

enum DayOfWeek
{
    Sunday = 0,
    Monday = 1,
    Tuesday = 2,
    Wednesday = 3,
    Thursday = 4,
    Friday = 5,
    Saturday = 6,
}

与 .NET 不同,Rust enum 实例没有任何预定义行为,甚至无法进行像 dow == DayOfWeek::Friday 这种简单的等价检验。为了赋予它一些和 C# enum 差不多的功能,需要使用 #derive属性 来自动实现一些常用功能:

#[derive(Debug,     // 为 "{:?}" 启用格式化
         Clone,     // Copy 需要
         Copy,      // 启用按值复制语义
         Hash,      // 为了在各种 map 中使用,启用哈希能力
         PartialEq  // 启用值等价性 (==)
)]
enum DayOfWeek
{
    Sunday = 0,
    Monday = 1,
    Tuesday = 2,
    Wednesday = 3,
    Thursday = 4,
    Friday = 5,
    Saturday = 6,
}

fn main() {
    let dow = DayOfWeek::Wednesday;
    println!("Day of week = {dow:?}");

    if dow == DayOfWeek::Friday {
        println!("Yay! It's the weekend!");
    }

    // 强制转换为整形
    let dow = dow as i32;
    println!("Day of week = {dow:?}");

    let dow = dow as DayOfWeek;
    println!("Day of week = {dow:?}");
}

上面的例子表明,一个 enum 类型可以被强制转换到为它分配的整数类型,不过不像 C#,反过来的转换时不可行的(尽管这在 C#/.NET 中也有负面影响,即 enum 实例可能持有的是不存在的值)。反之,开发者需要来提供这一帮助函数:

impl DayOfWeek {
    fn from_i32(n: i32) -> Result<DayOfWeek, i32> {
        use DayOfWeek::*;
        match n {
            0 => Ok(Sunday),
            1 => Ok(Monday),
            2 => Ok(Tuesday),
            3 => Ok(Wednesday),
            4 => Ok(Thursday),
            5 => Ok(Friday),
            6 => Ok(Saturday),
            _ => Err(n)
        }
    }
}

如果n是有效值,from_i32 函数返回一个包裹在表示成功(Ok)的 Result 中的 DayOfWeek,否则它将n包裹在表示失败(Err)的 Result 中原样返回。

let dow = DayOfWeek::from_i32(5);
println!("{dow:?}"); // prints: Ok(Friday)

let dow = DayOfWeek::from_i32(50);
println!("{dow:?}"); // prints: Err(50)

Rust 有一些 crate 可以帮助实现这种从整数类型的映射从而不必手动编码。

Rust 中的 enum 类型也是一种设计(可区分)联合类型的方式,它允许不同的 成员variants)持有特定于自己的数据。例:

enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(String),
}

let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));

这种 enum 声明形式不存在于 C# 中,不过可以用(类)记录来模拟:

var home = new IpAddr.V4(127, 0, 0, 1);
var loopback = new IpAddr.V6("::1");

abstract record IpAddr
{
    public sealed record V4(byte A, byte B, byte C, byte D): IpAddr;
    public sealed record V6(string Address): IpAddr;
}

二者不同之处在于 Rust 定义产生的是成员的 封闭类型(closed type),也就是说编译器知道 IpAddr 没有除了 IpAddr::V4IpAddr::V6之外的成员,并且可以借此执行更严格的检查,例如在 match 表达式(相当于 C# switch 表达式)中,Rust 编译器不会通过代码除非所有变体都已被覆盖。相比之下,C# 的模拟方案实际上创建了类架构(尽管表达很简单),并且由于 IpAddr 是一个 抽象基类,它能表示的所有类型集合对于编译器是未知的。

成员(Members)

构造函数

Rust 没有任何构造函数的表示法。相反,你可以之禅秀编写返回该类型实例的工厂函数。工厂函数可以是单独的也可以是该类型的 关联函数。用 C# 的说法,关联函数就像一个类型的静态方法。按照惯例,如果一个 struct 只有一个工厂函数,它应被命名为 new

struct Rectangle {
    x1: i32, y1: i32,
    x2: i32, y2: i32,
}

impl Rectangle {
    pub fn new(x1: i32, y1: i32, x2: i32, y2: i32) -> Self {
        Self { x1, y1, x2, y2 }
    }
}

由于 Rust 函数(不管是否关联)不支持重载,这些工厂函数必须被唯一地命名。例如,下面是 String 类型可用的所谓构造函数或工厂函数:

  • String::new:创建空字符串
  • String::with_capacity:创建带有指定初始缓冲区容量的字符串
  • String::from_utf8:从一串 UTF-8 字节编码字符串。
  • String::from_utf16:从一串 UTF-16 字节编码字符串。

对于 Rust enum 类型的情况,它的变体成员就是构造函数。参见 enum 一节

另见:

方法(静态的和实例的)

就像 C#,Rust 类型(enumstruct 都是)可以有静态和实例方法。在 Rust 语境中,方法 始终是实例上的,并且通过它的第一个参数是self来标识,self 参数没有类型标注,因为它始终是该方法所属的类型。静态方法则被称作 关联函数,下面的例子中,new 是一个关联函数,而剩下的(lengthwidtharea)则是该类型的方法:

struct Rectangle {
    x1: i32, y1: i32,
    x2: i32, y2: i32,
}

impl Rectangle {
    pub fn new(x1: i32, y1: i32, x2: i32, y2: i32) -> Self {
        Self { x1, y1, x2, y2 }
    }

    pub fn length(&self) -> i32 {
        self.y2 - self.y1
    }

    pub fn width(&self)  -> i32 {
        self.x2 - self.x1
    }

    pub fn area(&self)  -> i32 {
        self.length() * self.width()
    }
}

常量

像 C# 一样,Rust 类型可以包含常量。不过,更有意思的一点是 Rust 允许类型实例被定义为常量:

struct Point {
    x: i32,
    y: i32,
}

impl Point {
    const ZERO: Point = Point { x: 0, y: 0 };
}

C# 中,这需要静态只读字段来实现:

readonly record struct Point(int X, int Y)
{
    public static readonly Point Zero = new(0, 0);
}

事件

Rust 没有内置的支持来让类型成员广播和激发事件,不像 C#拥有 event 关键字。

属性1

C# 中,类型字段通常是私有的,它们被属性成员保护/封装着,通过访问器方法(getset)来读写字段。访问器方法可以包含额外逻辑,例如,设置值的时候进行验证或读取值的时候进行计算。Rust 只有方法,getter 用字段名命名(Rust 中方法和字段的标识符可以一样),setter 则前缀 set_

下面的例子演示了 Rust 中的类似属性的访问器方法:

struct Rectangle {
    x1: i32, y1: i32,
    x2: i32, y2: i32,
}

impl Rectangle {
    pub fn new(x1: i32, y1: i32, x2: i32, y2: i32) -> Self {
        Self { x1, y1, x2, y2 }
    }

    // 相当于属性 getter (都和对应的字段名一样)

    pub fn x1(&self) -> i32 { self.x1 }
    pub fn y1(&self) -> i32 { self.y1 }
    pub fn x2(&self) -> i32 { self.x2 }
    pub fn y2(&self) -> i32 { self.y2 }

    // 相当于属性 setter

    pub fn set_x1(&mut self, val: i32) { self.x1 = val }
    pub fn set_y1(&mut self, val: i32) { self.y1 = val }
    pub fn set_x2(&mut self, val: i32) { self.x2 = val }
    pub fn set_y2(&mut self, val: i32) { self.y2 = val }

    // 相当于计算属性

    pub fn length(&self) -> i32 {
        self.y2 - self.y1
    }

    pub fn width(&self)  -> i32 {
        self.x2 - self.x1
    }

    pub fn area(&self)  -> i32 {
        self.length() * self.width()
    }
}

扩展方法

C# 中的扩展方法允许开发者为现有类型附加新的静态绑定的方法,而不需要修改原有的类型定义。下面的 C# 示例中,通过 扩展,为 StringBuilder 类添加了一个新的 Wrap 方法:

using System;
using System.Text;
using Extensions; // (1)

var sb = new StringBuilder("Hello, World!");
sb.Wrap(">>> ", " <<<"); // (2)
Console.WriteLine(sb.ToString()); // Prints: >>> Hello, World! <<<

namespace Extensions
{
    static class StringBuilderExtensions
    {
        public static void Wrap(this StringBuilder sb,
                                string left, string right) =>
            sb.Insert(0, left).Append(right);
    }
}

注意为了使扩展方法可用(2),包含扩展方法的类型的命名空间必须先被引入(1)。Rust 通过 trait 提供了一种非常类似的方案,叫做 扩展 trait。下面的 Rust 例子和上面的 C# 例子等价,它为 String 扩展了方法 wrap

#![allow(dead_code)]

mod exts {
    pub trait StrWrapExt {
        fn wrap(&mut self, left: &str, right: &str);
    }

    impl StrWrapExt for String {
        fn wrap(&mut self, left: &str, right: &str) {
            self.insert_str(0, left);
            self.push_str(right);
        }
    }
}

fn main() {
    use exts::StrWrapExt as _; // (1)

    let mut s = String::from("Hello, World!");
    s.wrap(">>> ", " <<<"); // (2)
    println!("{s}"); // Prints: >>> Hello, World! <<<
}

就像 C# 中那样,为了使扩展 trait 的方法可用(2),扩展 trait 必须先被导入(1)。还需要注意,扩展 trait 标识符 StrWarpExt 自身可以通过 _ 被丢弃,这不会影响 wrapString 的可用性。

可见性/访问修饰符

C# 有许多可访问性,亦称可见性,修饰符:

  • private
  • protected
  • internal
  • protected internal (family)
  • public

Rust 中,编译过程就是构建一棵由模块组成的树,每个模块都包含并定义了若干程序项(items),比如类型、trait、枚举、常量和函数。几乎一切都是默认私有的。例外之一是,比如公共 trait 中的关联函数是默认公开的。类似的是 C# 的接口即使没有任何公共修饰符也是默认公开的。Rust 只有 pub 修饰符来改变在模块树中的可见性。pub 有一些变体改变了公开可见的作用域:

  • pub(self)
  • pub(super)
  • pub(crate)
  • pub(in PATH)

更多细节可以查看 Rust Reference 的 Visibility and Privacy 一节。

下表是 C# 和 Rust 修饰符的大致对照:

C#Rust备注
private(default)见备注 1.
protectedN/A见备注 2.
internalpub(crate)
protected internal (family)N/A见备注 2.
publicpub
  1. 没有关键字来表明私有可见性,这是 Rust 的默认行为。

  2. 由于 Rust 中没有基于类的类型架构,也就没有 protected 的等价物了。

可变性

在 C# 中设计类型时,开发者可以自行决定类型是否可变、是否支持破坏性修变更。C# 也为 positional record 声明record classreadonly record struct) 支持不可变设计。在 Rust 中,可变性通过方法的 self 参数来表达,如下面的例子所示:

struct Point { x: i32, y: i32 }

impl Point {
    pub fn new(x: i32, y: i32) -> Self {
        Self { x, y }
    }

    // self 不可变

    pub fn x(&self) -> i32 { self.x }
    pub fn y(&self) -> i32 { self.y }

    // self 可变

    pub fn set_x(&mut self, val: i32) { self.x = val }
    pub fn set_y(&mut self, val: i32) { self.y = val }
}

C# 中,你可以用 with 来进行非破坏性变更:

var pt = new Point(123, 456);
pt = pt with { X = 789 };
Console.WriteLine(pt.ToString()); // prints: Point { X = 789, Y = 456 }

readonly record struct Point(int X, int Y);

Rust 中没有 with,但是为了模拟类似的实现,它必须硬编码在类型设计中:

struct Point { x: i32, y: i32 }

impl Point {
    pub fn new(x: i32, y: i32) -> Self {
        Self { x, y }
    }

    pub fn x(&self) -> i32 { self.x }
    pub fn y(&self) -> i32 { self.y }

    // following methods consume self and return a new instance

    pub fn set_x(self, val: i32) -> Self { Self::new(val, self.y) }
    pub fn set_y(self, val: i32) -> Self { Self::new(self.x, val) }
}

1

译者注①:前文中也多次出现了“属性”一词,这里第一次出现 C# 语境下的 “属性”。由于 C# 和 Rust 社区的常用翻译不同,C# 中的属性指的是 Property,即封装字段的访问器。另一个词 Attribute 在 C# 中通常译作“特性”,即使用方括号 [] 包裹的用于为代码附加元信息的特殊类型,使用上类似于 Rust 中的 attribute macro,但这个词通常被 Rust 社区译为 “属性宏”。此译本的做法是,Property 始终译作“属性”,Attribute 在 C# 语境中译作“特性”,在 Rust 语境中译作“属性”。

本地函数

C# 和 Rust 都有本地函数,不过 Rust 中的本地函数仅仅相当于 C# 中的静态本地函数。 也就是说,Rust 中的本地函数无法使用使用外部作用域的变量,但是 闭包 可以。

Lambda表达式与闭包

C# 和 Rust 允许函数用作一级值(first-class value),这使得可以编写 高阶函数。高阶函数可以接受其它函数作为参数,从而允许调用者参与被调用函数的代码。在 C# 中,类型安全的函数指针 由委托表示,最常见的就是 FuncAction。C# 允许通过 lambda 表达式 来临时声明这些委托。

Rust 也有函数指针,最简单的就是 fn 类型:

fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
    f(arg) + f(arg)
}

fn main() {
    let answer = do_twice(|x| x + 1, 5);
    println!("The answer is: {}", answer); // Prints: The answer is: 12
}

但是,Rust 对 函数指针fn 定义的类型) 和 闭包 做了区分:闭包可以引用环绕它的作用域的变量,但函数指针不可以。尽管 C# 也有 函数指针 (*delegate),但托管的、类型安全的等价替代是静态 lambda 表达式。

接受闭包的函数和方法都使用泛型类型编写,最终绑定到其中一种表示函数的 trait:FnFnMutFnOnce。当为函数指针和闭包提供值时, Rust 开发者使用 闭包表达式 (例如上例中的 |x| x + 1),就像 C# 中的 lambda 表达式。闭包表达式创建函数指针还是闭包取决于闭包表达式是否引用了它的上下文。

当一个闭包从环境中捕获变量时,所有权规则也会参与其中,因为所有权最终会随着闭包而结束。更多信息参见《Rust 程序设计语言》的 “将被捕获的值移出闭包和 Fn trait”一节。

变量

下面是 C# 中变量赋值的例子:

int x = 5;

Rust 中同样的:

let x: i32 = 5;

目前看来,两个语言唯一的不同在于类型声明的位置。而且,C# 和 Rust 都是类型安全的:编译器会确保变量的值始终是它期望的类型。借助编译器自动推断变量类型的能力,上面的例子可以简写。C# 中:

var x = 5;

Rust 中:

let x = 5;

扩展一下第一个例子,要更新变量的值(重新赋值),C# 和 Rust 的行为就不同了:

var x = 5;
x = 6;
Console.WriteLine(x); // 6

In Rust, the identical statement will not compile:

let x = 5;
x = 6; // Error: cannot assign twice to immutable variable 'x'.
println!("{}", x);

Rust 中,变量默认是 不可变 的。一旦值绑定到了一个名称,变量值就不能再改变了。可以通过在变量名前添加 mut 来使其成为 可变的

let mut x = 5;
x = 6;
println!("{}", x); // 6

Rust 提供了一种替代方案来解决上面例子的问题,而不需要可变性,就是使用变量 隐藏(shadowing):

let x = 5;
let x = 6;
println!("{}", x); // 6

C# 也支持隐藏,例如,本地变量可以隐藏字段,类型成员可以隐藏基类成员。在 Rust 中,上面的例子演示了隐藏还允许改变变量的类型而不改变名称,这对于要转换数据为不同的类型而又不想每一次都给它们起不同的名字的情况十分有用。

另见:

命名空间

命名空间在 .NET 中用于整理类型,也用来控制项目中类型和方法的作用域。

在 Rust 中,命名空间是一个不同的概念。Rust 中命名空间的等价物是模块。不管是 C# 还是 Rust,程序项的可见性都能够用可访问性修饰符,又叫可见性修饰符,来限制。Rust 中,默认的可见性是 私有的(private)(只有少数例外)。Rust 中和 C# 的 public 等价的是 pubinternal 则相当于 pub(crate)。如果需要更多细粒度的访问控制,参考 visibility modifiers

相等性

C# 中,比较相等性有时是指检验 相等(又称 值相等),有时是指检验_引用相等_,这是检验两个变量是否指向内存中的同一个底层对象。每一个自定义类型都可以用上面提到的其中一种语义来比较相等性,因为它继承自 System.Object(或 System.ValueType,它又继承自 System.Object)。

例如,在 C# 中比较值相等性和引用相等性:

var a = new Point(1, 2);
var b = new Point(1, 2);
var c = a;
Console.WriteLine(a == b); // (1) True
Console.WriteLine(a.Equals(b)); // (1) True
Console.WriteLine(a.Equals(new Point(2, 2))); // (1) True
Console.WriteLine(ReferenceEquals(a, b)); // (2) False
Console.WriteLine(ReferenceEquals(a, c)); // (2) True

record Point(int X, int Y);
  1. record Point 上的相等操作符 ==Equals 方法会比较值相等性,因为记录默认支持值类型相等语义。

  2. 比较引用相等性是检验变量是否指向内存中的同一个底层对象。

Rust 中的等价实现:

#[derive(Copy, Clone)]
struct Point(i32, i32);

fn main() {
    let a = Point(1, 2);
    let b = Point(1, 2);
    let c = a;
    println!("{}", a == b); // Error: "an implementation of `PartialEq<_>` might be missing for `Point`"
    println!("{}", a.eq(&b));
    println!("{}", a.eq(&Point(2, 2)));
}

上面的编译器报错展示了 Rust 的相等比较 总是 和 trait 实现相关。要支持使用 == 的比较,类型必须实现 PartialEq

要修复上面的实例就是要为 Point 实现 PartialEq。默认情况下,派生 PartialEq 会比较所有字段的相等性,这些字段本身也必须实现 PartialEq。这类似 C# 中记录的等价性。

#[derive(Copy, Clone, PartialEq)]
struct Point(i32, i32);

fn main() {
    let a = Point(1, 2);
    let b = Point(1, 2);
    let c = a;
    println!("{}", a == b); // true
    println!("{}", a.eq(&b)); // true
    println!("{}", a.eq(&Point(2, 2))); // false
    println!("{}", a.eq(&c)); // true
}

另见:

  • Eq:更严格版本的 PartialEq

泛型

C# 中的泛型提供了一种定义使用其它类型作为参数的类型和方法的方式。这提高了代码的重用性、类型安全和性能(例如,避免了运行时类型强制转换)。考虑下面这个示例,这是一个泛型类型,它为任何值添加一个时间戳:

using System;

sealed record Timestamped<T>(DateTime Timestamp, T Value)
{
    public Timestamped(T value) : this(DateTime.UtcNow, value) { }
}

Rust 也有泛型,这是上面的等价实现:

use std::time::*;

struct Timestamped<T> { value: T, timestamp: SystemTime }

impl<T> Timestamped<T> {
    fn new(value: T) -> Self {
        Self { value, timestamp: SystemTime::now() }
    }
}

另见:

泛型类型约束

C# 中,通过使用 where 子句,泛型类型可以被约束。 下例展示了 C# 中的这种约束:

using System;

// 备注:记录自动实现了 `IEquatable`。
// 为了和 Rust 比较,下面的实现显式表明了这一行为。
sealed record Timestamped<T>(DateTime Timestamp, T Value) :
    IEquatable<Timestamped<T>>
    where T : IEquatable<T>
{
    public Timestamped(T value) : this(DateTime.UtcNow, value) { }

    public bool Equals(Timestamped<T>? other) =>
        other is { } someOther
        && Timestamp == someOther.Timestamp
        && Value.Equals(someOther.Value);

    public override int GetHashCode() => HashCode.Combine(Timestamp, Value);
}

Rust 中同样可以做到:

use std::time::*;

struct Timestamped<T> { value: T, timestamp: SystemTime }

impl<T> Timestamped<T> {
    fn new(value: T) -> Self {
        Self { value, timestamp: SystemTime::now() }
    }
}

impl<T> PartialEq for Timestamped<T>
    where T: PartialEq {
    fn eq(&self, other: &Self) -> bool {
        self.value == other.value && self.timestamp == other.timestamp
    }
}

在 Rust 中,泛型类型约束叫做 bounds

C# 版本中, Timestamped<T> 实例 可以为自身实现了 IEquatable<T>T 类型创建。但是注意,Rust 版本更加灵活,因为它的 Timestamped<T> 有条件地实现了 PartialEq。这意味着 Timestamped<T> 实例仍然可以为不可比较的 T 类型创建,但是此时 Timestamped<T> 不会为这个 T 通过 PartialEq 实现相等性。

另见:

多态

Rust 不支持类和子类因此多态无法用 C# 中一样的方法实现。

另见:

继承

结构一节中解释过,不像 C#,Rust 不提供(基于类的)继承。一种为结构提供共享行为的方法是使用 trait。然而, 就像 C# 中的 接口继承,Rust 允许定义 trait 之间的关系,通过使用父 trait

异常处理

在 .NET 中,异常是一种派生自 System.Exception 的类。如果一段代码发生了问题,那么异常就会被抛出。抛出的异常会沿着栈传递,直到应用程序处理了它,或者程序终止。

Rust 没有异常,但是区分了 可恢复不可恢复 的错误。一个可恢复的错误代表着一个应当被报告的问题,但是程序为此继续运行。对于可能会因可恢复的错误而失败的操作,它的的结果是类型 Result<T,E>,其中 E 是错误变体的类型。panic! 宏会在程序遇到不可恢复的错误时终止程序。不可恢复的错误总是意味着漏洞的存在。

自定义错误类型

在 .NET 中, 自定义异常从 Exception 类派生。关于 如何创建用户定义的异常 的文档提到了这个例子:

public class EmployeeListNotFoundException : Exception
{
    public EmployeeListNotFoundException() { }

    public EmployeeListNotFoundException(string message)
        : base(message) { }

    public EmployeeListNotFoundException(string message, Exception inner)
        : base(message, inner) { }
}

在 Rust 中,可以通过实现 Error trait 来实现错误值的基本要求。Rust 中最简短的用户定义错误实现是:

#[derive(Debug)]
pub struct EmployeeListNotFound;

impl std::fmt::Display for EmployeeListNotFound {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str("Could not find employee list.")
    }
}

impl std::error::Error for EmployeeListNotFound {}

.NET 中 Exception.InnerException 属性的等价是 Rust 中的 Error::source() 方法。然而,对 Error::source() 的实现并非必要的,通用(默认的)实现返回一个 None

引发异常

要在 C# 中引发异常,抛出一个异常的实例:

void ThrowIfNegative(int value)
{
    if (value < 0)
    {
        throw new ArgumentOutOfRangeException(nameof(value));
    }
}

对于 Rust 中的可恢复错误,方法应当返回一个 OkErr 变体:

fn error_if_negative(value: i32) -> Result<(), &'static str> {
    if value < 0 {
        Err("Specified argument was out of the range of valid values. (Parameter 'value')")
    } else {
        Ok(())
    }
}

panic! 宏创建不可恢复的错误:

fn panic_if_negative(value: i32) {
    if value < 0 {
        panic!("Specified argument was out of the range of valid values. (Parameter 'value')")
    }
}

错误传播

在 .NET 中,异常会沿着调用栈传递直到它们被处理或者程序终止。在 Rust 中,不可恢复的错误表现类似,不过通常不会处理它们。

然而,可恢复的错误需要被显式地传播和处理。它们的存在总是由 Rust 函数或方法的签名表示。在 C# 中,捕获异常允许你对错误的存在与否进行处理:

void Write()
{
    try
    {
        File.WriteAllText("file.txt", "content");
    }
    catch (IOException)
    {
        Console.WriteLine("Writing to file failed.");
    }
}

在 Rust 中,这大致相当于:

fn write() {
    match std::fs::File::create("temp.txt")
        .and_then(|mut file| std::io::Write::write_all(&mut file, b"content"))
    {
        Ok(_) => {}
        Err(_) => println!("Writing to file failed."),
    };
}

大多数情况下,可恢复错误更需要被传播而不是处理。这种情况下,方法签名需要与要传播的错误类型兼容。? 运算符 以一种符合人体工学的方式传播错误。

fn write() -> Result<(), std::io::Error> {
    let mut file = std::fs::File::create("file.txt")?;
    std::io::Write::write_all(&mut file, b"content")?;
    Ok(())
}

备注:若要使用问号运算符来传播错误,错误实现必须 兼容,就像传播错误的简写:? 运算符中提到的。最一般的错误类型是错误的 trait 对象 Box<dyn Error>

堆栈跟踪

在 .NET 中抛出异常会导致运行时打印堆栈跟踪信息,这样可以借助额外的上下文来调试问题。

对于 Rust 中的可恢复错误,panic backtrace 提供了类似行为。

stable Rust 中的可恢复错误还不支持 backtrace,不过它当前已有实验性支持,通过使用provide 方法

Nullable 和 Option

在 C# 中,null 通常用来表示不存在、缺失或逻辑上未初始化的值。例如:

int? some = 1;
int? none = null;

Rust 没有 null 从而也就没有启用可为 null 的上下文的说法。作为替代,可选的或不存在的值用 Option<T> 表示。上面的 C# 代码的 Rust 等价实现是:

let some: Option<i32> = Some(1);
let none: Option<i32> = None;

Rust 的 Option<T> 实际上和 F# 的 'T option 一致。

带有可选项的控制流

在 C# 中,你可能会使用 if/else 语句来控制带有可为 null 值时的程序流。

uint? max = 10;
if (max is { } someMax)
{
    Console.WriteLine($"The maximum is {someMax}."); // The maximum is 10.
}

在 Rust 中你可以用模式匹配取得同样的行为:

使用 if let 甚至会更简单:

let max = Some(10u32);
if let Some(max) = max {
    println!("The maximum is {}.", max); // The maximum is 10.
}

Null 条件运算符

C# 的 Null 条件运算符(?.?[])使得处理 null 更人性化。在 Rust 中,它们最好的替代就是使用 map 方法。下面的代码段展示了对照:

string? some = "Hello, World!";
string? none = null;
Console.WriteLine(some?.Length); // 13
Console.WriteLine(none?.Length); // (blank)
let some: Option<String> = Some(String::from("Hello, World!"));
let none: Option<String> = None;
println!("{:?}", some.map(|s| s.len())); // Some(13)
println!("{:?}", none.map(|s| s.len())); // None

Null 合并运算符

Null 合并运算符(??)常用于当一个可为 null 的值为 null 时默认为另一个值:

int? some = 1;
int? none = null;
Console.WriteLine(some ?? 0); // 1
Console.WriteLine(none ?? 0); // 0

在 Rust 中,你可以通过 unwrap_or 来取得同样的行为:

let some: Option<i32> = Some(1);
let none: Option<i32> = None;
println!("{:?}", some.unwrap_or(0)); // 1
println!("{:?}", none.unwrap_or(0)); // 0

备注:如果默认值的计算很昂贵,你可以使用 unwrap_or_else 替代。它接收闭包作为参数,因此可以懒加载默认值。

Null 包容运算符

Null 包容运算符(!)在 Rust 中没有对应的构造,因为在 C# 中,它也仅仅是影响编译器的静态分析。在 Rust 中,不需要它这样的存在。

弃元

在 C# 中,弃元 表示编译器和其它工具链会忽略一个表达式的(部分)结果。

有多种上下文可以用到这个,例如最基本的,忽略一个表达式的结果。 在 C# 中,像这样:

_ = city.GetCityInformation(cityName);

在 Rust 中,忽略表达式的值 看上去完全一致:

_ = city.get_city_information(city_name);

在 C# 中,弃元也用于解构元组:

var (_, second) = ("first", "second");

Rust 中,完全一致:

let (_, second) = ("first", "second");

除了解构元组,Rust 还提供了对结构和枚举的解构,通过使用 ..,它表示类型的剩余部分:

struct Point {
    x: i32,
    y: i32,
    z: i32,
}

let origin = Point { x: 0, y: 0, z: 0 };

match origin {
    Point { x, .. } => println!("x is {}", x), // x is 0
}

当进行模式匹配时,忽略一部分匹配表达式是很有用的,例如 C# 中:

_ = ("first", "second") switch
{
    ("first", _) => "first element matched",
    (_, _) => "first element did not match"
};

同样的,Rust 的看起来也几乎一致:

_ = match ("first", "second")
{
    ("first", _) => "first element matched",
    (_, _) => "first element did not match"
};

类型转换

C# 和 Rust 在编译时都是静态类型的。因此,一旦声明了一个变量,后续再为它赋值一个不同类型的值是不被允许的 (除非该值可以隐式转换到目标类型)。C# 中有好几种类型转换的方式,Rust中也有和它们相对的存在。

隐式转换

C# 中有隐式转换,Rust 也是(这被称作强制类型转换(Type coercion))。参考这个例子:

int intNumber = 1;
long longNumber = intNumber;

Rust 对于对于强制类型转换更严格:

let int_number: i32 = 1;
let long_number: i64 = int_number; // error: expected `i64`, found `i32`

一个有效的隐式转换的例子,通过使用 子类型(subtyping)

fn bar<'a>() {
    let s: &'static str = "hi";
    let t: &'a str = s;
}

另见:

显式转换

如果转换可能导致丢失信息,C# 需要使用强制转换表达式来显式转换:

double a = 1.2;
int b = (int)a;

当向下转换时,显式转换可能会在运行时失败,并抛出 OverflowExceptionInvalidCastException 之类的异常。

Rust 不提供原始类型之间的强制转换,不过可以使用 asas.rs 关键字来进行显式转换。Rust 中显式转换不会引发 panic。

let int_number: i32 = 1;
let long_number: i64 = int_number as _;

自定义转换

通常,.NET 类型提供用户定义的转换运算符来将一个类型转换成另一个。另外,System.IConvertible 也是为了类型互相转换而存在的。

再 Rust 中,标准库包含将一个值转换成另一种类型的抽象,通过 From trait 和与它对应的 Into。当为一个类型实现 From 时,会自动提供一个默认的 Into 实现(在 Rust 中叫做 通用实现)。下例演示了这两种类型转换:

fn main() {
    let my_id = MyId("id".into()); // 由于为 `String` 实现了 `from<&str>` trait,`into()` 也自动实现了。
    println!("{}", String::from(my_id)); // 这使用 `String` 的 `From<MyId>` 实现。
}

struct MyId(String);

impl From<MyId> for String {
    fn from(MyId(value): MyId) -> Self {
        value
    }
}

另见:

运算符重载

在 C# 中,自定义类型可以重载 可重载运算符。考虑下面这个 C# 示例:

Console.WriteLine(new Fraction(5, 4) + new Fraction(1, 2));  // 14/8

public readonly record struct Fraction(int Numerator, int Denominator)
{
    public static Fraction operator +(Fraction a, Fraction b) =>
        new(a.Numerator * b.Denominator + b.Numerator * a.Denominator, a.Denominator * b.Denominator);

    public override string ToString() => $"{Numerator}/{Denominator}";
}

在 Rust 中,大多数运算符可以通过 trait 重载。之所以这样可行是因为运算符是方法调用的语法糖。例如, a + b 中的 + 运算符调用了 add 方法。(参见 operator overloading)。

use std::{fmt::{Display, Formatter, Result}, ops::Add};

struct Fraction {
    numerator: i32,
    denominator: i32,
}

impl Display for Fraction {
    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
        f.write_fmt(format_args!("{}/{}", self.numerator, self.denominator))
    }
}

impl Add<Fraction> for Fraction {
    type Output = Fraction;

    fn add(self, rhs: Fraction) -> Fraction {
        Fraction {
            numerator: self.numerator * rhs.denominator + rhs.numerator * self.denominator,
            denominator: self.denominator * rhs.denominator,
        }
    }
}

fn main() {
    println!(
        "{}",
        Fraction { numerator: 5, denominator: 4 } + Fraction { numerator: 1, denominator: 2 }
    ); // 14/8
}

文档注释

C# 提供一种使用包含 xml 文本的注释语法来为类型 API 编写文档的方案。C# 编译器会产生一个 xml 文件,它包含了表示注释和 API 签名的结构化数据。其他工具可以将该输出处理为另一种人类可读的文档形式。一个 C# 中的简单示例:

/// <summary>
/// This is a document comment for <c>MyClass</c>.
/// </summary>
public class MyClass {}

Rust 文档注释提供了和 C# 中同等的功能。Rust 文档注释使用 Markdown 语法。rustdoc 是 Rust 代码的文档编译器,通常通过 cargo doc 调用,这会将注释编译为文档。例如:

/// This is a doc comment for `MyStruct`.
struct MyStruct;

.NET SDK 中并没有 dotnet doc 之类的和 cargo doc 对应的命令。

另见:

内存管理

资源管理

线程操作

同步

生产者-消费者

测试

基准测试

日志记录和跟踪

条件编译

环境与配置

LINQ

元编程

异步编程

项目结构

编译与构建