StarknetAstro

StarknetAstro

14_Cairo1.0 變數所有權

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 的花括號同樣適用。

變數所有權的由來#

在編程中,會出現許多傳遞變數的情況。比如調用一個函數,將變數作為函數的參數傳入。這個時候就出現了變數可以在多個作用域中穿梭的現象(注意⚠️:這裡說的是現象,並非打破了變數作用域的規則)。

這個現象實現出來有兩種方式:

  1. 傳遞副本(傳值)。把一個變數的副本傳到一個函數中,或是放到一個數據結構容器中,這時候就需要複製的操作。這個複製對於一個對象來說,需要深度複製才安全,否則就會出現各種問題,而深度複製就會導致性能問題
  2. 傳遞對象本身(傳引用)。傳引用也就是不需要考慮對象的複製成本,但是需要考慮對象在傳遞後,被多個地方引用的問題。比如:我們把一個對象的引用寫入到一個數組中或傳入其它的一個函數。這意味著,大家對同一個對象都有控制權,如果有一個人釋放了這個對象,那邊其它人就遭殃了,所以,一般會採用引用計數的規則來共享一個對象。

這裡提到的控制權,演變出了 Cairo 中的變數所有權的概念。

Cairo 變數所有權的規則#

在 Cairo 中,Cairo 強化了” 所有權” 的概念,下面是 Cairo 變數所有權的三大鐵律:

  1. Cairo 中的每一個值都有一個 所有者(owner)。
  2. 同一時間只有一個所有者。
  3. 當變數離開 所有者 作用域時,這個變數將被丟棄(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。

此文章部分內容參考一位大佬的文章,向他緬懷、致敬🫡

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。