【问题标题】:Programming style question on how to code functions关于如何编写函数的编程风格问题
【发布时间】:2010-04-18 22:41:36
【问题描述】:

所以,我今天只是写了一点代码,我意识到我在编写函数时的编码风格并没有太多的一致性。我主要关心的一个问题是它是否适合对其进行编码,以便检查用户的输入在函数外部是否有效,或者只是将用户传递的值扔到函数中并检查这些值是否有效在那里。让我画一个例子:

我有一个基于环境列出主机的功能,并且我希望能够将环境拆分为主机块。所以一个用法的例子是这样的:

listhosts -e testenv -s 2 1

这将从“testenv”中获取所有主机,将其分成两部分,并显示第一部分。

在我的代码中,我有一个函数,您可以将它传递到一个列表中,它会根据您的拆分参数返回一个列表列表。但是,在我传递一个列表之前,我首先在 getops 过程中验证我的 MAIN 中的参数,所以我主要检查以确保用户没有传递负数,我确保用户没有请求拆分为比如说,4 个部分,但要求显示第 5 个部分(这将是无效的),等等。

tl;dr:您会检查用户输入您是 MAIN 类的流程的有效性,还是检查您的函数本身,并在输入有效的情况下返回有效响应,或者输入无效返回NULL?

显然这两种方法都有效,我只是想听听专家关于哪种方法更好:) 感谢你们提出的任何 cmets 和建议!仅供参考,我的示例是用 Python 编码的,但我仍然对通用编程答案而不是特定语言的答案更感兴趣!

【问题讨论】:

    标签: programming-languages coding-style user-input


    【解决方案1】:

    好问题!我的主要建议是系统地解决问题。如果你正在设计一个函数f,我是这样看待它的规范的:

    1. f 的调用者必须满足哪些绝对要求?这些要求是f前提条件

    2. f 对其调用者做了什么?当f返回时,返回值是什么,机器的状态是什么? f在什么情况下抛出异常,抛出什么异常?所有这些问题的答案构成了f后置条件

      前置条件和后置条件共同构成f 与调用者的契约。 只有满足前置条件的调用者才能依赖后置条件。

    3. 最后,直接针对您的问题,如果f 的调用者不满足前提条件会怎样?你有两个选择:

      • 您保证停止程序,希望收到一条信息丰富的消息。这是一个已检查的运行时错误。

      • 一切顺利。也许有一个段错误,也许内存已损坏,也许f 默默地返回一个错误的答案。这是一个未检查运行时错误。

      注意一些不在此列表中的项目:引发异常或返回错误代码。如果要依赖这些行为,它们将成为f 合同的一部分。

    现在我可以改写你的问题:

    当调用者违反合约时,函数应该怎么做?

    在大多数类型的应用程序中,该函数应该以 checked 运行时错误暂停程序。如果程序是需要可靠的应用程序的一部分,则应用程序应提供外部机制来重新启动应用程序,该应用程序因检查运行时错误而停止(在 Erlang 代码中很常见),或者如果重新启动很困难,则所有功能' 合同应该非常宽松,这样“错误输入”仍然符合合同,但承诺总是提出例外。

    在每个程序中,未检查运行时错误应该很少发生。未经检查的运行时错误通常仅在性能方面是合理的,即使如此,也只有在代码对性能至关重要时。未经检查的运行时错误的另一个来源是使用不安全的语言进行编程。例如,在 C 中,没有办法检查指向的内存是否已经真正初始化。

    你问题的另一个方面是

    什么样的合同才能做出最好的设计?

    这个问题的答案因问题领域而异。 因为我所做的任何工作都不必是高可用性或安全关键的,所以我使用限制性合同和大量检查过的运行时错误(通常是断言失败)。当您设计大型系统的接口和合约时, 如果您保持合约简单,保持先决条件限制性(严格),并且在以下情况下依赖已检查的运行时错误论据是“坏的”。

    我有一个函数,您可以将它传递到一个列表中,它会根据您的拆分参数返回一个列表列表。但是,在我传递一个列表之前,我首先在 getops 过程中验证我的 MAIN 中的参数,所以我主要检查以确保用户没有传递负数,我确保用户没有请求拆分为比如说 4 个部分,但要求显示第 5 个部分。

    我认为这正是解决这个特殊问题的正确方法:

    1. 您与用户的合同是用户可以说任何话,如果用户发出无意义的请求,您的程序不会崩溃——它会发出一个合理的错误消息,然后继续。

    2. 您与请求处理功能的内部约定是您将只传递合理的请求。

    3. 因此,除了第二个函数之外,您还有第三个函数,它的工作是区分意义和废话并采取相应的行动——您的请求处理函数获得“意义”,用户被告知“废话”,并且所有合同均已履行。

    我主要关心的一个问题是它是否适合对其进行编码,以便检查用户的输入在函数之外是否有效。

    是的。这几乎总是最好的设计。事实上,在某个地方可能有一个设计模式,名字很花哨。但如果不是这样,有经验的程序员会一遍又一遍地看到这一点。发生以下两种情况之一:

    • 解析/验证/拒绝错误消息

    • 解析/验证/处理

    这种设计有一种数据类型(请求)和四种功能。由于我这周要编写大量的 Haskell 代码,所以我将在 Haskell 中给出一个示例:

    data Request -- type of a request
    parse    :: UserInput -> Request            -- has a somewhat permissive precondition
    validate :: Request -> Maybe ErrorMessage   -- has a very permissive precondition
    process  :: Request -> Result               -- has a very restrictive precondition
    

    当然还有很多其他方法可以做到这一点。可以在解析阶段和验证阶段检测到故障。 “有效请求”实际上可以由与“未验证请求”不同的类型表示。以此类推。

    【讨论】:

    • 哇,这是一个非常详细和有见地的回复,谢谢! (其他当然也不错!)
    【解决方案2】:

    我会在函数内部进行检查,以确保我所期望的参数确实是我得到的。

    将其称为“防御性编程”或“按合同编程”或“断言检查参数”或“封装”,但其想法是该函数应负责检查其自身的前置条件和后置条件,并确保不违反不变量。

    如果您在函数之外执行此操作,您可能会面临客户不执行检查的可能性。一个方法不应该依赖其他人知道如何正确使用它。

    如果合约失败,你要么抛出异常,如果你的语言支持它们,或者返回某种错误代码。

    【讨论】:

    • 如果合同失败了怎么办?
    • @Stefan 如果您的实现语言不支持异常,可能是 OP 不支持?请注意,我并不反对 PBC,但如果您使用它,您必须明确说明合同失败时会发生什么。
    • 你可以返回一个错误代码,就像你在 1970 年编程一样。我的建议是什么?获取有例外的语言。
    • @Neil,@Stefan:我不喜欢在违反合同时下意识地抛出异常的反应。异常成为合同的一部分,如果你愿意的话,也是接口的一部分,现在所有的调用者都必须弄清楚他们将如何合作来处理这个异常。我更喜欢在实际控制流中使用异常;如果违反合同,典型程序应停止并显示错误消息(如有必要,环境应启动另一个实例)。
    • @Norman,来电者无需弄清楚任何事情;如果它是一个逻辑错误(例如取消引用空指针、访问越界索引、传递非法参数等),那么调用者根本不需要处理它......它只会渗透所有返回到 main 的方式,它将像您想要的那样用错误消息停止程序。不过,该异常的优势在于,您可以在链上添加更多信息或更改报告错误的方式(例如,您可以选择将其发送到日志文件而不是标准错误)。跨度>
    【解决方案3】:

    在函数内进行检查会增加复杂性,因此我个人的策略是尽可能地在堆栈中进行完整性检查,并在出现异常时捕获它们。我还确保我的函数被记录在案,以便其他程序员知道函数对他们的期望。他们可能并不总是遵循这样的期望,但坦率地说,让他们的程序正常运行并不是我的工作。

    【讨论】:

    • @Ignacio,但是如果您返回不正确的结果,它们可能要等到程序的很晚才被发现,而这就是调试的地狱。虽然我碰巧在使用函数之前阅读了文档,但我认为大多数程序员实际上并不阅读文档。
    【解决方案4】:

    在这两个地方检查输入通常是有意义的。

    在函数中,您应该验证输入,如果输入不正确则抛出异常。这可以防止无效输入导致函数执行到一半,然后引发意外异常,例如“数组索引越界”或类似情况。这将使调试错误变得更加简单。

    但是,抛出异常不应用作流控制,并且您不希望将原始异常直接抛出给用户,因此我还将在用户界面中添加逻辑以确保我永远不会调用无效的函数输入。在您的情况下,这将在控制台上显示一条消息,但在其他情况下,它可能在 GUI 中显示验证错误,可能在您输入时。

    【讨论】:

      【解决方案5】:

      “代码完成”建议一种隔离策略,可以在验证所有输入的类和将其输入视为已验证的类之间划清界限。允许通过验证行的任何内容都被认为是安全的,并且可以传递给不进行验证的函数(它们使用断言代替,因此外部验证代码中的错误可以自行显现)。

      【讨论】:

        【解决方案6】:

        如何处理错误取决于编程语言;但是,在编写命令行应用程序时,命令行确实应该验证输入是否合理。如果输入不合理,则适当的行为是打印一条“Usage”消息并解释需求,并以非零状态代码退出,以便其他程序知道它失败(通过测试退出代码) .

        Silent failure is the worst kind of failure,如果您在给出无效参数时只是返回不正确的结果,就会发生这种情况。如果故障被捕获,那么它很可能会在离真正故障点很远的地方被发现(传递无效参数)。因此,恕我直言,最好在参数无效时抛出异常(或者,在不可能的情况下,返回错误状态代码),因为它会在错误发生时立即标记错误,从而更容易识别和纠正失败的真正原因。

        我还应该补充一点,在处理无效输入时保持一致非常重要;您应该检查并为所有函数的无效输入抛出异常,或者对它们都不这样做,因为如果您的界面用户发现某些函数抛出无效输入,他们将开始依赖这种行为并且会感到非常惊讶当其他函数只是返回无效结果而不是抱怨时。

        【讨论】:

          猜你喜欢
          • 2016-01-11
          • 1970-01-01
          • 2022-07-11
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2022-01-12
          • 2017-10-31
          • 1970-01-01
          相关资源
          最近更新 更多