在调用方中验证输入参数:代码重复?


16

验证函数输入参数的最佳位置在哪里:在调用方中还是在函数本身中?

因为我想改善自己的编码风格,所以我尝试找到此问题的最佳实践或一些规则。何时何地更好。

在我以前的项目中,我们曾经检查并处理函数中的每个输入参数(例如,如果它不为null)。现在,我在这里已经在一些答案以及《实用程序员》一书中读到,输入参数的验证是调用者的责任。

因此,这意味着我应该在调用函数之前验证输入参数。调用该函数的任何地方。这就提出了一个问题:不是在调用函数的所有地方都产生了检查条件的重复吗?

我对空条件不感兴趣,但对任何输入变量的验证(sqrt函数的负值,被零除,状态和邮政编码的错误组合或其他任何东西)都不感兴趣。

有一些规则如何决定在哪里检查输入条件?

我在考虑一些争论:

  • 当无效变量的处理方式可能有所不同时,最好在调用方进行验证(例如,sqrt()函数-在某些情况下,我可能想使用复数,因此我在调用方中处理条件)
  • 当每个调用者的检查条件都相同时,最好在函数内部进行检查,以避免重复
  • 调用方中输入参数的验证仅在使用此参数调用许多函数之前进行。因此,在每个函数中验证参数无效
  • 正确的解决方案取决于具体情况

我希望这个问题不能与其他任何问题重复,我搜索了这个问题,发现了类似的问题,但是他们没有确切提及此案。

Answers:


15

这取决于。确定验证的位置应基于该方法隐含(或记录)的合同描述和强度。验证是加强遵守特定合同的好方法。如果由于某种原因该方法具有非常严格的合同,那么可以,您可以在调用之前进行检查。

当您创建一个公共方法时,这是一个特别重要的概念,因为您基本上是在宣传某种方法执行某些操作。最好按照您所说的去做!

以以下方法为例:

public void DeletePerson(Person p)
{            
    _database.Delete(p);
}

隐含的合同是DeletePerson什么?程序员只能假定如果Person传入任何内容,它将被删除。但是,我们知道并非总是如此。如果值p是什么null?如果p数据库中不存在该怎么办?如果数据库断开连接怎么办?因此,DeletePerson似乎不能很好地履行其合同。有时,它删除一个人,有时它抛出NullReferenceException或DatabaseNotConnectedException,或者有时不执行任何操作(例如,如果该人已被删除)。

众所周知,这样的API很难使用,因为当您调用方法的“黑匣子”时,可能会发生各种可怕的事情。

您可以通过以下几种方法来改善合同:

  • 添加验证并向合同添加例外。 这使合同更牢固,但要求调用方执行验证。但是,不同之处在于,现在他们知道了他们的要求。在这种情况下,我将其与C#XML注释进行通信,但是您可以改为添加throws(Java),使用Assert或使用诸如代码合同之类的合同工具。

    ///<exception>ArgumentNullException</exception>
    ///<exception>ArgumentException</exception>
    public void DeletePerson(Person p)
    {            
        if(p == null)
            throw new ArgumentNullException("p");
        if(!_database.Contains(p))
            throw new ArgumentException("The Person specified is not in the database.");
    
        _database.Delete(p);
    }
    

    旁注:反对这种风格的说法通常是,它会导致所有调用代码进行过多的预验证,但以我的经验,通常不是这种情况。想一想您要删除空人员的情况。那是怎么发生的?空人来自哪里?例如,如果这是一个UI,为什么在没有当前选择的情况下处理Delete键?如果已将其删除,是否还应该将其从显示器中删除?显然,这是有例外的,但是随着项目的发展,您经常会感谢这样的代码,以防止错误渗透到系统的深处。

  • 防御性地添加验证和代码。这使合同更加宽松,因为现在此方法的作用不仅仅是删除人员。我更改了方法名称以反映这一点,但是如果您在API中保持一致,则可能没有必要。这种方法有其优点和缺点。优点是您现在可以调用TryDeletePerson传递各种无效输入,而不必担心异常。当然,缺点是您的代码用户可能会过多调用此方法,或者在p为null的情况下可能会使调试困难。这可能被认为是对“ 单一责任原则”的轻微违反,因此,如果爆发火焰战争,请牢记这一点。

    public void TryDeletePerson(Person p)
    {            
        if(p == null || !_database.Contains(p))
            return;
    
        _database.Delete(p);
    }
    
  • 结合方法。有时您需要两者兼而有之,希望外部调用者严格遵循规则(以迫使他们负责代码),但是您希望私有代码灵活。

    ///<exception>ArgumentNullException</exception>
    ///<exception>ArgumentException</exception>
    public void DeletePerson(Person p)
    {            
        if(p == null)
            throw new ArgumentNullException("p");
        if(!_database.Contains(p))
            throw new ArgumentException("The Person specified is not in the database.");
    
        TryDeletePerson(p);
    }
    
    internal void TryDeletePerson(Person p)
    {            
        if(p == null || !_database.Contains(p))
            return;
    
        _database.Delete(p);
    }
    

以我的经验,专注于您隐含的合同而不是硬性规定最为有效。在呼叫者难以确定操作是否有效的情况下,防御性编码似乎更好地工作。如果您希望调用方仅在确实有意义的情况下进行方法调用,则严格的合同似乎会更好地工作。


感谢您提供示例很好的答案。我喜欢“防御性”和“严格合同”方法的观点。
srnka

7

这是约定,文档和用例的问题。

并非所有功能都相等。并非所有要求都相等。并非所有验证都是平等的。

例如,如果您的Java项目尝试尽可能避免使用空指针(例如,请参见Guava样式建议),您是否仍对每个函数参数进行验证以确保其不为空?可能没有必要,但是您仍然有可能这样做,以便更轻松地发现错误。但是您可以在先前引发NullPointerException的位置使用断言。

如果项目使用C ++怎么办?C ++中的约定/传统用于记录前提条件,但仅在调试版本中对其进行验证(如果有的话)。

在任何一种情况下,您的函数都有一个记录的前提条件:任何参数都不能为null。您可以改为扩展函数的范围,使其包含具有定义行为的null,例如“如果任何参数为null,则引发异常”。当然,这又是我在C ++中的传统-在Java中,以这种方式记录前提条件已经足够普遍。

但是,并非所有前提条件可以合理检查。例如,二进制搜索算法的前提是必须对要搜索的序列进行排序。但是,验证它确实是O(N)操作,因此在每次调用时都要这样做,这首先使使用O(log(N))算法失去了意义。如果您在进行防御性编程,则可以进行较少的检查(例如,验证搜索到的每个分区的开始值,中间值和结束值均已排序),但这并不能捕获所有错误。通常,您只需要依靠先决条件即可。

您需要进行明确检查的一个真实地方是边界。您项目的外部输入?验证,验证,验证。灰色区域是API边界。这实际上取决于您要信任客户代码的数量,无效输入造成的损坏以及要提供的查找错误的帮助。当然,任何特权边界都必须算作外部特权-例如,系统调用在提升的特权上下文中运行,因此必须非常小心地进行验证。当然,任何此类验证都必须在syscall内部。


感谢您的回答。您能否提供番石榴风格推荐的链接?我无法用google找出它的意思。+1用于验证边界。
srnka

添加了链接。它实际上不是完整的样式指南,只是非null实用程序文档的一部分。
塞巴斯蒂安·雷德尔

6

参数验证应该是所调用函数的关注点。函数应该知道什么是有效输入,什么不是有效输入。调用者可能不知道这一点,尤其是当他们不知道该函数在内部如何实现时。应该期望该函数处理来自调用者的参数值的任何组合。

由于该函数负责验证参数,因此您可以针对此函数编写单元测试,以确保其在有效和无效参数值下均能达到预期的性能。


感谢您的回答。因此,您认为该函数在每种情况下都应同时检查有效和无效的输入参数。与“实用程序员”书中的肯定不同:“输入参数的验证是调用者的责任”。很好的想法是“该函数应该知道什么才是有效的...调用者可能不知道这一点”。所以您不喜欢使用前提条件?
srnka

1
如果愿意,可以使用先决条件(请参阅Sebastian的答案),但我更喜欢保持防御,并处理任何可能的输入。
伯纳德

4

在函数本身内。如果该函数被多次使用,则您不想为每个函数调用验证参数。

此外,如果以影响参数验证的方式更新函数,则必须搜索每次出现的调用者验证以进行更新。这不是很可爱:-)。

您可以参考警卫条款

更新资料

请参阅我对您提供的每种情况的答复。

  • 当无效变量的处理方式可能有所不同时,最好在调用方进行验证(例如,sqrt()函数-在某些情况下,我可能想使用复数,因此我在调用方中处理条件)

    回答

    大多数编程语言默认情况下都支持整数和实数,而不支持复数,因此它们的实现sqrt仅接受非负数。具有sqrt返回复数的函数的唯一情况是使用面向数学的编程语言(例如Mathematica)时

    而且,sqrt对于大多数编程语言而言,已经实现了,因此您无法对其进行修改,并且,如果您尝试替换实现(请参见猴子补丁),那么您的合作者将对为什么sqrt突然接受负数感到震惊。

    如果需要一个,可以将其包装在sqrt处理负数并返回复数的自定义函数周围。

  • 当每个调用者的检查条件都相同时,最好在函数内部进行检查,以避免重复

    回答

    是的,这是避免在代码中分散参数验证的一种好习惯。

  • 调用方中输入参数的验证仅在使用此参数调用许多函数之前进行。因此,在每个函数中验证参数无效

    回答

    如果调用者是一个函数,那会很好,您不觉得吗?

    如果调用方中的功能被其他调用方使用,是什么导致您无法验证调用方所调用的函数中的参数?

  • 正确的解决方案取决于具体情况

    回答

    争取可维护的代码。移动参数验证可确保函数可以接受或不接受的真相来源之一。


感谢您的回答。sqrt()只是一个示例,许多其他函数都可以使用与输入参数相同的行为。“如果以影响参数验证的方式更新函数,则必须搜索每次出现的调用者验证”-我不同意这一点。然后,我们可以对返回值说同样的话:如果以影响返回值的方式更新函数,则必须纠正每个调用者...我认为函数必须有一个定义明确的任务才能执行...否则无论如何,必须更改呼叫者。
srnka

2

函数应说明其前置条件和后置条件。
前提条件是调用者在可以正确使用该功能之前可以满足的条件,并且可以(通常)包括输入参数的有效性。
后置条件是函数对其调用方的承诺。

如果功能参数的有效性是前提条件的一部分,则调用方有责任确保这些参数有效。但这并不意味着每个调用者都必须在调用之前显式检查每个参数。在大多数情况下,不需要显式测试,因为调用者的内部逻辑和前提条件已经确保参数有效。

作为防止编程错误(错误)的安全措施,您可以检查传递给函数的参数是否确实符合规定的前提条件。由于这些测试的成本可能很高,因此最好将其关闭以进行发行版本构建。如果这些测试失败,则应终止程序,因为事实证明该程序已遇到错误。

尽管乍看之下,调用方的检查似乎邀请了代码重复,但实际上是相反的。在被调用方中进行检查会导致代码重复,并且会执行许多不必要的工作。
试想一下,您多久通过几层函数传递参数,而在此过程中只对其中一些进行了很小的更改。如果您始终应用check-in-callee方法,那么每个中间功能都将不得不对每个参数重新进行检查。
现在假设这些参数之一应该是排序列表。
在调用方中进行检查后,只有第一个函数才必须确保列表确实已排序。所有其他人都知道列表已经排序(这就是他们在先决条件中所说的),可以不经进一步检查就将其传递。


+1感谢您的回答。很好的反映:“在被调用方中进行检查会导致代码重复并完成许多不必要的工作”。在句子中:“在大多数情况下,因为内部逻辑和调用者的先决条件已经确保,所以不需要显式测试”-您对“内部逻辑”的表达是什么意思?DBC功能?
srnka

@srnka:“内部逻辑”是指函数中的计算和决策。它本质上是功能的实现。
Bart van Ingen Schenau 2013年

0

大多数情况下,您不知道谁,何时以及如何调用您编写的函数。最好假设最坏的情况:您的函数将使用无效的参数进行调用。因此,您绝对应该涵盖这一点。

但是,如果您使用的语言支持异常,则可能不会检查某些错误并确保会引发异常,但是在这种情况下,必须确保在文档中描述了这种情况(您需要具有文档)。异常将为调用者提供有关发生的情况的足够信息,并且还将引起对无效参数的注意。


实际上,验证参数可能会更好,如果参数无效,请自己抛出异常。这是为什么:那些不费吹灰之力地调用例程的小丑就是那些不愿意检查表明他们传递了无效数据的错误返回码的小丑。引发异常强制解决此问题。
John R. Strohm
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.