-
Notifications
You must be signed in to change notification settings - Fork 212
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
Some class members may not be valid for some actual type arguments: Check that situation statically #2308
Comments
Looking at the code for both void complete([FutureOr<T>? value]) {
if (!future._mayComplete) throw new StateError("Future already completed");
future._asyncComplete(value == null ? value as dynamic : value);
} and void _asyncComplete(FutureOr<T> value) { Is there any reason for the dynamic cast besides being able to make the parameter optional? Neither Removing the cast would only be a breaking change if someone omits the argument in a nullable case, ie, To test, I simplified the class A<T> { // equivalent to [Completer]
void b(FutureOr<T> value) { } // equivalent to [_Future._asyncComplete]
void c(FutureOr<T>? value) => b(value ?? value as dynamic); // equivalent to [Completer.complete]
}
void main() {
A<int>().c(); // case 1: throws when [A.c] calls [A.b]
A<int?>().c(); // case 2: OK
} As expected, Changing the call to Going a bit deeper, the same issue arises when calling factory Future.value([FutureOr<T>? value]) {
return new _Future<T>.immediate(value == null ? value as T : value);
} Again, the explicit cast to |
@Levi-Lesches wrote:
True. But in pre-null-safety Dart, null is always a value of type
The task was not to make the parameter optional, it was to avoid widespread breakage when the introduction of null safety caused the existing declaration to be a compile-time error. That parameter could be made mandatory (which would require insertion of an actual argument at a potentially very large number of call sites across the entire Dart community), or it could be made nullable (which was the choice that we actually made, and which gave rise to the dynamic check).
Yes, we had similar discussions about that one. To go back one step and get an overview:
|
Some instance methods will only work if certain operations are supported by objects associated with the receiver. For example, a
List
can be sorted without providing acompare
function iff the elements contained in the list areComparable
:This is currently handled by dynamic type checks. It would be useful to change the situation such that it is at least recognized to be statically type safe in some situations. We can specify this kind of constraint for extension methods:
However, this is not a plug-and-play solution: The extension declaration may declare type parameters that are needed for other purposes, so we may easily get into a situation where we'd want to use intersection types as the bounds, and we don't support intersection types (not for bounds, we do have a special case associated with promotion). An extension method cannot be overridden (so we can't have different implementations of
sort
for different lists). An extension method cannot be invoked dynamically. Extension methods need special treatment with respect to imports, so they will not become available just because the on-type is available at any given location.In general, we may wish to declare an instance method
m
in a given class with type parametersX1 .. Xk
, even in the case where the implementation ofm
will only be type safe when the corresponding type arguments satisfy certain constraints.Similarly, with null safety, some operations that used to make sense became problematic. For instance, dart-lang/sdk#46646 (comment) describes the use of the setter
length=
on aList
to change the length of the list. This method will usenull
as the value of newly created entries when this would make the list longer, and discarding entries when the list is made shorter.It is impossible to determine at compile-time whether any particular operation of the form
myList.length = anExpression
will make the list shorter or longer (or neither), but when it's made longer it will be a null safety violation to use null as the value at new entries, unless the list has the dynamic typeList<T>
for someT
which is nullable. In other words, thelength=
setter is safe on a list iff its actual type argument is nullable.We could use
void f<X>(List<X?> xs) => xs.length++;
, but that doesn't makexs.length++
safe per se, it just "manually" gives rise to the missing static type check on the receiver, for that particular call site.Another example is #1299, where it is noted that the member signature
void complete([FutureOr<T>?])
ofCompleter<T>
isn't null-safety friendly. In particular, it accepts null as the actual argument (by passing null or by omitting the argument entirely). However, if the actual argument is null then (at least with the built-in type_AsyncCompleter
) the program will encounter a dynamic error because it invokes a method whose parameter type isFutureOr<T>
on aFuture<T>
, and null does not have typeFutureOr<T>
unlessT
is nullable.Again, the
complete
operation allows the argument to be optional in all cases, but it is only safe to do so in the case where the actual type argument of the givenFuture
is nullable.As an aside, the use of dynamically checked covariance causes one more issue: As the example program above shows, it is possible to have a
List<int>
whose static type isList<int?>
. Consequently, we cannot ensure that the actual type argument of the list is a nullable type, just because the static type says so. We could eliminate this problem with use-site invariance, which would allow us to give the list the typeList<exactly int?>
). In any case, this is an orthogonal issue that we'd deal with separately.In summary, I think it would be useful to be able to declare methods in need of extra constraints on the type arguments of the enclosing class in a way that yields improved static type safety.
The text was updated successfully, but these errors were encountered: