No exemplo de código a seguir, temos uma classe para objetos imutáveis que representam uma sala. Norte, Sul, Leste e Oeste representam saídas para outras salas.
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; }
}
Então vemos que esta classe é projetada com uma referência circular reflexiva. Mas, como a classe é imutável, estou com um problema do tipo "galinha ou ovo". Estou certo de que programadores funcionais experientes sabem como lidar com isso. Como ele pode ser tratado em c #?
Estou tentando codificar um jogo de aventura baseado em texto, mas usando princípios de programação funcional apenas para o aprendizado. Estou preso a este conceito e posso usar alguma ajuda !!! Obrigado.
ATUALIZAR:
Aqui está uma implementação de trabalho baseada na resposta de Mike Nakis sobre inicialização lenta:
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();
}
}
fonte
Room
exemplo.type List a = Nil | Cons of a * List a
. E uma árvore binária:type Tree a = Leaf a | Cons of Tree a * Tree a
. Como você pode ver, ambos são auto-referenciais (recursivos). Veja como você definiria o seu quarto:type Room = Nil | Open of {name: string, south: Room, east: Room, north: Room, west: Room}
.Room
classe e aList
são semelhantes no Haskell que escrevi acima.Respostas:
Obviamente, não é possível fazê-lo usando exatamente o código que você postou, porque em algum momento você precisará construir um objeto que precise ser conectado a outro objeto que ainda não foi construído.
Existem duas maneiras em que posso pensar (que usei antes) para fazer isso:
Usando duas fases
Todos os objetos são construídos primeiro, sem nenhuma dependência e, depois de construídos, eles são conectados. Isso significa que os objetos precisam passar por duas fases na vida: uma fase mutável muito curta, seguida por uma fase imutável que dura o resto da vida.
Você pode encontrar exatamente o mesmo tipo de problema ao modelar bancos de dados relacionais: uma tabela possui uma chave estrangeira que aponta para outra tabela e a outra tabela pode ter uma chave estrangeira que aponta para a primeira tabela. A maneira como isso é tratado nos bancos de dados relacionais é que as restrições de chave estrangeira podem (e geralmente são) especificadas com uma
ALTER TABLE ADD FOREIGN KEY
instrução extra que é separada daCREATE TABLE
instrução. Então, primeiro você cria todas as suas tabelas e depois adiciona suas restrições de chave estrangeira.A diferença entre os bancos de dados relacionais e o que você deseja fazer é que os bancos de dados relacionais continuem permitindo
ALTER TABLE ADD/DROP FOREIGN KEY
instruções durante toda a vida útil das tabelas, enquanto você provavelmente definirá um sinalizador 'IamImmutable' e recusará outras mutações depois que todas as dependências forem realizadas.Usando inicialização lenta
Em vez de uma referência a uma dependência, você passa um delegado que retornará a referência para a dependência quando necessário. Depois que a dependência é buscada, o delegado nunca é chamado novamente.
O delegado geralmente assumirá a forma de uma expressão lambda, portanto parecerá um pouco mais detalhado do que realmente passar as dependências para os construtores.
A (pequena) desvantagem dessa técnica é que você precisa desperdiçar o espaço de armazenamento necessário para armazenar os ponteiros para os delegados, que serão usados apenas durante a inicialização do gráfico de objetos.
Você pode até criar uma classe genérica de "referência lenta" que implementa isso para que você não precise reimplementá-la para todos os membros.
Aqui está uma classe escrita em Java, você pode transcrevê-la facilmente em C #
(Meu
Function<T>
é como oFunc<T>
delegado de C #)Supõe-se que esta classe seja segura para threads e o material "verificação dupla" está relacionado a uma otimização no caso de simultaneidade. Se você não planeja ser multithread, pode retirar todo esse material. Se você decidir usar esta classe em uma instalação multithread, não deixe de ler sobre o "idioma da verificação dupla". (Essa é uma longa discussão além do escopo desta pergunta.)
fonte
O padrão de inicialização lenta na resposta de Mike Nakis 'funciona muito bem para uma inicialização única entre dois objetos, mas se torna pesado para vários objetos inter-relacionados com atualizações frequentes.
É muito mais simples e mais gerenciável manter os links entre salas fora dos próprios objetos da sala, em algo como um
ImmutableDictionary<Tuple<int, int>, Room>
. Dessa forma, em vez de criar referências circulares, você está apenas adicionando uma referência unidirecional fácil de atualizar e atualizável a este dicionário.fonte
Room
partir de aparecer para ter essas relações; mas devem ser getters que simplesmente leem do índice.A maneira de fazer isso em um estilo funcional é reconhecer o que você está realmente construindo: um gráfico direcionado com bordas rotuladas.
Uma masmorra é uma estrutura de dados que monitora várias salas e coisas, e quais são as relações entre elas. Cada chamada "com" retorna uma masmorra imutável nova e diferente . Os quartos não sabem o que é norte e sul deles; o livro não sabe que está no peito. A masmorra conhece esses fatos, e essa coisa não tem problema com referências circulares, porque não existem.
fonte
Frango e um ovo estão certos. Isso não faz sentido em c #:
Mas isso faz:
Mas isso significa que A não é imutável!
Você pode trapacear:
Isso esconde o problema. Claro que A e B têm um estado imutável, mas se referem a algo que não é imutável. O que poderia facilmente derrotar o ponto de torná-los imutáveis. Espero que C seja pelo menos tão seguro quanto o segmento que você precisar.
Existe um padrão chamado congelamento-descongelamento:
Agora 'a' é imutável. 'A' não é, mas 'a' é. Por que está tudo bem? Enquanto nada mais souber sobre 'a' antes de congelar, quem se importa?
Existe um método thaw (), mas ele nunca muda 'a'. Faz uma cópia mutável de 'a' que pode ser atualizada e congelada também.
A desvantagem dessa abordagem é que a classe não está impondo imutabilidade. O procedimento a seguir é. Você não pode dizer se é imutável do tipo.
Eu realmente não sei uma maneira ideal de resolver esse problema em c #. Eu sei maneiras de esconder os problemas. Às vezes é o suficiente.
Quando não é, eu uso uma abordagem diferente para evitar esse problema completamente. Por exemplo: veja como o padrão de estado é implementado aqui . Você pensaria que eles fariam isso como uma referência circular, mas não o fazem. Eles acionam novos objetos toda vez que o estado muda. Às vezes, é mais fácil abusar do coletor de lixo do que descobrir como tirar os ovos das galinhas.
fonte
a.freeze()
poderia retornarImmutableA
tipo. o que o torna basicamente padrão de construtor.b
segurando uma referência ao antigo mutávela
. A idéia é essaa
eb
deve apontar para versões imutáveis uma da outra antes de liberá-las para o restante do sistema.Algumas pessoas inteligentes já expressaram suas opiniões sobre isso, mas acho que não é responsabilidade da sala saber quais são seus vizinhos.
Eu acho que é responsabilidade do edifício saber onde estão os quartos. Se a sala realmente precisar conhecer seus vizinhos, passe o INeigbourFinder para ela.
fonte