https://github.com/diwic/reffers-rs/blob/master/docs/Pointers.md
若你是一名C程序员,你经常用指针,在Rust中有什么替代方案呢?
通常Rust中使用不可变指针&
和可变指针&mut
来替代指针,如果你不了解上面的话请阅读书。
指针通常被用来进行函数调用,局部变量访问,偶尔用来返回引用,例如下面的代码:
impl Foo {
fn get_bar(&self) -> &Bar { &self.bar }
}
如下是C API:
typedef struct Foo Foo;
Foo* foo_new(void);
foo_dosomething(Foo*);
foo_free(Foo*);
当我们尝试将上面的代码翻译成Rust时,我们发现无法从Foo::new
返回&mut Foo
,通常解决方案如下:
pub struct Foo { /* fields */ };
impl Foo {
pub fn new() -> Foo { /* code */ }
pub fn dosomething(&mut self) { /* code */ }
}
impl Drop for Foo {
fn drop(&mut self) { /* code for foo_free goes here, if any */ }
}
无需以抽象为目的将Foo
藏在指针里,因为struct 的内部变量都是私有的。也不必将Foo
用Box
包装起来,因为这意味着将Foo
分配在堆上。Box
通常是用来指向闭包(closures)和trait的。
下面的C 代码无任何问题:
struct Message {
Connection* conn; /* stores a reference to conn, but does not own it */
};
struct Server {
Message* message; /* owns the message */
};
Rust 中的形式显得略微笨拙,他们需要通过借用检查。你需要添加'a
来表明生命周期,像下面这样:
pub struct Message<'a> {
conn: &'a Connection,
}
pub struct Server<'a> {
message: Message<'a>,
}
如你所见,不仅Message
有'a
,任何包含Message的struct也需要'a
。
不仅如此,你还需要告诉编译器,Connection
不会被释放,修改和移动( destroyed, mutated or moved)。实际操作很难处理,我的经验是,带生命周期的struct通常为短期存在的struct,如iterator。
试想你开发一个文件struct—-MyFileFormat ,它有文件的内容file_contents
,和真实文件的开始位置data
,初始化之后data
指向数据的开始位置。C语言如下:
struct MyFileFormat {
unsigned char* file_contents; /* owned by MyFileFormat */
unsigned char* data; /* a pointer to somewhere inside file_contents */
};
直接翻译成Rust并不能编译:
pub struct MyFileFormat {
pub file_contents: Vec<u8>,
pub data: &[u8],
}
注:C中 unsigned char
和 signed char
对应着Rust中的u8
,因为Rust中的char
指代的是Unicode。此外unsigned char *
也不是对应&str
而是&[u8]
或Vec
。
引用其他struct中的变量也是不被允许的,此时你需要记录索引
pub struct MyFileFormat {
file_contents: Vec<u8>,
data: usize, /* data part of file is at file_contents[data..] */
}
当需要多次引用生命周期较长的struct时,你需要你用Rc(Arc,线程安全版本)。当然注意不要循环引用。你也很快会发现Rc
中的内容是不可修改的,你只能通过它获得不可变引用。
为了补救,RefCell所指向的内容是允许修改的,即使它自身是不可变的。你可能会遇见Rc
,RefCell
本身不可变,其指向的内容可变,但也有缺点,如下的代码可能会有问题:
struct Wheel {
bicycle: Rc>,
diameter: i32,
}
struct Bicycle {
wheels: Vec,
size: i32,
}
impl Bicycle {
pub fn inflate(&mut self) {
self.size += 1;
for w in &mut self.wheels {
w.adjust_diameter();
}
}
}
impl Wheel {
pub fn adjust_diameter(&mut self) {
self.diameter = self.bicycle.borrow().size / 2;
}
}
Bicycle::inflate
需要RefCell
的内容为可变的借用(&mut self
)
adjust_diameter
又可变 借用了bicycle
,但是它已经被借用了,此时你的程序会出错。这仅是一个简单的例子,现实情况会更加复杂。所以避免使用RefCell
或Cell
,上述代码修改如下:
struct Wheel {
bicycle: Rc,
diameter: Cell<i32>,
}
struct Bicycle {
wheels: RefCell>,
size: Cell<i32>,
}
impl Bicycle {
pub fn inflate(&self) {
self.size.set(self.size.get() + 1);
for w in &*self.wheels.borrow() {
w.adjust_diameter();
}
}
}
impl Wheel {
pub fn adjust_diameter(&self) {
self.diameter.set(self.bicycle.size.get() / 2);
}
}
此时避免了可变引用。
两则提示:
Cell
,而不是RefCell
,但得具体分析。Cell
set、get会复制数据,而不是借用,Cell
所能容纳的类型有限。Rc (Arc)
需要两个额外的usize
空间,RefCell
只需要一个。Cell
则不需要额外的空间,因为它直接复制了一份。如果有问题可以,参考Rc/RefCell 来进行预先配置。首先不建议使用原始指针,不仅仅是因为会失去原始指针的保障,还因为unsafe Rust与C有些不同,写出的代码比C还不安全。
一个陷阱是:Rust告诉LLVM编译后端&
是不可写的,&mut
是唯一的,为了实现
这个机制,Rust希望LLVM 不作优化。这意味着你不可以臆断将&
转为&mut
,仅因为当前不存在其他引用:这会违反上面的规则,引起未定义行为。可以从documentation for UnsafeCell 或Nomicon获取更多内容。
另一个陷阱是:这里告诉你什么该做什么不该做。不要抱有“我认为。。。我可以。。”除非你十分确定。
回到MyFileFormat
,让我们添加一个需求:缓冲区具有最大的灵活性。这将允许API用户指向除文件加载外的其他内容,例如共享内存,库中的资源,API用户在之前的步骤中创建的内容等。一个API用户使用Vec
存储在堆上,另一个使用&[u8]
存储在栈上。若文件过大,此时,就需要避免会进行内存复制。而在C中,你只需记录指针,不必考虑释放问题。
在Rust中,你想轻松使用&[u8]
,但是你不能写&[u8]
(因为强引用不被允许),这时你可以使用 AsRef(可变版本AsMut),修改如下:
pub struct MyFileFormat<T: AsRef<[u8]>> {
file_contents: T,
data: usize, /* data part of file is at file_contents.as_ref()[data..] */
}
这个方法还有另一个警告 - 在AsRef
和Deref
之间进行选择。 AsRef
是更为正确的解决方案,但是我的经验是,Deref
比AsRef
更经常地执行使用,所以Deref
是比AsRef
更实际的解决方案。
这是一个不同的例子,你可以在C中使用一个指针。如果你有一个计算商和余数的函数,那么在C中做这个的一个常见模式是:
long long divide(double dividend, double divisor, double* remainder);
别忘了检查指针是否为NULL.在Rust中只需要用tuple,
// Returns a tuple of (quotient, remainder)
pub fn divide(dividend: f64, divisor: f64) -> (i64, f64) { /* code */ }
另一个 GLib中相似的例子,用于返回错误信息的指针,
gint g_file_open_tmp(const gchar* tmpl, gchar **name_used, GError **error);
在Rust中返回Result
即可
/// Returns a tuple of (handle, name_used) on success or an error otherwise.
GFile::open_tmp(tmpl: &Path) -> Result<(i32, String), GError> { /* code */ }