Skip to content
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

Adapt implicit cast example for type system page #6478

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions examples/type_system/lib/strong_analysis.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,32 @@ void _miscDeclAnalyzedButNotTested() {
}
}

{
// #docregion downcast-check
void assumeStrings(dynamic objects) {
// ignore: stable, beta, dev, invalid_assignment
List<String> strings = objects; // Runtime downcast check.
String string = strings[0];
}
// #enddocregion downcast-check

// #docregion fail-downcast-check
assumeStrings(<int>[1, 2, 3]);
// #enddocregion fail-downcast-check
}

{
// #docregion type-inference-1-orig
Map<String, dynamic> arguments = {'argA': 'hello', 'argB': 42};
Map<String, Object?> arguments = {'argA': 'hello', 'argB': 42};
// #enddocregion type-inference-1-orig

// ignore: stable, beta, dev, argument_type_not_assignable
arguments[1] = null;

// #docregion type-inference-2-orig
Map<String, dynamic> message = {
Map<String, Object?> message = {
'method': 'someMethod',
'args': <Map<String, dynamic>>[arguments],
'args': <Map<String, Object?>>[arguments],
};
// #enddocregion type-inference-2-orig
}
Expand Down
11 changes: 11 additions & 0 deletions src/_sass/components/_code.scss
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,14 @@ pre {
padding-left: 1.25rem;
padding-right: 1.25rem;
width: 100%;
display: inline-block;
min-width: 100%;
border-left: 2px solid rgba(0, 0, 0, 0);

&.highlighted-line {
background: color.change($brand-primary, $alpha: 0.1);
min-width: 100%;
border-left-color: $brand-primary;
}
}

Expand Down Expand Up @@ -199,6 +204,12 @@ pre {
&:not([lang="console"]) {
line-height: 1.8;
}

code {
display: block;
min-width: fit-content;
width: 100%;
}
}
}

Expand Down
41 changes: 39 additions & 2 deletions src/content/language/type-system.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,43 @@ void main() {
}
```

### Implicit casts

Expressions with a static type of `dynamic` can be
implicitly cast to a more specific type.
If the actual type doesn't match, the cast throws an error at runtime.
Consider the following `assumeStrings` method:

<?code-excerpt "lib/strong_analysis.dart (downcast-check)" replace="/strings = objects/[!$&!]/g"?>
```dart tag=passes-sa
void assumeStrings(dynamic objects) {
List<String> [!strings = objects!]; // Runtime downcast check.
String string = strings[0];
}
```
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd recommend using a simpler example: The downcast in the current example takes two steps, but we would already have a perfectly useful example if it is adjusted to take just one step.

The first step in the current example is concerned with the class: Starting from dynamic, it is tested whether the given object is a list. We could say that we're checking for the type List, which is just an abbreviated notation for List<dynamic>. Next, when we know that it is a list, we check for the type argument String (which is a cast from List<dynamic> to List<String>). This second step is based on a subtype relation known as covariance.

I think the topic of the example should just be "implicit downcasts from dynamic", and there's no need to include anything that requires the reader to think about a two-step subtype relationship that also involves variance.

Here's one possible way to do this:

int assumeString(dynamic object) {
  String string = object; // Check at run time that it is a `String`.
  return string.length;
}

I'd suggest using 'run time' rather than 'runtime', because the latter commonly refers to the support system which is running along with a program (providing external functions like print, services like garbage collection, etc.) In contrast, 'run time' is a noun phrase that denotes a specific kind of point in time, namely: One that occurs during an execution of the program. If you want to use the corresponding adjective then it would be 'run-time', as in 'a run-time error occurs if it is not a string'.


In this example, if `objects` is a `List<String>`, the cast succeeds.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
In this example, if `objects` is a `List<String>`, the cast succeeds.
In this example, if `object` is a `String`, the cast succeeds.

If it's not a subtype of `List<String>`, such as `List<int>`,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
If it's not a subtype of `List<String>`, such as `List<int>`,
If it's not a subtype of `String`, such as `int`,

a `TypeError` is thrown:

<?code-excerpt "lib/strong_analysis.dart (fail-downcast-check)" replace="/\[.*\]/[!$&!]/g"?>
```dart tag=runtime-fail
assumeStrings(<int>[![1, 2, 3]!]);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The notation [!1!] should be a highlighted version of 1 (I'm not 100% sure about the syntax):

Suggested change
assumeStrings(<int>[![1, 2, 3]!]);
final length = assumeString([!1!]);

I introduced a local variable to store the returned result. That's still a bit artificial because it isn't used anywhere, but I thought it was one step more natural than just having a local variable inside assumeStrings and not returning anything.

```

:::tip
To prevent implicit downcasts from `dynamic` and avoid this issue,
consider enabling the analyzer's _strict casts_ mode.

```yaml title="analysis_options.yaml" highlightLines=3
analyzer:
language:
strict-casts: true
```

To learn more about customizing the analyzer's behavior,
check out [Customizing static analysis](/tools/analysis).
:::

## Type inference

Expand All @@ -291,9 +328,9 @@ pairs string keys with values of various types.

If you explicitly type the variable, you might write this:

<?code-excerpt "lib/strong_analysis.dart (type-inference-1-orig)" replace="/Map<String, dynamic\x3E/[!$&!]/g"?>
<?code-excerpt "lib/strong_analysis.dart (type-inference-1-orig)" replace="/Map<String, Object\?\x3E/[!$&!]/g"?>
```dart
[!Map<String, dynamic>!] arguments = {'argA': 'hello', 'argB': 42};
[!Map<String, Object?>!] arguments = {'argA': 'hello', 'argB': 42};
```

Alternatively, you can use `var` or `final` and let Dart infer the type:
Expand Down