Dartでオブジェクトをコピーする際に気をつけなければならないこと

はじめに

アプリを作っているとデータの編集機能を作ることがあるかと思います。その際、アプリが持っているオブジェクトデータを書き換えてしまうと、保存する処理の前にあたかも編集が完了したかのように見えてしまいます。

なので、編集機能では編集したいオブジェクトのコピーを渡すことで保存が確定するまでは編集元のデータが書き換わりません。

しかし、オブジェクトのコピーの際に気をつけなければならないことがあります。本記事ではオブジェクトのコピーについてちょこっと書いていこうと思います。

最近Flutterにハマっているので、Dartという言語を使いたいと思います。もしかしたら本記事の読者もFlutterをお使いの方が多いのではないでしょうか。

環境

  • MacOS BigSur 11.4
  • Dart 2.15

サンプルプログラム

本棚クラスと本クラスを用意します。

本棚には複数の本が格納できる、というものです。

void main() {
  Book book1 = Book('this is book1');
  Book book2 = Book('this is book2');
  List<Book> booksForBookshelfA = [book1, book2];
  Bookshelf bookshelfA = Bookshelf('bookshelf A', booksForBookshelfA);

  Book book3 = Book('this is book3');
  Book book4 = Book('this is book4');
  List<Book> booksForBookshelfB = [book3, book4];
  Bookshelf bookshelfB = Bookshelf('bookshelf B', booksForBookshelfB);

  bookshelfA.showContents();
  bookshelfB.showContents();

}

class Bookshelf {

  List<Book> _books = [];
  List<Book> get books => _books;

  late String _name;
  String get name => _name;

  Bookshelf(this._name, this._books);

  void updateName(String newName) {
    _name = newName;
  }

  void showContents() {
    print('======================================');
    print('Bookshelf Name: $name');
    print('Books:');
    books.forEach((book) { print('- ${book.title}'); });
  }
}

class Book {
  late String _title;
  String get title => _title;

  Book(this._title);
}

これを実行するとこんな感じになります。

======================================
Bookshelf Name: bookshelf A
Books:
- this is book1
- this is book2
======================================
Bookshelf Name: bookshelf B
Books:
- this is book3
- this is book4

本棚(Bookshelf)をコピーしたい

さて、本棚をコピーしたいと思います。

まずはBookshelfにコピーメソッド copyを作成します。

class Bookshelf {

  List<Book> _books = [];
  List<Book> get books => _books;

  late String _name;
  String get name => _name;

  Bookshelf(this._name, this._books);

  void updateName(String newName) {
    _name = newName;
  }

  void showContents() {
    print('======================================');
    print('Bookshelf Name: $name');
    print('Books:');
    books.forEach((book) { print('- ${book.title}'); });
  }

  // コピーメソッド
  Bookshelf copy() {
    return Bookshelf(name, books);
  }
}

ではbookshelfAで copyメソッドを実行して中身を見てみましょう

Bookshelf copiedBookshelfA = bookshelfA.copy(); // コピーを作成
copiedBookshelfA.showContents();

実行結果

======================================
Bookshelf Name: bookshelf A
Books:
- this is book1
- this is book2

元の bookshelfAと変化なしですね。コピーできてそうです。

本棚の中身を変更してみる

ではコピーされた本棚の中に入っている booksを変更してみましょう。

まずは Bookにタイトルを変更するメソッドを作成します。

class Book {
  late String _title;
  String get title => _title;

  Book(this._title);
  
  void updateTitle(String newTitle) {
    _title = newTitle;
  }
}

そしてコピーされた本棚オブジェクトの本のタイトルを変更します

  Bookshelf copiedBookshelfA = bookshelfA.copy();
  copiedBookshelfA.showContents();
  copiedBookshelfA.books[0].updateTitle('new book title!');
  copiedBookshelfA.showContents();

実行すると、

======================================
Bookshelf Name: bookshelf A
Books:
- this is book1
- this is book2
======================================
Bookshelf Name: bookshelf A
Books:
- new book title!
- this is book2

元々 this is book1だったのが new book title!に変更されていますね。成功です!

ここで、元のbookshelfAも見てみましょう

  Bookshelf copiedBookshelfA = bookshelfA.copy();
  copiedBookshelfA.showContents();
  copiedBookshelfA.books[0].updateTitle('new book title!');
  copiedBookshelfA.showContents();
  bookshelfA.showContents();

実行結果

======================================
Bookshelf Name: bookshelf A
Books:
- this is book1
- this is book2
======================================
Bookshelf Name: bookshelf A
Books:
- new book title!
- this is book2
======================================
Bookshelf Name: bookshelf A
Books:
- new book title!
- this is book2

!!??

元の bookshelfAオブジェクトの本のタイトルも変わっています。。

オブジェクトの中のオブジェクトはコピーされていない

当たり前かもしれませんが、コピーメソッドを見てみると、

Bookshelf copy() {
    return Bookshelf(name, books);
  }

booksをそのまま渡しているので、現状だと copiedBookshelfAbooksbookshelfAbooksは同じものを指しています。

これだと、どちらかに変更が入るともう片方も編集されることになります。

ではどう解決すればいいか?

全てのオブジェクトにコピーメソッドを作る

オブジェクトの中のオブジェクトも明示的にコピーしてあげる必要があります。

// Bookクラスにコピーメソッドを追加
Book copy() {
    return Book(title);
  }

// Bookshelfクラスのコピーメソッドを編集
Bookshelf copy() {
    List<Book> copiedBooks = books.map((book) => book.copy()).toList();
    return Bookshelf(name, copiedBooks);
  }

これを入れた上で再度実行してみましょう

  Bookshelf copiedBookshelfA = bookshelfA.copy();
  copiedBookshelfA.showContents();
  copiedBookshelfA.books[0].updateTitle('new book title!');
  copiedBookshelfA.showContents();
  bookshelfA.showContents();

実行結果↓
======================================
Bookshelf Name: bookshelf A
Books:
- this is book1
- this is book2
======================================
Bookshelf Name: bookshelf A
Books:
- new book title!
- this is book2
======================================
Bookshelf Name: bookshelf A
Books:
- this is book1
- this is book2

copiedBookshelfAの中のBookを編集してもコピー元のbookshelfAには変更がないことが確認できますね。

これで解決!

でも、多くのデータが詰まっているオブジェクトをコピーするときにこれを使うとメモリが気になる今日この頃です。

Related Posts