-
Notifications
You must be signed in to change notification settings - Fork 170
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
avoid_implementing_value_types seems misguided? #4558
Comments
The opposing view, as usual, is that if you don't check It can also be incompatible, but it shouldn't. import "dart:math";
class Point {
final double x, y;
Point(this.x, this.y);
int get hashCode => Object.hash(x, y);
bool operator==(Object other) => other is Point && x == other.x && y == other.y;
String toString() => "($x, $y)";
}
class PolarPoint implements Point {
final double _angle, _magnitude;
PolarPoint(double angle, double magnitude) : _angle = angle.remainder(pi * 2), _magnitude = magnitude;
double get x => cos(_angle) * _magnitude;
double get y => sin(_angle) * _magnitude;
int get hashCode => Object.hash(x, y);
bool operator==(Object other) => other is Point && x == other.x && y == other.y;
String toString() => "($x, $y)";
}
void main() {
var angle = asin(3/4);
var magnitude = 5.0;
// I compute the point coordinates.
var p1 = Point(cos(angle) * magnitude, sin(angle) * magnitude);
// Or I use a PolarPoint to do it for me.
var p2 = PolarPoint(angle, magnitude);
print(p1 == p2); // - doesn't matter, they're equal.
} My personal recommendation is that any class which overrides If you have a class which defines an equality, and allows subclasses which have different equalities, you should have made the original class abstract, and created a final subclass that people can instantiate instead. You can use (I can see why a widget-hierarchy or similar, with subtyping and change detection, may have different affordances than the code I normally write..) |
I think we need an auxiliary concept in order to explain what Projected structural equality is an asymmetric operation where one object The canonically incorrect (non-symmetric) version of class Point1D {
final int x;
Point1D(this.x);
@override
bool operator ==(other) => other is Point1D && other.x == x;
@override
int get hashCode => Object.hash(Point1D, x);
}
class Point2D extends Point1D {
final int y;
Point2D(super.x, this.y);
@override
bool operator ==(other) => other is Point2D && other.x == x && other.y == y;
@override
int get hashCode => Object.hash(Point2D, x, y);
}
void main() {
Point1D p1 = Point1D(1), p2 = Point2D(1, 2);
print('Receiver is 1D: ${p1 == p2}, 2D: ${p2 == p1}'); // Is true, false.
} The asymmetry arises because this is really a projected structural equality test: In any case, this implementation of We could obviously make (Of course, this makes identity equality much more expensive because it would call Here's the example again, with that change to class abstract class ProjectedEquals { // `projectedEquals` should have been declared in `Object`.
bool projectedEquals(ProjectedEquals other) { /*... primitive identity test ...*/ }
@override
bool operator ==(covariant ProjectedEquals other) =>
projectedEquals(other) && other.projectedEquals(this);
}
class Point1D extends ProjectedEquals {
final int x;
Point1D(this.x);
@override
bool projectedEquals(Object other) => other is Point1D && other.x == x;
@override
int get hashCode => Object.hash(Point1D, x);
}
class Point2D extends Point1D {
final int y;
Point2D(super.x, this.y);
@override
bool projectedEquals(Object other) =>
other is Point2D && other.x == x && other.y == y;
@override
int get hashCode => Object.hash(Point2D, x, y);
}
void main() {
Point1D p1 = Point1D(1), p2 = Point2D(1, 2);
print('Receiver is 1D: ${p1 == p2}, 2D: ${p2 == p1}'); // Is false, false.
} The overriding implementations of Now, to revisit However, allowing objects with "slightly" different run-time types to be equal is definitely a meaningful choice. In general, each class that uses a structural equality criterion would subscribe to a specific projection, which is given by (1) the type It is perfectly meaningful for a class to test for a proper supertype of the enclosing class: This is appropriate in the case where a given supertype of the current type specifies the properties required for equality test (and the current type may differ in many ways, but it still delegates the responsibility to that supertype for specifying which properties to test). class Point1DImpl implements Point1D {
final int x;
Point1DImpl(this.x);
... // Stuff.
@override
bool projectedEquals(Object other) => other is Point1D && other.x == x; // NB: Not `is Point1DImpl`.
@override
int get hashCode => Object.hash(Point1D, x); // Again, we're comparing "at" Point1D.
} We might certainly conclude that it is impractical to do anything like this, in order to ensure symmetry when we allow objects with slightly different run-time types to be equal. However, I do think that it paints a bigger picture where the use of Is it fair to say that |
I agree with much of what has been said on this issue, but all of it is consistent with the lint being wrong, IMHO. For example, I think there's lots of valid ways to implement equality. I think they largely center around conventions that have to be agreed ahead of time, e.g. that if a class uses |
Consideration: I originally authored this lint before there was I personally never use The original intent was "value types should be created or extended, not implemented" to avoid folks creating mocks or fakes for types that really should just be used directly. We could definitely create better lints for that purpose, IMO, and I'd be happy to be involved. |
Interestingly, the original intent is pretty much the same as "value classes should be |
Yeah, though not 100%. This lint does not instruct how to write value classes (I think that would be useful, FWIW), but rather how to use the large corpus of existing value classes that were written pre-Dart 3.x. |
In the documentation for
avoid_implementing_value_types
it says:...but as far as I can tell, that's just not true. Whether you can inherit from the superclass or not, to implement the equivalence relation contract, you have to include
runtimeType
in youroperator ==
check, and once you do that, there's no problem with usingoperator ==
withimplements
.Indeed, none of the examples that are given in https://dart.dev/tools/linter-rules/avoid_implementing_value_types include the
runtimeType
in theiroperator ==
, which seems concerning (they useis
instead, which is insufficient for implementing the contract, because a subclass could add fields).The text was updated successfully, but these errors were encountered: