控制是否引发异常或返回null的参数-好的做法?


24

我经常遇到带有附加布尔参数的方法/函数,该参数控制是否在失败时引发异常或返回null。

已经讨论了在哪种情况下哪种选择是更好的选择,因此在这里我们不要专注于此。参见例如返回魔术值,抛出异常或在失败时返回false?

相反,让我们假设我们有一个很好的理由要支持这两种方式。

我个人认为这样的方法应该分为两种:一种在失败时引发异常,另一种在失败时返回null。

那么,哪个更好?

答:一种带$exception_on_failure参数的方法。

/**
 * @param int $id
 * @param bool $exception_on_failure
 *
 * @return Item|null
 *   The item, or null if not found and $exception_on_failure is false.
 * @throws NoSuchItemException
 *   Thrown if item not found, and $exception_on_failure is true.
 */
function loadItem(int $id, bool $exception_on_failure): ?Item;

B:两种不同的方法。

/**
 * @param int $id
 *
 * @return Item|null
 *   The item, or null if not found.
 */
function loadItemOrNull(int $id): ?Item;

/**
 * @param int $id
 *
 * @return Item
 *   The item, if found (exception otherwise).
 *
 * @throws NoSuchItemException
 *   Thrown if item not found.
 */
function loadItem(int $id): Item;

编辑:C:还有别的吗?

许多人提出了其他选择,或者声称A和B都有缺陷。这些建议或意见是受欢迎的,相关的和有用的。完整的答案可以包含此类额外信息,但也可以解决以下主要问题:更改签名/行为的参数是否是一个好主意。

笔记

如果有人想知道:示例在PHP中。但是我认为这个问题适用于所有语言,只要它们与PHP或Java有点相似。


5
尽管我必须使用PHP进行工作并且相当熟练,但是它只是一种糟糕的语言,并且它的标准库无处不在。阅读eev.ee/blog/2012/04/09/php-a-fractal-of-bad-design作为参考。因此,某些PHP开发人员习惯不良习惯并不奇怪。但是我还没有看到这种特殊的设计气味。
Polygnome

在给定的答案中,我缺少一个要点:何时使用一个或另一个。如果找不到的项目是意外的并被视为错误(紧急时间),则可以使用异常方法。如果不太可能出现丢失的项目,并且肯定不是错误,而只是在正常流控制中包括的可能性,则可以使用Try ...方法。
马丁·马特


1
@Polygnome是的,标准库函数无处不在,并且存在许多不良代码,并且每个请求都有一个进程的问题。但是每个版本都变得更好。类型和对象模型完成了人们可以期望的全部或大部分工作。我希望看到泛型,协和和,协方差,指定签名的回调类型等。很多这样的讨论或正在实施中。我认为总体方向是好的,即使标准库的怪癖不会轻易消失。
donquixote

4
一个建议:代替loadItemOrNull(id),考虑是否loadItemOr(id, defaultItem)对您有意义。如果item是字符串或数字,通常会这样做。
user949300

Answers:


35

您是正确的:出于以下几个原因,两种方法好得多:

  1. 在Java中,可能引发异常的方法签名将包括此异常。其他方法不会。它特别清楚地说明了彼此的期望。

  2. 在诸如C#之类的语言中,方法的签名不告诉任何有关异常的信息,仍然应该对公共方法进行文档化,并且此类文档中应包含异常。记录单个方法并不容易。

    您的示例是完美的:第二段代码中的注释看起来更加清晰,我什至将“如果找到该项目(否则发现例外)”简称为“该项目”。您对此的描述是不言自明的。

  3. 在单个方法的情况下,很少有情况需要在运行时切换boolean参数的值,如果这样做,则意味着调用方将不得不处理两种情况(null响应异常)。 ),使代码比原来需要的困难得多。由于选择不是在运行时进行的,而是在编写代码时进行的,因此有两种方法很有意义。

  4. 诸如.NET Framework之类的某些框架已针对这种情况建立了约定,并且就像您建议的那样,它们使用两种方法来解决它。唯一的区别是他们使用Ewan在他的答案中解释的模式,因此int.Parse(string): int抛出异常,而int.TryParse(string, out int): bool没有抛出异常。该命名约定在.NET社区中非常严格,只要代码与您描述的情况相匹配,就应遵循此命名约定。


1
谢谢,这是我一直在寻找的答案!目的是我开始针对Drupal CMS进行有关此问题的讨论,drupal.org/project/ drupal/ issues/2932772,我不希望这与我的个人喜好有关,但是请他人提供反馈。
donquixote

4
实际上,至少对于.NET而言,长期以来的传统是不使用两种方法,而是为正确的任务选择正确的选择。例外是在特殊情况下,而不是处理预期的控制流。无法将字符串解析为整数?这不是非常意外或异常的情况。int.Parse()被认为是一个非常糟糕的设计决策,这正是.NET团队继续实施的原因TryParse
Voo

1
简便的方法是提供两种方法,而不用考虑我们正在处理的用例类型:不要考虑您在做什么,而是将责任放在API的所有用户身上。当然它的工作原理,但是这不是好的API设计的标志(或为此事的任何设计读如:乔尔的拿到这个
VOO

@Voo有效点。实际上,提供两种方法使调用者可以选择。也许对于某些参数值,预期存在一个项目,而对于其他参数值,不存在的项目被认为是常规情况。也许组件在一个项目中总是希望存在的应用程序中使用,而在不存在的项目被视为正常的另一应用程序中使用。也许呼叫者->itemExists($id)在致电之前先打电话->loadItem($id)。也许这只是过去其他人的选择,我们必须将其视为理所当然。
donquixote

1
原因1b:某些语言(Haskell,Swift)要求在签名中必须具有可空性。具体来说,Haskell对于可空值(data Maybe a = Just a | Nothing)具有完全不同的类型。
John Dvorak

9

一个有趣的变化是Swift语言。在Swift中,您将一个函数声明为“ throwing”,即允许其引发错误(类似于Java异常,但不完全相同)。调用者可以通过四种方式调用此函数:

一种。在try / catch语句中,捕获和处理异常。

b。如果调用方本身被声明为throwing,则只需对其进行调用,则任何引发的错误都将变为调用方引发的错误。

C。将函数调用标记为try!“我确定此调用不会抛出”。如果调用的函数确实抛出了,则应用程序肯定会崩溃。

d。标记函数调用的try?意思是“我知道函数可以抛出,但是我不关心到底抛出了哪个错误”。这会将函数的返回值从类型“ T”更改为类型“ optional<T>”,并且引发的异常将变为返回nil。

(a)和(d)是您要询问的情况。如果函数可以返回nil 并且可以引发异常怎么办?在那种情况下,optional<R>对于某些类型R,返回值的类型将为“ ”,而(d)会将其更改为“ optional<optional<R>>”,这有点神秘,很难理解,但完全可以。Swift函数可以返回optional<Void>,因此(d)可用于引发返回Void的抛出函数。


2

我喜欢这种bool TryMethodName(out returnValue)方法。

它为您提供了该方法是否成功的结论性指示,并且命名匹配将异常抛出方法包装在try catch块中。

如果仅返回null,则不知道它是否失败或返回值合法为null。

例如:

//throws exceptions when error occurs
Item LoadItem(int id);

//returns false when error occurs
bool TryLoadItem(int id, out Item item)

用法示例(out表示未初始化通过引用传递)

Item i;
if(TryLoadItem(3, i))
{
    Print(i.Name);
}
else
{
    Print("unable to load item :(");
}

抱歉,您在这里迷路了。您的“ bool TryMethodName(out returnValue)方法”在原始示例中的外观如何?
donquixote

编辑添加示例
Ewan

1
因此,您使用按引用参数而不是返回值,那么对于bool成功而言,您有没有可用的返回值?是否有支持该“ out”关键字的编程语言?
donquixote

2
是的,很抱歉没有意识到“退出”特定于c#
Ewan

1
并不总是没有。但是,如果第3项空怎么办?您不会知道是否有错误
Ewan

2

通常,布尔值参数表示可以将函数拆分为两个,并且通常应始终将其拆分而不是传入布尔值。


1
我通常不会在不添加一些限定或细微差别的情况下例外。删除布尔值作为参数可能会迫使您在其他地方编写哑/其他,因此您将数据编码为书面代码而非变量。
杰拉德(Gerard)

@杰拉德,你能给我一个例子吗?
最多

@Max当您有一个布尔变量时b:不必调用foo(…, b);,而不必编写if (b) then foo_x(…) else foo_y(…);并重复其他参数,或者类似((b) ? foo_x : foo_y)(…)该语言具有三元运算符和一流函数/方法的操作。甚至可能在程序中的多个不同位置重复此操作。
BlackJack

@BlackJack好吧,我可能会同意你的看法。我想到了这个例子,if (myGrandmaMadeCookies) eatThem() else makeFood()是的,我可能会包装另一个这样的函数checkIfShouldCook(myGrandmaMadeCookies)。我想知道您提到的细微差别是否与我们是否要传递布尔值(如原始问题)有关,我们是否希望函数如何运行(相对于我们是否传递有关模型的某些信息,如我刚刚举的奶奶例子
最多

1
我的评论中提到的细微差别是指“你应该一直”。也许将其改写为“如果a,b和c适用,则[...]”。您提到的“规则”是一个很好的范例,但是在用例上却非常不同。
杰拉德(Gerard)

1

干净代码建议避免使用布尔标志参数,因为它们的含义在调用站点是不透明的:

loadItem(id, true) // does this throw or not? We need to check the docs ...

而单独的方法可以使用表达性名称:

loadItemOrNull(id); // meaning is obvious; no need to refer to docs

1

如果我必须使用A或B,那么出于以下原因,我将使用B(两种单独的方法) Arseni Mourzenko的答案中

但是还有另一种方法1:在Java中称为Optional,但是您可以将相同的概念应用于其他语言,如果该语言尚未提供类似的类,则可以定义自己的类型。

您可以这样定义方法:

Optional<Item> loadItem(int id);

在您的方法中,Optional.of(item)如果找到了项,则返回Optional.empty();否则,返回。你永远不会投2,再也不会返回null

对于您的方法的客户而言,它类似于null,但两者之间的巨大差异迫使其考虑丢失项目的情况。用户不能简单地忽略可能没有结果的事实。为了使项目脱离Optional明确的操作,需要执行以下操作:

  • loadItem(1).get()将抛出一个NoSuchElementException或返回找到的项目。
  • loadItem(1).orElseThrow(() -> new MyException("No item 1"))如果返回Optional为空,也可以使用自定义异常。
  • loadItem(1).orElse(defaultItem)返回找到的项目或传递的项目defaultItem(也可能是null),而不会引发异常。
  • loadItem(1).map(Item::getName)会再次返回Optional带有商品名称的,如果存在的话。
  • 还有更多方法,请参阅JavadocOptional

优点是现在由客户端代码决定,如果没有项目,应该怎么办,而您仍然只需要提供一个方法


1我想这是有点像try?gnasher729s答案,但它并不完全清楚我的,因为我不知道斯威夫特,它似乎是一个语言功能,因此它是特定于斯威夫特。

2您可能会由于其他原因而引发异常,例如,由于与数据库通信遇到问题而导致您不知道是否存在某项。


0

同意使用两种方法的另一点是,这很容易实现。我将用C#编写示例,因为我不知道PHP的语法。

public Widget GetWidgetOrNull(int id) {
    // All the lines to get your item, returning null if not found
}

public Widget GetWidget(int id) {
    var widget = GetWidgetOrNull(id);

    if (widget == null) {
        throw new WidgetNotFoundException();
    }

    return widget;
}

0

我更喜欢两种方法,用这种方法,您不仅会告诉我您要返回一个值,而且要确切地返回哪个值。

public int GetValue(); // If you can't get the value, you'll throw me an exception
public int GetValueOrDefault(); // If you can't get the value, you'll return me 0, since it's the default for ints

TryDoSomething()也不错。

public bool TryParse(string value, out int parsedValue); // If you return me false, I won't bother checking parsedValue.

我更习惯于在数据库交互中看到前者,而后者则更用于解析东西。

正如其他人已经告诉您的那样,标志参数通常是代码气味(代码气味并不意味着您做错了事,只是有可能您做错了事)。尽管您准确地命名了它,但有时还是有些细微差别使您很难知道如何使用该标志,最终您只能在方法主体内部进行查找。


-1

尽管其他人则提出了其他建议,但我建议使用一种带有参数的方法来指示应该如何处理错误的方法比要求在两种使用情况下使用单独的方法要好。虽然可能需要需要为“错误抛出”和“错误返回错误指示”用法提供不同的入口点,但是拥有一个可以同时服务于两种使用模式的入口点可以避免包装方法中的代码重复。举一个简单的例子,如果有功能:

Thing getThing(string x);
Thing tryGetThing (string x);

并且想要一个功能类似但将x括在方括号中的函数,则有必要编写两组包装函数:

Thing getThingWithBrackets(string x)
{
  getThing("["+x+"]");
}
Thing tryGetThingWithBrackets (string x);
{
  return tryGetThing("["+x+"]");
}

如果存在有关如何处理错误的参数,则仅需要一个包装器:

Thing getThingWithBrackets(string x, bool returnNullIfFailure)
{
  return getThing("["+x+"]", returnNullIfFailure);
}

如果包装程序足够简单,则代码复制可能不会太糟糕,但是如果可能需要更改,则复制代码反过来又需要复制所有更改。即使选择添加不使用多余参数的入口点:

Thing getThingWithBrackets(string x)
{
   return getThingWithBrackets(x, false);
}
Thing tryGetThingWithBrackets(string x)
{
   return tryGetThingWithBrackets(x, true);
}

一个人可以将所有“业务逻辑”保存在一种方法中,该方法接受一个参数,该参数指示发生故障时的操作。


您是正确的:使用两种方法时,装饰器和包装器甚至常规实现都将具有一些代码重复。
donquixote

确实将有无异常的版本放在单独的程序包中,并使包装器通用吗?
Will Crawford

-1

我认为所有可以解释您不应该具有控制是否引发异常的标志的答案都不错。他们的建议将作为我对任何需要提出该问题的人的建议。

但是,我确实想指出,此规则有一个有意义的例外。一旦具备足够的高级才能开始开发供数百人使用的API,在某些情况下,您可能希望提供这样的标记。在编写API时,您不仅在写纯粹主义者。您正在写给有真实愿望的真实客户。您的部分工作是使他们对您的API满意。那变成了一个社会问题,而不是编程问题,有时社会问题决定了解决方案,而解决方案本身并不理想。

您可能会发现您的API有两个不同的用户,其中一个需要异常,而另一个则不需要。一个可能发生这种情况的现实示例是在开发和嵌入式系统中都使用的库。在开发环境中,用户很可能希望到处都有异常。在处理意外情况时,异常非常流行。但是,在许多嵌入式情况下都禁止使用它们,因为它们太难分析周围的实时约束。当您实际上不仅在乎执行功能所花费的平均时间,而且在为每一项个人娱乐所花费的时间时,在任何任意位置展开堆栈的想法都是非常不可取的。

对我来说,一个真实的例子是:一个数学库。如果您的数学库中有一个Vector类支持在相同方向上获取单位向量,则必须能够除以幅度。如果幅度为0,则表示被零除的情况。在开发中,您想抓住这些。您真的不想让0除以令人惊讶。为此,抛出异常是一种非常流行的解决方案。它几乎永远不会发生(这是真正的异常行为),但是当它发生时,您想知道它。

在嵌入式平台上,您不需要这些例外。进行检查会更有意义if (this->mag() == 0) return Vector(0, 0, 0);。实际上,我在实际代码中看到了这一点。

现在从企业的角度思考。您可以尝试教两种不同的API使用方式:

// Development version - exceptions        // Embeded version - return 0s
Vector right = forward.cross(up);          Vector right = forward.cross(up);
Vector localUp = right.cross(forward);     Vector localUp = right.cross(forward);
Vector forwardHat = forward.unit();        Vector forwardHat = forward.tryUnit();
Vector rightHat = right.unit();            Vector rightHat = right.tryUnit();
Vector localUpHat = localUp.unit()         Vector localUpHat = localUp.tryUnit();

这满足了此处大多数答案的观点,但是从公司的角度来看,这是不可取的。嵌入式开发人员将必须学习一种风格编码,而开发人员将必须学习另一种编码。这可能会让人讨厌,并迫使人们陷入一种思维方式。可能会更糟:如何证明嵌入式版本不包含任何引发异常的代码?投掷版本可以轻松地融入其中,隐藏在代码中的某个位置,直到昂贵的故障审查委员会找到它为止。

另一方面,如果您有一个用于打开或关闭异常处理的标志,则这两个组都可以学习完全相同的编码样式。实际上,在许多情况下,您甚至可以在另一个小组的项目中使用一个小组的代码。在使用的代码中unit(),如果您实际上关心被0除的情况会发生什么,那么您应该自己编写测试。因此,如果将其移动到嵌入式系统中,结果会很糟糕,那么这种行为是“理智的”。现在,从业务角度来看,我训练了我的编码人员一次,他们在我的业务的各个部分使用相同的API和相同的样式。

在大多数情况下,您希望遵循其他人的建议:tryGetUnit()对不引发异常的函数使用like 或相类似的习惯用法,而是返回诸如null的前哨值。如果您认为用户可能希望在单个程序中同时使用异常处理和非异常处理代码,请坚持使用该tryGetUnit()符号。但是,在极端情况下,实际业务可以使使用标记更好。如果您发现自己处于困境中,请不要害怕将“规则”扔在路边。这就是规则的目的!

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.