This tutorial explains the default behavior of object comparison in Dart and how you can change it.
Just like other programming languages, Dart has an operator for comparing two objects. The operator is called equality and it uses a double equals symbol (==
). It's used to check whether two objects are equal. If you want to understand how the operator works or change the behavior, you can read this tutorial.
Comparing Primitive Immutable Data Types
As you already know, Dart comes with some primitive data types. There are int, double, boolean, and string. All of them are immutable. Once the value is created, it cannot be mutated. A Dart variable can be reassigned if it's not declared with const
or final
. Below is a string variable declared using var
modifier, which means it can be reassigned.
var str1 = 'This is a string';
str1 = 'This is a new string';
In the example above, even though the variable is reassigned, the first string is not modified. Instead, Dart creates a new object at a different memory location. That's the basics of the immutability concept that you need to understand.
Each object in Dart has an equality operator (==
). By default, it returns true if the compared objects refer to the same object in the memory.
In the code below, we have several variables with primitive data types and we are going to compare them using the equality operator..
int i1 = 1;
int i2 = 1;
int i3 = 2;
double d1 = 1.0;
double d2 = 1.0;
double d3 = 1.5;
bool b1 = true;
bool b2 = true;
bool b3 = false;
String s1 = 'text';
String s2 = 'text';
String s3 = 'another text';
print('i1 == i2: ${i1 == i2}');
print('i1 == i3: ${i1 == i3}');
print('d1 == d2: ${d1 == d2}');
print('d1 == d3: ${d1 == d3}');
print('b1 == b2: ${b1 == b2}');
print('b1 == b3: ${b1 == b3}');
print('s1 == s2: ${s1 == s2}');
print('s1 == s3: ${s1 == s3}');
Output:
i1 == i2: true
i1 == i3: false
d1 == d2: true
d1 == d3: false
b1 == b2: true
b1 == b3: false
s1 == s2: true
s1 == s3: false
As you can see from the output, if two values are equal, it will return true. As I have written above, the default behavior of the equality operator is that it returns true if two objects have the same memory reference. Does it mean two variables with a primitive data type and the same value refer to the same object, even though they are declared separately.
For a better understanding, let's see the example below. Dart has a method named identical
which checks whether two references are to the same object. What if we try to use the identical
for comparing primitive data types.
print('identical(i1, i2): ${identical(i1, i2)}');
print('identical(i1, i3): ${identical(i1, i3)}');
print('identical(d1, d2): ${identical(d1, d2)}');
print('identical(d1, d3): ${identical(d1, d3)}');
print('identical(b1, b2): ${identical(b1, b2)}');
print('identical(b1, b3): ${identical(b1, b3)}');
print('identical(s1, s2): ${identical(s1, s2)}');
print('identical(s1, s3): ${identical(s1, s3)}');
Output:
identical(i1, i2): true
identical(i1, i3): false
identical(d1, d2): true
identical(d1, d3): false
identical(b1, b2): true
identical(b1, b3): false
identical(s1, s2): true
identical(s1, s3): false
The output shows that if two objects are immutable and have the same value, they refer to the same object in the memory. Primitive data types which include int, double, boolean, and string are immutable. As a result, Dart can canonicalize their literals even though you don't declare the variables as const
. That explains how the equality operator works on primitive data types.
Comparing Mutable Data Types
Dart also has built-in mutable data types such as List
, Set
, and Map
. In addition, you can also create a custom class to define your own data type. For example, we have a class named Item
as shown below.
class Item {
final String name;
final double price;
const Item({
required this.name,
required this.price,
});
}
Then, we create some instances of the class and compare the instances.
final Item itemA1 = Item(name: 'A', price: 100);
final Item itemA2 = Item(name: 'A', price: 100);
final Item itemA3 = itemA1;
final Item itemB = Item(name: 'B', price: 200);
print('itemA1 == itemA2: ${itemA1 == itemA2}');
print('itemA1 == itemA3: ${itemA1 == itemA3}');
print('itemA1 == itemB: ${itemA1 == itemB}');
print('identical(itemA1, itemA2): ${identical(itemA1, itemA2)}');
print('identical(itemA1, itemA3): ${identical(itemA1, itemA3)}');
print('identical(itemA1, itemB): ${identical(itemA1, itemB)}');
Output:
itemA1 == itemA2: false
itemA1 == itemA3: true
itemA1 == itemB: false
identical(itemA1, itemA2): false
identical(itemA1, itemA3): true
identical(itemA1, itemB): false
It's very clear that the result of itemA1 == itemB
is false because they are different objects whose fields have different values. Meanwhile, itemA1 == itemA3
returns true because the object of itemA3
is assigned to itemA1
. As a result, they refer to the same object in the memory.
The most interesting result is itemA1 == itemA3
. From the explanation above, the result makes sense because the two variables do not refer to the same object, despite having the same value. The question is, is it possible to make the comparison returns true. The answer is yes, and there are several ways to do it.
Canonicalization with const
Constructor
First, you can create the objects using const
constructor. That allows Dart to perform canonicalization if there are multiple callers that create the object using the same arguments. Therefore, if Dart detects a previous instance with the same arguments already created, it will refer to the existing object.
To define a const
constructor, all fields of the class must be final. The Item
class above actually uses a const
constructor. But the example above doesn't use the const
modifier when constructing the objects. Below is a different example where the constructor is called using the const
modifier to enable canonicalization.
final Item itemA4 = const Item(name: 'A', price: 100);
final Item itemA5 = const Item(name: 'A', price: 100);
print('itemA4 == itemA5: ${itemA4 == itemA5}');
print('identical(itemA4, itemA5): ${identical(itemA4, itemA5)}');
Output:
itemA4 == itemA5: true
identical(itemA4, itemA5): true
Because of the canonicalization, the itemA5
refers to the same object as the itemA4
. The result shows that those two objects are identical. As a result, the comparison also returns true.
Below is another way to declare the variables that also triggers the const
constructor to perform canonicalization.
const Item itemA4 = Item(name: 'A', price: 100);
const Item itemA5 = Item(name: 'A', price: 100);
Override Equality Operator
The equality operator can be overridden as well. For example, you can change the logic to return true if each field has the same value. In the code below, we override the equality operator to return true if the compared objects have the same name
and price
. By overriding the method, Dart no longer uses the memory reference for comparison. Keep in mind that if the equality operator is overridden, you also have to override the hashCode
property to keep the consistency.
class Item {
final String name;
final double price;
const Item({
required this.name,
required this.price,
});
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
return (other is Item
&& other.runtimeType == runtimeType
&& other.name == name
&& other.price == price
);
}
@override
int get hashCode => Object.hash(name, price);
}
If you try to run the comparison, it will return true if the name
and price
fields have the same values, despite the fact that they are two different objects in the memory.
print('itemA1 == itemA2: ${itemA1 == itemA2}');
print('identical(itemA1, itemA2): ${identical(itemA1, itemA2)}');
Output:
itemA1 == itemA2: true
identical(itemA1, itemA2): false
Using equatable
Package
An easier way to override the equality operator is by using the equatable
package. You can install it by using dart pub add equatable
(or flutter pub add equatable
for Flutter).
To use the package, you need to import package:equatable/equatable.dart
. Then, modify the class to extend Equatable
. It requires you to override the props
getter whose value is the list of fields or properties to be compared. Equatable
will override the equality operator and the hashCode
property, so you don't need to override them by yourself. If the class already extends another class, you can use the EquatableMixin
instead (replace extends Equatable
with with EquatableMixin
).
class Item extends Equatable {
final String name;
final double price;
const Item({
required this.name,
required this.price,
});
@override
List<Object?> get props => [name, price];
}
The result should be the same as the previous example.
Summary
In this tutorial, we have learned the fundamentals of object comparison in Dart. By default, Dart uses memory reference to determine whether two objects are equal. For primitive immutable data types, Dart canonicalizes the same values to refer to the same object in the memory. For mutable data types, if the compared objects refer to different instances, the comparison will return false by default. You can override the equality operator by yourself or using the equatable
package. Alternatively, you can use a const
constructor if you want Dart to canonicalize objects created with the same arguments.
You can also read about: