5

我有一个cheesesales.txt包含最近所有奶酪销售的 CSV 文件。我想创建一个CheeseSales可以执行以下操作的类:

CheeseSales sales("cheesesales.txt"); //has no default constructor
cout << sales.totalSales() << endl;
sales.outputPieChart("piechart.pdf");

上面的代码假定不会发生任何故障。实际上,失败会发生。在这种情况下,可能会发生两种故障:

  • 构造函数失败:文件可能不存在,可能没有读取权限,包含无效/不可解析的数据等。
  • 常规方法失败:文件可能已经存在,可能没有写入权限,可用于创建饼图的销售数据太少等。

我的问题很简单:您将如何设计此代码来处理故障?

一个想法:bool从表示失败的常规方法返回 a 。不知道如何处理构造函数。

经验丰富的 C++ 程序员会如何做这些事情?

4

7 回答 7

4

在 C++ 中,异常是报告错误的方式。可以处理初始化列表中的 BTW 异常。

函数尝试块将处理程序 seq 与 ctor-initializer(如果存在)和函数体相关联。在执行 ctor-initializer 中的初始化程序表达式期间或在执行函数体期间引发的异常将控制权转移到 function-try-block 中的处理程序,其方式与在执行 try 期间引发的异常相同-block 将控制权转移给其他处理程序。

好的代码通常应该在最上层(线程)级别上使用最少的 try/catch 块。理想情况下只有一个。这样,知道“一切都会抛出”,您不必过多考虑错误,并且您的正常场景代码流看起来很干净。

于 2012-12-02T10:59:50.840 回答
2

你对这两种失败的区分是对的,它们确实有细微的不同。


构造函数失败:文件可能不存在,可能没有读取权限,包含无效/不可解析的数据等。

只是抛出一个异常。半构建对象是解决程序错误的最短方法。

一个类应该有一个构造函数建立并且所有方法都维护的不变量。如果构造函数无法建立不变量,则对象不可用,在 C++ 中报告这一点的最佳方法是抛出异常,以便语言确保不使用半构建的对象。

如果人们建议您可能想要一个无效状态,请提醒他们单一职责原则:您的类已经具有特定的职责(它的不变量),希望更多的人可以将其封装在一个专门提供可选性的类中。离开我的头,boost::optional都是std::unique_ptr很好的选择。


常规方法失败:文件可能已经存在,可能没有写入权限,可用于创建饼图的销售数据太少等。

不幸的是,您未能区分两种情况:

  • 读取实例的方法
  • 也修改实例的方法

对于所有方法,您需要选择错误报告策略。我的建议是例外例外的。如果故障被认为是异常的(网络链接在 99.99% 的时间都在运行时断开),那么异常就可以了。另一方面,如果预计会失败,通常取决于输入(例如find方法或在您的情况下write是指定文件的方法),那么您希望让用户有机会做出适当的反应。

在排除了例外之后,至少还有两种方法:

  • 返回一个代码 ( bool, enum) 指示操作是否顺利
  • 要求用户提供将在出现问题时调用的错误策略

错误策略可能像enum(跳过、重试、抛出)一样简单,也可能像Strategy各种方法一样复杂。

此外,没有人说您的方法可能只有一种错误报告机制。例如,您可以选择:

  • 如果文件已经存在,则调用错误策略(毕竟是用户提供的,他们可能想要切换到不同的名称)
  • 如果无法访问磁盘,则抛出(通常预计硬件可以工作!)

最后,除此之外,也修改实例的方法必须担心维护构造函数建立的不变量。如果一个方法可能会搞砸不变量,那么它根本就不是不变量,每次使用该类时,您的用户都应该感到恐惧……一般的智慧是在开始修改对象之前执行所有可能抛出的操作。

一个简单(但非常容易)的实现是复制和交换习惯用法:在内部复制对象,对副本执行操作,并在方法结束时将副本的状态与当前对象的状态交换。如果出现任何问题,副本将被损坏并在堆栈展开期间立即丢弃,而对象不受影响。

有关此主题的更多信息,您可能需要阅读Exceptions Guarantees。我描述的是强异常保证方法的典型实现,类似于数据库事务(全有或全无)。

于 2012-12-02T11:55:28.807 回答
2

好吧,抛出异常是显而易见的选择。这在 C++ 中存在一些问题,因为无法捕获初始化器列表构造函数抛出的异常,从而导致各种麻烦。

因此,您实际上应该提供访问文件并可能引发异常的构造函数,以及使对象处于“数据未加载状态”的默认构造函数。这允许将您的对象安全地用作其他类的成员,同时还允许另一个构造函数加载(或抛出异常)数据。

另一种选择是让数据加载构造函数不抛出异常,而是在加载失败时将对象设置为无效状态,并让其他方法抛出异常,并为当前状态设置 getter。

无论如何,我相信你的班级需要错误/未初始化状态,没有安全的方法。

由@MathieuM. 的评论编辑:在外部实现“未初始化状态”的一种替代方法是使其成为可选,最容易通过使用指针包装类。然后在初始化列表中将其安全地初始化为 NULL,并在构造函数主体中尝试真正的初始化,并进行任何错误处理。通过选择这个,你可以让构造函数抛出异常,让类的用户担心它。

于 2012-12-02T10:29:12.860 回答
2

构造函数用于初始化对象的内部状态。不要使用它来进行繁重的操作,例如读取和处理文件。

如果发生错误,请改用读取文件并抛出异常(或返回布尔值表示成功)的方法。在您的主要流程中捕获此异常并按照您认为合适的方式处理它。

编辑:如果这是整个类的目的,那么可能ChessSales只包含数据,您应该使用工厂类(或者静态实用程序类),该类具有读取 CSV 文件并返回ChessSales包含从读取的相关数据的方法的方法CSV 文件。这样您就可以将数据与业务逻辑分开(在这种情况下读取和解析 CSV 文件)

于 2012-12-02T10:00:45.087 回答
0

当你问这个问题时,你错过了一个重要的细节:当操作失败时你想发生什么?

在构造函数的情况下,我肯定会抛出异常。我希望能够像您上面写的那样编写代码;我不希望它看起来像

CheeseSales sales("cheesesales.txt"); //has no default constructor
if (!sales.good()) {
    // Somehow handle things here.
}
// The if block has to break control flow or reinitialise sales, otherwise
// these next lines will break.
cout << sales.totalSales() << endl;
sales.outputPieChart("piechart.pdf");

投掷意味着在这种情况下我可以假设更强的不变量,因此是一件好事。

鉴于输出操作不会改变 的状态CheeseSales,扔在那里不会加强不变量。在这种情况下,使用辅助对象来打印图表可能会更好:

ChooseSales sales("cheesesales.txt");
PdfStream chart("piechart.pdf");
chart << sales.asPieChart();

PdfStream可能只是std::ofstream,但也许您想提供更多功能。)

如果操作失败,它可以将chart对象置于错误状态,这与 iostreams 通常的工作方式相对应。

于 2012-12-02T10:54:10.803 回答
0

我认为您可以在构造对象之前从输入文件中获取信息。例如,代码可能是这样的:

if(!getInfoFromFile("cheesesales.txt", date, amount, kindOfCheese, money)){
    cout << "Failed to get information from file." << endl;
    return FALSE;
}
CheeseSales sales(date, amount, kindOfCheese, money);
cout << sales.totalSales() << endl;
sales.outputPieChart("piechart.pdf");

然后你可以避免在构造函数中处理错误。
当然,抛出异常也是一种解决方案。但我不喜欢它,因为 c++ 中的异常非常复杂。你可能会遇到很多难以想象的问题。

于 2012-12-02T10:47:58.323 回答
0

这就是我处理错误的方式:我在日志文件中记录所有错误,并创建一个简单易用的函数来报告错误。如果程序运行不好,我打开日志文件查找原因。

关于如何在错误发生时了解错误:将异常引入 C++ 的原因之一是在构造函数中报告错误。由于构造函数不返回值,因此它们可以通过这种方式抛出异常并报告失败。就个人而言,我不太喜欢这个方案,因为它会让你把你的代码放在try..catch. 如果您忘记这样做,您可能想知道为什么您的程序崩溃了。

所以我通常会这样做:

1)让构造函数设置一个成员变量为成功/失败,取决于构造函数的成功操作。我以后可以用类似的东西检查它if (myobj.constructor_ok()...)。请注意,我没有直接访问成员变量。

2)我非常喜欢从方法返回真/假,尽可能表示成功/失败。这使代码非常易读。

3) ..和上面的日志文件。

于 2012-12-02T10:19:37.837 回答