値渡し、参照渡し
しばしば混乱のもとになる言葉です。
今回は、皆様お使いの言語が「何渡し」をしているのかを判明させる、短くてわかりやすい処理を紹介します。
その処理をそれぞれの言語で書いてみて出力を確認すれば、「何渡し」が行われているのか明らかにできます。
ぜひ、お使いの言語で確認してみてください!
断り書き
気にならない方は読み飛ばして本編へお急ぎください。
「共有渡し」で統一しますという断り書き
関数に値を渡す時の、英語で「call-by-sharing」と呼ばれたり、日本語で「参照の値渡し」と呼ばれたりしているやり方を、ここでは「共有渡し」で統一します。
なお、筆者がこの用語を推奨しているわけではありません。複数言語の仕様を比較する便宜上、この記事内で用いる名前をつけているだけです。
「値渡し」という言葉を狭い意味で使いますという断り書き
関数に値を渡す時、「共有渡し」も「値渡し」として解釈することが可能なのですが、ここでは、C++のように「対象となる値そのものがコピーされて渡される」というものだけを「値渡し」と呼んで、「共有渡し」と区別することにします。(ポインタを使う場合は別)
この記事の流れ
関数の引数について「値渡し・共有渡し・参照渡し」の3パターンの違いが端的に現れるコードを紹介します。関数の中身2行、実行部の中身3行です。
↓
代入について「値渡し的代入・共有渡し的代入・参照渡し的代入」の3パターンの違いが端的に現れるコードを紹介します。中身は5行です。
↓
この3パターン同士の対応関係を確認します。
↓
いくつかの言語で「違いが端的に現れるコード」を書いていき、その言語がどのパターンなのかを確認していきます。また、先程の「対応関係」が成立していることも確認します。
↓
最後に補足と提案をして終わります。
違いが端的に現れるコード
関数の引数について
void func(List<int> x) {
x[0] = 2;
x = [3];
}
void main() {
List<int> a = [1];
func(a);
print(a[0]);
}
処理内容を紹介したいだけで、特定の言語での話にしたくなかったので、あえて使ってる人の少なそうな言語(Dart)で書いてみました。やってることは以下です。
関数の引数については、このような処理で実験していきます。
各パターンで動きを確認しましょう。
「値渡し」だとしたら
引数に値を渡す時、値自体をコピーして渡すのが「値渡し」です。
この場合次のような動きになるはずです。
void func(List<int> x) {
x[0] = 2;
x = [3];
}
void main() {
List<int> a = [1];
func(a); // 関数に渡す値はコピーなので、何をされてもaに影響はない
print(a[0]); // 元のまま1が出力される
}
1が出力されたら「値渡し」です。
###「共有渡し」だとしたら
変数に値そのものではなく「値への参照」が格納されていると考え、参照のコピーを渡すのが「共有渡し」です。
変数に格納されている「参照」をまるごとコピーしているので、渡し方だけを見ると「値渡し」と考えることもできます。「参照の値渡し」とも呼ばれる所以です。
動きは次のようになるはずです。
void func(List<int> x) {
x[0] = 2; // xはaと同じ対象を指しており、その0番が変更される
x = [3]; // xが違うものを指すようになる。aには影響なし。
}
void main() {
List<int> a = [1];
func(a); // aが持っている「参照」をコピーしてxに渡す
print(a[0]); // 2が出力される
}
2が出力されたら「共有渡し」です。
「参照渡し」だとしたら
func(a)
とした時に、引数に「a
を指す参照」を渡すのが「参照渡し」です。
a
にx
という別名を付けて受け取る、と考えることもできます。
x
への操作は全てa
に影響します。
void func(List<int> x) {
x[0] = 2; // xとaともに中身が書き換わる
x = [3]; // xとaともに中身が書き換わる
}
void main() {
List<int> a = [1];
func(a); // aを指す参照を渡す
print(a[0]); // 3が出力される
}
3が出力されたら「参照渡し」です。
代入について
void main() {
List<int> a = [1];
List<int> b = a; // この時bが保持するものは?
b[0] = 2;
b = [3];
print(a[0]); // 出力は?
}
-
a
に、1
という要素を一つだけ持つ配列(またはそれに類するもの)を代入します -
b
にa
を代入します(ここでの動きの違いがこの記事の主題!) -
b
の0番要素に2
を代入します -
b
に、3
という要素を一つだけ持つ配列(またはそれに類するもの)を代入します -
a
の0番要素を出力します
実は結局やってることは先程と同じですが、なぜこれで違いが端的に現れるのか、一つずつ見ていきましょう。
「値渡し的代入」だとしたら
a = b
とした際、a
の中身をコピーしてb
に持たせるのが値渡し的代入です。
その時点でもうb
とa
は別物になりますので、b
をどういじってもa
には影響しません。
よって先程の例は次のようになるはずです。
void main() {
List<int> a = [1];
List<int> b = a; // bにも[1]が入る。以降aとは何の関係もない。
b[0] = 2;
b = [3];
print(a[0]); // aは[1]のままなので、出力は 1
}
1が出力されたら、値渡し的代入だったということになります。
「共有渡し的代入」だとしたら
変数に「値そのもの」ではなく、「その値を指す参照」が格納されていると考えて、
a = b
とした際には、a
に格納されている「参照」をコピーしてb
に持たせるのが「共有渡し的代入」です。
この場合は次のような動きになります。
void main() {
List<int> a = [1];
List<int> b = a; // aが指しているのと同じものを、bも指すようになる
b[0] = 2; // aとbは同じものを指しており、その0番要素が変更される
b = [3]; // bが違うものを指すようになる。aには影響なし
print(a[0]); // aは[2]なので、出力は 2
}
2が出力されたら共有渡し的代入です。
厳密には
厳密には、リストの中身であるa[0]
も変数ですから、その中身は参照と解釈すべきで、最後の行のaは[2]
はaは[(2を指す参照)]
とすべきかもしれませんが、長くなるので省略しています。以下同様。
「参照渡し的代入」だとしたら
このケースでは、b = a
としたときに、b
にa
を指し示す参照が代入されます。
すると、その後b
を操作するとそれらの操作はa
にもそのまま適用されます。
a
にb
という別名(alias, エイリアス)をつけたものと考えることができますね。
動きはこうなります。
void main() {
List<int> a = [1];
List<int> b = a; // aを指す参照がbに入る(aにbという別名をつける)
b[0] = 2; // aとbともに、0番要素が変更される
b = [3]; // bもaも中身が変更される。
print(a[0]); // aは[3]なので、出力は 3
}
3が出力されたら、bにはaを指す参照が代入されていたことになります。
「○○渡し的代入」と「○○渡し」の対応関係
代入のしかた | 引数への渡し方 |
---|---|
値渡し的代入 | 値渡し |
共有渡し的代入 | 共有渡し |
参照渡し的代入 | 参照渡し |
このような対応関係があると思われます。
以下、言語ごとに、きちんと対応しているかどうか見ていきます。
上記対応関係は成立するものと仮定して、代入の場合でも「的代入」を付けずに「○○渡し」と呼んでいくことにします。
コメントに書いてある数字が出力結果です。
「代入の時は値渡しだけど引数に渡す時は共有渡し」みたいなちぐはぐな例がないことを確認していきます。
とはいえ多くは扱いませんので、「メジャーな言語で対応してないやつあるぞ!」とご存知の方は教えて下さい!
値渡しの言語
C++
#include <iostream>
#include <array>
using namespace std;
int main()
{
array<int, 1> a{1};
array<int, 1> b = a;
b[0] = 2;
b = array<int, 1>{3};
cout << a[0] << endl; // 1
}
#include <iostream>
#include <vector>
using namespace std;
void func(vector<int> x)
{
x[0] = 2;
x = vector<int>{3};
}
int main()
{
vector<int> a{1};
func(a);
cout << a[0] << endl; // 1
}
C++は値渡しの言語です。(明示的に参照を扱う時は別)
(生配列を使った場合に若干の例外(?)のような動きになるので下で扱います)
共有渡しの言語
Java
class JavaAssignment{
public static void main(String[] args) {
int[] a = {1};
int[] b = a;
b[0] = 2;
b = new int[]{3};
System.out.println(a[0]); // 2
}
}
class JavaArgument{
private static void func(int[] x){
x[0] = 2;
x = new int[]{3};
}
public static void main(String[] args) {
int[] a = {1};
func(a);
System.out.println(a[0]); // 2
}
}
Javaは共有渡しの言語です。
JavaScript
let a = [1];
let b = a;
b[0] = 2;
b = [3];
console.log(a[0]); // 2
const func = (x) => {
x[0] = 2;
x = [3];
};
a = [1];
func(a);
console.log(a[0]); // 2
JavaScriptは共有渡しの言語です。
最近流行りのTypeScriptも同様です。
Python
a = [1]
b = a
b[0] = 2
b = [3]
print(a[0]) # 2
def func(x):
x[0] = 2
x = [3]
a = [1]
func(a)
print(a[0]) # 2
Pythonは共有渡しの言語です。
Dart
void main() {
List<int> a = [1];
List<int> b = a;
b[0] = 2;
b = [3];
print(a[0]); // 2
}
void func(List<int> x) {
x[0] = 2;
x = [3];
}
void main() {
List<int> a = [1];
func(a);
print(a[0]); // 2
}
Dartは共有渡しの言語です。
扱うものによって違うケース
扱う対象によって挙動が変化するケースがありました。この場合は注意して使う必要が生じますが、今回見つけたのはPHPだけでした。
基本的には言語内で統一されているものと思われます。
PHP
PHPでは、配列は値渡しで、オブジェクトは共有渡しとなるようです。(明示的に参照を扱う時は別)
値渡しとなる例
<?php
$a = [1];
$b = $a;
$b[0] = 2;
$b = [3];
echo $a[0]; // 1
<?php
function func($x){
$x[0]=2;
$x = [3];
}
$a = [1];
func($a);
echo $a[0]; // 1
共有渡しとなる例
<?php
class Hoge{
public $fuga;
function __construct($fuga){
$this -> fuga = $fuga;
}
}
$a = new Hoge(1);
$b = $a;
$b -> fuga = 2;
$b = new Hoge(3);
echo $a -> fuga; // 2
<?php
class Hoge{
public $fuga;
function __construct($fuga){
$this -> fuga = $fuga;
}
}
function func($x){
$x -> fuga = 2;
$x = new Hoge(3);
}
$a = new Hoge(1);
func($a);
echo $a -> fuga; // 2
(微妙だけど)C++の生配列
こちらのコード、2
が出力されているので一見すると共有渡しに見えます。
#include <iostream>
#include <vector>
using namespace std;
int main()
{
int a[] = {1};
int *b = a;
b[0] = 2;
cout << a[0] << endl; // 2
}
ですが、ここに出てくるa
とb
は、明示的に「ポインタ変数」なんですよね。
変数の中身であるポインタ自体はまるごとコピーされているわけなので、これをもって「*b = a
の所で共有渡しが行われている」とは言いづらいと思います。
C++は値渡しでいいと思います。(明示的に参照を扱う時は別)
参照渡しできる言語
明示的に参照を扱うことができる言語では、変数に参照を代入したり、関数に「参照渡し」できます。
3
を出力することになりますね。
C++
#include <iostream>
#include <array>
using namespace std;
int main()
{
array<int, 1> a{1};
array<int, 1> &b = a; // 参照を代入
b[0] = 2;
b = array<int, 1>{3};
cout << a[0] << endl; // 3
}
#include <iostream>
#include <array>
using namespace std;
void func(array<int, 1> &x) // 参照渡し
{
x[0] = 2;
x = array<int, 1>{3};
}
int main()
{
array<int, 1> a{1};
func(a);
cout << a[0] << endl; // 3
}
PHP
<?php
$a = [1];
$b = &$a; // 参照を代入
$b[0] = 2;
$b = [3];
echo $a[0]; // 3
<?php
function func(&$x){ // 参照渡し
$x[0]=2;
$x = [3];
}
$a = [1];
func($a);
echo $a[0]; // 3
まとめ
ということで、いくつかの言語で確認してきました。
結構多くのメジャー言語で「共有渡し」が使われているのですが、案外知られていませんね。私も最近まで知りませんでした。知っておくといいと思います!
その他の話題
プリミティブやイミュータブルを区別しなくていいの?
調べてみると「JavaScriptはプリミティブ値は値渡し、オブジェクトは参照渡し」と解説している記事が見つかります。
JavaScriptに「参照渡し」はないので、これは誤りでいいと思います。JavaScript以外の「参照渡し」のない言語や、あっても明示的に書かなければそうならない言語についても同様です。(そもそも暗黙的に参照渡しされる言語ってあるんですか?)
言葉の使い方が統一されていない話題なので、この記事で言う「共有渡し」を「参照渡し」と呼んでいるのだとすれば間違いとは言えませんが、恐らく誤解を元に書かれているのだと思います。
また、「JavaScriptはプリミティブ値は値渡し、オブジェクトは共有渡し(参照の値渡しなどの言い方も含む)」としている記事もみかけます。
実はプリミティブ値(イミュータブル値も同様)については値渡しと共有渡しで挙動が全く変わらないので区別が付きません。
なので「プリミティブ値は値渡し、オブジェクトは共有渡し」も「全て共有渡し」も同じです。
であれば「全て共有渡し」として区別せずに理解する方が負担が少なくて良いと思います。
ちなみに、実際の内部実装については、JavaScriptの場合を調べてみましたがよくわかりませんでした。
Javascriptの仕様で「オブジェクトは共有渡しで、プリミティブ値は値渡し」と区別しているのか
ECMAScript 仕様書
仕様書のPutValue
のあたりをよく読めばわかりそうな気もします。
Haskellは?
純粋関数型言語には変数がないので何渡しでも挙動は同じですが、強いて言えば「遅延評価」です。
この記事の対象にしてるのは全て正格評価の言語です。
こちらを参照してください↓
評価戦略 - Wikipedia
結局呼び方どうしたらいいだろう?
関数の「○○渡し」については呼び方が定まっていないようです。
それを統一しようと試みている方もいらっしゃいました。
それに加えて、ここで私が提案したいのは
「関数の引数の話だけでなく、代入の場合もまとめて同じ呼び方で呼んだらどうか」ということです。
この記事で見てきたように、引数の渡し方と代入のやり方にはハッキリした対応関係があります。
現状、代入の際に「○○渡し」という言葉を使うと注意を受けてしまうようですが
私にはそこを区別するメリットがわかりません。
- 値渡し的な代入を「値渡し」
- 共有渡し的な代入を「共有渡し」(または「参照の値渡し」か、覇権を取った呼び方)
- C++の
&b = a
のような代入を「参照渡し」
と呼んでもよいのではないでしょうか。
対応関係に例外があるのだとしても、それが少数ならば、都度言及することにして、基本的には同じ呼び方をしてよい気がしています。今の所私は例外を一つも知りません。
(ここで言う例外とは、例えば代入の際は値渡しをするが関数に渡す際は共有渡しをするような言語や型のことです)
おわりに
この記事と同じような手法を使って「ディープコピー」「シャローコピー」の理解を促進する記事を書きました。よろしければお読みください。
もしこの記事に間違いがありましたら、ご指摘いただけるとありがたいです。よろしくおねがいします。