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

Another null ref. thread?

Discussion in 'Scripting' started by SparrowGS, Mar 27, 2018.

  1. SparrowGS

    SparrowGS

    Joined:
    Apr 6, 2017
    Posts:
    2,536
    okay, so this isn't the usual newbie forgetting to put stuff in the inspector(although i often do, haha)

    my problem is this, i have enemies that hunt down the player and his base, the base is made out of blocks that the player builds, they can be destroyed either by the player interacting with the build/destroy menu or by taking damage(like all other IDamageable interface stuff in the game)

    the problem is this, when an enemy is tracking a block and the block gets destroyed, the enemy script seems to go wonky, i'm clearly checking if it's null 3 line before the error occurs so my first guess is that the object was destroyed by another thing between those lines, by sadly this isn't this simple, for starters i use Destroy and not DestroyImmediate, second is even if that's the case, the error keeps on happening, spamming the log and the enemy freezes while all else plays the same.

    am i missing something that happens with a ref. to an interface instead an any "normal" script, mb or not?

    this is the part of the enemy script getting the error, the strafe part doesn't really have anything to do with it.
    Code (CSharp):
    1.  
    2. public class Mob_Flying : MonoBehaviour, IDamageable, Ipool {
    3.  
    4.    
    5.    Rigidbody rb;
    6.    IDamageable target;
    7.    [SerializeField]GameObject destroyedPrefab;
    8.    public bool active{ get; private set; }
    9.    float health;
    10.    //bool engaged = false;
    11.    bool strafe = false;
    12.    Vector3 wantedPosition;//target position
    13.    Quaternion wantedRotation;
    14.    bool move = false;
    15.    bool stop = false;
    16.  
    17.    [SerializeField]float maxHealth = 100;
    18.    [SerializeField]float detectionRange = 350f;
    19.    [SerializeField]float shootingRange = 75f;
    20.    [SerializeField]float attackBubble = 250f;
    21.    [SerializeField]float rotSpeed = 2;
    22.    [SerializeField]float speed = 10;
    23.    [SerializeField]float explosionStr = 250f;
    24.    [SerializeField]float explosionSize = 10f;
    25.    [SerializeField]float DPS = 10;
    26.    [SerializeField]float maxFiringAngle = 3f;
    27.    AudioSource audioS;
    28.    LineRenderer lazer;
    29.    float crashCheckDis = 30;
    30.    [SerializeField]List<RayDir>Rays = new List<RayDir>();
    31.  
    32.    [SerializeField]LayerMask enemyLM;
    33.  
    34.    public Transform _transform{ get { return transform; } }
    35.  
    36.    float sqrShootingRange { get { return shootingRange * shootingRange; } }//TODO maybe cache this?
    37.  
    38.    Vector3 AttackBubble(){
    39.        return new Vector3 (Random.Range (-attackBubble, attackBubble), attackBubble, Random.Range (-attackBubble, attackBubble));
    40.    }
    41.  
    42.    void Update(){
    43.  
    44.        if (active == false || GameMaster.instance.paused)
    45.            return;
    46.  
    47.        if (target != null) { //attack target
    48.            
    49.            if (strafe) {
    50.                wantedPosition = target._transform.position;
    51.                move = true;
    52.  
    53.                float sqrDist = (transform.position - wantedPosition).sqrMagnitude;
    54.                if (sqrDist < sqrShootingRange) {//target below min range
    55.                    wantedPosition = target._transform.position + AttackBubble ();
    56.                    strafe = false;
    57.                    lazer.enabled = false;
    58.                    audioS.Stop ();
    59.                } else if (sqrDist < sqrShootingRange * 2) {//target below max range
    60.                    ShootLazer ();
    61.                }
    62.            } else {//hover around target
    63.                
    64.                if (Vector3.Distance (transform.position, wantedPosition) < crashCheckDis) {
    65.                    
    66.                    if (CanSeeTarget()) {
    67.                        strafe = true;
    68.                    } else {
    69.                        wantedPosition = target._transform.position + AttackBubble ();
    70.                    }
    71.                }
    72.                move = true;
    73.            }
    74.            wantedRotation = Quaternion.LookRotation (wantedPosition - transform.position);
    75.        } else { //move toward cented
    76.  
    77.            target = Scan ();
    78.            wantedPosition = (target == null) ? Vector3.up * 10 : target._transform.position + AttackBubble (); //TODO replace ve3.up with highest hex in center
    79.            wantedRotation = Quaternion.LookRotation (wantedPosition - transform.position);
    80.            move = true;
    81.        }
    82.  
    83.        //Movement & collision detection
    84.  
    85.        Quaternion offset = rb.rotation;
    86.        foreach (RayDir ray in Rays) {
    87.            if (Physics.Raycast (ray.rayDirection.position, ray.rayDirection.forward, crashCheckDis)) {
    88.                offset *= Quaternion.LookRotation (ray.offsetDirection);
    89.                Debug.DrawRay (ray.rayDirection.position, ray.rayDirection.forward * crashCheckDis, Color.red);
    90.            } else {
    91.                Debug.DrawRay (ray.rayDirection.position, ray.rayDirection.forward * crashCheckDis, Color.cyan);
    92.            }
    93.        }
    94.  
    95.  
    96.        if (Physics.Raycast (transform.position, transform.forward, crashCheckDis / 2))
    97.            stop = true;
    98.  
    99.        rb.rotation = Quaternion.Slerp (rb.rotation, (offset == rb.rotation) ? wantedRotation : offset, rotSpeed * 2 * Time.deltaTime);
    100.    }
    101.  
    102.    IDamageable Scan () { // Returns first enemy found
    103.  
    104.        Collider[] cols = Physics.OverlapSphere (transform.position, detectionRange, enemyLM);
    105.        RaycastHit hit;
    106.        for (int i = 0; i < cols.Length; i++) {
    107.  
    108.            hit = new RaycastHit ();
    109.            IDamageable id = cols[i].gameObject.GetComponentInParent<IDamageable>();
    110.            if (id != null && Physics.Raycast (transform.position, cols [i].transform.position - transform.position, out hit, detectionRange) && hit.collider == cols [i])
    111.                return id;
    112.        }
    113.        return null;
    114.    }
    115.  
    116.    /*IDamageable[] Scan () { // Returns all enemies in sight
    117.  
    118.        Collider[] cols = Physics.OverlapSphere (transform.position, detectionRange, enemyLM);
    119.        List<IDamageable> ids = new List<IDamageable> ();
    120.        RaycastHit hit;
    121.        for (int i = 0; i < cols.Length; i++) {
    122.  
    123.            hit = new RaycastHit ();
    124.            IDamageable id = cols[i].gameObject.GetComponentInParent<IDamageable>();
    125.            if (id != null && Physics.Raycast (transform.position, cols[i].transform.position - transform.position, out hit, detectionRange) && hit.collider == cols[i])
    126.                ids.Add (id);
    127.        }
    128.  
    129.        if (ids.Count > 0)
    130.            return ids.ToArray ();
    131.        else
    132.            return null;
    133.  
    134.    }*/
    135.  
    136.    bool CanSeeTarget(){
    137.        
    138.        RaycastHit hit;
    139.        Physics.Raycast (transform.position, target._transform.position - transform.position, out hit, detectionRange);
    140.        if (hit.rigidbody != null && hit.rigidbody.transform == target._transform)
    141.            return true;
    142.        else
    143.            return false;
    144.    }
    145.  
    146.    void FixedUpdate(){
    147.  
    148.        if (stop)
    149.            rb.drag = 35;
    150.        else
    151.            rb.drag = 1;
    152.  
    153.        if (move && !stop)
    154.            rb.AddForce (transform.forward.normalized * speed * Time.timeScale, ForceMode.Impulse);
    155.  
    156.        move = false;
    157.        stop = false;
    158.    }
    159.        
    160.    void ShootLazer(){
    161.        
    162.        if (Mathf.Abs (Vector3.Angle (lazer.transform.forward, target._transform.position - lazer.transform.position)) < maxFiringAngle && CanSeeTarget ()) {
    163.            if (!audioS.isPlaying)
    164.                audioS.Play ();
    165.            lazer.enabled = true;
    166.            lazer.SetPosition (0, transform.position);
    167.            lazer.SetPosition (1, target._transform.position);
    168.            target.GetDamage (new HitInfo(DPS*Time.deltaTime,this.gameObject));
    169.        } else{
    170.            //lazer.SetPosition (0, Vector3.zero);
    171.            audioS.Stop ();
    172.            lazer.enabled = false;
    173.        }
    174.    }
    175.    public void GetDamage (HitInfo hit){
    176.  
    177.        if (active) {
    178.            health -= hit.damage;
    179.            if (health <= 0) {
    180.                Die ();
    181.            } else if (hit.attacker != null) {
    182.                //attack back
    183.                IDamageable id = hit.attacker.GetComponent<IDamageable> ();
    184.                if (id != null) {
    185.                    strafe = true;
    186.                    target = id;
    187.                }
    188.            }
    189.        }
    190.    }
    191.  
    192.    public void Ini (){
    193.        
    194.        active = false;
    195.        rb = GetComponent<Rigidbody> ();
    196.        audioS = GetComponent<AudioSource> ();
    197.        lazer = GetComponentInChildren<LineRenderer> ();
    198.        gameObject.SetActive (false);
    199.    }
    200.  
    201.    public void SpawnObj (Vector3 pos){
    202.  
    203.        target = null;
    204.        health = maxHealth;
    205.        transform.position = pos + (Vector3.up * 350);
    206.        transform.rotation = Quaternion.LookRotation (Vector3.up);
    207.        wantedPosition = Vector3.up * 10;
    208.        wantedRotation = transform.rotation;
    209.        active = true;
    210.  
    211.    }
    212.  
    213.    void OnCollisionEnter(Collision col){
    214.        if (col.relativeVelocity.sqrMagnitude > 75)
    215.            GetDamage (new HitInfo (col.relativeVelocity.magnitude));
    216.    }
    217.  
    218.    void Die(){
    219.  
    220.        //Debug.Log ("Enemy dead " + Time.time);
    221.        active = false;
    222.        Wreck w = PoolManager.instance.Spawn (destroyedPrefab, transform.position).GetComponent<Wreck>();
    223.        w.SetRotation (rb.rotation, rb.angularVelocity);
    224.        w.SetVelocity (rb.velocity);
    225.        Explode ();
    226.        gameObject.SetActive (false);
    227.    }
    228.  
    229.    void Explode(){
    230.        Collider[] trgs = Physics.OverlapSphere (transform.position, explosionSize);
    231.        foreach (Collider col in trgs) {
    232.            IDamageable id = col.GetComponentInParent<IDamageable> ();
    233.            if (id != null) {
    234.                float dis = Vector3.Distance (transform.position, col.transform.position);
    235.                if (dis < 1)
    236.                    id.GetDamage (new HitInfo (explosionStr));
    237.                else
    238.                    id.GetDamage (new HitInfo (explosionStr / dis));
    239.            }
    240.        }
    241.  
    242.        Collider[] cols = Physics.OverlapSphere (transform.position, explosionSize);
    243.        Vector3 t = new Vector3 (Random.Range (-explosionStr, explosionStr), Random.Range (-explosionStr, explosionStr), Random.Range (-explosionStr, explosionStr));
    244.        foreach (Collider col in cols) {
    245.            if (col.attachedRigidbody != null) {
    246.                col.attachedRigidbody.AddExplosionForce (explosionStr, transform.position, 25, 0, ForceMode.Impulse);
    247.                col.attachedRigidbody.AddTorque (t,ForceMode.Impulse);
    248.            }
    249.        }
    250.    }
    251. }
    252.  
    this is the way i destroy the game object, please tell me if i'm wrong
    this is the script attached to the GameObject
    Code (CSharp):
    1. public class HexRep : MonoBehaviour, IDamageable {
    2.  
    3.     bool active;
    4.     float health;
    5.     [SerializeField]float maxHealth;
    6.     public Transform _transform{ get { return transform; } }//Part of IDamageable
    7.  
    8.     public Hex myHex;
    9.  
    10.     void Start(){
    11.         health = maxHealth;
    12.         active = true;
    13.     }
    14.  
    15.     public void GetDamage(HitInfo hi){ //Part of IDamageable
    16.         health -= hi.damage;
    17.         if (health < 0 && active)
    18.             Die ();
    19.     }
    20.  
    21.     void Die(){
    22.         active = false;
    23.         MapMaster.instance.RemoveHex (myHex);
    24.  
    25.     }
    26. }
    and this is the MapMaster RemoveHex function:
    Code (CSharp):
    1.     public void RemoveHex(Hex hex){
    2.  
    3.         if (hex == null) {
    4.             Debug.LogError ("No hex to destroy!");
    5.             return;
    6.         }
    7.  
    8.         if (hexes.ContainsKey (hex.coord) == false) {
    9.             Debug.LogError ("Hex at: " + hex.coord.ToString() + " isn't registered!");
    10.             return;
    11.         }
    12.  
    13.         Destroy (hex.rep.gameObject);
    14.         hex.rep = null;
    15.         hexes.Remove (hex.coord);
    16.     }
     
    Last edited: Mar 27, 2018
  2. Mokzen

    Mokzen

    Joined:
    Oct 10, 2016
    Posts:
    102
    Where/when is "strafe" defined?
     
  3. N00MKRAD

    N00MKRAD

    Joined:
    Dec 31, 2013
    Posts:
    210
    ...what is _transform? Maybe the underscore is causing the problems?
     
  4. Nigey

    Nigey

    Joined:
    Sep 29, 2013
    Posts:
    1,129
    When I use Interfaces I tend to make a GameObject GameObject { get; } in the interface, to make sure that the scene object exists and not just the reference. Plus it's nice to have a gameObject when you have an interface. Perhaps you could just have a bool Exists { get; } and internally do a gameObject check on the interface.

    However really what you want to do is do a breakpoint on strafe and see what's happening. Make sure strafe exists, check whether target exists, check whether it's transform exists. Without a breakpoint you'll be missing your vital eyes and ears on what's actually happening in your code. I can't confirm the issue isn't somewhere else, as I don't know how target's assigned, or what strafe is, but it does sound likely that the interface reference is there, but not the gameObject. Perhaps check that.
     
    SparrowGS likes this.
  5. BlackPete

    BlackPete

    Joined:
    Nov 16, 2016
    Posts:
    970
    Yep, this. It's transform, not _transform.
     
  6. SparrowGS

    SparrowGS

    Joined:
    Apr 6, 2017
    Posts:
    2,536
    Didn't really understand this part, I think i'm doing as you suggest only using a Transform and not a GameObject


    I'll update the op with the enitre enemy script, but i think looking at the update loop is enough, please tell me if you see something wrong.
    I'll go try again, thanks for the advice

    strafe is defined within this enemy class and is just used to determine if the enemy should go straight up to the player or do something else(like flanking), if strafe is equal to false the error occurs on another line, but the problem remains the same, the check for if(target != null) returns true even after the target is destroyed.
    Code (CSharp):
    1.  
    2.     Vector3 AttackBubble(){
    3.        return new Vector3 (Random.Range (-attackBubble, attackBubble), attackBubble, Random.Range (-attackBubble, attackBubble));
    4.    }
    5.  
    6.  
    7. //inside the update loop
    8. wantedPosition = target._transform.position + AttackBubble ();
    both of you obviously didn't read the entire post, because i'm using an interface i dont have access to the mb.transform so _transform belongs to the IDamageable interface and is returning the transform.

    Code (CSharp):
    1.     public Transform _transform{ get { return transform; } }//Part of IDamageable
     
  7. SparrowGS

    SparrowGS

    Joined:
    Apr 6, 2017
    Posts:
    2,536
    Did some more testing, how is it possible that this statement is causing a runtime error?

    Code (CSharp):
    1.         if (target != null && target._transform != null) {
    MissingReferenceException: The object of type 'HexRep' 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.
    HexRep.get__transform () (at Assets/Scripts/HexRep.cs:10)
    Mob_Flying.Update () (at Assets/Scripts/Mob_Flying.cs:55)
     
  8. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,513
    This is an issue that infuriates me about Unity.

    See, Unity has overloaded the == operator for UnityEngine.Object. When you use compare on UnityEngine.Object it does this instead:
    Code (csharp):
    1.  
    2.     public static bool operator ==(Object x, Object y)
    3.     {
    4.       return Object.CompareBaseObjects(x, y);
    5.     }
    6.  
    7.     private static bool CompareBaseObjects(Object lhs, Object rhs)
    8.     {
    9.       bool flag1 = (object) lhs == null;
    10.       bool flag2 = (object) rhs == null;
    11.       if (flag2 && flag1)
    12.         return true;
    13.       if (flag2)
    14.         return !Object.IsNativeObjectAlive(lhs);
    15.       if (flag1)
    16.         return !Object.IsNativeObjectAlive(rhs);
    17.       return lhs.m_InstanceID == rhs.m_InstanceID;
    18.     }
    19.  
    The object will return true when compared to null if the native object (the unity C++ side of things) has been destroyed. Despite the C# managed object still exists.

    Thing is, this only works if the variable is typed to UnityEngine.Object or one if it's child types.

    If it's typed to say System.Object, or as an interface... the compiler doesn't know this overload should be used. And it just does the usual object.ReferenceEquals(target, null) instead. Effectively only testing the C# side of things. And well... the managed C# object isn't null.

    ...

    Unity implemented this years ago back when Unity was intended to be VERY newb friendly. They didn't consider this use case like interfaces. They since realized the issue, and even had a blog post about it where they considered fixing it. But since it would break existing code... they didn't.

    I wish they did.

    ...

    So in your code:
    Code (csharp):
    1. if (target != null && target._transform != null)
    target isn't null, BUT target was destroyed, to you can access the 'transform' of it. Just like the exception says.
     
    SparrowGS likes this.
  9. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,513
    You'll have to cast it to the UnityEngine.Object to check if it was destroyed.

    What I do is I created a 'IsNullOrDestroyed' static function to do this for all System.Objects. It also evaluates for some other special interface contracts I have defined.

    https://github.com/lordofduct/space...pacepuppyUnityFramework/Utils/ObjUtil.cs#L745

    Unfortunately unity has made the method IsNativeObjectAlive an internal function, so I have to reflect it out. As you can see here:

    Code (csharp):
    1.  
    2.         private static System.Func<UnityEngine.Object, bool> _isObjectAlive;
    3.         public static System.Func<UnityEngine.Object, bool> IsObjectAlive
    4.         {
    5.             get { return _isObjectAlive; }
    6.         }
    7.      
    8.  
    9.         static ObjUtil()
    10.         {
    11.             try
    12.             {
    13.                 var tp = typeof(UnityEngine.Object);
    14.                 var meth = tp.GetMethod("IsNativeObjectAlive", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic);
    15.                 if (meth != null)
    16.                 {
    17.                     var d = System.Delegate.CreateDelegate(typeof(System.Func<UnityEngine.Object, bool>), meth) as System.Func<UnityEngine.Object, bool>;
    18.                     _isObjectAlive = (a) => !object.ReferenceEquals(a, null) && d(a);
    19.                 }
    20.                 else
    21.                     _isObjectAlive = (a) => a != null;
    22.             }
    23.             catch
    24.             {
    25.                 //incase there was a change to the UnityEngine.dll
    26.                 _isObjectAlive = (a) => a != null;
    27.                 UnityEngine.Debug.LogWarning("This version of Spacepuppy Framework does not support the version of Unity it's being used with. (ObjUtil)");
    28.                 //throw new System.InvalidOperationException("This version of Spacepuppy Framework does not support the version of Unity it's being used with.");
    29.             }
    30.         }
    31.  
    32.         /// <summary>
    33.         /// Returns true if the object is either a null reference or has been destroyed by unity.
    34.         /// This will respect ISPDisposable over all else.
    35.         /// </summary>
    36.         /// <param name="obj"></param>
    37.         /// <returns></returns>
    38.         public static bool IsNullOrDestroyed(this System.Object obj)
    39.         {
    40.             if (object.ReferenceEquals(obj, null)) return true;
    41.  
    42.             if (obj is ISPDisposable)
    43.                 return (obj as ISPDisposable).IsDisposed;
    44.             else if (obj is UnityEngine.Object)
    45.                 return !_isObjectAlive(obj as UnityEngine.Object);
    46.             else if (obj is IComponent)
    47.                 return !_isObjectAlive((obj as IComponent).component);
    48.             else if (obj is IGameObjectSource)
    49.                 return !_isObjectAlive((obj as IGameObjectSource).gameObject);
    50.          
    51.             return false;
    52.         }
    53.  
    Of course you could just cast and do ==, I did this to avoid the lengthy overload that unity implemented and instead directly call IsNativeObjectAlive.

    But yeah, you can do it like so:
    Code (csharp):
    1.  
    2.         /// <summary>
    3.         /// Returns true if the object is either a null reference or has been destroyed by unity.
    4.         /// This will respect ISPDisposable over all else.
    5.         /// </summary>
    6.         /// <param name="obj"></param>
    7.         /// <returns></returns>
    8.         public static bool IsNullOrDestroyed(this System.Object obj)
    9.         {
    10.             if (object.ReferenceEquals(obj, null)) return true;
    11.            
    12.             if(obj is UnityEngine.Object) return (obj as UnityEngine.Object) == null;
    13.  
    14.             return false;
    15.         }
    16.  
     
    IggyZuk, Zikran, Nigey and 1 other person like this.
  10. SparrowGS

    SparrowGS

    Joined:
    Apr 6, 2017
    Posts:
    2,536
    THANK YOU, i was going crazy over this!

    i knew it something to do with the c++ c#, how do you suppose the best way to get around this?
     

    Attached Files:

    • WTF.png
      WTF.png
      File size:
      166.6 KB
      Views:
      900
  11. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,513
    See my second post.

    The second code snippet is what you'll want.
     
    SparrowGS likes this.
  12. SparrowGS

    SparrowGS

    Joined:
    Apr 6, 2017
    Posts:
    2,536
    was just reading through it, you sir are the man, thanks a ton.
     
    Last edited: Mar 28, 2018
  13. ifightnoman

    ifightnoman

    Joined:
    Jan 15, 2022
    Posts:
    13
    Incredible. Truly forbidden knowledge that should be emblazoned all over Unity documentation. As a Unity "newbie" (2 years in) this has been an issue that has plagued me from day 1, and I somehow never found this post. I just continued to blindly null check and be left baffled when it didn't work. Then I'd read dozens, nay, hundreds of forum posts saying "just use null comparison." As a 20+ year software engineer this null overload has to be one of the worst programming mistakes I've ever seen, and in Unity that's saying something.

    If there is a Unity dev out there with any semblance of empathy, for the love of god at least document the MissingReferenceException to include a warning with this knowledge. A simple line like, "Unity has overloaded the equals operator, but it only works if your declared type is UnityEngine.Object. Are you using an interface?" would collectively save thousands of debugging hours.
     
  14. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,912
    Well, it's certainly not as bad as you try to portrait it here. What most people who read the blog post about if they should remove the overload miss is that is would not change the situation at all. The fact that you end up will a broken object that is not null but can't be used anymore does not change in the slightest. The only difference would be that you have to manually do a null check as well as an "alive" check afterwards. When you use an interface you probably don't have access to the alive check unless you include it in your interface definition. So things just get less convenient but does not magically just work. All the "new" null conditional operators (?. and the like) that didn't even exist back then still won't work. So the actual situation does not change at all. Currently, when your variable has the correct type, you simply get an additional rejection if the object is not alive. When they remove the == overload you just don't have that anymore. The problems are still exactly the same.
     
    DragonCoder likes this.
  15. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,514
    YES!!! I don't know why this is so hard for engineers to grasp.

    That parrot is dead. Null! False! Deceased! Insert all the rest of the Monty Python Dead Parrot sketch here.

    Exactly, and it's just like the solution... it is ALWAYS the same... ALWAYS!!

    How to fix a NullReferenceException error

    https://forum.unity.com/threads/how-to-fix-a-nullreferenceexception-error.1230297/

    Three steps to success:
    - Identify what is null <-- any other action taken before this step is WASTED TIME
    - Identify why it is null
    - Fix that

    To be fair though, this failure lies squarely with the C# language. If the ?? and ?. operators are supposed to perform an "if (x != null)" test, they really should just perform it and route it through the same overload chain.

    I'm sure the language weenies will come in here blithering about why they didn't route the check down that same equality-to-null operator but it's still a non-orthogonal wart in the C# language spec.

    That's why I stay away from that new wiggy-whacky question mark stuff, and besides it makes for horrible lines of code that make your beautiful C# start to look like Perl.

    And it doesn't "read" better... it just means when your code breaks (and it will!) and I get called in to fix your "readable" code, I get to rewrite your code so I can actually put a flippin' breakpoint on the extremely-rarely-called delegate call site, which I couldn't do before when it was
    RarelyUsedDelegate?.Invoke()
     
  16. lordofduct

    lordofduct

    Joined:
    Oct 3, 2011
    Posts:
    8,513
    To clarify my very old post here because I didn't necessarily say it explicitly.

    I always felt == should have never been the way of doing it. And instead have an explicit "IsDestroyed(UnityEngine.Object obj)" method (basically what I implemented of my own accord in the form of IsNullOrDestroyed).

    The fact that == null checks both null AND destroyed is not descriptive. You need to know that it's doing this extra thing and why it's doing it and how it's doing it resulting in the scenario where it fails to behave consistently based on the type it's cast too. Resulting in what I would call a code smell.

    I also agree the fact that the ??, ??=, and ?. operators don't rely on the same execution chain is also a big "huh?" in the part of C#. Honestly the ==, instance.Equals, and static object.ReferenceEquals all behaving in very different ways is super confusing as well completely unrelated to Unity.

    Case in point... string has a VERY similar issue to UnityEngine.Object with the == operator. Just not with null.

    You can have 2 strings that are equal, but not evaluate equal if not typed as string. Build 2 strings via some means independent means to be the same value. And then cast them to object and compare them and they likely will return false because they're not the same instances... they're distinct strings of same value. But when cast as string they compare true because string overloads == to do a comparison rather than a reference test.

    But like I also get it... it's convenient.

    But back at the UnityEngine.Object. I think my real beef is that it disguises the way Unity and C# interact. A lot of users can go years not even realizing there's actually 2 sides to all unity objects. And it's hidden behind this non-descript operator that lacks in depth documentation (while blog posts are nice, they aren't documentation).

    Do I think it's the most egregious thing in the world? Oh heck no. But... it's a sad case of premature design that resulted in a confusing interface down the line and undoing it is rather difficult.

    It ain't like I never made the same mistakes...
     
    Last edited: Aug 17, 2023
    SisusCo likes this.
  17. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    7,606
    Hindsight is always 20/20. Of course if Unity was coded today they would've done differently.

    There's lots of little quirks/mistakes from the older parts of Unity's code base. Such as
    EditorApplication.delayCall
    just being a regular delegate, and not an event, meaning someone can just straight assign a method to it (
    =
    as opposed to
    +=
    ) and wipe out all other subscriptions... which can and has happened from Unity's code.
     
    Last edited: Aug 17, 2023
    SisusCo, Kurt-Dekker and Bunny83 like this.