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

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

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


当前回答

你能怎么办?

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

检查参数

例如,方法可以检查不同的参数以查看它们是否为空,并抛出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)的东西了。

其他回答

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

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生成的代码中,这些代码可以很好地编译。。。

原因是什么?

要旨

您正在尝试使用空值(或VB.NET中的Nothing)。这意味着您要么将其设置为空值,要么从未将其设置任何值。

和其他任何东西一样,null也会被传递。如果在方法“A”中为空,则可能是方法“B”将空传递给了方法“A)。

null可以有不同的含义:

未初始化的对象变量,因此不指向任何对象。在这种情况下,如果访问此类对象的成员,则会导致NullReferenceException。开发人员有意使用null来表示没有可用的有意义的值。注意,C#具有变量的可空数据类型的概念(比如数据库表可以有可空字段)-您可以为它们赋值null,以表示其中没有存储值,例如int?a=空;(这是Nullable<int>a=null;的快捷方式),其中问号表示允许在变量a中存储null。您可以使用if(a.HasValue){…}或if(a==null){..}进行检查。与此示例类似,Nullable变量允许显式地通过.value访问值,也可以通过a正常访问值。请注意,如果a为null,则通过.Value访问它会引发InvalidOperationException而不是NullReferenceException-您应该事先进行检查,即如果您有另一个不可为null的变量int b;那么您应该执行if(a.HasValue){b=a.Value;}或更短的赋值,如果(a!=null){b=a;}。

本文的其余部分将更详细地介绍许多程序员经常犯的错误,这些错误可能会导致NullReferenceException。

更具体地说

引发NullReferenceException的运行时总是意味着相同的事情:您正在尝试使用引用,但该引用未初始化(或者它已初始化,但不再初始化)。

这意味着引用为空,您不能通过空引用访问成员(如方法)。最简单的情况:

string foo = null;
foo.ToUpper();

这将在第二行引发NullReferenceException,因为无法对指向null的字符串引用调用实例方法ToUpper()。

调试

如何查找NullReferenceException的源?除了查看异常本身(它将被准确地抛出在发生异常的位置)之外,Visual Studio中的一般调试规则也适用:放置策略断点并检查变量,方法是将鼠标悬停在变量名称上,打开(快速)观察窗口,或使用各种调试面板(如Locals和Autos)。

如果要查找引用的设置位置,请右键单击其名称并选择“查找所有引用”。然后,您可以在每个找到的位置放置一个断点,并在附加调试器的情况下运行程序。每当调试器在这样的断点上中断时,您都需要确定引用是否为非空,检查变量,并验证它是否指向您期望的实例。

通过以这种方式遵循程序流程,您可以找到实例不应为空的位置,以及为什么未正确设置。

示例

可以引发异常的一些常见情况:

通用的

ref1.ref2.ref3.member

如果ref1或ref2或ref3为空,则会得到NullReferenceException。如果要解决此问题,请通过将表达式重写为更简单的等效表达式来找出哪个为空:

var r1 = ref1;
var r2 = r1.ref2;
var r3 = r2.ref3;
r3.member

具体而言,在HttpContext.Current.User.Identity.Name中,HttpContext.CCurrent可以为null,User属性可以为null或Identity属性可以为空。

间接的

public class Person 
{
    public int Age { get; set; }
}
public class Book 
{
    public Person Author { get; set; }
}
public class Example 
{
    public void Foo() 
    {
        Book b1 = new Book();
        int authorAge = b1.Author.Age; // You never initialized the Author property.
                                       // there is no Person to get an Age from.
    }
}

如果要避免子(Person)空引用,可以在父(Book)对象的构造函数中初始化它。

嵌套对象初始化器

这同样适用于嵌套对象初始化器:

Book b1 = new Book 
{ 
   Author = { Age = 45 } 
};

这意味着:

Book b1 = new Book();
b1.Author.Age = 45;

当使用new关键字时,它只创建Book的一个新实例,而不会创建Person的新实例,因此Author属性仍然为空。

嵌套集合初始值设定项

public class Person 
{
    public ICollection<Book> Books { get; set; }
}
public class Book 
{
    public string Title { get; set; }
}

嵌套集合Initializer的行为相同:

Person p1 = new Person 
{
    Books = {
         new Book { Title = "Title1" },
         new Book { Title = "Title2" },
    }
};

这意味着:

Person p1 = new Person();
p1.Books.Add(new Book { Title = "Title1" });
p1.Books.Add(new Book { Title = "Title2" });

新Person仅创建Person的实例,但Books集合仍然为空。集合Initializer语法未创建集合对于p1.Books,它只翻译为p1.Books.Add(…)语句。

大堆

int[] numbers = null;
int n = numbers[0]; // numbers is null. There is no array to index.

数组元素

Person[] people = new Person[5];
people[0].Age = 20 // people[0] is null. The array was allocated but not
                   // initialized. There is no Person to set the Age for.

锯齿状阵列

long[][] array = new long[1][];
array[0][0] = 3; // is null because only the first dimension is yet initialized.
                 // Use array[0] = new long[2]; first.

集合/列表/字典

Dictionary<string, int> agesForNames = null;
int age = agesForNames["Bob"]; // agesForNames is null.
                               // There is no Dictionary to perform the lookup.

范围变量(间接/延迟)

public class Person 
{
    public string Name { get; set; }
}
var people = new List<Person>();
people.Add(null);
var names = from p in people select p.Name;
string firstName = names.First(); // Exception is thrown here, but actually occurs
                                  // on the line above.  "p" is null because the
                                  // first element we added to the list is null.

事件(C#)

public class Demo
{
    public event EventHandler StateChanged;
    
    protected virtual void OnStateChanged(EventArgs e)
    {        
        StateChanged(this, e); // Exception is thrown here 
                               // if no event handlers have been attached
                               // to StateChanged event
    }
}

(注意:VB.NET编译器会插入事件用法的空检查,因此不必在VB.NET中检查Nothing的事件。)

错误的命名惯例:

如果您对字段的命名与本地变量不同,您可能会意识到您从未初始化过字段。

public class Form1
{
    private Customer customer;
    
    private void Form1_Load(object sender, EventArgs e) 
    {
        Customer customer = new Customer();
        customer.Name = "John";
    }
    
    private void Button_Click(object sender, EventArgs e)
    {
        MessageBox.Show(customer.Name);
    }
}

这可以通过以下惯例来解决:在字段前加下划线:

    private Customer _customer;

ASP.NET页面生命周期:

public partial class Issues_Edit : System.Web.UI.Page
{
    protected TestIssue myIssue;

    protected void Page_Load(object sender, EventArgs e)
    {
        if (!IsPostBack)
        {
             // Only called on first load, not when button clicked
             myIssue = new TestIssue(); 
        }
    }
        
    protected void SaveButton_Click(object sender, EventArgs e)
    {
        myIssue.Entry = "NullReferenceException here!";
    }
}

ASP.NET会话值

// if the "FirstName" session value has not yet been set,
// then this line will throw a NullReferenceException
string firstName = Session["FirstName"].ToString();

ASP.NET MVC空视图模型

如果在ASP.NET MVC视图中引用@Model的属性时发生异常,则需要了解在返回视图时,Model会在操作方法中设置。当您从控制器返回一个空模型(或模型属性)时,当视图访问它时会发生异常:

// Controller
public class Restaurant:Controller
{
    public ActionResult Search()
    {
        return View();  // Forgot the provide a Model here.
    }
}

// Razor view 
@foreach (var restaurantSearch in Model.RestaurantSearch)  // Throws.
{
}
    
<p>@Model.somePropertyName</p> <!-- Also throws -->

WPF控件创建顺序和事件

WPF控件是在调用InitializeComponent期间按照它们在可视化树中的显示顺序创建的。如果使用事件处理程序等早期创建的控件在InitializeComponent过程中触发,并引用了后期创建的控件,则会引发NullReferenceException。

例如:

<Grid>
    <!-- Combobox declared first -->
    <ComboBox Name="comboBox1" 
              Margin="10"
              SelectedIndex="0" 
              SelectionChanged="comboBox1_SelectionChanged">
       <ComboBoxItem Content="Item 1" />
       <ComboBoxItem Content="Item 2" />
       <ComboBoxItem Content="Item 3" />
    </ComboBox>
        
    <!-- Label declared later -->
    <Label Name="label1" 
           Content="Label"
           Margin="10" />
</Grid>

这里,组合框1在label1之前创建。如果comboBox1_SelectionChanged尝试引用“标签1”,则尚未创建它。

private void comboBox1_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    label1.Content = comboBox1.SelectedIndex.ToString(); // NullReferenceException here!!
}

更改XAML中声明的顺序(即,在comboBox1之前列出label1,忽略设计理念的问题)至少可以解决此处的NullReferenceException。

使用铸造

var myThing = someObject as Thing;

这不会引发InvalidCastException,但在强制转换失败时(以及someObject本身为null时)返回null。所以要注意这一点。

LINQ FirstOrDefault()和SingleOrDefault(

普通版本First()和Single()在没有异常时抛出异常。在这种情况下,“OrDefault”版本返回null。所以要注意这一点。

前肢

foreach在尝试迭代null集合时抛出。通常由返回集合的方法的意外空结果引起。

List<int> list = null;    
foreach(var v in list) { } // NullReferenceException here

更现实的例子是从XML文档中选择节点。如果未找到节点,但初始调试显示所有财产都有效,则将引发:

foreach (var node in myData.MyXml.DocumentNode.SelectNodes("//Data"))

避免的方法

显式检查空值并忽略空值。

如果希望引用有时为空,可以在访问实例成员之前检查引用是否为空:

void PrintName(Person p)
{
    if (p != null) 
    {
        Console.WriteLine(p.Name);
    }
}

显式检查null并提供默认值。

您调用的方法期望实例可以返回null,例如,当找不到要查找的对象时。在这种情况下,您可以选择返回默认值:

string GetCategory(Book b) 
{
    if (b == null)
        return "Unknown";
    return b.Category;
}

显式检查方法调用中的null并引发自定义异常。

您还可以抛出自定义异常,只在调用代码中捕获它:

string GetCategory(string bookTitle) 
{
    var book = library.FindBook(bookTitle);  // This may return null
    if (book == null)
        throw new BookNotFoundException(bookTitle);  // Your custom exception
    return book.Category;
}

如果值永远不应为空,请使用Debug.Assert,以便在异常发生之前捕获问题。

当您在开发过程中知道一个方法可以返回null,但不应该返回null时,可以使用Debug.Assert()在发生时尽快中断:

string GetTitle(int knownBookID) 
{
    // You know this should never return null.
    var book = library.GetBook(knownBookID);  

    // Exception will occur on the next line instead of at the end of this method.
    Debug.Assert(book != null, "Library didn't return a book for known book ID.");

    // Some other code

    return book.Title; // Will never throw NullReferenceException in Debug mode.
}

尽管此检查不会在您的发布版本中结束,但会导致在运行时book==null处于发布模式时再次引发NullReferenceException。

对可为null的值类型使用GetValueOrDefault(),以在它们为null时提供默认值。

DateTime? appointment = null;
Console.WriteLine(appointment.GetValueOrDefault(DateTime.Now));
// Will display the default value provided (DateTime.Now), because appointment is null.

appointment = new DateTime(2022, 10, 20);
Console.WriteLine(appointment.GetValueOrDefault(DateTime.Now));
// Will display the appointment date, not the default

使用空合并运算符:??[C#]或If()[VB]。

遇到null时提供默认值的简写:

IService CreateService(ILogger log, Int32? frobPowerLevel)
{
   var serviceImpl = new MyService(log ?? NullLog.Instance);
 
   // Note that the above "GetValueOrDefault()" can also be rewritten to use
   // the coalesce operator:
   serviceImpl.FrobPowerLevel = frobPowerLevel ?? 5;
}

使用空条件运算符:?。或[x] 对于数组(在C#6和VB.NET 14中可用):

这有时也被称为安全导航或猫王(以其形状命名)操作员。如果运算符左侧的表达式为null,则不会对右侧求值,而是返回null。这意味着这样的情况:

var title = person.Title.ToUpper();

如果此人没有标题,这将引发异常,因为它试图对具有空值的属性调用ToUpper。

在C#5及以下版本中,可通过以下方式进行防护:

var title = person.Title == null ? null : person.Title.ToUpper();

现在title变量将为null,而不是引发异常。C#6为此引入了更短的语法:

var title = person.Title?.ToUpper();

这将导致title变量为空,如果person.title为空,则不会调用ToUpper。

当然,您仍然需要检查null的标题,或者将null条件运算符与null合并运算符(??)一起使用以提供默认值:

// regular null check
int titleLength = 0;
if (title != null)
    titleLength = title.Length; // If title is null, this would throw NullReferenceException
    
// combining the `?` and the `??` operator
int titleLength = title?.Length ?? 0;

同样,对于可以使用的阵列?[i] 如下所示:

int[] myIntArray = null;
var i = 5;
int? elem = myIntArray?[i];
if (!elem.HasValue) Console.WriteLine("No value");

这将执行以下操作:如果myIntArray为null,则表达式返回null,您可以安全地检查它。如果它包含数组,则其操作与:elem=myIntArray[i];并返回第i个元素。

使用空上下文(在C#8中可用):

在C#8中引入了空上下文和可空引用类型,它们对变量执行静态分析,并在值可能为空或已设置为空时提供编译器警告。可为null的引用类型允许显式地允许类型为null。

可以使用csproj文件中的nullable元素为项目设置可为null的注释上下文和可为null警告上下文。此元素配置编译器如何解释类型的可空性以及生成什么警告。有效设置为:

enable:启用可为null的注释上下文。已启用可为null的警告上下文。例如,引用类型(字符串)的变量不可为空。所有可为空警告都已启用。disable:禁用可为null的注释上下文。禁用了可为null的警告上下文。引用类型的变量是不可见的,就像早期版本的C#一样。禁用所有可为null的警告。safeonly:启用了可为null的注释上下文。可为null的警告上下文是安全的。引用类型的变量不可为空。启用所有安全可为零警告。警告:禁用了可为null的注释上下文。已启用可为null的警告上下文。引用类型的变量是不可见的。所有可为空警告都已启用。safetonlywarnings:禁用了可为null的注释上下文。可为null的警告上下文是安全的。引用类型的变量是不可见的。启用所有安全可为零警告。

可为null的引用类型使用与可为null值类型相同的语法进行标注:A?附加到变量的类型。

调试和修复迭代器中的空derefs的特殊技术

C#支持“迭代器块”(在其他一些流行语言中称为“生成器”)。由于延迟执行,在迭代器块中调试NullReferenceException可能特别棘手:

public IEnumerable<Frob> GetFrobs(FrobFactory f, int count)
{
    for (int i = 0; i < count; ++i)
    yield return f.MakeFrob();
}
...
FrobFactory factory = whatever;
IEnumerable<Frobs> frobs = GetFrobs();
...
foreach(Frob frob in frobs) { ... }

如果结果为空,则MakeFrob将抛出。现在,你可能认为正确的做法是:

// DON'T DO THIS
public IEnumerable<Frob> GetFrobs(FrobFactory f, int count)
{
   if (f == null) 
      throw new ArgumentNullException("f", "factory must not be null");
   for (int i = 0; i < count; ++i)
      yield return f.MakeFrob();
}

为什么这是错误的?因为迭代器块直到foreach!对GetFrobs的调用只返回一个对象,当迭代时,该对象将运行迭代器块。

通过编写这样的空检查,可以防止NullReferenceException,但可以将NullArgumentException移动到迭代点,而不是调用点,这对调试来说非常混乱。

正确的修复方法是:

// DO THIS
public IEnumerable<Frob> GetFrobs(FrobFactory f, int count)
{
   // No yields in a public method that throws!
   if (f == null) 
       throw new ArgumentNullException("f", "factory must not be null");
   return GetFrobsForReal(f, count);
}
private IEnumerable<Frob> GetFrobsForReal(FrobFactory f, int count)
{
   // Yields in a private method
   Debug.Assert(f != null);
   for (int i = 0; i < count; ++i)
        yield return f.MakeFrob();
}

也就是说,创建一个具有迭代器块逻辑的私有助手方法和一个执行空检查并返回迭代器的公共表面方法。现在,当调用GetFrobs时,立即执行空检查,然后在序列迭代时执行GetFrobsForReal。

如果您检查LINQ to Objects的参考源,您将看到整个过程中都使用了这种技术。它编写起来稍显笨拙,但它使调试无效错误变得更加容易。优化代码是为了方便调用者,而不是作者。

关于不安全代码中空引用的注释

C#有一种“不安全”模式,顾名思义,这种模式非常危险,因为提供内存安全和类型安全的正常安全机制没有得到强制执行。除非您对内存的工作原理有透彻深入的了解,否则不应该编写不安全的代码。

在不安全模式下,您应该了解两个重要事实:

取消引用空指针会产生与取消引用空引用相同的异常在某些情况下,取消引用无效的非空指针可能会产生该异常

要理解这一点,首先要了解.NET如何生成NullReferenceException。(这些详细信息适用于在Windows上运行的.NET;其他操作系统使用类似的机制。)

内存在Windows中虚拟化;每个进程都会获得一个由操作系统跟踪的许多“页面”内存组成的虚拟内存空间。内存的每一页上都设置了标志,以确定如何使用:读取、写入、执行等等。最低的一页被标记为“如果以任何方式使用都会产生错误”。

C#中的空指针和空引用在内部都表示为数字零,因此任何试图将其解引用到相应的内存存储中的尝试都会导致操作系统产生错误。然后,.NET运行时检测到此错误并将其转换为NullReferenceException。

这就是为什么取消引用空指针和空引用会产生相同的异常。

第二点呢?取消引用位于虚拟内存最低页的任何无效指针都会导致相同的操作系统错误,从而导致相同的异常。

为什么这有意义?好吧,假设我们有一个包含两个int的结构和一个等于null的非托管指针。如果我们尝试取消引用结构中的第二个int,CLR将不会尝试访问位置0处的存储;它将访问位置4处的存储器。但从逻辑上讲,这是一个空的解引用,因为我们是通过空来到达那个地址的。

如果您使用的是不安全的代码,并且得到了NullReferenceException,请注意,有问题的指针不必为null。它可以是最低页面中的任何位置,将生成此异常。

这基本上是一个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()等系统方法检查空值,如果存在空值,则分配默认值。

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

希望这也有帮助!

错误行“Object reference not set to an instance of a Object.”表示您尚未将实例对象分配给对象引用,但仍在访问该对象的财产/方法。

例如:假设您有一个名为myClass的类,它包含一个属性prop1。

public Class myClass
{
   public int prop1 {get;set;}
}

现在,您正在访问其他类中的prop1,如下所示:

public class Demo
{
     public void testMethod()
     {
        myClass ref = null;
        ref.prop1 = 1;  // This line throws an error
     }
}

上述行引发错误,因为类myClass的引用已声明,但未实例化,或者对象的实例未分配给该类的引用。

要解决这个问题,必须实例化(将对象分配给该类的引用)。

public class Demo
{
     public void testMethod()
     {
        myClass ref = null;
        ref = new myClass();
        ref.prop1 = 1;
     }
}

有趣的是,本页的答案中没有一个提到两种边缘情况:

边缘案例#1:对字典的并发访问

.NET中的泛型字典不是线程安全的,当您尝试从两个并发线程访问密钥时,它们有时可能会抛出NullReference,甚至(更频繁)抛出KeyNotFoundException。在这种情况下,这个例外情况很容易引起误解。

边缘案例#2:不安全代码

如果不安全代码引发NullReferenceException,您可以查看指针变量,并检查它们是否存在IntPtr.Zero或其他内容。这是同一回事(“空指针异常”),但在不安全的代码中,变量通常被转换为值类型/数组等,你会把头撞在墙上,想知道值类型如何抛出此异常。

(顺便说一句,除非您需要,否则不使用不安全代码的另一个原因。)

边缘案例3:Visual Studio多监视器设置,辅助监视器的DPI设置与主监视器不同

此边缘案例是特定于软件的,属于Visual Studio 2019 IDE(以及可能更早的版本)。

一种重现问题的方法:将任何组件从工具箱拖到非主监视器上的Windows窗体上,该窗体的DPI设置与主监视器不同,然后会弹出一个“Object reference not set to A instance of A Object”(对象引用未设置为对象的实例)。根据这个线程,这个问题已经知道了很长一段时间,在编写时仍然没有解决。