基礎●●○○○

値渡しと参照渡し

Pass by Value / Pass by Referenceあたいわたしとさんしょうわたし

関数へ値のコピーを渡すか、同じ実体への参照を渡すかの区別。共有された参照が思わぬ書き換え事故を生む。

概要

値渡しと参照渡しは、「変数を関数に渡したり別の変数に代入したりしたとき、渡るのは中身のコピーか、それとも同じ実体を指す参照か」という区別です。数値を渡した場合、呼び出し先でいくら書き換えても呼び出し元の変数は変わりません。ところがオブジェクトや配列を渡すと、呼び出し先での書き換えが呼び出し元にも「見えて」しまう — この非対称な挙動の正体を説明するのがこの語彙です。

この区別を知らないまま書いたコードは、「関数に渡しただけの配列がいつの間にか並び替わっていた」「コピーしたつもりの設定オブジェクトを変更したら元も変わった」といった、症状と原因が離れた追いにくいバグを量産します。逆にこの仕組みがメモリの絵として頭に入っていれば、どの言語に移っても「これはコピーされるのか、共有されるのか」を自信を持って判断できるようになります。

なぜ生まれたか

プログラミング言語が関数という抽象を持った瞬間から、「引数に渡した変数は、呼び出し先で書き換えたら呼び出し元も変わるのか」という問いが生まれました。厄介なのは、この答えが言語ごと、さらに同じ言語でも型ごとに違うことです。C は常に値渡し、C++ は & で参照渡しを選べ、Java は「すべて値渡し」なのにオブジェクトの書き換えは呼び出し元に見え、JavaScript や Python もそれに似た挙動をする。用語の使われ方も曖昧で、「Java は参照渡し」という誤った説明が広まるなど、混乱と事故の温床になっていました。

この混乱を整理する鍵がメモリモデルです。変数の実体がスタック上の小さな値なのか、ヒープ上のオブジェクトを指す参照(アドレス)なのかを区別すれば、各言語の挙動は「何がコピーされるか」の一点で統一的に説明できます。渡すときにコピーされるのはスタック上にあるものだけ。それが値そのものならコピー後は無関係になり、それがヒープへの参照なら「参照はコピーされたが、指す先は同じ1つの実体」という共有状態が生まれる — この絵さえあれば、言語ごとの違いは些細なバリエーションに見えてきます。

詳細

プリミティブは値のコピー

数値・真偽値のようなプリミティブ型は、変数そのものが値を直接持っています。代入や関数呼び出しではその値がまるごとコピーされるため、コピー後の2つの変数は完全に独立です。let a = 1; let b = a; b = 2; としても a は 1 のまま — ここに驚きはありません。値渡しの世界は安全で予測可能です。

オブジェクトは「参照の値渡し」

一方、オブジェクトや配列の変数が持っているのは実体そのものではなく、ヒープ上の実体を指す参照です。代入や関数呼び出しでコピーされるのはこの参照だけで、指す先の実体は1つのまま共有されます。JavaScript や Java の挙動はこれで説明できます。関数の中で obj.name = "変更" と中身を書き換えれば、同じ実体を見ている呼び出し元にも変更が見えます。しかし obj = 別のオブジェクト と引数変数自体に再代入しても、書き換わるのはコピーされた参照だけなので、呼び出し元の変数は元のオブジェクトを指し続けます。

スタックヒープa42b = a42値のコピー — 以後は完全に独立obj参照obj2 = obj参照のコピー同一のオブジェクト実体は1つだけどちらの変数から書き換えてももう一方にも変更が見える
コピーされるのは参照だけ — 2つの変数が同じ実体を共有する

この挙動は「参照の値渡し」(call by sharing とも)と呼ぶのが正確です。「参照渡し」という言葉の誤用には注意してください。本来の参照渡し(C++ の & や C# の ref)は「呼び出し先での変数への再代入までもが呼び出し元に反映される」仕組みを指します。Java・JavaScript・Python にこの機能はなく、コピーされるのがたまたま参照という値である値渡ししか存在しません。「中身の書き換えは共有されるが、再代入は共有されない」— この一行が両者を見分けるリトマス試験紙です。

シャローコピーとディープコピー

「共有したくないなら複製すればよい」となりますが、複製にも深さがあります。シャローコピー(浅い複製)は最上位のプロパティだけをコピーする方法で、JavaScript のスプレッド構文 { ...obj }Object.assign がこれにあたります。手軽ですが、プロパティの値が参照なら参照がコピーされるだけなので、入れ子のオブジェクトは依然として共有されたまま — 「コピーしたのに中の配列を変えたら元も変わった」という第二の罠です。入れ子の底まで複製するディープコピー(深い複製)には、現代の JavaScript なら structuredClone が使えます。古くからの定番だった JSON シリアライズ経由の複製(JSON.parse(JSON.stringify(obj)))も広く見かけますが、JSON で表現できないもの(Date は文字列に化け、undefined や関数は消え、循環参照はエラー)が壊れる点は知っておく必要があります。

代入・シャローコピー・ディープコピーで「何がコピーされ、何が共有されるか」を、実際に操作して確かめられます。

⚡ 体験: 参照のコピーとコピーの深さ
スタック(変数)ヒープ(実体)a参照オブジェクトname: "太郎"tags: 参照 →配列[ "a", "b" ]

まず「代入 b = a」を押してください。代入でコピーされるのは中身ではなく参照です。

防御的コピーとミューテーション事故

共有の危険がわかると、境界でコピーを取る「防御的コピー」という定石が生まれます。外から受け取ったオブジェクトを保存する前に複製する、内部状態を返すときは複製を返す — こうすれば呼び出し側の書き換えが内部に、内部の変更が呼び出し側に漏れません。典型的な事故は、JavaScript の sortreverse が配列を破壊的に並び替えるのを忘れて元データを壊すケース、関数に渡したオプションオブジェクトをライブラリ側が書き換えてしまうケースなどです。より根本的な解決は、そもそも書き換え自体をやめてしまうイミュータビリティの採用で、防御的コピーはその手前にある実務的な護身術と言えます。どちらを選ぶにせよ、出発点は「その変数が持っているのは値か、参照か」というメモリの絵です。