For example, given the following code structure:
a.dart:
class A {
int _m() { return 0; }
}
useA(A a){ a._m(); }
b.dart:
class B implements A {}
void main() {
useA(B());
}
The code will pass type check but throws error in runtime.
Since even an abstract class can have private properties, the lack of explicit interfaces is making
Dart's inheritance much less useful than many other languages, e.g. currently it's hard to implement a correct proxy object (as in proxy pattern), and there's no way to let the language to check the proxy's correctness.
Silently allowing class B .. implements A .. in a situation where B is concrete and some members of the interface of B are private and declared in a different library than B is really a residual from Dart 1.
An implementation of Dart 2 will generate noSuchMethod forwarders, but the author of B cannot even write an implementation of noSuchMethod that implements all those "missing" private methods, because there is no direct way to recognize which of the private methods is being called. (The given Invocation contains a memberName, but that member name cannot be denoted by a symbol which is specified in the library of B). In other words, it just won't work, but you may be lucky enough that no such private methods are called in your particular program execution.
So it would definitely be in the spirit of Dart 2 to do something about this.
A simple approach would be to say that it is now a compile-time error to declare a class like B (so we stop generating noSuchMethod forwarders for these missing private methods, and just yell at developers instead, in the situation where we used to generate them).
We didn't do that; I believe it was too much of a breaking change, in particular for situations where a limited testing scenario would rely on a mock object. For a specific test, it may be OK to rely on manual inspection of the classes involved to conclude that we won't call those missing private methods. So we get to write such mock classes even though they are known to be incomplete (because some private methods are missing): They are incomplete in so many other ways as well, and we can handle that.
However, I don't think a separate notion of interfaces would be very helpful here: They should presumably be usable also for specifying private members, and then you could set up the exact same scenario with class B and _interface_ A. It isn't the "classness" of A that creates the problem, it's the fact that it would be a breaking change to outright ban an implements clause for a concrete class that doesn't (and can't) implement certain private methods.
So it's a well-known issue, and the question is how we can best help developers to avoid getting any of these "missing private methods" without breaking too much existing code. We could of course lint the missing private methods, and rely on something like // ignore:missing_private_methods to suppress that lint.
They should presumably be usable also for specifying private members, and then you could set up the exact same scenario with class B and interface A.
Thanks for the excellent explanation. May I ask if there's something stopping Dart from having TypeScript-alike inheritance? Where implements is usable to inherit form both interfaces and classes, but interfaces can only contain public members, and programmers are just more careful when implementing a class.
It may be discussed for many times, but IMHO it would also encourage the usage of mixins and make Dart's ecosystem even more extensible. When a method in mixin is overriding a method with a return value in the super class, to make itself composable, the overriding method may probably want to create its own return value by wrapping the overridden method's return value, which is not possible in the linked code for the discussed reason. If such an interface (or whatever allows such pattern) is present, library authors would at least have a way to allow that.
@pinyin wrote:
implements is usable to inherit form both interfaces and classes,
but interfaces can only contain public members,
The language team has had discussions about adding several kinds of declarations that are similar to classes (for instance, interfaces), and they would basically provide support for various constraints on classes: A class can be used to create an instance, it can be used as a superclass, it can be used as a supertype (implements), and it can be used as a mixin. This is 4 bits, and we could have declarations that support any subset of them.
For instance, we can remove one bit by making a class abstract (cannot create instances of that class). We can remove two bits by declaring a mixin rather than a class (cannot inherit from, cannot create). Adding support for interface declarations would be similar (cannot create, cannot inherit from, cannot mix in). We'd want to support a useful subset of all 16 combinations of these bits.
This sets the scene for what interface could mean, and it seems like an anomaly (to me) to start talking about privacy in this context. For instance, if an interface is itself private, wouldn't it be a surprise if it were unable to specify the signatures of private members?
So there's no thing 'stopping us', but it might still fit less than perfectly to use that TypeScript based rule.
I think the most promising way to help developers avoid those accidentally missing private methods might be to make it a static warning to have them, and then allow developers to suppress the warning in some way (maybe via a magic comment or metadata).
OK, that make sense.
So many other language's interfaces are actually existing in 2 orthogonal semantic dimensions, the type dimension (= supertype) and the visibility dimension (= all members public), but Dart's interface is only supposed to have a meaning in the inheritance dimension (= supertype), leaving the visibility dimension empty for something else. I'm not a PL expert, but IMHO Dart's way is actually cleaner.
I still can't fully get the 4-bit class model (can' t find the corresponding 4 dimensions for now), hopefully that will be as elegant as the design of the interface.
My questions regarding the issue itself is over, thanks for your work and the explanation, hoping such mechanism will come to Dart soon.
Here's one old sketch proposal (we're not going to do this, and in particular direct will never fly, but I guess you can see the direction that this might go ;-):
We have four capabilities: Implement, Extend, Construct, Mix-in. We have the capability introducers interface => Implement; class => Implement, Extend, Construct; mixin => Mixin, Implement; plus some capability eliminators: abstract <=> ~Construct, direct => ~Implement, sealed => ~Implement, ~Extend.
sealed interface // โ
(none)
interface // โ
โ
Implement
direct abstract class // โ
โ
Extend
abstract class // โ
โ
Implement Extend
sealed class // โ
โ
Construct
direct class // โ
โ
Extend Construct
class // โ
โ
Implement Extend Construct
direct mixin // โ
Mix-in
mixin // โ
โ
Mix-in Implement
I believe that we have no action items at this point, so I'll close this issue now. Thanks for your input!