0

In the code below, stepping through the debugger, observe that MyWrapperInstance, an instance of Child.Wrapper, has only the hidden field from Base.BaseWrapper populated as an instance of Child.Node instantiated via the new() constructor from the type constraint, so that MyWrapperInstance.Node still evaluates to null.

I am somewhat new to C# but this went against my intuition. Is there an explanation for why this works this way?

var MyChildInstance = new Child();
var MyWrapperInstance = MyChildInstance.New();
// evaluates to true:
var isNull = MyWrapperInstance.Node is null;

public class Base
{
    public class BaseNode { }
    public class BaseWrapper
    {
        public BaseNode Node;
    }
    public TWrapper MakeNew<TWrapper, TNode>()
      where TWrapper : BaseWrapper, new()
      where TNode : BaseNode, new()
    {
        var wrapper = new TWrapper()
        {
            Node = null
        };
        // I expected this to write the derived class's property
        // but it seems to write the hidden field of base class
        // instead
        wrapper.Node = new TNode();

        return wrapper;
    }
}

public class Child : Base
{
    public class Node : BaseNode { }
    public class Wrapper : BaseWrapper
    {
        // Hides BaseWrapper Property
        public new Node Node;
    }

    public Wrapper New() => MakeNew<Wrapper, Node>();
}

Note: I created a new Console application in Visual Studio 17.3 using .NET 6.0 and this is the content of Program.cs.

Expectation: MyWrapperInstance.Node is not null, and the hidden property is null instead. I tried to inspect in the debugger that the instantiated wrapper has not somehow been cast to BaseWrapper.

Uwe Keim
  • 39,551
  • 56
  • 175
  • 291

1 Answers1

2

Proper intuition - whenever one uses shadowing (like public new Node Node;) it is the best to assume to access such property the compiler will choose base or derived version at random. Indeed, there are well defined rules and in every case one can reason about the choice compiler will make, but "random" is a good starting point.

In this case compiler sees TWrapper as a type that is at least BaseWrapper but knows nothing about any derived classes. So inside generic code wrapper.Node will always be BaseWrapper.Node as compiler must generate all code at compile time and it has no information about essentially unrelated Wrapper.Node.

Note that in most cases when one tries to use new modifier virtual is likely more appropriate - review Difference between shadowing and overriding in C#?.

Alexei Levenkov
  • 98,904
  • 14
  • 127
  • 179
  • Thanks! Your second paragraph is helpful for my intuition since I'm fairly new to static types/OOP. – user266517 Nov 03 '22 at 17:29
  • So I guess this is unintuitive to me coming from a more dynamically typed background. Is it correct to say that compilation generally happens such that the result is independent of the particular type passed in the type parameters? (With an exception for the `new()` constraint) – user266517 Nov 03 '22 at 17:53
  • Re-reading, I think you already covered that last question. I still don't really understand why the compiled code is able to say "call your type's parameter-less constructor" (with the `new()` constraint), but not "assign to your own type's field". I am reading more here https://learn.microsoft.com/en-us/dotnet/standard/base-types/common-type-system#constructors and here https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/types/ but if you know another reference you recommend to better understand the compiler / type system, please let me know! – user266517 Nov 03 '22 at 18:09
  • @user266517 depends on language. For C# answer is yes, for Java/C++ answers are different. For C# https://stackoverflow.com/questions/14343498/how-does-the-c-sharp-compiler-work-with-generics may be helpful. – Alexei Levenkov Nov 03 '22 at 18:09
  • Ah thank you, I was wondering that too. I appreciate your help! – user266517 Nov 03 '22 at 18:12