閉じる

値渡しと参照渡しの違い 失敗しがちなポイントと対策

初心者がつまずきやすいのが、関数に値を渡したときに「元のデータは変わるのか、変わらないのか」です。

本稿では値渡しと参照渡しの根本的な違いを、やさしい言葉と短いコード例で丁寧に説明します。

よくある失敗と対策、言語別の注意点もまとめ、現場で迷わない判断軸を身につけられるようにします。

値渡しと参照渡しの基本と違い

初心者がまず知るべきポイント

値渡しは「中身のコピー」を渡します。

参照渡しは「同じ中身を指すメモのコピー」を渡します。

前者では関数内の変更は元のデータに影響しません。

後者では関数内の変更がそのまま呼び出し元にも見えます。

多くの言語では型や書き方によって、このどちらかの挙動になります。

用語の意味

  • 値(value): 数字や文字列など、そのもの自体のデータです。
  • 参照(reference): データがどこにあるかを指す手がかりです。手元にあるのは実体ではなく、実体への道しるべと考えるとわかりやすいです。
  • ミュータブル(mutable): 中身をあとから変更できるデータです(例: 配列、辞書)。
  • イミュータブル(immutable): 中身を変更できないデータです(例: 多くの言語の文字列や数値)。

何が渡るか

最重要ポイントは「コピーされるのか」「同じ実体を指すのか」です。

次の表で直感をつかみましょう。

方式渡すもの呼び出し元のデータは向いている場面
値渡し中身のコピー影響されない安全に読み取りたい、高頻度で小さなデータ
参照渡し実体を指す参照(のコピー)影響される可能性がある大きなデータの共有、意図的な更新

安全性を優先するなら値をコピー、性能や共有を優先するなら参照という原則をまず覚えておくと判断が素早くなります。

関数呼び出しでの違い

同じ「足し算」でも、どちらの方式かで結果が変わります。

  • 値渡しのイメージ: 受け取った「写し」に加算するので、元は変わりません。
  • 参照渡しのイメージ: 元の「箱」を共有して加算するので、元も変わります。

ここで重要なのは、言語や型によって自動的にどちらかが選ばれることです。

自分で制御する場合は「コピーして渡す」などのテクニックを使います。

使い分けの目安

  • 入力を絶対に変えたくないときは、関数内でコピーしてから使うか、イミュータブルな型を使います。
  • 大きなデータを頻繁に処理するなら、参照共有(またはポインタ)でパフォーマンスを確保し、関数の目的と副作用を明確にします。
  • チーム開発では、「この関数は入力を変更するか」をドキュメントに明記し、名前でも意図を伝えます(例: updateInPlace、withXxxなど)。

コード例で理解する値渡し/参照渡し

値型の例

JavaScript(数値は値渡し)

JavaScript
function addOne(n) {
  n = n + 1; // 引数nのコピーを変更
}
let x = 5;
addOne(x);
console.log(x); // 5 のまま

Python(整数はイミュータブル)

Python
def add_one(n):
    n = n + 1  # 新しい整数をnに再代入。元の変数xは無関係
x = 5
add_one(x)
print(x)  # 5 のまま

値型やイミュータブルな型では、関数内の再代入は呼び出し元に影響しません。

参照型の例

JavaScript(配列やオブジェクトは参照を渡す)

JavaScript
function pushOne(arr) {
  arr.push(1); // 共有している配列の中身を変更
}
const a = [0];
pushOne(a);
console.log(a); // [0, 1] に変化

Python(リストはミュータブル)

Python
def push_one(lst):
    lst.append(1)  # 共有しているリストの中身を変更
a = [0]
push_one(a)
print(a)  # [0, 1] に変化

中身を変更する操作(mutate)は、参照を共有していると呼び出し元に影響します

引数を書き換えるとどうなるか

書き換え(再代入)は共有を切るだけで、元は変わらない

JavaScript
function replaceArray(arr) {
  arr = [9, 9]; // 参照そのものを別の配列に差し替え(ローカルだけ)
}
const a = [0];
replaceArray(a);
console.log(a); // [0] のまま
Python
def replace_list(lst):
    lst = [9, 9]  # ローカル名lstの参照先を変更。呼び出し元のaは変わらない
a = [0]
replace_list(a)
print(a)  # [0] のまま

「中身を変更」すれば元に影響、「引数名に再代入」しても元には影響しない、という区別が大切です。

コピーして渡す方法

同じ実体を共有したくないときは、先にコピーを作って関数に渡します。

JavaScriptの例

JavaScript
function safelyProcess(user) {
  const copy = {...user};      // オブジェクトの浅いコピー
  copy.name = copy.name + "!";
  return copy;
}
const u = {name: "Alice", tags: ["new"]};
const v = safelyProcess(u);
console.log(u.name); // "Alice" のまま

深い構造までコピーしたいときはstructuredClone(対応環境)やライブラリを使います。

JavaScript
const deep = structuredClone(u); // ネストも複製

Pythonの例

Python
import copy

def safely_process(user):
    c = copy.copy(user)      # 浅いコピー
    c["name"] = c["name"] + "!"
    return c

ネストがあるときはcopy.deepcopyを使います。

Python
deep = copy.deepcopy(user)

よく使うコピー方法の早見表です。

言語浅いコピー例深いコピー例
JavaScript{…obj} / Array.from(arr)structuredClone(obj)
Pythoncopy.copy(x) / list(x)copy.deepcopy(x)
Javanew ArrayList<>(list) / Arrays.copyOf再帰的に新規作成(手動)
C#list.ToList()手動で新規作成やシリアライズ
Gocopy(dst, src)でスライス要素を複写自作のディープコピー関数
Swift値型(struct)は代入でコピー参照型(class)は自作コピー
Kotlinlist.toList() / data classのcopy()ネストは手動でコピー

浅いコピーはネストの内側までは複製しません

必要に応じて深いコピーを選びます。

失敗しがちなポイントと対策

元のデータが勝手に変わる

参照を共有したまま中身を変更すると、呼び出し元のデータも変わります。

対策は関数の入口でコピーを作る、または入力を変更しない方針(イミュータブル設計)を徹底することです。

JavaScriptならObject.freeze(浅い凍結)で誤変更を検出しやすくできます。

更新されないと誤解する

関数内で引数名に新しいオブジェクトを代入しても、呼び出し元には反映されません。

これは引数名はローカルの別名にすぎないためです。

呼び出し元に反映したいなら中身を変更するか、結果をreturnする必要があります。

浅いコピーの落とし穴

浅いコピーでは、ネストされた配列やオブジェクトの中身は共有されたままです。

たとえば{user: {name: “A”}}を浅くコピーすると、内側のuserは同じ参照を指します。

内側を書き換えると元も変わるため、ネストがある場合は深いコピーを検討しましょう。

引数名での勘違い

「同じ名前を使っているから同じもの」と思いがちですが、引数名は関数の中だけで有効なローカル変数です。

値渡しか参照渡しかにかかわらず、別の箱に入った「コピー(値か参照か)」だと理解しておくと混乱しません。

失敗を防ぐチェックリスト

  • 関数は入力を変更しますか。しないならコードと名前で明示し、必要ならコピーを作っていますか。
  • データはミュータブルですか。イミュータブルで置き換えられませんか。
  • ネスト構造ですか。浅いコピーで十分か、深いコピーが必要かを確認しましたか。
  • 性能要件は満たしていますか。大きなデータを無闇にコピーしていませんか。
  • 言語の挙動は把握していますか。プリミティブとオブジェクトで処理を分けていますか。
  • テストで「入力が変わらないこと/変わること」を検証していますか。

言語別の注意点とベストプラクティス

Java/C#の挙動

JavaとC#はいずれも「引数は常に値渡し」。

オブジェクトは参照の値が渡るという挙動です。

つまり、オブジェクトの中身を変更すると呼び出し元に反映されますが、引数に別のオブジェクトを再代入しても呼び出し元には影響しません。

Javaの例:

Java
void add(List<Integer> xs) { xs.add(1); }      // 反映される
void replace(List<Integer> xs) { xs = new ArrayList<>(); } // 反映されない

ベストプラクティスとして、入力を変更しない関数は新しいオブジェクトを返す、またはCollections.unmodifiableListで意図を明確にします。

C#ではrefやoutを付けると「参照そのもの」を渡せますが、初心者はまず通常の値渡しを理解してから使うと安全です。

JavaScriptの挙動

JavaScriptはプリミティブ(数値、文字列、boolean、bigint、symbol)は値渡し、オブジェクト/配列/関数は参照の値が渡るという整理で覚えると混乱しません。

const引数は再代入を防ぎますが中身の変更は防ぎません

変更したくないときは、コピーを作るかObject.freezeを使います。

深いコピーが必要ならstructuredCloneやライブラリを検討します。

Pythonの挙動

Pythonは「オブジェクトへの参照を共有する(pass-by-sharing)」と説明されます。

再代入は呼び出し元に影響しませんが、ミュータブルなオブジェクトの中身を変更すると反映されます。

Python
def f(lst):
    lst.append(1)  # 反映される
def g(lst):
    lst = [9]      # 反映されない

入力を保護したいときはcopy.copyやcopy.deepcopyで複製し、関数のドキュメントに「入力は変更しない」ことを明記します。

Goの挙動

Goはすべて値渡しです。

ただし、sliceやmapは「ヘッダのコピー」なので、同じ基盤データを共有しがちです。

要素の変更は呼び出し元にも見えます。

go
func addOne(xs []int) { xs[0] = xs[0] + 1 } // 共有された配列部分を更新

共有したくないなら新しいスライスを作りcopy関数で複製します。

構造体を更新したい場合はポインタ引数(*T)を使うか、新しい値を返しましょう。

Swift/Kotlinのポイント

Swiftはstructやenumが値型、classが参照型です。

値型は代入や引数でコピーされるため安全です。

inoutを使うと引数を直接更新できますが、初心者はまず戻り値で返す設計に慣れるとよいです。

Kotlinは基本的にクラスは参照型です。

valは再代入を防ぎますが中身の変更は可能です。

data classのcopy()は浅いコピーである点に注意しましょう。

まとめ

値渡しは「コピーを渡す」ので安全、参照渡しは「同じ実体を指す」ので効率的だが副作用に注意という軸をまず押さえましょう。

そのうえで、言語ごとの挙動(プリミティブとオブジェクトの違い、スライスやデータクラスの特性など)を理解し、必要な場面でだけコピーを作る入力を変更しない関数は結果を返すという設計を徹底すれば、初学者が陥りがちな「勝手に変わる/変わらない」問題を確実に避けられます。

テストで意図を確認しながら、少しずつ安全で読みやすいコードへと近づけていきましょう。

この記事を書いた人
エーテリア編集部
エーテリア編集部

このサイトでは、プログラミングをこれから学びたい初心者の方に向けて記事を書いています。 基本的な用語や環境構築の手順から、実際に手を動かして学べるサンプルコードまで、わかりやすく整理することを心がけています。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!