这是用户在 2024-3-19 17:44 为 https://steveklabnik.com/writing/rusts-golden-rule 保存的双语快照页面,由 沉浸式翻译 提供双语支持。了解如何保存?

Rust's Golden Rule Rust的黄金法则

Home  Blog

2023-03-27

I find myself thinking about a particular design principle of Rust today. I’m not sure I’ve ever seen it named specifically before, but it gets referred to from time to time, and I think it’s an under-rated but very important aspect of why Rust works so well.
今天我发现自己在思考Rust的一个特定的设计原则。我不确定我以前是否见过它被特别命名,但它不时被提及,我认为这是一个被低估的,但非常重要的方面,为什么Rust工作得这么好。

I was going to refer to it as “the signature is the contract” today, but then I ended up changing it. Regardless of that, if someone else has already written this out somewhere, and used a different name, please let me know!
我今天本来想说的是“签名就是合同”,但后来我改了,不管怎么说,如果有人已经在什么地方写出来了,用了别的名字,请告诉我!

Magic: the Gathering is a really interesting project. I say “project” rather than “card game” because while it is a card game, it also pioneered a whole bunch of incedental other things that had big effects on related hobbies.
魔术:聚会是一个非常有趣的项目。我说“项目”而不是“纸牌游戏”,因为虽然它是一个纸牌游戏,但它也开创了一大堆对相关爱好产生重大影响的其他事情。

I learned MtG in the late 90s. The rules were a bit different then, but many of them are the same. The very first rule I was taught was sort of the “Magic Golden Rule,” though in today’s Comprehensive Rulebook, there are four of them. This one is still the first, though:
我在90年代末学习了MtG。当时的规则有点不同,但其中许多是相同的。我学到的第一条规则是“神奇的黄金法则”,尽管在今天的综合规则手册中,有四条。但这仍然是第一个:

  1. The Magic Golden Rules
    神奇的黄金法则

101.1. Whenever a card’s text directly contradicts these rules, the card takes precedence. The card overrides only the rule that applies to that specific situation. The only exception is that a player can concede the game at any time (see rule 104.3a).
101.1.当一张卡片的文字直接与这些规则相抵触时,卡片优先。这张卡只覆盖适用于该特定情况的规则。唯一的例外是球员可以在任何时候认输(见规则104.3a)。

This rule is the most important rule because it kind of creates the spaces of possibilities for cards: many cards exist to tweak, modify, or break some sort of fundamental rule.
这条规则是最重要的规则,因为它为卡片创造了可能性的空间:许多卡片的存在是为了调整,修改或打破某种基本规则。

That being said, all these years later, this idea is so pervasive in games like this that it’s barely even considered an actual rule. It’s just part of the physics of the genre, it’s how these games work. Yet it’s critical to the entire enterprise.
话虽如此,这么多年过去了,这个想法在这样的游戏中是如此普遍,以至于它几乎没有被认为是一个实际的规则。这只是物理学的一部分,这是这些游戏的工作原理。然而,这对整个企业至关重要。

Rust also has a rule. It’s kinda funny, because in some senses, this rule is almost the opposite of Magic’s, if you can even stretch the comparison this far. Here it is:
#10001;有一个规则。这有点有趣,因为在某种意义上,这个规则几乎是相反的魔术的,如果你甚至可以延伸的比较这么远。这就是:

Whenever the body of a function contradicts the function’s signature, the signature takes precedence; the signature is right and the body is wrong.
当函数体与函数的签名相矛盾时,签名优先;签名是正确的,函数体是错误的。

This rule is also so pervasive in Rust that we take it for granted, but it is really, truly important. I think it is also important for Rust users to internalize the implications of this rule, so that they know why certain things work the way that they do.
这条规则在Rust中也是如此普遍,以至于我们认为它是理所当然的,但它真的非常重要。我认为对Rust用户来说,内化这条规则的含义也很重要,这样他们就知道为什么某些事情会以他们的方式工作。

Here is the most famous implication of this rule: Rust does not infer function signatures. If it did, changing the body of the function would change its signature. While this is convenient in the small, it has massive ramifications.
下面是这个规则最著名的含义:Rust不会推断函数签名。如果是这样的话,那么改变函数体就会改变它的签名。虽然这在小范围内很方便,但它具有巨大的影响。

Consider this example program:
考虑这个示例程序:

fn foo(x: i32) -> i32 {
    dbg!(x);
    
    x
}

This function prints out x, and then returns it. Nothing fancy going on here, this is just random stuff to make an example. This compiles just fine. But let’s imagine that we have a version of Rust that infers our signatures. So we could type this instead:
这个函数打印出 x ,然后返回它。这里没有什么花哨的东西,这只是一个随机的例子。这个编译得很好。但让我们想象一下,我们有一个版本的Rust,推断我们的签名。所以我们可以这样输入:

fn foo(x) {

This is what a Ruby-ish Rust might look like; we declare the name of our argument but not its type, and we don’t declare a return type. Now, let’s do a small refactoring, we’re gonna comment out the final value there:
这就是Ruby-ish的Rust可能的样子;我们声明参数的名称,但不声明其类型,也不声明返回类型。现在,让我们做一个小的重构,我们要注释掉最终的值:

fn foo(x) {
    dbg!(x);
    
    //x
}

the final expression has changed; it’s no longer x, but instead is (), which is what dbg!(x); evaluates to. Because of type inference, the inferrred type of foo is now fn(i32) -> (). Our function typechecks! It’s all good, right?
最后的表达式已经改变了;它不再是 x ,而是 () ,这是 dbg!(x); 的计算结果。由于类型推断, foo 的推断类型现在是 fn(i32) -> () 。我们的功能类型!一切都很好,对吧?

Well, no: 嗯,不:

error[E0369]: cannot add `{integer}` to `()`
 --> src/lib.rs:5:11
  |
5 |         y + 1
  |         - ^ - {integer}
  |         |
  |         ()

Wait, what? We don’t have a y + 1 anywhere in our code?! Where’s that error coming from… src/lib.rs:5. What’s at the top of lib.rs?
等等,你说什么?我们的代码中没有任何地方有 y + 1 吗?这个错误是从哪里来的? src/lib.rs:5 2#的顶部是什么?

mod bar {
    fn baz() -> i32 {
        let y = crate::foo(5);
        
        y + 1
    }
}

Oh. Some other code was using foo. When its signature changed, we broke this invocation.
哦其他代码使用了 foo 。当它的签名改变时,我们破坏了这个调用。

It’s nice that alt-Rust caught this for us, but this error is (would be, anyway!) really bad: no longer is it telling us that our code is wrong, but instead points to some other code somewhere else we weren’t even messing with. Sure, we can go “hey why is y ()?” and then figure it out, but compare that to today’s error:
很高兴alt-Rust为我们捕捉到了这个,但是这个错误是(无论如何都会是!)非常糟糕:它不再告诉我们我们的代码是错误的,而是指向其他地方的其他代码,我们甚至没有弄乱。当然,我们可以说“嘿,为什么是 y () ?”然后再算出来,但是把它和今天的错误比较一下:

error[E0308]: mismatched types
  --> src/lib.rs:9:19
   |
9  | fn foo(x: i32) -> i32 {
   |    ---            ^^^ expected `i32`, found `()`
   |    |
   |    implicitly returns `()` as its body has no tail or `return` expression
10 |     dbg!(x);
   |            - help: remove this semicolon to return this value

This error is far better: it points out that the body of our function contradicts the signature. It points out what in our body is generating the value that contradicts the signature. And it doesn’t complain about callers.
这个错误要好得多:它指出我们的函数体与签名相矛盾。它指出了我们身体中的什么产生了与签名相矛盾的值。它不会抱怨来电者。

So sure, this gets us nicer error messages, but is that really a big deal? I think it is, but it’s not the only implication here. I’d like to talk about two more: one that’s clearly an advantage, and one that has led to some pain that people would like to resolve. Balance :)
当然,这会给我们带来更好的错误消息,但这真的有什么大不了的吗?我想是的,但这不是唯一的暗示。我想再谈两个:一个显然是一个优势,另一个导致了一些人们想要解决的痛苦。余额:)

First, the one that’s an advantage. The advantage is modularity. That makes some forms of analysis much more reasonable, or sometimes even possible, as opposed to super difficult.
第一个是优势优点是模块化。这使得某些形式的分析更加合理,有时甚至是可能的,而不是超级困难。

Because everything you need for memory safety is described in the signature of the function, Rust doesn’t need to examine your entire program to determine if there’s some shenanigans going on elsewhere. This is far, far less work than just checking the signatures of the functions you call.
因为内存安全所需的一切都在函数的签名中描述了,所以Rust不需要检查整个程序来确定其他地方是否有恶作剧。这比仅仅检查你调用的函数的签名要少得多。

Each function can be checked in isolation, and then assembled together. This is a very nice property.
每个函数都可以单独检查,然后组装在一起。这是一家非常好的酒店。

Second, where this leads to some pain. Users have nicknamed this one “borrow splitting,” or “partial borrowing.” It looks like this:
第二,这会导致一些痛苦。用户将其昵称为“借用拆分”或“部分借用”。它看起来像这样:

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

impl Point {
    pub fn x_mut(&mut self) -> &mut i32 {
        &mut self.x
    }

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

I find accessors, and especially mutators, to be where this sort of thing pops up most often. This is the classic example. The above code is fine, but if we try to do this:
我发现访问器,尤其是修改器,是这类东西最常出现的地方。这是一个典型的例子。上面的代码很好,但是如果我们尝试这样做:

// this doesn't work
impl Point {
    pub fn calculate(&mut self) -> i32 {
        let x = self.x_mut();
        let y = self.y_mut();
        
        // yes I picked multiplication because this looks kinda funny
        *x * *y
    }
}

// we would call it like this:
let answer = p.calculate();

We get this: 我们得到这个:

error[E0499]: cannot borrow `*self` as mutable more than once at a time
  --> src/lib.rs:19:17
   |
18 |         let x = self.x_mut();
   |                 ------------ first mutable borrow occurs here
19 |         let y = self.y_mut();
   |                 ^^^^^^^^^^^^ second mutable borrow occurs here
20 |         
21 |         *x * *y
   |         -- first borrow later used here

However, if we didn’t have these accessors, x and y were instead just public, this very similar free function:
然而,如果我们没有这些访问器, xy 就只是公共的,这个非常相似的自由函数:

fn calculate(x: &mut i32, y: &mut i32) -> i32 {
    *x * *y
}

// called like this:
let answer = calculate(&mut point.x, &mut point.y);

works just fine. Why? Because of the signatures. This signature:
效果很好为什么?为什么?因为签名。此签名:

pub fn calculate(&mut self) -> i32 {

and these signatures: 这些签名:

pub fn x_mut(&mut self) -> &mut i32 {
pub fn y_mut(&mut self) -> &mut i32 {

says “hey, I am going to borrow all of self mutably,” which implies an exclusive reference to self. That the body of calculate borrows two different parts of self independently is 100% irrelevant, that’s what the signature says! And so rustc looks at this and says “hey wait a minute, calling x_mut borrows self mutably, and calling y_mut borrows self mutably. That’s aliasing! Bad programmer!
说“hey,I am going to borrow all of self mutably,”这意味着对 self 的排他性引用。 calculate 的身体独立地借用 self 的两个不同部分是100%无关的,这就是签名所说的!所以rustc看到这个,说“嘿,等一下,调用 x_mut 借用 self 可变,调用 y_mut 借用 self 可变。那是假名字坏程序员!

Whereas in the second example, this signature:
而在第二个例子中,这个签名:

fn calculate(x: &mut i32, y: &mut i32) -> i32 {

says “hey, I have two mutable references, to two different integers.” And at the call site, Rust sees that we’re creating two different borrows to two different integers, even though they’re both part of our point, and so it okays things.
说"嘿,我有两个可变的引用,指向两个不同的整数。"在调用站点,Rust看到我们创建了两个不同的整数的两个不同的借用,即使它们都是我们的 point 的一部分,所以这是可以的。

This is kind of a pain in the butt! But what it saves us from is that scary action at a distance in our typecheck example.
这是一种痛苦的屁股!但它让我们避免了在我们的类型检查示例中远距离的可怕行为。

Imagine that Rust somehow inferred that the first version was okay, due to the body only being disjoint. What happens in the future, when we refactor our function, and the borrows need to change?
想象一下,Rust以某种方式推断出第一个版本是好的,因为身体只是不相交的。当我们重构函数时,将来会发生什么,而借用需要改变?

That would have the same problem as before; we’d get weird errors elsewhere in our code. There have been some proposals over the years to possibly fix this pain point, but it’s a tough one.
这会有和以前一样的问题;我们会在代码的其他地方得到奇怪的错误。多年来,有一些建议可能解决这个痛点,但这是一个艰难的建议。

Putting “here’s the fields I’m borrowing in the body” into the signature, in order to conform with the Golden Rule, looks very odd. Maybe something will happen here, maybe not. I have complicated feelings.
为了符合黄金法则,将“这是我在身体中借用的字段”放入签名中,看起来非常奇怪。也许这里会发生什么,也许不会。我的感觉很复杂。

So beyond error messages and making accessors/mutators awkward, how does this affect you day to day? Well, one way is that I find I end up paying more attention to signatures and less attention to bodies, when I’m trying to sort something out.
In many cases, what you’re doing in the body is irrelevant, I care only about what your signature affords me. This isn’t quite as true as in some pure functional languages, but it has that vague feel to it. Another way, and a slightly bigger one, has to do with the relationships between types and TDD, but I think I’m going to save that for its own post.