Как я могу создать экземпляр неизменяемых взаимно рекурсивных объектов?
У меня есть неизменяемый рекурсивный тип:
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();
}