다트에서 같다는 것은 무엇일까?
test('test 1', () {
// 1과 2는 다르다
expect(1 == 2, false);
// 1과 1은 같다.
expect(1 == 1, true);
// 1.0 과 1은 같다. (num 타입)
expect(1.0 == 1, true);
expect('Hello' == 'Hi', false);
expect('Hello' == 'hello', false);
// 대소문자까지 같아야 같다.
expect('Hello' == 'Hello', true);
// 다트에서 1은 true 가 아니다.
expect(1 == true, false);
// 0도 false가 아니다.
expect(0 == false, false);
});
/// Object
/// The base class for all Dart objects except `null`.
///
/// Because `Object` is a root of the non-nullable Dart class hierarchy,
/// every other non-`Null` Dart class is a subclass of `Object`.
class Point extends Object {
Point(this.y, this.x);
int y;
int x;
void change(int newY, int newX) {
y = newY;
x = newX;
}
}
사용할 클래스 (Point)
모든 non-null 객체는 Object를 확장하기 때문에, extends Object를 빼주어도 똑같다.
test('object test', () {
// (1,2) 와 (2,3)은 다르다.
final point1 = Point(1, 2);
expect(point1 == Point(2, 3), false);
// point2는 그대로 복사했으니 같다
final point2 = point1;
expect(point1 == point2, true);
/// 포인트 1의 값을 이용해 새로운 Point3을 만듬
// 들어있는 y와 x의 값은 같지만 다르다?
final newX = point1.x;
final newY = point1.y;
final point3 = Point(newY, newX);
expect(point1 == point3, false);
expect(identical(point1, point3), false);
// 값을 바꾼다.
// 값을 바꿨지만 point2는 point1과 같다?
point2.change(2, 3);
expect(point1 == point2, true);
});
들어있는 값이 같다고 해서 객체가 같다고 판단하지 않는다.
오히려 마지막 예제에서 보듯 값이 달라도 객체는 같을 수 있다.
자바에서는 인스턴스라는 표현을 쓰지만 다트에서는 Object라고 한다. (Object 라는 이름의 클래스와는 구별해야 한다.)
클래스 이름과 같은 함수를 constructor 라고 하는데 이 함수로 Object를 만들어 낸다.
한 Object를 여러 개의 레퍼런스가 참조할 수 있다.
Object 의 레퍼런스를 만드는 법은 간단하다.
point1 = Point(1,2) 를 하면 point1이 Point(1,2) 의 레퍼런스가 된다.
위 코드를 그림으로 나타내면 이렇게 된다.
레퍼런스 point1과 레퍼런스 point2 는 모두 Point(1,2) 를 가리키고 있기 때문에 point1 == point2 는 true가 된다.
추가로, point2.change() 의 결과로 인해 Object Point(1,2) 의 값이 2,3 으로 바뀌게 되는데, point1 또한 같은 Object를 가리키고 있기 때문에
당연히 point1가 가리키는 Object 의 값도 2,3 으로 변경되었다.
결론 : Object 에서(정확히는 레퍼런스) 같다는 것은 두 레퍼런스가 같은 Object 를 가리키고 있다는 것이다.
결국 우리는 이런 현실을 받아들여야 한다.
expect(Point(1, 2) == Point(1, 2), false);
이걸 같게 만드려면 2가지 방법이 있는데, 사실 한 가지의 방법이다.
왜 하나의 방법이냐면, 2를 하려면 immutable 한 클래스로 만들어야 하기 때문이다.
@immutable
class ImmutablePoint {
// const constructor
const ImmutablePoint(this.y, this.x);
final int y;
final int x;
// void change(int newY, int newX) {
// y = newY;
// x = newX;
// }
}
immutable 클래스가 뭐냐면 모든 인스턴스 변수가 final 인 클래스다.
그러니 change 같은걸 할 수 없다.
constructor의 앞에 const 가 붙어있다. 이런 constructor를 constant constructor(https://dart.dev/guides/language/language-tour#constant-constructors) 라고 한다.
immutable 한 클래스는 constant constructor 를 만들어 주는 것이 좋다.
constant constructor 를 이용해서 만든 객체를 constant object 라고 한다.
이렇게 하면 여러 가지 장점이 있는데, 런타임에 속도가 빨라지는 이점이 있고, 안의 값이 같으면 같다고 판단한다.
test('immutable object test', () {
const point1 = ImmutablePoint(1, 2);
// 값이 다른 const object
const point2 = ImmutablePoint(2, 3);
// 당연히 다름
expect(point1 == point2, false);
// 값이 같은 const object
const point3 = ImmutablePoint(1, 2);
// GOOD!
expect(point1 == point3, true);
// non constant object
var point4 = ImmutablePoint(1, 2);
expect(point1 == point4, false);
});
constant constructor 를 만들었지만 point4 의 경우에는 const 를 쓰지 않아서 non-constant object 가 만들어진 것을 확인할 수 있다.
non-constant object 라고 하니까 새로운 개념 같지만, 우리가 알던 그냥 object 를 말하는 것이다.
이제 우리가 생각한대로 된다.
expect(const ImmutablePoint(1, 2) == const ImmutablePoint(1, 2), true);
expect(ImmutablePoint(1, 2) == ImmutablePoint(1, 2), false);
const를 빼먹지 않을까 걱정된다면 linter를 사용하자.
https://dart.dev/guides/language/analysis-options#enabling-linter-rules
linter 옵션이 켜져 있다면 이렇게 알아서 const 로 바꾸라고 해준다.
연산자 오버로딩은 연산자의 작동 방식을 직접 정의한다는 뜻이다. 그냥 메소드 하나 새로 만든다고 생각하면 된다.
Point 클래스에 == 연산자 오버로딩을 시도하면 linter가 화를 낸다.
https://dart.dev/guides/language/effective-dart/design#avoid-defining-custom-equality-for-mutable-classes 여기에 이유가 있다.
간단하게 말하면 hash를 사용하는 collection 에서 예측 불가능한 동작을 한다고 되어 있다. 여기서 hash collection 을 당장 사용할 일이 없으니 오버로딩을 해도 되지만 Immutable 클래스에다가 하는 것이 좋다.
@immutable
class ImmutablePoint2 {
// const constructor
const ImmutablePoint2(this.y, this.x);
final int y;
final int x;
@override
bool operator ==(Object other) {
return (other is ImmutablePoint2) && other.x == x && other.y == y;
}
// Map 과 Set 에서 key 역할을 한다.
@override
int get hashCode => x.hashCode & y.hashCode;
}
따로 예제는 없다. 어차피 constant object 에는 의미가 없고, non-constant object 에 대해서도 생각한대로 동작한다.
expect(ImmutablePoint2(1, 2) == ImmutablePoint2(1, 2), true);
매번 연산자 오버로딩을 하기 귀찮으니 우리는 패키지를 쓴다.
아마 제일 만만한 게 equatable (https://pub.dev/packages/equatable) 이다.
@immutable
class ImmutablePoint3 extends Equatable {
// const constructor
const ImmutablePoint3(this.y, this.x);
final int y;
final int x;
@override
List<Object?> get props => [y, x];
}
코드가 매우 간단해 졌다. Equatable 을 확장하고 props 를 오버라이드 했다.
props 안에 있는 값이 모두 같으면 같다고 판단한다.
expect(ImmutablePoint3(1, 2) == ImmutablePoint3(1, 2), true);
Equatable 클래스 내부에 연산자 오버로딩이 되어 있기 때문에 가능한 것이다.
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is Equatable &&
runtimeType == other.runtimeType &&
equals(props, other.props);
https://gist.github.com/letyletylety/85bd9b5f85e3b19c2b87cafb240763ef
다트 공식 투어 (클래스)
https://dart.dev/guides/language/language-tour#classes
Effective dart (equality)
https://dart.dev/guides/language/effective-dart/design#equality
코드팩토리님 equatable
https://blog.codefactory.ai/flutter/equatable/
coflutter
https://coflutter.com/dart-how-to-compare-2-objects/
lint 사용하기
https://dart.dev/guides/language/analysis-options#enabling-linter-rules
dart linter
https://dart-lang.github.io/linter/lints/index.html
linter 규칙 : avoid_equals_and_hash_code_on_mutable_classes
https://dart-lang.github.io/linter/lints/avoid_equals_and_hash_code_on_mutable_classes.html
equatable 클래스
https://pub.dev/packages/equatable