-
Notifications
You must be signed in to change notification settings - Fork 205
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
Class Modifiers: practical difference between interface and open (extending vs implementing) #1719
Comments
I see There are things you can do with It's reasonable to allow It's also reasonable to have a default implementation that you don't want people to reuse, but to also allow other independent implementations of the interface, so exposing a class that you can only implement, but not extend, is also useful. (Probably not as useful as the other direction, but useful nevertheless). |
I agree on the general statement that With
;-) |
Right, and my argument here is that whenever it is possible to extend a class, it is always possible to implement it -- in the extreme case, by using
Huh, TIL. But it still fits the pattern that if you can extend a class, by definition it is possible to implement it (ie, making sure to override everything). This restriction of non-public generative constructive goes the other way around: you can implement it but not extend it. Again, the point isn't to say that
So I guess this is what I missed. Even with I do see the redundancy of allowing
I agree with this, and from the proposal spec it seems that it's actually more common to implement and not extend. That's why I think it's a good idea to keep the |
So it sounds like you're saying that there's an advantage to using |
Even without final virtual declarations, you can't override everything from your superclass. |
Can you? // package1
class Private { // "open interface class" in the new syntax
void _log() => print("This is really important");
void doSomething() => _log();
} // package2
import "package:package1/package1.dart";
class Public1 implements Private {
@override
void doSomething() => print("No it's not"); // can't access _log from here anyway
}
class Public2 extends Private {
@override
void doSomething() => print("No it's not"); // can't access _log from here anyway
}
void main() {
Private().doSomething(); // "This is really important"
Public1().doSomething(); // "No it's not"
Public2().doSomething(); // "No it's not"
Public1()._log(); // error
Public2()._log(); // error
} When either extending or implementing, so long as I choose to override |
That's not a non- Try: open class Foo {
final String _foo;
Foo(String foo) : _foo = foo;
int get hashCode => _foo.hashCode;
bool operator==(Object other) => other is Foo && _foo == other._foo;
} That implementation of |
Right, I was using current syntax but consider
Well, using
Exactly. And if I want to override everything, I would need |
And you can do that with |
class MyFoo implements Foo { }
void main() {
Foo foo1 = Foo("h");
Foo foo2 = Foo("h");
MyFoo myFoo1 = MyFoo();
MyFoo myFoo2 = MyFoo();
print(foo1 == foo2); // true
print(myFoo1 == myFoo2); // false
print(myFoo1 == foo1); // false
/// Unhandled Exception. NoSuchMethodError: `MyFoo` has no instance getter `_foo`
print(foo1 == myFoo1);
} Well I didn't know this doesn't work today. Shouldn't this be an error even today? I didn't realize simply implementing a class can give runtime errors! From #1006 (comment):
So it seems this is only happening because catching it would be a breaking change... seems like module restrictions would be a good time to make that change then. But in any case, it's a runtime error today (in "safe" Dart), and it can be made into a static error today, so I don't see why this should be considered unsafe. People use |
I'm with Lasse and Erik in that I think implements is a very different kind of operation compared to extends and a library may want to support either, both, or neither. When a class C allows extends but not implements, it means that inside the code for the library where C is defined, if the library has an instance of C, it knows it has an actual instance of the library's own class C or a subclass of it. That could be useful because:
|
// a.dart
class _A { bool get _condition => true; }
class B extends _A { }
void test(_A obj); // b.dart
import "a.dart";
class C implements B { }
void main() {
final B b = B();
final C c = C();
test(b); // okay
test(c); // runtime error
}
// a.dart
class _A { }
class B extends _A { }
void test(_A obj) { } // b.dart
import "a.dart";
/// This analyzer knows about [_A] in this scope. It gives the error:
/// Missing concrete implementation of `getter _A.condition`.
class C implements B { }
void main() {
final B b = B();
final C c = C();
test(b); // okay
test(c); // okay
}
Even adding a field to the private class doesn't change anything: // a.dart
class _A {
final bool condition;
const _A(this.condition);
}
class B extends _A {
B(bool condition) : super(condition);
}
void test(_A obj) { } // c.dart
import "a.dart";
/// This analyzer knows about [_A] in this scope. It gives the error:
/// Missing concrete implementation of `getter _A.condition`.
class C implements B {
@override
bool get condition => true;
}
void main() {
final B b = B(true);
final C c = C();
test(b); // okay
test(c); // okay
} However, adding a private field (
|
Also (and the most important one):
|
That's exactly why implementing should always be allowed! // package:alarms/alarms.dart -- only works on Android!
open class AlarmsManager { // cannot implement
void playAlarm() => AndroidManager.playAlarm();
} // my Flutter app for iOS
import "package:alarms/alarms.dart";
// I have to manually check out the documentation to see if I got everything
class iOSAlarmsManager extends AlarmsManager {
@override
void playAlarm() => iOSManager.playAlarm();
} Now, if the package author adds Put another way, we try to avoid breaking changes because we want the people who use our packages to be happy. But by not allowing them to use That being said, I'd for sure support an annotation like |
Yes, and if the class author gives you that capability by using
I mean, the maintainer of a class can change the body of any existing method and cause just as much breakage. When you're reusing someone else's code... you're reusing their code. I think you're basically just arguing that people should be able to implement interfaces. I don't disagree with that. But I think class authors should be able to decide whether their class exposes an interface (just like they can in C#, Swift, Java, etc.). Your claim is that if an author doesn't want to expose an interface for their class, then they are also prohibited from allowing subclassing (even though C++, C#, Java, etc. let you do just that). I don't see the value in that restriction. |
Probably miscommunicated there -- I'm not saying anything about extending, I just want that in situations where you already can extend (ie,
Right, and I want a way to override the author's intent. All of Dart's current safety features have overrides:
It feels wrong to take away a widely-used feature (more common than |
But you are saying something about extending. Your proposal is that if an author wants to allow extending but not implementing, they have no way to express that. You are removing the ability to express that intent.
Sure, and the author might not want you to. We can't give 100% liberty to everyone any more than we can all eat the same ice cream cone.
You cannot construct a class marked
No feature is being taken away here. Dart would still support implicit interfaces and classes would still be able to expose them. The primary reason we would default to non-interface is because the majority of classes (by a large margin) are not implemented as far as I can tell. It may be the case that even though most classes are not implemented, the author still intends to support that, but that's harder to measure. Even so, the data I have is that less than 10% of classes are implemented. Defaults matter. Java is widely criticized for being verbose, but a big part of that is because users end up putting |
I'll concede that "All" was an exaggeration. I meant "the features that API authors can use to lock down their code".
According to the second section of data in the modules proposal, less than 10% of classes are being extended, and almost double the number of classes allow using
|
Oops, yes, you're right. Even so, this still points to me that non-extensible and non-interface are reasonable defaults since 13.5% and 8.29% are still both quite small numbers. Also, other languages don't support implicit interfaces at all. Swift and Kotlin seal by default. So there are existence proofs that you can have a thriving ecosystem with those defaults. |
That's true for the current spec. Java has default methods in interface though. |
That's true. I do like that Dart allows so much by default, and I'd love a way to maintain that, but keeping things in perspective, this certainly isn't as Earth-shattering as sound null safety or extensions and such. |
I think that dart-lang/linter#2918 should solve the problem regarding subclasses not inheriting private members |
The upcoming So dart-lang/linter#2918 is one trade-off, and |
True,
Additionally,
Overall, I'm happy that the Dart team agrees this unsafe private member access is a problem that needs to be solved (as the root cause behind dart-lang/sdk#57710 is not exactly recent), but I would hope that the solution is cleaner in the long term, and that future features aren't made more complicated by it. |
Also, since I just realized this issue was initially opened for the |
Well, "private to If a given member Conversely, a Not the same thing! ;-)
Let me comment on this in order to illustrate why
We'd certainly have a lint detecting the situations where The lint could of course be strengthened to be an error, if we decide that the guaranteed existence of
If we do use If this occurs in the same library as the declaration of
This makes no difference, I did not assume that every invocation of
That's true, we as a community need to have a discussion about the new concepts (for any new feature, but especially for the ones that have real novelty, like class modifiers), such that we understand how to use them well. |
TL;DR Since
implements
is stricter thanextends
, and since the latter is more permissive than the former, allow implementing with open and adjust the proposal accordingly (see the table all the way at the bottom).According to the modules feature spec:
Essentially, the difference I want to focus on is
interface class
(implement & construct) vsopen class
(extend & construct). I've always seenimplements
as a stricter form ofextends
, not an entirely separate feature. For example:For all intents and purposes, both
B
andC
can be used wherever anA
is expected. In other words, they both have implementations for every member thatA
defines. The only difference between the two is thatC
must re-implement every member ofA
, whereasB
is free to inherit any implementationsA
provides.The way I see it, a definition that uses
extends
can be seen as a subset of a definition thatimplements
, sinceB
is guaranteed to have at least as manyA
members asC
does, but no more. Sinceimplements
is sort of "more complete" thanextends
, it should be logical that anything you can do withextends
can be done withimplements
, but not vice-versa. For example, "can we guarantee __ about this class and its relation toA
?" Yes, if it implementsA
, but not necessarily if it extendsA
.So why are they treated separately in the spec?
If someone writes their class:
then others are free to extend
A
, but not implement it. But if implementing is only different from extending in that it's more complete, then can't a user do both?Of course, the same is not true the other way around -- you cannot half-implement a class, that's just extending. So it makes sense that an author can say "you must implement all members before using this", but not so much that "you can only implement some members". To see how this is true, look at Dart today -- you can make all members abstract to emulate
implements
, but there is no concept of not allowingimplements
. Perhaps a useful example:The author of
FileSystem
can prevent someone from only overriding some members (likeMyAppFileSystem
does) by marking the classinterface
, or makingopenFile
abstract. But it doesn't feel right that the author can only allow case A but not case B (ie, forcing the consumer to only override some methods). If consumers can't do this, they'll switch toextends
and make sure to extend every member, but without the compiler helping them.Regarding the issue of breaking changes with
implements
: I believe the error (and most static errors) are there for the user's benefit, not to burden API authors. While I understand someone not wanting to be responsible forimplements
-related errors, it seems unreasonable that devs can't rely on Dart to make sure they implement every member if that's the choice they make. Remember, a class that usesimplements
incorrectly can introduce compiler errors, while a class that emulatesimplements
withextends
can only make logical errors by using the wrong implementation.I often think of new features as "how can someone opt out of this if they want?" With
final
, it'svar
, nnbd has?
andlate
, type-safety hasdynamic
, butopen class
doesn't allow a developer to implement a class, and instead forces them to useextends
in an unsafe manner. What's worse is that, according to the spec doc,implements
is more common thanextends
-- 9.77% vs 6.47% in Google's code!Because of all this, I think that
open
should allow implementing, and therefore:The text was updated successfully, but these errors were encountered: