什么是方法
方法类似于函数:它们用fn关键字及其名称声明,它们可以具有参数和返回值,并且它们包含一些从其他地方调用它们时将运行的代码。但是,方法与函数的不同之处在于,它们是在struct(或分别在第6和17章中介绍的enum或trait对象)的上下文中定义的,并且它们的第一个参数始终为self和c++(this)一致,表示调用该方法的struct实例。
定义方法
让我们改变area,有一个函数Rectangle实例作为一个参数,而是做出area规定的方法Rectangle结构,如清单5-13英寸
文件名:src / main.rs
#[derive(Debug)]struct Rectangle { width: u32, height: u32,}impl Rectangle { fn area(&self) -> u32 { self.width * self.height }}fn main() { let rect1 = Rectangle { width: 30, height: 50, }; println!( "The area of the rectangle is {} square pixels.", rect1.area() );}
清单5-13:area在Rectangle结构上定义方法
为了在的上下文中定义功能Rectangle,我们开始一个impl (实现)块。然后,我们将area函数移到impl 大括号内,并将第一个(在这种情况下,仅此参数)参数更改为 self签名中以及正文中的所有位置。在中main,我们调用了area函数并rect1作为参数传递,我们可以改用方法语法area在Rectangle实例上调用方法。方法的语法在实例之后:我们在方法名称,括号和任何参数之后添加一个点。
在的签名中area,我们使用&self而不是,rectangle: &Rectangle 因为Rust知道类型self是Rectangle由于此方法位于impl Rectangle上下文中。请注意,就像在中一样,我们仍然需要使用& before 。方法可以像这里所做的那样获取所有权 ,不变地借用或可变地借用,就像它们可以使用任何其他参数一样。self&Rectangleselfselfself
我们之所以选择&self此处,是出于&Rectangle在函数版本中使用的相同原因:我们不想获得所有权,我们只想读取结构中的数据,而不是对其进行写入。如果要在方法执行的过程中更改调用方法的实例,则将其&mut self用作第一个参数。self很少有一种方法可以像第一个参数那样使用实例的所有权;该方法通常在方法转换self为其他方法时使用,并且您希望防止调用者在转换后使用原始实例。
除了使用方法语法,而且不必重复self每个方法签名中的类型之外,使用方法代替函数的主要好处是对组织而言。我们将一个类型的实例可以做的所有事情都放在一个impl块中,而不是让我们的代码的未来用户Rectangle在我们提供的库中的各个位置搜索功能。
->接线员在哪里?
在C和C++中,使用两种不同的运算符来调用方法,如果要直接在对象上调用方法,以及->要在指向对象的指针上调用方法,则需要先取消引用该指针。换句话说,如果object是指针, object->something()则类似于(*object).something()。
Rust没有与->运算符等效的对象;相反,Rust具有称为自动引用和取消引用的功能。调用方法是Rust少数具有这种行为的地方之一。
下面是它如何工作的:当你调用一个方法有object.something()锈会自动添加&,&mut或*使object该方法的签名相匹配。换句话说,以下是相同的:
p1.distance(&p2); (&p1).distance(&p2);
第一个看起来更干净。由于方法具有明确的接收方(的类型),因此这种自动引用行为有效self。给定方法的接收者和名称,Rust可以明确地确定该方法是读取(&self),更改(&mut self)还是使用(self)。在实践中,Rust使方法接收者隐含借贷这一事实是使所有权符合人体工程学的重要组成部分。
参数更多的方法
让我们通过在Rectangle 结构上实现第二种方法来练习使用方法。这次,我们希望of的一个实例采用of的Rectangle另一个实例,如果第二个实例可以完全放入其中Rectangle,则返回;否则它将返回。也就是说,一旦定义了 方法,我们希望能够编写清单5-14中所示的程序。trueRectangleselffalsecan_hold
文件名:src / main.rs
fn main() { let rect1 = Rectangle { width: 30, height: 50, }; let rect2 = Rectangle { width: 10, height: 40, }; let rect3 = Rectangle { width: 60, height: 45, }; println!("Can rect1 hold rect2? {}", rect1.can_hold(&rect2)); println!("Can rect1 hold rect3? {}", rect1.can_hold(&rect3));}
清单5-14:使用尚未编写的can_hold 方法
预期的输出将如下所示,因为的两个维度rect2均小于的维度,rect1但rect3大于 rect1:
Can rect1 hold rect2? trueCan rect1 hold rect3? false
我们知道我们想定义一个方法,因此它将在代码impl Rectangle 块内。方法名称为can_hold,它将以不可变的借用另一个Rectangle作为参数。通过查看调用该方法的代码,我们可以知道参数的类型: rect1.can_hold(&rect2)传入&rect2,这是对rect2的实例的不可变借用 Rectangle。这是有道理的,因为我们只需要读取rect2(而不是写,这意味着我们需要可变的借用),并且我们想要main保留rect2对它的所有权,因此可以在调用该can_hold方法之后再次使用它。的返回值can_hold将是一个布尔值,并且实现将检查的宽度和高度 self都分别大于另一个的宽度和高度Rectangle。让我们将新can_hold方法添加到impl清单5-13中的代码块中,如清单5-15所示。
impl Rectangle { fn area(&self) -> u32 { self.width * self.height } fn can_hold(&self, other: &Rectangle) -> bool { self.width > other.width && self.height > other.height }}
清单5-15:在该can_hold方法上 Rectangle实现以另一个Rectangle实例作为参数的方法
当使用main清单5-14中的函数运行此代码时,将获得所需的输出。方法可以采用多个参数,self这些参数在参数之后添加到签名中,这些参数的作用与函数中的参数相同。
相关功能
的另一个有用的功能impl块的是,我们能定义范围内的功能impl块是不带self作为参数。这些被称为关联函数,因为它们与结构关联。它们仍然是函数,而不是方法,因为它们没有可使用的结构实例。您已经使用了String::from关联的功能。
关联函数通常用于将返回该结构的新实例的构造函数。例如,我们可以提供一个关联的函数,该函数将具有一个维度参数并将其用作宽度和高度,从而使创建正方形Rectangle而不是必须两次指定相同的值变得更加容易:
文件名:src / main.rs
impl Rectangle { fn square(size: u32) -> Rectangle { Rectangle { width: size, height: size, } }}
要调用此关联函数,我们使用::带有结构名称的语法; let sq = Rectangle::square(3);是一个例子。该函数由struct命名空间:::语法用于关联的函数和模块创建的命名空间。我们将在第7章中讨论模块。
多个impl块
每个结构允许有多个impl块。例如,清单5-15等效于清单5-16所示的代码,清单5-16中的每个方法都在自己的impl块中。
impl Rectangle { fn area(&self) -> u32 { self.width * self.height }}impl Rectangle { fn can_hold(&self, other: &Rectangle) -> bool { self.width > other.width && self.height > other.height }}
清单5-16:使用多个impl 块重写清单5-15
这里没有理由将这些方法分为多个impl块,但这是有效的语法。impl在第10章中,我们将讨论一种情况,其中多个块是有用的,我们将在第10章中讨论泛型类型和特征。
总结
通过结构,您可以创建对您的域有意义的自定义类型。通过使用结构,可以使关联的数据段相互连接,并为每个数据段命名以使代码清晰。使用方法可以指定结构实例的行为,而使用关联的函数可以在没有实例可用的情况下使用特定于结构的命名空间功能。
但是结构并不是创建自定义类型的唯一方法:让我们转到Rust的枚举功能,向您的工具箱添加另一个工具。