在Java中,它们并不是真正的GOTO语句,而是被称为Branching语句,但是我发现前一个术语更能说明它们的实际作用。最近我一直在设计一些代码,这使我束手无策。一方面,我们被教导避免狂热地使用这样的语句,因为它们会使代码既非线性又难以遵循。它们当然有很好的用途,我不是那些应该不惜一切代价将其丢弃的清教徒。

另一方面,还有一个共同的原则说我应该如果可能的话,避免深度嵌套条件和循环。

所以这是我的难题。我希望有人可以指导我,并给我一些扎实的推理和最佳实践建议,因为显然我的大脑无法处理冲突的信号。类似于以下内容(其中for()lines,而List<String>values):理解用例的严格要求是因为我认为Map<String, String>循环并不是真正的“紧密”,它执行大量处理这一事实与样式选择有关。对我来说,使代码散布在整个区域中会使代码感到有些草率和混乱,这会使读者回到循环的开始。换句话说,它感觉很像每个人都喜欢讨厌的好旧的for
for(String line : lines) {

    if(line.charAt(0) == '#') {
        LOGGER.debug("Skipping commented line in file.");
        continue;
    }

    line = line.trim().toLowerCase();
    String[] pair = line.split("=");

    if(pair.length != 2) {
        LOGGER.error("Skipping malformed line in file: " + line);
        continue;
    }

    pair[0] = pair[0].trim();
    pair[1] = pair[1].trim();

    if(values.containsKey(pair[0])) {
        LOGGER.debug("Value is already assigned.");
        continue;
    }

    values.put(pair[0], pair[1]);

    //...and so on with still more processing
}


这两种技术/样式都因各种原因而使我的眼睛抽搐,并且我一直在重写代码以适合当前胜出的任何合理化方法。我还认为,如果您最终选择一个作为始终使用的“样式”,那么将上述代码段扩展到与另一个代码一起使用更有意义的地方就很简单了。

有谁对何时使用一种样式比另一种有更明确的陈述?甚至只是一些适用于某种情况的合理推理,以确保您遵循最佳规则和编码实践?处理此类情况有任何一般性的技巧,窍门和建议吗?

评论

在这两种方法中,都值得考虑对if块中的代码使用“提取直至删除”(充分考虑提取方法的名称)。当然,这是一种最佳实践,它可以通过提早返回(更容易在clean + small顶层函数中发现)或什至是嵌套的if来使最终的顶层函数干净且可读。当幸运的是,SMALL函数的意图非常清晰,读起来像散文时,我很少对这两种方法中的任何一种都感到困惑。

GOTO导致了速激肽。

我误解为Nietzsche vs GOTO ...我真的很高兴发现这意味着什么。

我同意;这些都使我抽搐。我建议的解决方案是:退后一步,问问自己自己真正在做什么。您正在编写解析器,而无需先编写词法分析器。我倾向于更像传统的编译器来构造此代码。定义词汇语法。将文本分解为一系列标记。然后对令牌运行解析器以生成数据结构。

所有控制结构都是伪装的GOTO,因此您应该编写所有方法而没有任何控制结构,没有for循环,没有while循环,没有if条件和函数。它们都是GOTO,我们都知道GOTO是所有邪恶中最邪恶的。

#1 楼

另一种方法是让这些对由一个类表示,该类本身具有静态工厂方法,该方法将在失败时返回null

public final class Pair {
    private String key;
    private String value;

    public Pair(key, value) {
        // TODO: Add proper argument checking/throw exceptions.

        this.key = key;
        this.value = value;
    }

    public String getKey() {
        return key;
    }

    public String getValue() [
        return value;
    }

    /**
     * Returns the string representation which looks like @{code key=value}.
     */
    @Override
    public String toString() {
        return key + "=" + value;
    }
}


静态工厂方法:

/**
 * Constructs a Pair from the given string,
 * returns null if the string is misformed or
 * the string was null.
 * 
 * A well formed input looks like {@code key=value} and
 * does not start with a {@code #} as that indicates comments.
 */
public static Pair fromString(String str) {
    // We do not accept nulls and empty strings.
    if (str == null || str.empty()) {
        return null;
    }

    // TODO: Figure out if trimming of str would be a good idea.

    // Skip comments.
    if (str.startsWith("#")) {
        return null;
    }

    String[] splitted = str.split("=");

    // We also do not accept anything else.
    if (splitted.length != 2) {
        return null;
    }

    // TODO: Are you accepting zero-length keys/values?

    return new Pair(splitted[0], splitted[1]);
}


它基本上看起来与您的循环相同,但有两个重要区别: ,易于重用,易于测试。
它具有说明输入内容的文档。

现在进入循环: >这不仅会缩短循环内的代码,而且最终会使代码更具可读性,因为您现在使用的是pair.getValue()而不是pair[1]。名称,例如StringPairSettingSettingsPairConfigEntry

这是您的带有日志语句的循环(假设LOGGER具有与Logger.log(...)类似的重载): />
for (String line : lines) {
    Pair pair = Pair.fromString(line);
    if (pair != null && !values.containsKey(pair.getKey())) {
        // TODO
    }
}


没有细颗粒d错误消息,因为从输入中可以明显看出为什么fromString函数无法创建Pair。尽管我们回到了两个带有if分支的else -statemens,但可读性要好得多。


这确实违反了有效Java(第2版)中的第43项,它是这样的: br />

但是为什么我不建议这种方法呢?有效的Java指出,最好抛出细粒度的异常,而不是返回null,以简化该方法的处理。这对于任何一种方法都是正确的,但是这不是该方法应该做的。我们不在乎是什么问题,我们只想要一个可用于我们程序逻辑或继续前进的对象,就可以丢弃过程中的细粒度错误。

如果您使用这种方法,需要记住一些事情:


不要为此目的而使用构造函数,而要使用静态工厂方法。
彻底记录静态工厂方法的行为和目的。
在看到输入时函数应该返回是显而易见的。 />知道何时使用此方法以及何时使用异常处理(请注意为什么这不是对所有事物通用的通用解决方案)。
如果这在库/公共API中,请始终提供一种方法确实会抛出细粒度的异常。 >

如果java.util.Properties适用于此版本,请勿滚动自己的版本。


评论


\ $ \ begingroup \ $
我将其称为StringPair或Pair
\ $ \ endgroup \ $
– 200_success
2014年1月27日17:19

\ $ \ begingroup \ $
如果重新插入LOGGER语句,则fromString方法看起来不太干净。这里失去了功能。
\ $ \ endgroup \ $
–安德鲁·拉撒路(Andrew Lazarus)
2014年1月27日在22:19

\ $ \ begingroup \ $
@AndrewLazarus:我不认为Logger语句具有“功能性”,我也看不到有必要重新插入所有这些语句。我认为很明显,为什么函数通过查看输入返回了返回的内容。如果从输入来看不是很明显,则该函数有问题。您可以通过拆分if条件并利用else部分来实现这些目标。这样,您就可以进行日志记录,而无需更细粒度的消息(可以是:“是的,这是错误的格式”或“已经存在”)。
\ $ \ endgroup \ $
– Bobby
2014年1月27日22:40

\ $ \ begingroup \ $
请不要使用null来表示无效行。抛出IllegalArgumentException或使用Guava / SE8的Optional 。 OP没有指定文件的外观,但是null并不是真正的问题。
\ $ \ endgroup \ $
–TC1
2014年1月28日12:13



\ $ \ begingroup \ $
但是,此代码存在一些问题:1.正如其他人所提到的,null是表示验证错误的一种不好的方法。 2. Pair.fromString同时进行解析和验证。通过返回null,您将丢失有关验证错误的信息(即“格式错误的行”消息)。因此,无论您是否认为此信息不重要,代码在功能上都是不同的。 3.您实际上没有回答OP的问题。好吧,隐式地做了,因为您选择了嵌套if,但是警卫声明更易于理解(即简单地编写if(pair == null)Continue;)。
\ $ \ endgroup \ $
– Groo
2014年1月29日在8:28



#2 楼

您的第一段摘录很好,比第二段要好。您的第二节摘录由于嵌套更深而变得更糟。 (此外,如果您仍然选择嵌套,则我会反转条件以首先放置较短的代码分支以减少阅读代码时的精神工作量。)

考虑为什么goto语句被认为是有害的。由于它有可能跳到代码中的任意位置,因此可以将您的程序变成一堆意大利面。

使用continue没问题。您仍然有一个结构化的循环,它唯一可以做的就是进入循环的顶部以处理下一个元素(如果有的话)。

如果您不关心日志记录,则可以使用捕获正则表达式来处理前两个条件并修剪空白。您只需要担心密钥是否已经存在。

评论


\ $ \ begingroup \ $
我不确定您为什么强烈反对继续。我很想知道您针对“继续陈述被认为是有害的”论文提出了哪些论据。
\ $ \ endgroup \ $
– 200_success
2014年1月27日17:23

\ $ \ begingroup \ $
@JeffGohlke-我所见过的有关continue / break的所有内容都只是说应该避免使用带标签的版本。避免循环内未标记的形式就像避免return语句。
\ $ \ endgroup \ $
–鲍勃森
2014年1月27日19:23

\ $ \ begingroup \ $
@鲍勃森我完全同意这个比喻。可以对问题进行重组,以询问是否有很多return语句(“早点返回,经常返回”)或嵌套的条件很多而返回的次数较少。相同的想法。
\ $ \ endgroup \ $
–asteri
2014年1月27日19:31

\ $ \ begingroup \ $
@IraBaxter-如果您的代码非常复杂,以至于您无法告诉自己处于循环中,那么您确实需要对其进行重构。而且由于未标记的版本仅适用于当前循环,因此告诉它们去向应该是微不足道的。
\ $ \ endgroup \ $
–鲍勃森
2014年1月28日在12:36

\ $ \ begingroup \ $
@IraBaxter-获得了指向该源的链接?我在Google上找不到任何内容,但不确定使用什么术语。
\ $ \ endgroup \ $
–鲍勃森
2014年1月28日15:05

#3 楼

我感到奇怪的是,尚未指出零长度线的问题:

for(String line : lines) {

     if(line.charAt(0) == '#') {
         ....


上面的代码在所有情况下均假定为非空行,否则获取IndexOutOfBoundsException。

这是应该识别并由单元测试解决的问题。

这还导致您似乎在重塑- -wheel,并且应该考虑使用java.util.Properties的建议。尽管有这样的建议,我也必须考虑continue / break的理论讨论。循环入口和出口点已经是Java生成的已编译(JIT或Byte)代码中分支指令的目标。这些分支点管理得当,并具有干净且有据可查的进入/退出条件。它们与GOTO的概念完全不同。随着时间的流逝,我对使用它们感到非常自在,并且它们不比从方法调用中早日返回时更加不信任。实际上,从概念上讲,这就是它们的本质,只是对恰好处于循环中的代码块的早期终止,终止可能允许(continue)或不允许(break)进一步迭代。

我不建议在不加惩罚的情况下使用这些语句,但是breakswitch语句中的自然语法,这一事实暗示了可以使用。它是某些工作的正确工具。适当时使用它。

#4 楼

您的问题不在于循环结构-GOTO也不对。您的问题是您正在避免使用一些可以使您的代码更好的工具。

这里是一个示例。

enum Validator {
    IsNotComment("Skipping commented line in file.") {

        @Override
        boolean isValid(String s) {
            return !s.startsWith("#");
        }

    },
    IsAPair("Not a pair") {

        @Override
        boolean isValid(String s) {
            return s.trim().split("=").length == 2;
        }

    };

    abstract boolean isValid(String s);

    public final String failMsg;

    Validator(String failMsg) {
        this.failMsg = failMsg;
    }
}

public void test() {
    String[] lines = {"Hello"};
    Map<String,String> values = new HashMap<>();
    for (String line : lines) {
        boolean valid = true;
        String failMsg = "";
        for ( Validator v : Validator.values() ) {
            valid &= v.isValid(line);
            if ( !valid ) {
                failMsg = v.failMsg;
            }
        }
        if ( valid ) {
            // Do stuff with valid lines.
            String [] parts = line.trim().split("=");
            values.add(parts[0],parts[1]);
        } else {
            System.out.println("Failed validation: "+failMsg);
        }
    }
}


通过正确地重构,可以看到现在可以添加更多的验证步骤,而又不影响主要代码,每个验证步骤只会增加质量。现在,您已经将难以理解和增强的脆性代码转换为清晰灵活的代码。

这鼓励您将注意力集中在试图将验证与解析混合在一起的事实上。

如果让您的后顾之忧正确地指导您,您会看到验证是一个单独的过程,而将验证与解析过程混合的尝试会使您的代码闻起来不容易。 >先验证-然后解析。除非有充分的合理理由,否则不要进行预优化并将其合并为一个-甚至再进行一次测量。

已添加

只是为了获得乐趣-这是Java 8中的样子

List<String> lines = Arrays.asList(
        "# Comment",
        "",
        "A=B",
        "A=C",
        "X=Y ",
        "P = Q");

public void test() {
    Map<String,String> values = new HashMap<>();
    lines.stream()
            // Trim it and lowercase.
            .map(s -> s.trim().toLowerCase())
            // Discard empty lines - good call @rolfl
            .filter(s -> !s.isEmpty())
            // Discard comments.
            .filter(s -> s.charAt(0) != '#')
            // Split the trimmed form
            .map(s -> s.split("="))
            // Must be two parts.
            .filter(a -> a.length == 2)
            // Trim each one.
            .map(a -> new String[]{a[0].trim(), a[1].trim()})
            // Not seen already.
            .filter(a -> !values.containsKey(a[0]))
            // Install in values.
            .forEach (a -> values.put(a[0], a[1]));
    System.out.println(values);
}


这实际上打印出来:

{p=q, a=b, x=y}


现在很酷还是什么?

对不起,但它并不能完成您所有有用的打印工作。

评论


\ $ \ begingroup \ $
@OldCurmudgeon这种方法的问题是您将验证和处理分开。这将导致代码重复。例如//用有效的行来填充。还将包含一个String.split(“ =”)。
\ $ \ endgroup \ $
– abuzittin gillifirca
2014年1月28日在8:40



\ $ \ begingroup \ $
@abuzittingillifirca-微不足道的损失与代码质量的显着提高取得了平衡。它还建议您应该使用新的类来处理String,以便保留零件。
\ $ \ endgroup \ $
–OldCurmudgeon
2014年1月28日在9:00



\ $ \ begingroup \ $
Java8流上的良好调用....看起来很有趣。同时也感谢您的“好电话”。您可能会发现,此.filter(s-> s.length()> 0)最好写为.filter(s->!s.isEmpty())...不错的答案! (您编辑的好东西。我现在有票!)
\ $ \ endgroup \ $
–rolfl
2014年1月31日,0:53



\ $ \ begingroup \ $
也许您的意思是failMsg + = v.failMsg,因为编写的代码会在出现多个错误的情况下覆盖现有的failMsg。如果仅应打印第一条消息,为什么不立即退出验证循环?我喜欢枚举设置-解决效率问题@abuzittingillifirca的一种方法是,您可以将验证器(接口的两个实现)拆分为预解析和后解析。在此示例中,效率低下可能是微不足道的,但如果简单的拆分成本很高,则效率不是很低。
\ $ \ endgroup \ $
–安德鲁·拉撒路(Andrew Lazarus)
2014年2月11日下午5:27

\ $ \ begingroup \ $
当然,我不会直接写到系统记录器,但是将所有错误收集到一个列表中并在以后进行记录将提供原始问题的所有功能!
\ $ \ endgroup \ $
– Falco
2014年7月25日在7:34

#5 楼

这是一个遥不可及的高级解决方案:使用java.util.Properties处理您的配置文件。 Properties支持一种key=value语法,该语法与您尝试解析的语法非常相似。由您决定该语法是否足够接近以处理您可能拥有的任何现有配置文件,以及是否需要验证和登录当前代码。

评论


\ $ \ begingroup \ $
即使问题更多地与代码样式有关,在这种情况下,问题仍可能是“ X或Y?”。答案是“ Z!”。提醒您一定要跳出框框思考。 +1为该答案的编辑版本。
\ $ \ endgroup \ $
–西蒙·福斯伯格
2014年1月27日21:00

#6 楼

我同意@ 200_sucess的观点,认为第一个版本更好。

替代方法可能是子例程:

for(String line : lines) {
    AddLine(line, values);
    //...and so on with still more processing
}

void AddLine(String line, Map<String, String> values)
{
    if(line.charAt(0) == '#') {
        LOGGER.debug("Skipping commented line in file.");
        return;
    }

    line = line.trim().toLowerCase();
    String[] pair = line.split("=");

    if(pair.length != 2) {
        LOGGER.error("Skipping malformed line in file: " + line);
        return;
    }

    pair[0] = pair[0].trim();
    pair[1] = pair[1].trim();

    if(values.containsKey(pair[0])) {
        LOGGER.debug("Value is already assigned.");
        return;
    }

    values.put(pair[0], pair[1]);
}


您介意“早期子程序中返回”?您是否会返回子程序的末尾以返回,或者构建一个嵌套得很深的if?

#7 楼

这是左场的答案,但可能会给您一些启发。如果我在Haskell中进行此操作,我会将其编写为地图和折叠列表的管道。我对Java不太了解,但是您能否将函数重写为几个较小的函数,每个函数作用于迭代器,并产生一个新的迭代器以馈入下一个函数?

processLines = insertInto dict
              . map trimPairs
              . filter notMalformed
              . map splitLine
              . filter notComment


评论


\ $ \ begingroup \ $
我也打算提出一种功能方法。问题基本上是通过一些过滤器传递一些输入,这些过滤器在功能上得到了完美的建模。
\ $ \ endgroup \ $
– d11wtq
2014年1月28日在9:47

\ $ \ begingroup \ $
这也是一个很好的主意。谢谢。我可能最终会这样。
\ $ \ endgroup \ $
–asteri
2014年1月28日15:34

\ $ \ begingroup \ $
Java 8可以在这里提供帮助:)
\ $ \ endgroup \ $
–西尔维·布鲁塞(Silviu Burcea)
2014年1月29日在8:55

\ $ \ begingroup \ $
@SilviuBurcea你能解释一下吗?我的生产环境今年才获得Java 7,因此我仍在忙于掌握lambda和try-with-resources块之类的东西。哈哈
\ $ \ endgroup \ $
–asteri
2014年1月29日21:39

\ $ \ begingroup \ $
@JeffGohlke Java 8引入了Streams和其他一些实用程序,例如返回Stream的Files.lines(path)。映射和过滤器应用于流。然后,您可以将每行映射到某物(甚至是bean),然后可以动态过滤它们。
\ $ \ endgroup \ $
–西尔维·布鲁塞(Silviu Burcea)
2014年1月30日7:57

#8 楼

让我回声@AJMasfield,对于本特定示例,请不要重新发明轮子。但是,此问题是在“属性”之外的其他上下文中提出的。在第二个版本中,LOGGER消息与相关的if子句越来越远。第一个版本使用了一个我们应该熟悉的习惯用法,检查先决条件和错误退出在顶部。我觉得它很可读。

#9 楼

解决问题的方法(在示例中给出):


选择for块的内容
重构->提取方法
检查程序以确保没有任何问题损坏
检查新生成的方法是否丑陋。
如果丑陋,请选择丑陋的部分,请转到#2
。如果由于它们分散在体内而无法选择它们,重新排列直到可以
如果由于代码中断而无法重新排列,请重新考虑算法设计-真的一定要费解吗?
如果可以,请接受并继续。并非所有代码都是漂亮的,有时完美主义并不值得。


问题的答案(除了示例):

goto很少现代语言所需要的。部分原因是存在诸如returnbreakcontinuethrow之类的东西。这些基本上是goto的非常有限的版本,无法引起任何程度的混乱,但仍然可以满足流行的goto用例。您不太可能会遇到goto有价值的代码,而这些代码无法使用这些工具和进行一些智能重构来加以驯服。

#10 楼

我同意其他人的观点,第一个更好,因为它更易于阅读,但是@AJMansfield也有一个很好的观点。值得一提的是,您还可以使用Guava的SplitterMapSplitter类或编写类似的东西。

#11 楼

基于“请求宽恕而不是允许”的概念,我将把行处理放在try-catch块中,就像这样:如果您可能没有考虑过,例如行为空或为空,或者行处理中有其他任何事情。例外。如果不满足条件,则可以将需要执行的任何类型的测试提取为引发异常的方法,例如

for(String line : lines) {

    try {
        if(line.charAt(0) == '#') {
            throw new Exception("Skipping commented line in file.");
        }

        line = line.trim().toLowerCase();
        String[] pair = line.split("=");

        pair[0] = pair[0].trim();
        pair[1] = pair[1].trim();

        if(values.containsKey(pair[0])) {
            throw new Exception("Value is already assigned.");
        }

        values.put(pair[0], pair[1]);

        //...and so on with still more processing
    } catch (Exception e) {
        LOGGER.error("Skipping line in file: " + line + " " + e.getMessage());
    }
}


总而言之,我认为异常之所以能在这种情况下工作,是因为您期望特定的输入,并且与之不匹配的任何输入(任何异常)都将被丢弃。

评论


\ $ \ begingroup \ $
评论太常规了,无法证明使用异常是合理的。可以通过这种方式处理已分配变量的情况。尽管如此,捕获所有异常仍然让我感到不安。为了追踪预期的流程,我必须检查try块中的每个函数调用,并就可能引发哪些类型的异常集思广益。它使我想起了这段代码。
\ $ \ endgroup \ $
– 200_success
2014年1月31日,0:13