LASSIC Media らしくメディア

2026.07.26 らしくコラム

値渡しと参照渡しの違い|引数の渡し方の基礎

引数の渡し方という問題

プログラムコード

関数やメソッドに引数を渡すとき、渡されるものが「値のコピー」なのか「変数そのもの(の参照)」なのかによって、関数内での変更が呼び出し元にどう影響するかが変わってきます。この違いを正しく理解していないと、「関数を呼び出しただけのつもりが、元の変数の中身まで変わってしまった」というバグを生みやすくなります。

サーバールーム

特にオブジェクトや配列を引数にするケースでは、意図せず呼び出し元のデータを書き換えてしまう事故が起こりがちです。レビューの現場でも、関数が引数を書き換えているかどうかは頻出の指摘ポイントです。なお、本記事が扱うのは「引数として渡した値やデータが、関数内でどう扱われるか」という受け渡しの仕組みと副作用の話であり、変数がどの範囲から見えるか(スコープ)という可視性の話とは論点が異なります。スコープに関する話は、関連概念として後半で軽く触れる程度です。

典型的な症状としては、設定値を保持したオブジェクトを複数の関数に渡して処理を任せたところ、途中の関数が中身を書き換えており、後続の処理で想定と違う値を参照してしまう、といったケースが挙げられます。原因を追うと「関数に渡した引数が、実は元のデータそのものを指していた」というだけの話であることが少なくないものです。仕組みを言葉として知っているかどうかで、こうした不具合の切り分け速度は大きく変わってきます。

本記事では、値渡しと参照渡しという2つの基本モデルを整理したうえで、実際の主要言語がどちらの性質を持つのか、そして実務でどこに注意すべきかを解説します。

値渡し(call by value)

値渡しは、関数を呼び出す際に実引数の「値」をコピーして仮引数に渡す方式です。関数の内部では、その仮引数はあくまで独立した新しい変数として扱われるため、関数内で仮引数の値を書き換えても、呼び出し元の変数には影響しません。

次の擬似コードで確認してみましょう。

function increment(n) {
  n = n + 1
}

let x = 5
increment(x)
print(x)   // x は 5 のまま

関数 increment の内部で n を書き換えていますが、呼び出し元の x はコピーされた別の実体なので変化しません。呼び出し元のデータが勝手に書き換わらないという点で扱いやすく、関数の副作用を考えなくてよいという利点があります。

一方で、渡す値のサイズが大きい場合(大きな構造体や巨大な配列など)は、その都度コピーが発生するためコストがかかります。値渡しは「呼び出し元に影響しにくい」代わりに、コピーの手間というトレードオフを抱えている方式だといえるでしょう。

C言語やC++のように構造体をそのまま値渡しできる言語では、構造体のサイズが大きいほどコピーの負荷が増していきます。数値や真偽値のような小さなプリミティブ型であればコピーのコストはほとんど気になりませんが、フィールドを多く持つ構造体を関数呼び出しのたびに丸ごとコピーする設計は、性能面で見直しの対象になりやすい部分です。設計段階で「この引数は値渡しで十分か」を検討しておくと、後からの手戻りを防ぎやすくなります。

参照渡し(call by reference)

参照渡しは、変数の値そのものではなく、変数が置かれている場所(実体への参照)を渡す方式です。仮引数は実引数そのものと同一視されるため、関数内で仮引数を変更すると、呼び出し元の変数にもその変更が反映されます。

function increment(ref n) {
  n = n + 1
}

let x = 5
increment(ref x)
print(x)   // x は 6 になる

この例では、increment 内で n を書き換えると、呼び出し元の x も一緒に変わります。関数を呼んだ結果として呼び出し元の変数が変化することを「副作用がある」と表現しますが、参照渡しはこの副作用を前提とした仕組みです。

複数の戻り値を擬似的に返したい場合や、大きなデータをコピーせずに操作したい場合には有用ですが、呼び出す側から見ると「関数の中で何が書き換えられるか」が読み取りにくくなる面があります。関数の名前やドキュメントだけでは副作用の有無が分からないコードは、後から読む人にとって負担になりやすいものです。

典型的な用途としては、2つの変数の値を入れ替えるswap処理が挙げられます。値渡しのままでは仮引数を入れ替えても呼び出し元には反映されないため、参照渡しやポインタ渡しを使って実引数そのものを操作する必要が生じるわけです。C言語のようにポインタ経由で疑似的な参照渡しを実現する言語もあれば、C++やC#のようにref・&といった専用の構文を用意している言語もあります。どちらも「仮引数と実引数を同一視する」という点では狙いは共通しているものです。

言語ごとの実際の挙動

多くのプログラミング言語は、基本的な引数渡しの方式として値渡しを採用しています。ただし、オブジェクトや配列を渡す場合には「参照(の値)がコピーされて渡される」という性質を持つ言語がほとんどです。これは値渡しの一種でありながら、渡されるものが実体そのものではなく実体への参照であるため、関数内でプロパティやフィールドを書き換えると呼び出し元にも影響が及びます。この挙動は「参照の値渡し」と呼ばれることもある表現です。

整理すると、プリミティブ型(数値・真偽値・文字列など)は値そのものがコピーされるのに対し、オブジェクト型は参照がコピーされる、という対比になります。以下の図と表で確認してみましょう。

図

言語 既定の引数渡し プリミティブ型 オブジェクト・配列 補足
Java 値渡し 値そのものがコピー 参照の値がコピー 常に値渡し(参照渡しの構文は無い)
C# 値渡し 値そのものがコピー 参照の値がコピー ref/out で明示的に参照渡し可能
C++ 値渡し 値そのものがコピー 値渡し(コピー)が既定 int& x のように&を付けると参照渡し
Python オブジェクト参照渡し 再代入は元に影響しない 中身の変更は元に反映 int・str は変更不能(イミュータブル)
JavaScript 値渡し(参照の値渡し) 値そのものがコピー 参照の値がコピー 再代入は元に影響しない
Go 値渡し 値そのものがコピー スライス等は内部参照を保持 ポインタ渡しで明示的に共有可能

ここで押さえておきたいのは、多くの言語では「値渡しか参照渡しか」という二択ではなく、プリミティブ型は値そのものが渡り、オブジェクト型は参照の値が渡る、という併存した仕組みになっている点です。「参照渡し」という言葉を使うときは、C++のref引数やC#のref/outのように仮引数と実引数が完全に同一視される仕組みを指すのか、それとも「参照の値渡し」を指すのか、混同しないよう意識しておくとよいでしょう。

Ruby・PHP・Swiftなど表に挙げていない言語でも、基本の考え方は変わりません。Rubyはオブジェクトの参照を値として渡す方式で、PHPは既定では値渡しですが引数名の前に&を付けると参照渡しに切り替えられるものです。Swiftは値型(構造体)と参照型(クラス)で挙動が分かれ、値型は値渡し、クラスのインスタンスは参照の値渡しになります。言語ごとに構文や用語の使い方には違いがあるものの、「プリミティブや値型は複製され、オブジェクトや参照型は実体を共有する」という骨格は共通していると捉えておくと理解しやすいでしょう。

実務での関わり

実務でこの違いが問題になりやすい場面を整理します。

  • 意図しない副作用:オブジェクトや配列を引数として渡した関数の内部でプロパティを書き換えると、呼び出し元のデータまで変わってしまうことがあります。設定オブジェクトや共有のキャッシュを渡すコードでは、この副作用が思わぬバグの温床になりがちです。特に外部APIから受け取ったレスポンスのオブジェクトをそのまま複数の処理に横流ししているような設計では、途中の1つの関数が値を書き換えただけで後続の処理全体に影響が波及することもあります。
  • コピーのコスト:値渡しであればこの副作用を避けやすくなりますが、巨大な構造体や大量データの場合、そのたびにコピーが発生し処理時間やメモリの消費につながるものです。速度とデータ保護のどちらを優先するかはケースバイケースの判断になります。
  • イミュータブルな設計:オブジェクトそのものを変更不可(イミュータブル)にして、変更したい場合は新しいオブジェクトを生成して返す設計にすると、関数が引数を書き換えないことが構造的に保証しやすくなります。関数型プログラミングでよく採用される考え方です。
  • 防御的コピー:引数として受け取ったオブジェクトを関数内で保持・変更する必要がある場合は、受け取った時点で複製(ディープコピー)してから扱うと、呼び出し元への影響を避けやすくなります。
  • レビュー観点:コードレビューでは「この関数は引数を書き換えていないか」「書き換える場合、それが関数名やドキュメントから読み取れるか」を確認する項目に加えるとよいでしょう。updateXxxやmutateのように変更を伴うことが名前から分かる関数と、getXxxのように参照だけのはずの関数を明確に区別しておくと、後からコードを読む人の負担が減ります。

特に複数人での開発やニアショア・オフショアを含む分業体制では、こうした暗黙のルールが共有されていないと、思わぬバグ修正の手戻りにつながります。関数のインターフェース設計とレビュー基準をチームで揃えておくことは、長く運用するシステムほど効果を発揮するものです。長期にわたって保守を続ける業務システムほど、担当者が入れ替わっていく中でこうした暗黙のルールが失われやすく、ドキュメント化と規約の明文化がより重要になっていきます。

テストの観点でも、引数として渡したオブジェクトが関数呼び出し後にどう変化しているかを検証するテストケースを用意しておくと、副作用の有無を早期に発見しやすくなります。ユニットテストで「呼び出し前後の引数の状態」を比較する項目を加えておくだけでも、リファクタリング時に思わぬ副作用が紛れ込んだことに気づける可能性が高まるものです。

まとめ

値渡しは実引数の値をコピーして渡すため、関数内での変更が呼び出し元に及ばない方式です。参照渡しは変数そのものを渡すため、関数内の変更が呼び出し元にも反映されます。多くの言語ではプリミティブ型は値渡し、オブジェクト・配列は「参照の値渡し」という形をとっており、この違いを意識しないと意図しない副作用を生みやすくなります。関数を設計・レビューする際は、引数を書き換えていないか、書き換える場合はそれが読み取りやすい形になっているかを確認する習慣が助けになるでしょう。

相談するメリット

「関数が引数をどこまで書き換えているか分からない」「レビューで副作用の有無を見落としがちになっている」といった課題は、設計ルールとレビュー体制の両面から見直すことで軽減できるものです。LASSICのニアショア開発・受託開発チームでは、副作用の少ない関数設計や防御的コピーの考え方を取り入れたコーディング規約の整備、レビュー観点の言語化を支援しています。既存システムの保守フェーズから新規開発まで、体制づくりを含めてご相談いただけます。

よくある質問

Javaは値渡しですか、参照渡しですか。

Javaは常に値渡しです。プリミティブ型は値そのものがコピーされ、オブジェクト型は参照の値がコピーされて渡されます。そのため、オブジェクトのフィールドを書き換えると呼び出し元にも影響しますが、引数に別のオブジェクトを代入し直しても呼び出し元の変数は変わりません。この挙動を「参照渡し」と呼ぶのは誤りで、正確には「参照の値渡し」です。

参照渡しはなぜ注意が必要だと言われるのですか。

関数の内部で仮引数を書き換えると、呼び出し元の変数まで変化してしまうためです。関数を呼び出す側からは、内部で何が変更されるのかコードを読まないと分かりにくく、離れた場所で予期しないデータの変化が起きる原因になります。副作用を伴う処理だと分かるように関数名やドキュメントで明示しておくことが望ましいでしょう。

オブジェクトを引数で渡すとき、コピーされないのですか。

多くの言語では、オブジェクトそのものはコピーされず、オブジェクトへの参照(の値)がコピーされて渡されます。そのため、関数内でオブジェクトのプロパティを変更すると呼び出し元にも影響しますが、引数の変数に別のオブジェクトを代入し直しても呼び出し元は変わらないという点です。呼び出し元への影響を避けたい場合は、関数内で複製してから扱う方法があります。

値渡しと参照渡し、どちらを使うべきですか。

言語によって既定の方式が決まっているため、自分で選べる場面は限られます。設計として意識すべきなのは、関数が引数を書き換える必要があるかどうかです。書き換える必要がなければ、内部で複製してから扱う、あるいは変更を加えない設計にすることで、呼び出し元に影響しにくいコードになります。書き換えが必要な場合は、その旨を関数名やコメントで明示しておくと読み手にとって親切です。

著者:テレリモ総研編集部 鈴木 亮佑

関数設計やコードレビュー体制の整備でお困りの際は

LASSICでは、副作用を抑えた関数設計やコーディング規約の策定、レビュー体制の構築をニアショア開発の実績を通じて支援しています。既存システムの保守・改修から新規開発まで、まずはお気軽にご相談ください。

出典


View