Search Unity

Huh? varName != null is false, when varName is not null?

Discussion in 'Scripting' started by jashan, Jan 9, 2008.

  1. jashan

    jashan

    Joined:
    Mar 9, 2007
    Posts:
    3,307
    I'm really confused right now... either I'm completely blind, or the Mono-framework or Unity is doing something VERY strange:

    Code (csharp):
    1.  
    2. TRaceONTeam teamToBeDeleted = null;
    3. foreach (TRaceONTeam team in teams) {
    4.     if (team.uniqueID.Equals(teamID)) {
    5.         teamToBeDeleted = team;
    6.         print("FOUND IT!");
    7.         break;
    8.     }
    9. }
    10. print(string.Format("B4: Team to be deleted: {0}", teamToBeDeleted));
    11. if (teamToBeDeleted != null) {
    12.     teams.Remove(teamToBeDeleted);
    13. } else {
    14.     print(string.Format("Can't find team: {0}, {1}", teamToBeDeleted, teamID));
    15. }
    16. print(string.Format("After: Team to be deleted: {0}", teamToBeDeleted));
    17.  
    What puzzles me: I get "found it", then I get the B4-Logmessage (and, guess what: it is NOT null), then, I get "Can't find team" which means that teamToBeDeleted != null is FALSE, even though just before that, it was still printed... And now, in the "didn't find it" message, it's printed again (so it is, in fact NEVER null). And afterwards, again.

    TRaceONTeam is a subclass of MonoBehaviour... is there any tricks or magic involved here that I'm not aware of? Something like the != operator has been changed to never return true when compared to null (that can't be because I'm using other code that relies on this, too)? Or have I just been working too much and don't see something obvious?

    Jashan
     
  2. jashan

    jashan

    Joined:
    Mar 9, 2007
    Posts:
    3,307
    Um... maybe this CAN be? I realized I was actually doing another test...

    The documentation is not perfectly specific on this (just says "Compares if two objects refer to a different object", but what if one of those objects is not an object but null): Object.operator !=

    Is it possible that there's a bug in that implementation? I would assume that in JavaScript, that never even gets to the surface because there I would have simply written if (!variableName) ...

    Jashan
     
  3. pete

    pete

    Joined:
    Jul 21, 2005
    Posts:
    1,647
    if you get "found it", then teamtobedeleted is not null (it's set to = team the line before). so is the teamID line always returning a match? just a thought...
     
  4. jashan

    jashan

    Joined:
    Mar 9, 2007
    Posts:
    3,307
    Yup, team.uniqueID.Equals(teamID) does always match with exactly one of the teams. At first I thought that the problem was there (in the Equals-check). However, if it did not match, I wouldn't get the "found it", and I would get quite a few "nulls" from the other statements where I print teamToBeDeleted.

    I've now converted this to an approach will nullable primitive types in which I rely on the fact that I can access the list via indices. This way, it works just fine, so I think it really has to do with the MonoBehaviour.

    Code (csharp):
    1.  
    2. int? index = null;
    3. for (int i = 0; i < teams.Count; i++) {
    4.     if (teams[i].uniqueID.Equals(teamID)) {
    5.         index = i;
    6.         break;
    7.     }
    8. }
    9. if (index != null) {
    10.     teams.RemoveAt((int) index);
    11. }
    12.  
    Could it be this is a bug in the !=-implementation of Unity Objects?

    Jashan
     
  5. pete

    pete

    Joined:
    Jul 21, 2005
    Posts:
    1,647
    i'd be surprised if it's a bug. i don't know what traceonteam does but maybe if there are no teams, teamID = 0 and team.uniqueid = 0 so it's never null where your new array approach returns null if the array is empty. glad you got it solved anyway.
     
  6. Joachim_Ante

    Joachim_Ante

    Unity Technologies

    Joined:
    Mar 16, 2005
    Posts:
    5,203
    I don't think it's likely to be an issue in the != operator.

    I think it's best if you reduce the script to something you can post here which we can try standalone so we can see what you are doing in the script exactly.
     
  7. jashan

    jashan

    Joined:
    Mar 9, 2007
    Posts:
    3,307
    Hi Joachim,

    as soon as I find time, I'll create a "micro-testproject" and see if I can reproduce the behavior there, and if so, send that to you as bug report. As I'm right now in the middle of making my game multiplayer (aside of my job to earn money ;-) ), that may take a few days, though (team-creation already works "multiplayer", and I've just finished the "spawning-scene", but it's really hacky and even though it seems to work more or less correctly over the net, my 2 PCs crash during that scene most of the time, probably because I have created a lot of bugs that I'm hoping to fix tomorrow ;-) )...

    I wish I had a few more Macs for testing ;-)

    Jashan... saying good-night at 2am ;-) ... once again 8)
     
  8. jashan

    jashan

    Joined:
    Mar 9, 2007
    Posts:
    3,307
    I couldn't resist testing this "all naked", and so I figured it out...

    The reason was that I didn't really use my subclasses of MonoBehavior as it was designed: What it's designed for is using it attached to instances of GameObject as components (which is what I'm doing most of the time). However, I'm also using it as a simple data container, separate of game objects. While I was aware that I can't use the constructor to do initialization stuff (that's what Awake() and Start() are for), I was not aware that I couldn't simply directly create instances of my subclasses of MonoBehavior.

    So, for a short version of this, don't EVER do something like:

    Code (csharp):
    1.  
    2. MyConvenientMonoBehaviour niceobject = new MyConvenientMonoBehaviour();
    3.  
    That does break the != and == operators (and probably much more).

    Those are the moments when I'm realizing that I'm still new to Unity ;-)

    However, I would say this really is a bit of a design issue in Unity, too. Those constructors should not be publicly accessible. If they were properly hidden, I would immediately get a compiler error when trying what I did (it may be, though, that Mono/.NET/C# doesn't really provide the means to do that "right" conveniently).

    I only realized it, when creating an instance of a MonoBehavior-instance directly, and getting a NullReferenceException by accessing it's name property (that's really tricky, because monoBehaviour.name looks like a variable, but in reality, it is a property, and I would assume it really assigns and returns the name of the game object the component is attached to, which I think is really nice).

    When doing

    Code (csharp):
    1.  
    2. myMonoBehaviourInstance.name = "New Name";
    3.  
    and receiving a NullReferenceException from it, it really looks like myMonoBehaviourInstance is null. Thank god that this funny convention of having small case properties is documented, so I had the chance of realizing my error ;-)

    I'm including the whole test-script below - with this, you can very easily reproduce the behavior I stumbled upon. I'm not sure this would be considered a bug, but it's definitely something that can give you significant headaches when you're not aware of it. Is that at least mentioned anywhere in the manual? I think it should be mentioned at least in the API doc of MonoBehaviour and Component.

    What I'll do now is create a bunch of new classes that are not MonoBehaviors and use those "po.nos" as data containers in my scripts (plain old .NET objects, pun intended ;-) - that's what happens when you move from the clean Java-world with it's pojos to the dirty world of .NET ;-) )

    Code (csharp):
    1.  
    2. using UnityEngine;
    3. using System.Collections;
    4.  
    5. [ExecuteInEditMode()]
    6. public class SimpleTest : MonoBehaviour {
    7.  
    8.     void OnGUI() {
    9.         GUI.Label(new Rect(30, 30, 600, 20),
    10.                    "This is a test, please open the"
    11.                    +" console (cmd+shift+c) and push the button!!!");
    12.        
    13.         if (GUI.Button(new Rect(30, 60, 150, 20), "Test != null")) {
    14.             print("SimpleTest myObject = new NewBehaviourScript();");
    15.             SimpleTest myObject = new SimpleTest();
    16.  
    17.             print("SimpleTest myObject2 = new NewBehaviourScript();");
    18.             SimpleTest myObject2 = new SimpleTest();
    19.            
    20.             if (myObject != null) {
    21.                 print("myObject != null => It is not null - CORRECT");
    22.             } else {
    23.                 print("NOT myObject != null => How could"
    24.                      +" this ever be called?");
    25.             }
    26.             if (myObject == null) {
    27.                 print("myObject == null => How can this be?");
    28.             } else {
    29.                 print("NOT myObject == null => Correct, but then:"
    30.                      +" why does the above happen?");
    31.             }
    32.             if (myObject == myObject2) {
    33.                 print("myObject == myObject2 - but those two objects"
    34.                      +" are NOT the same!");
    35.             } else {
    36.                 print("NOT myObject == myObject2 - that is CORRECT");
    37.             }
    38.            
    39.            
    40.             /*
    41.              * That's where I found out I was "misusing" MonoBehaviour,
    42.              * I'm getting a NullReferenceException, because name
    43.              * delegates to GameObject.name (I would assume), which is
    44.              * not initialized...
    45.              */
    46.            
    47.             //print("Now executing: myObject.name = 'Another name'");
    48.             //myObject.name = "Another name";
    49.             //if (myObject == myObject2) {
    50.             //  print("myObject == myObject2 - but those two objects"
    51.             //       +" are NOT the same!");
    52.             //} else {
    53.             //  print("NOT myObject == myObject2 - that is CORRECT,"
    54.             //       +" names are compared...");
    55.             //}
    56.         }
    57.     }
    58. }
    59.  
    Jashan
     
  9. freyr

    freyr

    Joined:
    Apr 7, 2005
    Posts:
    1,148
    On the line
    Code (csharp):
    1. SimpleTest myObject2 = new SimpleTest();
    you are using new on a class that inherits from MonoBehaviour. MonoBehaviour subclasses should always be attached to a game object and should never be created using the new operator. (Use AddComponent instead.) Doing that will result in undefined behavior.
     
  10. bronxbomber92

    bronxbomber92

    Joined:
    Nov 11, 2006
    Posts:
    888
    Could you explain why Mono Behaviour objects can't be created with the new operator?
     
  11. freyr

    freyr

    Joined:
    Apr 7, 2005
    Posts:
    1,148
    It's because MonoBehaviours are Components, and Components always have to be attached to a game object. When using new, the object will be created without attaching it to a game object, thus the gameObject member variable will be null.

    Some of the methods and properties inherited from MonoBehaviour use that gameObject member variable assuming it's never null and will therefore fail or produce unexpected results when used on an orphan.

    The name property is one of those. Doing myMonoBehaviourInstance.name = "New Name"; is actually a shortcut for myMonoBehaviourInstance.gameObject.name = "New Name";

    If your custom classes are not supposed to be attached to a game object, you should inherit from ScriptableObject instead.


    Edit: I noticed that holtsch post above actually explains this as well. And yes I agree that the constructor for MonoBehaviour should be marked unaccessible in order to fail on compile time. Although I haven't investigated whether there are architectural issues which prevent simply marking the constructor method as private or internal
     
  12. jashan

    jashan

    Joined:
    Mar 9, 2007
    Posts:
    3,307
    Ah, cool... so just for the record and to be sure: Is it safe to create those with "new", or would I have to use

    Code (csharp):
    1.  
    2. static function CreateInstance (className : string) : ScriptableObject
    3.  
    If it's possible, I think that is a really important change - and it might break some people's code on compile-time (but breaking it on compile-time is much nicer that creating Runtime errors which is what this change would prevent).

    Another thing that would create quite some hassles is namespaces for the Unity-API. Personally, I think it's kind of a mess to have all these classes in a single non-namespace. There's sure reasons for that, but the problem is that once you walk on that route, it's really hard to change because it breaks everything...

    Jashan