代码之家  ›  专栏  ›  技术社区  ›  Adriaan Koster

如何划分复杂条件,保持短路评估?

  •  9
  • Adriaan Koster  · 技术社区  · 16 年前

    有时情况会变得非常复杂,所以为了可读性,我通常将它们分开,给每个组件一个有意义的名称。但是,这会使短路评估失败,从而造成问题。我想出了一个包装方法,但在我看来这太冗长了。

    有人能想出一个很好的解决办法吗?

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

    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");
            }
        }
    }
    

    回答:

    谢谢你的想法!目前提交了以下备选方案:

    • 换行并添加注释
    • 照原样离开
    • 提取方法
    • 早期收益
    • 嵌套/拆分if's

    换行并添加注释:

    在Eclipse(ctrl+shift+f)中,只需在条件中添加换行符就可以被代码格式化程序撤消。内联注释有助于实现这一点,但每行上的空间很小,可能导致包装不美观。但在简单的情况下,这可能就足够了。

    离开原样:

    我给出的示例条件非常简单,因此在本例中可能不需要解决可读性问题。我想的是情况更复杂的情况,例如:

    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解决方案。

    我试图用BooleenExpression方法实现的是逻辑仍然是局部的。注意,即使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;
    }
    

    我意识到大多数人讨厌标签和分隔符,而且这种样式可能太不常见,以至于不能被认为是可读的。

    嵌套/拆分条件:

    它允许引入有意义的名称,并结合优化的评估。一个缺点是复杂的条件语句树会导致

    摊牌

    因此,为了了解我特别喜欢哪种方法,我将一些建议的解决方案应用到上面给出的复杂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,带注释 不那么冗长,但仍然很难阅读,而且容易创建错误

    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)));
    }
    

    重新访问BooleenExpression包装纸 注意,方法参数必须声明为final 对于这个复杂的案件,我认为冗长是可以辩护的。 如果他们同意的话,关闭可能会使这变得更容易。(-

    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);
        }
    }
    

    逻辑足够复杂,可以保证一个单独的类(和单元测试,yay!)。它将使使用此逻辑的代码更可读,并在其他地方需要此逻辑时促进重用。我认为这是一个很好的班级,因为它真正有一个单一的责任,只有最后的领域。

    11 回复  |  直到 9 年前
        1
  •  7
  •   T.J. Crowder    16 年前

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

    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");
        }
    }
    

    如果你真的想把事情说出来,多个 if S会做到这一点:

    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");
                }
            }
        }
    }
    

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

        2
  •  11
  •   Itay Maman    16 年前

    您可以使用早期返回(从方法)来实现相同的效果:

    [应用了一些修复程序]

      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");
      }
    
        3
  •  6
  •   Eli Acherkan    16 年前

    如果你的目标是可读性,为什么不简单地打断这些行并添加注释呢?

        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
  •  2
  •   Andy Gherna    16 年前

    booleanExpression类型似乎没有足够的用处,不能独立地成为主类之外的类,它也为您的应用程序增加了一些智力负担。我只需编写适当命名的私有方法来运行所需的检查。简单多了。

        5
  •  2
  •   Thorbjørn Ravn Andersen    16 年前

    必须执行变量赋值 内部 IF的。

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

    翻译为

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

    不管怎样,这样做通常是有益的,因为它命名了您要测试的概念,正如您在示例代码中清楚地演示的那样。这提高了可读性。

        6
  •  1
  •   JuanZe Nirbhay Tandon    16 年前

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

    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;
    
    }
    
        7
  •  1
  •   aalku    12 年前

    这个问题是从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
  •  0
  •   Cornel Masson    16 年前

    已经有了一些好的解决方案,但这是我的变体。在所有(相关的)方法中,我通常将空检查作为最顶层的保护子句。在这种情况下,如果我这样做,它只会在随后的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
  •  0
  •   Eric Petroelje    16 年前

    将它分割成一个单独的函数(以提高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函数中使用早期返回来提高可读性的想法。

        10
  •  0
  •   Jay    16 年前

    您的第一个解决方案在许多情况下都不起作用,包括上面给出的示例。如果args为空,则

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

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

    就我个人而言,当测试单独地很简单,而让测试变得复杂的是测试中有许多测试,我会投票赞成简单的“添加一些换行符和注释”解决方案。这比创建一系列附加函数更容易阅读。

    我唯一一次将事物分解成子例程是在单个测试很复杂的时候。如果你需要出去读一个数据库或者执行一个大的计算,那么当然,把它滚到一个子例程中,这样顶级代码就很容易读取了。

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

    编辑: 我将如何打破你给出的更复杂的例子。

    当我真正得到如此复杂的条件时,我尝试将它们分解成嵌套的ifs,以使它们更具可读性。就像…不好意思,如果下面的例子与你的例子不完全相同,我不想为了这样一个例子而过于仔细地研究括号(当然括号是很难阅读的原因):

    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);
      }
    }
    

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

    事实上,上面的方法可能比把它们全部塞进一行更有效,因为其他方法可以减少比较的次数。

    就我个人而言,当把一个复杂的条件放在一条线上是一个好主意时,我会很挣扎。但是,即使你理解了这一点,也很可能下一个人不会来。即使是一些相当直截了当的事情,比如

    返回s==空?-1:s.长度();

    我有时会告诉自己,是的,我明白了,但别人不会,也许最好还是写下来。

      if (s==null)
        return -1;
      else
        return s.length();
    
        11
  •  -1
  •   Jonathan Feinberg    16 年前

    第一段代码真的没什么问题,我觉得你在想太多了。

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

    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()