C #에서 불변 개체 간의 순환 참조를 모델링하는 방법은 무엇입니까?

다음 코드 예제에는 방을 나타내는 불변 객체에 대한 클래스가 있습니다. 북쪽, 남쪽, 동쪽 및 서쪽은 다른 방으로 나가는 출구를 나타냅니다.

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 #에서 어떻게 처리 할 수 ​​있습니까?

텍스트 기반 어드벤처 게임을 코딩하려고 노력하고 있지만 학습을 위해 기능적 프로그래밍 원칙을 사용하고 있습니다. 나는이 개념을 고수하고 도움을 사용할 수 있습니다! 감사.

최신 정보:

게으른 초기화에 관한 Mike Nakis의 답변을 기반으로 작동하는 구현은 다음과 같습니다.

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



답변

분명히 어떤 시점에서 아직 생성되지 않은 다른 객체에 연결 해야하는 객체를 생성해야하기 때문에 게시 한 코드를 사용하여 정확히 수행 할 수는 없습니다.

이것을하기 위해 내가 생각할 수있는 두 가지 방법이있다 :

두 단계 사용

모든 개체는 종속성없이 먼저 구성되며 모든 개체가 구성되면 연결됩니다. 이것은 객체가 인생에서 두 단계를 거쳐야한다는 것을 의미합니다 : 매우 짧은 가변 단계 다음에 수명이 다할 때까지 지속되는 불변 단계.

관계형 데이터베이스를 모델링 할 때 똑같은 종류의 문제가 발생할 수 있습니다. 한 테이블에는 다른 테이블을 가리키는 외래 키가 있고 다른 테이블에는 첫 번째 테이블을 가리키는 외래 키가있을 수 있습니다. 이것이 관계형 데이터베이스에서 처리되는 방식은 외래 키 제약 조건이 ALTER TABLE ADD FOREIGN KEY명령문 과 별도의 추가 명령문으로 지정할 수 있고 일반적으로 지정 되는 것입니다 CREATE TABLE. 따라서 먼저 모든 테이블을 만든 다음 외래 키 제약 조건을 추가하십시오.

관계형 데이터베이스와 수행하려는 작업의 차이점은 관계형 데이터베이스 ALTER TABLE ADD/DROP FOREIGN KEY는 테이블 수명 동안 명령문을 계속 허용 하지만 모든 종속성이 실현되면 ‘IamImmutable’플래그를 설정하고 추가 변이를 거부한다는 것입니다.

지연 초기화 사용

종속성에 대한 참조 대신 필요한 경우 종속성에 대한 참조를 반환 하는 대리자 를 전달 합니다. 종속성이 페치되면 델리게이트는 다시 호출되지 않습니다.

대리자는 일반적으로 람다 식의 형태를 취하므로 실제로 생성자에 종속성을 전달하는 것보다 약간 더 장황하게 보입니다.

이 기술의 단점은 객체 그래프를 초기화하는 동안에 만 사용되는 델리게이트에 대한 포인터를 저장하는 데 필요한 저장 공간을 낭비해야한다는 것입니다.

이를 구현하는 일반 “게으른 참조”클래스를 만들 수도 있으므로 멤버마다 하나씩 다시 구현할 필요가 없습니다.

Java로 작성된 클래스는 다음과 같습니다. C #으로 쉽게 작성할 수 있습니다.

(내 C # Function<T>Func<T>대표 와 같습니다 )

package saganaki.util;

import java.util.Objects;

/**
 * A {@link Function} decorator which invokes the given {@link Function} only once, when actually needed, and then caches its result and never calls it again.
 * It behaves as if it is immutable, which includes the fact that it is thread-safe, provided that the given {@link Function} is also thread-safe.
 *
 * @param <T> the type of object supplied.
 */
public final class LazyImmutable<T> implements Function<T>
{
    private static final boolean USE_DOUBLE_CHECK = false; //TODO try with "double check"
    private final Object lock = new Object();
    @SuppressWarnings( "FieldAccessedSynchronizedAndUnsynchronized" )
    private Function<T> supplier;
    @SuppressWarnings( "FieldAccessedSynchronizedAndUnsynchronized" )
    private T value;

    /**
     * Constructor.
     *
     * @param supplier the {@link Function} which will supply the supplied object the first time it is needed.
     */
    public LazyImmutable( Function<T> supplier )
    {
        assert supplier != null;
        assert !(supplier instanceof LazyImmutable);
        this.supplier = supplier;
        value = null;
    }

    @Override
    public T invoke()
    {
        if( USE_DOUBLE_CHECK )
        {
            if( supplier != null )
                doCheck();
            return value;
        }

        doCheck();
        return value;
    }

    private void doCheck()
    {
        synchronized( lock )
        {
            if( supplier != null )
            {
                value = supplier.invoke();
                supplier = null;
            }
        }
    }

    @Override
    public String toString()
    {
        if( supplier != null )
            return "(lazy)";
        return Objects.toString( value );
    }
}

이 클래스는 스레드로부터 안전해야하며 “더블 체크”항목은 동시성의 경우 최적화와 관련이 있습니다. 다중 스레드를 계획하지 않을 경우 모든 것을 제거 할 수 있습니다. 다중 스레드 설정에서이 클래스를 사용하기로 결정한 경우 “더블 체크 관용구”에 대해 읽어보십시오. (이 질문의 범위를 벗어나는 긴 토론입니다.)


답변

Mike Nakis의 답변에서 게으른 초기화 패턴은 두 객체 간의 일회성 초기화에 적합하지만 자주 업데이트되는 여러 관련 객체에 대해서는 다루기 어렵습니다.

룸 객체 외부 의 룸 사이의 링크를와 같은 방식으로 유지하는 것이 훨씬 간단하고 관리하기 쉽습니다 ImmutableDictionary<Tuple<int, int>, Room>. 이런 식으로 순환 참조를 작성하는 대신이 사전에 대해 쉽게 업데이트 할 수있는 단방향 단일 참조를 추가하면됩니다.


답변

기능적인 스타일로이 작업을 수행하는 방법은 실제로 구성하고있는 것을 인식하는 것입니다. 가장자리가 레이블이 지정된 방향 그래프.

Room library = new Room("Library");
Room ballroom = new Room("Ballroom");
Thing chest = new Thing("Treasure chest");
Thing book = new Thing("Ancient Tome");
Dungeon dungeon = Dungeon.Empty
  .WithRoom(library)
  .WithRoom(ballroom)
  .WithThing(chest)
  .WithThing(book)
  .WithPassage("North", library, ballroom)
  .WithPassage("South", ballroom, library)
  .WithContainment(library, chest)
  .WithContainment(chest, book);

던전은 방과 사물, 그리고 그 사이의 관계를 추적하는 데이터 구조입니다. 각각의 “with”호출은 새로운 다른 불변의 던전을 반환합니다 . 방은 북쪽과 남쪽이 무엇인지 모릅니다. 책은 그것이 가슴에 있다는 것을 모른다. 던전은 그 사실을 알고 있으며, 어느 것도 없기 때문에 것은 순환 참조 아무 문제가 없습니다.


답변

닭고기와 계란이 옳습니다. 이것은 C #에서 의미가 없습니다.

A a = new A(b);
B b = new B(a);

그러나 이것은 :

A a = new A();
B b = new B(a);
a.setB(b);

그러나 그것은 A가 불변이 아니라는 것을 의미합니다!

당신은 속일 수 있습니다 :

C c = new C();
A a = new A(c);
B b = new B(c);
c.addA(a);
c.addB(b);

그것은 문제를 숨 깁니다. 물론 A와 B는 불변 상태이지만 불변이 아닌 것을 말합니다. 그것들을 불변으로 만드는 지점을 쉽게 물리 칠 수 있습니다. C가 최소한 스레드 안전을 유지하기를 바랍니다.

동결 해동이라는 패턴이 있습니다.

A a = new A();
B b = new B(a);
a.addB(b);
a.freeze();

이제 ‘a’는 불변입니다. ‘A’는 아니지만 ‘a’입니다. 왜 괜찮아? 얼어 붙기 전에 ‘a’에 대해 아는 사람이 없다면, 누가 신경 쓰나요?

thaw () 메소드가 있지만 ‘a’는 변경되지 않습니다. ‘a’의 변경 가능한 복사본을 만들어 업데이트 한 다음 고정 할 수도 있습니다.

이 방법의 단점은 클래스가 불변성을 강제하지 않는다는 것입니다. 다음 절차는 다음과 같습니다. 유형에서 불변인지 여부를 알 수 없습니다.

나는 C # 에서이 문제를 해결하는 이상적인 방법을 정말로 모른다. 문제를 숨기는 방법을 알고 있습니다. 때로는 충분합니다.

그렇지 않은 경우이 문제를 완전히 피하기 위해 다른 접근법을 사용합니다. 예를 들어 여기 에서 상태 패턴이 어떻게 구현되는지 살펴보십시오 . 당신은 그들이 순환 참조로 그렇게 할 것이라고 생각하지만 그렇지 않습니다. 상태가 바뀔 때마다 새 객체를 크랭크합니다. 때로는 가비지 수집기를 남용하는 것이 닭에서 계란을 얻는 방법을 알아내는 것이 더 쉽습니다.


답변

일부 똑똑한 사람들은 이미 이것에 대한 의견을 표명했지만 그 이웃이 무엇인지 아는 것은 방의 책임이 아니라고 생각합니다 .

방이 어디에 있는지 아는 것은 건물의 책임이라고 생각합니다. 방이 이웃을 알아야 할 경우 INeigbourFinder를 전달하십시오.


답변