我想重构以下代码,因为我不喜欢在比较运算符中使用赋值。它看起来像是惯用的C语言,但是您认为这在Java中是一个好习惯吗?
private void demoA(BufferedReader reader) throws IOException {
String line = null;
while ((line = reader.readLine()) != null) {
doSomething(line);
}
}
这里是一种替代方法。
private void demoB(BufferedReader reader) throws IOException {
String line = reader.readLine();
while (line != null) {
doSomething(line);
line = reader.readLine();
}
}
更新:几年前,我偶然发现了一个类似的问题。似乎对是否可以接受的意见存在分歧。但是,Guava和Commons IO都为该问题提供了替代解决方案。如果我在当前项目中有这些库中的任何一个,则可能会改用它们。
#1 楼
在这种情况下,可以在条件内进行赋值,因为赋值被一对额外的括号包围–比较显然是!= null
,我们没有机会键入line == reader.readLine()
。但是,
for
循环实际上在这里可能更优雅:for (String line = reader.readLine(); line != null; line = reader.readLine()) {
doSomething(line);
}
或者,我们可以这样做,这也像
line
-loop一样限制了for
的范围,并消除了不必要的内容重复:while (true) {
final String line = reader.readLine();
if (line == null) break;
doSomething(line);
}
我最喜欢此解决方案,因为它不会突变任何变量。
评论
\ $ \ begingroup \ $
+1 for循环。我之前从未考虑过,但这很清楚,可以避免在循环范围之外添加行。
\ $ \ endgroup \ $
–艾米莉·L。
2014年3月12日上午10:40
\ $ \ begingroup \ $
我也喜欢for循环解决方案。它是密集的,可读的,不会引起doSomething(line)的注意。
\ $ \ endgroup \ $
–mkalkov
2014年12月12日11:59
\ $ \ begingroup \ $
@FabioF。我不会每次都创建一个新的String,但是由于readLine会发生这种情况。但是,将变量的可见性限制在最小范围内是一种最佳实践。在这里,这意味着该行不应在循环外部声明。不重新分配变量和不变性的其他方面是我进行函数编程时的一个好习惯:这使代码更易于理解,因为变量名称及其值可以互换使用(“引用透明性”),没有任何突变状态必须记住。
\ $ \ endgroup \ $
–阿蒙
2014年12月12日13:16
\ $ \ begingroup \ $
@FabioF。虽然这样会适当限制可见性,但会增加不必要的缩进级别。我们不妨对(String line; ...;){...}使用等效项-所有优点,没有缺点。
\ $ \ endgroup \ $
–阿蒙
2014年3月12日13:24
\ $ \ begingroup \ $
我最喜欢最后一个解决方案。最好避免让循环出口点被具有有意义的副作用的代码分隔开,但是,如果循环出口点的自然位置在中间,则最好使用while / break结构而不是复制应该在循环出口点之前的代码。
\ $ \ endgroup \ $
–超级猫
2014年12月12日15:58
#2 楼
您可以使用类似迭代器的模式来稍微提高代码的抽象级别,同时您可以(根据作者的经验)重用现有的库:Apache Commons IO LineIterator。它将空检查替换为一点可读的hasNext()
/ nextLine()
。使用迭代器隐藏了不必要的细节:当没有更多数据时,读取器将返回
null
。 hasNext()
方法更接近(英语)语言,因此代码更易于阅读。如果需要,您仍然可以在LineIterator
内检查详细信息,但通常读者/维护者会更满意,并且对方法有更高的了解,这更易于理解。 (此答案和问题包含一个表达性的示例。)示例方法:
private void demoC(BufferedReader reader) throws IOException {
final LineIterator it = new LineIterator(reader);
try {
while (it.hasNext()) {
String line = it.nextLine();
// do something with line
}
} finally {
it.close();
}
}
另请参见:有效的Java,第二版,项目47:了解和使用这些库(作者仅提及JDK的内置库,但我认为其他库也可以使用这种推理。)
评论
\ $ \ begingroup \ $
为什么要手动使用此迭代器,而不是在foreach循环中使用它?
\ $ \ endgroup \ $
– Ph子
2014年3月13日在15:05
\ $ \ begingroup \ $
@Phoshi:不幸的是foreach不能使用Iterator,它需要和Iterable。
\ $ \ endgroup \ $
–palacsint
2014年3月13日15:13
\ $ \ begingroup \ $
@palacsint:嗯,当然。我想我的问题是,为什么LineIterator实际上不实现Iterable,但是我想那不是您可以回答的问题。
\ $ \ endgroup \ $
– Ph子
2014年3月13日15:32
\ $ \ begingroup \ $
@Phoshi:您可以将任何阅读器(带有任何流)传递给LineIterator。其中一些可能不支持重置(并且只能读取一次,例如网络套接字)。多次重读需要使用Iterable.iterator()创建多个迭代器。
\ $ \ endgroup \ $
–palacsint
2014年3月13日15:40
\ $ \ begingroup \ $
(更不用说使用多个LineIterators进行并发可能比LineIterable类更糟糕,因为每个LineIterators可能会关闭Reader,这会使该Reader上的任何其他LineIterator无效,即使它是一个支持标记/重置的也不行。 / skip和迭代器被子类化以独立地跟踪它们的位置[尽管您也可以重写LineIterator.close()以不实际调用Reader.close(),直到该Reader的所有迭代器都已关闭/被消耗-可能是并发Map
\ $ \ endgroup \ $
– JAB
2014年3月13日17:11
#3 楼
除了将阅读器包装在迭代器中之外,您还可以将其包装在Iterable
中,然后返回迭代器。它允许您编写以下内容
for (String line: linesOf(reader)) {
// ...
}
代码非常简洁。
#4 楼
在我看来,demoA()
仍然可以正常使用。分配作为测试中的副作用通常是不被接受的,但是这种用法是语言功能存在的一个很好的例子。它紧凑,不重复,惯用且高效。使用它,不要对此感到内!!
#5 楼
请意识到if (cond(var = expr))
通常可以重写为
var = expr;
if (cond(var)) ...
和
while (cond(var = expr))
始终可以重写为
for (var = expr; cond(var); var = expr)
,甚至不影响循环中
break;
或continue;
的含义。因此,几乎没有明确需要将赋值填入条件条件中。
#6 楼
您可能也可以执行以下操作:Files.lines(path)
它以流的形式从文件中获取所有行,然后可以根据逻辑对字符串进行排序并然后将其收集在列表中并写入输出文件。
此答案为您提供了一种功能编程方法。该方法的大部分是不言自明的。
Files.lines(Paths.get(path)).map(--Your business logic--);
Steam API中提供了多种功能,可简化您的处理。
评论
您主要是在寻求基于基本观点的答案,就我而言,事实证明,当我需要在两者之间进行选择时,第一种方法更为清晰,但这只是我的情况。我假设最后一个reader.readLine();应该是line = reader.readLine();?
tobias_k:好的,谢谢。我已经解决了。 morgano:嗯,这是关于编码准则的问题,大多数这样的问题都是基于观点的。不过,我想知道大多数人是否喜欢这些选择之一,或者是否没有强烈的偏好。感谢您的回复!
我认为demoA对于Java是惯用的。也就是说,在同一主题上还有一个旧的SO问题:stackoverflow.com/questions/4677411/…特别是评分最高(未接受)的答案很有趣。
我不写String line = null;但是字符串行;。