Unity 之 NullReferenceException(空引用异常)问题

举报
陈言必行 发表于 2021/08/30 22:10:54 2021/08/30
【摘要】 当您试图访问没有引用任何对象的引用变量时发生。如果引用变量没有引用对象,那么它将被视为null。当变量为null发出NullReferenceException.

什么是NullReferenceException(空引用异常)?

来自官方的诠释:https://docs.unity3d.com/Manual/NullReferenceException.html
一个NullReferenceException当你试图访问没有引用任何对象的引用变量时发生。如果引用变量没有引用对象,那么它将被视为null。当变量为null发出NullReferenceException.

C#和JavaScript中的引用变量在概念上与C和C+中的指针相似。引用类型默认为null若要指示它们没有引用任何对象,请执行以下操作。因此,如果您尝试访问正在被引用的对象,但是没有引用对象,您将得到一个NullReferenceException.

当你得到一个NullReferenceException在代码中,这意味着在使用变量之前忘记设置变量。错误消息将类似于:

NullReferenceException: Object reference not set to an instance of an object
at Example.Start () [0x0000b] in /Unity/projects/nre/Assets/Example.cs:8
此错误消息表明NullReferenceException发生在脚本文件的第8行Example.cs。另外,消息说异常发生在Start()功能。这使得Null引用异常很容易找到和修复。在本例中,代码是:

using UnityEngine;
using System.Collections;

public class Example : MonoBehaviour {

    // Use this for initialization
    void Start () {
        GameObject go = GameObject.Find("cube");
        Debug.Log(go.name);
    }
}

代码只需查找一个名为“cube”的游戏对象。在本例中,没有具有该名称的游戏对象,因此Find()函数返回null。在下一行,我们使用go变量,并尝试打印出它引用的游戏对象的名称。因为我们访问的是一个不存在的游戏对象,运行时给我们提供了一个NullReferenceException



一种常见的处理方法:

使用try/catch检测,,,

  • 一个原因NullReferenceException对象中应该初始化的变量。是否被初始化过。
  • ** 另一种处理方法NullReferenceException是使用TRY/CATCH块。**

例如,此代码:

using UnityEngine;
using System;
using System.Collections;

public class Example : MonoBehaviour {
 // Use this for initialization
    void Start () {
        try {
            GameObject go = GameObject.Find("cube");
            Debug.Log(go.name);
        }       
        catch (NullReferenceException ex) {
            Debug.Log("myLight was not set in the inspector");
        }
    }
 }

摘要

NullReferenceException当脚本代码试图使用未设置(引用)和对象的变量时发生。
出现的错误消息告诉您在代码中问题发生的位置。
NullReferenceException可以通过编写检查null在访问对象或使用TRY/CATCH块之前。


我遇到的问题:

来自Android模拟器的误导:(绿色部分是catch到的异常)
nullReference
Unity Edior 的报错:

MissingReferenceException: The object of type ‘GameObject’ has been
destroyed but you are still trying to access it. Your script should
either check if it is null or you should not destroy the object.
UnityEngine.GameObject.GetComponent[RectTransform] () (at
C:/buildslave/unity/build/artifacts/generated/common/runtime/GameObjectBindings.gen.cs:38)

解决方法:
发现是在类似清理的方法里面做了,赋值的初始化导致。
推荐查看:https://blog.csdn.net/Czhenya/article/details/84333583



今天(19.05.07)在网上看到了一个关于空引用异常描述的比较详尽的文件,借鉴来和大家一起分享,学习一下,原文链接:http://blog.theknightsofunity.com/story-nullreferenceexception-part-22/
大致译文如下;

不要信任GetComponent()

GetComponent()是访问电流的常用函数。游戏对象组件。这里有一个代码片段,显示如何禁用对象的呈现器,因此它在游戏视图中不再可见。

var renderer=GetComponent<Renderer>();
renderer.enable=false

当然了GetComponent()会回来零如果当前对象没有请求的组件。这是一个严重的风险,因为设计人员可以随时在没有任何警告的情况下轻松地删除组件。

Use [RequireComponent] :
首先你应该用[索取成分]只要有可能就进行注释。

[RequireComponent(typeof(Renderer))]
public class Player : MonoBehaviour
{
    void Start()
    {
        var renderer = GetComponent<Renderer>();
        renderer.enabled = false;
    }
}

请注意[RequireComponent]不能保证给定组件始终可用。它实际上所做的是在对象设置期间添加缺少的组件。没有什么稀奇古怪的,但它降低了NullReferenceException一点。


决定失败后该怎么做

是的,你绝对应该做一个空检查。但是,如果组件碰巧丢失了,您必须决定要做什么。一般来说,你有三个选择。

1.显示错误并忽略。
2.将事件报告为bug
3.试着恢复。
正如您所看到的,抛出异常是没有选择的。那是因为我们希望我们的游戏尽可能稳定。

1。显示错误并忽略

这是处理这类bug最简单的方法。如果您在Unity编辑器中遇到它们,那么您将有一个很好的机会在它发布之前修复它。然而,如果这个问题出现在你的播放器设备上,你的情况就不会那么舒服了,而这些设备通常是你无法直接访问的。

[RequireComponent(typeof(Renderer))]
public class Player : MonoBehaviour
{
    void Start()
    {
        var renderer = GetComponent<Renderer>();
        if (renderer)
        {
            renderer.enabled = false;
        } else
        {
            Debug.LogError("Missing Renderer component", this);
        }
    }
}

请注意,在第7行,我写的是if (renderer)而不是if (renderer != null)。那是因为UnityEngine。对象覆盖了(bool)操作符,这是执行空检查的一个很好的快捷方式。


2. 将事件报告为bug

如果有机会,您可能希望将此事件报告为bug。自动bug报告可以帮助您修复生产中的bug,因此您应该认真考虑这种实现。当然,你必须请求玩家允许你发送bug报告,否则它可能违反了与谷歌Play或App Store的协议,因此你的应用程序可能会被禁止。
我个人使用的脚本名为SRDebugger,来自资产存储。它允许显示一个覆盖表单,允许将bug报告发送到我的电子邮件地址。

[RequireComponent(typeof(Renderer))]
public class Player : MonoBehaviour
{
    void Start()
    {
        var renderer = GetComponent<Renderer>();
        if (renderer)
        {
            renderer.enabled = false;
        } else
        {
            SRDebug.Instance.ShowBugReportSheet(
                null, true, "Missing Renderer Component");
        }
        
    }
}

3.尝试恢复

这是第三种选择,也是唯一能够让你的游戏正常运行的选择。我认为它永远不应该被使用,因为有烟就有火。恢复只是掩盖烟雾,而你应该开始寻找火灾。

[RequireComponent(typeof(Renderer))]
public class Player : MonoBehaviour
{
    void Start()
    {
        var renderer = GetComponent<Renderer>();
        if (!renderer)
        {
            renderer = gameObject.AddComponent<Renderer>();
            Debug.LogWarning("Creating missing component Renderer");
        }
 
        renderer.enabled = false;
    }
}

Assert GetComponent()
记住,你应该尽快寻找任何问题。如果您的脚本将来使用某个组件,请确保在开始时检查该组件是否可用。

[RequireComponent(typeof(Renderer))]
public class Player : MonoBehaviour
{
    private Renderer _renderer;
 
    void OnEnable()
    {
        _renderer = GetComponent<Renderer>();
        Assert.IsNotNull(_renderer, "Missing Renderer Component");
    }
 
    public void Hide()
    {
        _renderer.enabled = false;
    }
}

注意回调!
如果你的游戏包含从外部代码和/或从另一个线程调用的回调,你可能会发现自己处于一种情况,当NullReferenceException被抛出时。当回调函数在发出请求几秒钟后被调用时,它可以发生在Social API中。在那个时候,发出请求的对象可能已经不存在了。

这不是真的,但我们可能会遇到我们之前讨论过的伪空问题。让我们考虑这样一个场景:
结果加载场景。
ResultsScreen脚本正在为当前玩家的分数请求社交API。
玩家点击“主菜单”按钮。
结果场景被卸载。
加载主菜单场景。
正在ResultsScreen脚本中调用Social API回调。
因此,如果结果场景被卸载,那么在执行第六步时,所有的对象都应该被销毁。这是不正确的,因为对ResultsScreen对象的引用仍然有效,对象仍然存在,但是它通过伪造自己的null检查将自己报告为null,正如我在前一篇文章中解释的那样。它可能是这样的:

public class ResultScreen : MonoBehaviour
{
    public Text ScoreLabel;
 
    void Start() {
        Social.LoadScores("Leaderboard01", scores =>
        {
            if (scores.Length > 0)
                ScoreLabel.text = scores[0].formattedValue;
        });
    }
}

您能猜到您将在哪里接收到NullReferenceException吗?在第10行,ScoreLabel是否被分配并不重要。此时您将无法访问它。
如何在这种情况下保护自己?答案很奇怪,但很简单。你应该对这个做一个空检查。

public class ResultScreen : MonoBehaviour
{
    public Text ScoreLabel;
 
    void Start() {
        Social.LoadScores("Leaderboard01", scores =>
        {
            if (!this)
                Debug.LogWarning("Current object no longer usable. Discarding callback.");
                return;
 
            if (scores.Length > 0)
                ScoreLabel.text = scores[0].formattedValue;
        });
    }
}

是的,这是正确的!当前对象不会为空,因为它根本不可能是空的,但由于Unity正在伪造空检查,这是检查当前对象是否仍然可以使用的最好方法!
当然,您可以空检查将要使用的所有对象,但是在更复杂的场景中,这可能太复杂了。检查这个是否为空应该足够了。
保持冷静和单身
单例通常被认为是反模式的。由于简单和易于实现,仍然广泛使用单例。
在Unity中,最常见的单例类型是:
根据需要创建单例
引擎创建的单例程序(源自MonoBehaviour)
让我们看看单例的第二种类型。

class Singleton : MonoBehaviour
{
    public static Singleton Instance { get; private set; }
 
    void Awake()
    {
        Instance = this;
    }
}

首先,您应该确保尽快分配了单例实例字段,因此我将在Awake()方法中分配它。但这还不够。有一个严重的风险是Awake()不会被调用一次。这种风险是由场景中存在单例对象的假设引起的。
知道了这一点,你必须在每个类中创建一个断言,你打算使用这个单例:

class MyScript : MonoBehaviour
{
    void Start()
    {
        Assert.IsNotNull(Singleton.Instance, "Missing Singleton!");
    }
}

注意,我在Awake()函数中分配了一个单例,但在Start()函数中测试它的null值,而不是OnEnable()函数。
这是因为Awake()和OnEnable()函数会为每个已启用的GameObject逐一调用。在官方文档中没有很好地解释,所以我将试着澄清这一点。
有脚本:ScriptA和ScriptB都具有Awake()、OnEnable()和Start()函数。调用顺序可能是这样的:

ScriptA: Awake()
ScriptA: OnEnable()
ScriptB: Awake()
ScriptB: OnEnable()
ScriptA: Start()
ScriptB: Start()

建议吗?
对于如何通过保护代码不受nullreferenceexception的影响而使项目健壮,您还有其他有趣的建议吗?请在下面的评论栏中分享你的建议!我会很感激的!

【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。