0

Suppose you had a data structure consisiting of "Parents" and "Children", where references between parents and children were mutual:

Borrowing from a previous post, the following code is written to guarantee mutual referencing between parents and children (short of abuse of Reflection):

public static class Base
{
    private interface IParent
    {
        List<Child> Children { get; }
    }

    public class Parent : IParent
    {
        private readonly List<Child> _children = new List<Child>();

        public readonly ReadOnlyCollection<Child> Children = _children.AsReadOnly();

        List<Child> IParent.Children { get { return _children; } }
    }

    public class Child
    {
        private Parent _parent;

        public Parent Parent
        {
            get
            {
                return _parent;
            }
            set
            {
                if(value == _parent)
                    return;

                if(_parent != null)
                {
                    ((IParent)_parent).Children.Remove(this);
                    _parent = null;
                }

                if(value != null)
                {
                    ((IParent)value).Children.Add(this);
                    _parent = value;
                }
            }
        }
    }
}

Now suppose you wanted a similar structure, but you also wanted type safety. That being, instances of TParent could only reference instances of TChild and instances of TChild could only reference instances of TParent.

I came up with this solution:

public static class Base<TParent, TChild>
    where TParent : Base<TParent, TChild>.Parent
    where TChild  : Base<TParent, TChild>.Child
{
    private interface IParent
    {
        List<TChild> Children { get; }
    }

    public class Parent : IParent
    {
        private readonly List<TChild> _children = new List<Child>();

        public readonly ReadOnlyCollection<TChild> Children = _children.AsReadOnly();

        List<TChild> IParent.Children { get { return _children; } }
    }

    public class Child
    {
        private TParent _parent;

        public TParent Parent
        {
            get
            {
                return _parent;
            }
            set
            {
                if(value == _parent)
                    return;

                if(_parent != null)
                {
                    // Oh no, casting!
                    ((IParent)_parent).Children.Remove((TChild)this);
                    _parent = null;
                }

                if(value != null)
                {
                    // Oh no, casting!
                    ((IParent)value).Children.Add((TChild)this);
                    _parent = value;
                }
            }
        }
    }
}

And while this works, the points where Child is casted to TChild inside Child.Parent.set worry me a bit. Though I'm not sure there'd be a way to use this class that throws an InvalidCastException, it may still be impossible to break.

Is there a cleaner way to achieve this effect?

Community
  • 1
  • 1
Hatchling
  • 191
  • 1
  • 7
  • 3
    Good heavens please do not do this. See https://blogs.msdn.microsoft.com/ericlippert/2011/02/03/curiouser-and-curiouser/ for reasons why this is a bad pattern in C#. – Eric Lippert Mar 02 '17 at 23:33
  • 1
    @EricLippert - You do say, "in practice there are times when using this pattern really does pragmatically solve problems in ways that are hard to model otherwise in C#". I have found this approach useful when I dictate the object model and others just consume it. Does that mitigate the issues you see? – Enigmativity Mar 02 '17 at 23:53

1 Answers1

5

First off, the answer to question Generics and Parent/Child architecture has some thoughts on an architecture similar to yours for generic modeling of parent-child relations.

Second, I strongly recommend against this pattern. It is confusing, it is arguably an abuse of generics, and it is not actually typesafe. See https://blogs.msdn.microsoft.com/ericlippert/2011/02/03/curiouser-and-curiouser/

Third, your attitude that the compiler is probably wrong about there needing to be a typecast, and that it is probably impossible to cause it to fail, is an attitude likely to lead you into trouble in the future. I recommend instead defaulting to believing that the compiler is right when it says your program is not typesafe, and believing that you probably haven't thought it all the way through if you think the compiler is wrong:

class Car : Base<Car, Wheel>.Parent { ... }
class Wheel : Base<Car, Wheel>.Child { ... }
class AnotherWheel : Base<Car, Wheel>.Child { ... }
...
new Car() blah blah blah
new Wheel() blah blah blah
the wheel is the child of the car, blah blah blah
and what happens when you make a Car the parent of an AnotherWheel?

The compiler is entirely right to say that the cast is required; we cannot conclude that the conversion is valid because it is not.

Fourth, how to solve this problem without resorting to an abuse of generics that is actually not typesafe?

There are ways to do it but I don't think you'll like them. For example:

  • Make underlying immutable objects. A child does not know its parent; a parent does know its child.
  • Construct a facade object on top of the underlying immutable objects; the facade of the child does know its parent.
  • When "editing" the facade, a new underlying immutable tree is constructed by building a new spine of the tree.

It is possible to construct such a system using generic interface covariance such that there are no casts required; doing so is left as an exercise.

As a warm-up exercise, consider how to construct an interface for a generic immutable IStack<T>, such that such that when you push a Turtle onto a stack of Tigers, it becomes an immutable stack of Animals.

In general I think you are better to not try to capture this sort of restriction in the C# type system.

Community
  • 1
  • 1
Eric Lippert
  • 647,829
  • 179
  • 1,238
  • 2,067
  • Your example sounds a lot like your blog article on [Red-Green Trees](https://ericlippert.com/2012/06/08/red-green-trees/). – Brian Mar 03 '17 at 14:01
  • Could you clarify your suggestion (under "Fourth") please? Perhaps using a code example. – Hatchling Mar 03 '17 at 21:13