trpl-zh-cn/src/ch05-03-method-syntax.md
2017-10-31 13:49:44 +08:00

9.5 KiB
Raw Blame History

方法语法

ch05-03-method-syntax.md
commit 44bf3afd93519f8b0f900f21a5f2344d36e13448

方法 与函数类似:他们使用 fn 关键和名字声明,可以拥有参数和返回值,同时包含一些代码会在某处被调用时执行。不过方法与函数是不同的,因为他们在结构体(或者枚举或者 trait 对象,将分别在第六章和第十七章讲解)的上下文中被定义,并且他们第一个参数总是 self,它代表方法被调用的结构体的实例。

定义方法

让我们将获取一个 Rectangle 实例作为参数的 area 函数改写成一个定义于 Rectangle 结构体上的 area 方法,如示例 5-13 所示:

文件名: src/main.rs

#[derive(Debug)]
struct Rectangle {
    length: u32,
    width: u32,
}

impl Rectangle {
    fn area(&self) -> u32 {
        self.length * self.width
    }
}

fn main() {
    let rect1 = Rectangle { length: 50, width: 30 };

    println!(
        "The area of the rectangle is {} square pixels.",
        rect1.area()
    );
}

示例 5-13Rectangle 结构体上定义 area 方法

为了使函数定义于 Rectangle 的上下文中,我们开始了一个 impl 块(implimplementation 的缩写)。接着将函数移动到 impl 大括号中,并将签名中的第一个(在这里也是唯一一个)参数和函数体中其他地方的对应参数改成 self。然后在main 中将我们调用 area 方法并传递 rect1 作为参数的地方,改成使用 方法语法method syntax)在 Rectangle 实例上调用 area 方法。方法语法获取一个实例并加上一个点号后跟方法名、括号以及任何参数。

area 的签名中,开始使用 &self 来替代 rectangle: &Rectangle,因为该方法位于 impl Rectangle 上下文中所以 Rust 知道 self 的类型是 Rectangle。注意仍然需要在 self 前面加上 &,就像 &Rectangle 一样。方法可以选择获取 self 的所有权,像我们这里一样不可变的借用 self,或者可变的借用 self,就跟其他别的参数一样。

这里选择 &self 跟在函数版本中使用 &Rectangle 出于同样的理由:我们并不想获取所有权,只希望能够读取结构体中的数据,而不是写入。如果想要能够在方法中改变调用方法的实例的话,需要将第一个参数改为 &mut self。很少见到通过仅仅使用 self 作为第一个参数来使方法获取实例的所有权;这种技术通常用在当方法将 self 转换成别的实例的时候,这时我们想要防止调用者在转换之后使用原始的实例。

使用方法而不是函数,除了使用了方法语法和不需要在每个函数签名中重复 self 类型之外,其主要好处在于组织性。我将某个类型实例能做的所有事情都一起放入 impl 块中,而不是让将来的用户在我们的代码中到处寻找 Rectangle 的功能。

->运算符到哪去了?

像在 C++ 这样的语言中,有两个不同的运算符来调用方法:. 直接在对象上调用方法,而 -> 在一个对象的指针上调用方法这时需要先解引用dereference指针。换句话说如果 object 是一个指针,那么 object->something() 就像 (*object).something() 一样。

Rust 并没有一个与 -> 等效的运算符相反Rust 有一个叫 自动引用和解引用automatic referencing and dereferencing)的功能。方法调用是 Rust 中少数几个拥有这种行为的地方。

这是它如何工作的:当使用 object.something() 调用方法时Rust 会自动增加 &&mut* 以便使 object 符合方法的签名。也就是说,这些代码是等同的:

# #[derive(Debug,Copy,Clone)]
# struct Point {
#     x: f64,
#     y: f64,
# }
#
# impl Point {
#    fn distance(&self, other: &Point) -> f64 {
#        let x_squared = f64::powi(other.x - self.x, 2);
#        let y_squared = f64::powi(other.y - self.y, 2);
#
#        f64::sqrt(x_squared + y_squared)
#    }
# }
# let p1 = Point { x: 0.0, y: 0.0 };
# let p2 = Point { x: 5.0, y: 6.5 };
p1.distance(&p2);
(&p1).distance(&p2);

第一行看起来简洁的多。这种自动解引用的行为之所以能行得通是因为方法有一个明确的接收者————self 类型。在给出接收者和方法名的前提下Rust 可以明确的计算出方法是仅仅读取(&self),做出修改(&mut self)或者是获取所有权(self。Rust 这种使得借用对方法接收者来说是隐式的做法是其所有权系统程序员友好性实践的一大部分。

带有更多参数的方法

让我们更多的实践一下方法,通过为 Rectangle 结构体实现第二个方法。这回,我们让一个 Rectangle 的实例获取另一个 Rectangle 实例并返回 self 能否完全包含第二个长方形,如果能返回 true 若不能则返回 false。一旦定义了 can_hold 方法,就可以运行示例 5-14 中的代码了:

文件名: src/main.rs

fn main() {
    let rect1 = Rectangle { length: 50, width: 30 };
    let rect2 = Rectangle { length: 40, width: 10 };
    let rect3 = Rectangle { length: 45, width: 60 };

    println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2));
    println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));
}

示例 5-14展示还未实现的 can_hold 方法的应用

我们希望看到如下输出,因为 rect2 的长宽都小于 rect1,而 rect3rect1 要宽:

Can rect1 hold rect2? true
Can rect1 hold rect3? false

因为我们想定义一个方法,所以它应该位于 impl Rectangle 块中。方法名是 can_hold,并且它会获取另一个 Rectangle 的不可变借用作为参数。通过观察调用位置的代码可以看出参数是什么类型的:rect1.can_hold(&rect2) 传入了 &rect2,它是一个 Rectangle 的实例 rect2 的不可变借用。这是可以理解的,因为我们只需要读取 rect2(而不是写入,这意味着我们需要一个可变借用)而且希望 main 保持 rect2 的所有权这样就可以在调用这个方法后继续使用它。can_hold 的返回值是一个布尔值,其实现会分别检查 self 的长宽是否都大于另一个 Rectangle。让我们在示例 5-13 的 impl 块中增加这个新方法,如示例 5-15 所示:

文件名: src/main.rs

# #[derive(Debug)]
# struct Rectangle {
#     length: u32,
#     width: u32,
# }
#
impl Rectangle {
    fn area(&self) -> u32 {
        self.length * self.width
    }

    fn can_hold(&self, other: &Rectangle) -> bool {
        self.length > other.length && self.width > other.width
    }
}

示例 5-15Rectangle 上实现 can_hold 方法,它获取另一个 Rectangle 实例作为参数

如果结合示例 5-14 的 main 函数来运行,就会看到想要得到的输出。方法可以在 self 后增加多个参数,而且这些参数就像函数中的参数一样工作。

关联函数

impl 块的另一个有用的功能是:允许在 impl 块中定义 self 作为参数的函数。这被称为 关联函数associated functions),因为他们与结构体相关联。即便如此他们仍是函数而不是方法,因为他们并不作用于一个结构体的实例。你已经使用过一个关联函数了:String::from

关联函数经常被用作返回一个结构体新实例的构造函数。例如我们可以提供一个关联函数,它接受一个维度参数并且同时用来作为长和宽,这样可以更轻松的创建一个正方形 Rectangle 而不必指定两次同样的值:

Filename: src/main.rs

# #[derive(Debug)]
# struct Rectangle {
#     length: u32,
#     width: u32,
# }
#
impl Rectangle {
    fn square(size: u32) -> Rectangle {
        Rectangle { length: size, width: size }
    }
}

使用结构体名和 :: 语法来调用这个关联函数:比如 let sq = Rectangle::square(3);。这个方法位于结构体的命名空间中::: 语法用于关联函数和模块创建的命名空间,第七章会讲到后者。

多个 impl

每个结构体都允许拥有多个 impl 块。例如,示例 5-15 等同于示例 5-16 的代码,这里每个方法有其自己的 impl 块:

# #[derive(Debug)]
# struct Rectangle {
#     length: u32,
#     width: u32,
# }
#
impl Rectangle {
    fn area(&self) -> u32 {
        self.length * self.width
    }
}

impl Rectangle {
    fn can_hold(&self, other: &Rectangle) -> bool {
        self.length > other.length && self.width > other.width
    }
}

示例 5-16使用多个 impl 块重写示例 5-15

总结

结构体让我们可以在自己的范围内创建有意义的自定义类型。通过结构体,我们可以将相关联的数据片段联系起来并命名他们来使得代码更清晰。方法允许为结构体实例指定行为,而关联函数将特定功能置于结构体的命名空间中并且无需一个实例。

结构体并不是创建自定义类型的唯一方法;让我们转向 Rust 的枚举功能并为自己的工具箱再填一个工具。