Search code examples
c#.netexplicit-interface

C# Interface where property shares target class's name, and where property is 'init', cannot assign property in constructor


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.


Solution

  • 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;
      }