Document properties that don’t round-trip.


I personally think most API documentation is lame and either leaves critical behavior qualities unspecified, or don’t comment when standard assumptions may be broken. One example is not documenting object lifespans. Another example is that you’d normally expect setting a property and then immediately getting that property would round-trip. In other words, the value you set is the value you get back. But there are cases where this isn’t true, and they should be called out.


    Document properties that don’t round-trip, and explain why they don’t round-trip.


Imagine you have a class with that has some read/write property. Eg, C# code like this:
    class Foo {
        public T MyProp { get { … }; set {…}; }
    }


So if you use it like:
    void DoStuff(Foo f , T val) {        
        f.MyProp = val;
        Debug.Assert(f.MyProp == val);      
    }


Then, you’d expect that assert never fires. This is very straightforward, especially considering that:
1) the property and value are the same type, so there isn’t any type coercion. It’s not like there’s a float-to-int truncation issue.
2) no other members are called on f, so it’s not like another operation is changing MyProp from underneath us.
3) we’re doing the ‘set’ first, and then the ‘get’. This is significant because calling the ‘set’ first forces the property into a known state and thus avoids the case where ‘get’ would catch the property in an undefined state.

Why are round-trippable properties important?
It’s the intuitive expectation and not doing so will confuse your library users and cause subtle bugs (I give a real example below). After all, fields round-trip, and it’s reasonable to expect properties to have the same semantics as fields. Consider that FXCop has a rule to change public fields to public properties.


Imagine you have a text property. Should this:
    int Test1(Foo f , T val) {        
        int x = Calculate(val);
        f.MyProp = val;
        return x;             
    }

and this:
    int Test2(Foo f , T val) {        
        f.MyProp = val;
        int x = Calculate(f.MyProp);
        return x;             
    }

really return two different values? It’s reasonable to expect  that Test1 and Test2 would behave the same way.


Round-tripping properties also allow you to build stronger invariants and add stronger asserts. For example, suppose you have a Text property that round-trips.  You may write an Append() function like so:
    void Append(Foo f, string textToAppend) {
        int lenStart = f.Text.Length;
        f.Text += textToAppend;
        Debug.Assert(fText.Length = lenStart + textToAppend.Length); // <– only valid if Text property round trips.
    }

If Text didn’t round-trip, you couldn’t add the assert.


Why might a property not round-trip?
The getter and setter are arbitrary functions and so in general, can do anything they please. Languages like C# can’t force the getters and setters to live up to any semantics. Imagine a property like:
    public T MyEvilProp { set { /*nop*/; } get { return new T(); }}
that is clearly not round-trippable! This sort of behavior is just evil and should be avoided.

The only legitimate reason I’ve found for not round-tripping is when a property round-trips to the same “logical” value, but represented in a different physical way. For eg,  perhaps you have a string property representing XML. The object internally stores the XML in some document structure that doesn’t track whitespace. Thus setting and getting the xml via a string will be different because the getter will have all the whitespace stripped out.
 


What’s a real example?
I recently hit this with the TextBoxBase.Text property. That lets you get or set the text contents of a TextBox or RichTextBox. However, empirically, when you set the text, it strips out all the ‘\r’ characters. (I don’t know what other filtering it may do on things like non-printable characters). This becomes very important with RichTextBox, because it has formatting commands that take character offsets. Suppose you wanted to write a helper function like:
    InsertText(Color c, string text, int characterIndex)
which injects a string ‘text’ into the RichTextBox at the given character index and marks it the given color C. To colorize with RichTextBox, you need to call something like:
    box.Select(offset, length);
    box.SelectionColor= c;
Where Select() will mark a region starting at the given character offset and of the given length of characters, and then SelectionColor will set the color for the given selected range.  Intuitively, the length parameter to Select() would be text.Length, but you can’t use that because the input string may get filtered and doesn’t round-trip. If text contains a ‘\r’, that gets stripped out, and so now text.Length would be 1 too large.


   

Comments (12)

  1. AndrewSeven says:

    I though that side effects in properties were anathema.

    If they have side effects, then the order in which they are called can become very significant.

    When I have a situation where they don’t round-trip, I will change from property semantics to methods so that it is clearer.

    When it looks like this, you don’t create the same expectations.

    t.SetText("Hello n World");

    string t.GetText();

    From the code in the box example can one tell: Is SelectionColor a property of the box that will change what color selected text will appear in that box or does it change the actual color of the text that is currently selected?

    box.Select(offset, length);

    box.SelectionColor= c;

    So, is it the same as :

    box.SelectionColor= c;

    box.Select(offset, length);

    Or does it mean something more like:

    selection = box.Select(offset, length);

    selection.Color= c;

  2. Another good example is the Visible property in ASP.NET. This will always work:

    myControl.Visible = false;

    Assert(myControl.Visible == false);

    But this:

    myControl.Visible = true;

    Assert(myControl.Visible == true);

    will only work if the control’s parent is also visible. Intuitive, huh?

  3. Andrew – even changing them to methods like SetText() and GetText() may still yield these expectations. It’s very good to document the _relationship_ between methods in a class, and in that case, pointing out that they still don’t round-trip.

    RichTextBox is like your 2nd case. It has state about the "Current Selection" – so imagine if ‘selection’ in you example was a member. The Select() method and SelectionColor property will operate on that current state.

    Stuart – that’s a great example. Thanks for pointing it out. Perhaps a better name for it would be a method like "TrySetVisible(bool)".

  4. Gabe says:

    I would just like to point out that having side-effects is the whole point of using properties as opposed to just using fields.

    For example, if I have a Size property of a window, I would expect that setting the property would have the side-effect of setting the size of the window. Of course, Size might not round-trip because not all valid settings of Size might be valid window sizes.

  5. Actually it’s worse than you think: TrySetVisible would also be misleading because the set actually always succeeds and always *does* have an effect.

    The behavior is best described by giving a possible implementation that would behave the same way:

    private bool visible;

    public bool Visible {

    set {visible = value;}

    get {return visible && Parent.Visible;}

    }

    In other words, even though "Visible" is returning false, the state of the control has still changed when visible was set to true. If the parent later becomes visible, this change will become apparent. Until then there’s no way to find out what the true visibility actually is.

    My proposal, therefore (which will never happen due to backcompat) would be:

    private bool visible;

    public bool Visible {

    set {visible = value;}

    get {return visible;}

    }

    public bool ReallyVisible {

    get {return Visible && Parent.ReallyVisible;}

    }

    Ok, ReallyVisible is a dumb name, but better a dumb name than dumb behavior, don’t you think? 🙂

  6. Gabe: I agree with you about side effects, but not about violating round-tripping. Surely if you try to set Size to a value that’s not an actually valid size for the window the setter should throw an exception?

  7. Stuart – I agree with your Visible / ReallyVisible example. All good points.

    Re Gabe’s point about ‘Size’ raises a problem similar to rounding errors. It’s similar to setting an integer property to some floating point value like 3.4. (Though languages can mitigate this via the type system, where only valid values are in the type’s domain)

    Throwing an exception is one behavior; but then you need some other way of finding out what the valid values are.

    Perhaps a method like "SetSizeApproximation(size s)", which clearly documents that the size value may get rounded.

    However, it’s a tough balance because following all of this guidance may mean that a lot of things that are conceptually properties become methods.

  8. About the Visible problem,

    call it:

    Visibility

    And have an IsVisible as a getter only that return if the control is seen.

  9. Ayende: Excellent suggestion. If only the ASP.NET team had thought of that… 🙂

  10. Jeremy H says:

    I have not had the time to really dig into it but I wonder why COM Interops do not appear to be ’round tripable’. Do this… add a reference to any project for a com object. The ide will create the interop wrapper. Take that interop.*.dll, and run it through ildasm. Then re-assemble it right away using ilasm. It will no longer import into the IDE. (try to add a reference to this reconstructed dll and it will fail). This happens no matter if I include or do not include the resource file. Anyone know why this is?

  11. Jeremy – I don’t know offhand, but I’ll add that to my list to check out.

    FYI, here’s a thread that is more targetted at round-tripping:

    http://blogs.msdn.com/jmstall/archive/2006/01/13/debug_roundtripping.aspx

  12. Be wary of "required" properties that must be explicitly set correctly in order for the object to function