【问题标题】:How to split up complex conditions and keep short circuit evaluation?如何拆分复杂条件并保持短路评估?
【发布时间】:2010-12-28 01:47:19
【问题描述】:

有时条件会变得相当复杂,因此为了便于阅读,我通常将它们分开并给每个组件一个有意义的名称。然而,这会破坏短路评估,这可能会带来问题。我想出了一个包装方法,但在我看来它太冗长了。

谁能为此想出一个巧妙的解决方案?

有关我的意思的示例,请参见下面的代码:

public class BooleanEvaluator {

    // problem: complex boolean expression, hard to read
    public static void main1(String[] args) {

        if (args != null && args.length == 2 && !args[0].equals(args[1])) {
            System.out.println("Args are ok");
        }
    }

    // solution: simplified by splitting up and using meaningful names
    // problem: no short circuit evaluation
    public static void main2(String[] args) {

        boolean argsNotNull = args != null;
        boolean argsLengthOk = args.length == 2;
        boolean argsAreNotEqual = !args[0].equals(args[1]);

        if (argsNotNull && argsLengthOk && argsAreNotEqual) {
            System.out.println("Args are ok");
        }
    }

    // solution: wrappers to delay the evaluation 
    // problem: verbose
    public static void main3(final String[] args) {

        abstract class BooleanExpression {
            abstract boolean eval();
        }

        BooleanExpression argsNotNull = new BooleanExpression() {
            boolean eval() {
                return args != null;
            }
        };

        BooleanExpression argsLengthIsOk = new BooleanExpression() {
            boolean eval() {
                return args.length == 2;
            }
        };

        BooleanExpression argsAreNotEqual = new BooleanExpression() {
            boolean eval() {
                return !args[0].equals(args[1]);
            }
        };

        if (argsNotNull.eval() && argsLengthIsOk.eval() && argsAreNotEqual.eval()) {
            System.out.println("Args are ok");
        }
    }
}

对答案的回应:

感谢您的所有想法!目前已提交以下替代方案:

  • 换行并添加 cmets
  • 保持原样
  • 提取方法
  • 提前退货
  • 嵌套/拆分 if's

换行并添加 cmets:

Eclipse 中的代码格式化程序(ctrl+shift+f)会撤消在条件中添加换行符。内联 cmets 对此有所帮助,但在每一行上留下的空间很小,并且可能导致难看的换行。然而,在简单的情况下,这可能就足够了。

保持原样:

我给出的示例条件非常简单,因此在这种情况下您可能不需要解决可读性问题。我在考虑条件更复杂的情况,例如:

private boolean targetFound(String target, List<String> items,
        int position, int min, int max) {

    return ((position >= min && position < max && ((position % 2 == 0 && items
            .get(position).equals(target)) || (position % 2 == 1)
            && position > min && items.get(position - 1).equals(target)))
            || (position < min && items.get(0).equals(target)) || (position >= max && items
            .get(items.size() - 1).equals(target)));
}

我不建议保持原样。

提取方法:

我考虑了提取方法,正如几个答案中所建议的那样。这样做的缺点是这些方法通常具有非常低的粒度,并且它们本身可能没有多大意义,因此它可能会使您的类变得混乱,例如:

private static boolean lengthOK(String[] args) {
    return args.length == 2;
}

这真的不应该成为类级别的单独方法。您还必须将所有相关参数传递给每个方法。如果您创建一个单独的类纯粹用于评估非常复杂的条件,那么这可能是 IMO 的一个好的解决方案。

我尝试使用 BooleanExpression 方法实现的是逻辑保持本地化。请注意,即使 BooleanExpression 的声明也是本地的(我认为我以前从未遇到过本地类声明的用例!)。

提前退货:

早期退货解决方案似乎足够了,尽管我不赞成这个成语。另一种表示法:

public static boolean areArgsOk(String[] args) {

    check_args: {
        if (args == null) {
            break check_args;
        }
        if (args.length != 2) {
            break check_args;
        }
        if (args[0].equals(args[1])) {
            break check_args;
        }
        return true;
    }
    return false;
}

我知道大多数人讨厌标签和中断,而且这种风格可能太不常见而无法被认为是可读的。

嵌套/拆分 if's:

它允许结合优化评估引入有意义的名称。一个缺点是可能会出现复杂的条件语句树

摊牌

因此,为了看看我最喜欢哪种方法,我将几个建议的解决方案应用于上面介绍的复杂 targetFound 示例。这是我的结果:

嵌套/拆分 if,具有有意义的名称 非常冗长,有意义的名称并没有真正帮助这里的可读性

private boolean targetFound1(String target, List<String> items,
        int position, int min, int max) {

    boolean result;
    boolean inWindow = position >= min && position < max;
    if (inWindow) {

        boolean foundInEvenPosition = position % 2 == 0
                && items.get(position).equals(target);
        if (foundInEvenPosition) {
            result = true;
        } else {
            boolean foundInOddPosition = (position % 2 == 1)
                    && position > min
                    && items.get(position - 1).equals(target);
            result = foundInOddPosition;
        }
    } else {
        boolean beforeWindow = position < min;
        if (beforeWindow) {

            boolean matchesFirstItem = items.get(0).equals(target);
            result = matchesFirstItem;
        } else {

            boolean afterWindow = position >= max;
            if (afterWindow) {

                boolean matchesLastItem = items.get(items.size() - 1)
                        .equals(target);
                result = matchesLastItem;
            } else {
                result = false;
            }
        }
    }
    return result;
}

嵌套/拆分 if,使用 cmets 不那么冗长,但仍然难以阅读并且容易产生错误

private boolean targetFound2(String target, List<String> items,
        int position, int min, int max) {

    boolean result;
    if ((position >= min && position < max)) { // in window

        if ((position % 2 == 0 && items.get(position).equals(target))) {
            // even position
            result = true;
        } else { // odd position
            result = ((position % 2 == 1) && position > min && items.get(
                    position - 1).equals(target));
        }
    } else if ((position < min)) { // before window
        result = items.get(0).equals(target);
    } else if ((position >= max)) { // after window
        result = items.get(items.size() - 1).equals(target);
    } else {
        result = false;
    }
    return result;
}

提前回报 更紧凑,但条件树仍然很复杂

private boolean targetFound3(String target, List<String> items,
        int position, int min, int max) {

    if ((position >= min && position < max)) { // in window

        if ((position % 2 == 0 && items.get(position).equals(target))) {
            return true; // even position
        } else {
            return (position % 2 == 1) && position > min && items.get(
                    position - 1).equals(target); // odd position
        }
    } else if ((position < min)) { // before window
        return items.get(0).equals(target);
    } else if ((position >= max)) { // after window
        return items.get(items.size() - 1).equals(target);
    } else {
        return false;
    }
}

提取方法 在您的班级中产生无意义的方法 参数传递很烦人

private boolean targetFound4(String target, List<String> items,
        int position, int min, int max) {

    return (foundInWindow(target, items, position, min, max)
            || foundBefore(target, items, position, min) || foundAfter(
            target, items, position, max));
}

private boolean foundAfter(String target, List<String> items, int position,
        int max) {
    return (position >= max && items.get(items.size() - 1).equals(target));
}

private boolean foundBefore(String target, List<String> items,
        int position, int min) {
    return (position < min && items.get(0).equals(target));
}

private boolean foundInWindow(String target, List<String> items,
        int position, int min, int max) {
    return (position >= min && position < max && ((position % 2 == 0 && items
            .get(position).equals(target)) || (position % 2 == 1)
            && position > min && items.get(position - 1).equals(target)));
}

重新审视 BooleanExpression 包装器 注意方法参数必须声明为final 对于这种复杂的情况,冗长是可以辩护的 IMO 如果他们同意的话,也许闭包会让这更容易(-

private boolean targetFound5(final String target, final List<String> items,
        final int position, final int min, final int max) {

    abstract class BooleanExpression {
        abstract boolean eval();
    }

    BooleanExpression foundInWindow = new BooleanExpression() {

        boolean eval() {
            return position >= min && position < max
                    && (foundAtEvenPosition() || foundAtOddPosition());
        }

        private boolean foundAtEvenPosition() {
            return position % 2 == 0 && items.get(position).equals(target);
        }

        private boolean foundAtOddPosition() {
            return position % 2 == 1 && position > min
                    && items.get(position - 1).equals(target);
        }
    };

    BooleanExpression foundBefore = new BooleanExpression() {
        boolean eval() {
            return position < min && items.get(0).equals(target);
        }
    };

    BooleanExpression foundAfter = new BooleanExpression() {
        boolean eval() {
            return position >= max
                    && items.get(items.size() - 1).equals(target);
        }
    };

    return foundInWindow.eval() || foundBefore.eval() || foundAfter.eval();
}

我想这真的取决于情况(一如既往)。对于非常复杂的情况,包装方法可能是可辩护的,尽管它并不常见。

感谢您的所有意见!

编辑:事后诸葛亮。为复杂的逻辑创建一个特定的类可能会更好,例如:

import java.util.ArrayList;
import java.util.List;

public class IsTargetFoundExpression {

    private final String target;
    private final List<String> items;
    private final int position;
    private final int min;
    private final int max;

    public IsTargetFoundExpression(String target, List<String> items, int position, int min, int max) {
        this.target = target;
        this.items = new ArrayList(items);
        this.position = position;
        this.min = min;
        this.max = max;
    }

    public boolean evaluate() {
        return foundInWindow() || foundBefore() || foundAfter();
    }

    private boolean foundInWindow() {
        return position >= min && position < max && (foundAtEvenPosition() || foundAtOddPosition());
    }

    private boolean foundAtEvenPosition() {
        return position % 2 == 0 && items.get(position).equals(target);
    }

    private boolean foundAtOddPosition() {
        return position % 2 == 1 && position > min && items.get(position - 1).equals(target);
    }

    private boolean foundBefore() {
        return position < min && items.get(0).equals(target);
    }

    private boolean foundAfter() {
        return position >= max && items.get(items.size() - 1).equals(target);
    }
}

逻辑足够复杂,需要一个单独的类(和单元测试,耶!)。它将使使用此逻辑的代码更具可读性并促进重用,以防其他地方需要此逻辑。我认为这是一个不错的类,因为它确实有一个单一的职责,并且只有 final 字段。

【问题讨论】:

  • 我喜欢您的第一个解决方案,但是通过正确命名变量,可能没有必要。您的第二个解决方案过于冗长 IMO。

标签: java


【解决方案1】:

你可以使用提前返回(从一个方法)来达到同样的效果:

[应用了一些修复]

  public static boolean areArgsOk(String[] args) {
     if(args == null)
        return false;

     if(args.length != 2)
        return false;

     if(args[0].equals(args[1]))
        return false;

     return true;
  }

  public static void main2(String[] args) {

        boolean b = areArgsOk(args);
        if(b)
           System.out.println("Args are ok");
  }

【讨论】:

  • 您忘记将参数传递给 areArgsOk()。
  • 注意!他有&&条件。使用 if(args == null) return false; if(args.length != 2) 返回假; if(args[0].equals(args[1])) 返回假;返回真;
  • 为了提高可读性,请使用 if 中的方法调用,不要将其分配给变量。 “If(B)”不利于可读性。
【解决方案2】:

实际上,我发现换行符和空格做得很好:

public static void main1(String[] args) {

    if (args != null
        && args.length == 2
        && !args[0].equals(args[1])
        ) {
            System.out.println("Args are ok");
    }
}

诚然,它更适合我的(不受欢迎的)支撑样式(上面未显示),但即使使用上面的方法,如果您将右括号和左括号放在自己的行上,它也可以正常工作(所以它们不会丢失在最后一个条件的结束)。

有时我什至会评论个别位:

public static void main1(String[] args) {

    if (args != null                // Must have args
        && args.length == 2         // Two of them, to be precise
        && !args[0].equals(args[1]) // And they can't be the same
        ) {
            System.out.println("Args are ok");
    }
}

如果你真的想大声疾呼,多个ifs 就可以了:

public static void main1(String[] args) {

    if (args != null) {
        if (args.length == 2) {
            if (!args[0].equals(args[1])) {
                System.out.println("Args are ok");
            }
        }
    }
}

...任何优化编译器都会崩溃。不过对我来说,这可能有点过于冗长了。

【讨论】:

  • Multiple if 是最自然最清晰的解决方案。它准确地显示了正在发生的事情。您也可以在每一行上添加 cmets。但是为什么你需要优化编译器呢?没有什么可以崩溃的。
  • 每行的 cmets 具有 Eclipse 格式化程序不更改换行符的额外优势。
  • 深度嵌套在大多数情况下被认为是不好的做法。但是,我不确定这种特殊情况是否是例外。然而,它确实提高了可读性。
  • @BalusC:是的,这就是为什么我说它太冗长了。 @PauliL:公平点,它可能不需要。
  • 只需添加换行符就会被 Eclipse 中的代码格式化程序撤消(ctrl+shift+f)。条件中的注释有帮助,但给逻辑留下的空间很小。多个 if 是一个合理的想法,尽管深度嵌套不是我最喜欢的可读性。
【解决方案3】:

如果您的目标是可读性,为什么不简单地打破界限并添加 cmets?

    if (args != null                // args not null
        && args.length == 2         // args length is OK
        && !args[0].equals(args[1]) // args are not equal
    ) {

        System.out.println("Args are ok");
    }

【讨论】:

  • 对于更复杂的条件最好将每个条件提取为一个方法,使用每个注释作为命名这些方法的基础。
【解决方案4】:

BooleanExpression 类型似乎不足以成为主类之外的一个类,而且它也为您的应用程序增加了一些智力重量。我只想编写适当命名的私有方法来运行您想要的检查。这要简单得多。

【讨论】:

  • 我认为创建私有方法仍然过于侵入(并且需要传递很多参数)。 BooleanExpression 位于方法范围内,因此我可以拆分条件而不会弄乱我的顶级类。
  • 我不同意。对于任何人来说,使用私有方法比将类嵌入到方法中要容易得多。实际上,BooleanExpression 的定义方式,还不如只使用私有方法。不仅如此,而且因为它们是私有的,所以以后可以更改它们。如果这是像 Groovy 或 Python 这样的语言,那么我会说传入一个方法(或闭包)来处理验证。既然不是,最好保持简单(必须支持这一点的人会感谢你)。 BooleanExpression 试图模仿闭包(或方法参数)。
【解决方案5】:

你必须做变量赋值INSIDE if's。

if (a && b && c) ...

翻译成

calculatedA = a;
if (calculatedA) {
  calculatedB = b;
  if (calculatedB) {
    calculatedC = c;
    if (calculatedC) ....
  }
}

这样做通常是有益的,因为它命名了您正在测试的概念,正如您在示例代码中清楚地展示的那样。这增加了可读性。

【讨论】:

    【解决方案6】:

    您的第一个解决方案适合这种复杂性。如果条件更复杂,我会为您需要运行的每项检查编写私有方法。比如:

    public class DemoStackOverflow {
    
        public static void main(String[] args) {
        if ( areValid(args) ) {
            System.out.println("Arguments OK");
        }
        }
    
        /**
         * Validation of parameters.
         * 
         * @param args an array with the parameters to validate.
         * @return true if the arguments are not null, the quantity of arguments match 
         * the expected quantity and the first and second are not equal; 
         *         false, otherwise.
         */
        private static boolean areValid(String[] args) {
           return notNull(args) && lengthOK(args) && areDifferent(args);
        }
    
        private static boolean notNull(String[] args) {
           return args != null;
        }
    
        private static boolean lengthOK(String[] args) {
           return args.length == EXPECTED_ARGS;
        }
    
        private static boolean areDifferent(String[] args) {
           return !args[0].equals(args[1]);
        }
    
        /** Quantity of expected arguments */
        private static final int EXPECTED_ARGS = 2;
    
    }
    

    【讨论】:

    • 这是我建议的答案的说明。 +1
    【解决方案7】:

    这个问题是从 2009 年开始的,但在未来(Java 8)我们将能够使用 Lambda 表达式,它可能像布尔表达式一样用于这种上下文,但您可以使用它,以便仅在需要时对其进行评估。

    public static void main2(String[] args) {
    
        Callable<Boolean> argsNotNull = () -> args != null;
        Callable<Boolean> argsLengthOk = () -> args.length == 2;
        Callable<Boolean> argsAreNotEqual = () -> !args[0].equals(args[1]);
    
        if (argsNotNull.call() && argsLengthOk.call() && argsAreNotEqual.call()) {
            System.out.println("Args are ok");
        }
    }
    

    你可以用 java 5/6 做同样的事情,但是用匿名类来写效率低下而且更丑陋。

    【讨论】:

      【解决方案8】:

      已经有一些很好的解决方案,但这是我的变体。我通常将 null 检查作为所有(相关)方法中最顶层的保护子句。如果我在这种情况下这样做,它只会在随后的 if 中留下长度和相等性检查,这已经可以被认为是足够降低复杂性和/或提高可读性?

          public static void main1(String[] args) {
      
              if (args == null) return;
      
              if (args.length == 2 && !args[0].equals(args[1])) {
                  System.out.println("Args are ok");
              }
          }
      

      【讨论】:

      • 务实,但对于我认为的复杂情况来说还不够。
      【解决方案9】:

      将其拆分为一个单独的函数(以提高 main() 中的可读性)并添加注释(以便人们了解您要完成的工作)

      public static void main(String[] args) {
          if (argumentsAreValid(args)) {
                  System.out.println("Args are ok");
          }
      }
      
      
      public static boolean argumentsAreValid(String[] args) {
          // Must have 2 arguments (and the second can't be the same as the first)
          return args == null || args.length == 2 || !args[0].equals(args[1]);
      }
      

      ETA:我也喜欢 Itay 在 ArgumentsAreValid 函数中使用提前返回来提高可读性的想法。

      【讨论】:

      • 感谢您的回复。我真的不明白以这种方式提取方法如何改善事情。添加评论很有用。哦,是的,'bool' 应该是布尔值,并且方法名称在 Java 中以小写开头。
      • @Adriaan - 修复了那里的代码 - 现在对 C# 太熟悉了,忘记了我的 Java。就个人而言,我认为将其拆分会使其更具可读性,但这肯定是一种主观判断。
      【解决方案10】:

      您的第一个解决方案在很多情况下都行不通,包括您上面给出的示例。如果 args 为 null,则

      boolean argsNotNull = args != null;
      // argsNotNull==false, okay
      boolean argsLengthOk = args.length == 2;
      // blam! null pointer exception
      

      短路的一个优点是它可以节省运行时间。另一个优点是它允许您进行早期测试,以检查可能导致后续测试抛出异常的条件。

      就个人而言,当测试单独简单而复杂的只是其中有很多测试时,我会投票支持简单的“添加一些换行符和 cmets”解决方案。这比创建一堆附加函数更容易阅读。

      我唯一一次将事情分解成子程序是在单个测试很复杂的时候。如果您需要外出读取数据库或执行大型计算,那么当然,将其放入子程序中,以便顶级代码易于阅读

      if (salesType=='A' && isValidCustomerForSalesTypeA(customerid))
      etc
      

      编辑: 我将如何分解您给出的更复杂的示例。

      当我真的得到如此复杂的条件时,我会尝试将它们分解为嵌套的 IF 以使它们更具可读性。就像...如果以下内容与您的示例不完全等价,请原谅,我不想仅仅为了这样的示例而过于仔细地研究括号(当然括号是难以阅读的原因) :

      if (position < min)
      {
        return (items.get(0).equals(target));
      }
      else if (position >= max)
      {
        return (items.get(items.size() - 1).equals(target));
      }
      else // position >=min && < max
      {
        if (position % 2 == 0)
        {
          return items.get(position).equals(target);
        }
        else // position % 2 == 1
        {
           return position > min && items.get(position - 1).equals(target);
        }
      }
      

      这对我来说似乎是可读的。在这个例子中,“顶级”条件显然是位置与最小值和最大值的关系,所以在我看来,打破它确实有助于澄清事情。

      实际上,上面的方法可能比将所有内容都塞在一行上更有效,因为 else 可以让您减少比较次数。

      就我个人而言,何时将复杂条件放在一行中是个好主意。但即使你理解它,很可能下一个出现的人也不会。甚至一些相当简单的事情,比如

      返回 s==null ? -1 : s.length();

      我有时会告诉自己,是的,我明白了,但其他人不会,也许写得更好

        if (s==null)
          return -1;
        else
          return s.length();
      

      【讨论】:

      • 第一个解决方案打破了短路,这就是我的观点。我不考虑在这里进行大计算或从数据库中读取数据,考虑到所有数据都存在,这只是一个复杂的条件。
      • @Adriaan:好的。我的印象是,您认为这只是效率问题,而不是“不起作用”的事情。好吧,我认为我的回答是正确的,即使你已经知道了!
      • 好的,那么您将如何呈现我在顶部添加的更复杂的示例条件?
      • 很公平。您对条件进行了一些优化(给出示例的所有有效简化),这使得它可以在嵌套的 if 中呈现它。然而,在某些时候,条件可能会变得如此复杂,以至于难以保持可读性。不管怎样,我想我现在已经把这个话题延伸得够久了。感谢您的意见!
      • 当然。这是一个很好的讨论。如果您或其他阅读本文的人希望得到一个简单、普遍正确且容易的答案,我认为没有答案。相反,就像现实世界中的许多事情一样,在每种情况下尽可能地应用它是一个一般原则的问题。我们可以推测,未来的计算机语言可能会提供更好的解决方案——就像现代语言提供的“AND”和“OR”运算符相比你必须用汇编语言编写的内容是一个巨大的飞跃。但我不知道有什么比这里讨论的更好。
      【解决方案11】:

      第一段代码确实没有错;你想多了,IMO。

      这是另一种方式,虽然冗长,但很容易理解。

      static void usage() {
          System.err.println("Usage: blah blah blah blah");
          System.exit(-1);
      }
      
      // ...
      
      if (args == null || args.length < 2)
          usage();
      if (args[0].equals(args[1]))
          usage()
      

      【讨论】:

      • 虽然它在有安全管理员在场的情况下不会做同样的事情。
      • 我的例子很简单。考虑一个非常复杂的情况。上面分成两个条件语句对我来说似乎是任意的。
      • 是的,当然是任意的!您编写代码供人们阅读,因此您可以判断什么是最清晰的。
      猜你喜欢
      • 2013-10-18
      • 1970-01-01
      • 1970-01-01
      • 2015-11-02
      • 1970-01-01
      • 2023-03-10
      • 2010-10-03
      相关资源
      最近更新 更多