Cú pháp Method
Method tương tự như các hàm: chúng ta khai báo chúng với từ khóa fn
và một
tên, chúng có thể có các tham số và giá trị trả về, và chúng chứa một số mã được
chạy khi method được gọi từ nơi khác. Không giống như các hàm, method được định
nghĩa trong ngữ cảnh của một struct (hoặc một enum hoặc một trait object, mà
chúng ta sẽ đề cập trong Chương 6 và Chương
18, tương ứng), và tham số đầu tiên của chúng
luôn là self
, đại diện cho instance của struct mà method đang được gọi.
Định nghĩa Method
Hãy thay đổi hàm area
có một instance Rectangle
làm tham số và thay vào đó,
tạo một method area
được định nghĩa trên struct Rectangle
, như thể hiện
trong Listing 5-13.
#[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() ); }
Để định nghĩa hàm trong ngữ cảnh của Rectangle
, chúng ta bắt đầu một khối
impl
(implementation) cho Rectangle
. Mọi thứ trong khối impl
này sẽ được
liên kết với kiểu Rectangle
. Sau đó, chúng ta di chuyển hàm area
vào trong
dấu ngoặc nhọn impl
và thay đổi tham số đầu tiên (và trong trường hợp này, duy
nhất) thành self
trong chữ ký và mọi nơi trong nội dung hàm. Trong main
, nơi
chúng ta đã gọi hàm area
và truyền rect1
như một đối số, chúng ta có thể sử
dụng cú pháp method để gọi method area
trên instance Rectangle
của chúng
ta. Cú pháp method được đặt sau một instance: chúng ta thêm một dấu chấm theo
sau bởi tên method, dấu ngoặc đơn, và bất kỳ đối số nào.
Trong chữ ký cho area
, chúng ta sử dụng &self
thay vì
rectangle: &Rectangle
. &self
thực tế là viết tắt của self: &Self
. Trong
một khối impl
, kiểu Self
là bí danh cho kiểu mà khối impl
đang áp dụng.
Method phải có một tham số tên là self
kiểu Self
làm tham số đầu tiên, vì
vậy Rust cho phép bạn viết tắt điều này với chỉ tên self
ở vị trí tham số đầu
tiên. Lưu ý rằng chúng ta vẫn cần sử dụng &
trước viết tắt self
để chỉ ra
rằng method này mượn instance Self
, giống như chúng ta đã làm trong
rectangle: &Rectangle
. Method có thể lấy quyền sở hữu của self
, mượn self
không thay đổi như chúng ta đã làm ở đây, hoặc mượn self
có thể thay đổi,
giống như chúng có thể với bất kỳ tham số nào khác.
Chúng ta chọn &self
ở đây vì cùng lý do chúng ta đã sử dụng &Rectangle
trong
phiên bản hàm: chúng ta không muốn lấy quyền sở hữu, và chúng ta chỉ muốn đọc dữ
liệu trong struct, không viết vào nó. Nếu chúng ta muốn thay đổi instance mà
chúng ta đã gọi method như một phần của những gì method làm, chúng ta sẽ sử dụng
&mut self
làm tham số đầu tiên. Có một method lấy quyền sở hữu của instance
bằng cách chỉ sử dụng self
làm tham số đầu tiên là hiếm; kỹ thuật này thường
được sử dụng khi method chuyển đổi self
thành một cái gì đó khác và bạn muốn
ngăn người gọi sử dụng instance ban đầu sau khi chuyển đổi.
Lý do chính để sử dụng method thay vì hàm, ngoài việc cung cấp cú pháp method và
không phải lặp lại kiểu của self
trong mọi chữ ký method, là để tổ chức. Chúng
ta đã đặt tất cả những thứ có thể làm với một instance của một kiểu trong một
khối impl
thay vì làm cho người dùng tương lai của mã chúng ta phải tìm kiếm
khả năng của Rectangle
ở nhiều nơi khác nhau trong thư viện mà chúng ta cung
cấp.
Lưu ý rằng chúng ta có thể chọn đặt tên cho một method giống với tên của một
trong các trường của struct. Ví dụ, chúng ta có thể định nghĩa một method trên
Rectangle
cũng được đặt tên là width
:
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } impl Rectangle { fn width(&self) -> bool { self.width > 0 } } fn main() { let rect1 = Rectangle { width: 30, height: 50, }; if rect1.width() { println!("The rectangle has a nonzero width; it is {}", rect1.width); } }
Ở đây, chúng ta đang chọn cách làm cho method width
trả về true
nếu giá trị
trong trường width
của instance lớn hơn 0
và false
nếu giá trị là 0
:
chúng ta có thể sử dụng một trường trong một method cùng tên cho bất kỳ mục đích
nào. Trong main
, khi chúng ta theo sau rect1.width
với dấu ngoặc đơn, Rust
biết chúng ta đang đề cập đến method width
. Khi chúng ta không sử dụng dấu
ngoặc đơn, Rust biết chúng ta đang đề cập đến trường width
.
Thường xuyên, nhưng không phải lúc nào cũng vậy, khi chúng ta đặt cho một method cùng tên với một trường, chúng ta muốn nó chỉ trả về giá trị trong trường đó và không làm gì khác. Các method như thế này được gọi là getter, và Rust không tự động triển khai chúng cho các trường của struct như một số ngôn ngữ khác. Getter rất hữu ích vì bạn có thể làm cho trường này riêng tư nhưng method là công khai, và do đó cho phép truy cập chỉ đọc vào trường đó như một phần của API công khai của kiểu. Chúng ta sẽ thảo luận về các khái niệm công khai và riêng tư là gì và cách chỉ định một trường hoặc method là công khai hay riêng tư trong Chương 7.
Toán tử
->
Ở Đâu?Trong C và C++, hai toán tử khác nhau được sử dụng để gọi method: bạn sử dụng
.
nếu bạn đang gọi một method trực tiếp trên đối tượng và->
nếu bạn đang gọi method trên một con trỏ đến đối tượng và cần giải tham chiếu con trỏ trước. Nói cách khác, nếuobject
là một con trỏ,object->something()
tương tự như(*object).something()
.Rust không có một toán tử tương đương với
->
; thay vào đó, Rust có một tính năng gọi là tham chiếu và giải tham chiếu tự động. Gọi method là một trong số ít nơi trong Rust có hành vi này.Đây là cách nó hoạt động: khi bạn gọi một method bằng
object.something()
, Rust tự động thêm&
,&mut
, hoặc*
đểobject
khớp với chữ ký của method. Nói cách khác, những điều sau đây là giống nhau:#![allow(unused)] fn main() { #[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); }
Cái đầu tiên trông gọn gàng hơn nhiều. Hành vi tham chiếu tự động này hoạt động bởi vì method có một người nhận rõ ràng—kiểu của
self
. Với người nhận và tên của một method, Rust có thể xác định rõ ràng liệu method đang đọc (&self
), biến đổi (&mut self
), hay tiêu thụ (self
). Thực tế là Rust làm cho việc mượn ngầm định cho người nhận method là một phần lớn của việc làm cho quyền sở hữu trở nên tiện dụng trong thực tế.
Method với Nhiều Tham số
Hãy thực hành sử dụng method bằng cách triển khai một method thứ hai trên struct
Rectangle
. Lần này chúng ta muốn một instance của Rectangle
lấy một instance
khác của Rectangle
và trả về true
nếu Rectangle
thứ hai có thể nằm hoàn
toàn bên trong self
(tức là Rectangle
đầu tiên); nếu không, nó sẽ trả về
false
. Nghĩa là, một khi chúng ta đã định nghĩa method can_hold
, chúng ta
muốn có thể viết chương trình được hiển thị trong Listing 5-14.
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));
}
Đầu ra dự kiến sẽ trông giống như sau bởi vì cả hai kích thước của rect2
đều
nhỏ hơn kích thước của rect1
, nhưng rect3
rộng hơn rect1
:
Can rect1 hold rect2? true
Can rect1 hold rect3? false
Chúng ta biết mình muốn định nghĩa một method, vì vậy nó sẽ nằm trong khối
impl Rectangle
. Tên method sẽ là can_hold
, và nó sẽ lấy một bản mượn không
thay đổi của một Rectangle
khác làm tham số. Chúng ta có thể biết kiểu của
tham số sẽ là gì bằng cách nhìn vào mã gọi method: rect1.can_hold(&rect2)
truyền vào &rect2
, đó là một bản mượn không thay đổi cho rect2
, một instance
của Rectangle
. Điều này có ý nghĩa vì chúng ta chỉ cần đọc rect2
(thay vì
viết, có nghĩa là chúng ta cần một bản mượn có thể thay đổi), và chúng ta muốn
main
giữ quyền sở hữu của rect2
để chúng ta có thể sử dụng nó lại sau khi
gọi method can_hold
. Giá trị trả về của can_hold
sẽ là một giá trị Boolean,
và việc triển khai sẽ kiểm tra xem chiều rộng và chiều cao của self
có lớn hơn
chiều rộng và chiều cao của Rectangle
khác hay không, tương ứng. Hãy thêm
method can_hold
mới vào khối impl
từ Listing 5-13, như được hiển thị trong
Listing 5-15.
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } 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 } } 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)); }
Khi chúng ta chạy mã này với hàm main
trong Listing 5-14, chúng ta sẽ nhận
được đầu ra mong muốn. Method có thể lấy nhiều tham số mà chúng ta thêm vào chữ
ký sau tham số self
, và những tham số đó hoạt động giống như tham số trong
hàm.
Hàm Liên kết
Tất cả các hàm được định nghĩa trong một khối impl
được gọi là hàm liên kết
bởi vì chúng được liên kết với kiểu được đặt tên sau impl
. Chúng ta có thể
định nghĩa các hàm liên kết mà không có self
là tham số đầu tiên của chúng (và
do đó không phải là method) bởi vì chúng không cần một instance của kiểu để làm
việc với. Chúng ta đã sử dụng một hàm như thế này: hàm String::from
mà được
định nghĩa trên kiểu String
.
Các hàm liên kết không phải là method thường được sử dụng cho các constructor sẽ
trả về một instance mới của struct. Những hàm này thường được gọi là new
,
nhưng new
không phải là một tên đặc biệt và không được tích hợp sẵn trong ngôn
ngữ. Ví dụ, chúng ta có thể chọn cung cấp một hàm liên kết có tên square
sẽ có
một tham số kích thước và sử dụng nó làm cả chiều rộng và chiều cao, từ đó giúp
tạo một Rectangle
vuông dễ dàng hơn thay vì phải chỉ định cùng một giá trị hai
lần:
Tên tệp: src/main.rs
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } impl Rectangle { fn square(size: u32) -> Self { Self { width: size, height: size, } } } fn main() { let sq = Rectangle::square(3); }
Từ khóa Self
trong kiểu trả về và trong nội dung của hàm là bí danh cho kiểu
xuất hiện sau từ khóa impl
, trong trường hợp này là Rectangle
.
Để gọi hàm liên kết này, chúng ta sử dụng cú pháp ::
với tên struct;
let sq = Rectangle::square(3);
là một ví dụ. Hàm này được đặt tên trong không
gian tên bởi struct: cú pháp ::
được sử dụng cho cả hàm liên kết và không gian
tên được tạo bởi các module. Chúng ta sẽ thảo luận về các module trong Chương
7.
Nhiều Khối impl
Mỗi struct được phép có nhiều khối impl
. Ví dụ, Listing 5-15 tương đương với
mã được hiển thị trong Listing 5-16, trong đó mỗi method nằm trong khối impl
riêng của nó.
#[derive(Debug)] struct Rectangle { width: u32, height: u32, } 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 } } 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)); }
Không có lý do gì để tách các method này thành nhiều khối impl
ở đây, nhưng
đây là cú pháp hợp lệ. Chúng ta sẽ thấy một trường hợp trong đó nhiều khối
impl
là hữu ích trong Chương 10, nơi chúng ta thảo luận về các kiểu và trait
generic.
Tóm tắt
Struct cho phép bạn tạo các kiểu tùy chỉnh có ý nghĩa cho miền của bạn. Bằng
cách sử dụng struct, bạn có thể giữ các phần dữ liệu liên quan với nhau và đặt
tên cho mỗi phần để làm mã của bạn rõ ràng. Trong các khối impl
, bạn có thể
định nghĩa các hàm được liên kết với kiểu của bạn, và method là một loại hàm
liên kết cho phép bạn chỉ định hành vi mà các instance của struct của bạn có.
Nhưng struct không phải là cách duy nhất để bạn tạo kiểu tùy chỉnh: hãy chuyển sang tính năng enum của Rust để thêm một công cụ khác vào hộp công cụ của bạn.