如果反模式


9

我在此博客上阅读了有关“如果-如果反模式”的信息,但我不确定我是否理解为什么它是反模式。

foreach (string filename in Directory.GetFiles("."))
{
    if (filename.Equals("desktop.ini", StringComparison.OrdinalIgnoreCase))
    {
        return new StreamReader(filename);
    }
}

问题1:

是因为return new StreamReader(filename);里面for loop吗?还是for在这种情况下不需要循环的事实?

正如博客作者所指出的那样,这种方式的疯狂程度较小:

if (File.Exists("desktop.ini"))
{
    return new StreamReader("desktop.ini");
} 

两者都处于竞争状态,因为如果在创建之前删除文件StreamReader,您将得到一个File­Not­Found­Exception

问题2:

要修复第二个示例,您是否要在不使用if语句的情况下重新编写它,而是StreamReader使用try-catch块将其包围,并且如果引发了File­Not­Found­Exception您在该catch块中进行相应的处理?



1
@amon-谢谢,但是我没有提出任何其他要点,我只是想了解本文在说什么以及如何纠正。

3
我不会考虑太多。博客发布者正在组成一些没人写的愚蠢代码,给它起一个名字,并称其为反模式。这是另一个:“我称之为无意义的分配模式:x = x;我一直都在这样做。太愚蠢了。我想我会写一篇关于它的博客。”
马丁·马特

4
To fix the second example, would you re-write it without the if statement, and instead surround the StreamReader with a try-catch block, and if it throws a File­Not­Found­Exception you handle it in the catch block accordingly?-是的,这正是我要做的。解决竞争条件比“作为控制流的例外”的某些概念更为重要,并且它可以优雅,干净地解决。
罗伯特·哈维

1
如果两个文件都不符合给定标准,该怎么办?它会返回null吗?通常,您可以使用LINQ来清理类似于示例代码的代码:return Directory.GetFiles(".").FirstOrDefault(fileName => fileName.Equals("desktop.ini", StringComparison.OrdinalIgnoreCase))?.Select(fileName => new StreamReader(filename)); 注意?.两个LINQ调用之间的运算符。人们可能还会争辩说,这样的对象创建不是LINQ的最合适的用法,但我认为在这里还可以。这不是您的问题的答案,但确实离题了。
Panzercrisis

Answers:


7

这是一种反模式,因为它的形式为:

loop over a set of values
   if current value meets a condition
       do something with value
   end
end

并可以替换为

do something with value

一个典型的例子是这样的代码:

for (var i=0; i < 5; i++)
{
    switch (i)
        case 1:
            doSomethingWith(1);
            break;
        case 2:
            doSomethingWith(2);
            break;
        case 3:
            doSomethingWith(4);
            break;
        case 4:
            doSomethingWith(4);
            break;
    }
}

当以下工作正常时:

doSomethingWith(1);
doSomethingWith(2);
doSomethingWith(3);
doSomethingWith(4);

如果您发现自己正在执行一个循环,然后执行一个ifswitch,则停下来,想一想您在做什么。您是否在使事情变得过于复杂,是否可以将整个循环和测试替换为简单的“只需执行”行。但是有时,您会发现需要执行该循环(例如,可能有多个项与一项条件匹配),在这种情况下,模式就可以了。

这就是为什么它是反模式的原因:它采用“循环并测试”模式并滥用了它。

关于第二个问题:是的。在代码不是整个设备上唯一可以更改被测项目状态的唯一线程的情况下,“尝试做”模式比“先测试然后做”模式更可靠。

此代码的问题:

if (File.Exists("desktop.ini"))
{
    return new StreamReader("desktop.ini");
}

是在File.ExistsStreamReader尝试打开该文件之间的时间中,另一个线程或进程可能会删除该文件。因此,您将获得一个例外。因此,需要通过以下方式防止该异常:

try
{
    return new StreamReader("desktop.ini");
}
catch (File­Not­Found­Exception)
{
    return null; // or whatever
}

@Flater提出了一个好观点?这本身就是反模式吗?我们是否使用异常作为控制流?

如果代码读取如下内容:

try
{
    if (!File.Exists("desktop.ini")
    {
        throw new IniFileMissingException();
        return new StreamReader("desktop.ini");
    }
}
catch (IniFileMissingException)
{
    return null;
}

那么我的确会使用异常作为美化的goto,这的确是一种反模式。但是在这种情况下,我们只是在解决创建新流的不良行为,因此,这不是该反模式的示例。但这是解决该反模式的一个例子。

当然,我们真正想要的是一种更优雅的创建流的方式。就像是:

return TryCreateStream("desktop.ini", out var stream) ? stream : null;

try catch如果您发现自己经常使用此代码,我建议将其包装在这样的实用程序方法中。


1
@Flater,我倾向于同意这并不理想(尽管我对此表示质疑,这是反模式的一个例子)。代码应该显示其意图,而不是机制。因此,我更喜欢Scala之类的东西Try,例如return Try(() => new StreamReader("desktop.ini")).OrElse(null);,但是C#不支持该构造(不使用第3方库),因此我们必须使用笨拙的版本。
David Arno

1
@Flater,我完全同意例外应该是例外。但是他们很少。例如,不存在的文件几乎没有例外,但是File.Open如果找不到该文件,则会抛出一个文件。由于我们已经抛出了异常异常,因此有必要将其作为控制流的一部分进行捕获(除非有人想让应用程序崩溃)。
David Arno

1
@Flater:第二个代码示例是打开文件的正确方法。其他任何情况都会导致比赛条件。即使您检查文件是否存在,在检查与实际打开之间,也可能会导致该文件不可用,并且Open()调用会爆炸。因此,无论如何,您必须为异常做好准备。因此,您最好不要打扰检查,而是尝试将其打开。例外是可以帮助我们完成工作的工具,不应将其使用视为宗教教条。
whatsisname

1
@Flater:避免在文件操作中进行尝试/追赶的任何方法都会引入竞争条件。您要写入的文件系统在调用File.Create之前即变得不可用,从而使所有检查无效。
whatsisname

1
@Flater“ 您实际上在争论每个方法调用都需要一个返回类型...这实际上是在尝试以另一种方式重新发明异常 ”。正确。虽然那不是我的发明。它已经在功能语言中使用了多年。他们通常通过使用联合类型来避免整个“作为控制流问题的例外”(您一口气地说这是一种反模式,然后在下一阶段主张反对),例如,在这种情况下,我们将使用一种Maybe<Stream>类型nothing如果文件不存在,则返回;如果存在,则返回流。
David Arno

1

问题1 :(此for循环是否是反模式)

是的,因为您不需要自己搜索存储在可查询子系统中的项目。

概括而言,文件系统就像数据库一样,能够响应查询。在与可查询子系统交互时,我们有一个基本的选择,就是我们是否希望这样的子系统向我们枚举其内容,以便我们在子系统外部执行匹配,或者使用子系统的本机查询功能。

让我们假设一下,您正在数据库中查找记录,而不是在文件系统目录中查找文件。你想看看吗

SELECT * FROM SomeTable;

然后在返回的游标上循环查找(例如,在C#中)以查找ID = 100,或者让可查询子系统尽其所能来确切查找您要查找的内容?

SELECT * FROM SomeTable WHERE ID = 100;

我应该认为我们大多数人都会正确选择让子系统执行感兴趣的确切查询。替代方案包括与子系统的潜在多次往返,效率低下的相等性测试以及放弃使用任何索引或其他搜索加速器,而数据库和文件系统都为我们提供了索引或其他搜索加速器。


问题2:要解决第二个示例,您是否要在没有if语句的情况下重新编写它,而是用try-catch块将StreamReader包围起来,如果它抛出FileNotFoundException,您可以在catch块中进行相应的处理?

是的,因为这正是特定API的工作方式-因为这是一个库函数,所以这并不是我们的真正选择。调用之前的if检查没有提供任何附加值:我们仍然必须使用try / catch,因为(1)可能发生FileNotFound以外的其他错误,以及(2)竞争条件。


0

问题1:

是因为返回了新的StreamReader(filename); 在for循环内?还是在这种情况下不需要for循环的事实?

流读取器与此无关。反模式出现,因为之间的意向明确的冲突foreachif

目的是foreach什么?

我认为您的答案将类似于:“我想重复执行一段特定的代码”

您希望处理多少个文件?

由于特定文件夹中只能有一个特定的文件名(包括扩展名),因此可以证明您的代码旨在查找一个适用的文件。

您立即返回一个值的事实也证实了这一点。即使存在第二场比赛,您实际上也不在乎。


在某些情况下,这不是反模式。

  • 如果您还查看子目录(Directory.GetFiles(".", SearchOption.AllDirectories)),则可以找到多个具有相同文件名(包括扩展名)的文件
  • 如果您查找部分文件名匹配项(例如,每个名称以开头的文件"Test_",或每个"*.zip"文件。

请注意,这两种情况都将要求您实际处理多个匹配项,因此不会立即返回值。


问题2:

要修复第二个示例,您是否要在不使用if语句的情况下重新编写它,而是用try-catch块将StreamReader包围,并且如果它抛出FileNotFoundException,则可以在catch块中进行相应处理?

例外很昂贵。切勿使用它们代替适当的流程逻辑。顾名思义,例外是特殊情况。

因此,您不应该删除if

按照在SoftwareEngineering.SE上的此答案

通常,将异常用于控制流是一种反模式,具有特定情况和特定于语言的咳嗽异常咳嗽。

快速概括一下为什么通常是反模式:

  • 本质上,异常是复杂的GOTO语句
  • 因此,使用异常进行编程会导致更难以阅读和理解代码
  • 大多数语言都具有旨在解决您的问题而无需使用异常的现有控制结构
  • 对于现代编译器而言,效率的争论往往毫无意义,它们会在不将异常用于控制流的假设下进行优化。

阅读Ward Wiki上的讨论,以获取更多深入信息。

是否需要将其包装在try / catch中,高度取决于您的情况:

  • 您将遇到比赛状况的可能性有多大?
  • 您实际上是否能够处理这种情况,还是因为您不知道如何处理该问题而使该问题浮出水面?

“永远使用它”根本不是问题。为了证明我的观点:

单独的研究证明,当您戴上安全帽,护目镜和防弹背心时,受伤的可能性较小。

那么,为什么我们不始终都戴着这种安全设备呢?

简单的答案是因为佩戴它们有缺点:

  • 它要花钱
  • 它使您的运动更加繁琐
  • 穿起来会很温暖。

现在我们到了某个地方:优点和缺点。换句话说,只有在专业人士胜过弊端的情况下才佩戴此设备才有意义。

  • 建筑工人在工作中遭受伤害的可能性更大。他们受益于安全帽。
  • 另一方面,上班族受伤的机会要低得多。安全帽不值得。
  • 与上班族相比,特警队成员更容易遭到枪击。

您应该将呼叫包装在try / catch中吗?这在很大程度上取决于这样做的好处是否大于实现它的成本。

请注意,其他人可能会争辩说只需要几次击键就可以包装它,因此显然应该这样做。但这不是全部论点:

  • 捕获异常后,您需要决定如何处理。
  • 如果在整个代码库中对不同文件有很多不同的调用,则决定在try / catch中包装一个通常会意味着必须包装所有这些情况。这可能对实现它所需的工作量产生巨大影响。
  • 这是完全可能的,你故意处理异常。
    • 请注意,您的应用程序必须在某个时候处理异常,但这不一定是在引发异常之后立即进行。

所以选择是您的。这样做有好处吗?您是否认为它改进了应用程序,而不是花更多的精力来实现它?

更新 -从我写的评论到其他答案,因为我认为这也是与您相关的考虑因素:

这很大程度上取决于周围的环境。

  • 如果在打开流阅读器之前先打开流阅读器if(!File.Exists) File.Create(),那么在打开流阅读器时缺少文件的确是例外
  • 如果文件名是从现有文件列表中选择的,则文件名突然丢失也很特殊
  • 如果您正在使用尚未针对目录进行实际测试的字符串,请执行以下操作:那么缺少该文件是完全合乎逻辑的结果,因此也不例外
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.