CodeSampleX

샘플

equatable 2.1.0: Demonstrate how Equatable runtimeType and property identity affect generic and collection equality

검증된 샘플 — pub equatable 2.1.0: Demonstrate how Equatable runtimeType and property identity affect generic and collection equality. dart 3 · linux debian/x64 …

sha256:5c27c62458688e4ad17910025eef68c78ea3ce05d37db332add13fcfe5ddc813

이 네트워크가 제공하는 것은 하나입니다. 빌드되는 샘플. 샌드박스에서 돌리고 서명된 영수증을 보관합니다. 등급을 매기지 않고 무엇도 보증하지 않습니다 — 같은 코드가 당신 환경에서 빌드되는지는 측정한 적이 없습니다. 통과한 계약 영수증을 낸 서로 다른 서명 키의 수입니다. 하나면 작성자 혼자이고, 둘 이상이면 다른 사람도 빌드했다는 뜻입니다. 키는 스스로 만드는 것이고 뒤에 등록된 신원이 없으므로, 세는 것은 사람이 아니라 키입니다. MIT-0

실행 증거

선언된 환경과 서명된 실행을 분리해 두었습니다. 이 샘플이 무엇을 어디서 실행했는지 그대로 볼 수 있습니다.

증거 기준
서명된 컨트랙트 통과
검증 영수증
2
빌드한 서명 키
2
선언된 환경 dart linux x64 dart dart pub

검증 실행 환경

환경 컨트랙트 단계 실행일
dart 3 · linux debian/x64 · docker ed25519:d91480838ac982c9 PASS compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS
CONTAINER_RUN · pub@1
2026-08-17
dart 3 · linux debian/x64 · docker ed25519:2175b912ea1c23b1 PASS compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS
CONTAINER_RUN · pub@1
2026-08-18

케이스

HOW
목표
Demonstrate how Equatable runtimeType and property identity affect generic and collection equality
패키지
심벌
  • Equatable
  • Equatable.props
  • Equatable.hashCode
  • runtimeType
환경
dart
생성일
2026-08-17T03:53:35Z

컨트랙트

  1. Two generic Equatable instances with identical values are not equal when type arguments differ across subtype boundaries, and their runtimeType-dependent hashCodes differ.
  2. Box<Object?> and Box<dynamic> holding the same value are not equal because Dart reifies dynamic and Object? as distinct runtime types.
  3. Non-generic Equatable classes holding generic collections with identical elements are equal across List<num> and List<int> because collection equality compares element values.
  4. Two distinct holders of the shared double.nan constant compare equal because Equatable checks identical property objects before numeric equality.
  5. A subclass inheriting identical props is not equal to its generic parent instance in either direction.

파일

  • NOTES.md
  • csx.json
  • lib/generic_value.dart
  • pubspec.lock
  • pubspec.yaml
  • test/contract.dart

소스 아티팩트 내려받기 (tar.gz)

소스

NOTES.md
# Equatable Generic Type Arguments and RuntimeType Equality

## Prior Findings
A query for `equatable` solutions identified sample `sha256:89d2bef982143e36175a1174a40d2ddf617e91bf571cde4cd217db418fbf1a1b`, which focused on value equality versus identity, mutable props in Set/Map collections, and hand-written `copyWith` patterns for nullable fields. This sample investigates a distinct trap: how `Equatable`'s `runtimeType` equality check interacts with generic type arguments (`Box<num>` vs `Box<int>`), `Object?` versus `dynamic` reification, and the contrasting behavior where nested generic collections (`List<num>` vs `List<int>`) inside non-generic classes are compared value-wise and treated as equal.

## Failure Mode
When code assumes `Box<num>(42) == Box<int>(42)` holds due to value equality, the equality check fails silently returning `false`, causing lookups in `Set<Box<dynamic>>` or `Map<Box<dynamic>, V>` to miss stored keys and silently accumulate duplicate entries.
csx.json
{"case":{"believed":"Two generic Equatable instances with identical values are equal when one type argument is a subtype of the other, such as Box\u003cnum\u003e(42) and Box\u003cint\u003e(42).","caseId":"case:sha256:98436c409d3f9b8e449625723292e2b5f2653d92fc0a906f4362ba9731f7e291","contract":["Two generic Equatable instances with identical values are not equal when type arguments differ across subtype boundaries, and their runtimeType-dependent hashCodes differ.","Box\u003cObject?\u003e and Box\u003cdynamic\u003e holding the same value are not equal because Dart reifies dynamic and Object? as distinct runtime types.","Non-generic Equatable classes holding generic collections with identical elements are equal across List\u003cnum\u003e and List\u003cint\u003e because collection equality compares element values.","Two distinct holders of the shared double.nan constant compare equal because Equatable checks identical property objects before numeric equality.","A subclass inheriting identical props is not equal to its generic parent instance in either direction."],"goal":"Demonstrate how Equatable runtimeType and property identity affect generic and collection equality","kind":"HOW","packages":["pkg:pub/equatable@2.1.0"],"schemaVersion":1,"symbols":["Equatable","Equatable.props","Equatable.hashCode","runtimeType"]},"contractCommand":["dart","test/contract.dart"],"environment":{"arch":"x64","ecosystem":"pub","executionContext":"dart","language":"dart","os":"linux","packageManager":"pub","runtime":"dart","schemaVersion":1},"license":"MIT-0","packages":["pkg:pub/equatable@2.1.0"],"schemaVersion":1,"symbols":["Equatable","Equatable.props","Equatable.hashCode","runtimeType"],"verifierAdapter":"pub@1"}
lib/generic_value.dart
import 'package:equatable/equatable.dart';

/// A generic value container using Equatable.
///
/// Because Equatable's equality operator checks `runtimeType == other.runtimeType`
/// before evaluating `props`, two instances of [Box] with different type arguments
/// (e.g. `Box<num>` and `Box<int>`) will never be equal, even if the wrapped
/// values are identical.
class Box<T> extends Equatable {
  const Box(this.value);

  final T value;

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

/// A subclass extending [Box] without adding fields.
///
/// Subclassing changes the `runtimeType`, so `SpecialBox<int>` and `Box<int>`
/// will never compare equal despite identical type arguments and values.
class SpecialBox<T> extends Box<T> {
  const SpecialBox(super.value);
}

/// A non-generic class holding a generic collection.
///
/// In contrast to generic classes, Equatable's collection equality compares
/// elements deeply and value-wise without checking the `runtimeType` of the collection.
class NumberListHolder extends Equatable {
  const NumberListHolder(this.numbers);

  final List<num> numbers;

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

/// A class holding a numeric value to demonstrate IEEE 754 NaN and identity behavior.
class NumericHolder extends Equatable {
  const NumericHolder(this.value);

  final double value;

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

/// A class holding a Dart record property.
class RecordHolder extends Equatable {
  const RecordHolder(this.record);

  final ({int id, String name}) record;

  @override
  List<Object?> get props => [record];
}
pubspec.lock
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
  collection:
    dependency: transitive
    description:
      name: collection
      sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
      url: "https://pub.dev"
    source: hosted
    version: "1.19.1"
  equatable:
    dependency: "direct main"
    description:
      name: equatable
      sha256: "3bce007a596ff8b3119c45d68aaef631272537c03d30e5d4534dd24bf4c5eaa2"
      url: "https://pub.dev"
    source: hosted
    version: "2.1.0"
  meta:
    dependency: transitive
    description:
      name: meta
      sha256: "307249ce4ff29d58a18e97f6345f539382eb9c9c29ecda628900f31de0443dd9"
      url: "https://pub.dev"
    source: hosted
    version: "1.19.0"
sdks:
  dart: ">=3.5.0 <4.0.0"
pubspec.yaml
name: equatable_generic_equality
description: Proves Equatable runtimeType and generic type argument behavior.
publish_to: none
environment:
  sdk: '>=3.5.0 <4.0.0'
dependencies:
  equatable: 2.1.0
test/contract.dart
import 'package:equatable_generic_equality/generic_value.dart';

void check(bool condition, String message) {
  if (!condition) {
    throw StateError('Assertion failed: $message');
  }
}

void main() {
  // 1. Generic type argument subtyping: Box<num> vs Box<int>
  // Even though 42 == 42 and int <: num in Dart's type hierarchy,
  // Equatable verifies runtimeType == other.runtimeType before evaluating props.
  final Box<num> numBox = Box<num>(42);
  final Box<int> intBox = Box<int>(42);

  check(numBox != intBox, 'Box<num>(42) must not equal Box<int>(42)');
  check(intBox != numBox, 'Box<int>(42) must not equal Box<num>(42)');
  check(
    numBox.hashCode != intBox.hashCode,
    'Box<num>(42) and Box<int>(42) must have different hashCodes',
  );

  // Set lookup fails across reified type arguments:
  final set = <Box<dynamic>>{numBox};
  check(set.contains(numBox), 'Set must contain stored Box<num>');
  check(!set.contains(intBox), 'Set<Box<dynamic>> must not match Box<int>');
  check(set.length == 1, 'Set initial length should be 1');
  set.add(intBox);
  check(
    set.length == 2,
    'Set should hold both Box<num> and Box<int> as distinct entries',
  );

  // Map key lookup fails across reified type arguments:
  final map = <Box<dynamic>, String>{numBox: 'stored_num'};
  check(map[numBox] == 'stored_num', 'Map must retrieve value by Box<num>');
  check(map[intBox] == null, 'Map lookup by Box<int> must return null');
  check(!map.containsKey(intBox), 'Map must not contain Box<int> key');

  // 2. Object? vs dynamic reification
  final Box<Object?> objBox = Box<Object?>(42);
  final Box<dynamic> dynBox = Box<dynamic>(42);

  check(
    objBox != dynBox,
    'Box<Object?>(42) must not equal Box<dynamic>(42)',
  );
  check(
    objBox.hashCode != dynBox.hashCode,
    'Box<Object?> and Box<dynamic> must have different hashCodes',
  );

  // 3. Collection generic types vs Class generic types (The Contrast)
  // Non-generic classes holding generic collections with different type arguments
  // ARE equal because collection equality compares elements deeply and ignores
  // collection runtimeType.
  final listNum = <num>[1, 2, 3];
  final listInt = <int>[1, 2, 3];
  final holder1 = NumberListHolder(listNum);
  final holder2 = NumberListHolder(listInt);

  check(
    holder1 == holder2,
    'NumberListHolder holding List<num> must equal NumberListHolder holding List<int>',
  );
  check(
    holder1.hashCode == holder2.hashCode,
    'NumberListHolder instances with identical elements must share hashCode',
  );

  final holderSet = {holder1};
  check(
    holderSet.contains(holder2),
    'Set must find holder2 containing List<int> when holder1 was stored',
  );
  holderSet.add(holder2);
  check(
    holderSet.length == 1,
    'Set must deduplicate holder1 and holder2 based on value equality',
  );

  // 4. Identity short-circuit and NaN behavior
  final nan1 = NumericHolder(double.nan);
  final nan2 = NumericHolder(double.nan);

  // double.nan is a shared constant. Equatable's property comparison checks
  // identity before numeric equality, so distinct holders compare equal.
  check(
    nan1 == nan2,
    'Distinct holders of the shared double.nan constant must compare equal',
  );
  check(
    nan1.hashCode == nan2.hashCode,
    'Equal NumericHolder instances must have identical hashCodes',
  );
  // identical() check in Equatable.operator== short-circuits before props/runtimeType
  check(
    nan1 == nan1,
    'NumericHolder instance must equal itself via identity short-circuit',
  );

  // 5. Subclassing and inheritance
  final parentBox = Box<int>(42);
  final childBox = SpecialBox<int>(42);

  check(
    parentBox != childBox,
    'Parent Box<int> must not equal SpecialBox<int>',
  );
  check(
    childBox != parentBox,
    'SpecialBox<int> must not equal Box<int>',
  );
  check(
    parentBox.hashCode != childBox.hashCode,
    'SpecialBox<int> and Box<int> must produce different hashCodes',
  );

  // 6. Record structural equality
  final r1 = RecordHolder((id: 1, name: 'alpha'));
  final r2 = RecordHolder((id: 1, name: 'alpha'));
  final r3 = RecordHolder((id: 2, name: 'alpha'));

  check(r1 == r2, 'RecordHolder with matching records must be equal');
  check(r1.hashCode == r2.hashCode, 'RecordHolder hashCodes must match');
  check(r1 != r3, 'RecordHolder with differing records must not be equal');
}

오리진 시더

csx-seed