处理C#构造函数中损坏的输入数据最合适的方法是什么

本文关键字:是什么 方法 数据 输入 构造函数 损坏 处理 | 更新日期: 2023-09-27 17:58:14

我正在从文件中读取数据,并基于这些数据创建对象。数据格式不在我的控制之下,偶尔会损坏。在C#中构造对象时,处理这些错误最合适的方法是什么?

在其他编程语言中,我返回了一个null,但这似乎不是C#的选项。

我已经设法找到了以下选项,但我很感激更有经验的C#程序员的建议:

选项1.读取构造函数内的文件,并在源数据损坏时引发异常:

try
{
    obj = Constructor(sourceFile);
    ... process object ...
}
catch (IOException ex)
{
    ...
}

选项2.创建对象,然后使用一种方法从源文件读取数据:

obj = Constructor();
obj.ReadData(sourceFile);
if (obj.IsValid)
{
    ... process object ...
}

或者可能在出现错误时抛出异常:

obj = Constructor();
try
{
    obj.Read(sourceFile);
    ... process object ...
}
catch
{
    ...
}

选项3.使用静态TryParse方法创建对象:

if (ObjClass.TryParse(sourceFile, out obj))
{
    ... process object ...
}

如果是,我是否应该使用选项1在内部实现选项3?

public static bool TryParse(FileStream sourceFile, out ObjClass obj)
{   
    try
    {
        obj = Constructor(sourceFile);
        return true;
    }
    catch (IOException ex)
        return false;
}

处理C#构造函数中损坏的输入数据最合适的方法是什么

我会按照选项3):

class ObjectClass
{
    protected ObjectClass(...constructor parameters your object depends on...)
    {
    }
    public static ObjectClass CreateFromFile(FileStream sourceFile)
    {
        .. parse source file
        if (parseOk)
        {
            return new ObjectClass(my, constructor, parameters);
        }
        return null;
    }
}

然后像这样使用:

ObjClass.CreateFromFile(sourcefile);

通常,构造函数应该将本质上定义类的所有属性作为参数。进行重量级计算(如解析文件)最好留给工厂方法,因为通常不希望构造函数执行复杂且可能长时间运行的任务。

更新:正如评论中提到的,更好的模式是:

    public static ObjectClass CreateFromFile(FileStream sourceFile)
    {
        .. parse source file
        if (!parseOk)
        {
            throw new ParseException(parseErrorDescription);
        }
        return new ObjectClass(my, constructor, parameters);
    }
    public static bool TryCreateFromFile(FileStream sourceFile, out ObjectClass obj)
    {
        obj = null;
        .. parse source file
        if (!parseOk)
        {
            return false;
        }
        obj = new ObjectClass(my, constructor, parameters);
        return true;
    }

我不会在构造函数中放入任何可能引发异常的内容,除非出现真正的错误
如果您的构造函数有一个可能的返回值,而不是一个有效的对象,您应该封装它。

最安全的方法可能是创建一个工厂方法(类中的公共静态函数,该函数接受文件引用并返回该类的新实例或null)。此方法应首先验证文件及其数据,然后再创建一个新对象。

如果文件数据的结构很简单,可以先将其加载到某个局部变量中,然后用这些数据构造对象。否则,您仍然可以在工厂方法内部决定是否要尝试/捕获构造或使用上面提到的任何其他点。

选项#1和#3都是不错的选择,在.Net框架中很常见。为同一类型提供两者也是常见的。考虑Int32.TryParseInt32.Parse。同时提供这两种功能可以为开发人员提供更多的灵活性,同时又不会降低类型的完整性。

我强烈建议你避开#2。这种模式迫使类型作者和类型使用者处理处于多种状态的类型实例

  • 已构造但未完全初始化
  • 已初始化且有效
  • 已初始化且无效

这给每个消费者带来了处理处于所有不同状态的实例的负担(即使响应只是抛出)。此外,它还强制消费者采用非标准模式。开发人员必须了解您的类型是特殊的,需要构造并初始化它。它违背了在.Net.中创建对象的标准方式

注意#3,不过我会用一些不同的方法。异常表单应该按照try表单来实现。这是向用户提供这两个选项时的标准模式。考虑以下模式

class MyType { 
  struct ParsedData { 
    // Data from the file
  }
  public MyType(string filePath) : this(Parse(filePath)) { 
    // The Parse method will throw here if the data is invalid
  }
  private MyType(ParsedData data) {
    // Operate on the valid data.  This doesn't throw since the errors
    // have been rooted out already in TryParseFile
  }
  public static bool TryParse(string filePath, out MyType obj) {
    ParsedData data;
    if (!TryParseFile(filePath, out data)) {
      obj = null;
      return false;
    }
    obj = new MyType(data);
    return true;
  }
  private static ParsedData Parse(string filePath) {
    ParsedData data;
    if (!TryParseFile(filePath, out data)) {
      throw new Exception(...);
    }
    return data;
  }
  private static bool TryParseFile(string filePath, out ParsedData data) {
    // Parse the file and implement error detection logic here
  }
}

来自Microsoft Constructor Design Guidelines(MSDN),

如果合适,请从实例构造函数中抛出异常。

构造函数应该像任何方法一样抛出和处理异常。具体来说,构造函数不应该捕获和隐藏它无法处理的任何异常。


工厂方法不是解决此问题的正确方法。参见施工人员与工厂方法

来自框架设计指南:可重用.NET库的惯例、习语和模式

5.3建造商设计

如果所需操作的语义不会直接映射到构造新实例的,或者如果遵循构造函数设计指南感觉不自然。

如果合适,请从实例构造函数中抛出异常。


.NET BCL实现确实从构造函数抛出异常

例如,列表构造函数(Int32)在列表的容量参数为负数时抛出ArgumentOutOfRangeException。

var myList = new List<int>(-1); // throws ArgumentOutOfRangeException

类似地,构造函数在读取文件时应该抛出适当类型的异常。例如,如果文件在指定位置不存在,它可能会抛出FileNotFoundException,等等。


更多信息

  • 代码合同
  • 从.Net中的构造函数引发异常
  • 在构造函数中引发ArgumentNullException
  • C#中的构造函数参数验证-最佳实践

所有这些解决方案都有效,但正如您所说,C#不允许从构造函数返回null。您要么得到对象,要么得到异常。由于这是C#的发展方向,我不会选择选项3,因为这只是模仿了你正在谈论的其他语言。

很多人[edit],其中包括Martin,正如我在他的回答中所读到的:)[/edit]认为保持构造函数干净小巧是件好事。我对此不太确定。如果没有这些数据,您的对象就没有用处,那么您也可以在构造函数中读取数据。如果你想构造对象,设置一些选项,然后读取数据(尤其是在读取失败时可以重试),那么单独的方法也可以。因此,选择2也是一个很好的可能性。也许更好,主要取决于口味。

所以,只要你不选择3个,就选择你最舒服的一个。:)