How can I instantiate immutable mutually recursive

2019-02-04 07:04发布

I have an immutable recursive type:

public sealed class Foo
{
    private readonly object something;
    private readonly Foo other; // might be null

    public Foo(object something, Foo other)
    {
        this.something = something;
        this.other = other;
    }
    public object Something { get { return something; } }
    public Foo Other { get { return other; } }
}

I need to instantiate two objects of this type that refer to each other, i.e. a.Other == b && b.Other == a.

I don't want to abandon the immutability invariants because Foo is intended for use as a flyweight. I can (and I think must) abandon readonly on the fields, and leave the "guts" mutable, but the public interface must be immutable.

Is popsicle immutability the only way to get this done?

I'm trying to model a collection of types. Each type has a name and several attributes. Each attribute has a name and a type. There are a few mutually recursive types, and that's where this problem arose.

4条回答
甜甜的少女心
2楼-- · 2019-02-04 07:43

There are other ways to get it done, but they might not have properties that you want.

Suppose you wanted to represent an immutable tree of values, built from the leaves up. That's pretty straightforward. You might have a node constructor that takes a value and a list of child nodes. That makes it pretty straightforward to construct new trees out of old trees, and they're guaranteed to be acyclic.

Now suppose you want to represent an immutable directed graph of values. Now you have the problem that nodes can have cycles; there might not be a "leaf" to build the graph from. The solution is to abandon the principle that the node knows what its neighbours are. You could represent an immutable graph by making an immutable set of nodes, and an immutable list of edges. To add a node to the immutable graph you construct a new graph with that node added to the node set. Similarly for adding an edge; you construct a new list of edges. Now the fact that there are cycles in the topology of the graph is irrelevant; no one object has a cycle in what objects it references.

Without knowing more about your actual problem space it is hard to say what immutable data structure would work for your application. Can you tell us more about what you're trying to do?

I'm trying to model a collection of types. Each type has a name and several attributes. Each attribute has a name and a type. There are a few mutually recursive types, and that's where this problem arose.

Well geez, you should have said so in the first place. If there's one thing I know about, it's analysis of types. Obviously the compiler needs to be able to handle all kinds of crazy type situations, including types with cyclic base classes, cycles involving inner types, type arguments, type constraints and so on.

In the C# compiler we solve these problems mostly by making objects "staged" in their immutability. That is, when we first create a set of types, each type object knows its name and its position in source code (or metadata). The name then becomes immutable. We then resolve the base types and check them for cycles; the base types then become immutable. Then we check the type constraints... then we resolve the attributes... and so on, until everything is analyzed.

I have considered other ways of doing this. For example, we might use the same technique that I just suggested for graphs: make an immutable object, called, say "compilation", to which you can add types, thereby producing new immutable compilations. The compilation can keep track of the "edges" between a type and its base type in an immutable hash map, and can then check the resulting graph for cycles. The down side is then that a type does not know its base type; you have to ask the compilation what the base type of a type is.

You could do the same thing here. You could have a class "typeset" that contains a set of immutable types, and a multi-map from type to a set of immutable attributes. You can build up the set of types and the set of attributes however you like; the thing that changes is the map, not the type.

The down side of this is that you no longer ask the type for its attributes; you ask the typeset for the attributes of a type. That might not meet your needs if you need to pass types around independently of any typeset.

查看更多
一夜七次
3楼-- · 2019-02-04 07:48

You can do it by adding a constructor overload that accepts a function, and passing the this reference to that function. Something like this:

public sealed class Foo {
    private readonly object something;
    private readonly Foo other;

    public Foo(object something, Foo other) {
        this.something = something;
        this.other = other;
    }

    public Foo(object something, Func<Foo, Foo> makeOther) {
        this.something = something;
        this.other = makeOther(this);
    }

    public object Something { get { return something; } }
    public Foo Other { get { return other; } }
}

static void Main(string[] args) {
    var foo = new Foo(1, x => new Foo(2, x)); //mutually recursive objects
    Console.WriteLine(foo.Something); //1
    Console.WriteLine(foo.Other.Something); //2
    Console.WriteLine(foo.Other.Other == foo); //True
    Console.ReadLine();
}
查看更多
劳资没心,怎么记你
4楼-- · 2019-02-04 07:52

It is definitely impossible using write-once immutability. Let me explain why. You can set fields' values only at constructor. So if you want a to refer to b you have to pass reference to b to a's constructor. But b will be already frozen. So the only option is to instantiate b in a's constructor. But this is impossible, because you can't pass reference to a, because this is invalid inside constructor.

From this point popsicle immutability is the simplest and most elegant solution. Another way is to create static method Foo.CreatePair that will instantiate two objects, set cross reference and return frozen object.

public sealed class Foo
{
    private readonly object something;
    private Foo other; // might be null

    public Foo(object something, Foo other)
    {
        this.something = something;
        this.other = other;
    }
    public object Something { get { return something; } }
    public Foo Other { get { return other; } private set { other = value; } }

    public static CreatePair(object sa, object sb)
    {
        Foo a = new Foo(sa, null);
        Foo b = new Foo(sb, a);
        a.Other = b;
        return a;
    }
}
查看更多
可以哭但决不认输i
5楼-- · 2019-02-04 07:53

Below is an example of a class Foo with write-once immutability rather than popsicle immunity. A bit ugly, but pretty simple. This is a proof-of-concept; you will need to tweak this to fit your needs.

public sealed class Foo
{
    private readonly string something;
    private readonly Foo other; // might be null

    public Foo(string s)
    {
        something = s;
        other = null;
    }

    public Foo(string s, Foo a)
    {
        something = s;
        other = a;
    }
    public Foo(string s, string s2)
    {
        something = s;
        other = new Foo(s2, this);
    }
}

Usage:

static void Main(string[] args)
{
    Foo xy = new Foo("a", "b");
    //Foo other = xy.GetOther(); //Some mechanism you use to get the 2nd object
}

Consider making the constructors private and creating a factory to produce Foo pairs, as this is rather ugly as written and forces you to provide public access to other.

查看更多
登录 后发表回答