99re热这里只有精品视频,7777色鬼xxxx欧美色妇,国产成人精品一区二三区在线观看,内射爽无广熟女亚洲,精品人妻av一区二区三区

Rust 什么是所有權(quán)?

2023-03-22 15:09 更新
ch04-01-what-is-ownership.md
commit 9c9a522555c05cae6717adfbb419af58ebd1cea0

Rust 的核心功能(之一)是 所有權(quán)ownership)。雖然該功能很容易解釋?zhuān)鼘?duì)語(yǔ)言的其他部分有著深刻的影響。

所有程序都必須管理其運(yùn)行時(shí)使用計(jì)算機(jī)內(nèi)存的方式。一些語(yǔ)言中具有垃圾回收機(jī)制,在程序運(yùn)行時(shí)有規(guī)律地尋找不再使用的內(nèi)存;在另一些語(yǔ)言中,程序員必須親自分配和釋放內(nèi)存。Rust 則選擇了第三種方式:通過(guò)所有權(quán)系統(tǒng)管理內(nèi)存,編譯器在編譯時(shí)會(huì)根據(jù)一系列的規(guī)則進(jìn)行檢查。如果違反了任何這些規(guī)則,程序都不能編譯。在運(yùn)行時(shí),所有權(quán)系統(tǒng)的任何功能都不會(huì)減慢程序。

因?yàn)樗袡?quán)對(duì)很多程序員來(lái)說(shuō)都是一個(gè)新概念,需要一些時(shí)間來(lái)適應(yīng)。好消息是隨著你對(duì) Rust 和所有權(quán)系統(tǒng)的規(guī)則越來(lái)越有經(jīng)驗(yàn),你就越能自然地編寫(xiě)出安全和高效的代碼。持之以恒!

當(dāng)你理解了所有權(quán),你將有一個(gè)堅(jiān)實(shí)的基礎(chǔ)來(lái)理解那些使 Rust 獨(dú)特的功能。在本章中,你將通過(guò)完成一些示例來(lái)學(xué)習(xí)所有權(quán),這些示例基于一個(gè)常用的數(shù)據(jù)結(jié)構(gòu):字符串。

棧(Stack)與堆(Heap)

在很多語(yǔ)言中,你并不需要經(jīng)??紤]到棧與堆。不過(guò)在像 Rust 這樣的系統(tǒng)編程語(yǔ)言中,值是位于棧上還是堆上在更大程度上影響了語(yǔ)言的行為以及為何必須做出這樣的抉擇。我們會(huì)在本章的稍后部分描述所有權(quán)與棧和堆相關(guān)的內(nèi)容,所以這里只是一個(gè)用來(lái)預(yù)熱的簡(jiǎn)要解釋。

棧和堆都是代碼在運(yùn)行時(shí)可供使用的內(nèi)存,但是它們的結(jié)構(gòu)不同。棧以放入值的順序存儲(chǔ)值并以相反順序取出值。這也被稱作 后進(jìn)先出last in, first out)。想象一下一疊盤(pán)子:當(dāng)增加更多盤(pán)子時(shí),把它們放在盤(pán)子堆的頂部,當(dāng)需要盤(pán)子時(shí),也從頂部拿走。不能從中間也不能從底部增加或拿走盤(pán)子!增加數(shù)據(jù)叫做 進(jìn)棧pushing onto the stack),而移出數(shù)據(jù)叫做 出棧popping off the stack)。棧中的所有數(shù)據(jù)都必須占用已知且固定的大小。在編譯時(shí)大小未知或大小可能變化的數(shù)據(jù),要改為存儲(chǔ)在堆上。 堆是缺乏組織的:當(dāng)向堆放入數(shù)據(jù)時(shí),你要請(qǐng)求一定大小的空間。內(nèi)存分配器(memory allocator)在堆的某處找到一塊足夠大的空位,把它標(biāo)記為已使用,并返回一個(gè)表示該位置地址的 指針pointer)。這個(gè)過(guò)程稱作 在堆上分配內(nèi)存allocating on the heap),有時(shí)簡(jiǎn)稱為 “分配”(allocating)。(將數(shù)據(jù)推入棧中并不被認(rèn)為是分配)。因?yàn)橹赶蚍湃霔V袛?shù)據(jù)的指針是已知的并且大小是固定的,你可以將該指針存儲(chǔ)在棧上,不過(guò)當(dāng)需要實(shí)際數(shù)據(jù)時(shí),必須訪問(wèn)指針。想象一下去餐館就座吃飯。當(dāng)進(jìn)入時(shí),你說(shuō)明有幾個(gè)人,餐館員工會(huì)找到一個(gè)夠大的空桌子并領(lǐng)你們過(guò)去。如果有人來(lái)遲了,他們也可以通過(guò)詢問(wèn)來(lái)找到你們坐在哪。

入棧比在堆上分配內(nèi)存要快,因?yàn)椋ㄈ霔r(shí))分配器無(wú)需為存儲(chǔ)新數(shù)據(jù)去搜索內(nèi)存空間;其位置總是在棧頂。相比之下,在堆上分配內(nèi)存則需要更多的工作,這是因?yàn)榉峙淦鞅仨毷紫日业揭粔K足夠存放數(shù)據(jù)的內(nèi)存空間,并接著做一些記錄為下一次分配做準(zhǔn)備。

訪問(wèn)堆上的數(shù)據(jù)比訪問(wèn)棧上的數(shù)據(jù)慢,因?yàn)楸仨毻ㄟ^(guò)指針來(lái)訪問(wèn)?,F(xiàn)代處理器在內(nèi)存中跳轉(zhuǎn)越少就越快(緩存)。繼續(xù)類(lèi)比,假設(shè)有一個(gè)服務(wù)員在餐廳里處理多個(gè)桌子的點(diǎn)菜。在一個(gè)桌子報(bào)完所有菜后再移動(dòng)到下一個(gè)桌子是最有效率的。從桌子 A 聽(tīng)一個(gè)菜,接著桌子 B 聽(tīng)一個(gè)菜,然后再桌子 A,然后再桌子 B 這樣的流程會(huì)更加緩慢。出于同樣原因,處理器在處理的數(shù)據(jù)彼此較近的時(shí)候(比如在棧上)比較遠(yuǎn)的時(shí)候(比如可能在堆上)能更好的工作。

當(dāng)你的代碼調(diào)用一個(gè)函數(shù)時(shí),傳遞給函數(shù)的值(包括可能指向堆上數(shù)據(jù)的指針)和函數(shù)的局部變量被壓入棧中。當(dāng)函數(shù)結(jié)束時(shí),這些值被移出棧。

跟蹤哪部分代碼正在使用堆上的哪些數(shù)據(jù),最大限度的減少堆上的重復(fù)數(shù)據(jù)的數(shù)量,以及清理堆上不再使用的數(shù)據(jù)確保不會(huì)耗盡空間,這些問(wèn)題正是所有權(quán)系統(tǒng)要處理的。一旦理解了所有權(quán),你就不需要經(jīng)??紤]棧和堆了,不過(guò)明白了所有權(quán)的主要目的就是為了管理堆數(shù)據(jù),能夠幫助解釋為什么所有權(quán)要以這種方式工作。

所有權(quán)規(guī)則

首先,讓我們看一下所有權(quán)的規(guī)則。當(dāng)我們通過(guò)舉例說(shuō)明時(shí),請(qǐng)謹(jǐn)記這些規(guī)則:

  1. Rust 中的每一個(gè)值都有一個(gè) 所有者owner)。
  2. 值在任一時(shí)刻有且只有一個(gè)所有者。
  3. 當(dāng)所有者(變量)離開(kāi)作用域,這個(gè)值將被丟棄。

變量作用域

既然我們已經(jīng)掌握了基本語(yǔ)法,將不會(huì)在之后的例子中包含 fn main() { 代碼,所以如果你是一路跟過(guò)來(lái)的,必須手動(dòng)將之后例子的代碼放入一個(gè) main 函數(shù)中。這樣,例子將顯得更加簡(jiǎn)明,使我們可以關(guān)注實(shí)際細(xì)節(jié)而不是樣板代碼。

在所有權(quán)的第一個(gè)例子中,我們看看一些變量的 作用域scope)。作用域是一個(gè)項(xiàng)(item)在程序中有效的范圍。假設(shè)有這樣一個(gè)變量:

let s = "hello";

變量 s 綁定到了一個(gè)字符串字面值,這個(gè)字符串值是硬編碼進(jìn)程序代碼中的。這個(gè)變量從聲明的點(diǎn)開(kāi)始直到當(dāng)前 作用域 結(jié)束時(shí)都是有效的。示例 4-1 中的注釋標(biāo)明了變量 s 在何處是有效的。

    {                      // s 在這里無(wú)效, 它尚未聲明
        let s = "hello";   // 從此處起,s 是有效的

        // 使用 s
    }                      // 此作用域已結(jié)束,s 不再有效

示例 4-1:一個(gè)變量和其有效的作用域

換句話說(shuō),這里有兩個(gè)重要的時(shí)間點(diǎn):

  • 當(dāng) ?s進(jìn)入作用域 時(shí),它就是有效的。
  • 這一直持續(xù)到它 離開(kāi)作用域 為止。

目前為止,變量是否有效與作用域的關(guān)系跟其他編程語(yǔ)言是類(lèi)似的。現(xiàn)在我們?cè)诖嘶A(chǔ)上介紹 ?String ?類(lèi)型。

String 類(lèi)型

為了演示所有權(quán)的規(guī)則,我們需要一個(gè)比第三章 “數(shù)據(jù)類(lèi)型” 中講到的都要復(fù)雜的數(shù)據(jù)類(lèi)型。前面介紹的類(lèi)型都是已知大小的,可以存儲(chǔ)在棧中,并且當(dāng)離開(kāi)作用域時(shí)被移出棧,如果代碼的另一部分需要在不同的作用域中使用相同的值,可以快速簡(jiǎn)單地復(fù)制它們來(lái)創(chuàng)建一個(gè)新的獨(dú)立實(shí)例。不過(guò)我們需要尋找一個(gè)存儲(chǔ)在堆上的數(shù)據(jù)來(lái)探索 Rust 是如何知道該在何時(shí)清理數(shù)據(jù)的。

我們會(huì)專(zhuān)注于 String 與所有權(quán)相關(guān)的部分。這些方面也同樣適用于標(biāo)準(zhǔn)庫(kù)提供的或你自己創(chuàng)建的其他復(fù)雜數(shù)據(jù)類(lèi)型。在第八章會(huì)更深入地講解 String。

我們已經(jīng)見(jiàn)過(guò)字符串字面值,即被硬編碼進(jìn)程序里的字符串值。字符串字面值是很方便的,不過(guò)它們并不適合使用文本的每一種場(chǎng)景。原因之一就是它們是不可變的。另一個(gè)原因是并非所有字符串的值都能在編寫(xiě)代碼時(shí)就知道:例如,要是想獲取用戶輸入并存儲(chǔ)該怎么辦呢?為此,Rust 有第二個(gè)字符串類(lèi)型,String。這個(gè)類(lèi)型管理被分配到堆上的數(shù)據(jù),所以能夠存儲(chǔ)在編譯時(shí)未知大小的文本。可以使用 from 函數(shù)基于字符串字面值來(lái)創(chuàng)建 String,如下:

let s = String::from("hello");

這兩個(gè)冒號(hào) :: 是運(yùn)算符,允許將特定的 from 函數(shù)置于 String 類(lèi)型的命名空間(namespace)下,而不需要使用類(lèi)似 string_from 這樣的名字。在第五章的 “方法語(yǔ)法”(“Method Syntax”) 部分會(huì)著重講解這個(gè)語(yǔ)法而且在第七章的 “路徑用于引用模塊樹(shù)中的項(xiàng)” 中會(huì)講到模塊的命名空間。

可以 修改此類(lèi)字符串 :

    let mut s = String::from("hello");

    s.push_str(", world!"); // push_str() 在字符串后追加字面值

    println!("{}", s); // 將打印 `hello, world!`

那么這里有什么區(qū)別呢?為什么 String 可變而字面值卻不行呢?區(qū)別在于兩個(gè)類(lèi)型對(duì)內(nèi)存的處理上。

內(nèi)存與分配

就字符串字面值來(lái)說(shuō),我們?cè)诰幾g時(shí)就知道其內(nèi)容,所以文本被直接硬編碼進(jìn)最終的可執(zhí)行文件中。這使得字符串字面值快速且高效。不過(guò)這些特性都只得益于字符串字面值的不可變性。不幸的是,我們不能為了每一個(gè)在編譯時(shí)大小未知的文本而將一塊內(nèi)存放入二進(jìn)制文件中,并且它的大小還可能隨著程序運(yùn)行而改變。

對(duì)于 String 類(lèi)型,為了支持一個(gè)可變,可增長(zhǎng)的文本片段,需要在堆上分配一塊在編譯時(shí)未知大小的內(nèi)存來(lái)存放內(nèi)容。這意味著:

  • 必須在運(yùn)行時(shí)向內(nèi)存分配器(memory allocator)請(qǐng)求內(nèi)存。
  • 需要一個(gè)當(dāng)我們處理完 ?String ?時(shí)將內(nèi)存返回給分配器的方法。

第一部分由我們完成:當(dāng)調(diào)用 String::from 時(shí),它的實(shí)現(xiàn) (implementation) 請(qǐng)求其所需的內(nèi)存。這在編程語(yǔ)言中是非常通用的。

然而,第二部分實(shí)現(xiàn)起來(lái)就各有區(qū)別了。在有 垃圾回收garbage collectorGC)的語(yǔ)言中, GC 記錄并清除不再使用的內(nèi)存,而我們并不需要關(guān)心它。在大部分沒(méi)有 GC 的語(yǔ)言中,識(shí)別出不再使用的內(nèi)存并調(diào)用代碼顯式釋放就是我們的責(zé)任了,跟請(qǐng)求內(nèi)存的時(shí)候一樣。從歷史的角度上說(shuō)正確處理內(nèi)存回收曾經(jīng)是一個(gè)困難的編程問(wèn)題。如果忘記回收了會(huì)浪費(fèi)內(nèi)存。如果過(guò)早回收了,將會(huì)出現(xiàn)無(wú)效變量。如果重復(fù)回收,這也是個(gè) bug。我們需要精確的為一個(gè) allocate 配對(duì)一個(gè) free。

Rust 采取了一個(gè)不同的策略:內(nèi)存在擁有它的變量離開(kāi)作用域后就被自動(dòng)釋放。下面是示例 4-1 中作用域例子的一個(gè)使用 String 而不是字符串字面值的版本:

    {
        let s = String::from("hello"); // 從此處起,s 是有效的

        // 使用 s
    }                                  // 此作用域已結(jié)束,
                                       // s 不再有效

這是一個(gè)將 String 需要的內(nèi)存返回給分配器的很自然的位置:當(dāng) s 離開(kāi)作用域的時(shí)候。當(dāng)變量離開(kāi)作用域,Rust 為我們調(diào)用一個(gè)特殊的函數(shù)。這個(gè)函數(shù)叫做 drop,在這里 String 的作者可以放置釋放內(nèi)存的代碼。Rust 在結(jié)尾的 } 處自動(dòng)調(diào)用 drop

注意:在 C++ 中,這種 item 在生命周期結(jié)束時(shí)釋放資源的模式有時(shí)被稱作 資源獲取即初始化Resource Acquisition Is Initialization (RAII))。如果你使用過(guò) RAII 模式的話應(yīng)該對(duì) Rust 的 ?drop ?函數(shù)并不陌生。

這個(gè)模式對(duì)編寫(xiě) Rust 代碼的方式有著深遠(yuǎn)的影響?,F(xiàn)在它看起來(lái)很簡(jiǎn)單,不過(guò)在更復(fù)雜的場(chǎng)景下代碼的行為可能是不可預(yù)測(cè)的,比如當(dāng)有多個(gè)變量使用在堆上分配的內(nèi)存時(shí)?,F(xiàn)在讓我們探索一些這樣的場(chǎng)景。

變量與數(shù)據(jù)交互的方式(一):移動(dòng)

在Rust 中,多個(gè)變量可以采取不同的方式與同一數(shù)據(jù)進(jìn)行交互。讓我們看看示例 4-2 中一個(gè)使用整型的例子。

    let x = 5;
    let y = x;

示例 4-2:將變量 ?x? 的整數(shù)值賦給 ?y?

我們大致可以猜到這在干什么:“將 5 綁定到 x;接著生成一個(gè)值 x 的拷貝并綁定到 y”?,F(xiàn)在有了兩個(gè)變量,x 和 y,都等于 5。這也正是事實(shí)上發(fā)生了的,因?yàn)檎麛?shù)是有已知固定大小的簡(jiǎn)單值,所以這兩個(gè) 5 被放入了棧中。

現(xiàn)在看看這個(gè) String 版本:

    let s1 = String::from("hello");
    let s2 = s1;

這看起來(lái)與上面的代碼非常類(lèi)似,所以我們可能會(huì)假設(shè)他們的運(yùn)行方式也是類(lèi)似的:也就是說(shuō),第二行可能會(huì)生成一個(gè) s1 的拷貝并綁定到 s2 上。不過(guò),事實(shí)上并不完全是這樣。

看看圖 4-1 以了解 String 的底層會(huì)發(fā)生什么。String 由三部分組成,如圖左側(cè)所示:一個(gè)指向存放字符串內(nèi)容內(nèi)存的指針,一個(gè)長(zhǎng)度,和一個(gè)容量。這一組數(shù)據(jù)存儲(chǔ)在棧上。右側(cè)則是堆上存放內(nèi)容的內(nèi)存部分。

trpl04-01

圖 4-1:將值 ?"hello"? 綁定給 ?s1 ?的 ?String ?在內(nèi)存中的表現(xiàn)形式

長(zhǎng)度表示 String 的內(nèi)容當(dāng)前使用了多少字節(jié)的內(nèi)存。容量是 String 從分配器總共獲取了多少字節(jié)的內(nèi)存。長(zhǎng)度與容量的區(qū)別是很重要的,不過(guò)在當(dāng)前上下文中并不重要,所以現(xiàn)在可以忽略容量。

當(dāng)我們將 s1 賦值給 s2,String 的數(shù)據(jù)被復(fù)制了,這意味著我們從棧上拷貝了它的指針、長(zhǎng)度和容量。我們并沒(méi)有復(fù)制指針指向的堆上數(shù)據(jù)。換句話說(shuō),內(nèi)存中數(shù)據(jù)的表現(xiàn)如圖 4-2 所示。

trpl04-02

圖 4-2:變量 ?s2 ?的內(nèi)存表現(xiàn),它有一份 ?s1 ?指針、長(zhǎng)度和容量的拷貝

這個(gè)表現(xiàn)形式看起來(lái) 并不像 圖 4-3 中的那樣,如果 Rust 也拷貝了堆上的數(shù)據(jù),那么內(nèi)存看起來(lái)就是這樣的。如果 Rust 這么做了,那么操作 s2 = s1 在堆上數(shù)據(jù)比較大的時(shí)候會(huì)對(duì)運(yùn)行時(shí)性能造成非常大的影響。

trpl04-03

圖 4-3:另一個(gè) s2 = s1 時(shí)可能的內(nèi)存表現(xiàn),如果 Rust 同時(shí)也拷貝了堆上的數(shù)據(jù)的話

之前我們提到過(guò)當(dāng)變量離開(kāi)作用域后,Rust 自動(dòng)調(diào)用 drop 函數(shù)并清理變量的堆內(nèi)存。不過(guò)圖 4-2 展示了兩個(gè)數(shù)據(jù)指針指向了同一位置。這就有了一個(gè)問(wèn)題:當(dāng) s2 和 s1 離開(kāi)作用域,他們都會(huì)嘗試釋放相同的內(nèi)存。這是一個(gè)叫做 二次釋放double free)的錯(cuò)誤,也是之前提到過(guò)的內(nèi)存安全性 bug 之一。兩次釋放(相同)內(nèi)存會(huì)導(dǎo)致內(nèi)存污染,它可能會(huì)導(dǎo)致潛在的安全漏洞。

為了確保內(nèi)存安全,在 let s2 = s1 之后,Rust 認(rèn)為 s1 不再有效,因此 Rust 不需要在 s1 離開(kāi)作用域后清理任何東西??纯丛?nbsp;s2 被創(chuàng)建之后嘗試使用 s1 會(huì)發(fā)生什么;這段代碼不能運(yùn)行:

    let s1 = String::from("hello");
    let s2 = s1;

    println!("{}, world!", s1);

你會(huì)得到一個(gè)類(lèi)似如下的錯(cuò)誤,因?yàn)?Rust 禁止你使用無(wú)效的引用。

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0382]: borrow of moved value: `s1`
 --> src/main.rs:5:28
  |
2 |     let s1 = String::from("hello");
  |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 |     let s2 = s1;
  |              -- value moved here
4 | 
5 |     println!("{}, world!", s1);
  |                            ^^ value borrowed here after move

For more information about this error, try `rustc --explain E0382`.
error: could not compile `ownership` due to previous error

如果你在其他語(yǔ)言中聽(tīng)說(shuō)過(guò)術(shù)語(yǔ) 淺拷貝shallow copy)和 深拷貝deep copy),那么拷貝指針、長(zhǎng)度和容量而不拷貝數(shù)據(jù)可能聽(tīng)起來(lái)像淺拷貝。不過(guò)因?yàn)?Rust 同時(shí)使第一個(gè)變量無(wú)效了,這個(gè)操作被稱為 移動(dòng)move),而不是淺拷貝。上面的例子可以解讀為 s1 被 移動(dòng) 到了 s2 中。那么具體發(fā)生了什么,如圖 4-4 所示。

trpl04-04

圖 4-4:s1 無(wú)效之后的內(nèi)存表現(xiàn)

這樣就解決了我們的問(wèn)題!因?yàn)橹挥?nbsp;s2 是有效的,當(dāng)其離開(kāi)作用域,它就釋放自己的內(nèi)存,完畢。

另外,這里還隱含了一個(gè)設(shè)計(jì)選擇:Rust 永遠(yuǎn)也不會(huì)自動(dòng)創(chuàng)建數(shù)據(jù)的 “深拷貝”。因此,任何 自動(dòng) 的復(fù)制可以被認(rèn)為對(duì)運(yùn)行時(shí)性能影響較小。

變量與數(shù)據(jù)交互的方式(二):克隆

如果我們 確實(shí) 需要深度復(fù)制 String 中堆上的數(shù)據(jù),而不僅僅是棧上的數(shù)據(jù),可以使用一個(gè)叫做 clone 的通用函數(shù)。第五章會(huì)討論方法語(yǔ)法,不過(guò)因?yàn)榉椒ㄔ诤芏嗾Z(yǔ)言中是一個(gè)常見(jiàn)功能,所以之前你可能已經(jīng)見(jiàn)過(guò)了。

這是一個(gè)實(shí)際使用 clone 方法的例子:

    let s1 = String::from("hello");
    let s2 = s1.clone();

    println!("s1 = {}, s2 = {}", s1, s2);

這段代碼能正常運(yùn)行,并且明確產(chǎn)生圖 4-3 中行為,這里堆上的數(shù)據(jù) 確實(shí) 被復(fù)制了。

當(dāng)出現(xiàn) clone 調(diào)用時(shí),你知道一些特定的代碼被執(zhí)行而且這些代碼可能相當(dāng)消耗資源。你很容易察覺(jué)到一些不尋常的事情正在發(fā)生。

只在棧上的數(shù)據(jù):拷貝

這里還有一個(gè)沒(méi)有提到的小竅門(mén)。這些代碼使用了整型并且是有效的,他們是示例 4-2 中的一部分:

    let x = 5;
    let y = x;

    println!("x = {}, y = {}", x, y);

但這段代碼似乎與我們剛剛學(xué)到的內(nèi)容相矛盾:沒(méi)有調(diào)用 clone,不過(guò) x 依然有效且沒(méi)有被移動(dòng)到 y 中。

原因是像整型這樣的在編譯時(shí)已知大小的類(lèi)型被整個(gè)存儲(chǔ)在棧上,所以拷貝其實(shí)際的值是快速的。這意味著沒(méi)有理由在創(chuàng)建變量 y 后使 x 無(wú)效。換句話說(shuō),這里沒(méi)有深淺拷貝的區(qū)別,所以這里調(diào)用 clone 并不會(huì)與通常的淺拷貝有什么不同,我們可以不用管它。

Rust 有一個(gè)叫做 Copy trait 的特殊注解,可以用在類(lèi)似整型這樣的存儲(chǔ)在棧上的類(lèi)型上(第十章將會(huì)詳細(xì)講解 trait)。如果一個(gè)類(lèi)型實(shí)現(xiàn)了 Copy trait,那么一個(gè)舊的變量在將其賦值給其他變量后仍然可用。

Rust 不允許自身或其任何部分實(shí)現(xiàn)了 Drop trait 的類(lèi)型使用 Copy trait。如果我們對(duì)其值離開(kāi)作用域時(shí)需要特殊處理的類(lèi)型使用 Copy 注解,將會(huì)出現(xiàn)一個(gè)編譯時(shí)錯(cuò)誤。要學(xué)習(xí)如何為你的類(lèi)型添加 Copy 注解以實(shí)現(xiàn)該 trait,請(qǐng)閱讀附錄 C 中的 “可派生的 trait”。

那么哪些類(lèi)型實(shí)現(xiàn)了 Copy trait 呢?你可以查看給定類(lèi)型的文檔來(lái)確認(rèn),不過(guò)作為一個(gè)通用的規(guī)則,任何一組簡(jiǎn)單標(biāo)量值的組合都可以實(shí)現(xiàn) Copy,任何不需要分配內(nèi)存或某種形式資源的類(lèi)型都可以實(shí)現(xiàn) Copy 。如下是一些 Copy 的類(lèi)型:

  • 所有整數(shù)類(lèi)型,比如 ?u32?。
  • 布爾類(lèi)型,?bool?,它的值是 ?true ?和 ?false?。
  • 所有浮點(diǎn)數(shù)類(lèi)型,比如 ?f64?。
  • 字符類(lèi)型,?char?。
  • 元組,當(dāng)且僅當(dāng)其包含的類(lèi)型也都實(shí)現(xiàn) ?Copy ?的時(shí)候。比如,?(i32, i32)? 實(shí)現(xiàn)了 ?Copy?,但 ?(i32, String)? 就沒(méi)有。

所有權(quán)與函數(shù)

將值傳遞給函數(shù)與給變量賦值的原理相似。向函數(shù)傳遞值可能會(huì)移動(dòng)或者復(fù)制,就像賦值語(yǔ)句一樣。示例 4-3 使用注釋展示變量何時(shí)進(jìn)入和離開(kāi)作用域:

文件名: src/main.rs

fn main() {
    let s = String::from("hello");  // s 進(jìn)入作用域

    takes_ownership(s);             // s 的值移動(dòng)到函數(shù)里 ...
                                    // ... 所以到這里不再有效

    let x = 5;                      // x 進(jìn)入作用域

    makes_copy(x);                  // x 應(yīng)該移動(dòng)函數(shù)里,
                                    // 但 i32 是 Copy 的,
                                    // 所以在后面可繼續(xù)使用 x

} // 這里, x 先移出了作用域,然后是 s。但因?yàn)?s 的值已被移走,
  // 沒(méi)有特殊之處

fn takes_ownership(some_string: String) { // some_string 進(jìn)入作用域
    println!("{}", some_string);
} // 這里,some_string 移出作用域并調(diào)用 `drop` 方法。
  // 占用的內(nèi)存被釋放

fn makes_copy(some_integer: i32) { // some_integer 進(jìn)入作用域
    println!("{}", some_integer);
} // 這里,some_integer 移出作用域。沒(méi)有特殊之處

示例 4-3:帶有所有權(quán)和作用域注釋的函數(shù)

當(dāng)嘗試在調(diào)用 takes_ownership 后使用 s 時(shí),Rust 會(huì)拋出一個(gè)編譯時(shí)錯(cuò)誤。這些靜態(tài)檢查使我們免于犯錯(cuò)。試試在 main 函數(shù)中添加使用 s 和 x 的代碼來(lái)看看哪里能使用他們,以及所有權(quán)規(guī)則會(huì)在哪里阻止我們這么做。

返回值與作用域

返回值也可以轉(zhuǎn)移所有權(quán)。示例 4-4 展示了一個(gè)返回了某些值的示例,與示例 4-3 一樣帶有類(lèi)似的注釋。

文件名: src/main.rs

fn main() {
    let s1 = gives_ownership();         // gives_ownership 將返回值
                                        // 轉(zhuǎn)移給 s1

    let s2 = String::from("hello");     // s2 進(jìn)入作用域

    let s3 = takes_and_gives_back(s2);  // s2 被移動(dòng)到
                                        // takes_and_gives_back 中,
                                        // 它也將返回值移給 s3
} // 這里, s3 移出作用域并被丟棄。s2 也移出作用域,但已被移走,
  // 所以什么也不會(huì)發(fā)生。s1 離開(kāi)作用域并被丟棄

fn gives_ownership() -> String {             // gives_ownership 會(huì)將
                                             // 返回值移動(dòng)給
                                             // 調(diào)用它的函數(shù)

    let some_string = String::from("yours"); // some_string 進(jìn)入作用域.

    some_string                              // 返回 some_string 
                                             // 并移出給調(diào)用的函數(shù)
                                             // 
}

// takes_and_gives_back 將傳入字符串并返回該值
fn takes_and_gives_back(a_string: String) -> String { // a_string 進(jìn)入作用域
                                                      // 

    a_string  // 返回 a_string 并移出給調(diào)用的函數(shù)
}

示例 4-4: 轉(zhuǎn)移返回值的所有權(quán)

變量的所有權(quán)總是遵循相同的模式:將值賦給另一個(gè)變量時(shí)移動(dòng)它。當(dāng)持有堆中數(shù)據(jù)值的變量離開(kāi)作用域時(shí),其值將通過(guò) drop 被清理掉,除非數(shù)據(jù)被移動(dòng)為另一個(gè)變量所有。

雖然這樣是可以的,但是在每一個(gè)函數(shù)中都獲取所有權(quán)并接著返回所有權(quán)有些啰嗦。如果我們想要函數(shù)使用一個(gè)值但不獲取所有權(quán)該怎么辦呢?如果我們還要接著使用它的話,每次都傳進(jìn)去再返回來(lái)就有點(diǎn)煩人了,除此之外,我們也可能想返回函數(shù)體中產(chǎn)生的一些數(shù)據(jù)。

我們可以使用元組來(lái)返回多個(gè)值,如示例 4-5 所示。

文件名: src/main.rs

fn main() {
    let s1 = String::from("hello");

    let (s2, len) = calculate_length(s1);

    println!("The length of '{}' is {}.", s2, len);
}

fn calculate_length(s: String) -> (String, usize) {
    let length = s.len(); // len() 返回字符串的長(zhǎng)度

    (s, length)
}

示例 4-5: 返回參數(shù)的所有權(quán)

但是這未免有些形式主義,而且這種場(chǎng)景應(yīng)該很常見(jiàn)。幸運(yùn)的是,Rust 對(duì)此提供了一個(gè)不用獲取所有權(quán)就可以使用值的功能,叫做 引用references)。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)