错误报告

错误报告

GLib 提供了一种标准方法来向调用代码报告已调用函数的错误。(这是在其他语言中由异常解决的相同问题。)重要的是要理解,此方法既是一种数据类型(GError 结构)又是一种规则集。如果您不正确地使用 GError,那么您的代码将无法与其他使用 GError 的代码正确地交互,并且您的 API 的用户可能会感到困惑。在大多数情况下,使用 GError 优于使用数字错误代码,但在某些情况下,数字错误代码对于性能而言是有用的。

首先也是最重要的:GError 仅应用来报告可恢复的运行时错误,切勿用来报告编程错误。如果程序员搞砸了,那么您应该使用 g_warning()g_return_if_fail()g_assert()g_error() 或一些类似的工具。(顺便提一下,请记住,g_error() 函数只能用来处理编程错误,不应用来打印任何可通过 GError 报告的错误。)

可恢复的运行时错误的示例包括“文件未找到”或“解析输入失败”。编程错误的示例包括“NULL 传递给 strcmp()”或“尝试了两次释放同一个指针”。这两种类型的错误有着本质的区别:运行时错误应处理或向用户报告,编程错误应通过修复程序中的错误来消除。这就是为什么 GLib 和 GTK 中的大多数函数不使用 GError 设施的原因。

可能失败的函数将 GError 的返回值位置作为其最后一个参数。如果发生错误,将分配一个新的 GError 实例,并通过该参数返回给调用方。例如

gboolean g_file_get_contents (const char  *filename,
                              char       **contents,
                              gsize       *length,
                              GError     **error);

如果您为 error 参数传递非 NULL 值,它应指向可以放置错误的位置。例如

char *contents;
GError *err = NULL;

g_file_get_contents ("foo.txt", &contents, NULL, &err);
g_assert ((contents == NULL && err != NULL) || (contents != NULL && err == NULL));
if (err != NULL)
  {
    // Report error to user, and free error
    g_assert (contents == NULL);
    fprintf (stderr, "Unable to read file: %s\n", err->message);
    g_error_free (err);
  }
else
  {
    // Use file contents
    g_assert (contents != NULL);
  }

请注意,在这个示例中,err != NULLg_file_get_contents() 是否失败的可靠指示符。此外,g_file_get_contents() 返回一个布尔值,表示它是否成功。

由于 g_file_get_contents() 在失败时返回 FALSE,因此,如果您只关心它是否失败而不关心要显示错误消息,您可以为 error 参数传递 NULL

if (g_file_get_contents ("foo.txt", &contents, NULL, NULL)) // ignore errors
  // no error occurred
  ;
else
  // error
  ;

GError 对象包含三个字段:domain 指示错误报告函数所在的模块,code 指示发生的特定错误,而 message 是包含尽可能多详细信息的用户可读错误消息。提供了若干函数来处理来自被调用函数的错误:g_error_matches() 在错误与给定的域和代码匹配时返回 TRUEg_propagate_error() 将错误复制到错误位置(以便调用函数将收到它),而 g_clear_error() 通过释放错误并将位置重置为 NULL 来清除错误位置。要向用户显示错误,只需显示 message,也许还可以显示仅调用函数才知道的其他上下文(正在打开的文件或其他内容 - 不过在 g_file_get_contents() 情况下,message 已经包含文件名)。

由于错误消息可能会显示给用户,因此它们需要是有效的 UTF-8(所有 GTK 小部件都期望文本是 UTF-8)。尤其在使用文件名(采用“文件名编码”)设置错误消息格式时要记住这一点,需要使用 g_filename_to_utf8()g_filename_display_name()g_utf8_make_valid() 将其转换为 UTF-8。

但请注意,许多错误消息对于在应用程序中向用户显示来说过于技术化,因此更喜欢使用g_error_matches()从已调用的函数分类错误,并为应用程序中的上下文生成适当的错误消息。来自GError的错误消息更适合打印到系统日志或命令行上。它们通常是翻译过的。

报告错误

在实现可报告错误的函数时,基本工具是g_set_error()。通常,如果发生致命错误,您要g_set_error(),然后立即返回。如果传递给它的错误位置是NULLg_set_error()将不执行任何操作。以下是一个示例

int
foo_open_file (GError **error)
{
  int fd;
  int saved_errno;

  g_return_val_if_fail (error == NULL || *error == NULL, -1);

  fd = open ("file.txt", O_RDONLY);
  saved_errno = errno;

  if (fd < 0)
    {
      g_set_error (error,
                   FOO_ERROR,                 // error domain
                   FOO_ERROR_BLAH,            // error code
                   "Failed to open file: %s", // error message format string
                   g_strerror (saved_errno));
      return -1;
    }
  else
    return fd;
}

如果你自己调用另一个可以报告GError的函数,事情会变得有点复杂。如果子函数以报告GError以外的方式表明致命错误,例如通过返回TRUE表示成功,你可以简单地执行以下操作

gboolean
my_function_that_can_fail (GError **err)
{
  g_return_val_if_fail (err == NULL || *err == NULL, FALSE);

  if (!sub_function_that_can_fail (err))
    {
      // assert that error was set by the sub-function
      g_assert (err == NULL || *err != NULL);
      return FALSE;
    }

  // otherwise continue, no error occurred
  g_assert (err == NULL || *err == NULL);
}

如果子函数不通过报告GError表示错误(或其返回值并不可靠地表明错误),你需要创建一个临时的GError,因为传入的可能是NULL。在这种情况下,可以使用g_propagate_error()

gboolean
my_function_that_can_fail (GError **err)
{
  GError *tmp_error;

  g_return_val_if_fail (err == NULL || *err == NULL, FALSE);

  tmp_error = NULL;
  sub_function_that_can_fail (&tmp_error);

  if (tmp_error != NULL)
    {
      // store tmp_error in err, if err != NULL,
      // otherwise call g_error_free() on tmp_error
      g_propagate_error (err, tmp_error);
      return FALSE;
    }

  // otherwise continue, no error occurred
}

错误堆叠始终是一个错误。例如,此代码不正确

gboolean
my_function_that_can_fail (GError **err)
{
  GError *tmp_error;

  g_return_val_if_fail (err == NULL || *err == NULL, FALSE);

  tmp_error = NULL;
  sub_function_that_can_fail (&tmp_error);
  other_function_that_can_fail (&tmp_error);

  if (tmp_error != NULL)
    {
      g_propagate_error (err, tmp_error);
      return FALSE;
    }
}

tmp_error应在sub_function_that_can_fail()之后立即检查,并清除或向上传播。规则是:在每个错误之后,你必须解决该错误或将它返回给调用函数。

请注意,将NULL传递给错误位置等效于通过始终不采取任何措施来处理错误。因此,以下代码很好,假设sub_function_that_can_fail()中的错误对my_function_that_can_fail()不是致命的

gboolean
my_function_that_can_fail (GError **err)
{
  GError *tmp_error;

  g_return_val_if_fail (err == NULL || *err == NULL, FALSE);

  sub_function_that_can_fail (NULL); // ignore errors

  tmp_error = NULL;
  other_function_that_can_fail (&tmp_error);

  if (tmp_error != NULL)
    {
      g_propagate_error (err, tmp_error);
      return FALSE;
    }
}

请注意,为错误位置传递NULL会忽略错误;它等效于

try { sub_function_that_can_fail (); } catch (...) {}

在 C++ 中。它并不意味着使错误得不到处理;它的意思是通过不采取任何措施来处理它们。

错误域

错误域和代码通常按以下方式命名

  • 错误域称为<NAMESPACE>_<MODULE>_ERROR,例如G_SPAWN_ERRORG_THREAD_ERROR: “`c #define G_SPAWN_ERROR g_spawn_error_quark ()

G_DEFINE_QUARK (g-spawn-error-quark, g_spawn_error) “`

  • 错误域的 quark 函数称为<namespace>_<module>_error_quark,例如g_spawn_error_quark()g_thread_error_quark()

  • 错误代码位于一个名为<Namespace><Module>Error的枚举中;例如,GThreadErrorGSpawnError

  • 错误代码枚举的成员称为<NAMESPACE>_<MODULE>_ERROR_<CODE>,例如G_SPAWN_ERROR_FORKG_THREAD_ERROR_AGAIN

  • 如果针对无法恢复的错误有一个“通用”或“未知”错误代码,用特定代码进行区分没有意义,它应该称为<NAMESPACE>_<MODULE>_ERROR_FAILED,例如G_SPAWN_ERROR_FAILED。对于可能在未来版本中扩展的错误代码枚举,你通常不应明确处理此错误代码,而是应将任何无法识别的错误代码视为等效于FAILED

GError与传统错误处理的比较

GError 相比传统数字错误代码有几项优势:最重要的是诸如 gobject-introspection 之类的工具了解 GError 并将其转换为绑定中的异常;其消息包含的信息不仅仅是一个代码;并且使用一个域有助于避免错误代码的误解。

GError 也有一些缺点:它需要内存分配,且格式化错误消息字符串有性能开销。这使得它不适合使用在重试循环中,因为在这些循环中,出现错误是常见情况,而不是特殊情况。例如,使用 G_IO_ERROR_WOULD_BLOCK 表示在普通控制流中会遇到这些开销。在某些情况下,可使用 g_set_error_literal() 消除字符串格式化开销。

如果一个函数封装其调用的函数返回的 GError,这些性能问题可能会加剧:这会增加分配和字符串格式化操作的次数。这可以通过使用 g_prefix_error() 缓解。

使用 GError 的规则

GError 使用规则总结

  • 不要通过 GError 报告编程错误。

  • 返回错误的函数的最后一个参数应是可放置 GError 的位置(即 GError **error)。如果 GError 与变参一起使用,则 GError** 应在 ... 前的最后一个参数中。

  • 如果对发生的具体错误详情不感兴趣,则调用者可为 GError** 传递 NULL

  • 如果为 GError** 参数传递 NULL,则不应将错误返回给调用者,但如果发生错误,仍应中止函数并返回。也就是说,控制流不应受到调用者是否想要获取 GError 的影响。

  • 如果报告了 GError,那么按定义函数出错了,且未完成原本该完成的任务。如果这个错误不是致命的,则应处理此错误,且不应报告此错误。如果错误是致命的,则必须报告此错误,立即停止执行正在进行的操作。

  • 如果报告了 GError,则不能保证将输出参数设置为某个已定义值。

  • 在将 GError* 的地址传递给能报告错误的函数之前,必须将该地址初始化为 NULL

  • GError 结构体不能栈分配。

  • 堆积”错误始终是一个 bug。也就是说,如果将一个新的 GError 分配给一个非 NULLGError* 从而覆盖先前的错误,则表明应终止操作,而不是继续执行。如果可以继续,则应使用 g_clear_error() 清除先前的错误。g_set_error() 会在堆积错误时发出提示。

  • 根据惯例,如果您返回一个指示成功布尔值,则TRUE表示成功,FALSE表示失败。避免创建具有布尔返回值以及GError参数,但布尔值执行其他操作,而不是发信号指示是否设置GError的函数。除了其他问题以外,这需要 C 调用者分配一个临时错误。相反,提供一个gboolean *输出参数。GLib 中本身存在的使用此功能会很困难的函数,例如g_key_file_has_key()。如果返回FALSE,那么必须将错误设置为非NULL值。对此的一个例外情况是,在这种情况下已经考虑为未定义行为(例如,当g_return_val_if_fail()检查失败时),则不必设置错误。调用者不应单独检查是否设置错误,应该确保不会激发未定义的行为,并假设错误将在 失败时设置。

  • NULL返回值还经常用于表示发生错误。您应当在文档中说明NULL是否在非错误情况下是有效的返回值;如果NULL是有效的返回值,那么用户必须检查是否返回了错误,以查看函数是否 成功。

  • 当实现一个报告错误的函数时,您可能希望在函数顶部添加一个检查,以检查错误返回位置是否为NULL或包含NULL错误(例如g_return_if_fail (error == NULL || *error == NULL);)。

扩展的GError

自 GLib 2.68 起,可以扩展GError类型。这是通过G_DEFINE_EXTENDED_ERROR()宏完成的。要创建一个扩展的GError类型,可以在头文件中完成如下操作 

typedef enum
{
  MY_ERROR_BAD_REQUEST,
} MyError;
#define MY_ERROR (my_error_quark ())
GQuark my_error_quark (void);
int
my_error_get_parse_error_id (GError *error);
const char *
my_error_get_bad_request_details (GError *error);

并在 实现中完成

typedef struct
{
  int parse_error_id;
  char *bad_request_details;
} MyErrorPrivate;

static void
my_error_private_init (MyErrorPrivate *priv)
{
  priv->parse_error_id = -1;
  // No need to set priv->bad_request_details to NULL,
  // the struct is initialized with zeros.
}

static void
my_error_private_copy (const MyErrorPrivate *src_priv, MyErrorPrivate *dest_priv)
{
  dest_priv->parse_error_id = src_priv->parse_error_id;
  dest_priv->bad_request_details = g_strdup (src_priv->bad_request_details);
}

static void
my_error_private_clear (MyErrorPrivate *priv)
{
  g_free (priv->bad_request_details);
}

// This defines the my_error_get_private and my_error_quark functions.
G_DEFINE_EXTENDED_ERROR (MyError, my_error)

int
my_error_get_parse_error_id (GError *error)
{
  MyErrorPrivate *priv = my_error_get_private (error);
  g_return_val_if_fail (priv != NULL, -1);
  return priv->parse_error_id;
}

const char *
my_error_get_bad_request_details (GError *error)
{
  MyErrorPrivate *priv = my_error_get_private (error);
  g_return_val_if_fail (priv != NULL, NULL);
  g_return_val_if_fail (error->code != MY_ERROR_BAD_REQUEST, NULL);
  return priv->bad_request_details;
}

static void
my_error_set_bad_request (GError     **error,
                          const char  *reason,
                          int          error_id,
                          const char  *details)
{
  MyErrorPrivate *priv;
  g_set_error (error, MY_ERROR, MY_ERROR_BAD_REQUEST, "Invalid request: %s", reason);
  if (error != NULL && *error != NULL)
    {
      priv = my_error_get_private (error);
      g_return_val_if_fail (priv != NULL, NULL);
      priv->parse_error_id = error_id;
      priv->bad_request_details = g_strdup (details);
    }
}

使用的错误示例可以 为

gboolean
send_request (GBytes *request, GError **error)
{
  ParseFailedStatus *failure = validate_request (request);
  if (failure != NULL)
    {
      my_error_set_bad_request (error, failure->reason, failure->error_id, failure->details);
      parse_failed_status_free (failure);
      return FALSE;
    }

  return send_one (request, error);
}

请注意,假定您是一个库作者,您的库公开了现有错误域,则您不可能将此错误域生成为一个扩展版本,这样做会破坏 ABI。这是因为,先前可以创建带此错误域的错误,然后通过 g_error_copy()进行复制。如果库的新版本导致错误域成为一个扩展版本,那么分配了错误的堆栈代码所调用的g_error_copy()将尝试复制比以前更多的数据,这将导致未定义的行为。您不得分配具有扩展错误域的堆栈错误,分配任何其他GError堆栈也是不好的操作。

不支持可卸载插件/模块中的扩展错误域。