0

I have an interface called IHierarchable, intended for both the 'Entity' class and the 'Scene' class which serves as the root of all entities.

    /// <summary>
/// Signifies that an object can exist in the game hierarchy and can have children.
/// </summary>
public interface IHierarchable
{
    public abstract Scene Scene { get; protected init; }

    /// <summary>
    /// The parent of this object.
    /// </summary>
    public abstract IHierarchable Parent { get; protected set; }

    /// <summary>
    /// Any children belongong to this object.
    /// </summary>
    public abstract ReadOnlyCollection<IHierarchable> Children { get; protected set; }
}

But when I implement this on my Scene class:

    /// <summary>
/// A base 2D world, into which <see cref="Entity"/> instances can be placed.
/// </summary>
public class Scene : IHierarchable
{
    Scene IHierarchable.Scene { get; init; }
    public IHierarchable Parent { get; set; }
    public ReadOnlyCollection<IHierarchable> Children { get; set; }

    public Scene()
    {
        (this as IHierarchable).Scene = null;
    }
}

I have the following error in the constructor:

Init-only property or indexer 'IHierarchable.Scene' can only be assigned in an object initializer, or on 'this' or 'base' in an instance constructor or an 'init' accessor

I have to implement the interface explicitly in this class, as the 'Scene' property happens to share the name of the 'Scene' class. But once I implement it this way, I'm unable to actually set the property, even though I'm setting it within Scene's constructor and using the 'this' keyword, just as the error message suggests.

Thomas Slade
  • 129
  • 9
  • 1
    Remove the `public abstract` keywords from those properties: [that's the syntax for DIM ("Default interface members/methods")](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/proposals/csharp-8.0/default-interface-methods) which isn't the same thing as a good-ol-fashioned `interface` member. – Dai Apr 24 '23 at 23:15
  • 3
    `(this as IHierarchable)` <-- This is an inappropriate use of the `as` operator because `this` **is** `IHierarchable` - you don't need maybe-fail semantics and a nullable expression result type. – Dai Apr 24 '23 at 23:16
  • 1
    Is there a reason you're using the concrete (and rather old-fashioned) `ReadOnlyCollection` type instead of the covariant (i.e. _cool and sexy_) `IReadOnlyCollection` type? – Dai Apr 24 '23 at 23:17
  • Your `IHierarchable Parent` property should be nullable, methinks - otherwise how can every `Parent` have a `Parent` stretching to infinity? – Dai Apr 24 '23 at 23:19
  • 1
    Is there a (good) reason why `IHierarchable.Scene` is an `init` property? (`init` properties are a lot less useful than people think, and are a great way to introduce subtle bugs that violate class invariants - personally I think `init` is a massive mistake in the design of the C# language) – Dai Apr 24 '23 at 23:23
  • Why do you want explicitly initialize it to `null`? It will be null without the initialization. – Guru Stron Apr 24 '23 at 23:34
  • @Dai _"are a great way to introduce subtle bugs that violate class invariants"_ - You have sparked my curiosity, can you please give some examples? – Guru Stron Apr 24 '23 at 23:35
  • 1
    @GuruStron If a `class` has a parameterized constructor that validates those parameter arguments before assiging them to `init`-able properties (such that if validation fails it throws `ArgumentException`) then the call-site of that class's constructor can use a C# object-initializer (which runs _after_ the constructor) to overwrite those properties without the class having any way to re-validate those new values. Hang on, I'll show you a concrete example in a new reply in a few min. – Dai Apr 24 '23 at 23:40
  • @Dai yes, that was my guess, thank you. – Guru Stron Apr 24 '23 at 23:41
  • 1
    @GuruStron alright - that means I can get back to my day-job then :) – Dai Apr 24 '23 at 23:43
  • @Dai Is there a particular advantage to using `IReadOnlyCollection` rather than `ReadOnlyCollection` when this collection will only be set once, will always be of the same type, and never needs to be polymorphic? (am I using that word right? ...) – Thomas Slade Apr 25 '23 at 15:56
  • @Dai I'm surprised you're against the use of `init`, could you elaborate? I'm using it because I want `Scene` to only be set at an object's construction, after which I can be certain it will never change. I thought this supported class invariants, rather than violated it? – Thomas Slade Apr 25 '23 at 15:58
  • @Dai Just read your response regardng `init`, sorry, and I'm also very curous (and baffled). You're saying `try { entity = new Entity(someInvalidScene) }` `catch(ArgumentException e) { entity = new Entity() { Scene = someInvalidScene }` would work without error, even though `Scene` is marked with an `init` setter? Keep in mind, also, that the above ought not to be possible in my code as I don't plan on giving `Entity` a parameterless constructor (it should be impossible for an `Entity` to exist without a `Scene`) – Thomas Slade Apr 25 '23 at 16:04
  • @ThomasSlade In a `class` or `struct` (but not an `interface`), use `{ get; }`-only properties for immutable (field-backed) auto-properties that can only be set in a constructor - whereas `{ get; init; }` properties can _also_ be (re-)set by an external object-initializer (which is _after_ the constructor). – Dai Apr 25 '23 at 20:47
  • 1
    @ThomasSlade Yes, you misunderstand my point about `init` properties - [here's a GitHub Gist that demonstrates the problem](https://gist.github.com/daiplusplus/52b9617919fb02e2fa9e4c39aad7fb03) - just copy-and-paste it into SharpLab or LinqPad. – Dai Apr 25 '23 at 20:59
  • 1
    @GuruStron See my above comment – Dai Apr 25 '23 at 21:00
  • @Dai Interesting, but isn't this solved by making it `protected init`? – Thomas Slade Apr 25 '23 at 21:33
  • @ThomasSlade Only partially: using `protected init;` still allows a subclass (instead of _everyone_) to subvert the base class' ctor; if your class is `sealed` (which most classes _should_ be, imo) then there's no point using `protected` anyway. (I'm not saying that `init` properties have no reason for existing because they do: they're good for when you have _optional additional_ data members that don't participate in class invariants. The problem I see is that this is a very narrow and limited use-case and the _developer evangelists_ are overselling it and then people use it incorrectly). – Dai Apr 25 '23 at 21:34

2 Answers2

3

You've defined your interface type using DIM syntax which isn't what you want. I do appreciate that this is a big cause of confusion, especially as it's also a very new language feature too.

A big gotcha with DIM properties in particular is the fact that interface types (including DIM interfaces) cannot have fields - therefore you cannot have auto-properties - so when you use C# class/struct auto-property syntax in a DIM interface you're actually just defining an abstract-like property

  1. First, change your interface type to a good ol' fashioned interface:

    • Also make Scene and Parent as nullable properties, as I assume eventually the top-most IHierarchable object will have a null parent - and you're explicitly setting Scene = null so I assume that's intentional.
    • I assume your IHierarchable is intended to be a read-only interface for its consumers (i.e. a consumer of IHierarchable shouldn't be able to overwrite any of the properties - and there's no point having init members of an interface because you can't use (post-construction) object-initializer syntax with an interface type.
    • Because Children is a collection property it should not have a set accessor - only get - and use a covariant collection interface instead of concrete type (i.e. use IReadOnlyList<out T> instead of ReadOnlyCollection<T>), that way you can legally pass Children around as, e.g. IEnumerable<IHierarchable>, or IReadOnlyCollection<Object?> without needing an unsafe cast or creating a .ToList() copy. Using IReadOnly...<T> interfaces also means that recipients of the collection can't modify it unexpectedly, which is another common cause of software bugs.
    • Like so:
    public interface IHierarchable
    {
        Scene? Scene { get; }
    
        /// <summary>The parent of this object.</summary>
        IHierarchable? Parent { get; }
    
        /// <summary>Any children belongong to this object.</summary>
        IReadOnlyList<IHierarchable> Children { get; }
    }
    
  2. Then implement that interface...

    • Constructors exist for a reason, use them to implement class invariants (but from your post I can't tell if you need one here or not yet).
    • There's no point making Scene's Scene property a field-backed auto-property if it's always going to be null - just make it a get-only property that always returns null
    • Make the class sealed unless you absolutely know that you need to subclass it, because subclassing and explicit interface implementations (like Scene? IHierarchable.Scene => null;) don't play nice together.
    public sealed class Scene : IHierarchable
    {
        public Scene()
        {
            // Nothing needs to go here - unless it does?
        }
    
        public IHierarchable? Parent { get; }
    
        public IReadOnlyList<IHierarchable> Children { get; } = new List<IHierarchable>();
    
        Scene? IHierarchable.Scene => this;
        // or return null:
        Scene? IHierarchable.Scene => null;
    }
    
Dai
  • 141,631
  • 28
  • 261
  • 374
  • Is it actually DIM? As far as I understand he problem here is that class and property are named the same way, so the need for explicit interface implementation, hence the attempt in OP ctor. – Guru Stron Apr 24 '23 at 23:40
  • @GuruStron An `interface` becomes a DIM interface if any of its members have implementation-specific modifiers, such as `public`, `abstract`, etc. (kinda like how if you use the `export` keyword anywhere at the root of a `*.js` file it magically becomes an ECMAScript `module` and then breaks your build - _fun_. – Dai Apr 24 '23 at 23:42
  • Are you sure - [sharplab](https://sharplab.io/#v2:EYLgtghglgdgNAExAagD4AEBMBGAsAKHQAYACdbAOgGEB7AGzoFMBjAFyhpgGcKB5YAFYtWAWRoJGdANwEC6AMwlYrRgCcAZhGaMSASQASUNRFXMAFhGBMCAbwIBIBSUtdWqraxIBlbTB0/GP2wSGxIAc0ZWKRIAB1UaFTZGBCUYKCiSAF9ZfEdFFzcPPUNjUwsrHQAFE0DPUIiMuIThZJIuSOjs/AcnAvc2EgAlRggEXhg6AE9aBmEOGAAeAyN3MssmAD4SKjMoOgRVQJDwjtj4xJUU9oyurqcsb18dEGKVk3N1xlsexQC/R8CjGC9VOsHSnR+r1KHwqJGqhxgdROGWuENyTmGo3GUxmTDY8yWJVWMM2212+wRxwa0VRWRyeQBfgAFABKBx2XL2P5AkgAXhIMAArgwZLlbkA===)? – Guru Stron Apr 24 '23 at 23:46
  • @GuruStron If you look at the raw IL you'll see the DIM version is twice as big: https://pastebin.com/vr665hZy – Dai Apr 24 '23 at 23:51
  • As far as I understand properties has not changed behaviour, you can mark them as `public` as methods and it will be still required to implement them in the class. [sharplab](https://sharplab.io/#v2:EYLgtghglgdgNAExAagD4FgBQABAzAAlgBcBTAJwDMIBjE/ASQAkpyIzqALCYAGxKwDeWAJB583AM5EyNIvgDKtGHUUllARnwD8AcxJEA3PgAOZAPalqpBIRhRD+AL5YszzFjHYATAqV0QDMys7Fy8/JhCmKIEqsoAFACUIpHCbo5AA=) – Guru Stron Apr 24 '23 at 23:52
  • @GuruStron In this particular case there is no effective difference in behaviour, yes. – Dai Apr 24 '23 at 23:53
  • _"Similarly, although `abstract` is the default on interface members without bodies, that modifier may be given explicitly. "_, _"The access level `public` is the default but it may be given explicitly."_. `protected` can have effect though, I guess. – Guru Stron Apr 25 '23 at 00:01
  • 1
    @GuruStron You're right – Dai Apr 25 '23 at 00:01
  • Thank you for the informative reply. It's made it clear that in my particular case I don't need to assign `Scene.Scene` anyway, as you say, it is already `null`. I have other questions though. Regarding the type I should use for `IHierarchable.Children`, I imagined that if I made the property `IReadOnlyList` instead of `ReadOnlyCollection`, the consumer would be able to cast the list back to its readable version, and make changes to it? It was my layman impression that this is what `ReadOnlyCollection` is for, hence why I need a setter for it. – Thomas Slade Apr 25 '23 at 15:47
  • I'm also confused on your link about invariance, which from what I can tell just refers to an immutable field? It's precisely because I want `IHierarchable.Scene` to be immutable that I want to use the `init` keyword, so why should I not be doing that? I get the impressiont that I'm misunderstanding you completely though. – Thomas Slade Apr 25 '23 at 15:52
1
  1. There is no need to explicitly assign the Scene to null.

  2. If you still want to assign it you can use property initialization syntax:

public class Scene : IHierarchable
{
    Scene IHierarchable.Scene { get; init; } = null;
    public IHierarchable Parent { get; set; }
    public ReadOnlyCollection<IHierarchable> Children { get; set; }

    public Scene()
    {
    }
}
  1. Finally if both are not suitable for you - rename property or class to get out of the explicit interface implementation handling:
public interface IHierarchable
{
    Scene Scene1 { get; init; } // Just for example, can be something more meaningful
    // ...
}

public class Scene : IHierarchable
{
    public Scene Scene1 { get; init; } 
    // ...

    public Scene()
    {
        Scene1 = null;
    }
}

P.S.

  1. Consider using nullable reference types Scene Scene1 -> Scene? Scene1
  2. Note that protected modifier in the interface might not mean what do you think it does, see Modifiers in interfaces section of default interface methods doc.
Guru Stron
  • 102,774
  • 10
  • 95
  • 132