在 Rust 中模拟 C++ 类的功能 在派生类中调用基类被重写的方法第八
上一节,我们实现了重写函数,但还有些细节还没写完,C++ 派生类重新实现的虚函数可以调用基类的实现,有时我们不需要完全重写基类的实现,只需要在基类的实现的基础上做一些小的更改即可。为支持调用基类的实现,我们需要明确指出被调用的函数是位于基类的,如下:
#[class]
pub struct Derive2 : Derive1
{
w: i32,
override fn func2(&self, i: i32) -> i32
{
self.w + Derive1::func2(self, i)
}
override fn func3(&self) -> i32
{
self.w + Derive1::func3(self)
}
}
这里我们用 Derive1:: 前缀来表示我们想要调用基类的实现,如果没有这个前缀,就成了对函数自身的调用。我们开始实现函数:
fn func2_impl1(&self, i: i32) -> i32
{
self.w + (Derive1::VTABLE.func2)(self, i)
}
fn func3_impl1(&self) -> i32
{
self.w + (Derive1::VTABLE.func3)(self)
}
可以看到我们在派生类中对基类方法的调用是通过基类虚表实现的,而不是直接通过调用 Derive1::func2_impl0(self, i) 来实现,因为我们不能够确定 Derive1 有实现 func2 方法,但虚表总会指向一个最近的实现。
最开始我们通过操作符 :: 来识别对基类方法的调用,但 :: 操作符的作用远不止于此,我们刚刚的关注点一直在虚函数上面,而忽略了非虚函数,或者其他类的关联方法,如下:
pub struct Base
{
...
pub fn non_virtual_func(&self, ...) {...}
}
#[class]
pub struct Derive2 : Derive1
{
w: i32,
override fn func2(&self, i: i32) -> i32
{
let v = Vec::new();
v....
let s = String::from("xxxx");
s....
Base::non_virtual_func(self, ...);
self.w + Derive1::func2(self, i)
}
}
这需要我们能够准确识别出那些类是基类,哪些方法是虚方法,为此,我们增加 class_option 系列宏来记录基类信息:
macro_rules! class_option
{
($($func:ident)*, $callback:ident $($params:tt)*) =>
{ $callback!($($func)* $($params)*); };
}
macro_rules! base_class_option
{
($($params:tt)*) => { class_option!(Base $($params)*) };
}
macro_rules! derive1_class_option
{
($($params:tt)*) => { base_class_option!(Derive1 $($params)*) };
}
macro_rules! derive2_class_option
{
($($params:tt)*) => { derive1_class_option!(Derive2 $($params)*) };
}
class_option 系列宏和之前的 vtable_option 系列宏很相似,都是回调模式。有了 class_option 系列宏,我们就可以查询一个类是否是当前类的基类,上面的代码可以转换为下面的形式:
fn func2_impl1(&self, i: i32) -> i32
{
let v = derive1_class_option!(, call_super_func, Vec new);
v....
let s = derive1_class_option!(, call_super_func, String from "xxxx");
s....
derive1_class_option!(, call_super_func, Base non_virtual_func self, ...);
self.w + derive1_class_option!(, call_super_func, Derive1 func2 self, i)
}
我们还有宏 call_super_func 没有定义,现在给出伪代码如下:
macro_rules! call_super_func
{
($($name_list:ident)*,$class_name:ident $func:ident $($params:tt)*) =>
{
// 伪代码
if $name_list.contains(class_name)
{
let vtable_option = concat_idents($class_name.to_lowercase(), _vtable_option);
vtable_option!(, call_class_func, $class_name $func $($params)*)
}
else
{
$class_name::$func($($params)*)
}
}
}
call_super_func 宏需要用过程宏来实现,规则宏无法实现,call_super_func 首先判断待调用的类是否在基类列表中,如果是,则继续调用 vtable_option 系列宏,进行下一步的判断,否则,转换为正常的函数调用,如下:
// derive1_class_option!(, call_super_func,... 宏展开如下:
fn func2_impl1(&self, i: i32) -> i32
{
let v = Vec::new();
v....
let s = String::from("xxxx");
s....
base_vtable_option!(, call_class_func, Base non_virtual_func self, ...);
self.w + derive1_vtable_option!(, call_class_func, Derive1 func2 self, i)
}
现在就剩下宏 call_class_func 了,仍然给出伪代码:
macro_rules! call_class_func
{
($($func_list:ident)*,$class_name:ident $func:ident $($params:tt)*) =>
{
// 伪代码
if $func_list.contains(func)
{
($class_name::VTABLE.$func)($($params)*)
}
else
{
$class_name::$func($($params)*)
}
}
}
call_class_func 宏判断待调用的方法是否为虚方法,如果是,转换为虚表调用,否则,转换为正常调用,如下:
// xxx_vtable_option!(, call_class_func,... 宏展开如下:
fn func2_impl1(&self, i: i32) -> i32
{
let v = Vec::new();
v....
let s = String::from("xxxx");
s....
Base::non_virtual_func(self, ...);
self.w + (Derive1::VTABLE.func2)(self, i)
}
至此我们可以正确处理各种 :: 的函数调用操作了。当然,使用 :: 来调用基类方法,不仅可以在重写方法中用,也可以在新定义的虚方法,甚至非虚方法中使用。
宏和分号
这里我们遇到了 Rust 宏的一个坑,我们之前定义了 vtable_option 系列宏,用于生成和初始化虚表。而在宏 call_super_func 中,我们重用 vtable_option 系列宏用来生成表达式。于是问题出现了。
我来简单描述一下这个问题,是关于宏调用的分号的,Rust 的宏可以有三种调用方式,如下:
println!("hello, world!");
println!["hello, world!"];
println!{"hello, world!"}
一般情况下,三种方式都是等价的,具体用选哪种括号,可以根据使用场景来选择,如果把宏当作函数,一般选择小括号,如果当作数组来用,一般选择中括号,如 vec! 宏。除此之外,一般用大括号。
各种括号的使用也有小小的区别,如上所示,大括号后面不需要分号,但是小括号和中括号有时需要分号,有时不需要。
那么什么时候需要呢?我们来看下面的例子:
macro_rules! def_struct { ($name:ident) => { struct $name {} }; }
def_struct!(A)
首先我定义了一个 def_struct 宏,作用是定义一个空的结构体,然后用宏 def_struct 来定义结构体 A。当我们尝试编译这段代码时,编译器会报错,如下:
error: macros that expand to items must be delimited with braces or followed by a semicolon
--> src/main.rs:12:12
12 | def_struct!(A)
| ^^^
help: change the delimiters to curly braces
12 | def_struct!{A}
| ~ ~
help: add a semicolon
12 | def_struct!(A);
| +
我的理解是用宏来生成函数作用域之外的定义时,如结构体、枚举以及函数等,宏调用需要分号结束。当然也可以改用大括号。
我们再看一个例子:
macro_rules! add { ($left:expr, $right:expr) => { $left + $right }; }
fn main()
{
let i = add!(1, 2); + 3;
println!("i = {}.", i);
}
这里我们用宏来生成表达式的一部分。不需要编译器,我们自己也能看出来,分号在这里并不合适,编译错误如下:
error: leading `+` is not supported
--> src/main.rs:22:25
22 | let i = add!(1, 2); + 3;
| ^ unexpected `+`
help: try removing the `+`
22 - let i = add!(1, 2); + 3;
22 + let i = add!(1, 2); 3;
编译器虽然没有猜出我们的意图,但也指出了分号标志着表达式结束了。当然这两个问题都很好解决,如下:
def_struct!(A);
// 或者 def_struct!{A}
fn main()
{
let i = add!(1, 2) + 3;
println!("i = {}.", i);
}
然而当遇到宏回调时,情况变得复杂了,如下:
macro_rules! test_callback
{
($callback:tt $($params:tt)*) => { $callback! ($($params)*) };
}
...
test_callback!(def_struct A);
fn main()
{
let i = test_callback!(add 1, 2) + 3;
println!("i = {}.", i);
}
在这里,我们根据之前的经验,特别注意了宏调用的分号,但是编译器仍然有意见,如下:
error: macros that expand to items must be delimited with braces or followed by a semicolon
--> src/main.rs:3:51
3 | ($callback:tt $($params:tt)*) => { $callback! ($($params)*) };
| ^^^^^^^^^^^^^
...
11 | test_callback!(def_struct A);
| ---------------------------- in this macro invocation
= note: this error originates in the macro `test_callback` (in Nightly builds, run with -Z macro-backtrace for more info)
help: change the delimiters to curly braces
3 | ($callback:tt $($params:tt)*) => { $callback! {} };
help: add a semicolon
3 | ($callback:tt $($params:tt)*) => { $callback! ($($params)*); };
本来我们以为 test_callback 宏调用加了分号就可以了,没想到编译器要求内部的宏调用也必须加分号,我们也只好乖乖听话,为内部的宏调用加上分号:
macro_rules! test_callback
{
($callback:tt $($params:tt)*) => { $callback! ($($params)*); };
}
但是编译器仍然没有放过我们,又有了下面的警告:
warning: trailing semicolon in macro used in expression position
--> src/main.rs:3:64
3 | ($callback:tt $($params:tt)*) => { $callback! ($($params)*); };
| ^
...
20 | let i = test_callback!(add 1, 2) + 3;
| ------------------------ in this macro invocation
= note: `#[warn(semicolon_in_expressions_from_macros)]` on by default
= warning: this was previously accepted by the compiler but is being phased out; it will become a hard error in a future release!
= note: for more information, see issue #79813 <https://github.com/rust-lang/rust/issues/79813>
= note: this warning originates in the macro `test_callback` (in Nightly builds, run with -Z macro-backtrace for more info)
编译器认为我们在宏内部调用其它宏不应该加分号,而我们也认可当前场景中分号的存在确实多余。但是加分号不行,不加分号也不行,我们该怎么办呢?
我们希望宏 test_callback 即能用于生成函数域的表达式的一部分,也用于能生成函数域之外的定义。可是如果需要将 test_callback 宏在不同的场景下拆分为两个,那么意味着之前我们设计的 vtable_option 系列宏都将面临拆分的问题。
当然了,这只是一个警告,不影响编译,但警告通常意味着代码存在着隐患,所以良好的代码也应该从 0 警告开始。而且编译器还说,不排除将来将该警告升级为错误。
问题的解决
我们注意到,之前编译器报错时曾建议我们用大括号:
help: change the delimiters to curly braces
3 | ($callback:tt $($params:tt)*) => { $callback! {} };
我们来试一下:
macro_rules! test_callback
{
($callback:tt $($params:tt)*) => { $callback! {$($params)*} };
}
编译通过,没有错误和警告。问题解决。
结论就是,用大括号在宏调用中具有最广泛的适应性。当我们需要一个用于各种场合的回调宏时,大括号是唯一的选择。
宏调用分号的坑已经填好了,我们将在下一节实现将函数调用 String::from("xxxx") 转换为宏调用 derive1_class_option!(, call_super_func, String from "xxxx") 的代码。
真好呢
哈哈哈,写的太好了https://www.lawjida.com/
哈哈哈,写的太好了https://www.lawjida.com/