diff --git a/examples/misc/lib/effective_dart/design_bad.dart b/examples/misc/lib/effective_dart/design_bad.dart index 3d2f96c432..1107e01b22 100644 --- a/examples/misc/lib/effective_dart/design_bad.dart +++ b/examples/misc/lib/effective_dart/design_bad.dart @@ -140,9 +140,9 @@ void miscDeclAnalyzedButNotTested() { } { - // #docregion prefer-dynamic + // #docregion prefer-object-question mergeJson(original, changes) => ellipsis(); - // #enddocregion prefer-dynamic + // #enddocregion prefer-object-question } // #docregion avoid-function diff --git a/examples/misc/lib/effective_dart/design_good.dart b/examples/misc/lib/effective_dart/design_good.dart index e15a2f0879..9cfecf5968 100644 --- a/examples/misc/lib/effective_dart/design_good.dart +++ b/examples/misc/lib/effective_dart/design_good.dart @@ -278,22 +278,9 @@ void miscDeclAnalyzedButNotTested() { } { - // #docregion prefer-dynamic - dynamic mergeJson(dynamic original, dynamic changes) => ellipsis(); - // #enddocregion prefer-dynamic - } - - { - // #docregion infer-dynamic - Map readJson() => ellipsis(); - - void printUsers() { - var json = readJson(); - var users = json['users']; - print(users); - } - - // #enddocregion infer-dynamic + // #docregion prefer-object-question + Object? mergeJson(Object? original, Object? changes) => ellipsis(); + // #enddocregion prefer-object-question } // #docregion avoid-function @@ -329,6 +316,20 @@ void miscDeclAnalyzedButNotTested() { // #enddocregion object-vs-dynamic }; + () { + // #docregion cast-for-dynamic-member + /// Returns whether the length of [value] is exactly [length]. + /// + /// The argument may be a [String], an [Iterable] or [Map], or any other + /// type that has a `length` field. + bool hasLength(Object? value, int length) { + var actualLength = (value as dynamic).length; + return length == actualLength; + } + + // #enddocregion cast-for-dynamic-member + }; + // #docregion future-or Future triple(FutureOr value) async => (await value) * 3; // #enddocregion future-or diff --git a/firebase.json b/firebase.json index 7e9b03833c..4639ade8d7 100644 --- a/firebase.json +++ b/firebase.json @@ -276,7 +276,7 @@ { "source": "/keyword/default", "destination": "/language/branches#switch", "type": 301 }, { "source": "/keyword/deferred", "destination": "/language/libraries#lazily-loading-a-library", "type": 301 }, { "source": "/keyword/do", "destination": "/language/loops#while-and-do-while", "type": 301 }, - { "source": "/keyword/dynamic", "destination": "/effective-dart/design#avoid-using-dynamic-unless-you-want-to-disable-static-checking", "type": 301 }, + { "source": "/keyword/dynamic", "destination": "/effective-dart/design#avoid-using-dynamic-unless-you-want-to-invoke-dynamic-members", "type": 301 }, { "source": "/keyword/else", "destination": "/language/branches#if", "type": 301 }, { "source": "/keyword/enum", "destination": "/language/enums", "type": 301 }, { "source": "/keyword/export", "destination": "/tools/pub/create-packages", "type": 301 }, diff --git a/src/content/effective-dart/_toc.md b/src/content/effective-dart/_toc.md index e468d48c7c..e9b8a0be24 100644 --- a/src/content/effective-dart/_toc.md +++ b/src/content/effective-dart/_toc.md @@ -233,13 +233,13 @@ the project: * DO write type arguments on generic invocations that aren't inferred. * DON'T write type arguments on generic invocations that are inferred. * AVOID writing incomplete generic types. -* DO annotate with dynamic instead of letting inference fail. +* DO annotate with Object? instead of letting inference fail. * PREFER signatures in function type annotations. * DON'T specify a return type for a setter. * DON'T use the legacy typedef syntax. * PREFER inline function types over typedefs. * PREFER using function type syntax for parameters. -* AVOID using dynamic unless you want to disable static checking. +* AVOID using dynamic unless you want to invoke dynamic members. * DO use Future<void> as the return type of asynchronous members that do not produce values. * AVOID using FutureOr<T> as a return type. diff --git a/src/content/effective-dart/design.md b/src/content/effective-dart/design.md index d2df953867..8fe721c6d7 100644 --- a/src/content/effective-dart/design.md +++ b/src/content/effective-dart/design.md @@ -1425,52 +1425,48 @@ var completer = Completer>(); ``` -### DO annotate with `dynamic` instead of letting inference fail + -When inference doesn't fill in a type, it usually defaults to `dynamic`. If -`dynamic` is the type you want, this is technically the most terse way to get -it. However, it's not the most *clear* way. A casual reader of your code who -sees that an annotation is missing has no way of knowing if you intended it to be -`dynamic`, expected inference to fill in some other type, or simply forgot to -write the annotation. +### DO annotate with `Object?` instead of letting inference fail -When `dynamic` is the type you want, write that explicitly to make your intent -clear and highlight that this code has less static safety. +When inference doesn't fill in a type, it usually defaults to `dynamic`, +which is rarely the best type to use. +A `dynamic` reference allows for unsafe operations that +use identical syntax to operations that are +statically safe when the type is not `dynamic`. +An `Object?` reference is safer. +For example, a `dynamic` reference might fail a type cast that +is not visible in the syntax, while an `Object?` reference will +guarantee that the `as` cast is explicitly written. - +Use `Object?` to indicate in a signature that +any type of object, or null, is allowed. + + ```dart tag=good -dynamic mergeJson(dynamic original, dynamic changes) => ... +Object? mergeJson(Object? original, Object? changes) => ... ``` - + ```dart tag=bad mergeJson(original, changes) => ... ``` -Note that it's OK to omit the type when Dart *successfully* infers `dynamic`. - - -```dart tag=good -Map readJson() => ... - -void printUsers() { - var json = readJson(); - var users = json['users']; - print(users); -} -``` - -Here, Dart infers `Map` for `json` and then from that infers -`dynamic` for `users`. It's fine to leave `users` without a type annotation. The -distinction is a little subtle. It's OK to allow inference to *propagate* -`dynamic` through your code from a `dynamic` type annotation somewhere else, but -you don't want it to inject a `dynamic` type annotation in a place where your -code did not specify one. +In the cases where a dynamic member will be invoked, +this is technically the tersest way to get a dynamic reference. +However, it's not the most *clear* way. +A casual reader of your code who sees that an annotation is missing has +no way of knowing if you intended it to be `dynamic`, +expected inference to fill in some other type, +or simply forgot to write the annotation. +When `dynamic` is the type you want, +write that explicitly to make your intent clear and +highlight that this code has less static safety. :::note -With Dart's strong type system and type inference, -users expect Dart to behave like an inferred statically-typed language. -With that mental model, +With Dart's strong type system and type inference, +users expect Dart to behave like an inferred statically-typed language. +With that mental model, it is an unpleasant surprise to discover that a region of code has silently lost all of the safety and performance of static types. @@ -1667,7 +1663,9 @@ The new syntax is a little more verbose, but is consistent with other locations where you must use the new syntax. -### AVOID using `dynamic` unless you want to disable static checking + + +### AVOID using `dynamic` unless you want to invoke dynamic members Some operations work with any possible object. For example, a `log()` method could take any object and call `toString()` on it. Two types in Dart permit all @@ -1675,8 +1673,8 @@ values: `Object?` and `dynamic`. However, they convey different things. If you simply want to state that you allow all objects, use `Object?`. If you want to allow all objects *except* `null`, then use `Object`. -The type `dynamic` not only accepts all objects, but it also permits all -*operations*. Any member access on a value of type `dynamic` is allowed at +The type `dynamic` not only accepts all objects, but it also statically permits +all *operations*. Any member access on a value of type `dynamic` is allowed at compile time, but may fail and throw an exception at runtime. If you want exactly that risky but flexible dynamic dispatch, then `dynamic` is the right type to use. @@ -1697,11 +1695,30 @@ bool convertToBool(Object arg) { } ``` -The main exception to this rule is when working with existing APIs that use -`dynamic`, especially inside a generic type. For example, JSON objects have type -`Map` and your code will need to accept that same type. Even -so, when using a value from one of these APIs, it's often a good idea to cast it -to a more precise type before accessing members. +Prefer using `Object?` over `dynamic` in code not invoking a member dynamically, +even when working with existing APIs that use `dynamic`. +For example, the static types `Map` and `Map` +can both be used as the static type for the same value, and +the `Object?` form is preferred. + +For intentional dynamic member access, consider +using a cast to `dynamic` for the member access specifically. +Separating the use of `Object?` for non-dynamic behavior and +limiting `dynamic` to the places where dynamic operations are +intended makes them syntactically distinct and highlights the places where +static type checking might not catch mistakes like misspellings. + + +```dart tag=good +/// Returns whether the length of [value] is exactly [length]. +/// +/// The argument may be a [String], an [Iterable] or [Map], or any other +/// type that has a `length` field. +bool hasLength(Object? value, int length) { + var actualLength = (value as dynamic).length; + return length == actualLength; +} +``` ### DO use `Future` as the return type of asynchronous members that do not produce values diff --git a/src/content/language/index.md b/src/content/language/index.md index cc2203d09d..565be13845 100644 --- a/src/content/language/index.md +++ b/src/content/language/index.md @@ -567,7 +567,7 @@ This site's code follows the conventions in the [ns]: /null-safety [`Object`]: {{site.dart-api}}/dart-core/Object-class.html [language version]: /resources/language/evolution#language-versioning -[ObjectVsDynamic]: /effective-dart/design#avoid-using-dynamic-unless-you-want-to-disable-static-checking +[ObjectVsDynamic]: /effective-dart/design#avoid-using-dynamic-unless-you-want-to-invoke-dynamic-members [Libraries and imports]: /language/libraries [conditional expression]: /language/operators#conditional-expressions [if-else statement]: /language/branches#if diff --git a/src/content/resources/whats-new.md b/src/content/resources/whats-new.md index d1b4ba0007..a2a285a2be 100644 --- a/src/content/resources/whats-new.md +++ b/src/content/resources/whats-new.md @@ -1154,7 +1154,7 @@ we made the following changes to this site: [dart-tool]: /tools/dart-tool [diagnostics]: /tools/diagnostic-messages -[dynamic]: /effective-dart/design#avoid-using-dynamic-unless-you-want-to-disable-static-checking +[dynamic]: /effective-dart/design#avoid-using-dynamic-unless-you-want-to-invoke-dynamic-members [Effective Dart]: /effective-dart [evolution]: /resources/language/evolution [experiments]: /tools/experiment-flags#using-experiment-flags-with-ides