这是好的C#编程风格吗?

10

考虑以下方法签名:

public static bool TryGetPolls(out List<Poll> polls, out string errorMessage)

该方法执行以下操作:

  • 访问数据库以生成Poll对象列表。
  • 如果成功,返回true,errorMessage将为空字符串。
  • 如果失败,返回false,errorMessage将包含异常消息。

这是一个好的编程风格吗?

更新:

假设我使用以下方法签名:

public static List<Poll> GetPolls()

在那个方法中,它不会捕获任何异常(所以我依赖调用者来捕获异常)。如何处理并关闭在该方法范围内的所有对象?一旦抛出异常,关闭和处理方法中的对象的代码就无法访问。


1
要格式化代码,请在之前添加四个空格。最简单的方法是键入/复制您的代码,将其全部高亮显示,然后在编辑器中点击“code”按钮。 - Will Eddins
17个回答

43

该方法试图执行三个不同的操作:

  1. 检索并返回一个投票列表
  2. 返回一个指示成功或失败的布尔值
  3. 返回错误消息

从设计的角度来看,这相当混乱。

更好的方法是简单地声明:

public static List<Poll> GetPolls()

如果有任何错误发生,那么就让这个方法抛出一个Exception异常。


3
如果 TryGetPolls 可以合法地返回空值,那么它可能会很有用。 - Michael
4
“Try…”方法在某些情况下非常有用,特别是当你不关心某些事情是否出错时。如果该方法可以合法地返回空,则应返回null/Nothing。 - STW
@SnOrfus:还要注意的是,当失败很常见时,TryParse更快,因为它避免了抛出异常。是的,人们已经对此进行了基准测试。是的,这通常并不重要。 - Brian
为什么需要先尝试呢?如果没有记录,那么集合应该是空的。 - JC.
在任何情况下,如果可能没有返回任何项,返回一个空列表比返回null更直观、更方便。 - Ryan Lundy
显示剩余9条评论

11

我相信

public static bool TryGetPolls(out List<Poll> polls)

更为适当的方式是使用 TryGet 方法。如果方法是一个 TryGet,那么我的初步假设就是存在失败的可能性,并且责任在于调用者确定接下来该做什么。如果调用者没有处理错误,或者想要错误信息,我会期望他们调用相应的 Get 方法。


更好的做法是使用IList<Poll>来增加抽象性。 - Callum Rogers

11

这绝对不是编写C#的惯用方式,这也意味着这可能不是一个好的风格。

如果您有一个TryGetPolls方法,那么它意味着如果操作成功,您希望得到结果,如果不成功,则您不关心失败原因。

如果您只有一个GetPolls方法,那么它意味着您总是希望得到结果,如果不成功,则希望以Exception的形式知道失败原因。

将两者混合在一起介于其中间,这对大多数人来说都很奇怪。因此,我建议要么不返回错误消息,要么在失败时引发一个Exception,但不要使用这种奇怪的混合方法。

因此,您的方法签名应该是以下之一:

IList<Poll> GetPolls();
或者
bool TryGetPolls(out IList<Poll> polls);

请注意,我返回的是一个 IList<Poll> 而不是一个 List<Poll>,因为编程应该按照抽象而不是实现进行。


我完全支持在公共API中使用抽象,特别是当客户端不在您的控制范围内时。但如果这是一个内部类,那么您会用其他东西替换.Net框架中的List实现吗?如果它是一个可以模拟的接口,那就没问题了,但是.Net List类呢?如何考虑KISS原则。 - softveda
@Pratik - IList<T>比List<T>复杂在哪里?它只是在前面多了一个“I”。而且它使您的库在面对变化时更加健壮。有多频繁?我不知道。在当前项目中,我们已经这样做了几次。每一次,调用代码都不需要改变。 - Greg Beech
@Greg - 对于声明类来说,这并不复杂。但对于内部类,在你编写类的用户时,通常需要再次创建一个列表,使用AddRange,然后才能使用List ForEach方法。直接返回List可以简化此过程。我很想知道您用哪个集合类替换了List<T>,假设这是您用作IList<T>的具体实现。 - softveda
为什么要使用List<T>.ForEach方法?它没有任何作用。您已经有了foreach循环,或者可以轻松编写一个扩展的ForEach方法。至于我们用什么替换它 - 其他具有不同性能特征的集合,例如当暴露类通常需要将集合作为字典访问时,使用KeyedCollection,而类的用户需要将其作为列表访问。将其公开为IList<T>使我们能够在不破坏任何消费者的情况下实现此目的。 - Greg Beech

7
作为一般规则,我会说不。
我之所以这样说并不是因为你正在执行一个TryGetX并返回一个带有out参数的bool。我认为这是不好的风格,因为你还返回了一个错误字符串。
Try应该只忽略一个特定的、常见的错误。其他问题可能仍然会抛出一个带有适当异常消息的异常。请记住,像这样的Try方法的目标是在你预期某种特定类型的故障比不发生更频繁时避免抛出异常的开销。
相反,你要找的是一对方法:
public static bool TryGetPolls( out List<Poll> polls );
public static List<Poll> GetPolls();

这样用户就可以做出适当的行动,GetPolls 可以在 TryGetPolls 的基础上实现。我假设你的静态性在上下文中是有意义的。


7

考虑返回:

  • 一个空集合
  • null

对我来说,多个输出参数是一种代码异味。该方法应该只做一件事。

考虑使用以下方式引发和处理错误消息:

throw new Exception("Something bad happened");
//OR
throw new SomethingBadHappenedException();

1
+1:还要考虑在已知情况下,如果满足特定条件将会失败的情况下实现自定义异常。 - Steven Evers
但是你应该抛出比 System.Exception 更具体的东西,以更精确地匹配错误类型。 - Ian G
谢谢Ian。这更多是作为一个指导方针,意思是应该抛出某种异常,而不是直接从代码贴文复制/粘贴。 :) - p.campbell

2
不,从我的角度来看,这是非常不好的风格。我会像这样写:
public static List<Poll> GetPolls();

如果调用失败,抛出异常并将错误消息放入异常中。这就是异常的作用,您的代码将变得更加清洁、易读和易于维护。

1

这取决于错误信息是什么。例如,如果由于数据库连接不可用等原因导致无法继续处理,则应像其他人提到的那样抛出异常。

但是,也可能只是想返回有关尝试的“元”信息,在这种情况下,您只需要一种方法来从单个方法调用返回多个信息。在这种情况下,建议创建一个PollResponse类,其中包含两个属性:List < Poll > Polls和string ErrorMessage。然后让您的方法返回一个PollResponse对象:

class PollResponse
{
    public List<Poll> Polls { get; }
    public string MetaInformation { get; }
}

在我看来,这和原始代码一样不符合惯用法。惯用的 C# 使用异常来报告错误,而不是带有错误消息的自定义对象。 - Greg Beech
3
这里的 Scott 技术可能类似于 System.Web.HttpResponse。我认为处理一组投票对象可能需要一个全新的对象来处理错误、语言、过期、用户可见性等问题,这并不是一个不公平的假设。 - Joseph Yaduvanshi
@Stuart - 我不是在屈服于同行的压力。在适当的情况下,这种方法没有任何问题。在请求“获取”有关过程中发生的事情时,返回元数据是完全合法的。 - Scott Whitlock
@Scott - 我完全同意,将额外的元数据与结果一起返回通常是有用的(我们经常这样做,例如具有扩展IEnumerable<T>并报告分页查询中可用项的总数的ISearchResult<T>接口)。我只是认为在这种特殊情况下,这不是用户想要的内容,他们试图拥有"无异常模型的错误处理"。我没有对此进行反对投票,因为我认为这既不是错误的答案也不是没有帮助的答案,我只是认为这不是适合所提出问题的正确方法。 - Greg Beech
@Scott - 给你一个赞...至少可以把你从“死亡”底部推上去 :-) - Greg Beech
显示剩余3条评论

1

编程指南建议尽量避免使用refout参数,除非它们是绝对必要的,因为它们会使 API 更难使用(不能再链接方法,开发者必须在调用方法之前声明所有变量)。

另外,返回错误代码或消息不是最佳实践,最佳实践是使用异常和异常处理进行错误报告,否则错误很容易被忽略,并且传递错误信息需要更多的工作,同时丢失有价值的信息,如堆栈跟踪或内部异常。

一种更好的声明方法是这样的。

public static List<Poll> GetPolls() ... 

并且在错误报告方面使用异常处理

try 
{
   var pols = GetPols();
   ...
} catch (DbException ex) {
   ... // handle exception providing info to the user or logging it.
}

1

不完全是这样的 - 我可以看到这个方法存在许多问题。

首先,该方法听起来像是通常期望它成功的方法;错误(无法连接到数据库、无法访问polls表等)很少见。在这种情况下,使用异常报告错误要合理得多。 Try...模式用于通常期望调用“失败”的情况 - 例如,在将字符串解析为整数时,字符串很可能是用户输入的,可能无效,因此需要有一种快速处理此类情况的方法 - 因此使用TryParse。但这里并非如此。

其次,您将错误报告为指示错误存在或不存在的bool值和一个字符串消息。那么调用者如何区分各种错误呢?他肯定不能匹配错误消息文本 - 那是一个实现细节,可能会更改,并且可以本地化。而且,“无法连接到数据库”之类的内容可能与“已连接到数据库,但显示“访问被拒绝””之间存在天壤之别。您的API没有提供良好的区分这些错误的方法。

总之:使用异常而不是`bool + out string`报告消息。 一旦您这样做,就可以将`List<Poll>`作为返回值使用,而无需`out`参数。当然,重命名该方法为`GetPolls`,因为`Try...`保留用于`bool + out`模式。

0

我会这样重新表述。

public static List<Poll> GetPolls()
{
    ...
}

如果无法检索到投票,则应该抛出异常(errorMessage),这样可以进行方法链接,比处理输出参数更方便。

如果运行FxCop,您需要将List更改为IList以使其正常工作。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接