Home > front end >  What's better, an initially empty non-nullable or an initially nullable and null container?
What's better, an initially empty non-nullable or an initially nullable and null container?

Time:10-07

Let's say you're managing the list of serial numbers of the bicycles owned by residents in your building, with the objective of planning ahead to build additional safe bike storage.

Some people will of course have no bikes.

In Dart > 2.12 (with null safety) you could use a non-nullable List<String> and initialize it to an empty list

class Resident {
  String name;
  List<String> bicycles = [];
}

or you could use a nullable List<String> and use null as a flag to signal that someone has no bikes.

class Resident {
  String name;
  List<String>? bicycles;
}

Both designs are of course workable, but does it turn out down the road that one is clearly better than the other—more idiomatic to the new Dart, for example? In other words, what's better, an initially empty non-nullable or an initially nullable and null container?

Even if I count the bits needed, it's not quite clear. There would be wasted storage to construct an empty list in the first case, but there is also storage wasted in the second case—though it's of an unknown, and perhaps implementation dependent—amount.

CodePudding user response:

If you want to represent having none of something, then prefer non-nullable container types with an empty state to nullable types.

  • With a nullable container, you need to potentially need to deal with the container being empty anyway, and now you have to do extra work to check for null everywhere. Meanwhile, dealing with an empty container often doesn't involve any extra work. Contrast:
    // Nullable case.
    final bicycles = resident.bicycles;
    if (bicycles != null) {
      for (var bicycle in bicycles) {
        doSomething(bicycle);
      }
    }
    
    with
    /// Non-nullable case.
    for (var bicycle in resident.bicycles) {
      doSomething(bicycle);
    }
    
  • You could try to reset references to null when the container becomes empty so that there aren't two cases to deal with, but as noted above, the empty case often is free anyway, so that'd be more work for usually no gain. Furthermore, resetting references can be a lot of extra work:
    var list = [1, 2, 3];
    mutateList(list);
    if (list.isEmpty) {
      list = null;
    }
    
    If mutateList could remove elements from list, then every caller would need to do extra work to replace empty Lists to null.
  • Even if you don't care to replace empty containers with null, you'd still have different behaviors when transitioning to a non-empty container. Consider:
    var sam = Resident();
    var samsBicycles = sam.bicycles;
    sam.addBicycle();
    
    What would you expect samsBicycles to be? If Resident.bicycles is initially null, then samsBicycles will remain null and will no longer refer to the same object as sam.bicycles.
  • Related