Language: "as" should not allow you to cast from nullable to non-nullable

Created on 25 Aug 2020  路  5Comments  路  Source: dart-lang/language

void main() {
  int? x = null;
  print(x as int);
}

...has no analyzer errors, but is guaranteed to fail at runtime. Typically i see this in contexts where the declaration is very far from the use site, and so what the code looks like it's doing is downcasting, but what it's actually doing is violating null safety.

I think it's an important aspect of the language that ! is _the_ clear warning sign that you're risking a null check exception.

I would say the code above should be:

void main() {
  int? x = null;
  print(x! as int);
}

...to compile.

nnbd

Most helpful comment

I don't see a big difference between

int? x = null;
print(x as int);

and

Object x = "a";
print(x as int);

Either can throw. An as cast is a way for a user to say that they know the actual type better than the compiler, and if they are wrong, the cast throws to preserve soundness.
Doing x as int is no more violating null soundness than x!, it's exactly the same operation in this case.
Doing x! as int will give you a warning about an unnecessary cast because you are casting int to int. (Even if we did promote x to Null in the initializer, it would be casting Never to int, which is an unnecessary up-cast).
So, it would be just print(x!).

You are asking for ! to be the only way to get out of nullability, but nullability is just a subtype relation to the type system. Down-casting is a perfectly good way to get rid of it.

Disallowing as casts too widely would break a number of places where we have a potentially nullable type, and use x as T to cast from T? to T. Using ! there won't work since it will throw even when T is nullable. So the restriction could only apply to definitely nullable types.

The restriction would also break the symmetry between is promotion and as cast.
You can check-and-promote in two ways:

if (x is int) { print(x); }

or

if (x != null) { print(x); }

just as you currently can check-or-throw in two ways: x as int or x!.
By disallowing x as int, we'd make the language less orthogonal.

I too would suggest a lint. It's a valid coding style to prefer one approach to another, but I see no reason for the language itself to make that decision. And the lint should probably only apply to e as T where T is definitely non-nullable, not just potentially non-nullable.

All 5 comments

cc @leafpetersen

My first reaction is that this feels like more like a candidate for an opt in lint. I think the lint would be something like "if e has a potentially nullable type, then warn on any casts of the form e as T where T is non-nullable unless e ends in a postfix !".

In general, we've not had any language restrictions on casts. They're basically your "get out of the type system free" card.

Another option would be to expose this as as top level function or extension method that requires a non-nullable argument.

/// Cast that can only be applied to non-nullable things
S safeCast<S>(Object x) => x as S

or as an extension:

extension SafeCast on Object {
  S get safeCast => this as S;
}

The ergonomics of extensions are horrific, let's not use those anywhere near any standard libraries.

I would be fine with a lint that doesn't allow you to use as to change the nullability. In general, I view lints as the same as errors, and I'm totally fine with making anything that's an error be a lint if there's sane behavior to be had by not making it illegal.

I don't see a big difference between

int? x = null;
print(x as int);

and

Object x = "a";
print(x as int);

Either can throw. An as cast is a way for a user to say that they know the actual type better than the compiler, and if they are wrong, the cast throws to preserve soundness.
Doing x as int is no more violating null soundness than x!, it's exactly the same operation in this case.
Doing x! as int will give you a warning about an unnecessary cast because you are casting int to int. (Even if we did promote x to Null in the initializer, it would be casting Never to int, which is an unnecessary up-cast).
So, it would be just print(x!).

You are asking for ! to be the only way to get out of nullability, but nullability is just a subtype relation to the type system. Down-casting is a perfectly good way to get rid of it.

Disallowing as casts too widely would break a number of places where we have a potentially nullable type, and use x as T to cast from T? to T. Using ! there won't work since it will throw even when T is nullable. So the restriction could only apply to definitely nullable types.

The restriction would also break the symmetry between is promotion and as cast.
You can check-and-promote in two ways:

if (x is int) { print(x); }

or

if (x != null) { print(x); }

just as you currently can check-or-throw in two ways: x as int or x!.
By disallowing x as int, we'd make the language less orthogonal.

I too would suggest a lint. It's a valid coding style to prefer one approach to another, but I see no reason for the language itself to make that decision. And the lint should probably only apply to e as T where T is definitely non-nullable, not just potentially non-nullable.

Filed https://github.com/dart-lang/linter/issues/2235 with the lint request, closing this out.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

Cat-sushi picture Cat-sushi  路  3Comments

leafpetersen picture leafpetersen  路  3Comments

marcelgarus picture marcelgarus  路  3Comments

har79 picture har79  路  5Comments

wytesk133 picture wytesk133  路  4Comments