為自己Coding
為自己Coding

YO~~ 剛跨入AI人工智慧領域的小小工程師, 熱愛自學, 熱愛分享, 下班後的我想為自己Coding, 積極撰寫教學文, 想將自學的程式知識分享給大家, 不斷追求進步的自己, 希望有一天能回饋社會,幫助需要幫助的人, 如果您有什麼很酷的想法,也覺得我還行,歡迎您找我合作~~ IG: https://www.instagram.com/coding_4_me/

[Aptos學習筆記#5]Move進階使用 - 所有權和引用 - Ownership & References

這篇紀錄Move中的 Ownership 和 References 的觀念
攝影師:Cliford Mervil: https://www.pexels.com/zh-tw/photo/2469122/


目錄

  • 作用域中的變數
  • move 和 copy
  • 引用 References
  • Borrow 檢查 Borrow Checking
  • 取值運算 Dereferencing
  • 引用原生類型 Referencing Primitive Types


一. 作用域中的變數

每個變數只有一個所有者作用域。當所有者作用域結束後,變數就會被清除
  • 所有者是擁有某變數的作用域
  • 變數可以在作用域內被定義(像是在Script中使用let來定義變數)
  • 也可以作為參數傳遞給作用域
  • 每個變量只可以有一個所有者,所以當把變數作為參數傳遞給函數時,該函數就會是新的所有者,並且第一個函數已不再擁有該變數的所有權,也可以說第二個函數接管了變數的所有權
  • 例子: 變數a先作為參數傳遞給了第一個函數,所以接下來當有第二個函數要使用變數a的時候就會報錯

Script: 設定一個變數a,先以參數傳遞到第一個函數,所有權轉給了第一個函數,第一個函數執行完後,沒有返回變數a,所以第二個函數執行的時候,就報錯了

script {
    use {{sender}}::M;

    fun main() {
        // Module::T 是一個結構體 struct
        let a : Module::T = Module::create(10);

				// 變數a離開了main函數
        // 且被放到了新的作用域 M::value 函數中
        M::value(a);

        // 變數已經不存在 main 函數中
	      // 這段程式碼不會編譯 compile
        M::value(a);
    }
}

Module: 函數中返回的是u8類型的參數值,參t已經被drop了,所以上面Script中當執行完第一個函式後,變數a被drop了,第二個函數接著執行的時候才會報錯

module M {

    struct T { value: u8 }

    public fun create(value: u8): T {
        T { value }
    }

    // 參數t被傳進了函數 value 中
    // value 函數拿走了它的所有權
    public fun value(t: T): u8 {
        t.value
    }
    // 函數value的作用域結束,t被刪除了,只有u8格式的值回傳
    // t 已經不存在
}

從結果來看,當函數 value() 結束時,t已經不存在了,返回的只是一個u8類型的值。

如何讓t依然可以用呢? 有一個快速的解決方法就是返回一個元組,該元組包含原始變數和其他結果,但 Move 還有一個更好的解決方案。


二. move 和 copy

Move VM中有兩個字節碼: MoveLoc 和 CopyLoc,

反映到 Move 語言層面,它們分別為 move 和 copy


關鍵字 move

當變數傳到另一個函數時,MoveLoc指令會被使用,它會被 move

  • 例子: 平常我們並不會顯示地使用關鍵字move,但程式碼依然可以正常運行,這邊我們顯示地使用來示範
script {
    use {{sender}}::M;

    fun main() {
        let a : Module::T = Module::create(10);
        
        // 變數a被移動到函數作用域中
        M::value(move a);

        // main作用域中的變數a被刪除了
    }
}


關鍵字 copy

使用時機: 想保留變數的值,同時僅將值的副本傳遞給某個函數

  • 例子: 當我們第一次調用函數 value 時,將變數a的副本傳給函數,並保留a在本地作用域(main function)中,以便第二次調用函數時再次使用變數a
script {
    use {{sender}}::M;

    fun main() {
        let a : Module::T = Module::create(10);
        
        // 使用關鍵字 copy 來clone整個結構體
        // 也可以這樣使用 let a_copy = copy a
        M::value(copy a);
        // 變數a還在main作用域中,所以可以被使用
        M::value(a); // won't fail, a is still here
    }
}


重要筆記:

  • 使用 copy ,其實我們實際上複製了變數值,從而增加了程序佔用的內存。
  • 複製的數據量如果很大,內存的消耗可能會很高
  • 在區塊鏈中,交易執行佔用的內存資源是會消耗交易費的,每個Byte都會影響交易的執行費用,因此如果過多的使用 copy 會浪費很多交易費用

利用引用,可以幫助我們避免不必要的 copy 從而節省一些費用


三. 引用 - References

  • 指向變數的鏈接(通常是內存中的某個片段)
  • 可以將其傳遞到程序的其他部分,而無需移動變數值
引用 (使用&符號來表示) 讓我們可以使用直而無需擁有所有權
  • 例子: 在參數類型T前面加入了&引用符號,將參數類型T轉換成了T的引用&T

Move 支援兩種類型的引用

  • 不可變引用 & (ex. &T): 不可變的引用讓我們可以在不更改值的情況下讀取值
  • 可變引用 &mut (ex. &mut T): 可變引用給了我們讀取和更改值的能力
  • 例子: 在Module中寫入兩個函數,一個使用不可變的引用,另一個使用可變的引用

Module

module M {
    struct T { value: u8 }

    // 回傳值為非引用的類型
    public fun create(value: u8): T {
        T { value }
    }

    // 不可變的引用,只允許讀取
    public fun value(t: &T): u8 {
        t.value
    }

    // 可變的引用允許讀取和改變其值
    public fun change(t: &mut T, value: u8) {
        t.value = value;
    }
}

Script

script {
    use {{sender}}::M;

    fun main() {
        let t = M::create(10);

        // 直接創建一個引用
        M::change(&mut t, 20);

        // 將一個引用賦予到一個變數
        let mut_ref_t = &mut t;

        // 使用可變的引用,並改變其值為100
        M::change(mut_ref_t, 100);

        // 不可變的引用
        let value = M::value(&t);

        // this method also takes only references
        // printed value will be 100
        0x1::Debug::print<u8>(&value);
    }
}
使用不可變的引用(&)從結構體中讀取數據,使用可變的引用(&mut)來修改它們
透過使用適當類型的引用,我們可以更加安全地讀取模塊,因為這樣的方式能告訴程式的閱讀者,該變數是否會被修改


四. Borrow 檢查 - Borrow Checking

Move 控制了我們使用參考(Reference)的方式,這樣可以防止意外發生

  • 例子: 透過例子了解這個運作方法

Module

module Borrow {

    struct B { value: u64 }
    struct A { b: B }

    // 用內部B來創建A
    public fun create(value: u64): A {
        A { b: B { value } }
    }

    // 給一個可變的引用到內部B
    public fun ref_from_mut_a(a: &mut A): &mut B {
        &mut a.b
    }

    // 改變B
    public fun change_b(b: &mut B, value: u64) {
        b.value = value;
    }
}

Script

script {
    use {{sender}}::Borrow;

    fun main() {
        // 創建一個結構體(struct)A
        let a = Borrow::create(0);

        // 從mut A中獲取可變的引用B
        let mut_a = &mut a;
        let mut_b = Borrow::ref_from_mut_a(mut_a);

        // 改變B
        Borrow::change_b(mut_b, 100000);

        // 從A中獲得另一個可變的引用
        // get another mutable reference from A
        let _ = Borrow::ref_from_mut_a(mut_a);
    }
}

結果: 執行上是沒有問題的,我們使用對A的可變引用來獲得其內部結構B的可變引用(&mut a.b),然後改變B

如果我們交換了最後兩段表達式,先創建一個新的A可變引用,而B的可變引用依然存在,會如何呢?

let mut_a = &mut a;
let mut_b = Borrow::ref_from_mut_a(mut_a);

let _ = Borrow::ref_from_mut_a(mut_a);

Borrow::change_b(mut_b, 100000);

程式會報錯

┌── /scripts/script.move:10:17 ───
    │
 10 │         let _ = Borrow::ref_from_mut_a(mut_a);
    │                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Invalid usage of reference as function argument. Cannot transfer a mutable reference that is being borrowed
    ·
  8 │         let mut_b = Borrow::ref_from_mut_a(mut_a);
    │                     ----------------------------- It is still being mutably borrowed by this reference
    │

這段程式無法編譯,因為 &mut A 被 &mut B 借用了,如果我們在對它的內容具有可變引用的時候同時更改 A,就會陷入一個狀況,就是A可以更改但是對它內容的引用依然還在此處,如果沒有實際的B,mut_b會指向哪裡?


我們得到一些結論

  1. Move透過Borrow Checking阻止了一些狀況,造成編譯錯誤,編譯器建構一個 Borrow 圖並禁止移動借用(borrow)的值,這就是Move在區塊鏈中很安全的原因之一
  2. 我們可以從引用(reference)創建引用(reference),這樣新的引用將會借用原始的引用,可變引用可以創建不可變與可變的引用,但不可變引用僅能創建不可變引用
  3. 當引用被借用時,它不能任意被移動,因為其它值依賴於它


五. 取值運算 - Dereferencing

透過取值運算來獲得引用所指向的值 - 符號使用星號*表示

取值運算實際上產生了一個副本,但要確定這個值具有 Copy 這個 ability 能力
module M {
    struct T has copy {}
    
    // 這邊的t值是一個引用類型
    public fun deref(t: &T): T {
        *t
    }
}
取值運算不會將原始值移動move到目前的作用域中,而是實際上生成了一個副本
module M {
    struct H has copy {}
    struct T { inner: H }

    // ...

    // 我們透過不可變的引用一樣可以做到
    public fun copy_inner(t: &T): H {
        *&t.inner
    }
}

有一個方法可以用來複製一個結構體的內部字段(fields),使用*&來引用並取值

透過使用*&,我們複製了結構體的內部值


六. 引用原生類型 - Referencing primitive types

  • 原生類型,由於他們的簡單性,所以不需要作為引用傳遞,而是會以複製的方式操作
  • 即使我們按值將它們傳遞給函數,它們也將保留在當前的範圍之中
  • 可以刻意的使用move關鍵字來移動,但由於原生類型的大小非常小,所以複製它們甚至比透過引用傳遞或是移動move它們更便宜
script {
    use {{sender}}::M;

    fun main() {
        let x = 10;
        M::do_smth(a);
        let _ = x;
    }
}

即使我們沒有使用引用來傳遞x,這個script也會編譯,添加copy是不必要的,因為VM已經將它放入


Reference

The Move Language - The Move Book

CC BY-NC-ND 2.0 版权声明

喜欢我的文章吗?
别忘了给点支持与赞赏,让我知道创作的路上有你陪伴。

加载中…

发布评论