Dev Study
← どの言語にもある仕組み — 値・型・数・文字・エラー・メモリ・並行
レッスン 1 / 12·約 8 分·第25期 どの言語にもある仕組み

代入は何をコピーするか — 名前と実体と、言語ごとの約束

リクエストがアプリケーションに届くまで
  1. ブラウザURL入力
  2. DNS名前→IP
  3. ルータ経路選択
  4. ロードバランサ振り分け
  5. Nginx受け口
  6. アプリ処理今ここ
  7. DB保存

コースの初回です。同じ1行でも、何が起きるかはその言語が約束した分しか決まっていないことを、最も基本的な操作である代入から確かめます。

まず結論

`b = a` で複製されるものは言語ごとに違います。Python のリストと JavaScript の配列では実体を指す参照が、Go の配列では中身が、Go のスライスでは配列の一部を指す小さな記述子が複製されます。Rust の Vec ではポインタ・長さ・容量が複製されますが、元の a は使えなくなります(move)。

「プリミティブは複製、オブジェクトは共有」の二分法では、Go の append が容量しだいで元を書き換えたり書き換えなかったりすることを説明できません。何が複製されるかは、その言語の文書が約束した分だけ決まっています。

? 読む前に予測

Python で `c = [1]` を関数 `rebind(x)` に渡し、関数の中で `x = [9]` と代入します。呼び出しのあと c を表示すると何が出るでしょうか(Python 3.14.7)。

当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。

抽象原理

変数は名前で、代入は名前を実体に結び付ける

b = a と書いたとき、何が複製されるのでしょうか。Python の FAQ はこう答えます。

Variables are simply names that refer to objects. Doing y = x doesn’t create a copy of the list – it creates a new variable y that refers to the same object x refers to.

リストは1つで、x と y はそれを指す2つの名前です。第10期で、ディレクトリの中の名前はファイルの実体(inode)を指す参照にすぎないと見ました。構図は同じです。

関数の呼び出しも同じ仕組みで説明されます。

Remember that arguments are passed by assignment in Python. Since assignment just creates references to objects, there’s no alias between an argument name in the caller and callee, and consequently no call-by-reference.

関数の引数 x は、呼び出し元の c と同じ実体を指す、別の名前です。だから x が指す実体を変更すれば c から見えますが、x を別の実体に付け替えても c は動きません。

名前の付け替えと、実体の変更は別の操作
名前 c呼び出し元
名前 x関数の引数
どちらも同じ実体を指す
リスト [1]実体は1つ
関数の中で何をするか
x.append(9)実体を変える → c からも見える
x = [9]x の指す先を替える → c は元のまま
  • ・関数に渡ったのは、実体を指す参照という値
  • ・呼び出し元の変数そのものは渡っていない
具体実物

Python と JavaScript で同じことをする

Docker の aarch64 コンテナで、Python 3.14.7 と Node v24.21.0 に同じ処理を書いて並べました。Python 版はこうです。

a = [1, 2]; b = a; b.append(3); print("alias:", a, a is b)
def rebind(x): x = [9]
def mutate(x): x.append(9)
c = [1]; rebind(c); print("rebind:", c); mutate(c); print("mutate:", c)

JavaScript 版は const・push・=== に置き換えただけです。

Python 3.14.7 / Node v24.21.0(aarch64)の出力コマンド出力
--- $ python /w/a.py
alias: [1, 2, 3] True
rebind: [1]
mutate: [1, 9]
[exit 0]
--- $ node /w/a.js
alias: [ 1, 2, 3 ] true
rebind: [ 1 ]
mutate: [ 1, 9 ]
[exit 0]

b.append(3) は a にも見え、a is b は True です。複製は作られていません。関数の中の代入(rebind)は外に届かず、実体の変更(mutate)は届きます。JavaScript も同じ結果でした。

✓ ここまでの確認

実演で、関数の中の `x.append(9)` は c を [1, 9] に変えたのに、`x = [9]` は c を変えませんでした。この違いの説明として正しいのはどれですか。

抽象原理

何が複製されるかは、言語が決めている

「オブジェクトなら共有」とまとめたくなりますが、他の言語では成り立ちません。

Go の FAQ は everything in Go is passed by value と書き、関数は a copy of the thing being passed を受け取るとします。そのうえでこう続けます。

Map and slice values behave like pointers: they are descriptors that contain pointers to the underlying map or slice data. Copying a map or slice value doesn’t copy the data it points to.

スライスは a descriptor of an array segment(Go blog)で、配列へのポインタ・長さ・容量の組です。**複製されるのはこの記述子で、配列は共有されます。**なお現行の Go 仕様の Calls 節は「値渡し」という語を使わず、引数は仮引数に assigned されると書きます。

C では、配列の式は多くの文脈で is converted to an expression with type "pointer to type"(N3220 §6.3.2.1)。関数に配列を渡すと先頭要素へのポインタが渡ります。構造体は代入で中身ごと複製されます。

Rust Book(第4.1節)は String の例で、代入は we copy the pointer, the length, and the capacity that are on the stack. We do not copy the data on the heap と説明します。浅い複製にあたりますが、let s2 = s1; のあと Rust considers s1 as no longer valid。元の変数を無効にするので、Book は shallow copy ではなく move と呼びます。Vec も同じ形で、std の文書は Vec is and always will be a (pointer, capacity, length) triplet. No more, no less. と保証しています。

b = a(または引数渡し)で複製されるもの表
言語・型 複製されるもの 実体
Python のリスト・JS の配列 実体を指す参照 共有
Go の配列 中身すべて 別
Go のスライス 記述子(ポインタ・長さ・容量) 共有
C の構造体 中身すべて 別
C の配列引数 先頭要素へのポインタ 共有
Rust の Vec ポインタ・長さ・容量(a は無効になる) 移る
具体実物

Go の append は容量しだい、Rust は代入で持ち主が移る

Go 1.27.1 の実演です(main 関数から表示の行を除いて抜粋)。s は長さ2・容量4、full は要素2個のリテラルで作りました。

arr := [2]int{1, 2}; arr2 := arr; arr2[0] = 99
s := make([]int, 2, 4); s[0], s[1] = 1, 2
t := s; t[0] = 99
u := append(s, 3); u[1] = 77
full := []int{1, 2}
v := append(full, 3); v[1] = 77
go1.27.1 linux/arm64 の出力コマンド出力
--- $ go run /w/main.go
array copy: [1 2] [99 2]
slice share: [99 2] [99 2]
append within cap: [99 77] [99 77 3]
append beyond cap: [1 2] [1 77 3]
[exit 0]

配列は複製され、スライスは共有されました。append の2行が要点です。容量に余りがある s では、u[1] = 77 が s の2番目の要素まで書き換えました。余りの無い full は元のままです。仕様の通りです。

If the capacity of s is not large enough to fit the additional values, append allocates a new, sufficiently large underlying array that fits both the existing slice elements and the additional values. Otherwise, append re-uses the underlying array.

同じ書き方でも、共有されるかどうかはその時点の容量で決まります。

Rust 1.98.1 で let b = a; のあとに a を使うと、コンパイルが止まります。

rustc 1.98.1(aarch64)の出力コマンド出力
--- $ rustc /w/main.rs -o /tmp/m && /tmp/m (debug default)
error[E0382]: borrow of moved value: `a`
--> /w/main.rs:5:22
|
2 | let a = vec![1, 2];
| - move occurs because `a` has type `Vec<i32>`, which does not implement the `Copy` trait
3 | let b = a;
| - value moved here
4 | println!("{:?}", b);
5 | println!("{:?}", a);
| ^ value borrowed here after move
|
help: consider cloning the value if the performance cost is acceptable
|
3 | let b = a.clone();
| ++++++++
 
error: aborting due to 1 previous error
 
For more information about this error, try `rustc --explain E0382`.
[exit 1]

複製が欲しければ a.clone() と書け、とコンパイラが示しています。

✓ ここまでの確認

Go の実演で s を `make([]int, 2, 2)`(容量2)で作っていたら、`u := append(s, 3); u[1] = 77` のあと s はどうなると考えられますか(s は [99 2] の状態)。

⇄ 2つを見比べる
具体例 A

Go のスライスの代入

t := s でスライスの記述子が複製されます。記述子は同じ配列を指しているので、t[0] = 99 は s からも見えます。配列そのものを代入した arr2 は別物です。

go run の出力(先頭2行を抜粋)コマンド出力
array copy: [1 2] [99 2]
slice share: [99 2] [99 2]
具体例 B

C の配列を引数に渡す

関数に渡るのは配列式から変換された先頭要素へのポインタです。関数の中の arr[0] = 99 は呼び出し元の x を書き換えます。構造体の代入は中身ごと複製されます(gcc 13.5.0、aarch64)。

gcc -O0 でビルドして実行した出力コマンド出力
struct copy: a=1 b=99
array param: x[0]=99

Go のスライスと C の配列引数。どちらも「値の複製」なのに元が書き換わるのはなぜでしょうか。

複製されたものが「中身」なのか「中身の場所を指す情報」なのかを見比べてください。

この章のまとめ

  • 変数は実体を指す名前。Python の y = x はリストを複製せず、同じ実体に2つ目の名前を付ける。
  • Python の引数は passed by assignment。実体の変更は呼び出し元に届き、名前の付け替えは届かない。FAQ は no call-by-reference と明記。
  • Go は everything in Go is passed by value。ただしスライスで複製されるのは記述子で、配列は共有される。
  • Go の append は容量に余りがあれば同じ配列を使い、無ければ新しい配列を確保する。共有されるかは型でなく容量で決まる。
  • C の配列式は先頭要素へのポインタに変わり、構造体は中身ごと複製される。
  • Rust の Vec は代入で move し、元の変数を使うと E0382 でコンパイルが止まる。
  • 実演は Python 3.14.7 / Node v24.21.0 / Go 1.27.1 / rustc 1.98.1 / gcc 13.5.0、いずれも aarch64 のコンテナ。JS 以外は、上に引いた各言語の文書の記述と結果が対応している。

自分の現場で確かめる

読んだだけでは身につきません。手元の環境で実際に見てみてください。

環境:

理解度チェック

読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。

問題 1 / 3正解: 0

Python の公式 FAQ は、関数への引数の渡り方をどう説明していますか。

用語集(4語)
名前と実体
変数は実体(オブジェクト)を指す名前で、代入は名前を実体に結び付ける操作、という見方。Python FAQ は `Variables are simply names that refer to objects.` と書く。
値渡し / call-by-reference
値渡しは、渡した値の複製を関数が受け取る方式。call-by-reference は、呼び出し元の変数そのものを渡し、関数内の代入が呼び出し元に届く方式。Python FAQ は後者ではないと明記している。
スライス(Go)
配列の一部を表す記述子。配列へのポインタ・長さ・容量からなる。代入で複製されるのはこの記述子で、指す配列は共有される。
move(Rust)
代入や関数呼び出しで値の持ち主が移ること。String などではポインタ・長さ・容量が複製されるが、元の変数は使えないとコンパイラが扱う。Copy トレイトを実装する整数などは move せず複製される。