14_Cairo1.0 變數所有權#
此文章使用的 Cairo 編譯器版本:2.0.0-rc0。因為 Cairo 正在快速更新,所以不同版本的語法會有些許不同,未來將會將文章內容更新到穩定版本。
變數的作用域#
變數的作用域,也可以理解為變數所有者的作用域,通常是指:變數的有效範圍,或者可訪問範圍,它決定了變數的生命週期和可見性。我們來看個例子:
fn main() {
...
{
// 在變數聲明前不可以訪問
let mut v1 = 1; // 變數聲明語句
// 變數聲明後,還未超出變數的作用域,可以訪問
v1 += 1;
} // 花括號結尾,變數的作用域結束,下面將不可以再訪問 v1 變數
...
}
上面變數 v1 在 main 函數內部的花括號代碼塊中被創建,所以 v1 的作用域就是:在 v1 被創建起,到花括號結束。這裡的花括號使用的是普通的花括號, if、 loop 和 fn 的花括號同樣適用。
變數所有權的由來#
在編程中,會出現許多傳遞變數的情況。比如調用一個函數,將變數作為函數的參數傳入。這個時候就出現了變數可以在多個作用域中穿梭的現象(注意⚠️:這裡說的是現象,並非打破了變數作用域的規則)。
這個現象實現出來有兩種方式:
- 傳遞副本(傳值)。把一個變數的副本傳到一個函數中,或是放到一個數據結構容器中,這時候就需要複製的操作。這個複製對於一個對象來說,需要深度複製才安全,否則就會出現各種問題,而深度複製就會導致性能問題。
- 傳遞對象本身(傳引用)。傳引用也就是不需要考慮對象的複製成本,但是需要考慮對象在傳遞後,被多個地方引用的問題。比如:我們把一個對象的引用寫入到一個數組中或傳入其它的一個函數。這意味著,大家對同一個對象都有控制權,如果有一個人釋放了這個對象,那邊其它人就遭殃了,所以,一般會採用引用計數的規則來共享一個對象。
這裡提到的控制權,演變出了 Cairo 中的變數所有權的概念。
Cairo 變數所有權的規則#
在 Cairo 中,Cairo 強化了” 所有權” 的概念,下面是 Cairo 變數所有權的三大鐵律:
- Cairo 中的每一個值都有一個 所有者(owner)。
- 同一時間只有一個所有者。
- 當變數離開 所有者 作用域時,這個變數將被丟棄(Drop)。
為了體會以上三個規則對代碼的影響,可以看下面這段 Cairo 代碼:
use core::debug::PrintTrait;
struct User {
name: felt252,
age: u8
}
// takes_ownership 取得調用函數傳入參數的所有權,因為不返回,所以變數進來了就出不去了
fn takes_ownership(s: User) {
s.name.print();
} // 這裡,變數 s 移出作用域並調用 drop 方法。佔用的內存被釋放
// gives_ownership 將返回值所有權 move 給調用它的函數
fn give_ownership(name: felt252,age: u8)->User{
User{name,age}
}
fn main() {
// gives_ownership 將返回值移給 s
let s = gives_ownership();
// 所有權轉給了 takes_ownership 函數, s 不可用了
takes_ownership(s);
// 如果編譯下面的代碼,會出現s1不可用的錯誤
// s.name.print();
}
把一個對象的所有權移動到給另外一個對象,這樣的操作被稱作 move。這樣的 Move 方式,在性能上和安全性上都是非常有效的,而 Cairo 的編譯器會幫你檢查出使用了” 所有權被 move 走的變數” 的錯誤。
注⚠️:現在基本類型的變數(felt252,u8 等)都已經實現了 Copy Trait,所以使用基本類型變數不會出現 move 的情況,所以,為了展現 move 的效果,上面使用了一個未實現 Copy Trait 的 struct 來作為 takes_ownership
函數的參數。
結構體字段也可以被單獨 move#
結構體字段可以被 move,所以在其他語言中看似很正常的代碼,在 Cairo 中編譯會報錯:
use core::debug::PrintTrait;
#[derive(Drop)]
struct User {
name: felt252,
age: u8,
school: School
}
#[derive(Drop)]
struct School {
name: felt252
}
fn give_ownership(name: felt252, age: u8, school_name: felt252) -> User {
User { name: name, age: age, school: School { name: school_name } }
}
fn takes_ownership(school: School) {
school.name.print();
}
fn use_User(user: User) {}
fn main() {
let mut u = give_ownership('hello', 3, 'high school');
takes_ownership(u.school); // 將結構體中的 school 字段單獨傳入,這樣 school 字段就被 move
// u.school.name.print(); // school字段已經被move,所以這裡編譯出錯
u.name.print(); // name字段還是可以照常訪問
// use_User(u); // 此時因為school字段被move,整個結構體都不可以被move了
// 如果重新給結構體的school字段賦值,這個結構體變量又可以繼續被move了
u.school = School{
name:'new_school'
};
use_User(u)
}
結構體中的成員是可以被 Move 掉的,如果訪問 move 掉的成員會出現編譯問題,但是依然可以訪問結構體其他成員。
另外,其中一個字段被 move,整個結構體將不可以再被 move,如果將被 move 的字段重新賦值,結構體又可以繼續被 move 了。
Copy trait#
Copy trait 就是值拷貝的特徵(trait)。任何類型實現了 copy trait,那麼當它作為函數參數傳入時,就會傳入一份拷貝的副本。給類型添加 copy trait 方式如下:
use core::debug::PrintTrait;
#[derive(Copy,Drop)]
struct User {
name: felt252,
age: u8
}
fn give_ownership(name: felt252,age: u8)->User{
User{name,age}
}
fn takes_ownership(s: User) {
s.name.print();
}
fn main() {
let s = give_ownership('hello',3);
s.age.print();
takes_ownership(s);
s.name.print(); // 由於 User 實現了 Copy trait,所以上面並沒有轉移 s 的所有權,這裡依然可以訪問 s
}
使用 Copy trait 時有一些限制需要注意:如果一個類型中包含沒有實現 copy trait 的字段,那麼這個類型就不可以被添加 copy trait。
Drop trait#
Drop trait 中包含的方法可以理解為一個析構函數(destructor),與構造函數(constructor)相對應。析構函數(destructor)在對象被銷毀時自動調用,用於清理對象所佔用的資源或狀態。相反,構造函數用於初始化對象的狀態。
前文提到變數的作用域,當一個變數要超出它的作用域時,就意味著這個變數到了生命週期的終點,此時就需要使用 Drop,來將這個變數佔用的資源清理掉。如果一個類型沒有實現 Drop trait,那麼編譯器會捕獲到,並且拋出錯誤。如:
use core::debug::PrintTrait;
// #[derive(Drop)]
struct User {
name: felt252,
age: u8
}
fn give_ownership(name: felt252,age: u8)->User{
User{name,age}
}
fn main() {
let s = give_ownership('hello',3);
// ^ error: Variable not dropped.
}
上面代碼將會編譯報錯,如果將 #[derive(Drop)]
註釋解除,那麼就為 User
類型添加了 Drop trait,此時它就可以被 Drop,編譯就不會報錯。另外,標量類型默認都實現了 Drop trait。
Destruct trait#
目前已知使用到 Destruct trait 的地方是字典。因為字典不能實現 Drop trait,但是實現了 Destruct trait,這讓它在超過它的作用域的時候,可以自動的調用 squashed 方法進行內存釋放。所以在寫一個包含字典的 struct 的時候就需要注意,如:
use dict::Felt252DictTrait;
use traits::Default;
#[derive(Destruct)]
struct A{
mapping: Felt252Dict<felt252>
}
fn main(){
A { mapping: Default::default() };
}
結構體 A 需要指明實現了 Destruct trait,不然會編譯報錯;另外,也不可以是 Drop trait,因為 Felt252Dict 無法實現 Drop trait。
總結#
將變數作為函數的參數傳入,要麼傳入的是一個副本,要麼傳入的是變數本身,同時發生所有權的轉移,作用域也隨之改變。 另外函數的返回值同樣也可以實現所有權轉移。
當一個變數超出它的作用域,它就會被 Drop 或者 Destruct。
以上的動作,就分別對應了 Copy、Drop 和 Destruct 三個 traits。
此文章部分內容參考一位大佬的文章,向他緬懷、致敬🫡