ディープコピー・シャローコピー…
しばしば混乱の原因になる言葉です。
ここでは、各コピー方法の違いが端的に現れる最小の例を使って、各コピー方法でプログラムの動きがどう違ってくるかを見ていきたいと思います。
断り書き
断り書きです。別に気にならないという方は「なにやるの?」の章まで読み飛ばしてください。
「共有渡し」という言葉を使います
「pass-by-sharing」「call-by-sharing」「共有渡し」「参照の値渡し」などと呼ばれているやり方の呼び方を、この記事内では「共有渡し」で統一します。
なお、筆者がこの用語を推奨しているわけではありません。複数言語の仕様を比較する便宜上、この記事内で用いる名前をつけているだけです。
代入の場合にも「○○渡し」という言葉を使うことにします
この記事内では、「参照渡し」「共有渡し」という言葉を「変数代入の際の方式」を表す際にも使います。
通常は関数に引数を渡す時に用いる言葉ですが、ほぼ同様の概念が代入の場合も成立するので、言葉もそのまま用いることにします。
(これは私見ですが、「○○渡し」という言葉を関数に引数を渡す時に限定して使うメリットがよくわかりません。代入の場合も使って良いと思っています。なのでこの記事ではそうします。)
新たな実体を生成しないものは「シャローコピー」と呼ばないことにします
検索してみると「シャローコピーとは参照またはポインタだけをコピーして、新たな実体を生成しないやり方を言う」という旨の記事がたくさんヒットするのですが、
この記事内では「新たな実体は生成するが、その中身のどこかでコピー前と同じ実体を参照している部分がある」というケースを「シャローコピー」と呼ぶことにします。
英語版Wikipedia や Pythonの公式ドキュメント でもそうなっています。
(これは私見ですが、少なくともこの「新たな実体を生成するがコピー元を参照している部分がある」というケースは「シャローコピー」と呼ばれるべきはずです。なので、「シャローコピーとは新たな実体を作らないものだ」という説明は誤りだと思います。「新たな実体を作らないものもシャローコピーと呼ぶ」なら矛盾はないですが、でもそれも誤りだと思います。)
#なにやるの?
変数b
に変数a
の「何かしらの意味でのコピー」を代入し、b
をいじったあと、a
の中身を確認するという流れにします。
その際、2種類のコピー方法だけではなく
- 参照渡し的代入
- 共有渡し的代入
- シャローコピーを代入
- ディープコピーを代入
の4つを比較していくことにします。
それぞれの用語の意味は、下でコードを説明する時に一緒に説明していくことにします。
最小の例の処理内容
void main() {
var a = [
["did deep copy"]
];
// ここで、bに、aの何かしらの意味でのコピーを代入する処理
b[0][0] = "did shallow copy";
b[0] = ["did pass-by-sharing"];
b = [
["did pass-by-reference"]
];
print(a[0][0]);
}
処理内容を紹介したいだけで、特定の言語に依存した話にしたくなかったので、使っている人の少なそうなDartで書いてみました。(最近増えてきた…?)
処理内容は
- 変数
a
に文字列のリストのリストを代入します。 - 変数
b
に、a
の何かしらの意味でのコピーを代入します。 -
b
の0番要素の0番要素に、違う文字列を代入します。 -
b
の0番要素に、違う文字列のリストを代入します。 -
b
に、違う文字列のリストのリストを代入します。 -
a
の0番要素の0番要素を出力します。
なぜこれで違いが端的に現れるのか見ていきましょう。
参照渡しの場合
仮にb = a
という代入が参照渡しだとすると、b
はa
を指し示す「参照」というものを持つことになり、その後b
に施した処理は全てa
にも影響します。
a
にb
という別名を付けた、と考えると動きを理解しやすいです。
この場合、先程のコードの動きはこうになります。
void main() {
var a = [
["did deep copy"]
];
// ここで、bに、aを参照渡し
// 以下の処理は全て、aに対して行ったのと同等
b[0][0] = "did shallow copy";
b[0] = ["did pass-by-sharing"];
b = [
["did pass-by-reference"]
];
print(a[0][0]); // did pass-by-reference
}
最後の
b = [
["did pass-by-reference"]
];
によってa
の中身はこの文字列を持つリストのリストになっています。
よって出力はdid pass-by-reference
となります。
ちなみにこの文字列は「参照渡ししました」という意味です。
共有渡しの場合
共有渡しというのは、厳密には渡し方だけを指す言葉ではありません。
共有渡しがなされる言語においては、そもそも変数の中身は値そのものではなく、その値への参照を持っています。(Java, Python, JavaScriptなど多くの言語がこれです。)
そして、b=a
とした際には、a
に格納されている「参照」をコピーしてb
にも格納します。
共有渡しが「参照の値渡し」と呼ばれることがあるのはこのためです。
この場合先程のコードは次のような動きになります。
void main() {
// aは、この二重配列への参照を持つ
var a = [
["did deep copy"]
];
// ここで、bに、aを共有渡し
// aとbは同じ実体を指す参照を持っているので、この処理はaに影響する
b[0][0] = "did shallow copy";
// この処理もaに影響する
b[0] = ["did pass-by-sharing"];
// ここで、bは新しい実体を指す新たな参照を格納されるので、この処理はaに影響しない
b = [
["did pass-by-reference"]
];
print(a[0][0]); // did pass-by-sharing
}
最後の
b = [
["did pass-by-reference"]
];
は、b
に「[["did pass-by-reference"]]
という、前とは違う実体を指す参照」を代入してしまっているため、この処理はa
に影響しません。
よって出力はdid pass-by-sharing
となります。
ちなみにこの文字列は「共有渡ししました」という意味です。
シャローコピーの場合
引き続き、変数の中身が値そのものではなく、その値への参照を持っている言語についての話です。(Java, Python, JavaScriptなど)
変数の中身が値そのものになっている言語(C++など)では、シャローコピーはありません。(ポインタや参照渡しを使って再現することはできると思います)
シャローコピーは、その名の通りコピーを作成します。
中身が「参照」の場合は、それをそのままコピーします。
どういうことか、コードで見てみましょう。
void main() {
// aは、この二重配列への参照を持つ
var a = [
["did deep copy"]
];
// ここで、aのシャローコピーを作成し、bに代入する
// aとbは違う実体を指す参照を持っている。
// ただし、どちらの実体も、その0番要素には同じ「参照」が入っているので、
// この処理はaに影響する
b[0][0] = "did shallow copy";
// bの0番要素が、それまでと違うものを指す参照に書き換えられる。
// この処理はaには影響しない。
b[0] = ["did pass-by-sharing"];
// ここで、bは新しい実体を指す新たな参照を格納されるので、この処理もaに影響しない
b = [
["did pass-by-reference"]
];
print(a[0][0]); // did shallow copy
}
a
と、そのシャローコピーは、実体としては違うものです。Pythonでいうid
やDartでいうhashcode
を出力して確認すると、a
とb
が異なる実体を指しているのがわかります。
ただし、その中身は同じものがコピーされています。
もっと言うと、同じものを指す「参照」がコピーされています。
なので、その「中身の参照先」の中身を書き換えると、a
とb
ともに影響します。
それがこれです。
b[0][0] = "did shallow copy";
一方、「b
の中身」自体を書き換えてしまうと、a
には影響しなくなります。
それが
b[0] = ["did pass-by-sharing"];
です。この処理はa
に影響しません。結果、出力はdid shallow copy
となります。
ディープコピーの場合
ディープコピーもシャローコピーと同様、コピーを作成するのですが、
中身の参照先の値を調べ、それもコピーします。
それも「参照」だった場合はその参照先の値を調べ、それもコピーします。
これを、参照先を全てコピーし終えるまで繰り返します。
a
とb
はもはや何も共有しません。
片方に行った操作は、他方に一切影響しません。
void main() {
// aは、この二重配列への参照を持つ
var a = [
["did deep copy"]
];
// ここで、bに、aのディープコピーを渡す。以降、bへの操作はaに影響しない。
b[0][0] = "did shallow copy";
b[0] = ["did pass-by-sharing"];
b = [
["did pass-by-reference"]
];
print(a[0][0]); // did deep copy
}
a
は何も変化していないので、初期値のdid deep copy
が出力されます。
実例で見てみよう
では実際にいくつかの言語で、どの方法で代入されているのか見ていくことにしましょう!!!
JavaScript
JavaScriptは、普通に代入すると共有渡しを行います。
a = [["did deep copy"]];
b = a;
b[0][0] = "did shallow copy";
b[0] = ["did pass-by-sharing"];
b = [["did pass-by-reference"]];
console.log(a[0][0]); // did pass-by-sharing
実体を共有したくない場合は、配列ならば、slice
を使ってこのようにするとコピーを作成して持たせることが出来ます。
a = [["did deep copy"]];
b = a.slice(0, a.length);
b[0][0] = "did shallow copy";
b[0] = ["did pass-by-sharing"];
b = [["did pass-by-reference"]];
console.log(a[0][0]); // did shallow copy
この場合のコピーはシャローコピーです。もしディープコピーをしたい場合は、工夫が必要になります。
配列ではなくオブジェクトの場合も普通に代入すると共有渡しです。コピーしたい場合は次のようにできます。
a = { x: { y: "did deep copy" } };
b = Object.assign({}, a); // ここで b = a とすると共有渡しになる
b.x.y = "did shallow copy";
b.x = { y: "did pass-by-sharing" };
b = { x: { y: "did pass-by-reference" } };
console.log(a.x.y); // did shallow copy
Python
続いてPythonです。
import copy
a = [['did deep copy']]
b = a
b[0][0] = 'did shallow copy'
b[0] = ['did pass-by-sharing']
b = [['did pass-by-reference']]
print(a[0][0]) # did pass-by-sharing
Pythonでも、普通に代入すると共有渡しになります。
Pythonにはcopy
というモジュールがあり、これを使うと明示的にシャローコピーやディープコピーを行うことが出来ます。
ドキュメントはこちら
copy --- 浅いコピーおよび深いコピー操作
copy.copy
でシャローコピーができます。
import copy
a = [['did deep copy']]
b = copy.copy(a)
b[0][0] = 'did shallow copy'
b[0] = ['did pass-by-sharing']
b = [['did pass-by-reference']]
print(a[0][0]) # did shallow copy
copy.deepcopy
でディープコピーができます。
import copy
a = [['did deep copy']]
b = copy.deepcopy(a)
b[0][0] = 'did shallow copy'
b[0] = ['did pass-by-sharing']
b = [['did pass-by-reference']]
print(a[0][0]) # did deep copy
オブジェクトもこのモジュールで同様にコピーできます。便利ですね。
Dart
void main() {
var a = [
["did deep copy"]
];
var b = a;
b[0][0] = "did shallow copy";
b[0] = ["did pass-by-sharing"];
b = [
["did pass-by-reference"]
];
print(a[0][0]); // did pass-by-sharing
}
Dartも普通に代入すると共有渡しになります。
C++
ここまでの言語と大きく異なる挙動を示すのがC++です。
C++は普通に代入すると共有渡しになりません。
そもそもC++では、変数の中身が「参照」ではなく「値そのもの」です。
それがそのままコピーされますので、その時点でコピー元との関係はなくなります。
コピーして作成した方をどういじっても、コピー元に影響しないわけですね。
実質的にディープコピーと全く同じ動きをします。
#include <iostream>
#include <vector>
using namespace std;
int main()
{
vector<vector<string>> a{vector<string>{"did deep copy"}};
vector<vector<string>> b = a;
b[0][0] = "did shallow copy";
b[0] = vector<string>{"did pass-by-sharing"};
b = vector<vector<string>>{vector<string>{"did pass-by-reference"}};
cout << a[0][0] << endl; // did deep copy
}
また、C++では参照渡しが可能です。
参照渡しの場合はコピーして作成した方にした操作が全てコピー元に影響します。
#include <iostream>
#include <vector>
using namespace std;
int main()
{
vector<vector<string>> a{vector<string>{"did deep copy"}};
vector<vector<string>> &b = a;
b[0][0] = "did shallow copy";
b[0] = vector<string>{"did pass-by-sharing"};
b = vector<vector<string>>{vector<string>{"did pass-by-reference"}};
cout << a[0][0] << endl; // did pass-by-reference
}
おわり
ここまで見てきたように、この処理を書いてみれば、代入時に何が代入されているのか明確にできると思います。
それを把握していれば予期せぬ挙動で悩まされることも少なくなるはずです。
また、似たような記事ですが、関数に引数を渡した時の挙動と代入時の挙動を比較して理解を促しているものもありますのでよろしければお読みください。
値渡し・共有渡し・参照渡しを最小の例(5行)で端的に理解する
もしこの記事に間違いがありましたら、ご指摘いただけると大変ありがたいです!よろしくお願いいたします。