Closed eernstg closed 11 months ago
The rationale is that extension methods are generally invoked implicitly, and a
toString
on an extensionE
wouldn't ever be called because every object has a statically knowntoString
instance method, and instance methods always win.
That was not the entire rationale.
As you write, the same could be said for any extension member with the same name as a member of its on
type.
The real reason is that members of Object
are often invoked implicitly by the language specification.
If you write Ext(o).foo()
and Ext
does not have a foo
member, but it has a non-trivial noSuchMethod
, should you invoke Ext.noSuchMethod
? If so, must that noSuchMethod
be a valid override of Object.noSuchMethod
, even though no other members of extensions need to follow the signature of the on
type? What if the extension declares int toString(int x) => x;
, under which conditions would that be called (Ext(o).toString(4)
could work). We were anticipating extension types, and worried what would happen with Ext e = o; print("$e");
.
So, if static extension members were allowed to "override" members of Object
, they would either be hard to call, inconsistently called by the platform, or they'd have to follow the Object
interface signatures. Rather that making a choice, and complicating the feature, we simply disallowed declaring such members, and with the functionality available, that was no big loss.
That is also the issue here:
extension Money on int {
String toString() => this < 0 ? "(${-this})" : this.toString();
}
void main() {
Money e = -42;
print(e);
print("$e");
print(e.toString());
}
Will these print
s print the same thing or different things?
The print(e)
passes e
to a function expecting Object?
. That will definitely use the on
-type's toString
.
The print(e.toString())
should call Ext(e).toString()
, otherwise there is no reason to allow declaring toString
at all.
What does print("$e")
do? Is the underlying model print("" + e.toString() + "")
or is it print(interpolate("", e, ""))
?
toString
of the extension type, not just cast to Object?
and call toString
(like we effectively do now, because blindly call toString
because we know that all objects have one with a compatible signature). The "override" of the Object?
methods in an extension must have a valid override of the signature."$e"
to use e.toString()
?If the "$e"
uses the extension type's toString
, then people are also going to get confused when they do print(e)
or log(e)
or use any other helper function which converts to string, because those will not see the extension type, just an Object?
reference.
The only way to win is not to play. The risk of user confusion is an argument against allowing toString
at all.
It's plainly impossible to allow ==
and hashCode
to be overridden unless we box. Those are almost entirely used by hash maps, which cast to Object?
before calling ==
.
The only exception is the plain if (e == o) ...
check. It might make sense in a few places (like adding equality to collections), but since it's only skin deep, and doesn't work if you pass the two objects to any other general function, it's very likely that it's going to be subtly confusing as well.
(Overriding runtimeType
just doesn't make sense, and noSuchMethod
is potentially confusing - should it work with implicit invocations too?)
Nits:
int i = 42;
i.isOdd; // Error, `E` does not allow for implicit invocations.
It's not an error because int
has an isOdd
(and isEven
) member already.
(It is required that we include the hide clause to hide
toString
, because the members ofObject?
are made available in the interface of the extension type by default, and we need to remove the one fromObject?
to avoid a name clash).
I'd expect an explicitly declared member to automatically hide any instance member which isn't explicitly shown. It would be very annoying if I wrote:
extension OpaqueString on String hide length, operator[] {
...
OpaqueIndex indexOf(Pattern pattern, [int start = 0]) { .... }
...
}
and then also have to add a hide
for indexOf
which I'm explicitly replacing.
About this:
E2 x = 10;
print(x.toString()); // "Something"
print(x); // print 10
@tatumizer wrote:
I think this requires autoboxing. ... This would be very confusing.
This kind of confusion is certainly the main reason why we would not want to have a member name m
which is both the name of an extension member and the name of an instance member, and as @lrhn mentioned, it gets even worse with Object?
members, because they
are often invoked implicitly by the language specification
Later, @lrhn writes:
extension Money on int {
String toString() => this < 0 ? "(${-this})" : this.toString();
}
void main() {
Money e = -42;
print(e);
print("$e");
print(e.toString());
}
The
print(e.toString())
should callExt(e).toString()
, otherwise there is no reason to allow declaring toString at all.
The proposal only allows an extension member named toString
when it is an explicit extension type (extension type ...
). So let's assume the variant of Money
which is explicit:
extension type Money on int {
String toString() => this < 0 ? "(${-this})" : this.toString();
}
With this declaration, I agree that print(e.toString())
would call the extension method, and that's also the intention.
What does
print("$e")
do? Is the underlying modelprint("" + e.toString() + "")
or is itprint(interpolate("", e, ""))
?
The language specification explicitly says that toString
is invoked on the result of evaluating the expression.
If we consider that to be a syntactic desugaring step then it is not unreasonable to say that print("$e")
should also invoke the extension method.
It is still true that print(e)
would (presumably) invoke the instance member named toString
. We might actually want to consider the argument type of print
as obsolete and make it String
, and we could certainly have a lint that recommends passing a string to the-print
-exported-by-dart:core.
So toString
will work, mostly. ;-)
It's plainly impossible to allow
==
andhashCode
to be overridden unless we box.
We could then simply allow toString
(which is the only Object?
member that I've seen actual requests for in this context), and disallow the others.
One might argue that the boxed entity should support overriding any and all of the Object?
members, just like any other class. They would of course be instance members, so they will work as expected in all contexts (like print(e.box)
or (e as dynamic).hashCode
).
So do we want to introduce a mechanism that allows us to write declarations for E.class
(they would then only be available in the boxed object, not as extension members)?
extension type Money on int {
} class {
String toString() => this < 0 ? "(${-this})" : this.toString();
void whatEverExtrasYouWantInTheBoxedObject() {}
}
I got what I paid for :-)
Indeed! ;-)
But this kind of scenario is exactly the reason why I think it is so important to make sure the distinction between a dynamic mechanism like classes and methods is kept visibly distinct from a static mechanism like extensions (extension methods as well as extension types). I do not think it's going to get easier to handle if we try to hide that distinction by using implicit coercions all over the place.
The point is performance.
If you're willing to pay the time and space needed to create a real wrapper object then you will get a more robust entity to work on, no question about that. But if you want to impose a certain discipline on the use of some existing objects (especially a whole object graph where you wouldn't want to wrap each object as you traverse the structure) then the extension types provide a way to do it.
Closing: We decided that extension types should not have the ability to redeclare the five members of Object
.
An extension declaration, as supported by Dart 2.6 and up, makes it an error for a member to have the same name as a member of
Object?
. The rationale is that extension methods are generally invoked implicitly, and atoString
on an extensionE
wouldn't ever be called because every object has a statically knowntoString
instance method, and instance methods always win.(Exception: we could do
E(o).toString()
to invoke an extension member namedtoString
explicitly, but that was considered too inconvenient to have in practice).However, extension types (as proposed in https://github.com/dart-lang/language/pull/1452, cf. #1426) include a variant called 'explicit extension types', and they have the property that implicit invocations are impossible.
This makes it possible and practical to declare and use extension members even in the case where their name is already the name of an instance member. For example,
E
can "override"isEven
on anint
. This would be a very brittle design with the existing extension methods feature, because we would have to remember to invokeisEven
explicitly (likeE(i).isEven
, noti.isEven
) all the time, and if we forget it somewhere then we'll (silently) get the instance member instead. But when we're using an explicit extension type we're forced to useE
as the static type of the receiver, and then we don't have to remember to do anything special in order to invokeisEven
(as an extension method).This also means that we could allow explicit extensions to declare members with the same name as members of
Object?
:As we can see, this allows an explicit extension type to "override" even members of
Object?
, as long as the static type of the receiver is the extension type.(It is required that we include the
hide
clause to hidetoString
, because the members ofObject?
are made available in the interface of the extension type by default, and we need to remove the one fromObject?
to avoid a name clash).However, even though both
isEven
andtoString
can be "overridden" (in quotes because it's resolved statically), we can use a cast to access the underlying object under its run-time type (or a supertype thereof), and then we'll have standard object-oriented late binding, and we'll call the most specific instance member for the given run-time type.We could make extension types even more strict and prohibit all declarations of members whose basename is already the basename of a member of the on-type (but that would be a breaking change for existing extension methods), in which case we wouldn't have this dichotomy of "if this static type run the extension method, otherwise run the instance method".
We could also keep the permission to declare instance members that "statically override" instance members. But in this case it seems inconsistent if we make an exception for members of
Object?
.The current proposal allows extension members that "statically override" members of the on-type, including the ones that are also members of
Object?
.