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.

apfelstrudel24
  • 442
  • 3
  • 8
  • 1
    @SeM It's not really a duplicate, since this question is about the affect that overriding the string property is having. – Matthew Watson Nov 30 '18 at 11:04
  • @MatthewWatson you're talking about first or second one? – SᴇM Nov 30 '18 at 11:04
  • 2
    @SeM Both, because neither of those answers talk about overriding properties. – Matthew Watson Nov 30 '18 at 11:06
  • Sorry for causing confusion, I edited the question, so it might be clearer now. – apfelstrudel24 Nov 30 '18 at 11:06
  • 2
    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. https://sharplab.io/#v2:D4AQDABCCMDcCwAoEAmMSQGYrQGxRRwHYIBvJCSiCqmyrAiAFQFMBnAFwCEBDNlpOURUo2AG4BLAE4cArjwA2OSACUWPACYB5AHYKAnmQgBzFh1gBfJHVHN23PiwAUAShtCRItZt0GIAXggAIgAzKQB7AFsIACNHIIRhKitEFIxCVk4IAC47Tl5+QRsGcLEWKSkJDRZlCG9tPUNSEzNLYuxMjld3GxEAej6IHXCOAAsJHWMIUfKBJMoUlJEbdpx8EAAWCABZHgnXMl6qMR4pCAAPAKGWAHc8rpcIRM9aec8YAE4nc4A6et99C5nskkBYgA== – pinkfloydx33 Nov 30 '18 at 11:07
  • @MatthewWatson ah, yes you're right, didn't noticed `virtual` keyword here, sorry. – SᴇM Nov 30 '18 at 11:08
  • `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`. – IronAces Nov 30 '18 at 11:09
  • ... 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 obvious – pinkfloydx33 Nov 30 '18 at 11:18
  • 2
    For bonus points, add `Console.WriteLine(ReadOnly);` in the base constructor, after assigning to the property. – Lasse V. Karlsen Nov 30 '18 at 11:34
  • @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 suppose – pinkfloydx33 Nov 30 '18 at 11:41
  • You can forward to the base if that helps: `public override string ReadOnly => base.ReadOnly;` – Ben Adams Nov 30 '18 at 11:52
  • 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. – Siavash Dec 16 '18 at 11:30

2 Answers2

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
        }
    }
}
Marc Gravell
  • 927,783
  • 236
  • 2,422
  • 2,784
  • 2
    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. – Lasse V. Karlsen Nov 30 '18 at 11:24
  • @LasseVågsætherKarlsen I really want to send up the "Jared Signal" - kinda like the Bat Signal, but... geekier. Should I? – Marc Gravell Nov 30 '18 at 11:26
  • 1
    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. – Lasse V. Karlsen Nov 30 '18 at 11:27
  • 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 :) – Lasse V. Karlsen Nov 30 '18 at 11:27
  • @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. – Marc Gravell Nov 30 '18 at 11:30
  • 3
    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*. – Lasse V. Karlsen Nov 30 '18 at 11:34
  • 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 not – pinkfloydx33 Nov 30 '18 at 11:46
  • @pinkfloydx33 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 problem – Marc Gravell Nov 30 '18 at 11:48
  • @MarcGravell I meant--in theory--at the compiler level as a potential "fix". Obviously doing it yourself makes it not an auto property anymore =) – pinkfloydx33 Nov 30 '18 at 11:49
  • 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. – Lasse V. Karlsen Nov 30 '18 at 11:53
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."
Matthew Watson
  • 90,570
  • 7
  • 128
  • 228