总长,博士
- PHPMD有一点
- 意图是好的,但 PHPMD 的
else
“从来没有必要”的建议是错误的
- 你总是可以删除
else
——但很多时候这会导致代码更混乱(这很讽刺,因为你会遵循“混乱检测器”的建议)。
- 所以你应该经常问自己:我应该避免这种情况
else
吗?
- 甚至 PHPMD 本身也使用
else
,所以看起来有时它是必要的
长答案
else
是一种非常标准的语言结构,用于许多语言和软件包(包括 PHPMD 本身)。对我来说,说那else
从来没有必要与说那从来没有必要是一样的,do...while
因为你可以结合while (true)
and break
,这是真的,但毫无意义。因此,我会对这个建议持保留态度,并在仅基于静态分析器建议更改任何软件之前进行一点思考。
综上所述,我认为 PHPMD 开发人员的意思是,在许多情况下,您可以并且应该else
从代码中删除以使其更具可读性。
最简单的情况是:
if ($condition) {
$a = 1;
} else {
$a = 2;
}
可以简化为:
$a = ($condition ? 1 : 2);
现在看看这个表达式:
// Calculate using different formulas, depending on $condition.
if ($condition) {
// Calculate using secret formula.
$a = power($r * M_PI, 2) / sqrt(exp($k));
} else {
// Calculate using standard formula.
$a = power(($r / $k) * M_PI, 2) / sqrt(1 / $k);
}
这可以更改为:
$a = ($condition ? power($r * M_PI, 2) / sqrt(exp($k)) : power(($r / $k) * M_PI, 2) / sqrt(1 / $k));
当然,第二种形式更简洁,或者,我应该说,更小。但是从代码清晰性和可维护性的角度来看,我想带有 an 的原始代码else
要清晰得多,更不用说使用代码注释来解释“改进”版本要困难得多,不是吗?
恕我直言,它是。我总是在这样的情况下使用else
。
另一个简单的例子:
// Check animal first.
if ($animal === 'dog') {
if ($breed === 'pinscher') {
$weight = 'light';
} else {
$weight = 'heavy';
}
} else {
// We only deal with dogs.
$weight = "we cannot say anything about $animal";
}
没有别的版本:
$weight = ($animal === 'dog' ? ($breed === 'pinscher' ? 'light' : 'heavy') : "we cannot say anything about $animal");
请注意,在这种情况下,没有 else 的版本直接违反了 PSR-2,它禁止嵌套三元运算符。
通常,人们会保留简单的结构,否则这些结构可能会被三元运算符替换,只是因为希望避免代码中出现长行,这不利于代码的可读性:
if (sqrt($weight) > 30 && $animal === 'elephant' && $location == 'zoo') {
$visitors_max = sqrt($guards) / ($ticker_price * M_2_SQRTPI)
} else {
$visitors_max = $visitors_max_last_year * exp($ticket_price) * 1.1;
}
那变成:
$visitors_max = (sqrt($weight) > 30 && $animal === 'elephant' && $location == 'zoo') ? sqrt($guards) / ($ticker_price * M_2_SQRTPI) : $visitors_max_last_year * exp($ticket_price) * 1.1);
继续前进,这是我想 PHPMD 希望您解决的另一个众所周知的模式:
function myfunc($arg)
{
if ($arg === 'foo') {
$res = 'foo found';
} else {
$len = strlen($arg);
if ($len > 10) {
$res = 'arg is too big';
} else {
$bar = dosomething($res);
$res = "$arg results in $bar";
}
return $res;
}
这个函数使用了一个曾经在编程类上教授过的建议,即函数应该有一个单一的出口点,因为(可以说)这样可以更容易地理解程序流程和发现错误。
恕我直言(和 PHPMD),我们可以else
在这样的代码中删除和提高代码的清晰度/可维护性,而不会丢失任何内容:
function myfunc($arg)
{
if ($arg === 'foo') {
return 'foo found';
}
$len = strlen($arg);
if ($len > 10) {
return 'arg is too big';
}
$bar = dosomething($res);
return "$arg results in $bar";
}
但这可能并不总是可取的:
function mycalc($n)
{
if ($n === 0) {
$multiplier = 0.5;
} elseif ($n === 1) {
$multiplier = M_LN2;
} else {
$multiplier = power(sqrt($n * M_PI), 2);
}
return $multiplier * (M_2_PI * power($n * M_PI, 2));
}
“改进”版本应该是这样的:
function mycalc($n)
{
if ($n === 0) {
return 0.5 * (M_2_PI * power($n * M_PI, 2));
}
if ($n === 1) {
return M_LN2 * (M_2_PI * power($n * M_PI, 2));
}
return power(sqrt($n * M_PI), 2) * (M_2_PI * power($n * M_PI, 2));
}
我不确定你,但第一个版本更接近我的计算思路,因此比第二个版本更容易理解和维护,即使它使用了“禁止” else
。
(有人可能会争辩说我们可以使用第二种形式,加上一个辅助变量来保存公共计算。这很公平,但人们总是会反驳说,添加一个不必要的变量会使代码变得不那么清晰,维护成本也更高。)
那么,要回答您的问题,我应该如何避免?,我会问另一个:你为什么要?
@tereško 的回答是有道理的,并且确实使代码更简洁。但是,我个人认为您的第一个版本非常好,其意图更明确,因此从可理解性和可维护性 POV 来看要好得多:
if (I do not have $object)
create a new $object with my settings
else
call the "fill" method of $object with my settings
endif
do stuff with $object
相对:
if (I do not have $object)
create a new $object
endif
call the "fill" method of $object with my settings
do stuff with $object
另请注意,在没有else
上述内容的版本中,编程逻辑会发生细微的变化:您(和所有未来的开发人员)必须假设使用我的设置调用 $object 的“填充”方法,并始终使用我的设置创建一个新的 $object最终得到一个具有相同内部状态的对象。原始版本不需要此假设。
换句话说,只要fill()
方法和对象的构造函数对对象的内部状态做同样的事情,重构的代码就可以工作,这可能是也可能不是——现在或永远。
为了说明这一点,假设对象定义如下:
class MyClass
{
protected $fillCount = 0;
protected $settings;
public function __construct(array $settings)
{
$this->settings = $settings;
}
public function fill(array $settings)
{
$this->fillCount++;
$this->settings = $settings;
}
}
在这种情况下,您的原始版本和没有的版本else
最终会得到具有不同内部状态的对象,并且发现错误会更加困难,因为它将隐藏在假设和隐式构造之后。
现在,让我们看一下 PHPMD 自己的else
:
// File: src/bin/phpmd
if (file_exists(__DIR__ . '/../../../../autoload.php')) {
// phpmd is part of a composer installation
require_once __DIR__ . '/../../../../autoload.php';
} else {
require_once __DIR__ . '/../../vendor/autoload.php';
// PEAR installation workaround
if (strpos('@package_version@', '@package_version') === 0) {
set_include_path(
dirname(__FILE__) . '/../main/php' .
PATH_SEPARATOR .
dirname(__FILE__) . '/../../vendor/pdepend/pdepend/src/main/php' .
PATH_SEPARATOR .
'.'
);
}
}
// (...100+ lines of code follows...)
问题是:我们应该避免这种情况else
吗?