我有一些代码,当它执行时,它会抛出NullReferenceException,说:

对象引用未设置为对象的实例。

这意味着什么,我可以做什么来修复这个错误?


当前回答

请注意,无论情况如何,.NET中的原因总是相同的:

您正在尝试使用值为Nothing/null的引用变量。当引用变量的值为Nothing/null时,这意味着它实际上没有保存对堆上存在的任何对象实例的引用。您要么从未向变量赋值,要么从未创建分配给变量的值的实例,要么手动将变量设置为Nothing/null,要么为您调用了将变量设置成Nothing/nnull的函数。

其他回答

另一种情况是将空对象转换为值类型。例如,下面的代码:

object o = null;
DateTime d = (DateTime)o;

它将在强制转换时引发NullReferenceException。在上面的示例中,这似乎很明显,但这可能发生在更“后期绑定”的复杂场景中,其中空对象是从您不拥有的某些代码返回的,例如,强制转换是由某些自动系统生成的。

其中一个示例是带有Calendar控件的简单ASP.NET绑定片段:

<asp:Calendar runat="server" SelectedDate="<%#Bind("Something")%>" />

这里,SelectedDate实际上是CalendarWebControl类型的DateTime类型的属性,绑定可以完全返回null。隐式ASP.NET生成器将创建一段与上述转换代码等效的代码。这将引发一个很难发现的NullReferenceException,因为它存在于ASP.NET生成的代码中,这些代码可以很好地编译。。。

另一种可能发生NullReferenceExceptions的情况是(不正确)使用as运算符:

class Book {
    public string Name { get; set; }
}
class Car { }

Car mycar = new Car();
Book mybook = mycar as Book;   // Incompatible conversion --> mybook = null

Console.WriteLine(mybook.Name);   // NullReferenceException

在这里,Book和Car是不兼容的类型;汽车不能转换成书。当此强制转换失败时,as返回null。在此之后使用mybook会导致NullReferenceException。

通常,应使用强制转换或,如下所示:

如果您希望类型转换总是成功的(即,您知道对象应该是什么),那么应该使用强制转换:

ComicBook cb = (ComicBook)specificBook;

如果您不确定该类型,但希望尝试将其用作特定类型,请将其用作:

ComicBook cb = specificBook as ComicBook;
if (cb != null) {
   // ...
}

你能怎么办?

这里有很多很好的答案来解释空引用是什么以及如何调试它。但是关于如何防止这个问题或者至少让它更容易被发现的问题却很少。

检查参数

例如,方法可以检查不同的参数以查看它们是否为空,并抛出ArgumentNullException,这显然是为此目的创建的异常。

ArgumentNullException的构造函数甚至将参数的名称和消息作为参数,以便您可以确切地告诉开发人员问题所在。

public void DoSomething(MyObject obj) {
    if(obj == null) 
    {
        throw new ArgumentNullException("obj", "Need a reference to obj.");
    }
}

使用工具

还有几个库可以提供帮助。例如,“Resharper”可以在编写代码时向您提供警告,尤其是当您使用它们的属性:NotNullAttribute时

在“Microsoft代码契约”中,您可以使用Contract.Requals(obj!=null)这样的语法,这为您提供了运行时和编译检查:引入代码契约。

还有“PostSharp”,它允许您只使用如下属性:

public void DoSometing([NotNull] obj)

通过这样做并使PostSharp成为构建过程的一部分,将在运行时检查obj是否为空。参见:PostSharp空检查

普通代码解决方案

或者,您可以始终使用简单的旧代码编写自己的方法。例如,这里有一个可以用来捕获空引用的结构。它是按照与Nullable<T>相同的概念建模的:

[System.Diagnostics.DebuggerNonUserCode]
public struct NotNull<T> where T: class
{
    private T _value;

    public T Value
    {
        get
        {
            if (_value == null)
            {
                throw new Exception("null value not allowed");
            }

            return _value;
        }
        set
        {
            if (value == null)
            {
                throw new Exception("null value not allowed.");
            }

            _value = value;
        }
    }

    public static implicit operator T(NotNull<T> notNullValue)
    {
        return notNullValue.Value;
    }

    public static implicit operator NotNull<T>(T value)
    {
        return new NotNull<T> { Value = value };
    }
}

您使用的方式与使用Nullable<T>的方式非常相似,但目的恰恰相反——不允许null。以下是一些示例:

NotNull<Person> person = null; // throws exception
NotNull<Person> person = new Person(); // OK
NotNull<Person> person = GetPerson(); // throws exception if GetPerson() returns null

NotNull<T>隐式转换为T和T,因此您可以在任何需要的地方使用它。例如,您可以将Person对象传递给采用NotNull<Person>的方法:

Person person = new Person { Name = "John" };
WriteName(person);

public static void WriteName(NotNull<Person> person)
{
    Console.WriteLine(person.Value.Name);
}

正如您在上面看到的,对于空值,您可以通过value属性访问基础值。或者,您可以使用显式或隐式转换,您可以看到以下返回值的示例:

Person person = GetPerson();

public static NotNull<Person> GetPerson()
{
    return new Person { Name = "John" };
}

或者,您甚至可以在方法通过执行强制转换仅返回T(在本例中为Person)时使用它。例如,以下代码与上面的代码类似:

Person person = (NotNull<Person>)GetPerson();

public static Person GetPerson()
{
    return new Person { Name = "John" };
}

结合扩展

将NotNull<T>与扩展方法相结合,您可以涵盖更多情况。下面是扩展方法的示例:

[System.Diagnostics.DebuggerNonUserCode]
public static class NotNullExtension
{
    public static T NotNull<T>(this T @this) where T: class
    {
        if (@this == null)
        {
            throw new Exception("null value not allowed");
        }

        return @this;
    }
}

下面是一个如何使用它的示例:

var person = GetPerson().NotNull();

github

为了便于参考,我在GitHub上提供了上述代码,您可以在以下位置找到:

https://github.com/luisperezphd/NotNull

相关语言功能

C#6.0引入了“空条件运算符”,这有点帮助。使用此功能,您可以引用嵌套对象,如果其中任何一个为空,则整个表达式返回空。

这减少了在某些情况下必须执行的空检查的数量。语法是在每个点前加一个问号。以以下代码为例:

var address = country?.State?.County?.City;

假设country是一个country类型的对象,该对象具有名为State等属性。如果country、State、County或City为空,则地址将变为空。因此,您只需检查地址是否正确。

这是一个很好的功能,但它提供的信息较少。这并不能明显看出4中的哪一个是空的。

像Nullable一样内置?

C#对Nullable<T>有一个很好的简写,你可以在类型后面加一个问号,比如so int?。

如果C#有类似于上面NotNull<T>结构的东西,并且有类似的速记,也许是感叹号(!),这样你就可以写类似于:public void WriteName(Person!Person)的东西了。

这基本上是一个Null引用异常。如Microsoft所述-

尝试访问值为空的类型的成员。

这是什么意思?

这意味着,如果任何成员不具有任何价值,而我们让该成员执行某项任务,那么系统无疑会抛出一条消息,并表示-

“嘿,等等,该成员没有值,因此无法执行您正在移交的任务。”

异常本身表示正在引用某个对象,但未设置其值。因此,这表示它只在使用引用类型时发生,因为Value类型不可为null。

如果使用Value类型成员,则不会发生NullReferenceException。

class Program
{
    static void Main(string[] args)
    {
        string str = null;
        Console.WriteLine(str.Length);
        Console.ReadLine();
    }
}

上面的代码显示了分配了空值的简单字符串。

现在,当我尝试打印字符串str的长度时,我确实收到了“System.NullReferenceException”类型的未处理异常消息,因为成员str指向null,并且不能有任何长度的null。

当我们忘记实例化引用类型时,也会出现“NullReferenceException”。

假设我有一个类和成员方法。我没有实例化我的类,只是命名了我的类。现在,如果我尝试使用该方法,编译器将抛出错误或发出警告(取决于编译器)。

class Program
{
    static void Main(string[] args)
    {
        MyClass1 obj;
        obj.foo();  // Use of unassigned local variable 'obj'
    }
}

public class MyClass1
{
    internal void foo()
    {
        Console.WriteLine("Hello from foo");
    }
}

上述代码的编译器引发一个错误,即变量obj未赋值,这意味着我们的变量有空值或没有值。上述代码的编译器引发一个错误,即变量obj未赋值,这意味着我们的变量有空值或没有值。

为什么会发生这种情况?

NullReferenceException是由于我们没有检查对象的值而导致的。在代码开发中,我们经常不检查对象值。当我们忘记实例化对象时,也会出现这种情况。使用可以返回或设置空值的方法、财产、集合等也可能是此异常的原因。

如何避免?

有多种方式和方法可以避免这一著名的例外:

显式检查:我们应该坚持检查对象、财产、方法、数组和集合是否为null的传统。这可以使用if-else-if-else等条件语句简单地实现。异常处理:管理此异常的重要方法之一。使用简单的try-catch finally块,我们可以控制这个异常,并维护它的日志。这在应用程序处于生产阶段时非常有用。Null操作符:在为对象、变量、财产和字段设置值时,也可以方便地使用Null合并操作符和Null条件操作符。调试器:对于开发人员来说,我们有调试的利器。如果我们在开发过程中遇到NullReferenceException,我们可以使用调试器找到异常的源。内置方法:GetValueOrDefault()、IsNullOrWhiteSpace()和IsNullorEmpty()等系统方法检查空值,如果存在空值,则分配默认值。

这里已经有很多好的答案。你也可以在我的博客上查看更详细的描述和示例。

希望这也有帮助!

关于“我该怎么办”的问题,可以有很多答案。

在开发时防止这种错误情况的一种更“正式”的方法是在代码中应用契约设计。这意味着在开发时,需要在系统上设置类不变量,和/或甚至函数/方法前置条件和后置条件。

简而言之,类不变量确保类中有一些约束在正常使用中不会被违反(因此,类不会处于不一致的状态)。前置条件意味着作为函数/方法输入的数据必须遵循某些约束集,并且永远不会违反这些约束,后置条件意味着函数/方法输出必须再次遵循这些约束集,而永远不会违反它们。在执行无错误程序期间,不应违反合同条件,因此,在调试模式下实际检查合同设计,而在发布版本中禁用,以最大化开发的系统性能。

通过这种方式,可以避免由于违反约束集而导致的NullReferenceException情况。例如,如果在类中使用对象属性X,然后尝试调用其方法之一,并且X具有空值,则这将导致NullReferenceException:

public X { get; set; }

public void InvokeX()
{
    X.DoSomething(); // if X value is null, you will get a NullReferenceException
}

但是,如果您将“属性X永远不能有空值”设置为方法前提条件,则可以防止前面描述的情况:

//Using code contracts:
[ContractInvariantMethod]
protected void ObjectInvariant() 
{
    Contract.Invariant(X != null);
    //...
}

因此,.NET应用程序存在代码合同项目。

或者,可以使用断言应用契约设计。

更新:值得一提的是,这个词是Bertrand Meyer在设计Eiffel编程语言时创造的。