【在主画面加入捷径】
       
【选择语系】
繁中 简中

Rust 程序设计教学:所有权 (Ownership)

【赞助商连结】

    所有权 (ownership) 是 Rust 的核心概念之一,在许多主流语言中没有强调所有权的观念,而 Rust 从一开始就放入这个概念,Rust 的安全性和所有权的概念息息相关,但是,这也是 Rust 难以上手的原因。虽然 Rust 官方文件没有强调参考 (reference) 的概念,不过,了解参考,对于了解所有权有相当的帮助。

    注:Rust 的参考类似 C/C++ 的指针。

    指针与参考

    指针 (pointer) 本身的值不是数据,而是指向另一个数据的内存位置。如下图:

    指针

    由于指针本身储存整数,该整数代表数据在内存中的位置。透过传递指针,不需要拷贝整个原始数据,若数据量较大时,传递指针可使得程序更有效率。

    不同语言对指针的处理方式大异其趣,C 或 C++ 给使用者很大的自由,让使用者自行操作指针,而许多高阶语言将指针隐藏起来,使用者完全不会碰触到指针。Rust 则介于两者之间,虽然平常不需要指针,需要时,Rust 让使用者有操作指针的自由。

    以下是一个 C 语言的例子:

    以 Rust 撰写类似的代码:

    在本程序中,n_ref 的类型是 & i32,意思是「指向 i32 类型的参考 (reference)」,但其观念上相当接近指针。

    以下是另一个 C++ 中使用指针的例子:

    在本程序中,我们取得 vec 的元素的指针,解址取得其值,处理后再存回该元素中。以上例子改写成类似的 Rust 程序代码如下:

    同样地,我们得到该 vector 的参考,解参考取得其值,处理后再存回该元素中。

    接下来,我们开始探讨 Rust 的所有权。

    所有权

    C 和 C++ 给使用者较大的自由,但对某些指针使用造成的问题,没有特定的规范。我们来看一个 C 语言的迷途指针 (dangling pointer) 的例子:

    简单地说,a 和 a1 指向同一块内存区块,在释放 a 的内存后,a1 变成迷途指针 (dangling pointer)。在 C 或 C++ 中,没有规范如何处理这样的行为,而 Rust 以 所有权 (ownership) 来处理这个问题。见以下程序代码:

    这样的程序代码会引发错误。在本程序中,为了避免 vv1 存取同一块内存后,因释放内存而造成迷途指针的问题,Rust 将 v 的所有权转移到 v1,来避免这个问题发生。在 Rust 中,每份数据在同一个时间只会有一个拥有者 (owner);而程序设计者需主动地控制数据的所有权,某种程度造成 Rust 的门槛。

    然而,以下的程序却可正常运行:

    这是因为 Rust 将 x 的值拷贝一份到 x1,故不会有前述问题发生。在 Rust 中,有些内建类型在传递时会自动拷贝一份,以避开所有权的议题,对于其他类型,则要明确地转移或拷贝数据。

    在使用函式时,也会发生同样的状况。见以下程序:

    在本程序中,即使我们对 v 没有进行任何实质的操作,仍然发生了所有权转移的问题。为了处理上述问题,Rust 引入 borrowing 的机制,也就是我们下文要讨论的内容。

    Borrowing

    承接上节的内容,我们来看一个 borrowing 的例子:

    在本程序中,Rust 将 v 的所有权暂时借给 sum 之中,待函式运行结束后,再将所有权转回 v,使得所有权的机制可正常运行。在其他语言中,也有类似的概念,像是 C++ 的参考 (reference)。将以上程序以 C++ 重新改写如下:

    在本程序中,我们没有拷贝整个 vec,而是将其位址传入 sum。不过,在 C++ 程序中,并没有强调所有权的概念。

    注:C++ 的参考和 Rust 的参考是不同的概念。

    结合我们先前谈的可变性的概念,如果我们要在转移参数所有权后修改其值,必需要明确地指定可变性。例如,以下的程序会引发错误:

    若将程序进行适当的修改,则可正确执行。范例如下:

    虽然以上程序可正确执行,但却不是一个良好的模式,因为这个程序对 vec 造成了副作用 (side effect),也就是说,这个程序会更动 vec 的状态。当然,并不是绝对不能用这样的方式写程序,只是,要思考一下,这样子的效果是否是自己想要的。

    Lifetime

    Lifetime 所要处理的问题

    假设以下的情形:

    1. A 取得资源
    2. A 将资源的所有权借给 B
    3. A 将资源释放掉
    4. B 欲取得资源,造成程序错误

    而 Rust 透过 lifetime 避免以上问题。如以下范例:

    在本程序中,yn 借得所有权后,将其转给 x。但在该区块结束后,n 的 lifetime 已经结束,实质上已经无法取得 n,而 Rust 侦测到这个问题并在编译程序时引发相关的错误。然而,在 C 或 C++,却没有规范上述行为,见以下范例:

    笔者实测,此程序印出 0,但不同电脑上,可能结果不同,而程序设计者不应依赖其结果。由此例可见 Rust 和 C 或 C++ 在设计上的相异点。

    指定 Lifetime

    其实在撰写函式时,也隐藏着 lifetime 的概念。像是以下的函式:

    若明确指定 lifetime 则变成:

    若参数是可变的,则变成:

    其中的 'a 是一个代称,代表的是 foo函数的 lifetime,而 'a 不是固定的,可以换成其他的字。由于我们到目前为止,都没有明确写出 lifetime,读者可能会感到困惑,这是由于 Rust 自动推断 lifetime 的功能 (lifetime elision),藉此减少使用者输入。

    如果 struct 内的属性有参考,也要明确指定 lifetime,如下:

    Rust 的 lifetime 语法相对难读,某种程度也受到使用者的批评;随着 Rust 进入 1.0 版后,不太可能把 lifetime 的语法修掉,基本上只能多练习来适应这个语法。

    static

    static 是一个特别的 lifetime 修饰,表示该变量的 lifetime 为整个程序。如下:

    也可用在常数,如下:

    【赞助商连结】