【Dart】同一オブジェクト判定にidentical

  • 2023年10月25日
  • 2023年10月25日
  • Dart

対象者

  • Dartプログラミング言語を学び始めたばかりで、オブジェクトの同一性と等価性の違いについて深く理解したい方
  • identical関数と==演算子の使い分けや、それらの違いを明確に把握したいソフトウェアエンジニア
  • 効率的でバグの少ないコードを書くスキルを身につけ、プロジェクトをスムーズに進めたいと考えている方

はじめに

プログラミングの世界では、コードの効率性と正確性が求められます。特にDart言語を学び始めたばかりの方々にとって、オブジェクトの同一性と等価性の違いを理解することは、バグの少ないクリーンなコードを書く上で非常に重要です。この記事では、Dartにおけるidentical関数と==演算子の使い分け、それらの違いに焦点を当てて解説しています。これにより、あなたのコードのパフォーマンスを最適化し、より効率的なプログラミングが可能になります。

identical というのは、日本語で「同一の」や「全く同じ」という意味です。Flutterにおいては、オブジェクトの同一性を判断するための関数です。そのため、異なるインスタンスが同じ状態を持っているかどうかを超えて、本当に同じオブジェクトを指しているかを厳密にチェックすることができます。
実際のアプリとしては、シングルトンパターンを実装したクラスのインスタンスが本当に一つしか存在しないかを確認するケースにidenticalを使用するというような機能を実現することができます。これにより、アプリケーション全体で状態を共有する際のバグを防ぐことが可能になります。

この記事を読むことで、あなたはDart言語におけるオブジェクトの同一性と等価性の違いを深く理解し、バグの少ない効率的なコードを書くスキルを身につけることができるでしょう。それでは、Dartの世界へ一緒に深く潜っていきましょう。

identicalとは

Dart言語において、オブジェクトの同一性を判断する際にidentical関数は重要な役割を果たします。この関数は、二つのオブジェクトが同じインスタンスを指しているかどうかを確認するために使用されます。

Dartにおけるオブジェクト参照

Dartでは、変数はオブジェクトへの参照を保持しています。したがって、二つの変数が同じオブジェクトを指しているかどうかを判断することは、プログラムの正確な動作を保証する上で非常に重要です。identical関数は、このような比較を行うための信頼性の高い方法を提供します。

オブジェクトの同一性とは

オブジェクトの同一性とは、プログラミングにおいて、二つのオブジェクトが全く同じものであるかどうかを判断することを指します。これは、オブジェクトが持っている値や状態が同じであるかどうかではなく、メモリ上で同じ場所を指しているかどうかに基づいています。

例えば、「Flutter完璧ガイド」という本が2冊あります。内容は同じでも、これらは二つの異なる物理的な本です。プログラミングの世界で言えば、これらは同じ値を持っているが異なるオブジェクトであり、オブジェクトの同一性は「False」となります。

identical関数の基本的な使い方

identical関数は、二つのオブジェクトを引数として取り、それらが同じインスタンスを指しているかどうかをブール値で返します。基本的な使用方法は非常にシンプルで、以下のように記述することができます。

var obj1 = Object();
var obj2 = Object();
expect(identical(obj1, obj2), isFalse);

この例では、obj1obj2は異なるオブジェクトを指しているため、identical関数はfalseを返します。もし同じオブジェクトを指している場合は、trueを返します。

identical関数はDartにおいてオブジェクトの同一性を判断する際に非常に重要な役割を果たします。オブジェクト参照の比較を行う際には、この関数を使用することで、正確で信頼性の高い結果を得ることができます。

identicalの使用例

identical関数はDartプログラミングにおいて多岐にわたるシーンで活用されます。この関数の主な役割は、二つのオブジェクトが同じインスタンスを指しているかを判断することです。

シングルトンパターンでの利用

シングルトンパターンは、クラスのインスタンスが一つしか存在しないことを保証するデザインパターンです。identical関数を使用することで、異なる場所からアクセスされたインスタンスが本当に同一のものであるかを確認することができます。これにより、シングルトンの特性を保ちながらプログラムを安全に実行することが可能です。

class Singleton {
  static final Singleton _instance = Singleton._internal();

  Singleton._internal();

  static Singleton get instance {
    return _instance;
  }
}


test('Singleton', () {
    var singleton1 = Singleton.instance;
    var singleton2 = Singleton.instance;
    expect(identical(singleton1, singleton2), isTrue);
});

パフォーマンス最適化

identical関数は非常に高速に動作するため、オブジェクトの同一性を頻繁にチェックする必要がある場合にパフォーマンスの最適化として利用することができます。

オブジェクトの変更検出

オブジェクトが変更されたかどうかを検出する際にもidentical関数が役立ちます。オブジェクトの参照が変わっているかどうかをチェックすることで、変更を検出することが可能です。

デバッグとトラブルシューティング

プログラムのデバッグやトラブルシューティングを行う際に、オブジェクトが予期せずに変更されていないかを確認するためにidentical関数を使用することができます。これにより、バグの原因を素早く特定し、修正することが可能となります。

結論として、identical関数はDartプログラミングにおいて非常に幅広い用途で利用される重要なツールです。その高速な動作とシンプルな使用法により、プログラムの安全性と効率性を向上させることができます。

identicalと==演算子

Dartにおいて、オブジェクトの等価性を判断するためには==演算子とidentical関数の二つの方法がありますが、これらは異なるアプローチを取ります。

==演算子のカスタマイズ

==演算子はクラスによってオーバーライドすることができます。これにより、オブジェクトの属性を基にした独自の等価性判断を実装することが可能です。例えば、以下のコードではPersonクラスのインスタンスが名前と年齢が同じであれば等しいと判断されます。

class Person {
  final String name;
  final int age;
  Person(this.name, this.age);

  @override
  bool operator ==(Object other) {
    if (identical(this, other)) return true;
    return other is Person && other.name == name && other.age == age;
  }

  @override
  int get hashCode => Object.hash(name, age);

}

final person1 = Person('John', 30);
final person2 = Person('John', 30);

expect(identical(person1, person2), isFalse);
expect(person1 == person2, isTrue);

identicalと==の違い

一方でidentical関数はオブジェクトの参照が同じであるかどうかを判断します。これは==演算子がオーバーライドされていても影響を受けません。上記の例でidentical(person1, person2)を評価するとfalseが返されます。

いつ==を使い、いつidenticalを使うべきか

一般的に、オブジェクトの内容が等しいかどうかを判断したい場合は==を使用し、オブジェクトの参照が同じかどうかを判断したい場合はidenticalを使用します。==はカスタマイズ可能で柔軟な比較が可能ですが、identicalはより高速でシンプルな比較を提供します。

identicalの注意点と落とし穴

Darにおいて、identical関数は非常に強力でありながら、注意深く使用しなければならないいくつかの落とし穴があります。

identicalとnullの関係

identical関数はnull値を正しく処理します。つまり、二つのnull値は互いにidenticalとみなされます。しかし、nullと非nullのオブジェクトを比較した場合は常にfalseが返されます。これは期待通りの動作ですが、nullチェックを忘れると予期しないバグの原因となることがあります。

expect(identical(null, null), isTrue);

final nullValue = null;
expect(identical(nullValue, null), isTrue);

final notNullValue = 1;
expect(identical(notNullValue, null), isFalse);

オブジェクトのコピーとidentical

オブジェクトをコピーした場合、新しいオブジェクトは元のオブジェクトとは異なるメモリアドレスを持つため、identical関数はfalseを返します。これは例えば、オブジェクトを変更不可にしたい場合などに注意が必要です。

class Person {
 
  Person copyWith(int newAge) {
    return Person(name, newAge);
  }
}

identicalの罠と回避策

identicalは非常に高速である一方で、そのシンプルさが罠となることがあります。例えば、オブジェクトの内容が同じでも異なるインスタンスであれば、identicalはfalseを返します。これを回避するためには、オブジェクトの内容を比較するカスタム関数を用意するか、==演算子を適切にオーバーライドする必要があります。

結論として、identical関数はその高速性とシンプルさから多くの場面で有用ですが、その挙動を正しく理解し、適切な場面で使用することが重要です。null値の取り扱いに注意し、オブジェクトのコピーとの関係を理解し、必要に応じてカスタムの等価性判断を実装することで、identicalの落とし穴を回避することができます。

オブジェクトの同一性の実例

プログラミングにおいて「オブジェクトの同一性」を理解することは非常に重要です。特に、Riverpodのような状態管理ライブラリを使用する際には、オブジェクトが同じものを指しているのか、それとも別のオブジェクトなのかを正確に把握する必要があります。identical関数を使用して、リストやセット、マップなどのコレクションにおけるオブジェクトの同一性を確認する方法を紹介します。

まず、リストに関する例を見てみましょう。

// List
final list1 = [1, 2, 3];
final list2 = list1;
expect(identical(list1, list2), isTrue);

final list3 = [...list1];
expect(identical(list1, list3), isFalse);

final list4 = list1.toList();
expect(identical(list1, list4), isFalse);

final list5 = []..addAll(list1);
expect(identical(list1, list5), isFalse);

Listをそのまま使うと「同じオブジェクト」扱いですが、Spread式(ピリオド3つ)を使うと別のオブジェクトとして認識されています。これを利用してRiverpodでも変更検知をできるListを簡単に書けます。

// set
final set1 = {1, 2, 3};
final set2 = {...set1};
expect(identical(set1, set2), isFalse);

// map
final map1 = {'a': 'b'};
final map2 = {...map1};
expect(identical(map1, map2), isFalse);

Listだけでなく、SetやMapでもSpread式を使えば、新しいオブジェクトを作成し、元のオブジェクトとは異なる新しいオブジェクトになります。

Dartにおけるオブジェクトの同一性とインターンの理解

Dartにおけるオブジェクトの同一性とインターンについて、具体的なコード例を交えて解説します。
(Dartについてのインターンの情報が見つからないので、Pythonを参考にしてます。結果を見る限り使われていると思いますが、文献やコードを確認したわけではないので、話半分でお願いします)

オブジェクトの同一性

Dartでは、identical関数を使用して二つのオブジェクトが同一のものであるかを判断できます。例えば、以下のコードを見てみましょう。

final a = 'abc';
final b = a;
final c = 'abc';

expect(identical(a, b), isTrue);
expect(identical(a, c), isTrue);

ここで、abは同じオブジェクトを指しているため、identical(a, b)trueを返します。一方で、acも同じ文字列を持っていますが、異なる場所で宣言しているため同一のオブジェクトではありません。しかし、これもidentical(a, c)trueを返します。これは、Dartが文字列リテラルに対してインターンを行っているためです。

インターンとは

インターンとは、同じ内容を持つオブジェクトを複数作成する代わりに、一つのオブジェクトを共有することでメモリ使用量を削減する技術です。Dartでは、文字列リテラルに対して自動的にインターンが行われます。そのため、同じ文字列リテラルがプログラム内で複数回使用された場合、それらは全て同じオブジェクトを指します。

整数の場合

整数に関しても同様の挙動が見られます。

var int1 = 1;
var int2 = 1;
expect(identical(int1, int2), isTrue);

int1int2は同じ整数値を持っていますが、別のオブジェクトにみえます。しかしidentical(int1, int2)trueを返します。これもDartが整数値に対してインターンを行っているためです。

文字列や整数のような基本的なデータ型に対しては、Dartが自動的にインターンを行うことでメモリ効率が向上しています。しかし、この挙動を理解していないと予期せぬバグを引き起こす可能性があるため、注意が必要です。

Q&A

Q1: Dartのidentical関数はどのような場合に使用するのが適切ですか?

A1: identical関数は、二つのオブジェクトが同一のインスタンスを指しているかどうかを判断する際に使用します。オブジェクトの変更検出、シングルトンパターンのインスタンス確認、パフォーマンス最適化などの状況で特に有効です。

Q2: identical==演算子の主な違いは何ですか?

A2: identicalはオブジェクトの同一性をチェックし、二つのオブジェクトが同じインスタンスを指しているかどうかを判断します。一方で、==演算子はオブジェクトの等価性をチェックし、オブジェクトの内容が等しいかどうかを判断します。カスタマイズ可能な==演算子を使用すると、オブジェクトの内容に基づいて等価性を定義できます。

Q3: identical関数を使用する際の注意点は何ですか?

A3: identical関数を使用する際には、特にnullとの比較やオブジェクトのコピーに注意が必要です。また、オブジェクトの同一性だけでなく、内容の等価性も考慮する必要がある場合は==演算子を使用することを検討すると良いでしょう。

まとめ

この記事を通して、読者の皆さんはDartのidentical関数について深く理解しました。オブジェクト参照の仕組みと、identical関数を使ってオブジェクトが同一かどうかを判断する基本的な方法を学びました。シングルトンパターンの利用やパフォーマンス最適化、オブジェクトの変更検出、デバッグとトラブルシューティングにおいてidenticalがどのように役立つかも理解しました。

参考

ソース(main.dartにコピペして動作確認用)

import 'package:flutter_test/flutter_test.dart';

class Singleton {
  static final Singleton _instance = Singleton._internal();

  Singleton._internal();

  static Singleton get instance {
    return _instance;
  }
}

class Person {
  final String name;
  final int age;
  Person(this.name, this.age);

  @override
  bool operator ==(Object other) {
    if (identical(this, other)) return true;
    return other is Person && other.name == name && other.age == age;
  }

  @override
  int get hashCode => Object.hash(name, age);

  Person copyWith(int newAge) {
    return Person(name, newAge);
  }
}

void main() {
  test('basic', () {
    var obj1 = Object();
    var obj2 = Object();
    expect(identical(obj1, obj2), isFalse);
  });

  test('', () async {
    final a = 'abc';
    final b = a;
    final c = 'abc';

    expect(identical(a, b), isTrue);
    expect(identical(a, c), isTrue);

    var int1 = 1;
    var int2 = 1;
    expect(identical(int1, int2), isTrue);
  });

  test('Singleton', () {
    var singleton1 = Singleton.instance;
    var singleton2 = Singleton.instance;
    expect(identical(singleton1, singleton2), isTrue);
  });

  test('Person', () {
    final person1 = Person('John', 30);
    final person2 = Person('John', 30);
    final person3 = Person('Michel', 30);
    final person4 = Person('John', 32);

    expect(identical(person1, person2), isFalse);

    expect(person1 == person2, isTrue);
    expect(person1 == person3, isFalse);
    expect(person1 == person4, isFalse);
  });

  test('copyWith', () {
    final person1 = Person('John', 30);
    final person2 = person1.copyWith(30);

    expect(identical(person1, person2), isFalse);
    expect(person1 == person2, isTrue);
  });

  test('null', () {
    expect(identical(null, null), isTrue);

    final nullValue = null;
    expect(identical(nullValue, null), isTrue);

    final notNullValue = 1;
    expect(identical(notNullValue, null), isFalse);
  });

  test('List', () {
    final list1 = [1, 2, 3];
    final list2 = list1;
    expect(identical(list1, list2), isTrue);

    final list3 = [...list1];
    expect(identical(list1, list3), isFalse);

    final list4 = list1.toList();
    expect(identical(list1, list4), isFalse);

    final list5 = []..addAll(list1);
    expect(identical(list1, list5), isFalse);
  });

  test('other collections', () {
    final set1 = {1, 2, 3};
    final set2 = {...set1};
    expect(identical(set1, set2), isFalse);

    final map1 = {'a': 'b'};
    final map2 = {...map1};
    expect(identical(map1, map2), isFalse);
  });
}