4

曾经有人告诉我,具有一个输入和一个输出(不完全是一个)的函数在被调用时不应打印消息。但我不明白。是为了安全还是为了约定?

让我举个例子。如何处理尝试访问具有不正确索引的顺序列表中的数据的尝试?

// 1. Give out the error message inside the function directly.
DataType GetData(seqList *L, int index)
{
    if (index < 0  ||  index >= L->length) {
        printf("Error: Access beyond bounds of list.\n");
        // exit(EXIT_FAILURE);
    }
    return L->data[index];
}

// 2. Return a value or use a global variable(like errno) that
//    indicates whether the function performs successfully.
StateType GetData(seqList *L, int index, int *data)
{
    if (index < 0  ||  index >= L->length) {
        return ERROR;
    }
    *data = L->data[index];
    return OK;
}
4

6 回答 6

6

我认为这里发生了两件事:

  1. 任何可见的和意想不到的副作用,例如写入流通常都是不好的,而不仅仅是对于具有一个输入和一个输出的函数。如果我使用的是列表库,并且它开始默默地将错误消息写入我用于常规输出的同一输出流,我会认为这是一个问题。但是,如果正在编写这样一个函数供您个人使用,并且您提前知道您想要执行的操作始终是打印一条消息和exit(),那么就可以了。只是不要将这种行为强加于其他人。

  2. 这是如何通知调用者有关错误的一般问题的具体案例。很多时候,函数无法知道对错误的正确响应,因为它没有调用者所做的上下文。以malloc()为例。绝大多数时候,当malloc()失败时,我只想终止,但偶尔我可能想通过调用来故意填充内存,malloc()直到它失败,然后继续做其他事情。在这种情况下,我不希望函数决定是否终止——我只想让它告诉我它失败了,然后将控制权交还给我。

有许多不同的方法来处理库函数中的错误:

  1. 终止 - 如果您自己编写程序,那很好,但对通用库函数不利。一般来说,对于一个库函数,你会想让调用者决定在错误的情况下做什么,所以函数的作用仅限于通知调用者错误。

  2. 返回错误值 - 有时可以,但有时没有可行的错误值。atoi()是一个很好的例子——它返回的所有可能的值都可能是输入字符串的正确翻译。无论您在错误时返回什么,无论是它0还是-1其他任何东西,都无法将错误与有效结果区分开来,这正是遇到未定义行为的原因。从稍微纯粹的角度来看,它在语义上也是有问题的——例如,返回数字平方根的函数是一回事,但有时返回数字平方根的函数,但有时返回错误代码而不是平方根是另一回事。当返回值用于两个完全不同的目的时,您可能会失去函数的自我记录简单性。

  3. 让程序处于错误状态,例如设置errno. 您仍然有一个根本问题,即如果没有可行的返回值,该函数仍然无法告诉您发生了错误。您可以errno提前设置为 0 并在每次之后检查它,但这需要大量工作,并且当您开始涉及并发时可能不可行。

  4. 调用错误处理函数——这基本上只是推卸责任,因为错误函数也必须解决上述问题,但至少你可以提供自己的。此外,正如 R. 在下面的评论中指出的那样,除了在非常简单的情况下,例如“总是在任何错误时终止”之外,它可能会要求过多的单个全局错误处理函数能够明智地处理可能出现的任何错误您的程序可以恢复正常执行的一种方式。拥有大量错误处理函数并将适当的函数单独传递给每个库函数在技术上是可行的,但几乎不是最佳解决方案。在存在并发的情况下,以这种方式使用错误处理函数也可能很难甚至不可能正确使用。

  5. 如果遇到错误,传入一个由函数修改的参数。在技​​术上是可行的,但是为此目的为每个编写的库函数添加一个额外的参数并不是真正可取的。

  6. 抛出异常——你的语言必须支持它们这样做,它们伴随着各种相关的困难,包括不明确的结构和程序流程、更复杂的代码等等。有些人——我不是其中之一——认为例外在道德上等同于longjmp().

所有可能的方法都有其缺点和优点,到目前为止,人类还没有发现从库函数报告错误的完美方法。

于 2013-09-23T16:54:30.807 回答
2

一般来说,您应该确保您有一个一致且连贯的错误处理策略,这意味着您要考虑是否要将错误传递到更高级别或在最初发生的级别处理它。这个决定与函数有多少输入和输出无关。

例如,在一个深度嵌入式系统中,内存分配失败发生在关键时刻,没有必要将该错误传递回去(实际上你可能无法做到)——你所能做的就是进入一个紧密的循环让看门狗重置你。在这种情况下,保留无效的返回值来指示错误是没有意义的,或者如果这样做没有意义,甚至根本检查返回值是没有意义的。(请注意,我并不是在提倡懒惰地不去检查返回值,这完全是另一回事)。

另一方面,在一个可爱漂亮的 GUI 应用程序中,您可能希望尽可能优雅地失败并将错误传递到可以解决/重试/任何适当的级别;或者如果无法执行其他操作,则将其作为错误呈现给用户。

于 2013-09-23T16:45:31.850 回答
1

是的,这是一种不好的做法;更糟糕的是,您将输出发送到stdout而不是stderr. 这最终可能会通过将错误消息与输出混合来破坏数据。

就个人而言,我非常坚信这种“错误处理”是有害的。您无法验证调用方是否为 传递了有效值L,因此检查 的有效性index是不一致的。该函数的文档化接口契约应该只是L一个指向正确类型对象的有效指针和index一个有效索引(在任何意义上对您的代码有意义)。如果为Lorindex传递了无效值,则这是代码中的错误,而不是运行时可能发生的合法错误。如果您需要帮助调试它,assert宏 fromassert.h可能是一个好主意;当您不再需要它时,它可以很容易地关闭它。

上述原则的一个可能例外是,值L来自程序中的其他数据结构,但index来自一些不受您控制的外部输入。然后,您可以在调用此函数之前执行外部验证步骤,但如果您总是需要验证,那么像您正在做的那样集成它是有意义的。但是,在这种情况下,您需要有一种方法向调用者报告失败,而不是向stdout. 因此,您需要保留一个可能的返回值作为错误标记,或者有一个额外的参数允许您向调用者返回结果和错误状态。

于 2013-09-23T16:39:58.580 回答
1

返回对成功条件无效的保留值。例如,NULL

建议不要打印,因为:

  1. 它对错误的调用代码没有帮助。
  2. 您正在写入可能被更高级别代码用于其他内容的流。
  3. 该错误可能恢复得更高,因此您可能只是在打印误导性错误消息。

正如其他人所说,处理错误情况的一致性也是一个重要因素。但是考虑一下:

  • 如果您的代码被用作另一个应用程序的组件,一个不遵循您的打印约定的应用程序,那么通过打印您不允许客户端代码忠实于它自己的策略。因此,使用这种策略,您将您的约定强加于所有相关代码。
  • 另一方面,如果您遵循返回保留值的“更清洁”解决方案并且客户端代码想要遵循打印约定,则客户端代码可以通过制作简单的包装器轻松适应您返回的内容甚至打印错误你的功能。因此,使用这种策略,您可以为代码的用户提供足够的空间来选择最适合他们的策略并忠实于它。
于 2013-09-23T16:40:07.783 回答
1

最好用于perror()显示错误消息而不是使用 printf()

句法:

void perror(const char *s);

错误消息也应该发送到标准错误流而不是标准输出。

于 2013-09-23T16:42:25.397 回答
0

代码只处理一件事总是最好的。它更容易理解,更容易使用,并且适用于更多的实例。

打印错误消息的 GetData 函数不适合在可能没有值的情况下使用。即调用代码想要尝试获取一个值并通过使用默认值(如果不存在)来处理错误。

由于 GetData 不知道上下文,它不能报告好的错误消息。作为调用堆栈上方的示例,我们可以报告嘿,您忘记给该用户一个年龄,而在 GetData 中,它所知道的只是我们无法获得一些价值。

多线程的情况呢?GetData 似乎可能会从多个线程中调用。如果在中间插入一个随机的 IO 位,如果所有线程都需要同时写入,则会导致争用谁可以访问控制台。

于 2013-09-23T17:03:52.457 回答