懂你,更懂Rust系列之ownership
我祈禱擁有一顆透明的心靈
和會(huì)流淚的眼睛
給我再去相信的勇氣
oh~越過(guò)謊言去擁抱你
每當(dāng)我找不到存在的意義
每當(dāng)我迷失在黑夜里
oh~夜空中最亮的星
請(qǐng)指引我靠近你
夜空中最亮的星
在了解這個(gè)ownership之前,先需要認(rèn)識(shí)下兩種計(jì)算機(jī)內(nèi)存結(jié)構(gòu)
Heap《堆》和Stack《?!?br />
Heap和Stack都是內(nèi)存的一部分,在程序運(yùn)行的時(shí)候,Heap和Stack中保存有系統(tǒng)運(yùn)行的相關(guān)數(shù)據(jù)或變量,Heap和Stack的結(jié)構(gòu)方式不同,其中Stack是一個(gè)先進(jìn)后出的隊(duì)列結(jié)構(gòu),如下圖:

如上圖所示,Stack中保存了abcde五個(gè)數(shù)據(jù),其中按照abcde的順序保存,獲取數(shù)據(jù)的時(shí)候,只能按照edcba的順序出棧。
具體可以參照有一篇關(guān)于JVM虛擬機(jī)描述的Java虛擬機(jī)棧中操作數(shù)棧的描述的那樣
在Rust語(yǔ)言中,所有存儲(chǔ)在Stack上的數(shù)據(jù)必須有一個(gè)已知的固定的大小,Rust編譯的時(shí)候,那些未知大小的數(shù)據(jù),或者大小可能發(fā)生改變的數(shù)據(jù),將會(huì)被保存在Heap中,Heap的組織性較差,當(dāng)保存數(shù)據(jù)在Heap中的時(shí)候,需要確定的空間大小,內(nèi)存分配器《memory allocator》在Heap中會(huì)找到一個(gè)足夠大小的空間存儲(chǔ)數(shù)據(jù),同時(shí)返回一個(gè)指針,即表明該數(shù)據(jù)所在Heap中的位置。這個(gè)過(guò)程即為堆上分配。
這里可以看出,在Stack中分配數(shù)據(jù)的效率遠(yuǎn)遠(yuǎn)比在Heap中分配數(shù)據(jù)的效率要高很多,因?yàn)镾tack中,只需要將數(shù)據(jù)壓入棧頂即可,而Heap中則需要去尋找一個(gè)足夠大的地方,同時(shí)獲取這個(gè)地方的指針?biāo)饕?,同樣的,獲取數(shù)據(jù),Stack只需要彈出棧頂元素即可,而Heap中則需要根據(jù)索引指針找到數(shù)據(jù)所在位置,然后才能獲取數(shù)據(jù)。
通常情況下,當(dāng)代碼調(diào)用一個(gè)函數(shù)的時(shí)候,傳遞到函數(shù)中的值(可能包含指向Heap中的指針對(duì)象)和函數(shù)局部變量將被壓入Stack中,當(dāng)函數(shù)調(diào)用結(jié)束,這些值將會(huì)被彈出Stack
ownership則是解決如何跟蹤代碼中哪部分使用了Heap中的數(shù)據(jù),最小化堆中數(shù)據(jù)重復(fù)量,以及及時(shí)清理未使用的堆中數(shù)據(jù)。
所有權(quán)規(guī)則<ownership Rule> :
Each value in Rust has a variable that’s called its owner.
There can only be one owner at a time.
When the owner goes out of scope, the value will be dropped.
大致意思就是:
Rust語(yǔ)言中每一個(gè)變量值都有一個(gè)屬于自己的所有權(quán)
一次只能擁有一個(gè)所有權(quán)
當(dāng)所有權(quán)超出作用域時(shí),該值將被刪除
一、Variable Scope

看上面這段代碼,在main方法里申明了一個(gè)變量s,然后打印輸出,在s還沒(méi)有被申明的時(shí)候,s是無(wú)效的,當(dāng)退出main方法的時(shí)候,s離開作用域,同時(shí)也變無(wú)效
即:
當(dāng)s進(jìn)入作用域時(shí),它是有效的,
然后它一直有效,直到超出作用域
這里需要指出的是,在前面說(shuō)到的Rust語(yǔ)言基本類型<懂你,更懂Rust系列之?dāng)?shù)據(jù)類型>(除開數(shù)組和元祖)的數(shù)據(jù),都是存儲(chǔ)在Stack中的,并且在它們的作用域結(jié)束時(shí),彈出Stack。
所以,基于基本類型保存在Stack中的數(shù)據(jù),不需要過(guò)多的去探討,重點(diǎn)需要關(guān)注的是那些保存在Heap中的數(shù)據(jù),上面這個(gè)小例子,過(guò)于簡(jiǎn)化,因?yàn)檫@里申明了一個(gè)定長(zhǎng)的string類型的數(shù)據(jù),即這里是將string類型的s硬編碼到程序中,所以其執(zhí)行效率是很高的。那么對(duì)于那些未知大小的數(shù)據(jù)情況呢?
比如下面代碼:

這里的雙冒號(hào)(::)是一個(gè)操作符號(hào),String::from是指從字符串文本中創(chuàng)建字符串,s.push_str是指將變量s追加字符串
這里可以看到,這是一個(gè)可變、不定長(zhǎng)的字符串類型,不能通過(guò)硬編碼的方式在程序中引入二進(jìn)制的內(nèi)存塊來(lái)保存數(shù)據(jù)了。
所以,對(duì)于這種類型的string,需要在Heap中分配一定數(shù)量的內(nèi)存,同時(shí),這需要程序或者程序員做以下事:
1、必須在程序運(yùn)行的時(shí)候,從內(nèi)存分配器中請(qǐng)求內(nèi)存
2、當(dāng)用完這個(gè)字符串的時(shí)候,把這個(gè)內(nèi)存返回給分配器
第一件事,在調(diào)用String::from的時(shí)候,程序給我們完成了,這個(gè)在很多編程語(yǔ)言中都是比較相似的,
第二件事,在不同的編程語(yǔ)言中,是有很大區(qū)別的,在有GC收集器的語(yǔ)言(比如Java、Python等),GC收集器會(huì)跟蹤并識(shí)別這些區(qū)域,然后將其清理掉。在沒(méi)有GC收集器時(shí),需要我們識(shí)別出內(nèi)存什么時(shí)候不再被使用,并顯示調(diào)用代碼,返回內(nèi)存
做到第二點(diǎn)其實(shí)是比較難的,如果忘記了,則浪費(fèi)內(nèi)存,容易引起內(nèi)存泄漏,如果太早的調(diào)用代碼清除返回內(nèi)存,則可能使定義的變量是個(gè)無(wú)效的變量。如果做了2次,那也是個(gè)BUG,我們就必須將分配的一個(gè)內(nèi)存和釋放的一個(gè)內(nèi)存精確的配對(duì)。
Rust語(yǔ)言提供了不同的路徑解決這個(gè)內(nèi)存回收的問(wèn)題,一旦擁有內(nèi)存的變量超出作用域,則內(nèi)存就自動(dòng)釋放并返回。
針對(duì)上面的代碼做些說(shuō)明:

類似在C++語(yǔ)言中,在項(xiàng)目生命周期結(jié)束時(shí),重新分配資源的模式被稱為RAII,這里可以類比的理解
?二、Ways Variables and Data Interact: Move
看下面代碼:

這里不難理解,首先定義一個(gè)變量x,將5賦值給這個(gè)變量,然后再次復(fù)制一個(gè)x的值5綁定到y(tǒng)中,此時(shí)我們擁有了2個(gè)變量值,都等于5,此時(shí)兩個(gè)5均被壓入Stack中:

因?yàn)榛绢愋投际嵌ㄩL(zhǎng)大小的,是保存在Stack中的,那么如果是Heap中的String呢?

這里和上一段代碼是很類似的,但是其內(nèi)存實(shí)現(xiàn)方式是有很大區(qū)別的:
首先我們來(lái)看下s1在內(nèi)存中的保存方式:

首先,字符串類型由3部分組成,如上圖左邊所示:指向保存字符串內(nèi)容的內(nèi)存指針地址、字符串長(zhǎng)度、字符串容量,這些數(shù)據(jù)是保存在Stack中的,而這個(gè)內(nèi)存指針指向堆中字符串內(nèi)容。
那么當(dāng)執(zhí)行l(wèi)et s2 = s1的時(shí)候,發(fā)生了什么呢?

如上圖所示,當(dāng)執(zhí)行l(wèi)et s2 = s1時(shí),程序會(huì)復(fù)制Stack中的內(nèi)容,即s2的內(nèi)存地址指針指向了原本s1指向的地址,Heap中的內(nèi)容并沒(méi)有跟隨堆的復(fù)制而復(fù)制。
前面我們說(shuō)過(guò),當(dāng)變量超出作用域時(shí),會(huì)自動(dòng)釋放內(nèi)存(rust調(diào)用drop實(shí)現(xiàn)),但這里,兩個(gè)變量同時(shí)指向了相同的堆內(nèi)存地址,即當(dāng)s1和s2超出作用域的時(shí)候,將釋放2次相同的內(nèi)存,這就是double free error,屬于一個(gè)內(nèi)存安全漏洞,兩次釋放可能導(dǎo)致內(nèi)存損壞。
為了解決這個(gè)問(wèn)題,Rust認(rèn)為,當(dāng)s2創(chuàng)建完成后,s1不再有效。即:

比如代碼嘗試使用s1:

編譯報(bào)錯(cuò):

這里既是數(shù)據(jù)之間的移動(dòng),即移動(dòng)之后,原本的對(duì)象無(wú)效了。
這種移動(dòng)僅僅限于在堆上分配內(nèi)存保存的數(shù)據(jù),基本類型由于是在棧上存儲(chǔ),則是可以的,比如前面的代碼是可以運(yùn)行的:

編譯運(yùn)行:

?三、Ways Variables and Data Interact: Clone
那么針對(duì)上面移動(dòng)的字符串例子,如果我們確實(shí)需要復(fù)制一個(gè)呢?這里有一種克隆方法:

編譯運(yùn)行:

此時(shí)內(nèi)存發(fā)生的變化則是這樣了:

即Heap中的內(nèi)存也得到了復(fù)制,不過(guò)這是一個(gè)很昂貴的代價(jià),系統(tǒng)需要花費(fèi)比較大的消耗去完成這件事。
克隆結(jié)果的兩個(gè)對(duì)象,其==比較結(jié)果是為true的

編譯運(yùn)行:

四、Stack-Only Data: Copy
這里就是針對(duì)前面介紹的基本類型的棧上分配數(shù)據(jù)是不會(huì)產(chǎn)生移動(dòng)的,不在過(guò)多贅述:

編譯運(yùn)行:

五、Ownership and Functions
看如下代碼:

需要指出的是,當(dāng)非基本類型變量進(jìn)入函數(shù)體時(shí),變量產(chǎn)生移動(dòng),原始變量不在有效。這是區(qū)別于其他很多高級(jí)語(yǔ)言的。
編譯運(yùn)行:

但是如果我們?cè)噲D在main函數(shù)內(nèi),takes_ownership函數(shù)之后,調(diào)用s,編譯就會(huì)報(bào)錯(cuò),比如:

編譯報(bào)錯(cuò):

六、Return Values and Scope
函數(shù)返回值,也是具有移動(dòng)的,看下面代碼:

擁有返回值的函數(shù),在被調(diào)用之后,其返回值將移動(dòng)到調(diào)用者
所以,在調(diào)用takes_and_gives_back函數(shù)之后,試圖拿到s2也是會(huì)編譯報(bào)錯(cuò)的,這里就不做試驗(yàn)了。
可以簡(jiǎn)單做個(gè)小結(jié):
變量的所有權(quán),在每次給另外一個(gè)變量賦值時(shí),將會(huì)移動(dòng)該變量,當(dāng)變量超出作用域時(shí),變量被回收,釋放內(nèi)存。
函數(shù)會(huì)獲取參數(shù)的所有權(quán),被調(diào)用者調(diào)用,又將返回所有權(quán)給調(diào)用者,那么有時(shí)候,需要讓函數(shù)只是使用某個(gè)值,并不讓其產(chǎn)生變量移動(dòng)效果,怎么解決這個(gè)問(wèn)題呢?一般來(lái)說(shuō),可以使用函數(shù)返回值是元組的方式來(lái)完成這個(gè)操作。
即:

編譯運(yùn)行:

這里,length_string函數(shù)返回了元祖數(shù)據(jù),將參數(shù)返回了,還可以繼續(xù)用。
這里的代碼稍微做改變:

這里是會(huì)編譯報(bào)錯(cuò)的,
當(dāng)執(zhí)行返回值構(gòu)造方法后,其第一個(gè)元素為_string,所以參數(shù)_string已經(jīng)發(fā)生了移動(dòng),不在有效,當(dāng)調(diào)用_string.len()方法時(shí),_string已經(jīng)無(wú)效了。
編譯報(bào)錯(cuò)

如果在實(shí)際項(xiàng)目中,需要調(diào)用函數(shù)的時(shí)候,還可以返回參數(shù),通過(guò)這種方式固然可以實(shí)現(xiàn),但是有點(diǎn)太過(guò)于麻煩了,這時(shí)候,Rust提出了另外一個(gè)概念,引用-->References
七、References?&?Borrowing
這里還是為了解決上面那個(gè)問(wèn)題,看下面代碼:

編譯運(yùn)行:

這里可以看到,s作為參數(shù)傳入函數(shù)length_string中后,并沒(méi)有發(fā)生移動(dòng)。在函數(shù)length_string中,_string變量其實(shí)只是引用了s,并沒(méi)有將s的所有權(quán)移動(dòng)到_string中,所以當(dāng)_string超出作用域后,其指向的值,并不會(huì)被刪除。
其內(nèi)存指針如下圖:

給上面代碼加些注釋:

我們把&s稱為s的引用<references>
把這種引用稱為函數(shù)的參數(shù)租借<borrowing>
一般來(lái)說(shuō),在調(diào)用函數(shù)的時(shí)候,需要進(jìn)行參數(shù)值的一些改變,這里由于沒(méi)有得到參數(shù)所有權(quán),所以是會(huì)編譯報(bào)錯(cuò)的
這里舉個(gè)簡(jiǎn)單的例子,引用好比現(xiàn)在去租了一個(gè)房子,但是并沒(méi)有擁有房子的所有權(quán),就只能簡(jiǎn)單的住在這里,所以,如果在沒(méi)有經(jīng)過(guò)房東的同意,假設(shè)我修改了房子的結(jié)構(gòu),那肯定是會(huì)發(fā)生狀況的。
比如:

這里在調(diào)用函數(shù)的時(shí)候,對(duì)參數(shù)進(jìn)行了拼接,編譯報(bào)錯(cuò):

`_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable
這個(gè)報(bào)錯(cuò)信息給與了我們一個(gè)解決方案:
即,它告訴我們,這個(gè)引用必須是可變的
于是,改進(jìn)代碼:

編譯運(yùn)行:

這里有一個(gè)需要記住的地方,可變引用在特定范圍內(nèi),只能有一個(gè)對(duì)特定數(shù)據(jù)塊的可變引用,比如下面代碼:

編譯報(bào)錯(cuò):

移動(dòng)下位置:

這個(gè)時(shí)候,編譯運(yùn)行:

這個(gè)時(shí)候?yàn)槭裁淳褪强梢缘??可以想一下,看看前面的?nèi)容是不是忘了?
tips:_s1變量在調(diào)用length_string函數(shù)的時(shí)候,傳入的是_s1,_s1發(fā)生了移動(dòng),_s1不在有效。
在特定范圍內(nèi),保證只有一個(gè)可變引用,可以防止多個(gè)指針指向相同對(duì)象時(shí),發(fā)生對(duì)指針指向?qū)ο蟾淖冎邓鶎?dǎo)致的數(shù)據(jù)競(jìng)爭(zhēng),這種情況在其他多數(shù)語(yǔ)言中都可能發(fā)生,但Rust在編譯階段就防止這種情況。
看下面這種情況:

這里12行,定義_s3為可變變量s的不可變引用,_s4為可變變量s的不可變引用,這兩個(gè)變量的定義,都沒(méi)有問(wèn)題,只是后面定義了一個(gè)_s5為可變變量s的可變引用,
編譯報(bào)錯(cuò):

報(bào)錯(cuò)信息告訴我們,當(dāng)變量不可變引用存在租借的情況,在變量不可變引用沒(méi)有超過(guò)作用域的時(shí)候不能定義變量為可變引用。
修改下代碼:

編譯運(yùn)行:

因?yàn)楫?dāng)調(diào)用println方法后,_s3和_s4發(fā)生了移動(dòng),不在有效,所以可以再次定義變量可變引用。
通常情況下,在有指針存在的語(yǔ)言中,可能出現(xiàn)指針存在,但是指針指向的內(nèi)存已經(jīng)被釋放的情況,即dangling pointer,在Rust中,這種情況,編譯階段就會(huì)出現(xiàn)錯(cuò)誤:
比如:

這里,定義一個(gè)String類型,然后返回這個(gè)變量_k的引用,當(dāng)_k超出dangling_pointer函數(shù)的時(shí)候,程序會(huì)調(diào)用drop,清理掉_k的內(nèi)存,但是返回了_k的棧引用,
這其實(shí)是個(gè)錯(cuò)誤示范
編譯報(bào)錯(cuò):

但是可以這樣:

編譯運(yùn)行

八、The Slice Type
下面實(shí)現(xiàn)這么一個(gè)功能,從字符串中找到某個(gè)字符在字符串中所在的位置,

程序編譯運(yùn)行肯定是沒(méi)有什么問(wèn)題的,
也可以很容易的拿到_p的值是5

那么,接下來(lái),拿到這個(gè)5,一般來(lái)說(shuō),是需要有所作用的,比如根據(jù)這個(gè)5,獲取_s中指定部分?jǐn)?shù)據(jù),就上面代碼示例來(lái)看,也是可以調(diào)用截取,根據(jù)_s和_p做一些操作,那如果,我們清空_s呢?
比如:

在調(diào)用_s.clear(),之后,仿佛這個(gè)_p的意義就不大了<這里僅僅是為了接下來(lái)的內(nèi)容引入,不接受類似‘不調(diào)用_s.clear()就好了’之類的反駁。(#^.^#)>,因?yàn)闆](méi)有辦法截取了,_s已經(jīng)空了!
Rust語(yǔ)言提供了一種類型,幫助我們解決這個(gè)問(wèn)題,即,可以獲取字符串指定部分內(nèi)容,同時(shí)將截取的部分內(nèi)容稱為:片(slice)
看下面代碼:

編譯輸出:

這里就是簡(jiǎn)單的slice類型的定義,
細(xì)心一點(diǎn),可以發(fā)現(xiàn)一個(gè)問(wèn)題:這里定義的時(shí)候,用的&s,即使用了s的引用,那么可以直接使用s的截取部分么?即:

這里編譯是會(huì)報(bào)錯(cuò)的:

編譯器說(shuō),在編譯階段,無(wú)法知道str類型的固定的長(zhǎng)度
這里仿佛又獲得一個(gè)新的東西,str
在Rust中,String類型具體分為兩種,一個(gè)是未知長(zhǎng)度的字符串類型String,一種是已知長(zhǎng)度大小的str
已知長(zhǎng)度大小的字符串,其內(nèi)存分配的時(shí)候,是在棧中保存的,所以不會(huì)存在移動(dòng),比如下面代碼:

這里如果_是String類型的,在調(diào)用look_for_str函數(shù)之后,肯定會(huì)發(fā)生移動(dòng),所以,后面打印輸出的時(shí)候,是一定會(huì)報(bào)錯(cuò)的,但是這里由于是棧中內(nèi)存,可以視為和Rust基本類型一樣,不會(huì)發(fā)生移動(dòng),而打印輸出的時(shí),并沒(méi)有超出_s的作用域,所以,是沒(méi)有問(wèn)題的
編譯運(yùn)行:

而這樣是肯定會(huì)發(fā)生錯(cuò)誤的:

編譯報(bào)錯(cuò):

所以,這里就比較容易的理解,剛剛定義slice類型時(shí),為啥要使用引用?
因?yàn)槿绻皇褂靡?,那么定義的string類型是str的,str是需要固定大小的,然而本身_s并不是str,而是string類型,所以截取出來(lái)的_s部分也是一個(gè)string引用,并不是str
那么接下來(lái),繼續(xù)前面的內(nèi)容,slice定義語(yǔ)法是
[starting_index..ending_index]
這里的starting_index是起始下標(biāo),
ending_index是結(jié)束下標(biāo),
兩者是可以省略的,
[..ending_index]
等價(jià)于[0..ending_index]
[starting_index..]
等價(jià)于[starting_index..string.len()]
[..]
等價(jià)于[0..string.len()]
定義的slice是變量的引用對(duì)象的部分指向,內(nèi)存并沒(méi)有生成多余的空間來(lái)保存這個(gè)數(shù)據(jù),比如前面的定義代碼:

內(nèi)存結(jié)構(gòu)為:

注意,這里的索引截取,必須位于有效的UTF-8字符邊界內(nèi),如果是從一個(gè)多字節(jié)字符的中間位置截取,這里是不允許的,比如:

編譯報(bào)錯(cuò):

這個(gè)時(shí)候,只能這樣:

編譯運(yùn)行:

在有了這些基本語(yǔ)法之后,再來(lái)看看前面那個(gè)截取字符串的功能:

這里合理的截取到了需要的部分:

此時(shí),如果在調(diào)用打印輸出前,將_s清空,則編譯報(bào)錯(cuò),此時(shí),可以有效的幫助我們發(fā)現(xiàn),如果這樣做,那么_p將變得毫無(wú)意義:
即:

編譯報(bào)錯(cuò):

此外,還可以將slice當(dāng)做參數(shù)傳入函數(shù)中:

編譯運(yùn)行:

關(guān)于Rust語(yǔ)言的一些特性,就先到這兒了,主要需要多多自己寫代碼,然后去理解下。后續(xù)將分享實(shí)體結(jié)構(gòu)相關(guān)
喜歡的歡迎關(guān)注轉(zhuǎn)發(fā),謝謝
點(diǎn)個(gè)再看和贊咯,(#^.^#)
