-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Extension type: Confusing behavior when an extension type both implements
the wrapped type and define members with the same name
#60675
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
Comments
This is working as intended. The point is, as you mention, that there is no override relation between The choice to invoke So there is no soundness requirement that those two declarations should be compatible with each other in any way, they just happen to have the same name. We could of course have required that a redeclare relationship must obey the same rules as an override relationship, even though it is not motivated by soundness. We instead chose to allow the signature of a redeclaration to be completely independent of the signature of the corresponding redeclaree (haha, let's just say that's a word ;-). This allows the extension type to specify exactly the signature which is most appropriate for the given member. |
I get how this is happening. But I must insist if I were to show this to most Dart developers, they'd find this very confusing (fortunately extension types are rare). I think the main issue IMO is the usage of the word If I do B value = A();
value.member(); I could replace Consider static extensions instead: Extensions cannot shadow members of a type: class A {
int get value => 0;
}
extension on A {
int get value => 42;
}
print(a.value); // Will print 0 This makes static extensions and extension type a bit inconsistent here IMO. |
The point of subclass substitutability is that you can use an instance of the subclass as an instance of the superclass. That doesn't mean that it necessarily has the same API if you use it as the subclass. It's traditionally that way, but for a statically resolved (non-virtual) member, which extension members are, the member you get for the name (I think we suggested a lint to avoid accidentally shadowing, and an annotation |
I understand on a technical level :) Relying on Say we had In fact, until this very comment, because of relying on |
All other things equal, I'd have preferred a different word than 'implements' for the clause that declares a subtype relationship from an extension type to another type, exactly because this word makes it tempting to assume that members behave in a similar way as they do across an By the way, it is true that the extension type is a subtype of the operands of its However, it's crucial that reasoning about extension type members is based on an entirely different kind of thinking than that which is applicable to reified types like classes. Member lookups for extension typed receivers must be understood as static types. If the static type of a receiver expression This means that the reasoning is inherently going off track if we do something like
where
"Changing the static type of a receiver" in general means "changing the behavior" for extension type members. When a member invocation resolves to an extension type member, you know exactly which implementation (that is, which piece of code) the invocation will run, but if the static type of the receiver changes (for any reason) then your investigation into what this invocation will do must start from scratch. This is similar to a paradigm shift rather than an implementation detail. You mentioned extension members as well (that is, the old For both With standard object-oriented semantics, an object But the object-oriented semantics is irrelevant to the statically resolved invocations, and vice versa. It's necessary to think about these two kinds of member accesses as two different things (starting from the point, at the latest, where something has a behavior which is surprising).
That's right, if the receiver type has an instance member with the specified name then it will be the result of the resolution, and it doesn't matter whether there exist any Note, however, that an extension method can certainly be invoked even though there is an instance member with the same name: We just have to make sure that the instance member isn't known statically: class A {}
class B extends A {
String get g => 'Instance member';
}
extension on A {
String get g => 'Extension member';
}
void main() {
A a = B();
print(a.g); // 'Extension member'.
} So we need to know which paradigm is being used for each member access (statically resolved or OO dispatched), and then we know what to expect ("the code which will run is known exactly at compile time", vs. "the object is known to have some implementation of this member, and that's what we will run"). Finally, the reason why an extension type can redeclare members of the representation type is that the extension type feature was created in order to allow us to "dress up" an existing object with a new interface, without paying for it in terms of allocating a wrapper object. The extension on int {
Cm get cm => Cm(this);
Inch get inch => Inch(this);
}
extension type Cm(int _value) {
Cm operator +(Cm other) => Cm(this._value + other._value);
}
extension type Inch(int _value) {
Inch operator +(Inch other) => Inch(this._value + other._value);
}
void main() {
final sum1 = 1.cm + 2.cm; // Has type `Cm`.
final sum2 = 3.inch + 4.inch; // Has type `Inch`.
final sum3 = 5.cm + 6.inch; // Compile-time error.
final Cm sum4 = sum2; // Compile-time error.
} In this case we assume that we'd like to have an operator |
Consider:
The fact that this snippet is legal feels very odd to me.
Subclass
implementsBase
, butSubclass.method2
member doesn't respect that ofBase.method2
. The latter expects anint
param, and the former aString
.This leads to weirdness such as:
I'm aware that this
method2
definition is not an override (and hence why the linter doesn't request for an@override
). But it feels odd that we "implemented" an interface, yet the implementation of our class clearly violates that interface.It feels like it should be a compilation error to both
implements
the encapsulated type and define members with the same name.The text was updated successfully, but these errors were encountered: