Java, PHP習得者がDartを学ぶ 3

スマホアプリ開発を夢見てFlutter/Dartの勉強を真剣にはじめてみたバックエンドエンジニア歴十数年の筆者が躓いた点やプログラミングに関してあらためて理解を深めたこと、などを記した備忘録です。

値渡し、参照渡し

ある変数をDartの関数に引数として渡した場合、値渡し、参照渡しのどちらを取るのか確認したくて、サンプルコードを実行して調べていたのですが、かなりあいまいな認識だったためここに整理することにしました。値渡し、参照渡しの他にもう一つタイプがあったのです。

  1. 値渡し ⇒ 変数の値のコピーを渡す
  2. 参照渡し ⇒ 参照(変数のアドレス)を渡す
  3. 参照の値渡し ⇒ 参照(変数のアドレス)のコピーを渡す

JavaやDartは3の挙動を取ります。
正直2と3が区別されていることを知らず、Javaでメソッドの引数にオブジェクト変数を渡すとメソッド側での変更が呼び出し元にも反映されることを「参照渡し」と呼んでいました。意識して区別しておかないと不具合のもとになりそうです。
これでサンプルコードの挙動とその理由がつながりました。

以降はサンプルプログラムを実行して確認した結果です。

変数を関数に引数として渡した場合の値の変化

関数mapfuncの引数に設定されているのはMapオブジェクトの参照のコピーなので、関数の中でbの要素を操作すると呼び出し元のaにも反映される。

void main() {
    Map<String, dynamic> a = {'num':0, 'message':"Hello!"};
    print("a $a");
    mapfunc(a);
    print("a $a");
}
void mapfunc(Map b) {
    b["num"] = 1; // 呼び出し元のListオブジェクトを操作
    b["message"] = "How are you"; // 呼び出し元のListオブジェクトを操作
    b["reply"] = "I'm fine thank you"; // 呼び出し元のListオブジェクトを操作
    print("b $b");
}
// a {num: 0, message: Hello!}
// b {num: 1, message: How are you, reply: I'm fine thank you}
// a {num: 1, message: How are you, reply: I'm fine thank you}

List、クラスを引数に指定する際も同様。以下はListの例

void main() {
    List<int> a = [1,2];
    print("a $a");
    listfunc(a);
    print("a $a");
}
void listfunc(List  b) {
    b[0]= 2; // 呼び出し元のListオブジェクトを操作
    print("b $b");
}
// a [1, 2]
// b [2, 2]
// a [2, 2]

調査をはじめたきっかけとなったサンプルコード。
8行目で新しいオブジェクトへの参照が代入されているため、listfunc内ではbの出力値が変わっているが、元々引数に設定されていたのは、呼び出し元のListオブジェクトの参照のコピーなので、呼び出し元のaには影響がない。

void main() {
  List<int> a = [1,2];
  print("a $a");
  listfunc(a);
  print("a $a");
}
void listfunc(List  b) {
  b = List.from([100,200]); // ここで新しいオブジェクトへの参照が代入される
  print("b $b");
}
// a [1, 2]
// b [100, 200]
// a [1, 2]

変数を変数に代入する場合の値の変化

bに代入されているのはMapオブジェクトの参照のコピーなので、bの要素を操作すると呼び出し元も同じように反映される。

void main() {
    Map<String, dynamic> a = {"num":0, "message":"Hello!"};
    print("a $a");
    Map b = a;
    b["num"] = 1;
    a["message"] = "How are you";
    b["reply"] = "I'm fine thank you";
    print("a $a");
    print("b $b");
}
// a {num: 0, message: Hello!}
// a {num: 1, message: How are you, reply: I'm fine thank you}
// b {num: 1, message: How are you, reply: I'm fine thank you}
void main() {
    List<int> a = [1,2];
    print("a $a");
    List<int> b = a;
    b[0] = 2;
    print("a $a");
    print("b $b");
}
// a [1, 2]
// a [2, 2]
// b [2, 2]

下の例は、4行目でbに代入されるのはaの参照のコピーであるが、5行目で新しいListオブジェクトの参照がbに代入されるため、bの出力値が変わるが、aの値には影響がない。

void main() {
    List<int> a = [1,2];
    print("a $a");
    List<int> b = a;
    b = [10,20]; // 新しいListオブジェクトへの参照が代入される
    print("a $a");
    print("b $b");
}
// a [1, 2]
// a [1, 2]
// b [10, 20]

それではintやString型ではどうなるか。
2行目でbに代入されるのはaの参照のコピーであるが、3行目でbに新たに値が設定されると新しいintオブジェクト(※)の参照がbに代入されるため、
bの出力値が変わるが、aの値には影響がない。

void main() {
    int a = 0;
    int b = a; // aの参照のコピーがbに代入される
    b = 1;     // 新しいintオブジェクトへの参照が代入される
    print("a=$a");
    print("b=$b");
}
// a=0
// b=1

※サンプルコードは「Flutter 3.7.6 Dart SDK 2.19.3」で実行しました。