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はスコープを超えてドロップメソッドが呼び出されます。使用されていたメモリが解放されます

// 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 コンパイラは、「所有権が move された変数」を使用するエラーを検出するのを助けてくれます。

注意:基本型の変数(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 を追加できません。

Drop trait#

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

前述のように、変数のスコープを超えると、その変数は 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 に対応しています。

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

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