为什么在构造函数中抛出异常会导致空引用?

21
为什么在构造函数中抛出异常会导致空引用? 例如,如果我们运行下面的代码,则teacher的值为null,而st.teacher不是(创建了一个Teacher对象)。为什么?
using System;

namespace ConsoleApplication1
{
  class Program
  {
    static void Main( string[] args )
    {
      Test();
    }

    private static void Test()
    {
      Teacher teacher = null;
      Student st = new Student();
      try
      {
        teacher = new Teacher( "", st );
      }
      catch ( Exception e )
      {
        Console.WriteLine( e.Message );
      }
      Console.WriteLine( ( teacher == null ) );  // output True
      Console.WriteLine( ( st.teacher == null ) );  // output False
    }
  }

  class Teacher
  {
    public string name;
    public Teacher( string name, Student student )
    {
      student.teacher = this;
      if ( name.Length < 5 )
        throw new ArgumentException( "Name must be at least 5 characters long." );
    }
  }

  class Student
  {
    public Teacher teacher;
  }

}
6个回答

40
构造函数永远不会完成,因此赋值也永远不会发生。并不是从构造函数返回了null(或者有一个“null对象”- 没有这样的概念),只是你从未给teacher赋予新的值,所以它保留了先前的值。
例如,如果你使用:
Teacher teacher = new Teacher("This is valid", new Student());
Student st = new Student();
try
{
    teacher = new Teacher("", st);
}
catch (... etc ...)

如果你这么做了,你仍然会有一个“这是有效的”老师。然而在那个Teacher对象中,name变量仍然不会被赋值,因为你的Teacher构造函数缺少这样一行代码:

this.name = name;

感谢您的清晰解释,我已将问题中的“null对象”编辑为“null引用”。 - Setyo N
很好的解释,同时你刚刚证明了在C#中未初始化的对象始终保持为“null”。我开始对此产生怀疑,因为当我尝试在Visual Studio中使用一个可能未初始化的对象时,在某些条件下它被检查为“null”,然后被使用,编译器显示了一个有关未初始化变量的错误。在我显式地将对象初始化为“null”之后,错误消失了。感谢你,现在我知道这只是Visual Studio的一个错误。 - Hi-Angel
5
不,这不是一个bug。这是“field”和本地变量之间的区别。字段有默认值,并且可以在没有被设置的情况下使用 - 本地变量不能在确定赋值之前读取。 - Jon Skeet

14
因为你正在检查参考文献。
  try
  {
    teacher = new Teacher( "", st ); //this line raises an exception 
                                     // so teacher REMAINS NULL. 
                                     // it's NOT ASSIGNED to NULL, 
                                     // but just NOT initialized. That is.
  }
  catch ( Exception e )
  {
    Console.WriteLine( e.Message );
  }

但是

public Teacher( string name, Student student )
{
  student.teacher = this;  //st.Teacher is assigned BEFORE exception raised.
  if ( name.Length < 5 )
    throw new ArgumentException( "Name must be at least 5 characters long." );
}

3
当你在构造函数中抛出异常时,会中断对象的构建。因此,它永远不会完成,因此没有对象可以返回。实际上,赋值运算符(teacher = new Teacher(“”,st);)从未执行,因为异常打破了调用堆栈。
而Teacher构造函数仍然将对自身的引用(正在构造的对象)写入Student对象的属性中。但是,您不应尝试在此之后使用此Teacher对象,因为它尚未构建。这可能导致未定义的行为。

1
如果 Foo 是引用类型,则语句 Foo = new FooType(); 将构造一个对象,然后在构造函数完成后将一个引用存储到 Foo 中。如果构造函数抛出异常,则将跳过将引用存储到 Foo 中的代码,而没有写入 Foo
在以下情况下:
  • 类似上述语句出现在try / catch块中
  • 可以在没有先前写入 Foo 的情况下到达该语句。
  • Foo 是定义在包围 catch 块的本地变量。
  • 可能会从 catch 开始执行,达到一个读取 Foo 的语句,而在 catch 之后没有写入它。
编译器将假定尝试后者读取 Foo 时可以在没有写入 Foo 的情况下执行,并且将在这种情况下拒绝编译。但是,如果满足以下条件,编译器将允许读取 Foo 而不必写入它:
  • Foo是一个类字段,或者是存储在类字段中的结构体的字段,是存储在存储着结构体字段的结构体字段中的字段,等等。
  • Foo作为out参数传递给了一个方法(用C#以外的语言编写),该方法不会对其进行任何存储,并且读取foo的语句只有在该方法正常返回时才能执行到。

在前一种情况下,Foo将具有已定义的null值。在后一种情况下,在方法执行期间第一次创建Foo时,它的值可能为null;如果在循环内重新创建,它可能包含null或最后一次创建后写入到其中的值;标准没有明确规定在这种情况下会发生什么。

请注意,如果FooType有类似于普通构造函数的内容,那么Foo = new FooType();语句将不会使Foo在之前为 null 的情况下变为null。如果该语句正常执行完成,则Foo将持有对类型为FooType的实例的引用,在宇宙中以前不存在任何引用;如果它抛出异常,将不会以任何方式影响Foo

0

你在赋值后抛出了异常 'student.teacher = this; //这行代码已经执行 if ( name.Length < 5 ) //这里进行了检查,在指定的情况下为真 throw new ArgumentException( "Name must be at least 5 characters long." );//BAM : 异常在此处被抛出。'

因此,teacher的值为空(因为在构造函数完成之前抛出了异常),而st.teacher的值不为空!


-1
构造函数的主要工作是初始化对象。如果在初始化过程中出现异常,那么拥有未正确初始化的对象就没有意义了。 因此,从构造函数抛出异常会导致空对象。

这是不正确的;正如其他答案所指出的那样,它根本没有产生任何结果。 - Asherah

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