-
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
Downcasting generic-types crashes with an Unhandled type exception #52943
Comments
This is all working at intended. The occurrence of It's still sound, which means that any time an expression can end up with a runtime value with a type that is not a subtype of the static type, the compiler has instead a check and throws to prevent unsoundness. That's what happens here. The static type of The static type of |
@lrhn What would be the correct way to do what I'm trying to do in the code sample? I have a more complex case in which I have multiple I'm also curious if the Dart analyzer should catch this error earlier. This feature of the Dart SDK does not seem intuitive, or if it is, it's not made clear. |
For the record for others to see, a strongly typed class instead of a field implementation will not fix this issue. Here's a code sample that produces the same problem but with a class: abstract class Foo {}
class Bar extends Foo {}
typedef BuilderDef<T extends Foo> = String Function(T someFoo);
class FooMatcher<T extends Foo> {
final BuilderDef<T> builder;
const FooMatcher({required this.builder});
}
// final FooMatcher<Bar> implementation = FooMatcher<Bar>(
// builder: (Bar bar) => '<3',
// );
class Implementation extends FooMatcher<Bar> {
Implementation() : super(builder: (Bar bar) => '<3');
}
void main() {
final FooMatcher matcher = Implementation();
print(matcher.builder.runtimeType);
} |
Why is String Function(Bar) not a subtype? Bar is a subtype of Foo, therefore, it is legal and safe to downcast to Foo since Bar extends Foo. Is it because Dart cannot resolve the inheritence of field function parameters? |
You have a What you can do is to drop the class entirely, and use the function type If you want a list of builders, it would be a Alternatively, hide the class Builder<T> {
final String Function(T) _builder;
Builder(this._builder);
bool isValidArgument(Object? o) => o is T;
String build(Object? input) {
if (input is! T) throw ArgumentError.value(input, "input", "Not a $T");
return _builder(input);
}
String? tryBuild(Object? input) {
if (input is! T) return null;
return _builder(input);
}
} You can't just call with any object (except There has to be tests (until Dart gets variance modifiers like dart-lang/language#524). |
The reason The point of subtyping in object orientation (and in general) is that an instance of a subtype can be used anywhere the supertype is expected. A So |
So this is a limitation with function parameters in the Dart SDK. That explains why the sample works if the T was the return type instead of the function paramter. I have a converter that takes some object and returns our T (Foo), the T is then consumed by the code sample as a function param. Returning the T is fine, using the T as a function parameter crashes. For visual aid: typedef StyleBuilder<T extends RichMatch> = List<TextSpan> Function(T match);
typedef MatchBuilder<T extends RichMatch> = T Function(RegExpMatch match); StyleBuilder crashes when accessed due to the discussed limitation with function parameter types, MatchBuilder succeeds normally since T is a returned type and is therefore not a contravariant. Is what I said accurate? |
That does sound correct. It's not just a limitation of the Dart SDK. |
That's fair. Do you believe it's possible for the Dart analyzer to protect against this error ahead of time? |
To address the issue of contravariance with function parameters, we can refactor the code to use typedefs without any generic constraints. Instead of using Here's the refactored code with the fix: abstract class Foo {}
class Bar extends Foo {}
typedef BuilderDef = String Function(Foo someFoo);
class FooMatcher {
final BuilderDef builder;
const FooMatcher({required this.builder});
}
final FooMatcher implementation = FooMatcher(
builder: (Foo foo) => '<3', // Changed the parameter to accept Foo instead of Bar
);
void main() {
final FooMatcher matcher = implementation;
print(matcher.builder.runtimeType);
} With this fix, you'll no longer encounter the runtime error related to contravariant function parameters. The code will now work as expected and provide a more consistent and safer behavior when handling function types. If you have any more questions or need further assistance, feel free to ask! |
@febirison While your solution does work, I'm afraid my sample code does not provide a good practical reason for its existence. I wrote the original code with generics so that the function that's reading the Foo function parameter in the builder can react to different implementations of Foo. With your solution, Foo would need to be casted whenever it needs to be accessed inside the builder function, therefore @lrhn's builder wrapper is a better solution to this as it allows intrinsic pre-casting of the builder's Foo parameter to any subtype. I'm keeping this issue open to allow discussion on potentially handling this issue from the Dart analyser side as it was not clear until runtime. |
The analyzer cannot tell you whether your program definitely has a bug. After all, the (current) language design allows the program, and if you don't up-cast, it'll work correctly. It's the combination of a contravariant getter and up-casting which is a problem. I guess the analyzer could try to detect that.
That does sound like lint material. Maybe: Lint:
|
@SaadArdati, much of the ground was already covered by @lrhn, but I'd like to add a few extras. Here is a slightly simplified restatement of the original example: abstract class Foo {}
class Bar extends Foo {}
class FooMatcher<T extends Foo> {
final String Function(T) builder;
const FooMatcher({required this.builder});
}
void main() {
final FooMatcher<Foo> matcher = FooMatcher<Bar>(builder: (_) => '<3');
matcher.builder; // Throws.
} The situation where a getter or a method has a return type which is non-covariant in the type variables of the enclosing class gives rise to a caller-side check (that's the situation where I proposed a lint This implies that you get a compile-time error rather than a run-time error, when an attempt is made to create the unsafe situation. This mechanism is part of the declaration-site variance feature, dart-lang/language#524. It has not been fully implemented at this time, and it hasn't been scheduled for release. But I hope we'll have it, soon (vote for it if you agree!), and we can try it out like this already: // Assuming `--enable-experiment=variance`.
abstract class Foo {}
class Bar extends Foo {}
class FooMatcher<inout T extends Foo> { // <----- Just add `inout` here!
final String Function(T) builder;
const FooMatcher({required this.builder});
}
void main() {
// The initialization of `matcher` is now a compile-time error.
final FooMatcher<Foo> matcher = FooMatcher<Bar>(builder: (_) => '<3');
// ...
} The keyword You can emulate invariance in the current language as well. The basic idea is that you use two type variables rather than one type variable to represent the invariant type variable. The first type variable will have the same value as the type variable you're using today. The second type variable must always be We use a Here is a variant of the example with the updated declaration of abstract class Foo {}
class Bar extends Foo {}
class _FooMatcher<T extends Foo, Invariance> {
final String Function(T) builder;
const _FooMatcher({required this.builder});
}
typedef FooMatcher<T extends Foo> = _FooMatcher<T, T Function(T)>;
void main() {
// The initialization of `matcher` is now a compile-time error.
final FooMatcher<Foo> matcher = FooMatcher<Bar>(builder: (_) => '<3');
} If we do not create the unsafe situation then we won't have the compile-time error, and the program runs with no errors: ... // Same.
void main() {
final matcher = FooMatcher<Bar>(builder: (_) => '<3');
matcher.builder; // No problems
matcher.builder(Bar());
} |
So Erik says we already have a lint request, and the current behavior is doing what it's specified to do, so I'll close this issue, and suggest chiming in on the lint request, if you want to make that happen. |
Given the following example:
There are two ways to crash it. The first way is downcasting
FooMatcher<Bar>
toFooMatcher
like you see in the first line of the main function. This crashes with the following error message when trying to access thebuilder
field. No need to run the builder, just accessing it crashes.runtimeType
is not necessary.Another way to crash it is to remove the generic from the
implementation
's return type. So instead offinal FooMatcher<Bar> implementation = FooMatcher<Bar>(
doing this instead will crash when trying to access
implementation.builder
. It's another way of downcasting the generic.final FooMatcher implementation = FooMatcher<Bar>(
Full example:
It produces the same error:
I'm using a Macbook Pro with Apple M1 Max.
Dart version:
Dart SDK version: 3.0.6 (stable) (Tue Jul 11 18:49:07 2023 +0000) on "macos_arm64"
The text was updated successfully, but these errors were encountered: