В следующем примере кода у нас есть класс для неизменяемых объектов, который представляет комнату. Север, Юг, Восток и Запад представляют выходы в другие комнаты.
public sealed class Room
{
public Room(string name, Room northExit, Room southExit, Room eastExit, Room westExit)
{
this.Name = name;
this.North = northExit;
this.South = southExit;
this.East = eastExit;
this.West = westExit;
}
public string Name { get; }
public Room North { get; }
public Room South { get; }
public Room East { get; }
public Room West { get; }
}
Итак, мы видим, что этот класс разработан с рефлексивной циклической ссылкой. Но поскольку класс неизменен, я застрял с проблемой «курица или яйцо». Я уверен, что опытные функциональные программисты знают, как справиться с этим. Как это можно сделать в C #?
Я пытаюсь написать текстовую приключенческую игру, но использую принципы функционального программирования только для обучения. Я застрял на этой концепции и могу использовать некоторую помощь !!! Спасибо.
ОБНОВИТЬ:
Вот рабочая реализация, основанная на ответе Майка Накиса относительно отложенной инициализации:
using System;
public sealed class Room
{
private readonly Func<Room> north;
private readonly Func<Room> south;
private readonly Func<Room> east;
private readonly Func<Room> west;
public Room(
string name,
Func<Room> northExit = null,
Func<Room> southExit = null,
Func<Room> eastExit = null,
Func<Room> westExit = null)
{
this.Name = name;
var dummyDelegate = new Func<Room>(() => { return null; });
this.north = northExit ?? dummyDelegate;
this.south = southExit ?? dummyDelegate;
this.east = eastExit ?? dummyDelegate;
this.west = westExit ?? dummyDelegate;
}
public string Name { get; }
public override string ToString()
{
return this.Name;
}
public Room North
{
get { return this.north(); }
}
public Room South
{
get { return this.south(); }
}
public Room East
{
get { return this.east(); }
}
public Room West
{
get { return this.west(); }
}
public static void Main(string[] args)
{
Room kitchen = null;
Room library = null;
kitchen = new Room(
name: "Kitchen",
northExit: () => library
);
library = new Room(
name: "Library",
southExit: () => kitchen
);
Console.WriteLine(
$"The {kitchen} has a northen exit that " +
$"leads to the {kitchen.North}.");
Console.WriteLine(
$"The {library} has a southern exit that " +
$"leads to the {library.South}.");
Console.ReadKey();
}
}
c#
functional-programming
immutability
circular-dependency
Рок Энтони Джонсон
источник
источник
Room
пример.type List a = Nil | Cons of a * List a
. И бинарное дерево:type Tree a = Leaf a | Cons of Tree a * Tree a
. Как видите, они оба ссылаются на себя (рекурсивно). Вот как вы бы определить номер:type Room = Nil | Open of {name: string, south: Room, east: Room, north: Room, west: Room}
.Room
класса и aList
в Haskell, который я написал выше.Ответы:
Очевидно, что вы не можете сделать это, используя именно тот код, который вы разместили, потому что в какой-то момент вам нужно будет создать объект, который должен быть связан с другим объектом, который еще не был создан.
Есть два способа, которыми я могу придумать (которые я использовал ранее), чтобы сделать это:
Используя две фазы
Все объекты строятся первыми, без каких-либо зависимостей, и, как только они все были построены, они соединяются. Это означает, что объекты должны пройти две фазы в своей жизни: очень короткую изменяемую фазу, за которой следует неизменная фаза, которая длится в течение всей оставшейся жизни.
При моделировании реляционных баз данных вы можете столкнуться с точно такой же проблемой: одна таблица имеет внешний ключ, который указывает на другую таблицу, а другая таблица может иметь внешний ключ, который указывает на первую таблицу. Способ, которым это обрабатывается в реляционных базах данных, заключается в том, что ограничения внешнего ключа могут (и обычно) указываются с помощью дополнительного
ALTER TABLE ADD FOREIGN KEY
оператора, который отделен отCREATE TABLE
оператора. Итак, сначала вы создаете все свои таблицы, затем добавляете ограничения внешнего ключа.Разница между реляционными базами данных и тем, что вы хотите сделать, состоит в том, что реляционные базы данных продолжают разрешать
ALTER TABLE ADD/DROP FOREIGN KEY
операторы в течение всего времени существования таблиц, в то время как вы, вероятно, установите флаг IamImmutable и откажетесь от любых дальнейших мутаций, как только будут реализованы все зависимости.Использование ленивой инициализации
Вместо ссылки на зависимость вы передаете делегат, который при необходимости вернет ссылку на зависимость. Как только зависимость извлечена, делегат больше никогда не вызывается.
Делегат обычно принимает форму лямбда-выражения, поэтому он будет выглядеть немного более многословным, чем фактическая передача зависимостей конструкторам.
(Крошечный) недостаток этого метода заключается в том, что вы должны тратить пространство памяти, необходимое для хранения указателей на делегатов, которые будут использоваться только во время инициализации вашего графа объектов.
Вы даже можете создать универсальный класс «ленивых ссылок», который реализует это, так что вам не нужно повторно реализовывать его для каждого из ваших членов.
Вот такой класс написан на Java, вы легко можете переписать его на C #
(Мой
Function<T>
какFunc<T>
делегат C #)Предполагается, что этот класс является поточно-ориентированным, а «двойная проверка» связана с оптимизацией в случае параллелизма. Если вы не планируете быть многопоточным, вы можете избавиться от всего этого. Если вы решите использовать этот класс в многопоточной установке, обязательно прочитайте о «двойной проверке идиомы». (Это долгое обсуждение, выходящее за рамки этого вопроса.)
источник
Ленивый шаблон инициализации в ответе Майка Накиса прекрасно работает для одноразовой инициализации между двумя объектами, но становится громоздким для нескольких взаимосвязанных объектов с частыми обновлениями.
Намного проще и удобнее управлять связями между комнатами вне самих комнатных объектов, что-то вроде
ImmutableDictionary<Tuple<int, int>, Room>
. Таким образом, вместо создания циклических ссылок, вы просто добавляете одну легко обновляемую одностороннюю ссылку на этот словарь.источник
Room
из появляясь , чтобы эти отношения; но они должны быть получателями, которые просто читают из индекса.Способ сделать это в функциональном стиле - узнать, что вы на самом деле строите: ориентированный граф с помеченными ребрами.
Подземелье - это структура данных, которая отслеживает кучу комнат и вещей, а также отношения между ними. Каждый вызов «с» возвращает новое, другое неизменное подземелье. Комнаты не знают, что к северу и югу от них; книга не знает, что это в сундуке. Темница знает эти факты, и что вещь не имеет никаких проблем с циклическими ссылками , потому что их нет.
источник
Цыпленок и яйцо это правильно. Это не имеет смысла в C #:
Но это делает:
Но это означает, что А не является неизменным!
Вы можете обмануть:
Это скрывает проблему. Конечно, A и B имеют неизменное состояние, но они относятся к чему-то, что не является неизменным. Что может легко победить цель сделать их неизменными. Я надеюсь, что C, по крайней мере, настолько потокобезопасен, насколько вам нужно.
Существует шаблон, называемый заморозка-оттаивание:
Теперь «а» является неизменным. «А» не есть, а «есть». Почему это нормально? До тех пор, пока ничто не знает об «а» до того, как оно замерзнет, кого это волнует?
Есть метод thaw (), но он никогда не меняет «a». Это делает изменчивую копию «а», которая может быть обновлена, а затем заморожена.
Недостатком этого подхода является то, что класс не обеспечивает неизменность. Следующая процедура есть. Вы не можете сказать, является ли это неизменным от типа.
Я действительно не знаю идеальный способ решить эту проблему в C #. Я знаю способы скрыть проблемы. Иногда этого достаточно.
Когда это не так, я использую другой подход, чтобы вообще избежать этой проблемы. Например: посмотрите, как здесь реализован шаблон состояния . Вы могли бы подумать, что они сделали бы это как круговую ссылку, но они этого не делают. Они заводят новые объекты каждый раз, когда состояние меняется. Иногда проще злоупотребить сборщиком мусора, чем выяснить, как достать яйца из кур.
источник
a.freeze()
может вернутьImmutableA
тип. которые делают его в основном образцом строителя.b
со ссылкой на старый изменяемый файлa
. Идея заключается в том , чтоa
иb
должно указывать на неизменных версии друг от друга , прежде чем выпустить их к остальной части системы.Некоторые умные люди уже высказали свое мнение по этому поводу , но я просто думаю, что зал не должен знать, каковы его соседи.
Я думаю, что здание должно знать, где находятся комнаты. Если комната действительно должна знать своих соседей, передайте ей INeigbourFinder.
источник