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 の波括弧も同様に適用されます。

変数所有権の由来#

プログラミングでは、変数を渡す場面が多くあります。たとえば、関数を呼び出すときに変数を関数の引数として渡すことがあります。この場合、変数は複数のスコープを移動する現象が発生します(注意:ここで言っているのは現象であり、変数のスコープのルールを破っているわけではありません)。

この現象は 2 つの方法で実現できます:

  1. コピー(値の渡し)。変数のコピーを関数に渡すか、データ構造のコンテナに入れる場合、コピーの操作が必要です。オブジェクトの場合、安全なコピーのためにはディープコピーが必要であり、さもなければさまざまな問題が発生します。しかし、ディープコピーはパフォーマンスの問題を引き起こす可能性があります。
  2. オブジェクト自体の渡し(参照の渡し)。参照の渡しはオブジェクトのコピーのコストを考慮する必要はありませんが、参照が渡された後、複数の場所で参照される可能性があることに注意する必要があります。たとえば、オブジェクトの参照を配列に書き込むか、他の関数に渡す場合です。これは、みんなが同じオブジェクトを制御していることを意味します。誰かがそのオブジェクトを解放すると、他の人も困ることになるため、通常は参照カウントのルールを使用してオブジェクトを共有します。

ここで言及されている制御権は、Cairo の変数所有権の概念に発展しました。

Cairo の変数所有権のルール#

Cairo では、"所有権" の概念を強化しており、Cairo の変数所有権の 3 つの重要なルールがあります:

  1. Cairo のすべての値には 所有者(owner)があります。
  2. 同時に所有者は 1 つだけです。
  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は返り値の所有権を呼び出し元の関数に移動します
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 コンパイラは *「所有権が移動された変数を使用する」というエラーを検出 * するのを助けてくれます。

注:基本型の変数(felt252、u8 など)はすでに Copy Trait を実装しているため、move の状況は発生しません。したがって、move の効果を示すために、上記のコードでは Copy Trait を実装していない struct を使用しています。

構造体のフィールドも個別に 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 されたメンバーにアクセスするとコンパイルエラーが発生しますが、構造体の他のメンバーには依然としてアクセスできます。

また、1 つのフィールドが 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にアクセスできます
}

Copy trait を使用する場合、注意する必要がある制限があります:もし型に Copy trait を実装していないフィールドが含まれている場合、その型に Copy trait を追加することはできません。

Drop trait#

Drop trait には、オブジェクトが破棄されるときに自動的に呼び出されるメソッドが含まれます。このメソッドは、オブジェクトが占有するリソースや状態をクリーンアップするために使用されます。逆に、コンストラクタ(constructor)はオブジェクトの状態を初期化するために使用されます。

前述のように、変数のスコープが超えると、その変数は Drop または Destruct される必要があります。もし型が 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 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 を実装することはできません。

まとめ#

変数を関数の引数として渡す場合、変数のコピーまたは変数自体が渡され、所有権の移動とスコープの変更が発生します。また、関数の戻り値も所有権の移動を実現できます。

変数がスコープを超えると、Drop または Destruct が行われます。

これらのアクションは、それぞれ Copy、Drop、Destruct の 3 つの traits に対応しています。

この記事の一部は、ある方の記事を参考にしており、彼に敬意を表し、追悼します🫡

読み込み中...
文章は、創作者によって署名され、ブロックチェーンに安全に保存されています。