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

Posted

技术标签:

【中文标题】为啥在构造函数中抛出异常会导致空引用?【英文标题】:Why throwing exception in constructor results in a null reference?为什么在构造函数中抛出异常会导致空引用? 【发布时间】:2012-04-24 13:57:57 【问题描述】:

为什么在构造函数中抛出异常会导致空引用? 例如,如果我们运行下面的代码,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;
  


【问题讨论】:

【参考方案1】:

构造函数永远不会完成,因此赋值永远不会发生。不是从构造函数返回 null (或者有一个“空对象” - 没有这样的概念)。只是你从来没有给teacher分配一个新的值,所以它保留了之前的值。

例如,如果您使用:

Teacher teacher = new Teacher("This is valid", new Student());
Student st = new Student();
try

    teacher = new Teacher("", st);

catch (... etc ...)

...那么你仍然会有“这是有效的”老师。 name 变量仍然不会在 Teacher 对象中被赋值,因为您的 Teacher 构造函数缺少如下行:

this.name = name;

【讨论】:

感谢您的精彩解释,我将问题中的“空对象”编辑为“空引用”。 很好的解释,你也只是证明了 C# 中未初始化的对象总是持有«null»。我开始怀疑这一点,因为当我尝试在 Visual Studio 中使用一个在特定条件下可能未初始化的对象时,无论如何检查了它是否为“null”,然后使用,编译器显示一个关于未初始化变量的错误。在我用«null»显式初始化对象后,错误消失了。谢谢你,现在我知道这只是 Visual Studio 的一个错误。 @Hi-Angel:不,这不是错误。这是 field 和局部变量之间的区别。一个字段有一个默认值,并且可以在没有设置的情况下使用 - 一个局部变量不能被读取,直到它被明确分配。【参考方案2】:

因为您正在检查引用

  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 对象,因为它还没有被构造。这可能会导致未定义的行为。

【讨论】:

【参考方案4】:

如果Foo 是引用类型,则语句Foo = new FooType(); 将构造一个对象,然后,在构造函数完成后,将引用存储到Foo。如果构造函数抛出异常,将跳过将引用存储到 Foo 的代码,而不会写入 Foo

在以下情况下:

类似上述的语句出现在 try/catch 块中 无需事先编写Foo 即可访问该声明。 Foocatch 块周围的上下文中定义的局部变量。 从 catch 开始执行可能会到达一条读取为 Foo 的语句,而没有在 catch 之后写入。

编译器将假定后者读取Foo 的尝试可以在没有写入Foo 的情况下执行,并且在这种情况下将拒绝编译。编译器将允许 Foo 在没有被写入的情况下被读取,但是,如果:

Foo是类字段,或者类字段中存储的结构字段,类字段中存储的结构字段中存储的结构字段等。 Foo 作为 out 参数传递给不存储任何内容的方法(用 C# 以外的语言编写),并且只有在方法正常返回时才能访问读取 foo 的语句而不是通过异常。

在前一种情况下,Foo 的定义值为null。在后一种情况下,Foo 的值在方法执行期间第一次创建时可能为 null;如果在循环中重新创建,它可能包含null 或在上次创建后写入它的最后一个值;该标准并未具体说明在这种情况下会发生什么。

请注意,如果 FooType 有任何类似于普通构造函数的东西,Foo = new FooType(); 永远不会导致 Foo 之前没有变为 null。如果语句正常完成,Foo 将持有对精确类型 FooType 的实例的引用,该引用以前在 Universe 中的任何地方都不存在;如果它抛出异常,它不会以任何方式影响Foo

【讨论】:

【参考方案5】:

你在赋值后抛出异常 'student.teacher = this; //这一行被执行 if ( name.Length

所以teacher的值为null(在构造函数完成之前抛出异常),而st.teacher不是!

【讨论】:

【参考方案6】:

构造函数的主要工作是初始化对象。如果初始化本身存在异常,则没有正确初始化的对象没有意义。 因此,从构造函数中抛出异常会导致 null 对象。

【讨论】:

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

以上是关于为啥在构造函数中抛出异常会导致空引用?的主要内容,如果未能解决你的问题,请参考以下文章

为啥在析构函数中抛出异常时不调用重载删除?

不要在构造函数中抛出异常

Java:构造函数中抛出异常,我的对象仍然可以创建吗?

为啥这个 Linq 方法会抛出空引用异常

从匿名内部类的构造函数中抛出异常

为啥在 ios/cordova 中抛出这个异常?