什么是 NullReferenceException,如何修复它?

2023-11-22

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

你调用的对象是空的。

这是什么意思?我该如何修复此错误?


原因是什么?

底线

你正在尝试使用的东西是null (or Nothing在 VB.NET 中)。这意味着您可以将其设置为null,或者您根本没有将其设置为任何内容。

就像其他任何事情一样,null被传递。如果是null in方法“A”,可能是方法“B”通过了null to方法“A”。

null可以有不同的含义:

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

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

进一步来说

The runtime扔一个NullReferenceException always意味着同样的事情:您正在尝试使用引用,并且该引用未初始化(或者它是once已初始化,但是是不再已初始化)。

这意味着参考是null,并且您无法通过null参考。最简单的情况:

string foo = null;
foo.ToUpper();

这会抛出一个NullReferenceException在第二行,因为你不能调用实例方法ToUpper() on a string参考指向null.

调试

你如何找到一个的来源NullReferenceException?除了查看异常本身(异常将在异常发生的位置准确抛出)之外,Visual Studio 中的调试一般规则也适用:放置策略断点并检查你的变量,通过将鼠标悬停在其名称上、打开(快速)监视窗口或使用各种调试面板(例如“本地”和“自动”)来实现。

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

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

Examples

一些可能引发异常的常见场景:

Generic

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, the HttpContext.Current可以为 null,或者User属性可以为 null,或者Identity属性可能为空。

Indirect

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

The new Person只创建一个实例Person,但是Books收藏还在null。收藏品Initializer语法不创建集合 为了p1.Books,它仅翻译为p1.Books.Add(...)声明。

Array

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 编译器插入了对事件使用情况的空检查,因此无需检查事件Nothing在 VB.NET 中。)

错误的命名约定:

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

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 空视图模型

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

// 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按照它们在视觉树中出现的顺序。 ANullReferenceException如果是带有事件处理程序等的早期创建的控件,则将在以下情况下引发: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>

Here comboBox1之前创建的label1. If comboBox1_SelectionChanged尝试引用`label1,它尚未被创建。

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

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

演员阵容as

var myThing = someObject as Thing;

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

LINQ FirstOrDefault() and SingleOrDefault()

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

foreach

foreach当你尝试迭代 a 时抛出null收藏。通常是由意外引起的null来自返回集合的方法的结果。

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

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

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

避免的方法

明确检查null并忽略null values.

如果您希望参考有时是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;
}

Use 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.
}

虽然这个检查不会出现在您的发布版本中,导致它抛出NullReferenceException再次当book == null在运行时处于发布模式。

Use GetValueOrDefault() for nullable值类型在出现时提供默认值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].

当 a 时提供默认值的简写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;
}

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

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

var title = person.Title.ToUpper();

如果此人没有头衔,这将引发异常,因为它正在尝试调用ToUpper具有空值的属性。

In C# 5下面,这可以通过以下方式来保护:

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

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

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

这将导致标题变量为null,并调用ToUpper不成立,如果person.Title is null.

当然,你still必须检查title for 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");

This will do the following: If myIntArray is null, the expression returns null and you can safely check it. If it contains an array, it will do the same as: elem = myIntArray[i]; and returns the ith element.

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

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

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

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

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

用于调试和修复迭代器中空解引用的特殊技术

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) { ... }

If whatever结果是null then 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();
}

为什么这是错误的?因为迭代器块实际上并不run直到foreach!致电给GetFrobs只是返回一个对象当迭代时将运行迭代器块。

通过写一个null像这样检查你可以防止NullReferenceException,但是你移动了NullArgumentException到了这一点迭代,还没有到这个地步call, 那就是调试起来非常混乱.

正确的修复方法是:

// 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对于对象,您将看到整个过程中都使用了这种技术。它写起来稍微有点笨拙,但它使调试无效错误变得更加容易。优化代码是为了调用者的方便,而不是为了作者的方便.

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

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

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

  • 取消引用 nullpointer产生与取消引用 null 相同的异常参考
  • 取消引用无效的非空指针can在某些情况下产生该异常

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

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

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

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

那么第二点呢?解引用any落在虚拟内存最低页中的无效指针会导致相同的操作系统错误,从而导致相同的异常。

为什么这是有道理的?好吧,假设我们有一个包含两个 int 的结构,以及一个等于 null 的非托管指针。如果我们尝试取消引用结构中的第二个 int,CLR不会尝试访问位置零处的存储;它将访问位置四的存储。但从逻辑上讲,这是一个空取消引用,因为我们正在访问该地址via零。

如果您正在使用不安全的代码并且您会得到NullReferenceException,只需注意有问题的指针不必为空。可以是最底层页面的任意位置,都会产生该异常。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

什么是 NullReferenceException,如何修复它? 的相关文章

  • 推导指南中的引用和值之间的差异

    考虑类型A template
  • 如何将 #ifdef DEBUG 添加到 Xcode?

    我的项目中有一些代码永远不应该在发布版本中使用 但在测试时很有用 我想做这样的事情 ifdef DEBUG Run my debugging only code endif 在 Xcode 4 中哪里添加 DEBUG 设置 我尝试将其放入
  • 在 Unity 进程和另一个 C# 进程之间进行本地 IPC 的最快方法 [关闭]

    Closed 这个问题需要多问focused help closed questions 目前不接受答案 我希望每秒大约 30 次从 C 应用程序向我的 Unity 应用程序传送大量数据 由于 Unity 不支持映射内存和管道 我考虑了 t
  • 单元测试一起运行时失败,单独运行时通过

    所以我的单元测试遇到了一些问题 我不能只是将它们复制并粘贴到这里 但我会尽力而为 问题似乎是 如果我一项一项地运行测试 一切都会按预期进行 但如果我告诉它一起运行测试 则 1 5 将通过 TestMethod public void Obj
  • 在 C# 中循环遍历文件文件夹的最简单方法是什么?

    我尝试编写一个程序 使用包含相关文件路径的配置文件来导航本地文件系统 我的问题是 在 C 中执行文件 I O 这将是从桌面应用程序到服务器并返回 和文件系统导航时使用的最佳实践是什么 我知道如何谷歌 并且找到了几种解决方案 但我想知道各种功
  • 如何在 Linq 中获得左外连接?

    我的数据库中有两个表 如下所示 顾客 C ID city 1 Dhaka 2 New york 3 London 个人信息 P ID C ID Field value 1 1 First Name Nasir 2 1 Last Name U
  • 批量更新 SQL Server C#

    我有一个 270k 行的数据库 带有主键mid和一个名为value 我有一个包含中值和值的文本文件 现在我想更新表格 以便将每个值分配给正确的中间值 我当前的方法是从 C 读取文本文件 并为我读取的每一行更新表中的一行 必须有更快的方法来做
  • Visual Studio 中的测试单独成功,但一组失败

    当我在 Visual Studio 中单独运行测试时 它们都顺利通过 然而 当我同时运行所有这些时 有些通过 有些失败 我尝试在每个测试方法之间暂停 1 秒 但没有成功 有任何想法吗 在此先感谢您的帮助 你们可能有一些共享数据 检查正在使用
  • 私有模板函数

    我有一堂课 C h class C private template
  • std::async 与重载函数

    可能的重复 std bind 重载解析 https stackoverflow com questions 4159487 stdbind overload resolution 考虑以下 C 示例 class A public int f
  • 如何对 Web Api 操作进行后调用?

    我创建了一个 Web API 操作 如下所示 HttpPost public void Load string siteName string providerName UserDetails userDetails implementat
  • C++ 密码屏蔽

    我正在编写一个代码来接收密码输入 下面是我的代码 程序运行良好 但问题是除了数字和字母字符之外的其他键也被读取 例如删除 插入等 我知道如何避免它吗 特q string pw char c while c 13 Loop until Ent
  • 有没有办法强制显示工具提示?

    我有一个验证字段的方法 如果无法验证 该字段将被清除并标记为红色 我还希望在框上方弹出一个工具提示 并向用户显示该值无效的消息 有没有办法做到这一点 并且可以控制工具提示显示的时间 我怎样才能让它自己弹出而不是鼠标悬停时弹出 If the
  • memset 未填充数组

    u32 iterations 5 u32 ecx u32 malloc sizeof u32 iterations memset ecx 0xBAADF00D sizeof u32 iterations printf 8X n ecx 0
  • 将自定义 ValueProviderFactories 添加到 ASP.NET MVC3?

    我试图尝试将 Protobuf ValueProviderFactory 添加到 MVC3 以便我可以选择 MIME 类型并将原始数据反序列化为操作参数的对象 我还可以使用它来更改默认的 Json 序列化器 看着JsonValueProvi
  • 使用 GROUP 和 SUM 的 LINQ 查询

    请帮助我了解如何使用带有 GROUP 和 SUM 的 LINQ 进行查询 Query the database IEnumerable
  • 如何将 Roslyn 语义模型返回的类型符号名称与 Mono.Cecil 返回的类型符号名称相匹配?

    我有以下代码 var paramDeclType m semanticModel GetTypeInfo paramDecl Type Type Where paramDeclType ToString returns System Col
  • 如何使用 Word Automation 获取页面范围

    如何使用办公自动化找到 Microsoft Word 中第 n 页的范围 似乎没有 getPageRange n 函数 并且不清楚它们是如何划分的 这就是您从 VBA 执行此操作的方法 转换为 Matlab COM 调用应该相当简单 Pub
  • 在客户端系统中安装后桌面应用程序无法打开

    我目前正在使用 Visual Studio 2017 和 4 6 1 net 框架 我为桌面应用程序创建了安装文件 安装程序在我的系统中完美安装并运行 问题是安装程序在其他计算机上成功安装 但应用程序无法打开 edit 在客户端系统中下载了
  • 如何正确使用 std::condition_variable?

    我很困惑conditions variables以及如何 安全 使用它们 在我的应用程序中 我有一个创建 gui 线程的类 但是当 gui 是由 gui 线程构造时 主线程需要等待 情况与下面的函数相同 主线程创建互斥体 锁和conditi

随机推荐