-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Weird not a subtype of
issue
#52826
Comments
It might help to use The point is that it is likely that Here is the core typing issue: enum Range { local, global }
class ValueButton<T extends Enum> {
final T value;
final void Function(T?) onChanged;
const ValueButton(this.value, this.onChanged);
}
void main() {
ValueButton<Enum> button = ValueButton<Range>(Range.local, (range) {});
button.onChanged; // Throws, already when we touch it (no need to call it).
} I'd recommend that you do not declare an instance variable whose type contains a type parameter in a non-covariant position (see this issue about "contravariant members"). It's no problem for code in the scope of the relevant type variable (such as code in the body of the The typical way to get this issue is that a type parameter is used as the type of a value parameter in a function type, here: So the advice is: Do not declare an instance variable with type The reason why it helps to make the invocation dynamic is that this allows you to evaluate So you should probably just do the following, in order to avoid touching class ValueButton<T extends Enum> extends StatefulWidget {
final String label;
final T value;
final void Function(T?) onChanged;
const ValueButton(
{required this.label,
required this.value,
required this.onChanged,
super.key});
@override
State<ValueButton> createState() => _ValueButtonState();
void callOnChanged() => onChanged(value);
}
class _ValueButtonState<T extends Enum> extends State<ValueButton<T>> {
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: () => widget.callOnChanged(),
child: Text(widget.label));
}
} |
Thank you for the extensive explanation. And your suggested fix worked. Can the error message be improved here? I never would have figured this out without your help. |
Marking as front-end, in case they can do something general to improve the error message. |
Well, I've proposed a lint (tentatively called It would probably not be easy to give advice about what else to do in this situation. However, indicating that this particular kind of declaration is dangerous would in any case be a useful first step. The lint proposal says that a getter/method return type In this case that could be |
Sorry if this is the wrong place, because I am not entirely sure if it belongs here. I came across a very similar error and was able to reproduce it with the following snippet. typedef Updater<T> = T Function(T initialValue);
abstract class SuperTest<T> {
final Updater<T>? updater;
SuperTest(this.updater);
}
class Test extends SuperTest<int> {
Test(super.updater);
}
void main() {
Test test = Test((initialValue) => initialValue);
// var superTest = test as SuperTest; // breaks too
var superTest = test as SuperTest<num>;
var returnTest = superTest.updater;
print(returnTest);
} |
Yes, this situation is similar. It is dangerous to use a class with a contravariant member, because the program will incur a dynamic error (it throws a You could vote for this lint if you want to get a heads-up whenever a class has that kind of member: #59050. You would be able to eliminate the covariance as follows (this will turn the run-time error into a compile-time error): // Assuming `--enable-experiment=variance`.
abstract class SuperTest<inout T> {
final Updater<T>? updater;
SuperTest(this.updater);
}
class Test extends SuperTest<inout int> {
Test(super.updater);
} The above code relies on a feature which hasn't yet been added to the language, namely declaration-site variance, dart-lang/language#524 (you can vote for that, too ;-). The modifier We don't have that feature at this time, but you can also emulate it: typedef Updater<T> = T Function(T initialValue);
typedef SuperTest<T> = _SuperTest<T, Function(T)>;
abstract class _SuperTest<T, Inv> {
final Updater<T>? updater;
_SuperTest(this.updater);
}
class Test extends SuperTest<int> {
Test(super.updater);
}
void main() {
Test test = Test((initialValue) => initialValue);
SuperTest<num> superTest = test; // Compile-time error.
var returnTest = superTest.updater;
print(returnTest);
} Note that the original code That's just like |
I think I just ran into this issue again, in a different context: I got the runtime exception:
Here, I'm actually highly surprised this code has an error, let alone a runtime error. The problem was I forgot to add type parameters to an enclosing class. (The actual code details don't matter, presumably.) However, I don't see how there would be any harm in letting this sort of error slide at runtime -- how could calling a function with a |
The problem is that you don't have a function with a |
I just ran into this issue yet again, and it took me a full half hour to figure out that this is the same issue... it would really help if, at a minimum, the linter could warn about this. Hitting a type error at runtime for something that on the surface seems correct is jarring (not to mention it decreases runtime safety, which is never a good thing, especially for a statically-typed language...). Is there any way for the compiler to check for this error at compiletime? |
Certainly. It's a deliberate choice that Dart uses dynamically checked covariance. Statically checked covariance is possible, and it's safe, but it is considerably less convenient in a lot of situations where there are no run-time failures. Still, I'd very much support the introduction of statically checked variance in Dart such that developers can make the choice. The lint proposed in #59050 would inform us that it's a bad idea in the first place to have an instance variable whose type is Alternatively, the declaration-site variance feature in dart-lang/language#524 would allow us to specify that void main() {
ValueButton<Enum> button = ValueButton<Range>(Range.local, (range) {}); // Compile-time error.
...
} This is considerably less convenient for the program as a whole, so that's the trade-off. On the other hand, when Finally, if that is possible, you could limit the issue to the library that declares class ValueButton<T extends Enum> extends StatefulWidget {
final String label;
final T value;
final void Function(T?) _onChanged; // DANGER: Only use from this class body.
const ValueButton(
{required this.label,
required this.value,
required this.onChanged,
super.key});
@override
State<ValueButton> createState() => _ValueButtonState();
void onChanged(T? value) => _onChanged(value);
} This might look like it makes no difference compared to the approach where |
Thanks for expanding your explanation further. I would actually be fine with the linter or compiler requiring instance variables to be private, if an attempt is made to invoke function-typed covariant instance variables in this way. Adding a function to access the instance variable had worked perfectly in every case I have run into so far. I suspect that very few programmers will be able to wrap their heads around this issue (I'm still struggling, to be honest... I need a deeper understanding of covariance and contravariance) -- so really the programmer needs to be told exactly how to fix this issue when it comes up. I would be fine with none of the deeper changes to the language that you suggested being made, if I was told directly every time that I can only invoke these types of instance variables' function values from a member function. |
@lukehutch, I think we can close this issue. The observed behavior is as intended, and the way to improve on the difficulties caused by this behavior is to report "contravariant members" (which is requested by #59050) and/or introduce statically checked variance (as proposed in dart-lang/language#524). WDYT? |
Sure, although I will point out again my suggestion in my previous comment that the source of the problem needs to be explained more clearly in the error message. (Dart's error messages are usually very helpful and information-rich, but not in this case.) Can this be fixed? If not, please go ahead and close this. |
(@johnniwinther, the text below contains a question for you—search for "@johnniwinther" to see it.) @lukehutch wrote:
Ah, I'm sorry! I forgot that the error message was actually the primary topic of this issue at this time. I was just thinking about the hard typing properties of the examples. ;-) The error message which was reported in the initial posting was something like the following:
This error message describes the soundness violation which was just about to take place. So the runtime threw a The description in the error message is precise, but it is also superficial. This is because it reports the immediate problem at the time where it exists, and there is no information about the sequence of events that caused this soundness violation to be lined up in the first place. It seems possible to track down the previous step when the upcoming soundness violation is detected: The type check was performed because line 69 in the original example would evaluate It is known at compile time that @johnniwinther, do you think it would be difficult to report the fact that any given run-time type failure is caused by a failing caller-side check? (The error message could perhaps give a link to a page about "contravariant members".) It could be a useful heads-up for developers whose code have this kind of member declarations that the typing of the member has caused this run-time type error. However, it could also be argued that the contravariant member declaration should be reported as highly questionable at compile time. That would simply be a matter of getting the lint proposed in #59050 implemented, and enabling it (preferably: as widely as possible). On the other hand, it could be argued that the contravariant member That is exactly the approach which is used if we add a method like We could introduce a feature like "members that are private to class A<X> {
void Function(X) this.onChanged; // Strawman syntax `this.` means "private to `this`".
X value;
A(this.onChanged, this.value);
void foo() { onChanged(value); } // OK.
}
void main() {
A<num> a = A<int>((i) => i.isEven, 42);
a.onChanged; // Compile-time error, receiver is not `this`.
a.onChanged(a.value); // Just another example of the same error.
} This kind of feature could allow us to turn any contravariant member into a type safe entity: As long as it is only accessed on However, as long as we don't have this kind of feature (or anything else that will do the job), we have to consider techniques like the addition of That is, such techniques are OK for developers who know exactly what they are doing (and who are willing and able to remember how to deal with contravariant members like I think the conclusion is that (1) it is probably not too hard to modify the error message which is associated with a failing caller-side check, but (2) the fact that techniques like |
The |
[Edit by eernstg: This issue has evolved over time; it is now used to report that diagnostics emitted for run-time failures associated with caller-side checks are uninformative.]
Sorry for the non-specific title, I don't even know how to describe this weird bug...
Also, I will illustrate this with Flutter code, but this is probably not a Flutter issue, it is a type system issue as far as I can tell...
Given this code:
if I define
ValueButton
as aStatelessWidget
, as followsthen when I click on the button, I get:
However if I convert
ValueButton
to aStatelessWidget
then I get no such error:
The only difference here is
StatefulWidget
vs.StatelessWidget
.--
dart --version
): 3.0.5The text was updated successfully, but these errors were encountered: