Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enum implementing Equatable related classes #147

Open
FMorschel opened this issue Aug 18, 2022 · 3 comments
Open

Enum implementing Equatable related classes #147

FMorschel opened this issue Aug 18, 2022 · 3 comments

Comments

@FMorschel
Copy link

FMorschel commented Aug 18, 2022

I've created a class that only takes Enums as parameters. I figured I could create a third Enum where I would manually put every option so they have a better naming.

E.g.:

Configuration

enum A {
  a,
  b;
}
enum B {
  c,
  d;
}

class Class with EquatableMixin {
  const EveryDayOfYear({required this.aValue, required this.bValue});

  final A aValue;
  final B bValue;
  
  @override
  List<Object?> get props => [aValue, bValue];
}

enum C implements Class {
  ac(Class(aValue: A.a, bValue: B.c)),
  ad(Class(aValue: A.a, bValue: B.d)),
  bc(Class(aValue: A.b, bValue: B.c)),
  bd(Class(aValue: A.b, bValue: B.d));

  const C(this._handler);

  final Class _handler;

  @override
  A get aValue => _handler.aValue;

  @override
  B get bValue => _handler.bValue;

  @override
  List<Object?> get props => [aValue, bValue];
}

Objective

final instance = Class(aValue: A.a, bValue: B.c);
instance == C.ac; // I would like something so this operation returns true.

Context

As I said here:

As commented by @Levi-Lesches here the answer to my problem was to override my operator ==:

  @override
  // ignore: hash_and_equals, already implemented by EquatableMixin
  bool operator ==(Object other) {
    return (super == other) ||
        ((other is Class) &&
            (aValue == other.aValue) &&
            (bValue == other.bValue));
  }

This solved my problem and is fair since my class instance is not an Enum, but my enum constant is actually my class instance.

Suggestion

Enable some kind of option or by default so that extended classes are also equal to the base class not by testing the runtimeType but by testing if the object is from the current class/equatable related.

@FMorschel
Copy link
Author

This would also solve #100

@FMorschel
Copy link
Author

I've tested:

Current case

All the tests on this code pass

import 'package:equatable/equatable.dart';
import 'package:test/test.dart';

class A extends Equatable {
  A(this.a);

  final int a;

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

class B with EquatableMixin {
  B(this.b);

  final int b;

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

void main() {
  test('Testing A', () {
    final a1 = A(1);
    final a2 = A(1);
    final a3 = A(2);
    expect(a1, equals(a2));
    expect(a1, isNot(a3));
    expect(a2, isNot(a3));
  });
  test('Testing B', () {
    final b1 = B(1);
    final b2 = B(1);
    final b3 = B(2);
    expect(b1, equals(b2));
    expect(b1, isNot(b3));
    expect(b2, isNot(b3));
  });
}

WARNING

But, these do as well:

//...
class A2 extends Equatable {
  A2(this.a);

  final int a;

  @override
  Type get runtimeType => A;

  @override
  List<Object?> get props => [a];
}
//...
void main() {
  //...
  test('Testing A2', () {
    final a1 = A2(1);
    final a2 = A2(2);
    final a = A(1);
    expect(a1, equals(a));
    expect(a1, isNot(a2));
    expect(a2, isNot(a));
  });
  //...
}

WARNING 2

But these all fail:

//...
enum E implements B {
  e1,
  e2;

  @override
  bool? get stringify => null;

  @override
  int get b => index;

  @override
  List<Object?> get props => [b];
}
//...
void main() {
  //...
  test('Testing E', () {
    final e1 = E.e1;
    final e2 = E.e2;
    final b1 = B(0);
    expect(e1, equals(b1));
    expect(b1, isNot(e1));
    expect(b1, isNot(e2));
  });
  //...
}
My suggested case

When the == operator is as I've configured below:
Mixin:

mixin EquatableMixin<T extends Object> on Object {
  //...
  @override
  bool operator ==(Object other) {
    return identical(this, other) ||
        other is EquatableMixin<T> && //CHANGED this line so the generic is used and removed the `runtimeType`
            equals(props, other.props);
  }
  //...
}

Equatable:

abstract class Equatable<T extends Object> {
  //...
  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is Equatable<T> && //CHANGED this line so the generic is used and removed the `runtimeType`
          equals(props, other.props);
  //...
}

All the tests on this code pass

import 'package:equatable/equatable.dart';
import 'package:test/test.dart';

class A extends Equatable<A> {
  A(this.a);

  final int a;

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

class B with EquatableMixin<B> {
  B(this.b);

  final int b;

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

enum E implements B {
  e1,
  e2;

  @override
  bool? get stringify => null;

  @override
  int get b => index;

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

void main() {
  test('Testing A', () {
    final a1 = A(1);
    final a2 = A(1);
    final a3 = A(2);
    expect(a1, equals(a2));
    expect(a1, isNot(a3));
    expect(a2, isNot(a3));
  });
  test('Testing B', () {
    final b1 = B(1);
    final b2 = B(1);
    final b3 = B(2);
    expect(b1, equals(b2));
    expect(b1, isNot(b3));
    expect(b2, isNot(b3));
  });
  test('Testing E', () {
    final e1 = E.e1;
    final e2 = E.e2;
    final b1 = B(0);
    expect(e1, equals(b1));
    expect(b1, isNot(e1));
    expect(b1, isNot(e2));
  });
}

WARNING

But, these do as well:

//...
class A2 extends Equatable<A> {
  A2(this.a);

  final int a;

  @override
  List<Object?> get props => [a];
}
//...
void main() {
  //...
  test('Testing A2', () {
    final a1 = A2(1);
    final a2 = A2(2);
    final a = A(1);
    expect(a1, equals(a));
    expect(a1, isNot(a2));
    expect(a2, isNot(a));
  });
  //...
}

There are some possible errors in the current case (problem I presented on this issue and #100). I know it's not something everyone will do, overriding the getter of the runtimeType to any other type that has no real relation, but there is still a chance for this to happen and the equality to fail.

My current suggestion would be a breaking change if done this way, but creating a new class and a new mixin for this, could be a solution to that. And this would give the control for the equality for the user, similar to when using Comparable or some other class that uses generics. And this would keep the subclasses all equal to their base class but the other way around (like on enums cases) sometimes not.

@FMorschel
Copy link
Author

The only thing I was thinking here was for my suggestions to work the props should have mustCallSuper and this should be explained to be written like:

@override
List<Object?> get props => [...super.props, newProps];

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant