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
補足: ここは ownership と lifetimes の入口です。最初は「値には持ち主がいる」と理解すれば十分です。
10.3. Value semantics
次に大事なのは、Mojo は値セマンティクスを基本にしている ことです。
10.3.1. 値セマンティクスとは
簡単に言うと、値を渡すときは、ひとまず独立したものとして考える ということです。
ポイントは次の通りです。
代入や引数渡しでは、論理的に独立した値として考える
実際にはムーブで効率よく処理されることがある
関数引数は
read参照が基本書き換えたいときは
mutなどを明示する
Counter は n を内側に持つ小さな値型です。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 を同時に許さないのは、そのためです。
こうした制約によって、データ競合をコンパイル時に防ぎやすくなります。
read と ref と ^ を混ぜると、「今この瞬間、誰が唯一の所有者で、誰が一時的に借りているか」がはっきりします。意図しない共有可変を減らす、というのが厳しさの理由です。
詳細: 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
補足: ここは難所です。まずは「参照は元の値より長生きできない」と押さえておくとよいでしょう。
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 では、不要になった値を早めに解放する方向で考えます。
実行すると init → use → del の順にメッセージが出ます。
詳細: 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
補足: Life と Death は対になる話です。作り方と壊し方をセットで見ると整理しやすいです。
10.8. Initialization
Initialization は、値をちゃんと使える状態にするまでの手順 です。
10.8.1. ポイント
フィールドごとに初期化する場合がある
最後に全体として整った状態にする必要がある
__init__の分け方で初期化の流れを変えられる
ここで大事なのは、とりあえず値を入れればよいわけではない ことです。 型として正しい状態になって、はじめて安全に使えます。
IntRange は center と width から 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
補足: ここは RAII の考え方につながります。最初は「必要な後始末を自動で安全に行う仕組みがある」と押さえれば十分です。
10.10. この章を一文で言うと
Mojo では、値の持ち主・参照の有効期間・生成から破棄までの流れを、型の仕組みで明確に扱います。
10.11. まとめ
値には持ち主があり、所有権が後始末を決める
値セマンティクスでは、値の受け渡しを独立したものとして考える
read、mut、var、refで役割がわかれるライフタイムは参照の有効期間を表す
ライフサイクルは値全体の一生を表す
__init__と__del__は、作るときと終わるときの中心になる
この章は一度で完全に理解しなくても大丈夫です。 まずは、「誰が持つか」「いつまで使えるか」「いつ片づけるか」 の3つで整理すると読みやすくなります。