Dart null / false / empty checking: How to write this shorter?

后端 未结 6 916
逝去的感伤
逝去的感伤 2020-12-04 23:49

This is my code for true on everything but empty string, null and false:

if (routeinfo[\"no_route\"] == \"\" || routeinfo[\"no_route\"] == null || routeinfo[         


        
相关标签:
6条回答
  • 2020-12-05 00:07

    Late 2020 Update

    Summary

    • This answer holds true, except for isNull and isNotNull. They no longer provide type promotion when Null Safety is introduced in dart/flutter in the future.
    • Other helpers like isNullOrEmpty do not provide type promotion, as they are in a different (sub-)scope compared to callsite.
    • My personal opinion, is that you can drop isNull and isNotNull but keep other helpers as you shouldn't expect them to do type promotion for you.

    Explanation

    • This is because Null Safety was finally introduced in Dart/Flutter. But as of October 2020, this feature is still not available for stable releases on dart/flutter. Check out the quick guide Null Safety or the thorough Understanding Null Safety.
    • Null Safety in Dart/Flutter should be similar to Swift (Optional) and Kotlin (Nullable Types, Non-Nullable Types).

    Demonstration

    Here's a demonstration why encapsulation/helper-getter of isNull (== null) and isNotNull (!= null) is a very big problem:

    // Promotion works
    int definitelyInt(int? aNullableInt) {
      if (aNullableInt == null) { // Promote variable `aNullableInt` of Nullable type `int?` to Non-Nullable type `int`
        return 0;
      }
      return aNullableInt; // Can't be null! This variable is promoted to non-nullable type `int`
    }
    

    When "Null Safety" is shipped in the dart release you are using, the type promotion in above code works! HOWEVER:

    // Promotion does NOT work!!!
    int definitelyInt(int? aNullableInt) {
      if (aNullableInt.isNull) { // does NOT promote variable `aNullableInt` of Nullable type `int?`
        return 0;
      }
      return aNullableInt; // This variable is still of type `int?`!!!
    }
    

    The above doesn't work, because the null check == null and != null are encapsulated in a sub-scope (different stack frame) and not inferred to have this "promotion" effect within definitelyInt scope.



    Early 2020 Update

    Here's a modified version using getters instead of instance/class methods and covering whitespaces, isNull, and isNotNull.

    Second Update

    It also accounts for empty lists and maps now!

    Third Update

    Added private getters for safety and modularity/maintainability.

    Fourth Update

    Updated the answer to fix a particular problem with Map, it's not correctly being handled when empty! Because Map is not an Iterable. Solved this issue by introducing _isMapObjectEmpty U

    Solution

    extension TextUtilsStringExtension on String {
      /// Returns true if string is:
      /// - null
      /// - empty
      /// - whitespace string.
      ///
      /// Characters considered "whitespace" are listed [here](https://stackoverflow.com/a/59826129/10830091).
      bool get isNullEmptyOrWhitespace =>
          this == null || this.isEmpty || this.trim().isEmpty;
    }
    
    /// - [isNullOrEmpty], [isNullEmptyOrFalse], [isNullEmptyZeroOrFalse] are from [this StackOverflow answer](https://stackoverflow.com/a/59826129/10830091)
    extension GeneralUtilsObjectExtension on Object {
      /// Returns true if object is:
      /// - null `Object`
      bool get isNull => this == null;
    
      /// Returns true if object is NOT:
      /// - null `Object`
      bool get isNotNull => this != null;
    
      /// Returns true if object is:
      /// - null `Object`
      /// - empty `String`s
      /// - empty `Iterable` (list, set, ...)
      /// - empty `Map`
      bool get isNullOrEmpty =>
          isNull ||
          _isStringObjectEmpty ||
          _isIterableObjectEmpty ||
          _isMapObjectEmpty;
    
      /// Returns true if object is:
      /// - null `Object`
      /// - empty `String`
      /// - empty `Iterable` (list, map, set, ...)
      /// - false `bool`
      bool get isNullEmptyOrFalse =>
          isNull ||
          _isStringObjectEmpty ||
          _isIterableObjectEmpty ||
          _isMapObjectEmpty ||
          _isBoolObjectFalse;
    
      /// Returns true if object is:
      /// - null `Object`
      /// - empty `String`
      /// - empty `Iterable` (list, map, set, ...)
      /// - false `bool`
      /// - zero `num`
      bool get isNullEmptyFalseOrZero =>
          isNull ||
          _isStringObjectEmpty ||
          _isIterableObjectEmpty ||
          _isMapObjectEmpty ||
          _isBoolObjectFalse ||
          _isNumObjectZero;
    
      // ------- PRIVATE EXTENSION HELPERS -------
      /// **Private helper**
      ///
      /// If `String` object, return String's method `isEmpty`
      ///
      /// Otherwise return `false` to not affect logical-OR expression. As `false` denotes undefined or N/A since object is not `String`
      bool get _isStringObjectEmpty =>
          (this is String) ? (this as String).isEmpty : false;
    
      /// **Private helper**
      ///
      /// If `Iterable` object, return Iterable's method `isEmpty`
      ///
      /// Otherwise return `false` to not affect logical-OR expression. As `false` denotes undefined or N/A since object is not `Iterable`
      bool get _isIterableObjectEmpty =>
          (this is Iterable) ? (this as Iterable).isEmpty : false;
    
      /// **Private helper**
      ///
      /// If `Map` object, return Map's method `isEmpty`
      ///
      /// Otherwise return `false` to not affect logical-OR expression. As `false` denotes undefined or N/A since object is not `Map`
      bool get _isMapObjectEmpty => (this is Map) ? (this as Map).isEmpty : false;
    
      /// **Private helper**
      ///
      /// If `bool` object, return `isFalse` expression
      ///
      /// Otherwise return `false` to not affect logical-OR expression. As `false` denotes undefined or N/A since object is not `bool`
      bool get _isBoolObjectFalse =>
          (this is bool) ? (this as bool) == false : false;
    
      /// **Private helper**
      ///
      /// If `num` object, return `isZero` expression
      ///
      /// Otherwise return `false` to not affect logical-OR expression. As `false` denotes undefined or N/A since object is not `num`
      bool get _isNumObjectZero => (this is num) ? (this as num) == 0 : false;
    }
    

    Assumptions

    This presumes Dart 2.7 or above to support Extension Methods.

    Usage

    The above are two Extension classes. One for Object and one for String. Put them in a file separately/together and import the files/file at callsite file.

    Description

    Coming from Swift, I tend to use extensions like user @Benjamin Menrad's answer but with getter instead of method/function when applicable -- mirroring Dart's computed properties like String.isEmpty.

    Coming from C#, I like their String's helper method IsNullOrWhiteSpace.

    My version merges the above two concepts.

    Naming

    Rename the Extension classes whatever you like. I tend to name them like this:

    XYExtension

    Where:

    • X is File/Author/App/Unique name.
    • Y is name of Type being extended.

    Name examples:

    • MyAppIntExtension
    • DartDoubleExtension
    • TextUtilsStringExtension
    • OHProviderExtension

    Criticism

    Why private getters instead of simple this == null || this == '' || this == [] || this == 0 || !this

    • I think this is safer as it first casts the object to the right type we want to compare its value to.
    • More modular as changes are central within the private getters and any modification is reflected everywhere.
    • If type check fails within the private getter, we return false which doesn't affect the outcome of the root logical-OR expression.
    0 讨论(0)
  • 2020-12-05 00:07

    package:quiver has an isEmpty function that returns true if the argument is null or the empty string.

    It's also trivial to implement such a function yourself.

    0 讨论(0)
  • 2020-12-05 00:10

    I would write a helper function instead of doing everything inline.

    bool isNullEmptyOrFalse(Object o) =>
      o == null || false == o || "" == o;
    
    bool isNullEmptyFalseOrZero(Object o) =>
      o == null || false == o || 0 == o || "" == o;
    

    That avoids the repeated lookup (like the contains operation), but it is much more readable. It also doesn't create a new List literal for each check (making the list const could fix that).

    if (isNullEmptyOrFalse(routeinfo["no_route"])) { ... }
    

    When struggling with making something short and readable, making a well-named helper function is usually the best solution.

    (Addition: Now that Dart has extension methods, it's possible to add the functionality as methods or getters that are seemingly on the object, so you can write value.isNullOrEmpty directly).

    0 讨论(0)
  • 2020-12-05 00:25

    As coming from Android and Kotlin, I prefer extension methods over a static helper method. And with Dart 2.7 you can now use that as well:

    extension Extension on Object {
      bool isNullOrEmpty() => this == null || this == '';
    
      bool isNullEmptyOrFalse() => this == null || this == '' || !this;
    
      bool isNullEmptyZeroOrFalse() =>
          this == null || this == '' || !this || this == 0;
    }
    

    Now you can just call these methods from anywhere in your code:

    if (anyVariable.isNullOrEmpty()) {
      // do something here
    }
    

    You might need to manually import the dart class, where you put your extension methods, for example:

    import 'package:sampleproject/utils/extensions.dart';
    
    0 讨论(0)
  • 2020-12-05 00:29

    If your requirement was simply empty or null (like mine when I saw this title in a search result), you can use Dart's safe navigation operator to make it a bit more terse:

    if (routeinfo["no_route"]?.isEmpty ?? true) {
      // 
    }
    

    Where

    • isEmpty checks for an empty String, but if routeinfo is null you can't call isEmpty on null, so we check for null with
    • ?. safe navigation operator which will only call isEmpty when the object is not null and produce null otherwise. So we just need to check for null with
    • ?? null coalescing operator
    0 讨论(0)
  • 2020-12-05 00:32

    You could do

    if (["", null, false, 0].contains(routeinfo["no_route"])) {
      // do sth
    }
    
    0 讨论(0)
提交回复
热议问题