Rust 指针与其对应的C样式指针

翻译来源

https://github.com/diwic/reffers-rs/blob/master/docs/Pointers.md

前言

若你是一名C程序员,你经常用指针,在Rust中有什么替代方案呢?

引用

通常Rust中使用不可变指针&和可变指针&mut来替代指针,如果你不了解上面的话请阅读

指针通常被用来进行函数调用,局部变量访问,偶尔用来返回引用,例如下面的代码:

impl Foo {
    fn get_bar(&self) -> &Bar { &self.bar }
}

新型free接口

如下是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 的内部变量都是私有的。也不必将FooBox 包装起来,因为这意味着将Foo分配在堆上。Box
通常是用来指向闭包(closures)和trait的。

struct中的引用

下面的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 charsigned char对应着Rust中的u8 ,因为Rust中的char指代的是Unicode。此外unsigned char *也不是对应&str 而是&[u8]Vec<u8>

引用其他struct中的变量也是不被允许的,此时你需要记录索引

pub struct MyFileFormat {
    file_contents: Vec<u8>,
    data: usize, /* data part of file is at file_contents[data..] */
}

Rc (Arc)

当需要多次引用生命周期较长的struct时,你需要你用Rc(Arc,线程安全版本)。当然注意不要循环引用。你也很快会发现Rc
中的内容是不可修改的,你只能通过它获得不可变引用。

为了补救,RefCell所指向的内容是允许修改的,即使它自身是不可变的。你可能会遇见Rc<RefCell<T>>RefCell本身不可变,其指向的内容可变,但也有缺点,如下的代码可能会有问题:

struct Wheel {
    bicycle: Rc<RefCell<Bicycle>>,
    diameter: i32,
}

struct Bicycle {
    wheels: Vec<Wheel>,
    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<Bicycle>,
    diameter: Cell<i32>,
}

struct Bicycle {
    wheels: RefCell<Vec<Wheel>>,
    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);
    }
}

此时避免了可变引用。

两则提示:

  1. 优先使用Cell ,而不是RefCell,但得具体分析。Cell set、get会复制数据,而不是借用,Cell 所能容纳的类型有限。

  2. 当内存有限时,牢记Rc (Arc)需要两个额外的usize空间,RefCell只需要一个。Cell则不需要额外的空间,因为它直接复制了一份。如果有问题可以,参考Rc/RefCell 来进行预先配置。

插曲:Unsafe /原始指针

首先不建议使用原始指针,不仅仅是因为会失去原始指针的保障,还因为unsafe Rust与C有些不同,写出的代码比C还不安全。

一个陷阱是:Rust告诉LLVM编译后端&是不可写的,&mut是唯一的,为了实现
这个机制,Rust希望LLVM 不作优化。这意味着你不可以臆断将&转为&mut,仅因为当前不存在其他引用:这会违反上面的规则,引起未定义行为。可以从documentation for UnsafeCellNomicon获取更多内容。

另一个陷阱是:这里告诉你什么该做什么不该做。不要抱有“我认为。。。我可以。。”除非你十分确定。

通用的版本

回到MyFileFormat,让我们添加一个需求:缓冲区具有最大的灵活性。这将允许API用户指向除文件加载外的其他内容,例如共享内存,库中的资源,API用户在之前的步骤中创建的内容等。一个API用户使用Vec<u8>存储在堆上,另一个使用&[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..] */
}

这个方法还有另一个警告 - 在AsRefDeref之间进行选择。 AsRef是更为正确的解决方案,但是我的经验是,DerefAsRef更经常地执行使用,所以Deref <Target = T>是比AsRef <T>更实际的解决方案。

返回参数

这是一个不同的例子,你可以在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 */ }

猜你喜欢

转载自blog.csdn.net/guiqulaxi920/article/details/78847081