dart-lang / sdk

The Dart SDK, including the VM, JS and Wasm compilers, analysis, core libraries, and more.
https://dart.dev
BSD 3-Clause "New" or "Revised" License
10.21k stars 1.57k forks source link

Null safety feedback: How to use firstWhere? #42947

Open xvrh opened 4 years ago

xvrh commented 4 years ago

EDIT - best solutions so far:

import 'package:collection/collection.dart';

void main() {
  var list = ['a', 'b', 'c'];
  var d = list.firstWhereOrNull((e) => e == 'd');
}
extension IterableExtension<T> on Iterable<T> {
  T? firstWhereOrNull(bool Function(T element) test) {
    for (var element in this) {
      if (test(element)) return element;
    }
    return null;
  }
}

void main() {
  var list = ['a', 'b'];
  var d = list.firstWhereOrNull((e) => e == 'd');
}

Original question:

I want to search a list and return null when the element is not found

void main() {
  var list = ['a', 'b', 'c'];

  String? d = list.firstWhere((e) => e == 'd', orElse: () => null);
}

https://nullsafety.dartpad.dev/2d0bc36ec1f3a5ade5550c0944702d73

With null safety, I get this error:

main.dart:4:62: Error: A value of type 'Null' can't be assigned to a variable of type 'String'.
  String? d = list.firstWhere((e) => e == 'd', orElse: () => null);

The orElse parameter of firstWhere is declared not nullable:

E firstWhere(bool test(E element), {E orElse()?})

Why not?

E? firstWhere(bool test(E element), {E? orElse()?})

What is the recommended way to search a collection with null safety enabled?

cedvdb commented 2 years ago

Actually, some have suggested the !! operator for just that.

The !! operator does not force you to do anything which is not null safe since there is no checked exceptions. In this context I'd much prefer the return type to be nullable. It's indeed a bit annoying when you are sure that there is an element in the list, but if you are really sure I don't see why ! would not fit here.

I would be backward to have to add !! when you are sure than the where clause might not be matched all the time instead of adding ! when you are sure it will.

The one issue with just making the return type nullable is that you can no longer distinguish a null from finding no value satisfying where, and where null is a value which satisfies the where.

That's reasonable but that's such an edge case compared to the dozen times you are not in this scenario does it matter ?

Levi-Lesches commented 2 years ago

It's been a while, but I still think it would be beneficial to make firstWhere nullable (note the lack of shouldThrow):

E? firstWhere(bool Function(E) test, {@deprecated E Function()? orElse}) { 
  for (final E element in this) {
    if (test(element)) return element;
  }
  if (orElse != null) return orElse();
  return null;
}

To avoid breaking changes, keep the current semantics of orElse, but mark it @deprecated and encourage ?? instead (although removing it may actually be safe, see below).

I thought a bit more, and I don't think we need to handle a case where firstWhere checks for null. If you want to check that null is in your list, use .contains(null). But if you want the element that equals null... just use null? I'm not sure why we had so many examples in this thread like

List<int?> a;
int? b = myList.firstWhere((c) => c == null);

There is no benefit to checking myList here, just set b = null directly. Thus, using ?? for defaults is never an issue.

Then, the big breaking change that doesn't get caught automatically is people using try/catch (with something useful in the catch) and depending on .firstWhere to throw, like this:

List<int> a;
int? b;

// this won't be affected by making firstWhere nullable, as b will still be null
try { b = a.firstWhere((c) => c % 2 == 0); } catch (_) { /* leave b null */} 

// but this will, as the catch block will never run
try { b = a.firstWhere((c) => c % 2 == 0); } catch (_) { b = 0; } 

But I don't think that's such a big issue either. In the first case, as the comment says, making .firstWhere return null on failures keeps the current behavior. In the case of a useful default, people are hopefully using orElse instead, which is the only case that would really break:

int b = list.firstwhere((c) => c % 2, orElse: () => 0);  // int? cannot be assigned to int

However, this is a relatively quick fix by either changing orElse to ?? or adding a !, which can be automated with a dart fix.

lrhn commented 2 years ago

Would be nice. If I had to redesign firstWhere today, I'd definitely do that. (And, shucks, someone actually told me that orElse was a bad idea when I first introduced it, so ... you were right! We should have designed with null safety in mind all the way :cry:.)

It's still not going to happen. There is too much code out there which will need to be changed from .firstWhere(...) to .firstWhere(...)!. It's just not viable to migrate it all. We can't just rely on dartfix because a change to the API will affect all code, not just code migrated to the newest language version. We'll need some kind of API versioning for us to be able to make such a breaking change work. We don't have that.

Also, all the existing third-party implementations of firstWhere which return a non-nullable result will stay valid, and will still throw. We need to migrate those too, but there is nothing which will help us find them (since they're still valid).

sebthom commented 1 year ago

Since Dart 3.0.0 is about streamlining the dart APIs, enabling null safety by default and contains a lot of breaking changes anyways, couldn't this please be fixed/redesigned too now?

lrhn commented 1 year ago

I'd love to, but it's still really not a viable breaking change. Too hard and work-intensive to migrate, too breaking to not migrate.

I don't expect this change can happen until we actually get library versioning, where new code can use the new API, and old code can stay on the old API, on the same object and type. That's not currently on the table (we keep thinking about it, but it's not on the short-list of features we are currently working actively on.)

lukepighetti commented 1 year ago

Until that happens, firstWhereOrNull from collection is OK

lukepighetti commented 1 year ago

I will note that in practice firstWhere is becoming a bit of a footgun. If we could get firstWhereOrNull in the core framework it would be more discoverable

BlagojeV93 commented 3 weeks ago

My workaround is to wrap the firstWhere logic with try catch block, and handle null exceptions inside catch

Levi-Lesches commented 1 week ago

Maybe there should be a @deprecated on .firstWhere() that recommends to use firstWhereOrNull? Or if @deprecated would cause too many warnings, a lint in package:lints/recommended.yaml? If that's too breaking, a lint that's not enabled by default but is recommended on the firstWhere docs so teams can audit their firstWhere use?

The reason I bring it up is because ! is the recommended way of exposing these kinds of errors, but firstWhere hides it when quickly reading code.