Câu trả lời ngắn gọn: Để có tính linh hoạt tối đa, bạn có thể lưu trữ lệnh gọi lại dưới dạng một FnMut
đối tượng được đóng hộp , với bộ cài đặt gọi lại chung về kiểu gọi lại. Mã cho điều này được hiển thị trong ví dụ cuối cùng trong câu trả lời. Để có lời giải thích chi tiết hơn, hãy đọc tiếp.
"Con trỏ hàm": gọi lại dưới dạng fn
Tương đương gần nhất của mã C ++ trong câu hỏi sẽ là khai báo gọi lại dưới dạng một fn
kiểu. fn
đóng gói các hàm được định nghĩa bởi fn
từ khóa, giống như các con trỏ hàm của C ++:
type Callback = fn();
struct Processor {
callback: Callback,
}
impl Processor {
fn set_callback(&mut self, c: Callback) {
self.callback = c;
}
fn process_events(&self) {
(self.callback)();
}
}
fn simple_callback() {
println!("hello world!");
}
fn main() {
let p = Processor {
callback: simple_callback,
};
p.process_events();
}
Mã này có thể được mở rộng để bao gồm một Option<Box<Any>>
để giữ "dữ liệu người dùng" được liên kết với hàm. Mặc dù vậy, nó sẽ không phải là Rust thành ngữ. Cách Rust để liên kết dữ liệu với một hàm là nắm bắt nó trong một bao đóng ẩn danh , giống như trong C ++ hiện đại. Vì không phải là bao đóng fn
, nên set_callback
sẽ cần phải chấp nhận các loại đối tượng chức năng khác.
Gọi lại dưới dạng các đối tượng hàm chung
Trong cả hai lệnh đóng Rust và C ++ với cùng một chữ ký cuộc gọi có các kích thước khác nhau để chứa các giá trị khác nhau mà chúng có thể nắm bắt. Ngoài ra, mỗi định nghĩa bao đóng tạo ra một kiểu ẩn danh duy nhất cho giá trị của bao đóng. Do những ràng buộc này, struct không thể đặt tên cho kiểu trường của nó callback
, cũng như không thể sử dụng bí danh.
Một cách để nhúng một bao đóng vào trường struct mà không tham chiếu đến một kiểu cụ thể là làm cho struct chung chung . Cấu trúc sẽ tự động điều chỉnh kích thước của nó và loại lệnh gọi lại cho hàm hoặc hàm cụ thể mà bạn chuyển cho nó:
struct Processor<CB>
where
CB: FnMut(),
{
callback: CB,
}
impl<CB> Processor<CB>
where
CB: FnMut(),
{
fn set_callback(&mut self, c: CB) {
self.callback = c;
}
fn process_events(&mut self) {
(self.callback)();
}
}
fn main() {
let s = "world!".to_string();
let callback = || println!("hello {}", s);
let mut p = Processor { callback: callback };
p.process_events();
}
Như trước đây, định nghĩa mới của callback sẽ có thể chấp nhận các hàm cấp cao nhất được xác định bằng fn
, nhưng định nghĩa này cũng sẽ chấp nhận các bao đóng || println!("hello world!")
cũng như các bao đóng nắm bắt các giá trị, chẳng hạn như || println!("{}", somevar)
. Bởi vì điều này, bộ xử lý không cần userdata
phải đi kèm với lệnh gọi lại; sự đóng cửa được cung cấp bởi người gọi set_callback
sẽ tự động nắm bắt dữ liệu mà nó cần từ môi trường của nó và có sẵn nó khi được gọi.
Nhưng giải quyết vấn đề là gì FnMut
, tại sao không chỉ Fn
? Vì các bao đóng giữ các giá trị đã được bắt, các quy tắc đột biến thông thường của Rust phải áp dụng khi gọi bao đóng. Tùy thuộc vào những gì mà các bao đóng làm với các giá trị mà chúng giữ, chúng được nhóm lại thành ba họ, mỗi họ được đánh dấu bằng một đặc điểm:
Fn
là các bao đóng chỉ đọc dữ liệu và có thể được gọi an toàn nhiều lần, có thể từ nhiều luồng. Cả hai cách đóng cửa trên đều được Fn
.
FnMut
là các bao đóng sửa đổi dữ liệu, ví dụ bằng cách ghi vào một mut
biến đã được capture . Chúng cũng có thể được gọi nhiều lần, nhưng không song song. (Gọi một bao FnMut
đóng từ nhiều luồng sẽ dẫn đến một cuộc chạy đua dữ liệu, vì vậy nó chỉ có thể được thực hiện với sự bảo vệ của mutex.) Đối tượng bao đóng phải được người gọi khai báo là có thể thay đổi được.
FnOnce
là các bao đóng sử dụng một số dữ liệu mà chúng thu thập được, ví dụ như bằng cách di chuyển một giá trị đã chụp sang một hàm có quyền sở hữu nó. Như tên của nó, chúng chỉ có thể được gọi một lần và người gọi phải sở hữu chúng.
Hơi phản trực giác, khi chỉ định một đặc điểm bị ràng buộc cho loại đối tượng chấp nhận một bao đóng, FnOnce
thực sự là một đặc điểm dễ dãi nhất. Tuyên bố rằng một kiểu gọi lại chung phải thỏa mãn FnOnce
đặc điểm có nghĩa là nó sẽ chấp nhận mọi sự đóng lại theo nghĩa đen. Nhưng điều đó đi kèm với một cái giá: nó có nghĩa là chủ sở hữu chỉ được phép gọi nó một lần. Vì process_events()
có thể chọn gọi lại nhiều lần và vì bản thân phương thức này có thể được gọi nhiều lần, nên giới hạn dễ chấp nhận nhất tiếp theo là FnMut
. Lưu ý rằng chúng tôi phải đánh dấu process_events
là đột biến self
.
Gọi lại không chung chung: đối tượng đặc điểm hàm
Mặc dù việc triển khai chung của lệnh gọi lại là cực kỳ hiệu quả, nó có những hạn chế nghiêm trọng về giao diện. Nó yêu cầu mỗi Processor
phiên bản phải được tham số hóa với một kiểu gọi lại cụ thể, có nghĩa là một phiên bản Processor
chỉ có thể xử lý một kiểu gọi lại duy nhất. Cho rằng mỗi bao đóng có một kiểu riêng biệt, chung chung Processor
không thể xử lý proc.set_callback(|| println!("hello"))
theo sau bởi proc.set_callback(|| println!("world"))
. Việc mở rộng cấu trúc để hỗ trợ hai trường gọi lại sẽ yêu cầu toàn bộ cấu trúc phải được tham số hóa thành hai loại, điều này sẽ nhanh chóng trở nên khó sử dụng khi số lượng lệnh gọi lại tăng lên. Việc thêm nhiều tham số kiểu hơn sẽ không hoạt động nếu số lượng lệnh gọi lại cần động, ví dụ: để triển khai một add_callback
hàm duy trì một vectơ của các lệnh gọi lại khác nhau.
Để loại bỏ tham số kiểu, chúng ta có thể tận dụng các đối tượng đặc điểm , tính năng của Rust cho phép tự động tạo giao diện động dựa trên các đặc điểm. Điều này đôi khi được gọi là xóa kiểu và là một kỹ thuật phổ biến trong C ++ [1] [2] , không nên nhầm lẫn với cách sử dụng thuật ngữ có phần khác nhau của ngôn ngữ FP và ngôn ngữ FP. Người đọc quen thuộc với C ++ sẽ nhận ra sự khác biệt giữa một bao đóng thực hiện Fn
và một Fn
đối tượng đặc điểm tương đương với sự phân biệt giữa các đối tượng hàm tổng quát và std::function
các giá trị trong C ++.
Một đối tượng đặc điểm được tạo ra bằng cách mượn một đối tượng với &
toán tử và ép hoặc ép đối tượng đó tham chiếu đến đặc điểm cụ thể. Trong trường hợp này, vì Processor
cần sở hữu đối tượng gọi lại, chúng ta không thể sử dụng phương thức mượn, mà phải lưu trữ lệnh gọi lại trong một heap được phân bổ Box<dyn Trait>
(tương đương với Rust std::unique_ptr
), có chức năng tương đương với một đối tượng đặc điểm.
Nếu Processor
lưu trữ Box<dyn FnMut()>
, nó không cần phải là chung nữa, nhưng set_callback
phương thức bây giờ chấp nhận một chung c
thông qua một impl Trait
đối số . Như vậy, nó có thể chấp nhận bất kỳ loại có thể gọi nào, bao gồm cả các bao đóng với trạng thái, và đóng hộp đúng cách trước khi lưu trữ trong Processor
. Đối số chung để set_callback
không giới hạn loại gọi lại mà bộ xử lý chấp nhận, vì loại gọi lại được chấp nhận được tách ra khỏi loại được lưu trữ trong Processor
cấu trúc.
struct Processor {
callback: Box<dyn FnMut()>,
}
impl Processor {
fn set_callback(&mut self, c: impl FnMut() + 'static) {
self.callback = Box::new(c);
}
fn process_events(&mut self) {
(self.callback)();
}
}
fn simple_callback() {
println!("hello");
}
fn main() {
let mut p = Processor {
callback: Box::new(simple_callback),
};
p.process_events();
let s = "world!".to_string();
let callback2 = move || println!("hello {}", s);
p.set_callback(callback2);
p.process_events();
}
Thời gian tồn tại của các tham chiếu bên trong đóng hộp
Các 'static
đời bị ràng buộc vào loại những c
lập luận được chấp nhận bởi set_callback
là một cách đơn giản để thuyết phục các trình biên dịch rằng tài liệu tham khảo chứa trong c
, đó có thể là một kết thúc nó liên quan tới môi trường của nó, chỉ đề cập đến giá trị toàn cầu và do đó sẽ vẫn có hiệu lực trên toàn việc sử dụng các gọi lại. Nhưng giới hạn tĩnh cũng rất nặng nề: trong khi nó chấp nhận các bao đóng sở hữu các đối tượng tốt (mà chúng tôi đã đảm bảo ở trên bằng cách thực hiện đóng move
), nó từ chối các bao đóng tham chiếu đến môi trường cục bộ, ngay cả khi chúng chỉ tham chiếu đến các giá trị tồn tại lâu hơn bộ xử lý và trên thực tế sẽ an toàn.
Vì chúng ta chỉ cần các lệnh gọi lại còn sống miễn là bộ xử lý còn sống, nên chúng ta nên cố gắng gắn thời gian tồn tại của chúng với thời gian tồn tại của bộ xử lý, đây là một ràng buộc ít nghiêm ngặt hơn 'static
. Nhưng nếu chúng ta chỉ xóa 'static
giới hạn thời gian tồn tại set_callback
, nó không còn biên dịch nữa. Điều này là do set_callback
tạo một hộp mới và gán nó vào callback
trường được định nghĩa là Box<dyn FnMut()>
. Vì định nghĩa không chỉ định thời gian tồn tại cho đối tượng đặc điểm được đóng hộp 'static
, nên việc gán này sẽ mở rộng thời gian tồn tại một cách hiệu quả (từ thời gian tồn tại tùy ý không được đặt tên của lệnh gọi lại tới 'static
), điều này không được phép. Cách khắc phục là cung cấp thời gian tồn tại rõ ràng cho bộ xử lý và gắn thời gian tồn tại đó với cả các tham chiếu trong hộp và các tham chiếu trong lệnh gọi lại nhận được bởi set_callback
:
struct Processor<'a> {
callback: Box<dyn FnMut() + 'a>,
}
impl<'a> Processor<'a> {
fn set_callback(&mut self, c: impl FnMut() + 'a) {
self.callback = Box::new(c);
}
}
Với những vòng đời này đã được thực hiện rõ ràng, nó không còn cần thiết để sử dụng 'static
. Việc đóng cửa bây giờ có thể tham chiếu đến s
đối tượng cục bộ , tức là không còn phải như vậy nữa move
, miễn là định nghĩa của s
được đặt trước định nghĩa của p
để đảm bảo rằng chuỗi tồn tại lâu hơn bộ xử lý.
CB
phải có'static
trong ví dụ cuối cùng?