[Connect .NET] Unexplained Behavior with DRange1d

I ran into a problem when using a DRange1d as a property in one of my classes where it would never change from null.

I managed to recreate this problem with a keyin. (The property is static just for this example to run with the keyin)

        private static DRange1d testRange { get;  set; }
       
        public static void RunTestCode(String unparsed)
        {
            testRange = DRange1d.NullRange;
            testRange.Extend(73.2);
            Debug.Print("testRange: " + (testRange.IsExactNull ? "Null": "Not Null"));

            DRange1d testRange2 = DRange1d.NullRange;
            testRange2.Extend(73.2);
            Debug.Print("testRange2: " + (testRange2.IsExactNull ? "Null": "Not Null"));

        }

The output is:

testRange: Null
testRange2: Not Null

Can someone explain to me what is going on?

Thanks,

Maury

Parents
  • Hi Maury,

    please, don't ask such questions, they create too perfect chance to procrastinate under "it's important and interesting question" mimics :-))))

    I ran into a problem when using a DRange1d as a property

    Yes, based on my testing (seriously, too much time spent with this procrastination ;-) , the property usage is on from important sources of the discussed behaviour.

    Can someone explain to me what is going on?

    Yes, at least a bit ;-)

    It seems the problem is part of a chain of several different conditions and rules how value types are treated and how static properties (accessors) are initialized and in what order. But honestly, it's still just my guess, I am running out of my current knowledge about C# rules and internals.

    General C# rules:

    • Structs (like DRange1d) belongs to value types.
    • When value type is assigned to another variable, a shallow copy is created automatically (contrary to reference type where assignment creates a new reference to the same object).
    • The previous rule is applied only when the value type is assigned to (local) variable...
    • ... and not when available through property.
    • When value type is defined as readonly, it becomes immutable and cannot be changed.

    When applied to your code and how DRange1d struct is defined:

    • DRange1d.NullRange is static readonly property initialized automatically in DRange1d struct static (type) constructor.
      ... which btw adds extra complexity because how exactly, when and in what order the static properties and constructor(s) are initialized is not intuitive. Personaly I would like to know the order of initialization in this pretty complex case.
    • Your testRange is property (not local variable).
    • When NullRange is assigned to testRange, it points to the same object. It seems it breaks "create value type copy during the assignment" rule, but probably this situation is not treated as assignment. This is something I do not know enough about.
    • At the end, you have "it seems to be variable" pointing to readonly struct ... which cannot be changed.
    • Next assignment to testRange2 is the assignment to local variable, where new struct instance is created (so it's not readonly reference to some property).

    Solution:

    • Do not use auto property, but declare private variable explicitly (and make it available through normal property).

    I hope it will help you, but please be aware it's just a guess based on hands on testing, not analysis based on knowledge of C# rules and internals ;-)

    With regards,

      Jan

  • From the looks of it, it probably does have something to do with the struct type being copied and how properties are handled.

    Solution:

    • Do not use auto property, but declare private variable explicitly (and make it available through normal property).

    For this solution, the only way to get it to work properly was to run "Extend" on the private variable directly and not the property exposing it.

    When calling extend on the property, it seems like the extend method is being called on the copy and not reflecting on the private variable.

        internal class TestClass
        {
            private DRange1d _privateVariable = DRange1d.NullRange;
    
            public DRange1d PrivateVariable
            {
                get { return _privateVariable; }
                set { _privateVariable = value; }
            }
    
            public DRange1d Property { get; set; } = DRange1d.NullRange;
    
            public static DRange1d StaticProperty { get; set; } = DRange1d.NullRange;
    
    
            public DPoint3d PointTest { get; set; }= DPoint3d.Zero;
    
            public void TestExtend()
            {
                StaticProperty = DRange1d.NullRange;
                
                DRange1d localVariable = DRange1d.NullRange;
    
                localVariable.Extend(72.5);
                Property.Extend(72.5);
                PrivateVariable.Extend(72.5);
                StaticProperty.Extend(72.5);
    
                Debug.Print("Extend Results:");
                Debug.Print("localVariable: " + (localVariable.IsExactNull ? "Null": "Not Null"));
                Debug.Print("Property: " + (Property.IsExactNull ? "Null": "Not Null"));
                Debug.Print("PrivateVariable: " + (PrivateVariable.IsExactNull ? "Null": "Not Null"));
                Debug.Print("StaticProperty: " + (StaticProperty.IsExactNull ? "Null": "Not Null"));
    
    
                PointTest.Set(1,2,3);
                Debug.Print("Point Test: " + PointTest.X + "  " + PointTest.Y + "  " + PointTest.Z);
    
    
            }
    
    
            public void TestUnion()
            {
                StaticProperty = DRange1d.NullRange;
    
                DRange1d localVariable = DRange1d.NullRange;
    
                localVariable = DRange1d.FromUnion(localVariable, DRange1d.From(72.5));
                Property = DRange1d.FromUnion(Property, DRange1d.From(72.5));
                PrivateVariable = DRange1d.FromUnion(PrivateVariable, DRange1d.From(72.5));
                StaticProperty = DRange1d.FromUnion(StaticProperty, DRange1d.From(72.5));
    
               
                Debug.Print("Union Results:");
                Debug.Print("localVariable: " + (localVariable.IsExactNull ? "Null": "Not Null"));
                Debug.Print("Property: " + (Property.IsExactNull ? "Null": "Not Null"));
                Debug.Print("PrivateVariable: " + (PrivateVariable.IsExactNull ? "Null": "Not Null"));
                Debug.Print("StaticProperty: " + (StaticProperty.IsExactNull ? "Null": "Not Null"));
            }
        }

    Output:

    Extend Results:
    localVariable: Not Null
    Property: Null
    PrivateVariable: Null
    StaticProperty: Null
    Point Test: 0 0 0


    Union Results:
    localVariable: Not Null
    Property: Not Null
    PrivateVariable: Not Null
    StaticProperty: Not Null

    I will leave it at this and will need to be wary of this in the future.

    As always, thanks for your input

  • When calling extend on the property, it seems like the extend method is being called on the copy and not reflecting on the private variable.

    That's probably the most important C# feature I did not mention in my previous answer (but is hidden in my note about to what struct instance a variable represent): When value type (e.g. struct) is returned by a method, its copy is returned. When struct copy is created, it's initialized to default values, so it's probably why .Equals method returns true for these structs and DRange1d.NullRange.

    Regards,

      Jan

    Answer Verified By: Maury 

  • One more note:

    When calling extend on the property, it seems like the extend method is being called on the copy and not reflecting on the private variable.

    When you need to return original value type, not its copy, you can define property getter with ref (and it's not allowed to use setter in such case):

    public ref DRange1d PublicRange
    {
        get { return ref this.myRange2; }
    }

    But personally I treat it as antipattern, because it breaks encapsulation and immutability (it's possible to modify internal struct state from outside).

    My question is whether it's really required to use properties when working with struct and whether it's really necessary. I see two (better and clearer in my opinion)  alternatives:

    • To pass struct to a method as ref, to modify it internally and return the same ref. It does not breaks encapsulation, because the struct comes from outside anyway.
    • To do not return complex structure like DRange1d, but to use e.g. Touple. But it's works fine for 1d data only, for 2d or 3d it will become too complicated. And also it makes no sense when the struct should be used in another method as parameter accepting the same type.

    With regards,

      Jan

  • it probably does have something to do with the struct type being copied and how properties are handled

    As is often the case, Stack Overflow yields some relevant comments.

     
    Regards, Jon Summers
    LA Solutions

Reply Children
No Data