C#中的构造函数参数验证-最佳做法


34

构造函数参数验证的最佳实践是什么?

假设有一个简单的C#:

public class MyClass
{
    public MyClass(string text)
    {
        if (String.IsNullOrEmpty(text))
            throw new ArgumentException("Text cannot be empty");

        // continue with normal construction
    }
}

抛出异常是否可以接受?

我遇到的替代方法是在实例化之前进行预验证:

public class CallingClass
{
    public MyClass MakeMyClass(string text)
    {
        if (String.IsNullOrEmpty(text))
        {
            MessageBox.Show("Text cannot be empty");
            return null;
        }
        else
        {
            return new MyClass(text);
        }
    }
}

10
“可以抛出异常吗?” 有什么选择?
S.Lott

10
@ S.Lott:什么都不做。设置默认值。将消息打印到控制台/日志文件。按门铃。闪烁一盏灯。
FrustratedWithFormsDesigner

3
@FrustratedWithFormsDesigner:“什么都不做?” 如何满足“文本不为空”的约束?“设置默认值”?文本参数值将如何使用?其他想法很好,但是这两个想法会导致对象处于违反此类约束的状态。
S.Lott

1
@ S.Lott由于害怕重复自己,我很乐意采用其他我不知道的方法。我相信我并不是所有人都知道,我肯定会知道很多。
MPelletier

3
@MPelletier,提前验证参数将最终违反DRY。(不要重复自己),因为您需要将该预验证复制到任何试图实例化此类的代码中。
CaffGeek

Answers:


26

我倾向于在构造函数中执行所有验证。这是必须的,因为我几乎总是创建不可变的对象。对于您的特定情况,我认为这是可以接受的。

if (string.IsNullOrEmpty(text))
    throw new ArgumentException("message", "text");

如果您使用的是.NET 4,则可以执行此操作。当然,这取决于您是否认为仅包含空格的字符串是无效的。

if (string.IsNullOrWhiteSpace(text))
    throw new ArgumentException("message", "text");

我不确定他的“具体案例”是否足够具体到可以提供这样的具体建议。我们不知道在这种情况下抛出异常还是简单地允许对象存在仍然有意义
FrustratedWithFormsDesigner

@FrustratedWithFormsDesigner-我认为您为此否决了我吗?当然,我在推断是正确的,但是很明显,如果输入文本为空,则该理论对象必须无效。Init当异常同样可以正常工作并强制对象始终保持有效状态时,您是否真的要调用以接收某种错误代码?
ChaosPandion'2

2
我倾向于将第一种情况分为两个单独的例外:一个例外情况测试是否为空并抛出an ArgumentNullException,另一个例外情况测试为空并抛出an ArgumentException。如果调用者希望在其异常处理中进行操作,则可以使他们更加挑剔。
Jesse C. Slicer

1
@Jesse-我已经使用了该技术,但是我仍然对它的整体实用性持怀疑态度。
ChaosPandion'2

3
+1为不可变的物件!我也会尽可能使用它们(我个人几乎总是这样)。

22

许多人指出,构造函数不应抛出异常。例如,此页面上的KyleG 就是这样做的。老实说,我想不出为什么。

在C ++中,从构造函数中引发异常是一个坏主意,因为它会给您分配的内存包含未引用的未初始化对象(即,这是经典的内存泄漏)。这可能是耻辱的源头-一群老派的C ++开发人员将他们的C#学习提高了一半,只是将他们从C ++中学到的知识应用到了它。相反,在Objective-C中,Apple将分配步骤与初始化步骤分开,因此使用该语言的构造函数可能会引发异常。

C#无法从对构造函数的不成功调用中泄漏内存。甚至.NET框架中的某些类都将在其构造函数中引发异常。


您将在C ++注释中折腾一些羽毛。:)
ChaosPandion,2011年

5
嗯,C ++是一门很棒的语言,但是我的一个讨厌的人是那些会很好地学习一种语言然后一直这样写作的人。C#不是C ++。
蚂蚁

10
在C#中从ctor引发异常仍然很危险,因为ctor可能已经分配了不受管理的资源,这些资源将永远不会被处置。而且,终结处理程序需要编写成可防御未完成构造的对象被终结的情况。但是这些情况相对较少。C#开发中心上即将发表的一篇文章将介绍这些场景以及如何围绕它们进行防御性编码,因此请留意该空间以获取详细信息。
埃里克·利珀特

6
嗯?如果无法构造对象,则只能在c ++中从ctor引发异常,并且没有理由这样会导致内存泄漏(尽管显然是由引起的)。
jk。

@jk:嗯,你是对的!尽管似乎替代方法是将不良对象标记为“僵尸”,但STL显然代替了抛出异常:yosefk.com/c++fqa/exceptions.html#fqa-17.2 Objective-C的解决方案更为简洁。
蚂蚁

12

抛出异常IFF,就其语义使用而言,无法将类置于一致状态。否则不要。切勿让对象以不一致的状态存在。这包括不提供完整的构造函数(例如在实际完全构建对象之前有一个空的构造函数+ initialize())...请说不!

紧要关头,每个人都在做。前几天,我在狭窄范围内对一个使用范围非常狭窄的对象进行了此操作。在未来的某一天,我或其他人可能会在实践中为此单支付费用。

我应该注意,“构造函数”是指客户端调用以构建对象的东西。除了名称为“ Constructor”的实际构造之外,这也很容易。例如,在C ++中这样的事情不会违反IMNSHO原理:

struct funky_object
{
  ...
private:
  funky_object();
  bool initialize(std::string);

  friend boost::optional<funky_object> build_funky(std::string);
};
boost::optional<funky_object> build_funky(std::string str)
{
  funky_object fo;
  if (fo.initialize(str)) return fo;
  return boost::optional<funky_object>();
}

因为创建a的唯一方法funky_object是通过调用build_funky永远不允许无效对象存在的原理保持不变,即使实际的“构造函数”并未完成工作。

尽管获得了可疑的收益(甚至可能是损失),但还有很多额外的工作要做。我还是更喜欢例外路线。


9

在这种情况下,我将使用工厂方法。基本上,将您的类设置为仅具有私有构造函数,并具有一个返回对象实例的工厂方法。如果初始参数无效,则只需返回null并让调用代码确定要执行的操作。

public class MyClass
{
    private MyClass(string text)
    {
        //normal construction
    }

    public static MyClass MakeMyClass(string text)
    {
        if (String.IsNullOrEmpty(text))
            return null;
        else
            return new MyClass(text);
    }
}
public class CallingClass
{
    public MyClass MakeMyClass(string text)
    {
        var cls = MyClass.MakeMyClass(text);
        if(cls == null)
             //show messagebox or throw exception
        return cls;
    }
}

除非情况特殊,否则不要抛出异常。我认为在这种情况下,可以轻松传递空值。如果真是这样,使用此模式将避免在保持MyClass状态有效的同时产生异常和性能损失。


1
我看不出优势。为什么将工厂的结果测试为null而不是在某个地方可以捕获构造函数异常的try-catch更好?您的方法似乎是“忽略异常,返回以显式检查方法的返回值是否有错误”。我们很久以前就接受了例外,通常来说,例外通常优于必须显式测试每个方法的返回值是否存在错误,因此您的建议似乎令人怀疑。对于您为什么认为一般规则在这里不适用,我想提出一些理由。
DW

我们从来没有接受过异常优于返回码的情况,如果抛出过多的异常会对性能造成巨大影响。但是,如果分配了不受管的内容,则从构造函数引发异常将泄漏内存,即使在.NET中也是如此。您必须在终结器中做很多工作才能弥补这种情况。无论如何,工厂在这里是一个好主意,并且在TBH中,它应该引发异常而不是返回null。假设您只创建了几个“坏”类,那么最好进行工厂抛出。
gbjbaanb

2
  • 构造函数不应有任何副作用。
    • 除了私有字段初始化外,任何其他事情都应视为副作用。
    • 具有副作用的构造函数破坏了单一职责原则(SRP),并且与面向对象编程(OOP)的精神背道而驰。
  • 构造函数应该轻巧,并且永不失败。
    • 例如,当我在构造函数中看到try-catch块时,我总是发抖。构造函数不应引发异常或记录错误。

一个人可以合理地质疑这些准则,然后说:“但是我不遵守这些规则,我的代码可以正常工作!” 为此,我会回答:“那可能是正确的,直到事实并非如此。”

  • 构造函数内部的异常和错误是非常意外的。除非被告知这样做,否则将来的程序员将不会倾向于将这些构造函数调用包含在防御性代码中。
  • 如果生产中有任何失败,则可能难以解析生成的堆栈跟踪。堆栈跟踪的顶部可能指向构造函数调用,但是在构造函数中发生了很多事情,并且可能没有指向失败的实际LOC。
    • 在这种情况下,我已经解析了许多.NET堆栈跟踪。

0

这取决于MyClass在做什么。如果MyClass实际上是数据存储库类,而参数文本是连接字符串,则最佳实践是引发ArgumentException。但是,如果MyClass是StringBuilder类(例如),则可以将其留空。

因此,这取决于该方法对参数文本的重要性-对象是否具有空值或空白值才有意义?


2
我认为这个问题暗示着该类实际上要求字符串不为空。在您的StringBuilder示例中,情况并非如此。
塞尔吉奥·阿科斯塔

那么答案必须是肯定的-抛出异常是可以接受的。为避免尝试/捕获此错误,可以使用工厂模式为您处理对象创建,其中包括空字符串大小写。
史蒂文·斯特里加

0

我的偏好是设置一个默认值,但是我知道Java具有“ Apache Commons”库,它可以执行类似的操作,而且我认为这也是一个不错的主意。如果无效值会使对象处于不可用状态,我不会抛出异常。字符串不是一个很好的例子,但是如果是穷人的DI呢?如果null传递值代替ICustomerRepository接口(例如),您将无法操作。我想说,在这种情况下,抛出异常是处理事情的正确方法。


-1

当我以前在c ++中工作时,我不曾在构造函数中引发异常,因为它可能导致内存泄漏问题,但我却很难学到。使用c ++的任何人都知道内存泄漏有多么困难和问题。

但是,如果您使用的是c#/ Java,则不会出现此问题,因为垃圾收集器将收集内存。如果您使用的是C#,我认为最好使用property来提供一种良好且一致的方式来确保数据约束得到保证。

public class MyClass
{
    private string _text;
    public string Text 
    {
        get
        {
            return _text;
        } 
        set
        {
            if (string.IsNullOrWhiteSpace(value))
                throw new ArgumentException("Text cannot be empty");
            _text = value;
        } 
    }

    public MyClass(string text)
    {
        Text = text;
        // continue with normal construction
    }
}

-3

抛出异常对于您的班级用户来说将是真正的痛苦。如果验证是一个问题,那么我通常将创建对象分为两个步骤。

1-创建实例

2-调用带有返回结果的Initialize方法。

要么

调用为您创建可以进行验证的类的方法/函数。

要么

有一个isValid标志,我并不特别喜欢,但有些人喜欢。


3
@Dunk,那是错误的。
CaffGeek

4
@Chad,这是您的意见,但我的意见没有错。它与您的不同。我发现try-catch代码很难阅读,而且与其他传统方法相比,当人们使用try-catch进行琐碎的错误处理时,我发现产生的错误也更多。那是我的经验,这没错。就是这样。
Dunk

5
要点是,在我调用构造函数后,如果由于输入无效而没有创建该构造函数,则该异常为EXCEPTION。如果我只是创建对象并设置标语,则该应用程序的数据现在处于不一致/无效状态isValid = false。创建一个类是否有效之后,必须进行测试,这是可怕的,容易出错的设计。而且,您说过,有一个构造函数,然后调用initialize ...如果我不调用initialize怎么办?然后怎样呢?如果Initialize收到不良数据怎么办,我现在可以抛出异常吗?您的课程应该很容易使用。
CaffGeek 2011年

5
@Dunk,如果您发现难以使用的异常,则表示您错误地使用了它们。简而言之,错误的输入被提供给构造函数。例外。除非已修复,否则该应用程序不应继续运行。期。如果是这样,您将面临更严重的问题,即错误的数据。如果您不知道如何处理该异常,那么您就不让它冒泡地扩大调用堆栈,直到可以对其进行处理为止,即使这意味着仅记录日志并停止执行。简而言之,您无需处理异常或对其进行测试。他们只是工作。
CaffGeek

3
抱歉,这是一个太多的反模式。
MPelletier
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.