C # 'daki değişken nesneler arasındaki dairesel bir referans nasıl modellenir?


24

Aşağıdaki kod örneğinde, bir odayı temsil eden değişmez nesneler için bir sınıfımız var. Kuzey, Güney, Doğu ve Batı diğer odalara çıkışları temsil eder.

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

Görüyoruz ki, bu sınıf refleks dairesel bir referansla tasarlandı. Ancak sınıf değişmez olduğu için, bir 'tavuk ya da yumurta' sorununa kapıldım. Deneyimli fonksiyonel programcıların bununla nasıl başa çıkacağını bildiğinden eminim. C # ile nasıl ele alınabilir?

Metin tabanlı bir macera oyununu kodlamaya çalışıyorum, ancak sadece öğrenme amacıyla işlevsel programlama ilkelerini kullanmaya çalışıyorum. Bu kavram üzerinde sıkışıp kaldım ve biraz yardım kullanabilirim !!! Teşekkürler.

GÜNCELLEŞTİRME:

İşte Mike Nakis'in tembel başlatma konusundaki cevabına dayanan bir çalışma uygulaması:

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

Bu, yapılandırma ve Oluşturucu Deseni için iyi bir durum gibi geliyor.
Greg Burghardt

Ayrıca, bir odanın seviye veya sahne düzeninden ayrılıp ayrılmayacağını merak ediyorum, böylece her oda diğerlerini bilmiyor.
Greg Burghardt

1
@RockAnthonyJohnson Buna gerçekten refleksif demeyeceğim, ama bu uygun değil. Neden bu bir problem? Bu son derece yaygın. Aslında, neredeyse tüm veri yapılarının nasıl inşa edildiğidir. Bağlantılı bir liste veya ikili bir ağaç düşünün. Hepsi özyinelemeli veri yapıları ve sizin de Roomörneğiniz.
gardenhead

2
@RockAnthonyJohnson Immutable veri yapıları, en azından fonksiyonel programlamada oldukça yaygındır. Bu, bir bağlantılı liste anladığım bu: type List a = Nil | Cons of a * List a. Ve bir ikili ağacı: type Tree a = Leaf a | Cons of Tree a * Tree a. Gördüğünüz gibi, ikisi de kendi kendini yönlendiriyor (özyinelemeli). Burada oda tanımlarsınız açıklanmıştır: type Room = Nil | Open of {name: string, south: Room, east: Room, north: Room, west: Room}.
gardenhead

1
Eğer ilgileniyorsanız, Haskell veya OCaml öğrenmek için zaman ayırın; zihninizi genişletir;) Ayrıca, veri yapıları ile "iş nesneleri" arasında net bir ayrım olmadığını unutmayın. RoomSınıfınızın ve List a'nın tanımının yukarıda yazdığım Haskell'de ne kadar benzer olduğuna bakın .
gardenhead,

Yanıtlar:


10

Açıkçası, tam olarak belirttiğiniz kodu kullanarak bunu yapamazsınız, çünkü bir noktada henüz inşa edilmemiş başka bir nesneye bağlı olması gereken bir nesneyi inşa etmeniz gerekecektir.

Bunu yapmak için düşünebildiğim (daha önce kullanmış olduğum) iki yol var:

İki fazın kullanılması

Tüm nesneler ilk önce, herhangi bir bağımlılık olmadan inşa edilir ve hepsi bir kez oluşturulduktan sonra birbirlerine bağlanırlar. Bu, nesnelerin yaşamlarında iki aşamadan geçmesi gerektiği anlamına gelir: çok kısa bir değişken evre, bunu takiben ömürleri boyunca devam eden değişmez bir evre.

İlişkisel veritabanlarını modellerken de aynı sorunla karşılaşabilirsiniz: bir tablonun diğer tablonun işaret ettiği bir yabancı anahtar var, diğer tablonun ilk tablonun işaret ettiği yabancı bir anahtar olabilir. Bunun ilişkisel veritabanlarında ele alınmasının yolu, yabancı anahtar kısıtlamalarının, ALTER TABLE ADD FOREIGN KEYifadeden ayrı bir ek ifade ile belirtilebilmesi (ve genellikle) olmasıdır CREATE TABLE. Yani, önce tüm masalarınızı yaratırsınız, sonra yabancı anahtar kısıtlamalarınızı eklersiniz.

İlişkisel veritabanları ile yapmak istedikleriniz arasındaki fark, ilişkisel veritabanlarının ALTER TABLE ADD/DROP FOREIGN KEYtabloların kullanım ömrü boyunca ifadelere izin vermeye devam etmesidir; bununla birlikte muhtemelen bir 'IamImmutable' bayrağı belirleyeceksiniz ve tüm bağımlılıklar gerçekleştiğinde başka mutasyonları reddedeceksiniz.

Tembel başlatma kullanma

Bir bağımlılık referansı yerine, referansı gerektiğinde bağımlılığa döndürecek bir temsilci geçersiniz . Bağımlılık alındıktan sonra, temsilci bir daha asla çağrılmaz.

Delege genellikle bir lambda ifadesi şeklini alır, bu yüzden aslında kuruculara geçen bağımlılıklardan sadece biraz daha ayrıntılı görünecektir.

Bu tekniğin dezavantajı (minik), işaretçileri delegelere depolamak için gereken depolama alanını boşa harcamanız ve yalnızca nesne grafiğinizin başlatılması sırasında kullanılacak.

Bunu uygulayan genel bir "tembel başvuru" sınıfı bile oluşturabilirsiniz, böylece üyelerinizin her biri için onu yeniden uygulamanıza gerek kalmaz.

İşte böyle bir sınıf Java ile yazılmış, kolayca C # ile yazabilirsiniz

(Benim C # ' Function<T>nin Func<T>delegesi gibi )

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

Bu sınıf iş parçacığı güvenli olması gerekiyordu ve "çift çek" şeyler eşzamanlılık durumunda bir optimizasyon ile ilgilidir. Çok iş parçacıklı olmayı planlamıyorsanız, tüm bunları kaldırabilirsiniz. Bu sınıfı çok iş parçacıklı bir kurulumda kullanmaya karar verirseniz, "double check deyim" hakkında bilgi aldığınızdan emin olun. (Bu, bu sorunun kapsamı ötesinde uzun bir tartışma.)


1
Mike, harikasın. Orijinal yayını, tembel başlatma hakkında yayınladığınıza dayanan bir uygulamayı içerecek şekilde güncelledim.
Rock Anthony Johnson

1
.Net kütüphanesi, uygun olarak Lazy <T> adında tembel bir referans sağlar. Ne kadar güzel! Bunu codereview.stackexchange.com/questions/145039/…
Rock Anthony Johnson

16

Mike Nakis'in cevabındaki tembel başlatma paterni, iki nesne arasındaki tek seferlik bir başlatma için gayet iyi çalışır, ancak sık güncellemeler içeren birbiriyle ilişkili çoklu nesneler için hantallaşır.

Odanın dışındaki odalar arasındaki bağlantıları kendileri gibi bir şeyle tutmak çok daha basit ve daha yönetilebilirdir ImmutableDictionary<Tuple<int, int>, Room>. Bu şekilde, dairesel referanslar oluşturmak yerine, bu sözlüğe yalnızca tek, kolayca güncellenebilen, tek yönlü bir referans ekliyorsunuz.


Unutmayın, değişmez nesneler hakkında konuşuyorduk, bu yüzden güncelleme yok.
Rock Anthony Johnson

4
İnsanlar değişmez nesneleri güncellemekten bahsettiklerinde, güncellenmiş niteliklerle yeni bir nesne oluşturmak ve eski nesnenin yerine yeni bir kapsamda bu yeni nesneye atıfta bulunmak demektir. Yine de her seferinde söylemesi biraz sıkıcı oluyor.
Karl Bielefeldt

Karl, lütfen beni affet. Hala işlevsel prensiplerde bir noobum, hahaa.
Rock Anthony Johnson

2
Bu doğru cevap. Genel olarak dairesel bağımlılıklar kırılmalı ve üçüncü bir tarafa devredilmelidir. Değişmez hale gelen değişken nesnelerden oluşan karmaşık bir oluştur ve dondur sistemini programlamaktan daha basittir.
Benjamin Hodgson,

Keşke bunu bir kaç + 1 daha verebilseydim ... Muhtemel ya da olmasın, "harici" bir depo ya da indeks olmadan (ya da her neyse ), bütün bu odaların düzgün bir şekilde bağlanmasını sağlamak gereksiz yere karmaşık olurdu. Ve bu yasaklar yok Roomdan görünen bu ilişkileri var; ancak, basitçe dizinden okuyan alıcılar olmalıdırlar.
svidgen

12

Bunu işlevsel bir tarzda yapmanın yolu, gerçekte ne inşa ettiğinizi tanımaktır: etiketli kenarları olan yönlendirilmiş bir grafik.

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

Bir zindan, bir sürü oda ve şeyi takip eden ve aralarındaki ilişkilerin ne olduğunu izleyen bir veri yapısıdır. Her "with" çağrısı yeni, farklı bir değişken zindan döndürür . Odalar kuzeyde ve güneyde ne olduğunu bilmiyor; kitap göğsünde olduğunu bilmiyor. Zindan bu gerçekleri biliyor ve o orada yok çünkü şey dairesel referanslarla sorunu yok.


1
Yönlendirilmiş grafikler ve akıcı geliştiriciler (ve DSL'ler) okudum. Bunun nasıl yönlendirilmiş bir grafik oluşturabildiğini görebiliyorum, ancak ilk iki fikre bağlı olduğumu gördüm. Kaçırdığım bir kitap veya blog yazısı var mı? Yoksa bu sadece soruları çözdüğü için yönlendirilmiş bir grafik oluşturuyor mu?
candied_orange

@CandiedOrange: Bu, API'nin nasıl görünebileceğinin bir taslağıdır. Aslında altında yatan değişmez yönlendirilmiş grafik veri yapısını inşa etmek biraz çalışmayı gerektiriyordu ama zor değildi. Bir değişmez yönlendirilmiş grafik sadece bir değişmez düğüm kümesi ve bir değişmez (başlangıç, bitiş, etiket) üçlü kümesidir, bu yüzden zaten çözülmüş problemlerin bir bileşimine indirgenebilir.
Eric Lippert

Dediğim gibi, hem DSL’leri hem de grafikleri yönettim. İkisini bir araya getiren bir şey okudunuz veya yazdıysanız ya da bu soruyu cevaplamak için bir araya getirdiyseniz anlamaya çalışıyorum. Onları bir araya getiren bir şey biliyorsan, beni işaret edersen çok isterim.
candied_orange

@CandiedOrange: Özellikle değil. Yıllar önce bir blog dizisini, geriye dönüş yapan bir sudoku çözücüsü yapmak için değiştirilemez yönlendirilmemiş bir grafiğe yazdım. Ve daha yakın zamanda sihirbazlar ve zindanlar alanındaki değişken veri yapıları için nesne yönelimli tasarım sorunları hakkında bir blog serisi yazdım.
Eric Lippert

3

Tavuk ve bir yumurta doğru. Bu c # 'a bir anlam ifade etmiyor:

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

Ancak bu yapar:

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

Ancak bu, A'nın değişmez olmadığı anlamına gelir!

Hile yapabilirsiniz:

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

Bu sorunu gizler. A ve B'nin değişmez bir durumu var, ancak değişmeyen bir şeyden bahsediyorlar. Bu onları kolayca değiştirilemez hale getirme noktasını yenebilir. İnşallah C en az ihtiyacınız olduğu kadar güvenlidir.

Donma-çözülme denilen bir desen var:

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

Şimdi 'a' değişmez. 'A' değil, 'a'. Bu neden iyi? Donmadan önce 'a' hakkında hiçbir şey bilmediği sürece, kimin umrunda?

Bir thaw () metodu var ancak 'a' değişmiyor. 'A' nın değiştirilebilir bir kopyasını güncelleştirilebilir ve sonra dondurulabilir.

Bu yaklaşımın dezavantajı, sınıfın değişmezliği zorlamamasıdır. Aşağıdaki prosedür. Türden etkilenmez olup olmadığını söyleyemezsin.

Bu sorunu c # ile çözmenin ideal bir yolunu gerçekten bilmiyorum. Problemleri gizlemenin yollarını biliyorum. Bazen bu yeterli.

Olmadığı zaman, bu sorunu tamamen önlemek için farklı bir yaklaşım kullanıyorum. Örneğin: burada devlet düzeninin nasıl uygulandığına bakın . Bunu dairesel bir referans olarak yapacaklarını düşünürdünüz ama yapmıyorlar. Devlet her değiştiğinde yeni nesneler çıkardılar. Bazen çöp toplayıcıyı kötüye kullanmak, sonra da tavukları yumurtadan nasıl çıkaracağımızı bulmak daha kolaydır.


Beni yeni bir kalıpla tanıştırmak için +1. İlk önce donma-çözülme duydum.
Rock Anthony Johnson,

a.freeze()ImmutableAtürünü döndürebilirdi . Bu temelde oluşturucu desen yapmak.
Bryan Chen

@BryanChen bunu yaparsanız b, eski değişkene referans olarak bırakılır a. Fikir olmasıdır a ve bsisteme geri kalanına kullanımına sunmadan önce birbirinden değişmez sürümleri işaret etmelidir.
candied_orange 25:16

@RockAnthonyJohnson Bu, Eric Lippert'in Popsicle değişmezliği dediği şey .
Benekli

1

Bazı akıllı insanlar bu konudaki görüşlerini zaten dile getirdiler, ancak ben sadece komşularının ne olduğunu bilmek odanın sorumluluğunun olmadığını düşünüyorum .

Bence odaların nerede olduğunu bilmek binanın sorumluluğu. Oda gerçekten komşularını bilmek gerekiyorsa, INeigbourFinder ona geçmek.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.