Enum implementing Equatable related classes
FMorschel opened this issue · 5 comments
I've created a class that only takes Enum
s 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.
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 enum
s cases) sometimes not.
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];
not sure if there is a real difference of using the runtime type or using is
because is
will have to check the runtime type of the other
object. Also I think we are no longer talking of real equality then anymore if an instance of a derived class with the same property values is seen as equal.