繁体   English   中英

什么是 NullReferenceException,我该如何修复它?

[英]What is a NullReferenceException, and how do I fix it?

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

Object 引用未设置为 object 的实例。

这是什么意思,我该怎么做才能解决这个错误?

原因是什么?

底线

您正在尝试使用null的东西(或 VB.NET 中的Nothing )。 这意味着您要么将其设置为null ,要么根本不将其设置为任何内容。

像其他任何东西一样, null被传递。 如果它方法“A”中为null ,则可能是方法“B”将null传递给了方法“A”。

null可以有不同的含义:

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

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

进一步来说

抛出NullReferenceExceptionruntime总是意味着同样的事情:您正在尝试使用引用,并且该引用未初始化(或者它曾经被初始化,但不再被初始化)。

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

string foo = null;
foo.ToUpper();

这将在第二行引发NullReferenceException ,因为您不能在指向nullstring引用上调用实例方法ToUpper()

调试

您如何找到NullReferenceException的来源? 除了查看异常本身(将在其发生的位置准确抛出)之外,Visual Studio 中的一般调试规则也适用:放置战略断点并检查您的变量,方法是将鼠标悬停在它们的名称上,打开一个 ( Quick)Watch 窗口或使用各种调试面板,如 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.Current可以为 null,或者User属性可以为 null,或者Identity属性可以为 null。

间接

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属性仍然为null

嵌套集合初始化器

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

嵌套集合Initializers的行为相同:

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" });

new Person只创建了Person的一个实例,但Books集合仍然是null 集合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 View中引用@Model的属性时发生异常,则需要了解在return视图时在您的操作方法中设置了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期间按照它们在可视树中出现的顺序创建的。 NullReferenceException将在具有事件处理程序等的早期创建控件的情况下引发,这些控件在InitializeComponent期间触发并引用后期创建的控件。

例如:

<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>

这里comboBox1label1之前创建。 如果comboBox1_SelectionChanged尝试引用 `label1,它还没有被创建。

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

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

使用as

var myThing = someObject as Thing;

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

LINQ FirstOrDefault()SingleOrDefault()

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

前锋

当您尝试迭代null时, foreach会抛出异常。 通常是由返回集合的方法的意外null结果引起的。

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

更现实的例子 - 从 XML 文档中选择节点。 如果未找到节点但初始调试显示所有属性都有效,则会抛出:

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

避免的方法

显式检查null并忽略null值。

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

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;
}

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

当您在开发过程中知道一个方法可以但绝不应该返回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

nullable值类型使用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 中可用):

这有时也称为安全导航或 Elvis(根据其形状)运算符。 如果运算符左侧的表达式为 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 变量为null ,如果person.Titlenull ,则不会调用ToUpper

当然,您仍然需要检查title是否为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");

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

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

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

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

  • enable :启用可空注释上下文。 可空警告上下文已启用。 例如,引用类型的变量(字符串)是不可为空的。 所有可空性警告均已启用。
  • disable :可空注释上下文被禁用。 可空警告上下文已禁用。 引用类型的变量是无意识的,就像 C# 的早期版本一样。 所有可空性警告都被禁用。
  • safeonly :启用可为空的注释上下文。 可为空的警告上下文是仅安全的。 引用类型的变量是不可为空的。 所有安全可空性警告均已启用。
  • warnings :可空注释上下文已禁用。 可空警告上下文已启用。 引用类型的变量是不经意的。 所有可空性警告均已启用。
  • safeonlywarnings :可空注释上下文被禁用。 可为空的警告上下文是仅安全的。 引用类型的变量是不经意的。 所有安全可空性警告均已启用。

可空引用类型使用与可空值类型相同的语法来记录? 附加到变量的类型。

调试和修复迭代器中的 null 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) { ... }

如果whatever结果为null ,则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的调用仅返回一个对象,该对象在迭代时将运行迭代器块。

通过编写这样的null检查,您可以防止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();
}

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

如果您检查LINQ to Objects 的参考源,您会发现该技术自始至终都在使用。 写起来有点笨拙,但它使调试无效错误更容易。 优化你的代码是为了调用者的方便,而不是作者的方便

关于不安全代码中的 null 取消引用的说明

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

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

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

要理解为什么会这样,首先要了解 .NET 如何产生NullReferenceException (这些细节适用于在 Windows 上运行的 .NET;其他操作系统使用类似的机制。)

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

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

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

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

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

如果您正在使用不安全的代码并且收到NullReferenceException ,请注意违规指针不必为空。 它可以是最低页面中的任何位置,并且会产生此异常。

NullReference 异常 - Visual Basic

Visual BasicNullReference ExceptionC#中的异常没有什么不同。 毕竟,它们都报告了它们都使用的 .NET Framework 中定义的相同异常。 Visual Basic 特有的原因很少见(可能只有一个)。

此答案将使用 Visual Basic 术语、语法和上下文。 使用的示例来自大量过去的 Stack Overflow 问题。 这是通过使用帖子中常见的情况来最大化相关性。 还为可能需要它的人提供了更多解释。 此处可能列出了与您类似的示例。

笔记:

  1. 这是基于概念的:没有代码可以粘贴到您的项目中。 它旨在帮助您了解导致NullReferenceException (NRE) 的原因、如何找到它、如何修复它以及如何避免它。 NRE 可以通过多种方式引起,因此这不太可能是您唯一的遭遇。
  2. 这些示例(来自 Stack Overflow 帖子)并不总是首先显示做某事的最佳方式。
  3. 通常,使用最简单的补救措施。

基本含义

消息“Object not set to an instance of Object”表示您正在尝试使用尚未初始化的对象。 这归结为其中之一:

  • 您的代码声明了一个对象变量,但它没有初始化它(创建一个实例或“实例化”它)
  • 您的代码假定会初始化对象的东西没有
  • 可能,其他代码过早地使仍在使用的对象无效

寻找原因

由于问题是对象引用,它是Nothing ,答案是检查它们以找出哪个。 然后确定它为什么没有初始化。 将鼠标悬停在各种变量上,Visual Studio (VS) 将显示它们的值 - 罪魁祸首将是Nothing

IDE调试显示

您还应该从相关代码中删除任何 Try/Catch 块,尤其是 Catch 块中没有任何内容的代码。 这将导致您的代码在尝试使用Nothing对象时崩溃。 这就是您想要的,因为它将识别问题的确切位置,并允许您识别导致问题的对象。

Catch 中显示Error while...MsgBox将无济于事。 这种方法还会导致非常糟糕的Stack Overflow 问题,因为您无法描述实际的异常、涉及的对象甚至发生它的代码行。

您还可以使用Locals Window ( Debug -> Windows -> Locals ) 来检查您的对象。

一旦您知道问题出在哪里和出在哪里,通常比发布新问题更容易修复并且更快。

也可以看看:

示例和补救措施

类对象/创建实例

Dim reg As CashRegister
...
TextBox1.Text = reg.Amount         ' NRE

问题是Dim没有创建 CashRegister对象 它只声明该类型的名为reg的变量。 声明对象变量和创建实例是两件不同的事情。

补救

声明实例时,通常可以使用New运算符创建实例:

Dim reg As New CashRegister        ' [New] creates instance, invokes the constructor

' Longer, more explicit form:
Dim reg As CashRegister = New CashRegister

当只适合稍后创建实例时:

Private reg As CashRegister         ' Declare
  ...
reg = New CashRegister()            ' Create instance

注意:不要在过程中再次使用Dim ,包括构造函数( Sub New ):

Private reg As CashRegister
'...

Public Sub New()
   '...
   Dim reg As New CashRegister
End Sub

这将创建一个局部变量reg ,它仅存在于该上下文(子)中。 您将在其他任何地方使用的具有模块级别Scopereg变量仍然是Nothing

缺少New运算符是在查看的堆栈溢出问题中看到的NullReference Exceptions的#1 原因

Visual Basic 尝试反复使用New使过程清晰:使用New运算符创建一个对象并调用Sub New (构造函数),您的对象可以在其中执行任何其他初始化。

需要明确的是, Dim (或Private )仅声明了一个变量及其Type 变量的作用域——它是否存在于整个模块/类或者是过程的局部变量——取决于它的声明位置 Private | Friend | Public Private | Friend | Public定义访问级别,而不是Scope

有关更多信息,请参阅:


数组

数组也必须实例化:

Private arr as String()

这个数组只是被声明了,没有被创建。 有几种初始化数组的方法:

Private arr as String() = New String(10){}
' or
Private arr() As String = New String(10){}

' For a local array (in a procedure) and using 'Option Infer':
Dim arr = New String(10) {}

注意:从 VS 2010 开始,当使用文字和Option Infer初始化本地数组时, As <Type>New元素是可选的:

Dim myDbl As Double() = {1.5, 2, 9.9, 18, 3.14}
Dim myDbl = New Double() {1.5, 2, 9.9, 18, 3.14}
Dim myDbl() = {1.5, 2, 9.9, 18, 3.14}

数据类型和数组大小是从分配的数据中推断出来的。 类/模块级别声明仍然需要As <Type>Option Strict

Private myDoubles As Double() = {1.5, 2, 9.9, 18, 3.14}

示例:类对象数组

Dim arrFoo(5) As Foo

For i As Integer = 0 To arrFoo.Count - 1
   arrFoo(i).Bar = i * 10       ' Exception
Next

数组已创建,但其中的Foo对象尚未创建。

补救

For i As Integer = 0 To arrFoo.Count - 1
    arrFoo(i) = New Foo()         ' Create Foo instance
    arrFoo(i).Bar = i * 10
Next

使用List(Of T)将使没有有效对象的元素变得非常困难:

Dim FooList As New List(Of Foo)     ' List created, but it is empty
Dim f As Foo                        ' Temporary variable for the loop

For i As Integer = 0 To 5
    f = New Foo()                    ' Foo instance created
    f.Bar =  i * 10
    FooList.Add(f)                   ' Foo object added to list
Next

有关更多信息,请参阅:


列表和集合

.NET 集合(其中有很多种 - 列表、字典等)也必须实例化或创建。

Private myList As List(Of String)
..
myList.Add("ziggy")           ' NullReference

由于相同的原因,您会得到相同的异常 - 仅声明了myList ,但没有创建实例。 补救措施是一样的:

myList = New List(Of String)

' Or create an instance when declared:
Private myList As New List(Of String)

一个常见的疏忽是一个使用集合Type的类:

Public Class Foo
    Private barList As List(Of Bar)

    Friend Function BarCount As Integer
        Return barList.Count
    End Function

    Friend Sub AddItem(newBar As Bar)
        If barList.Contains(newBar) = False Then
            barList.Add(newBar)
        End If
    End Function

任何一个过程都会导致 NRE,因为barList只是被声明的,而不是被实例化的。 创建Foo的实例不会同时创建内部barList的实例。 这可能是在构造函数中执行此操作的意图:

Public Sub New         ' Constructor
    ' Stuff to do when a new Foo is created...
    barList = New List(Of Bar)
End Sub

和以前一样,这是不正确的:

Public Sub New()
    ' Creates another barList local to this procedure
     Dim barList As New List(Of Bar)
End Sub

有关详细信息,请参阅List(Of T)


数据提供者对象

使用数据库为 NullReference 提供了许多机会,因为可以同时使用许多对象( CommandConnectionTransactionDatasetDataTableDataRows ....)。 注意:无论您使用哪个数据提供者——MySQL、SQL Server、OleDB 等——它们的概念都是相同的。

示例 1

Dim da As OleDbDataAdapter
Dim ds As DataSet
Dim MaxRows As Integer

con.Open()
Dim sql = "SELECT * FROM tblfoobar_List"
da = New OleDbDataAdapter(sql, con)
da.Fill(ds, "foobar")
con.Close()

MaxRows = ds.Tables("foobar").Rows.Count      ' Error

和以前一样,声明了ds Dataset 对象,但从未创建实例。 DataAdapter将填充现有的DataSet ,而不是创建一个。 在这种情况下,由于ds是一个局部变量, IDE 会警告您这可能会发生:

图像

当声明为模块/类级别变量时,就像con的情况一样,编译器无法知道该对象是否是由上游过程创建的。 不要忽视警告。

补救

Dim ds As New DataSet

示例 2

ds = New DataSet
da = New OleDBDataAdapter(sql, con)
da.Fill(ds, "Employees")

txtID.Text = ds.Tables("Employee").Rows(0).Item(1)
txtID.Name = ds.Tables("Employee").Rows(0).Item(2)

这里有一个错字: Employees vs Employee 没有创建名为“Employee”的DataTable ,因此尝试访问它时会出现NullReferenceException 另一个潜在的问题是假设当 SQL 包含 WHERE 子句时可能会有Items

补救

由于这使用一个表,因此使用Tables(0)将避免拼写错误。 检查Rows.Count还可以帮助:

If ds.Tables(0).Rows.Count > 0 Then
    txtID.Text = ds.Tables(0).Rows(0).Item(1)
    txtID.Name = ds.Tables(0).Rows(0).Item(2)
End If

Fill是一个返回受影响Rows数的函数,也可以对其进行测试:

If da.Fill(ds, "Employees") > 0 Then...

示例 3

Dim da As New OleDb.OleDbDataAdapter("SELECT TICKET.TICKET_NO,
        TICKET.CUSTOMER_ID, ... FROM TICKET_RESERVATION AS TICKET INNER JOIN
        FLIGHT_DETAILS AS FLIGHT ... WHERE [TICKET.TICKET_NO]= ...", con)
Dim ds As New DataSet
da.Fill(ds)

If ds.Tables("TICKET_RESERVATION").Rows.Count > 0 Then

DataAdapter将提供如前面示例中所示的TableNames ,但它不解析 SQL 或数据库表中的名称。 因此, ds.Tables("TICKET_RESERVATION")引用了一个不存在的表。

补救方法是一样的,按索引引用表:

If ds.Tables(0).Rows.Count > 0 Then

另请参见DataTable 类


对象路径/嵌套

If myFoo.Bar.Items IsNot Nothing Then
   ...

该代码仅测试Items ,而myFooBar也可能是 Nothing。 补救方法是一次测试一个对象的整个链或路径:

If (myFoo IsNot Nothing) AndAlso
    (myFoo.Bar IsNot Nothing) AndAlso
    (myFoo.Bar.Items IsNot Nothing) Then
    ....

AndAlso也很重要。 一旦遇到第一个False条件,将不会执行后续测试。 这允许代码一次安全地“钻入”对象一个“级别”,仅在确定myFoo有效之后(并且如果)评估myFoo.Bar 编码复杂对象时,对象链或路径可能会变得很长:

myBase.myNodes(3).Layer.SubLayer.Foo.Files.Add("somefilename")

不可能引用null对象的任何“下游”。 这也适用于控件:

myWebBrowser.Document.GetElementById("formfld1").InnerText = "some value"

这里, myWebBrowserDocument可能是 Nothing,或者formfld1元素可能不存在。


用户界面控件

Dim cmd5 As New SqlCommand("select Cartons, Pieces, Foobar " _
     & "FROM Invoice where invoice_no = '" & _
     Me.ComboBox5.SelectedItem.ToString.Trim & "' And category = '" & _
     Me.ListBox1.SelectedItem.ToString.Trim & "' And item_name = '" & _
     Me.ComboBox2.SelectedValue.ToString.Trim & "' And expiry_date = '" & _
     Me.expiry.Text & "'", con)

除其他事项外,此代码并未预期用户可能未在一个或多个 UI 控件中选择某些内容。 ListBox1.SelectedItem很可能是Nothing ,因此ListBox1.SelectedItem.ToString将导致 NRE。

补救

使用前验证数据(也使用Option Strict和 SQL 参数):

Dim expiry As DateTime         ' for text date validation
If (ComboBox5.SelectedItems.Count > 0) AndAlso
    (ListBox1.SelectedItems.Count > 0) AndAlso
    (ComboBox2.SelectedItems.Count > 0) AndAlso
    (DateTime.TryParse(expiry.Text, expiry) Then

    '... do stuff
Else
    MessageBox.Show(...error message...)
End If

或者,您可以使用(ComboBox5.SelectedItem IsNot Nothing) AndAlso...


Visual Basic 窗体

Public Class Form1

    Private NameBoxes = New TextBox(5) {Controls("TextBox1"), _
                   Controls("TextBox2"), Controls("TextBox3"), _
                   Controls("TextBox4"), Controls("TextBox5"), _
                   Controls("TextBox6")}

    ' same thing in a different format:
    Private boxList As New List(Of TextBox) From {TextBox1, TextBox2, TextBox3 ...}

    ' Immediate NRE:
    Private somevar As String = Me.Controls("TextBox1").Text

这是获得 NRE 的一种相当常见的方式。 在 C# 中,根据其编码方式,IDE 会报告当前上下文中不存在Controls ,或者“无法引用非静态成员”。 所以,在某种程度上,这只是 VB 的情况。 它也很复杂,因为它可能导致故障级联。

数组和集合不能以这种方式初始化。 此初始化代码将在构造函数创建FormControls之前运行。 因此:

  • 列表和集合将只是空的
  • 该数组将包含 Nothing 的五个元素
  • somevar分配将导致立即 NRE,因为 Nothing 没有.Text属性

稍后引用数组元素将导致 NRE。 如果您在Form_Load中执行此操作,由于奇怪的错误,IDE可能不会在发生异常时报告异常。 当您的代码尝试使用该数组时,稍后会弹出异常。 这个“沉默的例外”在这篇文章中有详细说明 就我们的目的而言,关键是当创建表单时发生灾难性事件( Sub NewForm Load事件)时,可能不会报告异常,代码退出过程并仅显示表单。

由于您的Sub NewForm Load事件中没有其他代码将在 NRE 之后运行,因此许多其他事情可以保持未初始化。

Sub Form_Load(..._
   '...
   Dim name As String = NameBoxes(2).Text        ' NRE
   ' ...
   ' More code (which will likely not be executed)
   ' ...
End Sub

请注意,这适用于任何和所有控件和组件引用,使它们在它们所在的位置是非法的:

Public Class Form1

    Private myFiles() As String = Me.OpenFileDialog1.FileName & ...
    Private dbcon As String = OpenFileDialog1.FileName & ";Jet Oledb..."
    Private studentName As String = TextBox13.Text

部分补救措施

奇怪的是VB没有提供警告,但补救措施是在表单级别声明容器,但在控件确实存在时在表单加载事件处理程序中初始化它们。 这可以在Sub New中完成,只要您的代码在InitializeComponent调用之后:

' Module level declaration
Private NameBoxes as TextBox()
Private studentName As String

' Form Load, Form Shown or Sub New:
'
' Using the OP's approach (illegal using OPTION STRICT)
NameBoxes = New TextBox() {Me.Controls("TextBox1"), Me.Controls("TestBox2"), ...)
studentName = TextBox32.Text           ' For simple control references

数组代码可能还没有走出困境。 Me.Controls中找不到容器控件(如GroupBoxPanel )中的任何控件; 它们将位于该 Panel 或 GroupBox 的 Controls 集合中。 当控件名称拼写错误( "TeStBox2" )时,也不会返回控件。 在这种情况下, Nothing将再次存储在这些数组元素中,并且当您尝试引用它时将产生 NRE。

既然您知道要查找的内容,这些应该很容易找到:VS 向你展示你的方式的错误

“Button2”位于Panel

补救

不要使用表单的Controls集合按名称间接引用,而是使用控件引用:

' Declaration
Private NameBoxes As TextBox()

' Initialization -  simple and easy to read, hard to botch:
NameBoxes = New TextBox() {TextBox1, TextBox2, ...)

' Initialize a List
NamesList = New List(Of TextBox)({TextBox1, TextBox2, TextBox3...})
' or
NamesList = New List(Of TextBox)
NamesList.AddRange({TextBox1, TextBox2, TextBox3...})

函数无返回

Private bars As New List(Of Bars)        ' Declared and created

Public Function BarList() As List(Of Bars)
    bars.Clear
    If someCondition Then
        For n As Integer = 0 to someValue
            bars.Add(GetBar(n))
        Next n
    Else
        Exit Function
    End If

    Return bars
End Function

在这种情况下,IDE 会警告您“并非所有路径都返回一个值,并且可能会导致NullReferenceException ”。 您可以通过将Exit Function替换为Return Nothing来抑制警告,但这并不能解决问题。 任何在someCondition = False时尝试使用 return 的东西都会导致 NRE:

bList = myFoo.BarList()
For Each b As Bar in bList      ' EXCEPTION
      ...

补救

将函数中的Exit Function替换为Return bList 返回List与返回Nothing不同。 如果返回的对象有可能是Nothing ,请在使用前进行测试:

 bList = myFoo.BarList()
 If bList IsNot Nothing Then...

执行不力的尝试/捕获

实施不当的 Try/Catch 可能会隐藏问题所在并导致新问题:

Dim dr As SqlDataReader
Try
    Dim lnk As LinkButton = TryCast(sender, LinkButton)
    Dim gr As GridViewRow = DirectCast(lnk.NamingContainer, GridViewRow)
    Dim eid As String = GridView1.DataKeys(gr.RowIndex).Value.ToString()
    ViewState("username") = eid
    sqlQry = "select FirstName, Surname, DepartmentName, ExtensionName, jobTitle,
             Pager, mailaddress, from employees1 where username='" & eid & "'"
    If connection.State <> ConnectionState.Open Then
        connection.Open()
    End If
    command = New SqlCommand(sqlQry, connection)

    'More code fooing and barring

    dr = command.ExecuteReader()
    If dr.Read() Then
        lblFirstName.Text = Convert.ToString(dr("FirstName"))
        ...
    End If
    mpe.Show()
Catch

Finally
    command.Dispose()
    dr.Close()             ' <-- NRE
    connection.Close()
End Try

这是一个未按预期创建对象的情况,但也演示了空Catch的计数器有用性。

SQL 中有一个额外的逗号(在“mailaddress”之后),这会导致.ExecuteReader出现异常。 Catch什么都不做之后, Finally尝试执行清理,但由于您无法Close null DataReader对象,因此会产生全新的NullReferenceException

一个空的Catch块是魔鬼的游乐场。 这位 OP 对他为什么在Finally块中获得 NRE 感到困惑。 在其他情况下,一个空的Catch可能会导致更下游的其他事情变得混乱,并导致您花时间在错误的地方寻找错误的东西来解决问题。 (上面描述的“无声例外”提供了相同的娱乐价值。)

补救

不要使用空的 Try/Catch 块 - 让代码崩溃,这样您就可以 a) 确定原因 b) 确定位置 c) 应用适当的补救措施。 Try/Catch 块并不是为了向唯一有资格修复它们的人(开发人员)隐藏异常。


DBNull 与 Nothing 不同

For Each row As DataGridViewRow In dgvPlanning.Rows
    If Not IsDBNull(row.Cells(0).Value) Then
        ...

IsDBNull函数用于测试是否等于System.DBNull来自 MSDN:

System.DBNull 值指示 Object 表示丢失或不存在的数据。 DBNull 和 Nothing 不同,表示一个变量还没有被初始化。

补救

If row.Cells(0) IsNot Nothing Then ...

和以前一样,您可以测试 Nothing,然后测试特定值:

If (row.Cells(0) IsNot Nothing) AndAlso (IsDBNull(row.Cells(0).Value) = False) Then

示例 2

Dim getFoo = (From f In dbContext.FooBars
               Where f.something = something
               Select f).FirstOrDefault

If Not IsDBNull(getFoo) Then
    If IsDBNull(getFoo.user_id) Then
        txtFirst.Text = getFoo.first_name
    Else
       ...

FirstOrDefault返回第一项或默认值,对于引用类型是Nothing并且从不DBNull

If getFoo IsNot Nothing Then...

控件

Dim chk As CheckBox

chk = CType(Me.Controls(chkName), CheckBox)
If chk.Checked Then
    Return chk
End If

如果找不到带有chkNameCheckBox (或存在于GroupBox中),则chk将为 Nothing 并且尝试引用任何属性将导致异常。

补救

If (chk IsNot Nothing) AndAlso (chk.Checked) Then ...

数据网格视图

DGV 有一些定期出现的怪癖:

dgvBooks.DataSource = loan.Books
dgvBooks.Columns("ISBN").Visible = True       ' NullReferenceException
dgvBooks.Columns("Title").DefaultCellStyle.Format = "C"
dgvBooks.Columns("Author").DefaultCellStyle.Format = "C"
dgvBooks.Columns("Price").DefaultCellStyle.Format = "C"

如果dgvBooksAutoGenerateColumns = True ,它将创建列,但它不命名它们,因此上面的代码在按名称引用它们时会失败。

补救

手动命名列,或按索引引用:

dgvBooks.Columns(0).Visible = True

示例 2 - 注意 NewRow

xlWorkSheet = xlWorkBook.Sheets("sheet1")

For i = 0 To myDGV.RowCount - 1
    For j = 0 To myDGV.ColumnCount - 1
        For k As Integer = 1 To myDGV.Columns.Count
            xlWorkSheet.Cells(1, k) = myDGV.Columns(k - 1).HeaderText
            xlWorkSheet.Cells(i + 2, j + 1) = myDGV(j, i).Value.ToString()
        Next
    Next
Next

当您的DataGridViewAllowUserToAddRows设置为True (默认值)时,底部空白/新行中的Cells将全部包含Nothing 大多数使用内容的尝试(例如, ToString )将导致 NRE。

补救

使用For/Each循环并测试IsNewRow属性以确定它是否是最后一行。 无论AllowUserToAddRows是否为真,这都有效:

For Each r As DataGridViewRow in myDGV.Rows
    If r.IsNewRow = False Then
         ' ok to use this row

如果您确实使用了For n循环,请修改行数或在IsNewRow为真时使用Exit For


我的.Settings (StringCollection)

在某些情况下,尝试使用My.Settings中的StringCollection项目可能会在您第一次使用它时导致 NullReference。 解决方案是相同的,但不是那么明显。 考虑:

My.Settings.FooBars.Add("ziggy")         ' foobars is a string collection

由于 VB 正在为您管理设置,因此期望它初始化集合是合理的。 它将,但前提是您之前已将初始条目添加到集合中(在设置编辑器中)。 由于在添加项目时(显然)对集合进行了初始化,因此当设置编辑器中没有要添加的项目时,它仍然是Nothing

补救

如果/需要时,在表单的Load事件处理程序中初始化设置集合:

If My.Settings.FooBars Is Nothing Then
    My.Settings.FooBars = New System.Collections.Specialized.StringCollection
End If

通常, Settings集合只需要在应用程序第一次运行时进行初始化。 另一种补救方法是在Project -> Settings |中为您的集合添加一个初始值。 FooBars ,保存项目,然后删除假值。


关键点

您可能忘记了New运算符。

或者

您认为可以完美执行以将初始化的对象返回到您的代码的东西,但事实并非如此。

不要忽略编译器警告(永远)并使用Option Strict On (总是)。


MSDN 空引用异常

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

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

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

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

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

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

这意味着您的代码使用了一个设置为 null 的对象引用变量(即它没有引用实际的对象实例)。

为防止该错误,可能为 null 的对象应在使用前进行 null 测试。

if (myvar != null)
{
    // Go ahead and use myvar
    myvar.property = ...
}
else
{
    // Whoops! myvar is null and cannot be used without first
    // assigning it to an instance reference
    // Attempting to use myvar here will result in NullReferenceException
}

这意味着有问题的变量没有指向任何东西。 我可以这样生成:

SqlConnection connection = null;
connection.Open();

这将引发错误,因为虽然我已经声明了变量“ connection ”,但它并没有指向任何东西。 当我尝试调用成员“ Open ”时,没有参考可以解决它,它会抛出错误。

为避免此错误:

  1. 在尝试对它们进行任何操作之前,请始终初始化您的对象。
  2. 如果您不确定对象是否为 null,请使用object == null进行检查。

JetBrains 的ReSharper工具将识别代码中可能出现空引用错误的每个位置,从而允许您进行空检查。 这个错误是错误的第一来源,恕我直言。

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

您正在尝试使用值为Nothing / null的引用变量。 当引用变量的值为Nothing / null时,这意味着它实际上并未持有对堆上存在的任何对象的实例的引用。

您要么从未为变量分配任何内容,从未创建分配给变量的值的实例,要么您手动将变量设置为Nothing / null ,或者您调用了一个为您将变量设置为Nothing / null的函数。

抛出此异常的一个示例是:当您尝试检查某些内容时,该异常为空。

例如:

string testString = null; //Because it doesn't have a value (i.e. it's null; "Length" cannot do what it needs to do)

if (testString.Length == 0) // Throws a nullreferenceexception
{
    //Do something
} 

当您尝试对尚未实例化的内容(即上面的代码)执行操作时,.NET 运行时将引发 NullReferenceException。

与通常作为防御措施抛出的 ArgumentNullException 相比,如果方法期望传递给它的内容不为 null。

更多信息在C# NullReferenceException 和 Null Parameter中。

2019 年 C#8.0 更新:可空引用类型

C#8.0 引入了可为空的引用类型不可为空的引用类型 因此,只有可以为空的引用类型必须检查以避免NullReferenceException


如果您尚未初始化引用类型,并且想要设置或读取其属性之一,它将抛出NullReferenceException

例子:

Person p = null;
p.Name = "Harry"; // NullReferenceException occurs here.

您可以通过检查变量是否不为空来简单地避免这种情况:

Person p = null;
if (p!=null)
{
    p.Name = "Harry"; // Not going to run to this point
}

要完全理解为什么会抛出 NullReferenceException,了解值类型和 [引用类型][3] 之间的区别很重要。

因此,如果您正在处理值类型,则不会发生 NullReferenceExceptions。 尽管在处理引用类型时需要保持警惕!

顾名思义,只有引用类型可以保存引用或从字面上指向任何内容(或“null”)。 而值类型总是包含一个值。

引用类型(必须检查这些):

  • 动态的
  • 目的
  • 细绳

值类型(您可以简单地忽略这些):

  • 数值类型
  • 积分型
  • 浮点类型
  • 十进制
  • 布尔
  • 用户定义的结构

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

在这里, BookCar是不兼容的类型; a Car不能转换/转换为Book 当此转换失败时, as返回null 在此之后使用mybook会导致NullReferenceException

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

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

ComicBook cb = (ComicBook)specificBook;

如果您不确定类型,但想尝试将其用作特定类型,请使用as

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

您正在使用包含空值引用的对象。 所以它给出了一个空异常。 在示例中,字符串值为 null,并且在检查其长度时发生了异常。

例子:

string value = null;
if (value.Length == 0) // <-- Causes exception
{
    Console.WriteLine(value); // <-- Never reached
}

异常错误是:

未处理的异常:

System.NullReferenceException:对象引用未设置为对象的实例。 在 Program.Main()

虽然导致NullReferenceExceptions原因以及避免/修复此类异常的方法已在其他答案中得到解决,但许多程序员尚未了解的是如何在开发过程中独立调试此类异常。

在 Visual Studio 中,这通常很容易,这要归功于Visual Studio 调试器


首先,确保将捕获正确的错误 - 请参阅如何允许在 VS2010 中中断“System.NullReferenceException”? 1

然后要么Start with Debugging (F5)要么Attach [the VS Debugger] to Running Process 有时使用Debugger.Break可能很有用,它将提示启动调试器。

现在,当 NullReferenceException 被抛出(或未处理)时,调试器将在发生异常的行上停止(还记得上面设置的规则吗?)。 有时错误很容易被发现。

例如,在以下行中,唯一可能导致异常的代码是myString的计算结果为 null。 这可以通过查看Watch Window或在Immediate Window中运行表达式来验证。

var x = myString.Trim();

在更高级的情况下,例如以下情况,您需要使用上述技术之一(Watch 或 Immediate Windows)来检查表达式以确定str1是否为 null 或str2是否为 null。

var x = str1.Trim() + str2.Trim();

一旦找到异常抛出的位置,通常很容易向后推理以找出 [不正确] 引入空值的位置 -

花点时间了解异常的原因。 检查空表达式。 检查可能导致此类空表达式的先前表达式。 根据需要添加断点并逐步执行程序。 使用调试器。


1如果 Break on Throws 过于激进,并且调试器在 .NET 或第 3 方库中的 NPE 上停止,则Break on User-Unhandled可用于限制捕获的异常。 此外,VS2012 引入了我建议启用的Just My Code

如果您在启用“仅我的代码”的情况下进行调试,则行为会略有不同。 启用“仅我的代码”后,调试器将忽略在“我的代码”之外引发且不通过“我的代码”的第一次机会公共语言运行时 (CLR) 异常

Simon Mourier 举了这个例子

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

其中object (或从System.ValueTypeSystem.Enum类之一,或从接口类型)值类型( Nullable<>除外)的拆箱转换(强制转换)本身会产生NullReferenceException

在另一个方向上,HasValue等于falseNullable<>引用类型的装箱转换可以给出一个null引用,该引用随后会导致NullReferenceException 经典的例子是:

DateTime? d = null;
var s = d.ToString();  // OK, no exception (no boxing), returns ""
var t = d.GetType();   // Bang! d is boxed, NullReferenceException

有时拳击以另一种方式发生。 例如,使用这种非通用扩展方法:

public static void MyExtension(this object x)
{
  x.ToString();
}

下面的代码会有问题:

DateTime? d = null;
d.MyExtension();  // Leads to boxing, NullReferenceException occurs inside the body of the called method, not here.

这些情况的出现是由于运行时在装箱Nullable<>实例时使用的特殊规则。

当实体框架中使用的实体的类名与 Web 表单代码隐藏文件的类名相同时添加一个案例。

假设您有一个 Web 表单 Contact.aspx,其代码隐藏类是 Contact,并且您有一个实体名称 Contact。

然后,当您调用 context.SaveChanges() 时,以下代码将引发 NullReferenceException

Contact contact = new Contact { Name = "Abhinav"};
var context = new DataContext();
context.Contacts.Add(contact);
context.SaveChanges(); // NullReferenceException at this line

为了完整起见 DataContext 类

public class DataContext : DbContext 
{
    public DbSet<Contact> Contacts {get; set;}
}

和联系实体类。 有时实体类是部分类,因此您也可以在其他文件中扩展它们。

public partial class Contact 
{
    public string Name {get; set;}
}

当实体和代码隐藏类都在同一个命名空间中时,会发生错误。 要解决此问题,请重命名 Contact.aspx 的实体类或代码隐藏类。

原因我仍然不确定原因。 但是,只要任何实体类扩展 System.Web.UI.Page 就会发生此错误。

有关讨论,请查看DbContext.saveChanges() 中的 NullReferenceException

另一种可能会收到此异常的一般情况涉及在单元测试期间模拟类。 无论使用何种模拟框架,您都必须确保正确模拟类层次结构的所有适当级别。 特别是,必须模拟被测试代码引用的HttpContext的所有属性。

请参阅“ 测试自定义 AuthorizationAttribute 时抛出的 NullReferenceException ”以获取一些详细的示例。

我有不同的观点来回答这个问题。 这种回答“我还能做些什么来避免它?

跨不同层工作时,例如在 MVC 应用程序中,控制器需要服务来调用业务操作。 在这种情况下,可以使用依赖注入容器来初始化服务以避免NullReferenceException 所以这意味着您不必担心检查 null 并且只需从控制器调用服务,就好像它们将始终作为单例或原型一样可用(并已初始化)。

public class MyController
{
    private ServiceA serviceA;
    private ServiceB serviceB;

    public MyController(ServiceA serviceA, ServiceB serviceB)
    {
        this.serviceA = serviceA;
        this.serviceB = serviceB;
    }

    public void MyMethod()
    {
        // We don't need to check null because the dependency injection container 
        // injects it, provided you took care of bootstrapping it.
        var someObject = serviceA.DoThis();
    }
}

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

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

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

这样,您可以避免因违反约束集而导致的NullReferenceException情况。 例如,如果您在类中使用对象属性X ,然后尝试调用其方法之一并且X具有 null 值,那么这将导致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在设计埃菲尔编程语言时创造的。

当我们尝试访问空对象的属性或字符串值变为空并且我们尝试访问字符串方法时,会引发NullReferenceException

例如:

  1. 当一个空字符串的字符串方法被访问时:

     string str = string.Empty; str.ToLower(); // throw null reference exception
  2. 当访问空对象的属性时:

     Public Class Person { public string Name { get; set; } } Person objPerson; objPerson.Name /// throw Null refernce Exception

TL;DR:尝试使用Html.Partial而不是Renderpage


当我尝试通过发送模型来渲染视图中的视图时,我Object reference not set to an instance of an object ,如下所示:

@{
    MyEntity M = new MyEntity();
}
@RenderPage("_MyOtherView.cshtml", M); // error in _MyOtherView, the Model was Null

调试显示模型在 MyOtherView 中为 Null。 直到我将其更改为:

@{
    MyEntity M = new MyEntity();
}
@Html.Partial("_MyOtherView.cshtml", M);

它奏效了。

此外,我没有Html.Partial开始的原因是因为 Visual Studio有时会在Html.Partial下抛出看起来错误的波浪线,如果它位于不同构造的foreach循环内,即使它不是真正的错误:

@inherits System.Web.Mvc.WebViewPage
@{
    ViewBag.Title = "Entity Index";
    List<MyEntity> MyEntities = new List<MyEntity>();
    MyEntities.Add(new MyEntity());
    MyEntities.Add(new MyEntity());
    MyEntities.Add(new MyEntity());
}
<div>
    @{
        foreach(var M in MyEntities)
        {
            // Squiggly lines below. Hovering says: cannot convert method group 'partial' to non-delegate type Object, did you intend to envoke the Method?
            @Html.Partial("MyOtherView.cshtml");
        }
    }
</div>

但是我能够运行应用程序而没有遇到这个“错误”的问题。 通过将foreach循环的结构更改为如下所示,我能够摆脱错误:

@foreach(var M in MyEntities){
    ...
}

虽然我有一种感觉,这是因为 Visual Studio 误读了 & 和括号。

你能为这个做什么?

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

检查参数

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

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

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

使用工具

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

在“Microsoft Code Contracts”中,您可以使用Contract.Requires(obj != null)之类的语法,它为您提供运行时和编译检查: Introducing Code Contracts

还有“PostSharp”,它允许你使用这样的属性:

public void DoSometing([NotNull] obj)

通过这样做并使 PostSharp 成为构建过程的一部分obj将在运行时检查是否为 null。 请参阅: 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 引入了“空条件运算符”,对此有一点帮助。 使用此功能,您可以引用嵌套对象,如果其中任何一个为null ,则整个表达式将返回null

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

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

想象一下,这个country是一个Country类型的对象,它有一个名为State的属性,依此类推。 如果countryStateCountyCitynull ,则address will be. Therefore you only have to check whether . Therefore you only have to check whether is空`。

这是一个很棒的功能,但它给你的信息更少。 这4个中的哪一个为空并不明显。

像 Nullable 一样内置?

C# 有一个很好的Nullable<T>简写,你可以通过在int? .

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

您可以使用 C# 6 中的 Null 条件运算符以干净的方式修复 NullReferenceException,并编写更少的代码来处理 null 检查。

它用于在执行成员访问 (?.) 或索引 (?[) 操作之前测试 null。

例子

  var name = p?.Spouse?.FirstName;

它相当于:

    if (p != null)
    {
        if (p.Spouse != null)
        {
            name = p.Spouse.FirstName;
        }
    }

结果是当 p 为空或 p.Spouse 为空时,名称将为空。

否则,变量名将被赋予 p.Spouse.FirstName 的值。

更多细节: 空条件运算符

有趣的是,此页面上的答案都没有提到两种极端情况:

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

.NET 中的通用字典不是线程安全的,当您尝试从两个并发线程访问键时,它们有时可能会抛出NullReference甚至(更频繁地) KeyNotFoundException 在这种情况下,这个例外非常具有误导性。

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

如果unsafe代码抛出NullReferenceException ,您可能会查看指针变量,并检查它们是否有IntPtr.Zero或其他东西。 这是同一件事(“空指针异常”),但在不安全的代码中,变量通常被强制转换为值类型/数组等,并且你把头撞在墙上,想知道值类型如何抛出这个例外。

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

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

此边缘案例是特定于软件的,适用于Visual Studio 2019 IDE(可能还有早期版本)。

重现该问题的方法:将任何组件从工具箱拖到具有与主监视器不同 DPI 设置的非主监视器上的 Windows 窗体,然后您会看到一个弹出窗口,其中显示“对象引用未设置为目的。” 根据这个线程,这个问题已经知道了很长一段时间,在撰写本文时它仍未得到修复。

错误行“对象引用未设置为对象的实例”。 声明您尚未将实例对象分配给对象引用,并且您仍在访问该对象的属性/方法。

例如:假设您有一个名为 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;
     }
}

当您尝试使用的类的对象未实例化时,会发生 NullReferenceException 或未设置对象实例的对象引用。 例如:

假设您有一个名为 Student 的类。

public class Student
{
    private string FirstName;
    private string LastName;
    public string GetFullName()
    {
        return FirstName + LastName;
    }
}

现在,考虑另一个您尝试检索学生全名的班级。

public class StudentInfo
{      
    public string GetStudentName()
    {
        Student s;
        string fullname = s.GetFullName();
        return fullname;
    }        
}

从上面的代码中可以看出,语句Student s - 只声明了 Student 类型的变量,注意此时 Student 类没有被实例化。 因此,当语句s.GetFullName()被执行时,它会抛出 NullReferenceException。

好吧,简单来说:

您正在尝试访问未创建或当前不在内存中的对象。

那么如何解决这个问题:

  1. 调试并让调试器中断...它将直接将您带到损坏的变量...现在您的任务是简单地修复它..在适当的位置使用关键字。

  2. 如果由于对象不存在而导致某些数据库命令,那么您需要做的就是进行空检查并处理它:

     if (i == null) { // Handle this }
  3. 最难的一个 .. 如果GC已经收集了对象... 如果您尝试使用字符串查找对象,通常会发生这种情况... 也就是说,通过对象的名称找到它,那么可能会发生 GC 可能已经清理它...这很难找到并且会成为一个很大的问题...解决这个问题的更好方法是在开发过程中在必要时进行空检查。 这将为您节省大量时间。

按名称查找是指某些框架允许您使用字符串查找对象,代码可能如下所示: FindObject("ObjectName");

从字面上看,修复 NullReferenceExeption 的最简单方法有两种。

例如,如果您有一个带有脚本的 GameObject 和一个名为rb (刚体)的变量,那么当您开始游戏时,此变量将以 null 开头。 这就是你得到 NullReferenceExeption 的原因,因为计算机没有存储在该变量中的数据。

我将使用 RigidBody 变量作为示例。 实际上,我们可以通过以下几种方式非常轻松地添加数据:

  1. 使用 AddComponent > Physics > Rigidbody 向您的对象添加 RigidBody 然后进入您的脚本并输入rb = GetComponent<Rigidbody>(); 这行代码在您的Start()Awake()函数下效果最好。
  2. 您可以通过一行代码以编程方式添加组件并同时分配变量: rb = AddComponent<RigidBody>();

进一步说明:如果您希望Unity向您的对象添加一个组件并且您可能忘记添加一个,您可以在您的类声明上方(所有using下方的空格)键入[RequireComponent(typeof(RigidBody))]

享受制作游戏的乐趣!

要使用方法和对象的成员,您首先必须创建该对象。 如果没有创建它(应该保存该对象的变量未初始化),但是尝试使用它的方法或变量,则会收到该错误。

有时您可能只是忘记进行初始化。

编辑: new不能返回null,但是失败时会触发异常。 很久以前,某些语言就是这种情况,但现在已经不复存在了。 感谢@John Saunders指出这一点。

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

你调用的对象是空的。

这是什么意思,我该怎么做才能解决这个错误?

有可能发生与有关的情况。 在我提出解决方案之前,问题最终变得封闭: https : //stackoverflow.com/questions/43348009/unable-to-instantiate-class

注意不要实例化的类:如果类中构造函数的任何部分抛出null reference exception则该类不会实例化。 以我为例,它试图从web.config获取不存在的连接字符串。

我实例化了一个类:

ClassName myClass = new ClassName();
myClass.RunSomeMethod();

类本身内部是一个从web.config获取连接字符串的调用。 构造函数的这一部分引发了null值异常,因此myClass为null。

如果遇到无法实例化类的情况,请尝试确保类构造函数的任何部分都不会抛出null value exception F-11并逐步完成课程,确保没有空值。

这基本上是一个空引用异常 正如微软所说——

当您尝试访问值为 null 的类型的成员时,将引发 NullReferenceException 异常。

这意味着什么?

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

“嘿等等,那个成员没有价值,所以它不能执行你交给它的任务。”

异常本身表示正在引用某些内容,但未设置其值。 所以这表示它仅在使用引用类型时发生,因为值类型不可为空。

如果我们使用值类型成员,则不会发生 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 的出现是因为我们没有检查对象的值。 我们经常在代码开发中不检查对象值。

  • 当我们忘记实例化我们的对象时也会出现这种情况。 使用可以返回或设置空值的方法、属性、集合等也可能是导致此异常的原因。

如何避免?

有多种方法和方法可以避免这种著名的异常:

  1. 显式检查:我们应该坚持检查对象、属性、方法、数组和集合是否为空的传统。 这可以使用 if-else if-else 等条件语句简单地实现。

  2. 异常处理:管理此异常的重要方法之一。 使用简单的 try-catch-finally 块,我们可以控制这个异常并维护它的日志。 当您的应用程序处于生产阶段时,这可能非常有用。

  3. Null 运算符:在为对象、变量、属性和字段设置值时,也可以方便地使用 Null Coalescing 运算符和 null 条件运算符。

  4. Debugger:对于开发者来说,我们有 Debugging 的大武器。 如果我们在开发过程中遇到 NullReferenceException,我们可以使用调试器来获取异常的来源。

  5. 内置方法:GetValueOrDefault()、IsNullOrWhiteSpace() 和 IsNullorEmpty() 等系统方法检查空值,如果有空值,则分配默认值。

这里已经有很多很好的答案。 您还可以在我的博客上查看更详细的说明和示例。

希望这也有帮助!

如果在保存或编译构建期间收到此消息,只需关闭所有文件,然后打开任何文件进行编译和保存。

对我来说,原因是我重命名了文件并且旧文件仍然打开。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM