Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

Unity Needs A Method for Ensuring a SerializeField is Not Null (Especially with C# 8)

Discussion in 'Scripting' started by Lyncks, Dec 20, 2020.

  1. Lyncks

    Lyncks

    Joined:
    Dec 7, 2018
    Posts:
    9
    With Unity now supporting C# 8.0 and nullable reference types it's now possible to cut down a lot of null checking code and to warn about nullability at compile time. Unfortunately SerializeField by its current nature always needs to be nullable, which cascades out into the rest of the code and limits the usefulness of nullable reference types. Of course sometimes it is valid to have nullable SerializeField but much like the motivation behind nullable reference types, I suspect the most common usage case is actually the opposite.

    A good example is SerializeField used for prefabs passed to Instantiate. Any further usage of the instantiated object can't ever be assumed to be not null if the prefab object used to instantiate it isn't guaranteed not null.

    Another is a MonoBehaviour reference to another MonoBehaviour on the same GameObject that is going to be around for the lifetime of the GameObject.

    This could be fixed by having an additional attribute like Microsoft's [NotNull] that can be applied to SerializeField that could be read by the Editor that could throw up a warning or error if that field is not supplied with a reference. The C# code could then be able to make that assumption that the field is not null.

    Obviously there are cases where a MonoBehaviour reference that is valid at the start might be destroyed but I believe the compile-time check could validate that nothing ever makes a call to destroy or null that particular reference if it has been marked with the attribute.

    Alternatively the [NotNull] attribute could be limited to only work with references to project assets that can't be destroyed through the lifetime of the MonoBehaviour, even that would be a huge quality of life improvement.

    This would also cut down a ton of boilerplate OnValidate code trying to catch null field references as early as possible.

    Thanks for reading!
     
    Mikael-H, JVimes, canklot and 3 others like this.
  2. Lyncks

    Lyncks

    Joined:
    Dec 7, 2018
    Posts:
    9
    Small bump for visibility.
     
  3. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,915
    Well, this wouldn't be possible. First of all destroyed gameobjects are not null, they pretend to be null to indicate that they can not be used anymore. This state is an inherent property of UnityEngine.Object types that could change through countless actions that can not be tracked by the compiler. The static code analysis that the compiler does for NotNull fields only work on the assumption that a non null value can never become null through external actions that do not involve the "variable". However Unity objects can get destroyed through external actions that can not be tracked by the compiler at all. The code analysis is about variables / fields, not their values. Reference values can be passed around and copied into other variables / fields. However when the object is destroyed all those references (which still are valid references) can no longer be used due to the internal state of the object.

    Objects can also be destroyed when you load a new scene or when you destroy the parent object that contains a component somewhere on a child. This all happens on the native side and the managed side has no chance to track this or get notified.

    Sure, Unity could implement a startup check for fields marked with NotNull to produce a warning right at start that the field hasn't been assigned. However any static code analysis would not work, at all. Again the main culprit is that a null reference and a fake null object are semantically not the same thing, not even a similar thing. They look / behave similar but actually aren't.

    What I would love to have is some kind of "AutoInitialize" attribute. So that Unity would automatically do a GetComponent of the field type in case the variable is null. This would simplify self references where we would need to either drag the own gameobject onto its own variable, or have a GetComponent call in awake, both actions you can easily forget. Though those are still just convenient methods and not really code analysis based code generation.

    While some of the newer C# features are certainly nice tools, always keep in mind that Unity is not a C# / managed application. It's a native C++ engine.

    Even though I don't think they will ever remove the custom == operator because it would break too much, it's still possible that they may replace it with an explicit "isAlive" check.

    In order to be prepared for the future you could implement your own IsAlive extension method

    Code (CSharp):
    1. public static class IsAliveExtension
    2. {
    3.     public bool IsAlive(this object aObj)
    4.     {
    5.         return (aObj as UnityEngine.Object) != null;
    6.     }
    7. }
    And use it everywhere. If Unity decides to remove the == and != operators you can modify this method and everything still works.
     
  4. Neto_Kokku

    Neto_Kokku

    Joined:
    Feb 15, 2018
    Posts:
    1,751
    Serializable fields can be changed at runtime via editor inspectors, so compile time guarantees are simply not possible.
     
    Bunny83 likes this.
  5. guyguy2001

    guyguy2001

    Joined:
    Feb 27, 2014
    Posts:
    5
    I don't think that the argument that "any Object could be destroyed, and therefore any Object that you initialize via [SerializeField] has to be nullable" holds on its own, since this applies to any Object in unity (e.g ones initialized in Start/Awake), so it's more of an argument against the nullable static-analysis system than an argument against [SerializeField(nullable=false)].

    And even without any static analysis, it would be really nice if Unity wouldn't let me start the game (or would show errors on startup) if I didn't assign a SerializeObject I marked as "must not be null".
    It wouldn't save me from the object I assigned getting destroyed later, but it would help me catch a common real mistake.

    As for changing Serializable fields at runtime - that is the same in my opinion as changing them via a debugger. If you are making changes at runtime, you are responsible for the results. (That is because in my view I never do a lot of design work while the game is running - do people do a lot of non-debugging work while the game is running?)
     
  6. R1PFake

    R1PFake

    Joined:
    Aug 7, 2015
    Posts:
    533
    Since when does Unity support nullable reference types? I mean sure you can enabled it, but it doesn't look like they support it properly, because they would have to change a lot of their code base and add ? or ! everywhere otherwise the analyzer would complain about everything.

    The .NET frameworks where refactored to fully support it, but as far as I can see the Unity code was was not changed at all to support it properly and it's disabled by default.

    For example T GetComponent<T>() would have to be changed to T? GetComponent<T>() because it can return null which is not "allowed" unless they change it to T?
     
    Last edited: Dec 30, 2021
  7. Vryken

    Vryken

    Joined:
    Jan 23, 2018
    Posts:
    2,106
    If it helps, I use a utility function that is typically called on a MonoBehaviour's
    Awake
    or
    Start
    methods which takes in an enumerable of fields that should be required, and throws an exception if any of them are null for this purpose:
    Code (CSharp):
    1. public class RequiredReference {
    2.   public UnityEngine.Object Reference { get; }
    3.   public string Name { get; }
    4.  
    5.   public RequiredReference(UnityEngine.Object reference, string name) {
    6.     Reference = reference;
    7.     Name = name;
    8.   }
    9. }
    Code (CSharp):
    1. public static void RequireReferences(this MonoBehaviour mb, IEnumerable<RequiredReference> requiredReferences) {
    2.   IEnumerable<RequiredReference> nullRefs = requiredReferences.Where(p => p.Reference == null);
    3.   bool hasNullRefs = nullRefs.Count() > 0;
    4.  
    5.   if(hasNullRefs) {
    6.     IEnumerable<string> nullRefNames = nullRefs.Select(p => $"[{p.Name}]");
    7.     string joinedNames = string.Join(",", nullRefNames);
    8.  
    9.     throw new UnassignedReferenceException($"[{mb.GetType()}] is missing required references: {joinedNames}");
    10.   }
    11. }
    Example usage:
    Code (CSharp):
    1. public class SomeScript : MonoBehaviour {
    2.   public Rigidbody body //NOT assigned in inspector
    3.   public ParticleSystem vfx; //Assigned in inspector
    4.  
    5.   //Works with ScriptableObjects as well
    6.   public SomeCustomScriptableObject mySO; //NOT assigned in inspector
    7.  
    8.   private void Awake() {
    9.     this.RequireReferences(new RequiredReference[] {
    10.       new RequiredReference(body, nameof(body)),
    11.       new RequiredReference(vfx, nameof(vfx)),
    12.       new RequiredReference(mySO, nameof(mySO))
    13.     });
    14.  
    15.     //Exception message:
    16.     //"[SomeScript] is missing required references: [body], [mySO]"
    17.   }
    18. }
    I know the
    [RequireComponent]
    attribute already exists, but I don't like using it because:
    • Required components have to be on the same GameObject.
    • The attribute has a maximum of only 3 required components.
    • It does not work with abstract component types.
     
    Last edited: Dec 30, 2021
    canklot likes this.
  8. TheZombieKiller

    TheZombieKiller

    Joined:
    Feb 8, 2013
    Posts:
    265
    This is not safe because it will potentially create a GameObject outside of the main thread (which will throw an exception). You would need to create the GameObject in a RuntimeInitializeOnLoad callback and assign it to the field (and also ensure that it's persistent by calling DontDestroyOnLoad). The object could always be destroyed though, which makes it an unreliable solution even still.
     
    Bunny83 likes this.
  9. passerbycmc

    passerbycmc

    Joined:
    Feb 12, 2015
    Posts:
    1,741
    really anything that extends UnityEngine.Object i would consider to be nullable from the start and can be destroyed at any time
     
    Bunny83 likes this.
  10. TheZombieKiller

    TheZombieKiller

    Joined:
    Feb 8, 2013
    Posts:
    265
    "new GameObject()" will call the GameObject constructor, which is only allowed to be called on the main thread. Static field initialization will typically happen when a thread first accesses it, which is not guaranteed to be the main thread. As a result, it's not safe to initialize a static field directly using most Unity APIs (this actually applies to instance fields too, even in MonoBehaviours and ScriptableObjects -- they will typically be created on a background initialization thread.)

    EDIT: To elaborate further:
    Code (csharp):
    1. public static class GameObjectUtility
    2. {
    3.     public static GameObject Empty { get; } = new GameObject();
    4. }
    is equivalent to:
    Code (csharp):
    1. // The [BeforeFieldInit] attribute doesn't actually exist, it's just
    2. // representing the beforefieldinit IL flag, which the C# compiler
    3. // will emit if there is no manually defined static constructor.
    4. [BeforeFieldInit]
    5. public static class GameObjectUtility
    6. {
    7.     public static GameObject Empty { get; }
    8.  
    9.     static GameObjectUtility()
    10.     {
    11.         Empty = new GameObject();
    12.     }
    13. }
    And when you do:
    Code (csharp):
    1. public class MyComponent : MonoBehaviour
    2. {
    3.     [SerializeField]
    4.     GameObject _prefab = GameObjectUtility.Empty;
    5. }
    It is equivalent to:
    Code (csharp):
    1. public class MyComponent : MonoBehaviour
    2. {
    3.     [SerializeField]
    4.     GameObject _prefab;
    5.  
    6.     public MyComponent()
    7.     {
    8.         _prefab = GameObjectUtility.Empty;
    9.     }
    10. }
    When Unity creates an instance of the component, it will run that constructor (potentially on a background thread). This will result in GameObjectUtility being initialized (if it hasn't been already), which will then potentially result in the GameObject constructor being executed outside of the main thread, and you'll get an exception.
    Even if you initialize the field in the inspector, the value is only assigned by Unity's serialization system AFTER the constructor runs, so this problem will still occur.
     
    Last edited: Apr 16, 2022
  11. R1PFake

    R1PFake

    Joined:
    Aug 7, 2015
    Posts:
    533
    If you just want to remove the warning you could make a "dirty" workaround and use the static property, but don't actually return a "dummy" object but just null. The return null will trigger a warning, but you can disable the warning for just that property and be done with it. You "tricked" the compiler and all other usages of the property will accept that you return a "valid" instance
     
  12. TheZombieKiller

    TheZombieKiller

    Joined:
    Feb 8, 2013
    Posts:
    265
    You don't actually need to even go that far:
    Code (csharp):
    1. #nullable enable
    2. using UnityEngine;
    3.  
    4. public class MyComponent : MonoBehaviour
    5. {
    6.     [SerializeField]
    7.     GameObject _prefab = null!;
    8. }
    You can use "null!" to assign a null that the compiler will treat as non-null, which will silence any warnings.
     
    guneyozsan and R1PFake like this.
  13. canklot

    canklot

    Joined:
    Jul 3, 2020
    Posts:
    3
    You can check if serialized field is null in awake and throw and exception. It doesn't prevent playing the game but displays an red error message at the console.

    Code (CSharp):
    1. public class Character : MonoBehaviour
    2. {
    3.     [SerializeField]
    4.     InventoryManager InventoryManagerInstance;
    5.  
    6.     void Awake()
    7.     {
    8.         if (InventoryManagerInstance == null)
    9.         {
    10.             throw new UnassignedReferenceException("InventoryManagerInstance");
    11.         }
    12.     }
    13.  
    14. }