Как я могу создать экземпляр неизменяемых взаимно рекурсивных объектов?

У меня есть неизменяемый рекурсивный тип:

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

Мне нужно создать два объекта этого типа, которые относятся друг к другу, т.е. a.Other == b && b.Other == a.

Я не хочу отказываться от инвариантов неизменяемости, потому что Foo предназначен для использования в качестве мухи. Я могу (и я думаю, должен) отказаться от readonly в полях и оставить "guts" изменчивым, но открытый интерфейс должен быть неизменным.

Является неизменность popsicle единственным способом сделать это?

EDIT. Я пытаюсь смоделировать коллекцию типов. Каждый тип имеет имя и несколько атрибутов. Каждый атрибут имеет имя и тип. Существует несколько взаимно-рекурсивных типов и что там возникла эта проблема.

Ответ 1

Есть другие способы сделать это, но у них могут не быть свойств, которые вы хотите.

Предположим, вы хотели представить неизменяемое дерево значений, построенное из листьев. Это довольно просто. У вас может быть конструктор node, который принимает значение и список дочерних узлов. Это делает довольно простым создание новых деревьев из старых деревьев, и они гарантированно будут ацикличными.

Теперь предположим, что вы хотите представить неизменяемый ориентированный граф значений. Теперь у вас есть проблема, что узлы могут иметь циклы; не может быть "листа" для построения графика. Решение состоит в том, чтобы отказаться от принципа, что node знает, что такое его соседи. Вы можете представить неизменный граф, создав неизменяемый набор узлов и неизменный список ребер. Чтобы добавить node к неизменяемому графу, вы создаете новый граф с этим node, добавленным в набор node. Аналогично для добавления ребра; вы создаете новый список ребер. Теперь факт, что существуют циклы в топологии графа, не имеет значения; ни один объект не имеет цикла в том, какие объекты он ссылается.

Не зная больше о вашем фактическом проблемном пространстве, трудно сказать, какая неизменяемая структура данных будет работать для вашего приложения. Можете ли вы рассказать нам больше о том, что вы пытаетесь сделать?

Я пытаюсь смоделировать коллекцию типов. Каждый тип имеет имя и несколько атрибутов. Каждый атрибут имеет имя и тип. Существует несколько взаимно-рекурсивных типов и что там возникла эта проблема.

Ну что ж, вы должны были сказать это в первую очередь. Если есть одна вещь, о которой я знаю, это анализ типов. Очевидно, что компилятор должен иметь возможность обрабатывать все виды сумасшедших ситуаций типа, включая типы с циклическими базовыми классами, циклы с внутренними типами, аргументы типа, ограничения типов и т.д.

В компиляторе С# мы решаем эти проблемы главным образом, делая объекты "поставленными" в их неизменности. То есть, когда мы сначала создаем набор типов, каждый тип объекта знает свое имя и его позицию в исходном коде (или метаданных). Затем имя становится неизменным. Затем мы разрешаем базовые типы и проверяем их на циклы; тогда базовые типы становятся неизменными. Затем мы проверяем ограничения типа... тогда мы разрешаем атрибуты... и так далее, пока все не будет проанализировано.

Я рассмотрел другие способы сделать это. Например, мы могли бы использовать ту же технику, которую я только что предложил для графиков: создать неизменяемый объект, называемый, например, "компиляцию", к которому вы можете добавлять типы, создавая тем самым новые неизменные компиляции. Компиляция может отслеживать "ребра" между типом и его базовым типом в неизменяемой хэш-карте и затем проверять полученный граф для циклов. Нижняя сторона - это то, что тип не знает своего базового типа; вы должны спросить компиляцию, что такое базовый тип типа.

Здесь вы можете сделать то же самое. У вас может быть класс "набор", который содержит набор неизменяемых типов, и мульти-карту от типа к набору неизменяемых атрибутов. Вы можете создать набор типов и набор атрибутов, как вам нравится; вещь, которая изменяется, - это карта, а не тип.

Нижняя сторона этого заключается в том, что вы больше не запрашиваете тип для своих атрибутов; вы запрашиваете набор для атрибутов типа. Это может не соответствовать вашим потребностям, если вам нужно передавать типы независимо от любого набора.

Ответ 2

Определенно невозможно использовать постоянную однократную запись. Позвольте мне объяснить, почему. Вы можете устанавливать значения полей только в конструкторе. Поэтому, если вы хотите, чтобы a ссылался на b, вам нужно передать ссылку на конструктор b на a. Но b будет уже заморожен. Таким образом, единственный вариант - создать экземпляр b в конструкторе a. Но это невозможно, потому что вы не можете передать ссылку на a, потому что this недопустим внутри конструктора.

С этой точки неизменность эскимосов - самое простое и изящное решение. Другой способ - создать статический метод Foo.CreatePair, который будет создавать экземпляры двух объектов, устанавливать перекрестную ссылку и возвращать замороженный объект.

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

Ответ 3

Ниже приведен пример класса Foo с неизменяемой однократной записью, а не с иммунитетом. Немного уродливый, но довольно простой. Это доказательство концепции; вам нужно будет настроить его в соответствии с вашими потребностями.

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

Использование:

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

Рассмотрите возможность создания конструкторов private и создания factory для создания пар Foo, поскольку это довольно уродливо, как написано, и заставляет вас предоставлять общедоступный доступ к другим.

Ответ 4

Вы можете сделать это, добавив перегрузку конструктора, которая принимает функцию, и передавая ссылку 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();
}