Why does an overridden get-only property stay null when set in base class constructor?
Asked Answered
S

2

46

I tried the following example:

public class TestBase
{
    public virtual string ReadOnly { get; }

    public TestBase()
    {
        ReadOnly = "from base";
    }
}

class Test : TestBase
{
    public override string ReadOnly { get; }
    public Test()
    {
        // nothing here
    }
}

When I create an instance of Test, I see that ReadOnly stays null. But why? I really do not get the hang of it, could somebody please explain to me why this happens? At least I would expect an error, that a read-only property cannot be set outside of the owning class.

Sandglass answered 30/11, 2018 at 10:49 Comment(12)
@SeM It's not really a duplicate, since this question is about the affect that overriding the string property is having.Untimely
@MatthewWatson you're talking about first or second one?Singular
@SeM Both, because neither of those answers talk about overriding properties.Untimely
Sorry for causing confusion, I edited the question, so it might be clearer now.Sandglass
I think it's related to that and virtual member access in constructor, no? Though it's clear if you look at how it's compiled: both classes get their own private backing field. The one in the child class gets returned. sharplab.io/…Padishah
@MatthewWatson ah, yes you're right, didn't noticed virtual keyword here, sorry.Singular
ReadOnly may be a confusing variable name to use in this instance. As I understand it, you are overriding a base classes property which can only be set within the constructor ({get;}). It isn't set, and is therefore null.Fabrikoid
... though once you make it a get/set (instead of readonly) you get the base class text since it invokes the child setter which then sets the child's private backing field. A lot more obvious of you run it through sharplab and check the "compiled" version and watch the flow arrows. It's related to the dupes but the overriding behavior doesn't make it obviousPadishah
For bonus points, add Console.WriteLine(ReadOnly); in the base constructor, after assigning to the property.Handspring
@LasseVågsætherKarlsen lol that definitely would make me double take. It's still virtual member call from constructor though, but not knowing any better I'd have figured it'd be assigned. Never actually ran into this issue before so it's good to know I supposePadishah
You can forward to the base if that helps: public override string ReadOnly => base.ReadOnly;Aluminate
How do you manage to set property in the base class in the first place? it is does not have a setter. this code should bring back only compile error. add setter and it should behave normally.Aam
L
39

The compiler treats this as below; basically, the code in the constructor writes to the original backing field, in TestBase. It seems that yours is not a supported scenario, but... I do wonder whether the language team have considered this case.

BTW: if you ever want to see what the compiler does with code: sharplab.io

public class TestBase
{
    [CompilerGenerated]
    private readonly string <ReadOnly>k__BackingField; // note: not legal in "real" C#

    public virtual string ReadOnly
    {
        [CompilerGenerated]
        get
        {
            return <ReadOnly>k__BackingField; // the one in TestBase
        }
    }

    public TestBase()
    {
        <ReadOnly>k__BackingField = "from base";
    }
}
internal class Test : TestBase
{
    [CompilerGenerated]
    private readonly string <ReadOnly>k__BackingField;

    public override string ReadOnly
    {
        [CompilerGenerated]
        get
        {
            return <ReadOnly>k__BackingField; // the one in Test
        }
    }
}
Lonnie answered 30/11, 2018 at 11:18 Comment(10)
And before anyone complains that "who cares, you shouldn't be doing this anyway as it gives you nothing", then consider that you can add attributes to the overridden property in the descendant, which will be combined with attributes on the property in the base class. So it does give you something, and although it's probably not a feature many uses, it might be important in serialization scenarios.Handspring
@LasseVågsætherKarlsen I really want to send up the "Jared Signal" - kinda like the Bat Signal, but... geekier. Should I?Lonnie
Definitely, personally I feel that using virtual members in the constructor is the wrong thing to do, but the compiler doesn't explicitly warn about it and does in fact work, with all the usual documented warnings about what might break and why, so here I did actually expect the property to be assigned though I fully understand why not. Perhaps this particular scenario needs to be explicitly handled, either with a warning or actual support.Handspring
Though I feel that adding full support for this smells like a can of worms, but yes, I believe in knowledge and willfull decisions, so light the floodlight and let the team decide :)Handspring
@LasseVågsætherKarlsen signal deployed; I acknowledge that it would be a complete mare to "fix" it in the "make it work as it appears" sense, though.Lonnie
There is no sane way to fix this, other than possibly adding the warning. Just try to add a Console.WriteLine(ReadOnly); in the base constructor, after assigning to ReadOnly and you'll see a different symptom, it will call the descendant getter, of the property we know hasn't been assigned, and thus try to print null. The only sane fix here, if anything, would be that compiling the descendant class should warn that you actually have two distinct property implementations.Handspring
Could you in theory change it so the auto implemented descendent invokes base.Property? That'd give you nothing, I suppose, but then you could still add attributes. I guess you'd also be limited by the readonly-ness of the base backing field, so probably notPadishah
@Padishah yeah, the problem here is the auto-prop-ness; if it is public override string ReadOnly => base.ReadOnly; (perhaps with attribs changes), then it isn't an auto-prop, and won't suffer the problemLonnie
@MarcGravell I meant--in theory--at the compiler level as a potential "fix". Obviously doing it yourself makes it not an auto property anymore =)Padishah
If you should still be allowed to write to the descendant property from the descendant constructor, and have the two "merge into one, implementation-wise" I don't really see any way this could be solved. I think a warning, if anything, is the only recourse.Handspring
U
17

The easiest way to explain this is to consider what code the compiler is generating to implement this.

The base class is equivalent to this:

public class TestBase
{
    public virtual string ReadOnly => _testBaseReadOnly;

    public TestBase()
    {
        _testBaseReadOnly = "from base";
    }

    readonly string _testBaseReadOnly;
}

The derived class is equivalent to this:

class Test : TestBase
{
    public override string ReadOnly => _testReadOnly;

    readonly string _testReadOnly;
}

The important thing to note here is that the derived class has its OWN BACKING FIELD for ReadOnly - it does NOT re-use the one from the base class.

Having realised that, it should be apparent why the overridden property is null.

It's because the derived class has its own backing field for ReadOnly, and its constructor is not initialising that backing field.

Incidentally, if you're using Resharper it will actually warn you that you're not setting ReadOnly in the derived class:

 "Get-only auto-property 'ReadOnly' is never assigned."
Untimely answered 30/11, 2018 at 11:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.