有没有更好的方法来编写这样的嵌套 if-else?

Is there a better way to write a nested if-else such as this?

以下是返回小部件的 flutter 函数的代码片段。它可以是 returns 按钮或文本小部件,具体取决于四个不同的变量。

Widget _renderReservationState() {
          if (reservationsDetailState.isFinished == false) {
            if (reservationsDetailState.rsvpStatus == "Attending") {
              if (reservationsDetailState.attendanceStatus == "Attended") {
                return Center(
                    child: Text("Registration completed");)
              } else {
                if (reservationsDetailState.isEditing == false) {
                  return SizedBox(
                      child: Button(
                          onPressed: () {
                            if (myReservationsDetailState.isEditing) {
                              _setEditMode(false);
                            } else {
                              _setEditMode(true);
                            }
                          },
                          title: "Edit reservation"),
                      width: double.infinity);
                } else {
                  return SizedBox(
                      child: Button(
                          onPressed: () {
                            if (reservationsDetailState.isEditing) {
                              _confirmReservation(dateController.text,
                                  timeController.text, attendeeController.text);
                            } else {
                              _setEditMode(true);
                            }
                          },
                          title: "Confirm reservation"),
                      width: double.infinity);
                }
              }
            } else {
              return SizedBox(
                  child: Button(
                      onPressed: () => {
                            _confirmReservation(dateController.text,
                                timeController.text, attendeeController.text),
                          },
                      title: "Make a reservation"),
                  width: double.infinity);
            }
          } else {
            if (myReservationsDetailState.attendanceStatus == "Attended") {
              return Center(
                child: Text("Thank you for your visit")
              );
            } else {
              return Center(
                  child: Text("Please join next time");)
            }
          }
        }

现有代码是几百行的嵌套 if-else。我已经将它重构为更短,但我对这个实现也不满意。如有任何建议或建议,我们将不胜感激。

如果您将代码拆分为许多较小的函数或 CustomWidget,您将帮自己一个忙并显着提高代码的可读性。这样你就可以摆脱极端的、不可读的嵌套。

这看起来像是对联合类型的很好使用。让我尝试将它与 ifs 和 switches 进行比较。

当您必须以各种不同的方式对值进行组合时,有时不可避免地会出现这样的代码。您的选择是:

  1. 嵌套 if 语句,就像您所做的那样
  2. 嵌套 switch 语句
  3. 将联合类型与包一起使用,例如 freezed

让我们考虑一下各自的优缺点:

  1. 嵌套 if 语句:
  • 易于上手
  • 最少的缩进量
  • (!) 用 || 分组案例有点尴尬
  • (!)有些情况容易忘记
  • (!) 出现新情况时要记得编辑代码
  1. 嵌套 switch 语句:
  • 相对容易打字
  • 可以分组案例
  • (!) 额外的缩进级别
  • (!) 个案例共享局部变量
  • 使用枚举,IDE 可以为缺失的案例提供警告。所以下面两个可以缓解
    • (!)有些情况容易忘记
    • (!) 出现新情况时要记得编辑代码
  1. 联合类型
  • 强制你涵盖所有情况。使忘记案例成为 compile-time 错误。
  • 添加新案例时,现有代码无法编译,除非您覆盖所有新案例。
  • 案例成为对象而不是简单的值,您可以为它们添加额外的职责。
  • (!) 冻结不可能对案例进行分组,只有一个“其他”案例 orElse
  • (!) 额外级别的缩进和额外的闭包语法
  • (!) 需要一个额外的步骤。使用 freezed 你必须创建那些 classess 和 运行 build_runner 来生成代码。

我相信这些是主要的选择。当您处于这种情况并且担心自己或队友有一天会忘记某个案例时,尝试 union classes with freezed 可能是个好主意。尽管带来不便,但它提供的覆盖所有案例的思想是无价的。

从包中借用例子,当你有一个 union class 不同的情况如下:

@freezed
class Union with _$Union {
  const factory Union(int value) = Data;
  const factory Union.loading() = Loading;
  const factory Union.error([String? message]) = ErrorDetails;
}

你这样用。不提供 when 中的任何参数是 compile-time 错误。所以你可以确定你不会忘记任何情况:

var union = Union(42);

print(
  union.when(
    (int value) => 'Data $data',
    loading: () => 'loading',
    error: (String? message) => 'Error: $message',
  ),
); // Data 42

在您的情况下,您可以为每个变量创建单独的联合类型,也可以创建表示这些变量组合的单个联合类型。所以你可以有

@freezed
class IsFinished with _$IsFinished {
  const factory IsFinished.finished() = Finished;
  const factory IsFinished.notFinished() = NotFinished;
}
@freezed
class RsvpStatus with _$RsvpStatus {
  const factory RsvpStatus.none() = None;
  const factory RsvpStatus.rsvpd() = Rsvpd;
  const factory RsvpStatus.canceled() = Canceled;
}
// usage
Widget w = isFinished.when(
  finished: () => isRsvpd.when(
    none: () => Text('finished and no rsvp'),
    rsvpd: () => Text('finished and rsvpd'),
    cancelled: () => Text('finished and canceled'),
  ),
  notFinished: () => isRsvpd.when(
    none: () => Text('not finished and no rsvp'),
    rsvpd: () => Text('not finished and rsvpd'),
    cancelled: () => Text('not finished and canceled'),
  ),
);

或一张冻结的 class,其中包含 finishedRsvpd 等条目。在任何一种情况下,忘记大小写都将是一个 compile-time 错误,您会更放心地编写代码。

关于提高可读性的一些一般性建议:

  • 去掉不必要的 if 检查:

    if (someCondition) {
      _setEditMode(false);
    } else {
      _setEditMode(true);
    }
    

    等同于:

    _setEditMode(!someCondition);
    
  • 避免if (condition == false)。如果 condition 是 non-nullable,请使用 if (!condition)

  • 尽可能将嵌套的 if 块折叠成 if-else if 链:

    if (condition1) {
      ...
    } else {
      if (condition2) {
        ...
      } else {
        ...
      }
    }
    

    可以取消缩进一个级别为:

    if (condition1) {
      ...
    } else if (condition2) {
      ...
    } else {
      ...
    }
    
  • 不要写top-heavyif-else块。代码如下:

    if (condition1) {
      imagine();
      that();
      there();
      are();
      many();
      many();
      many();
      lines();
      ofCode();
      if (condition2) {
        withNested();
        blocks();
      } else {
        that();
        require();
        scrolling();
      }
    } else {
      someSingleLineOfCode();
    }
    

    通常比反转条件并将更短的块放在顶部更难遵循:

    if (!condition1) {
      someSingleLineOfCode();
    } else {
      imagine();
      that();
      there();
      are();
      many();
      many();
      many();
      lines();
      ofCode();
      if (condition2) {
        withNested();
        blocks();
      } else {
        that();
        require();
        scrolling();
      }
    }
    
  • Unindent else 块利用提前退出:

    if (condition1) {
      return someWidget;
    } else {
      lots();
      of();
      other();
      code();
      return someOtherWidget;
    }
    

    可以是:

    if (condition1) {
      return someWidget;
    }
    
    lots();
    of();
    other();
    code();
    return someOtherWidget;
    
  • 尽量强调不同案例中哪些部分相同,哪些部分不同。示例:

    if (myReservationsDetailState.attendanceStatus == "Attended") {
      return Center(child: Text("Thank you for your visit"));
    } else {
      return Center(child: Text("Please join next time"));
    }
    

    可以变成:

      return Center(
          child: Text(myReservationsDetailState.attendanceStatus == "Attended"
              ? "Thank you for your visit"
              : "Please join next time"));
    

    对于具有多个相似 SizedBox(child: Button(...)) 案例的部分,我会创建局部变量来捕获差异并重新排列代码以便共享公共结构。

综合起来:

Widget _renderReservationState() {
  if (reservationsDetailState.isFinished) {
    return Center(
        child: Text(myReservationsDetailState.attendanceStatus == "Attended"
            ? "Thank you for your visit"
            : "Please join next time"));
  }

  if (reservationsDetailState.rsvpStatus == "Attending" &&
      reservationsDetailState.attendanceStatus == "Attended") {
    return Center(child: Text("Registration completed"));
  }

  VoidCallback onButtonPressed;
  String buttonTitle;
  if (reservationsDetailState.rsvpStatus != "Attending") {
    onButtonPressed = () => _confirmReservation(
        dateController.text, timeController.text, attendeeController.text);
    buttonTitle = "Make a reservation";
  } else if (!reservationsDetailState.isEditing) {
    onButtonPressed = () => _setEditMode(!myReservationsDetailState.isEditing);
    buttonTitle = "Edit reservation";
  } else {
    onButtonPressed = () {
      if (reservationsDetailState.isEditing) {
        _confirmReservation(
            dateController.text, timeController.text, attendeeController.text);
      } else {
        _setEditMode(true);
      }
    };
    buttonTitle = "Confirm reservation";
  }

  return SizedBox(
      child: Button(onPressed: onButtonPressed, title: buttonTitle),
      width: double.infinity);
}

它仍然很丑,但缩进明显减少了,在我看来比原来更好。