Dart

[실전 Dart] 01.객체 생성 & 비교

딸기케잌🍓 2024. 3. 14. 17:54

1. 객체 생성

객체 생성시 값을 메모리에 저장하고, 메모리 주소를 반환함

 

  • 값(Value) : 메모리에 저장된 데이터
  • 참조(Reference) : 값이 저장된 메모리 주소

 

변수는 참조(메모리 주소)를 저장한다.

String name = '철수'; //철수라는 값이 담긴 메모리 주소를 변수 name에 저장한다.

 

 

2. 객체 비교

객체 비교에는 두 가지가 있다.

  1. 값 비교 : 메모리에 담긴 값을 비교하는 방법
  2. 참조 비교 : 메모리 주소를 비교하는 방법

 

Dart는 기본적으로 참조 비교를 사용한다.

 

 

3. 메모리 할당 규칙

객체 생성시, 가변 객체는 항상 새 메모리를 할당하고 불변 객체는 값이 동일하다면 기존에 생성한 객체를 재활용한다.

 

void main() {
  print('철수' == '철수');   // true (메모리 주소가 같음)
  print([1] == [1]);       // false (메모리 주소가 다름)
}
  • "철수" : 불변 객체로 메모리에 동일한 값이 있다면 재할당X -> 기존 메모리 주소 반환
  • [1] : 가변 객체로 항상 새로운 메모리에 할당 -> 새로운 메모리 주소 반환

 

 

4. 가변 객체(Mutable Object)

메모리에 할당한 뒤 값을 변경할 수 있는 객체

  • List, Set, Map, 커스텀 클래스 등이 있음
List a = [];
a.add(1); // 메모리 주소를 유지하며 값을 변경

Set a = {};
a.add(1); // 메모리 주소를 유지하며 값을 변경

Map<String, dynamic> b = {};
b['name'] = '철수'; // 메모리 주소를 유지하며 값을 변경

A a = A(1);
a.value = 2; // 메모리 주소를 유지하며 값을 변경

class A {
	int value;
	A(this.value);
}

 

 

 

5. 불변 객체(Immutable Object)

메모리에 할당한 뒤 값을 변경할 수 없는 객체

  • String, int, double, bool, const로 선언된 객체 등이 있음
String a = '철수';
a = '영희'; // '철수'를 변경한게 아니라 '영희'를 생성(메모리 주소가 바뀜)

int a = 1;
a = 2; // 1을 변경한게 아니라 2를 생성(메모리 주소가 바뀜)

List a = const [];
// a.add(1); // const로 생성되었기 때문에 수정 불가능

A a = const A(1);      // const 생성자로 호출하여 불변 객체로 생성 (const 안붙으면 가변 객체 규칙을 따라 매번 새롭게 생성)
// a.value = 2;        // const로 생성된 객체는 수정 불가능

class A {
	final int value;     // 모든 속성이 final로 선언되야 불변 객체 지원 가능
	const A(this.value); // 생성자에 const를 붙여서 불변 객체 생성 지원
}

 

 const는 컴파일 타임에 고정 값인 객체 앞에만 선언할 수 있다.

  • 런타임(Runtime) : 앱을 실행하고 있는 시점
  • 컴파일 타임(Compile Time) : 앱 실행 전 소스 코드를 기계어로 변환하는 시점
  • 모든 가변 객체도 const 키워드로 생성되면 불변 객체가 된다.
  • const로 생성되지 않은 클래스는 가변 객체와 같이 생성 된다.

 

다음 코드 이해하기!

 

 

 

6. Equality (동등성) 비교

Dart에서는 두 가지 유형의 동등성을 구분한다.

  1. Identity Equality(정체성 동등성)
  2. Equality(동등성)

 

Identity Equality (정체성 동등성)

 

  • Identity Equality는 두 객체가 메모리 상에서 동일한 위치를 참조할 때, 즉 객체의 인스턴스가 완전히 같을 때를 의미함
  • identical()함수를 사용하여 두 객체가 동일한 인스턴스인지 검사 가능

 

Equality (동등성)

  • Equality는 두 객체의 내용이나 값이 같을 때를 의미함
  • 두 객체가 서로 다른 인스턴스라도 비교 연산자(==)를 사용한 비교 결과가 true이고, hashCode 값이 동일하다면 동등하다고 판단
  • hashCode 값이 동일해야 하는 이유는 객체를 해시 테이블과 같은 데이터 구조에 저장하고 검색할 때 사용되는 값이기 때문
  • 따라서 비교 연산자(==)를 오버라이드 하여 동등성 검사시에는 객체의 'hashCode' 메서드도 함께 오버라이드 해야함

 

Dart에서 참조 비교가 아닌 값(Equality) 비교를 하기 위한 두 가지 방법을 소개한다.

 

동등성 비교 방법1. (==) 오버라이딩

Dart는 기본적으로 참조 비교이지만 비교 연산자(==)와 hashCode를 @override하여 값 비교를 수행하도록 만들 수 있다.

 

 

그럼 여기서 말하는 hashCode란?

Dart의 모든 object들은 hashCode를 가지고 있다.

비교 연산자(==)와 hashCode는 object의 프로퍼티이며 해쉬맵 구현이 올바로 동작하기 위해서는 일관성이 유지되어야 한다.

예를 들어, Set에서 객체를 추가할 때, Dart는 hashCode== 연산자를 사용하여 객체가 이미 존재하는지를 검사한다. 따라서, 컬렉션 내에서 객체의 동등성을 올바르게 관리하려면 비교 연산자(==)와 hashCode 메서드를 적절히 오버라이드해야 한다.

따라서 == 연산자를 오버라이딩한다면 꼭!! hashCode도 오버라이딩 해야 한다. (반대도 마찬가지임)

 

@override
bool operator ==(Object other) {
  return identical(this, other) ||
      other is A && runtimeType == other.runtimeType && value == other.value;
}

@override
int get hashCode => value.hashCode;

메모리 주소가 같다면 true 반환(참조 비교)

  • identical(this, other)

또는 다음 조건 모두 만족시 true 반환(값 비교)

  • other is A : 비교 대상이 A 또는 A의 하위 클래스인지 확인
  • runtimeType == other.runtimeType : 타입이 같은지 확인
  • value == other.value : 속성이 같은지 확인 (int는 불변 객체이므로 참조 비교의 결과와 값 비교의 결과가 동일)

 

 

다음은 위 내용 관련하여 dart 공식 문서를 정리한 것임

https://dart.dev/effective-dart/design#equality

 

Effective Dart: Design

Design consistent, usable libraries.

dart.dev

 

Equality(동등성)

비교 연산자(==)를 오버라이드 한다면 hashCode도 오버라이드 하라

  • 두 개의 객체가 완전히 같은 객체일 경우에만 같은 해쉬 코드를 가진다.
  • 어떤 object라도 equal 하려면 같은 해쉬 코드를 가져야만 한다.

 

equal 하다면 다음의 세 조건을 만족한다.

  • Reflexive: a == a should always return true.
  • Symmetric: a == b should return the same thing as b == a.
  • Transitive: If a == b and b == c both return true, then a == c should too.

mutable class의 custom equality를 정의하는 것을 피해라

비교 연산자(==)를 정의할 때 hashCode도 정의해야 한다. 객체의 필드가 변경되면 객체의 해시 코드가 변경될 수 있다.

hashCode 기반 자료형은 hashCode 값이 변경되지 않는다는 전제하에 구현되어 있다.

 

==의 파라미터가 nullable이 아니게 해라

Good

class Person {
  final String name;
  // ···

  bool operator ==(Object other) => other is Person && name == other.name;
}

 

 

Bad

class Person {
  final String name;
  // ···

  bool operator ==(Object? other) =>
      other != null && other is Person && name == other.name;
}

 

 

 

동등성 비교 방법 2. Equatable 패키지 이용

Equatable 패키지

https://pub.dev/packages/equatable

 

equatable | Dart package

A Dart package that helps to implement value based equality without needing to explicitly override == and hashCode.

pub.dev

  • 비교연산자(==)와 hashCode를 오버라이드하여 조금 더 간결하게 동등성을 비교할 수 있게 해준다.
  • Equatable 패키지는 불변 객체를 전제로 설계 되었고, 따라서 객체의 모든 멤버 변수들은 final 로 선언되어야 한다.

 

 

Equatable 패키지 설치

dart pub add equatable
  • 플러터 프로젝트일 경우 dart -> flutter로 바꿔줌
  • 설치 후 pubspec.yaml 파일에 dependencies 확인

 

 

 

Equatable 사용 방법

import 'package:equatable/equatable.dart';

class Person extends Equatable {
  const Person(this.name);

  final String name;

  @override
  List<Object> get props => [name];
}

 

  • 커스텀 클래스(Person)가 Equatable을 extends 하도록 함
  • get메서드를 오버라이드 함, 배열안에 값을 비교할 변수를 넣음

 

아래의 Equatable 적용 전 코드와 비교해보면 조금 더 간결해진 것을 알 수 있다.

class Person {
  const Person(this.name);

  final String name;

  @override
  bool operator ==(Object other) =>
    identical(this, other) ||
    other is Person &&
    runtimeType == other.runtimeType &&
    name == other.name;

  @override
  int get hashCode => name.hashCode;
}