Как я могу создать неизменные взаимно рекурсивные объекты? - PullRequest
20 голосов
/ 29 декабря 2010

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

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 на полях и оставить изменяемыми «кишки», но общедоступный интерфейс должен быть неизменным.чтобы сделать это?

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

Ответы [ 4 ]

11 голосов
/ 30 декабря 2010

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

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

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

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

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

Ну и дела, ты должен был сказать об этом в первую очередь.Если есть одна вещь, о которой я знаю, это анализ типов.Очевидно, что компилятор должен уметь обрабатывать все виды ситуаций типа сумасшедших, включая типы с циклическими базовыми классами, циклы с внутренними типами, аргументы типов, ограничения типов и т.проблемы в основном из-за того, что объекты «поставлены» в своей неизменности.То есть, когда мы впервые создаем набор типов, каждый объект типа знает свое имя и свою позицию в исходном коде (или метаданных).Имя тогда становится неизменным.Затем мы определяем базовые типы и проверяем их на наличие циклов;базовые типы становятся неизменяемыми.Затем мы проверяем ограничения типа ... затем мы разрешаем атрибуты ... и так далее, пока все не будет проанализировано.

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

Вы можете сделать то же самое здесь.Вы можете иметь класс «typeset», который содержит набор неизменяемых типов, и мультикарту от типа к набору неизменяемых атрибутов.Вы можете создать набор типов и набор атрибутов так, как вам нравится;то, что меняется, это карта, а не тип.

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

6 голосов
/ 29 декабря 2010

Определенно невозможно использовать неизменяемость при однократной записи . Позвольте мне объяснить, почему. Вы можете установить значения полей только в конструкторе. Поэтому, если вы хотите, чтобы 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;
    }
}
4 голосов
/ 28 февраля 2014

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

Ниже приведен пример класса 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
}

Подумайте о том, чтобы сделать конструкторы частными и создать фабрику для производства пар Foo, поскольку это довольно уродливокак написано и заставляет вас предоставить публичный доступ к другим.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...