什么是 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,如何修复它? 的相关文章

  • 在 C 中匹配二进制模式

    我目前正在开发一个 C 程序 需要解析一些定制的数据结构 幸运的是我知道它们是如何构造的 但是我不确定如何在 C 中实现我的解析器 每个结构的长度都是 32 位 并且每个结构都可以通过其二进制签名来识别 举个例子 有两个我感兴趣的特定结构
  • 为什么极端下派生类(多重虚拟继承)的大小包括超类成员大小的两倍?

    include
  • 如何创建包含 IPv4 地址的文本框? [复制]

    这个问题在这里已经有答案了 如何制作一个这样的文本框 我想所有的用户都见过这个并且知道它的功能 您可以使用带有 Mask 的 MaskedTestBox000 000 000 000 欲了解更多信息 请参阅文档 http msdn micr
  • 使用接口有什么好处?

    使用接口有什么用 我听说它用来代替多重继承 并且还可以用它来完成数据隐藏 还有其他优点吗 哪些地方使用了接口 程序员如何识别需要该接口 有什么区别explicit interface implementation and implicit
  • 由 IHttpClientFactory 注入时模拟 HttpClient 处理程序

    我创建了一个自定义库 它会自动为依赖于特定服务的 Polly 策略设置HttpClient 这是使用以下方法完成的IServiceCollection扩展方法和类型化客户端方法 一个简化的例子 public static IHttpClie
  • 获取 FTP 服务器上的文件大小并将其放在标签上

    我正在尝试获取托管在FTP服务器并将其放入Label而 BackgroundWorker 在后台工作 我在用着 Try 来获取该值 但是该值在第一次尝试时被捕获 下载后 如果我按尝试再次获取它 那么它就可以工作 Note 第一次尝试时进度条
  • 标准化 UTF-8 到底是什么?

    The 重症监护室项目 http userguide icu project org transforms normalization 现在也有一个PHP库 http us php net manual en class normalize
  • 我可以使用 moq Mock 来模拟类而不是接口吗?

    正在经历https github com Moq moq4 wiki Quickstart https github com Moq moq4 wiki Quickstart 我看到它 Mock 一个接口 我的遗留代码中有一个没有接口的类
  • 如何检测表单的任何控件的变化?

    如何检测 C 中表单的任何控件的更改 由于我在一个表单上有许多控件 并且如果表单中的任何控件值发生更改 我需要禁用按钮 我正在寻找一些内置函数 事件处理程序 属性 并且不想为此创建自定义函数 不 我不知道任何时候都会触发任何事件any控制表
  • C#:帮助理解 UML 类图中的 <>

    我目前正在做一个项目 我们必须从 UML 图编写代码 我了解 UML 类图的剖析 但我无法理解什么 lt
  • C# HashSet 只读解决方法

    这是示例代码 static class Store private static List
  • 等待进程释放文件

    我如何等待文件空闲以便ss Save 可以用新的覆盖它吗 如果我紧密地运行两次 左右 我会得到一个generic GDI error
  • CMake 无法确定目标的链接器语言

    首先 我查看了this https stackoverflow com questions 11801186 cmake unable to determine linker language with c发帖并找不到解决我的问题的方法 我
  • 如何设置 log4net 每天将我的文件记录到不同的文件夹中?

    我想将每天的所有日志保存在名为 YYYYMMdd 的文件夹中 log4net 应该根据系统日期时间处理创建新文件夹 我如何设置它 我想将一天中的所有日志保存到 n 个 1MB 的文件中 我不想重写旧文件 但想真正拥有一天中的所有日志 我该如
  • 动态添加 ASP.Net 控件

    我有一个存储过程 它根据数据库中存储的记录数返回多行 现在我想有一种方法来创建 div 带有包含该行值的控件的标记 如果从数据库返回 10 行 则 10 div 必须创建标签 我有下面的代码来从数据库中获取结果 但我不知道如何从这里继续 S
  • Cmake 链接共享库:包含库中的头文件时“没有这样的文件或目录”

    我正在学习使用 CMake 构建库 构建库的代码结构如下 include Test hpp ITest hpp interface src Test cpp ITest cpp 在 CMakeLists txt 中 我用来构建库的句子是 f
  • 使用 %d 打印 unsigned long long

    为什么我打印以下内容时得到 1 unsigned long long int largestIntegerInC 18446744073709551615LL printf largestIntegerInC d n largestInte
  • 方法优化 - C#

    我开发了一种方法 允许我通过参数传入表 字符串 列数组 字符串 和值数组 对象 然后使用这些参数创建参数化查询 虽然它工作得很好 但代码的长度以及多个 for 循环散发出一种代码味道 特别是我觉得我用来在列和值之间插入逗号的方法可以用不同的
  • 我的班级应该订阅自己的公共活动吗?

    我正在使用 C 3 0 遵循标准事件模式我有 public event EventHandler
  • 如何从 ODBC 连接获取可用表的列表?

    在 Excel 中 我可以转到 数据 gt 导入外部数据 gt 导入数据 然后选择要使用的数据源 然后在提供登录信息后 它会给我一个表格列表 我想知道如何使用 C 以编程方式获取该列表 您正在查询什么类型的数据源 SQL 服务器 使用权 看

随机推荐