10. 値・所有権・ライフサイクル

10.1. この章で学ぶこと

  • 値と所有権の基本

  • ライフタイムとライフサイクルの違い

  • Initialization や Death の考え方

この章は、Mojo の中でも最初に少し難しく感じやすい部分です。 ただし、最初から細部まで理解しなくても大丈夫です。

まずは次の3つを押さえると読みやすくなります。

  • 値には持ち主がある

  • 参照は好きなだけ長く使えるわけではない

  • 値は作られてから捨てられるまでの流れを持つ

この章は、Mojo Manual — Values / Lifecycle に対応する内容です。

10.2. Values

最初に大事なのは、Mojo では値がどこに置かれ、誰が片づけるかをはっきり意識する ことです。

10.2.1. 基本

  • 値はスタックまたはヒープに置かれる

  • 所有権が、いつ誰が解放するかを決める

  • 参照は借りているだけなので、元の値より長くは使えない

次の例では、小さな Int のローカルと、要素を持つ List[Int] を並べています。Int は機械語に近いイメージではレジスタやスタック枠に載りやすく、List のような可変長コレクションはヒープ側に本体を持つ、とざっくり理解してよいです(正確な配置は実装依存ですが、「所有と解放の話」に入る入口として使えます)。

def main():
    var n: Int = 40
    var xs: List[Int] = [n, n + 1, n + 2]
    print(n, len(xs))

リスト-1: values_int_and_list.mojo

Mojo には GC(ガベージコレクタ)がありませんList のようにヒープを使う型は、型の規則と所有権のもとで、不要になったときに解放される、という考え方で読みます。

10.2.2. まずどう理解すればよいか

小さくて扱いやすい値は、スタックに置かれることがあります。 一方で、大きいものや可変長のものは、ヒープに置かれることがあります。

ここで大事なのは、値の置き場所そのものより、誰がその値の後始末をするか です。 Mojo では、その役目を 所有権 が決めます。

また、Mojo には GC がありません。 その代わり、値がいつ破棄されるかを、よりはっきり扱います。

後の節で見る ムーブ^)や デストラクタと結びつきます。ここでは 「値ごとに持ち主のイメージを持つ」 ことが第一歩です。

詳細: Values

出典: Mojo Manual — values

補足: ここは ownership と lifetimes の入口です。最初は「値には持ち主がいる」と理解すれば十分です。

10.3. Value semantics

次に大事なのは、Mojo は値セマンティクスを基本にしている ことです。

10.3.1. 値セマンティクスとは

簡単に言うと、値を渡すときは、ひとまず独立したものとして考える ということです。

ポイントは次の通りです。

  • 代入や引数渡しでは、論理的に独立した値として考える

  • 実際にはムーブで効率よく処理されることがある

  • 関数引数は read 参照が基本

  • 書き換えたいときは mut などを明示する

Countern を内側に持つ小さな値型です。show(c)c を読むだけの用途なので、引数は暗黙の共有読み取り(read に相当する扱い)として渡せます。一方 c.bump()mut self で自分自身を更新します。

struct Counter:
    var n: Int

    def __init__(out self, start: Int):
        self.n = start

    def get(self) -> Int:
        return self.n

    def bump(mut self):
        self.n += 1


def show(c: Counter):
    print(c.get())


def main():
    var c = Counter(0)
    show(c)
    c.bump()
    show(c)

リスト-2: value_semantics_counter.mojo

show を二回呼んでも、「共有オブジェクトを二か所からいじる」 という Python 的な絵ではなく、その時点の Counter の値を読むという解釈に近いです(実際のコピー/ムーブは型と呼び出し方に依存します)。

10.3.2. Python との違い

Python では、同じオブジェクトを複数の名前で共有している感覚が強いです。 一方で Mojo では、値をどう渡すか をもっとはっきり区別します。

そのため、

  • 読むだけなのか

  • 書き換えるのか

  • 持ち主ごと渡すのか

を意識して読むことが大切です。

「名前が同じオブジェクトを指す」 より 「その操作は共有読み取りか、単一の所有者の変更か」 を追うと、Mojo の値セマンティクスに慣れやすくなります。

詳細: Value semantics

出典: Mojo Manual — value-semantics

補足: Python の「参照を共有する感じ」とは少し違う、と感じられれば十分です。

10.4. Ownership

この節の中心は、値を誰が持っていて、誰が触れてよいかを区別する ことです。

10.4.1. よく出てくる見方

  • read は共有して読む

  • mut は書き換えのための扱い

  • var は所有していて移動できる

  • ref は借用を表す

次のコードは、read でリストを借りて合計するref で要素を直接書き換えるvar で所有するリストを ^ で渡して移動する、を一度に示したものです。

def sum_borrow(read xs: List[Int]) -> Int:
    var s = 0
    for x in xs:
        s += x
    return s


def take_owned(var xs: List[Int]):
    print(String("took len="), len(xs))


def main():
    var xs: List[Int] = [1, 2, 3]
    print(sum_borrow(xs))
    for ref x in xs:
        x *= 10
    print(sum_borrow(xs))

    var data: List[Int] = [7, 8]
    take_owned(data^)

リスト-3: ownership_read_ref_and_move.mojo

sum_borrow(xs)xs の所有を奪わず、読み取りに必要な期間だけ借用します。for ref x in xs は各要素への 可変借用で、インプレース更新です。最後の take_owned(data^) は、data所有権を callee に渡し切るので、その後 data は使えません(コンパイラが防ぎます)。

10.4.2. ^ の意味

^ は、所有権を移す ことを表します。

つまり、値を相手に渡し切る記号です。 そのため、移動したあとの元の変数はそのまま使えません。

これは少し厳しく見えますが、 どの値を誰が使ってよいかを曖昧にしないための仕組み です。

10.4.3. なぜ厳しくするのか

Mojo は、複数の場所から同時に危険な書き換えが起こるのを防ごうとします。 たとえば、複数の mut を同時に許さないのは、そのためです。

こうした制約によって、データ競合をコンパイル時に防ぎやすくなります。

readref^ を混ぜると、「今この瞬間、誰が唯一の所有者で、誰が一時的に借りているか」がはっきりします。意図しない共有可変を減らす、というのが厳しさの理由です。

詳細: Ownership

出典: Mojo Manual — ownership

補足: 記号や用語を全部一度に覚える必要はありません。まずは「読む」「書く」「所有する」「借りる」を分けて考えれば大丈夫です。

10.5. Lifetimes

ここでは、参照がどこまで有効か を考えます。

10.5.1. ライフタイムとは

ライフタイムは、その参照を安全に使える期間 のことです。

参照は元の値を借りているだけなので、元の値が消えたあとまで使ってはいけません。

read 引数は、呼び出し元の buf が生きているあいだだけ借用が有効です。関数を抜けたあとに参照を返すような形は、コンパイラが(多くの場合)許可しません。

def first_len(read xs: List[Int]) -> Int:
    return len(xs)


def main():
    var buf: List[Int] = [1, 2, 3, 4]
    print(first_len(buf))

リスト-5: lifetimes_read_param.mojo

10.5.2. ここで大事なこと

  • 参照には有効期間がある

  • 関数が参照を返すなら、その参照元がはっきりしている必要がある

  • ダングリング参照は拒否される

ダングリング参照とは、もう存在しない値を指してしまう参照です。 これは危険なので、Mojo では型の仕組みで防ごうとします。

ダングリングを起こすコードは、このドキュメントでは意図的に載せていません(コンパイルエラーになるため)。上の例のように、借用は所有者のスコープに収まると読むと、ライフタイムの感覚をつかみやすいです。

詳細: Lifetimes

出典: Mojo Manual — lifetimes

補足: ここは難所です。まずは「参照は元の値より長生きできない」と押さえておくとよいでしょう。

10.6. Lifecycle

次は、値の一生をもう少し大きく見ます。

10.6.1. ライフサイクルとは

ライフサイクルは、値が作られてから壊されるまでの全体の流れ です。

一方でライフタイムは、主に 参照の有効期間 を見ています。

つまり、

  • ライフサイクルは値全体の一生

  • ライフタイムは参照の有効期間

という違いがあります。

Resource__init__ で生成ログ__del__ で終了ログを出します。main の終わりで r のスコープを抜けるときに __del__ が呼ばれる、という順序が、実行時の出力として追えます。

struct Resource:
    var id: Int

    def __init__(out self, id: Int):
        self.id = id
        print(String("init "), id)

    def __del__(deinit self):
        print(String("del "), self.id)


def main():
    var r = Resource(1)
    print(String("use "), r.id)

リスト-6: lifecycle_resource_traces.mojo

10.6.2. 何がうれしいのか

この考え方を持つと、

  • いつ値が作られるか

  • いつ初期化されるか

  • いつ破棄されるか

を整理して考えられます。

Mojo では、不要になった値を早めに解放する方向で考えます。

実行すると initusedel の順にメッセージが出ます。

詳細: Lifecycle

出典: Mojo Manual — lifecycle

補足: ここでは用語をきっちり分けることが大事です。ライフタイムとライフサイクルは同じではありません。

10.7. Life

この節は、値がどう生まれるか に注目します。

10.7.1. 基本

  • __init__ で値を初期化する

  • コピーでは __copyinit__ が関わる

  • ムーブでは __moveinit__ が関わる

  • 代入時の動きも型によって決まる

初学者の段階では、細かな特殊メソッドを全部覚える必要はありません。 まずは、作る方法・コピーする方法・移す方法がわかれている とわかれば十分です。

Copyable を付けた型は、コピーの道筋がはっきりします。@fieldwise_initコンストラクタを自動生成しつつ、a.copy()明示的に複製できます。

@fieldwise_init
struct Label(Copyable):
    var text: String


def main():
    var a = Label("hi")
    var b = a.copy()
    print(a.text, b.text)

リスト-7: life_copyable_label.mojo

__copyinit__ を手で書かなくても、Copyable とフィールド構成に応じて コピー初期化子が生成される、とマニュアルに沿って読めます。ムーブは別経路(^ やコンテキスト)で、Life の話と対にして整理します。

詳細: Life

出典: Mojo Manual — life

補足: Life と Death は対になる話です。作り方と壊し方をセットで見ると整理しやすいです。

10.8. Initialization

Initialization は、値をちゃんと使える状態にするまでの手順 です。

10.8.1. ポイント

  • フィールドごとに初期化する場合がある

  • 最後に全体として整った状態にする必要がある

  • __init__ の分け方で初期化の流れを変えられる

ここで大事なのは、とりあえず値を入れればよいわけではない ことです。 型として正しい状態になって、はじめて安全に使えます。

IntRangecenterwidth から lo / hi を計算してフィールドを埋めます。呼び出し側は 意味のある引数だけ渡せばよく、不変条件lo <= hi など)をコンストラクタ内に閉じ込めやすいです。

struct IntRange:
    var lo: Int
    var hi: Int

    def __init__(out self, center: Int, width: Int):
        self.lo = center - width // 2
        self.hi = center + width // 2


def main():
    var r = IntRange(10, 4)
    print(r.lo, r.hi)

リスト-8: init_box_bounds.mojo

詳細: Initialization

出典: Mojo Manual — initialization

補足: 具体例は公式の struct の例と併せて確認すると理解が深まります。

10.9. Death

最後に、値がどう終わるか を見ます。

10.9.1. 基本

  • __del__ はデストラクタ

  • リソース解放に関わる

  • 明示的に破棄させる型もある

  • ムーブ後や二重解放の問題も意識する

10.9.2. ここで見ておきたいこと

ファイルやメモリなど、後始末が必要なものがあります。 その後始末を担うのが __del__ です。

また、同じものを二重に解放すると危険です。 Mojo では、そのような問題を型や規約で防ごうとします。

HeapIntsヒープに Int を並べて保持し、__del__各要素の破棄と free を行います。lifecycle_resource_traces.mojo のログ付きデストラクタと対になる、実リソースを返す例です。

struct HeapInts(Writable):
    var data: UnsafePointer[Int, MutExternalOrigin]
    var size: Int

    def __init__(out self, *values: Int):
        self.size = len(values)
        self.data = alloc[Int](self.size)
        for i in range(self.size):
            (self.data + i).init_pointee_copy(values[i])

    def __del__(deinit self):
        for i in range(self.size):
            (self.data + i).destroy_pointee()
        self.data.free()

    def at(self, i: Int) -> Int:
        return (self.data + i)[]


def main():
    var h = HeapInts(1, 2, 3)
    print(h.at(0), h.at(2))

リスト-9: death_heap_buffer.mojo

main を抜けて h のスコープが終わると __del__ が走り、ヒープが返却されます。ムーブで所有が移ったあとに同じバッファを二重に free しないよう、型システムとムーブの規則が関わります(詳細はマニュアルの Ownership / Death を参照)。

詳細: Death

出典: Mojo Manual — death

補足: ここは RAII の考え方につながります。最初は「必要な後始末を自動で安全に行う仕組みがある」と押さえれば十分です。

10.10. この章を一文で言うと

Mojo では、値の持ち主・参照の有効期間・生成から破棄までの流れを、型の仕組みで明確に扱います。

10.11. まとめ

  • 値には持ち主があり、所有権が後始末を決める

  • 値セマンティクスでは、値の受け渡しを独立したものとして考える

  • readmutvarref で役割がわかれる

  • ライフタイムは参照の有効期間を表す

  • ライフサイクルは値全体の一生を表す

  • __init____del__ は、作るときと終わるときの中心になる

この章は一度で完全に理解しなくても大丈夫です。 まずは、「誰が持つか」「いつまで使えるか」「いつ片づけるか」 の3つで整理すると読みやすくなります。