language icon indicating copy to clipboard operation
language copied to clipboard

Add "Safe" (nullable) cast operator `as?`

Open wrozwad opened this issue 6 years ago • 21 comments

It'll be a little enhancement but so much pleasant one :)

Kotlin and Swift has something like Safe casting with as? operator. It gives an opportunity to casting with null fallback if cast can't be performed:

void someFunction(dynamic arg) {
  // we could has something like
  final classVariable = (arg as? SomeClass)?.someValue;
  
  // in place of
  final classVariable = arg is SomeClass ? arg.someValue : null;
}

wrozwad avatar Jun 12 '19 15:06 wrozwad

Thanks for filing an issue - just a head up, this issue tracker is for issues with the website. Language feature requests can be filed at https://github.com/dart-lang/language

johnpryan avatar Jun 12 '19 22:06 johnpryan

Ahh, you're right, sorry ;)

wrozwad avatar Jun 12 '19 22:06 wrozwad

Moved to the language repo!

kevmoo avatar Jun 12 '19 22:06 kevmoo

I like this. I was honestly just about to file a request for the exact same thing (thanks to GitHub for listing relevant existing issues!)

Another practical use case would be;

int x = o as? int ?? 0;  // Could perhaps use some parentheses for readability :)

This is the safe cast operator that was originally requested when the current as operator was added, and it is very useful in a number of situations. See C# for prior art.

lrhn avatar Jul 10 '19 09:07 lrhn

I like it, too! I missed this when I came from C#

kevmoo avatar Jul 10 '19 14:07 kevmoo

Definitely something I miss from Kotlin. Would be great timing to have this implemented this alongside the upcoming null safety.

gnawf avatar Aug 28 '20 04:08 gnawf

Any progress about this issue? I'm from Swift and suffered exceptions when casting from time to time.

wjling avatar May 07 '21 09:05 wjling

We could also consider this to be a proposal to have a few extensions in a conveniently accessible library:

extension AsExtension on Object? {
  X as<X>() => this as X;
  X? asOrNull<X>() {
    var self = this;
    return self is X ? self : null;
  }
}

extension AsSubtypeExtension<X> on X {
  Y asSubtype<Y extends X>() => this as Y;
}

extension AsNotNullExtension<X> on X? {
  X asNotNull() => this as X;
}

void main() {
  num? n = 1 as dynamic;
  n.as<int>().isEven;
  n.asSubtype<int>().isEven; // `n.asSubtype<String>()` is an error.
  n.asNotNull().floor();
  n.asOrNull<int>()?.isEven; // Corresponds to `(n as? int)?.isEven`.
}

eernstg avatar May 07 '21 10:05 eernstg

hey @eernstg

EDIT: This question is answered here: https://github.com/dart-lang/language/issues/399#issuecomment-933255132

One question for the awesome extension you posted, why is it not possible to get it to work with dynamic objects?

For example:

import 'package:my_flutter_project/src/app/utils/type_utils.dart';

class SomeClass {
  /// This does NOT work.
  /// `as` or any other method within the extensions imported are not recognized!
  /// The import statement is marked as unused as well.
  void testCastingDynamicObject() {
    const dynamic someNumber = 1;
    final someInt = someNumber.as<int>();
    print(someInt);
  }

  /// This works!
  void testCastingNormalObject() {
    // const num someNumber = 5;
    // final someInt = someNumber.as<int>();
    // print(someInt);
  }

  /// This works also!
  void testCastingDynamicObjectWithPrecast() {
    // const dynamic someNumber = 1;
    // const Object someNumberObject = someNumber as Object;
    // final someInt = someNumberObject.as<int>();
    // print(someInt);
  }
}

om-ha avatar Oct 02 '21 17:10 om-ha

@om-ha, I believe it's because the extensions are declared on Object?. All types, like num and Object in your other two functions are subtypes of Object?. But since dynamic can be of any type, Dart won't try to check for extensions that could apply to it, including Object?, even though technically all objects are subtypes of Object?.

Levi-Lesches avatar Oct 03 '21 01:10 Levi-Lesches

Very insightful, thank you @Levi-Lesches.

I presumed dynamic objects should make dart check for Object? extensions since all objects basically are subtypes of it. Thanks for clearing that up.

om-ha avatar Oct 03 '21 11:10 om-ha

I think I'll add a little bit more detail. There are actually some situations where the static type of an expression could be any type whatsoever, and we're still going to invoke an extension method on Object? on it. For example, in the body of f below, the value of X could be any type:

extension E on Object? {
  void foo() => print('Running E.foo!');
}

void f<X>(X x) {
  x.foo();
}

main() => f<num>(2); // 'Running E.foo!'.

So the fact that we don't run extension methods on receivers of type dynamic is a separate rule about that type.

dynamic receives special treatment (and so does Never) with extension methods, because they are considered to have all members, with all signatures. So if e as static type dynamic then for any m, if you try to do e.m(true, 'Hello') or e.m = 42, etc., the analyzer and compiler will trust the value of e to actually have a suitable member named m. You might get a dynamic error because the actual receiver doesn't have any such m at run time, but at compile time we assume that it does.

An extension method is only applicable in the case where the static type of the receiver does not have a member with that name (technically: with that 'basename', which is needed in order to include setters), and this means that we will simply never call an extension method on a receiver of static type dynamic or Never.

eernstg avatar Oct 04 '21 08:10 eernstg

Thank you @eernstg for the awesome explanation!

om-ha avatar Oct 04 '21 09:10 om-ha

Other use case:

[ for (final key in keys) box.get(key) as? Item ]

gruvw avatar Jan 02 '22 11:01 gruvw

No but let's say I wanted null if key not in box (to later take action on null elements from the list)

gruvw avatar Jan 03 '22 07:01 gruvw

// The list type is List<Item?>
[ for (final key in keys) box.get(key) as? Item ]

@gruvw The code above may add null to the list. Why not check it like below and keep the code null-safe?

// The list type is List<Item>
[ for (final key in keys) if (box.get(key) is Item) key ]

EDIT: Forgot to write the variable name after the loop

You may write less characters now, but in the future you will need to check if the element is not null for each list element.

for (final element in list) {
  if (element != null) { ... }
}

In the end you will lose null-safely, write more code and have a worse performance.

Wdestroier avatar Apr 25 '22 23:04 Wdestroier

If we had null aware elements (#323), then

 [for (var key in keys) ?(box.get(key) as? Item)]

would work well. But then,

 keys.map(box.get).whereType<Item>().toList()

already works.

The alternative needs an object after the if, so it would be:

[ for (final key in keys) if (box.get(key) is Item) box.get(key) as Item]

or it needs away to introduce a local variable. Say we had let/in:

[for (final key in keys) let value = box.get(key) in if (value is Item) value]

Still, nothing beats the brevity of the first version.

Edit: We now allow patterns in collection if:

[ for (final key in keys) if (box.get(key) case Item item) item) ]

Still not as short as the first version, but also more generally applicable.

We can implement as? as:

extension AsQ<T> on T {
  R? tryAs<R>() {
    var self = this; 
    return self is R ? self : null;
  }

That's longer than => this is R ? this : null, because we still don't have this promotion.

So it's not because the functionality cannot exist, just that o.tryAs<R>() doesn't look as nice as o as? R. Should be just as efficient, if the tryAs function gets properly inlined. (If we had generic getters, o.tryAs<R> would look slightly better than the o.tryAs<R>(). The () is just noise, but needed because generics only apply to functions and constructors.)

lrhn avatar Apr 26 '22 07:04 lrhn

Any chance this will be added to Dart? Super handy. atm we just use something like value is SomeClass ? value as SomeClass : null

seanhamstra avatar Dec 13 '22 19:12 seanhamstra

@seanhamstra May I ask what is the complete function/method body?

Wdestroier avatar Dec 13 '22 19:12 Wdestroier

With patterns, you should be able to do

  switch (value) {case SomeClass x => x, _ => null}

instead. Not much of a saving in size, but you don't need to repeat the value expression.

With #2664 it would be (value case SomeClass x) ? x : null.

lrhn avatar Dec 13 '22 20:12 lrhn