错误报告
错误报告
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 != NULL
是 g_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()
在错误与给定的域和代码匹配时返回 TRUE
,g_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()
,然后立即返回。如果传递给它的错误位置是NULL
,g_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_ERROR
或G_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
的枚举中;例如,GThreadError
或GSpawnError
。 -
错误代码枚举的成员称为
<NAMESPACE>_<MODULE>_ERROR_<CODE>
,例如G_SPAWN_ERROR_FORK
或G_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
分配给一个非NULL
的GError*
从而覆盖先前的错误,则表明应终止操作,而不是继续执行。如果可以继续,则应使用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
堆栈也是不好的操作。
不支持可卸载插件/模块中的扩展错误域。