diff --git a/examples/misc/test/language_tour/generics_test.dart b/examples/misc/test/language_tour/generics_test.dart index f27d2d3f2e..6515dd1217 100644 --- a/examples/misc/test/language_tour/generics_test.dart +++ b/examples/misc/test/language_tour/generics_test.dart @@ -65,3 +65,21 @@ void main() { } class View {} + +// #docregion f-bound +// ignore: one_member_abstracts +abstract interface class Comparable { + int compareTo(T o); +} + +int compareAndOffset>(T t1, T t2) => + t1.compareTo(t2) + 1; + +class A implements Comparable { + @override + int compareTo(A other) => /*...implementation...*/ 0; +} + +var useIt = compareAndOffset(A(), A()); + +// #enddocregion f-bound diff --git a/examples/type_system/lib/bounded/instantiate_to_bound.dart b/examples/type_system/lib/bounded/instantiate_to_bound.dart index 1269453d5f..56c8cffd37 100644 --- a/examples/type_system/lib/bounded/instantiate_to_bound.dart +++ b/examples/type_system/lib/bounded/instantiate_to_bound.dart @@ -7,3 +7,13 @@ void cannotRunThis() { c.add(2); // #enddocregion undefined-method } + +// #docregion inference-using-bounds-2 +X max>(X x1, X x2) => x1.compareTo(x2) > 0 ? x1 : x2; + +void main() { + // Inferred as `max(3, 7)` with the feature, fails without it. + max(3, 7); +} + +// #enddocregion inference-using-bounds-2 diff --git a/examples/type_system/lib/strong_analysis.dart b/examples/type_system/lib/strong_analysis.dart index 93b955bf7a..a6ca120e60 100644 --- a/examples/type_system/lib/strong_analysis.dart +++ b/examples/type_system/lib/strong_analysis.dart @@ -156,3 +156,21 @@ void _miscDeclAnalyzedButNotTested() { // #enddocregion generic-type-assignment-implied-cast } } + +// #docregion inference-using-bounds +class A> {} + +class B extends A {} + +class C extends B {} + +void f>(X x) {} + +void main() { + f(B()); // OK. + f(C()); // OK. Without using bounds, inference relying on best-effort + // approximations would fail after detecting that `C` is not a subtype of `A`. + f(C()); // OK. +} + +// #enddocregion inference-using-bounds diff --git a/src/content/language/generics.md b/src/content/language/generics.md index 0e8c460bcb..62ce12ef80 100644 --- a/src/content/language/generics.md +++ b/src/content/language/generics.md @@ -144,6 +144,7 @@ an object is a List, but you can't test whether it's a `List`. When implementing a generic type, you might want to limit the types that can be provided as arguments, so that the argument must be a subtype of a particular type. +This restriction is called a bound. You can do this using `extends`. A common use case is ensuring that a type is non-nullable @@ -195,6 +196,31 @@ Specifying any non-`SomeBaseClass` type results in an error: var foo = [!Foo!](); ``` +### Self-referential type parameter restrictions (F-bounds) + +When using bounds to restrict parameter types, you can refer the bound +back to the type parameter itself. This creates a self-referential constraint, +or F-bound. For example: + + +```dart +abstract interface class Comparable { + int compareTo(T o); +} + +int compareAndOffset>(T t1, T t2) => + t1.compareTo(t2) + 1; + +class A implements Comparable { + @override + int compareTo(A other) => /*...implementation...*/ 0; +} + +var useIt = compareAndOffset(A(), A()); +``` + +The F-bound `T extends Comparable` means `T` must be comparable to itself. +So, `A` can only be compared to other instances of the same type. ## Using generic methods diff --git a/src/content/language/type-system.md b/src/content/language/type-system.md index 96d976eb64..52aed0c9a5 100644 --- a/src/content/language/type-system.md +++ b/src/content/language/type-system.md @@ -372,6 +372,83 @@ The return type of the closure is inferred as `int` using upward information. Dart uses this return type as upward information when inferring the `map()` method's type argument: ``. +#### Inference using bounds + +:::version-note +Inference using bounds requires a [language version][] of at least 3.7.0. +::: + +With the inference using bounds feature, +Dart's type inference algorithm generates constraints by +combining existing constraints with the declared type bounds, +not just best-effort approximations. + +This is especially important for [F-bounded][] types, +where inference using bounds correctly infers that, in the example below, +`X` can be bound to `B`. +Without the feature, the type argument must be specified explicitly: `f(C())`: + + +```dart +class A> {} + +class B extends A {} + +class C extends B {} + +void f>(X x) {} + +void main() { + f(B()); // OK. + f(C()); // OK. Without using bounds, inference relying on best-effort + // approximations would fail after detecting that `C` is not a subtype of `A`. + f(C()); // OK. +} +``` + +Here's a more realistic example using everyday types in Dart like `int` or `num`: + + +```dart +X max>(X x1, X x2) => x1.compareTo(x2) > 0 ? x1 : x2; + +void main() { + // Inferred as `max(3, 7)` with the feature, fails without it. + max(3, 7); +} +``` + +With inference using bounds, Dart can *deconstruct* type arguments, +extracting type information from a generic type parameter's bound. +This allows functions like `f` in the following example to preserve both the +specific iterable type (`List` or `Set`) *and* the element type. +Before inference using bounds, this wasn't possible +without losing type safety or specific type information. + +```dart +(X, Y) f, Y>(X x) => (x, x.first); + +void main() { + var (myList, myInt) = f1(); + myInt.whatever; // Compile-time error, `myInt` has type `int`. + + var (mySet, myString) = f1({'Hello!'}); + mySet.union({}); // Works, `mySet` has type `Set`. +} +``` + +Without inference using bounds, `myInt` would have the type `dynamic`. +The previous inference algorithm wouldn't catch the incorrect expression +`myInt.whatever` at compile time, and would instead throw at run time. +Conversely, `mySet.union({})` would be a compile-time error +without inference using bounds, because the previous algorithm couldn't +preserve the information that `mySet` is a `Set`. + +For more information on the inference using bounds algorithm, +read the [design document][]. + +[F-bounded]: /language/generics/#self-referential-type-parameter-restrictions-f-bounds +[design document]: {{site.repo.dart.lang}}/blob/main/accepted/future-releases/3009-inference-using-bounds/design-document.md#motivating-example ## Substituting types