C # में अपरिवर्तनीय वस्तुओं के बीच एक वृत्ताकार संदर्भ कैसे दें?


24

निम्नलिखित कोड उदाहरण में, हमारे पास अपरिवर्तनीय वस्तुओं के लिए एक वर्ग है जो एक कमरे का प्रतिनिधित्व करता है। उत्तर, दक्षिण, पूर्व और पश्चिम अन्य कमरों में बाहर निकलते हैं।

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

ऐसा लगता है कि कॉन्फ़िगरेशन और बिल्डर पैटर्न के लिए एक अच्छा मामला है।
ग्रेग बरगद्ट

मुझे यह भी आश्चर्य है कि क्या किसी कमरे को किसी स्तर या मंच के लेआउट से अलग किया जाना चाहिए, ताकि प्रत्येक कमरे को दूसरों के बारे में पता न चले।
ग्रेग बरगद्ट

1
@RockAnthonyJohnson मैं वास्तव में उस प्रतिवर्त को नहीं कहूंगा, लेकिन यह उचित नहीं है। हालांकि यह एक समस्या क्यों है? यह बेहद आम है। वास्तव में, यह लगभग सभी डेटा संरचनाओं का निर्माण कैसे किया जाता है। लिंक की गई सूची या बाइनरी ट्री के बारे में सोचें। वे सभी पुनरावर्ती डेटा संरचनाएं हैं, और इसलिए आपका Roomउदाहरण है।
बागीचा

2
@RockAnthonyJohnson अपरिवर्तनीय डेटा संरचनाएँ अत्यंत सामान्य हैं, कम से कम कार्यात्मक प्रोग्रामिंग में। यह आप कैसे किसी लिंक किए गए सूची निर्धारित है: 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}
बागीचा

1
यदि आप रुचि रखते हैं, तो Haskell या OCaml सीखने के लिए समय निकालें; यह आपके दिमाग का विस्तार करेगा;) यह भी ध्यान रखें कि डेटा संरचनाओं और "व्यावसायिक वस्तुओं" के बीच कोई स्पष्ट सीमांकन नहीं है। देखो कि आपकी Roomकक्षा की परिभाषा और List हास्केल में जो मैंने ऊपर लिखा था, उसी तरह की हैं।
बागीचा

जवाबों:


10

जाहिर है, आप इसे आपके द्वारा पोस्ट किए गए कोड का उपयोग करके नहीं कर सकते, क्योंकि कुछ बिंदु पर आपको एक ऑब्जेक्ट का निर्माण करने की आवश्यकता होगी जिसे किसी अन्य ऑब्जेक्ट से कनेक्ट करने की आवश्यकता है जो अभी तक निर्माण नहीं किया गया है।

ऐसा करने के दो तरीके हैं जो मैं सोच सकता हूं (जो मैंने पहले इस्तेमाल किया है):

दो चरणों का उपयोग करना

सभी वस्तुओं का निर्माण सबसे पहले किया जाता है, बिना किसी निर्भरता के, और एक बार जब वे सभी निर्माण कर चुके होते हैं, तो वे जुड़े होते हैं। इसका मतलब यह है कि वस्तुओं को अपने जीवन में दो चरणों से गुजरना पड़ता है: एक बहुत ही कम परिवर्तनशील चरण, इसके बाद एक अपरिवर्तनीय चरण होता है जो उनके पूरे जीवनकाल तक चलता है।

रिलेशनल डेटाबेस मॉडलिंग करते समय आप ठीक उसी तरह की समस्या में आ सकते हैं: एक तालिका में एक विदेशी कुंजी होती है जो दूसरी तालिका की ओर इशारा करती है, और दूसरी तालिका में एक विदेशी कुंजी हो सकती है जो पहली तालिका को इंगित करती है। जिस तरह से यह संबंधपरक डेटाबेस में संभाला जाता है वह यह है कि विदेशी कुंजी बाधाएं (और आमतौर पर) एक अतिरिक्त ALTER TABLE ADD FOREIGN KEYबयान के साथ निर्दिष्ट की जा सकती हैं जो बयान से अलग है CREATE TABLE। तो, पहले आप अपने सभी टेबल बनाते हैं, फिर आप अपने विदेशी कुंजी बाधाओं को जोड़ते हैं।

संबंधपरक डेटाबेस और आप क्या करना चाहते हैं के बीच का अंतर यह है कि संबंधपरक डेटाबेस ALTER TABLE ADD/DROP FOREIGN KEYतालिकाओं के पूरे जीवनकाल में बयान देने की अनुमति देते हैं , जबकि आप संभवतः एक 'IamImmutable' ध्वज सेट करेंगे और सभी निर्भरता का एहसास होने के बाद किसी भी आगे के उत्परिवर्तन से इनकार करेंगे।

आलसी आरंभीकरण का उपयोग करना

एक निर्भरता के संदर्भ के बजाय आप एक प्रतिनिधि को पास करते हैं जो जरूरत पड़ने पर निर्भरता के संदर्भ को वापस कर देगा। एक बार निर्भरता लाने के बाद, प्रतिनिधि को फिर कभी नहीं बुलाया जाता है।

प्रतिनिधि आमतौर पर एक लंबोदर अभिव्यक्ति का रूप लेगा, इसलिए यह वास्तव में निर्माणकर्ताओं पर निर्भरता से गुजरने की तुलना में केवल थोड़ी अधिक क्रिया लगेगा।

इस तकनीक का नकारात्मक पहलू यह है कि आपको प्रतिनिधियों को पॉइंटर्स को स्टोर करने के लिए आवश्यक स्टोरेज स्पेस को बर्बाद करना होगा जो केवल आपके ऑब्जेक्ट ग्राफ के आरंभीकरण के दौरान उपयोग किया जाएगा।

आप एक सामान्य "आलसी संदर्भ" वर्ग भी बना सकते हैं, जो इसे लागू करता है ताकि आपको अपने हर एक सदस्य के लिए इसे फिर से लागू न करना पड़े।

यहाँ जावा में ऐसा एक वर्ग लिखा गया है, आप इसे आसानी से 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 );
    }
}

यह वर्ग थ्रेड-सुरक्षित माना जाता है, और "डबल चेक" सामान संगामिति के मामले में एक अनुकूलन से संबंधित है। यदि आप बहु-थ्रेडेड होने की योजना नहीं बना रहे हैं, तो आप उस सभी सामान को दूर कर सकते हैं। यदि आप एक बहु-थ्रेडेड सेटअप में इस वर्ग का उपयोग करने का निर्णय लेते हैं, तो "डबल चेक आइडियल" के बारे में पढ़ना सुनिश्चित करें। (यह इस प्रश्न के दायरे से परे एक लंबी चर्चा है।)


1
माइक, तुम शानदार हो। आपने आलसी इनिशियलाइज़ेशन के बारे में जो पोस्ट किया है, उसके आधार पर कार्यान्वयन को शामिल करने के लिए मैंने मूल पोस्ट को अपडेट किया है।
रॉक एंथनी जॉनसन

1
.Net पुस्तकालय एक आलसी संदर्भ प्रदान करता है, जिसे आलसी <टी> नाम दिया गया है। क्या खूब! मैंने इसे एक कोड रिव्यू के जवाब से सीखा है जिसे मैंने codereview.stackexchange.com/questions/145039/…
रॉक एंथनी जॉनसन

16

माइक नाकिस के उत्तर में आलसी इनिशियलाइज़ेशन पैटर्न दो वस्तुओं के बीच एक बार के इनिशियलाइज़ेशन के लिए ठीक काम करता है, लेकिन लगातार अद्यतन के साथ कई इंटर-संबंधित ऑब्जेक्ट्स के लिए अनिच्छुक हो जाता है।

यह बहुत सरल और अधिक प्रबंधनीय है कि कमरे की वस्तुओं के बाहर के कमरों के बीच संबंध रखने के लिए खुद को, जैसे कुछ ImmutableDictionary<Tuple<int, int>, Room>। इस तरह, परिपत्र संदर्भ बनाने के बजाय, आप इस शब्दकोश में एक एकल, आसानी से अद्यतन करने योग्य, एकतरफा संदर्भ जोड़ रहे हैं।


ध्यान रखें कि अपरिवर्तनीय वस्तुओं के बारे में बात कर रहे थे, इसलिए कोई अद्यतन नहीं हैं।
रॉक एंथोनी जॉनसन

4
जब लोग अपरिवर्तनीय वस्तुओं को अद्यतन करने के बारे में बात करते हैं, तो उनका मतलब है कि अद्यतन विशेषताओं के साथ एक नई वस्तु बनाना और पुरानी वस्तु के बदले में उस नई वस्तु को एक नए दायरे में संदर्भित करना। हालांकि, हर बार कहने के लिए थोड़ा थकाऊ हो जाता है।
कार्ल बेज़ेलफेल्ट

कार्ल, मुझे माफ कर दो। मैं अभी भी कार्यात्मक सिद्धांतों में एक noob हूँ, हाहा।
रॉक एंथोनी जॉनसन

2
यह सही जवाब है। परिपत्र निर्भरता को सामान्य रूप से तोड़ दिया जाना चाहिए और तीसरे पक्ष को सौंप दिया जाना चाहिए। यह परिवर्तनशील वस्तुओं के एक जटिल बिल्ड-एंड-फ्रीज सिस्टम की प्रोग्रामिंग से बहुत सरल है जो अपरिवर्तनीय हो जाता है।
बेंजामिन हॉजसन

काश मैं इसे कुछ और + 1 का ... दे पाता या "बाहरी" भंडार या सूचकांक (या जो कुछ भी ) के बिना अपरिवर्तनीय होता , इन सभी कमरों को अच्छी तरह से ठीक करने के लिए बहुत अनावश्यक रूप से जटिल होता। और यह उन रिश्तों को प्रदर्शित करने Roomसे मना नहीं करता है ; लेकिन, वे ऐसे संकेतक होने चाहिए जो केवल सूचकांक से पढ़ते हैं।
svidgen

12

एक कार्यात्मक शैली में ऐसा करने का तरीका यह पहचानना है कि आप वास्तव में क्या निर्माण कर रहे हैं: लेबल किनारों के साथ एक निर्देशित ग्राफ।

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

एक तहखाने एक डेटा संरचना है जो कमरे और चीजों के एक गुच्छा का ट्रैक रखता है, और उनके बीच के रिश्ते क्या हैं। प्रत्येक "कॉल" के साथ एक नया, अलग अपरिवर्तनीय कालकोठरी देता है। कमरे नहीं जानते कि उनमें से उत्तर और दक्षिण क्या है; पुस्तक को नहीं पता है कि यह छाती में है। तहखाने उन तथ्यों जानता है, और है कि क्योंकि वहाँ कोई भी कर रहे हैं बात वृत्तीय संदर्भ के साथ कोई समस्या नहीं है।


1
मैंने निर्देशित रेखांकन और धाराप्रवाह बिल्डरों (और डीएसएल) का अध्ययन किया है। मैं देख सकता हूं कि यह कैसे एक निर्देशित ग्राफ का निर्माण कर सकता है लेकिन यह पहला है जो मैंने दो विचारों को जुड़ा हुआ देखा है। क्या कोई पुस्तक या ब्लॉग पोस्ट है जिसे मैंने याद किया है? या क्या यह एक निर्देशित ग्राफ का उत्पादन सिर्फ इसलिए करता है क्योंकि इससे प्रश्नों की समस्या हल हो जाती है?
कैंडिड_ओरेंज

@CandiedOrange: यह एक स्केच है जो एपीआई जैसा दिख सकता है। वास्तव में अंतर्निहित अपरिवर्तित ग्राफ डेटा संरचना का निर्माण अंतर्निहित यह कुछ काम ले जाएगा, लेकिन यह मुश्किल नहीं है। एक अपरिवर्तनीय निर्देशित ग्राफ, नोड्स का एक अपरिवर्तनीय सेट और (स्टार्ट, एंड, लेबल) ट्रायटल्स का एक अपरिवर्तनीय सेट है, इसलिए इसे पहले से ही हल की गई समस्याओं की संरचना में कम किया जा सकता है।
एरिक लिपपार्ट

जैसा मैंने कहा, मैंने डीएसएल और निर्देशित ग्राफ़ दोनों का अध्ययन किया है। यदि आप पढ़ते हैं या कुछ ऐसा लिख ​​रहे हैं, जो दोनों को एक साथ रखता है या यदि आप उन्हें इस विशेष प्रश्न का उत्तर देने के लिए लाए हैं, तो यह जानने की कोशिश कर रहा हूं। अगर आपको कुछ ऐसा पता है जो उन्हें एक साथ रखता है तो मुझे अच्छा लगेगा अगर आप मुझे इसके बारे में बता सकें।
candied_orange

@ कंडिडऑरेन्ज: विशेष रूप से नहीं। मैंने एक बैकग्राउंडिंग सुडोकू सॉल्वर बनाने के लिए एक अपरिवर्तित अप्रत्यक्ष ग्राफ पर कई साल पहले एक ब्लॉग श्रृंखला लिखी थी। और मैंने हाल ही में एक ब्लॉग श्रृंखला लिखी है जो विजार्ड्स-एंड-डंगेन्स डोमेन में परस्पर डेटा संरचनाओं के लिए ऑब्जेक्ट ओरिएंटेड डिज़ाइन समस्याओं के बारे में है।
एरिक लिपर्ट

3

चिकन और एक अंडा सही है। इसका कोई मतलब नहीं है:

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 के पास अपरिवर्तनीय स्थिति है, लेकिन वे ऐसी चीज़ का उल्लेख करते हैं जो अपरिवर्तनीय नहीं है। जो उन्हें अपरिवर्तनीय बनाने की बात को आसानी से हरा सकता था। मुझे उम्मीद है कि सी कम से कम धागे के रूप में आप की जरूरत के रूप में सुरक्षित है।

फ्रीज-पिघल नामक एक पैटर्न है:

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

अब 'क' अपरिवर्तनीय है। 'ए' नहीं है, लेकिन 'ए' है। क्यों ठीक है? जब तक 'ए' के ​​बारे में और कुछ नहीं पता, इससे पहले कि वह जमे हुए है, कौन परवाह करता है?

वहाँ एक पिघलना () विधि है, लेकिन यह कभी नहीं बदल 'एक'। यह 'a' की एक परस्पर प्रतिलिपि बनाता है जिसे अपडेट किया जा सकता है और फिर जमे हुए भी किया जा सकता है।

इस दृष्टिकोण का नकारात्मक पक्ष यह है कि वर्ग अपरिवर्तनीयता को लागू नहीं कर रहा है। निम्नलिखित प्रक्रिया है आप यह नहीं बता सकते हैं कि यह प्रकार से अपरिवर्तनीय है।

मैं वास्तव में c # में इस समस्या को हल करने का एक आदर्श तरीका नहीं जानता। मैं समस्याओं को छिपाने के तरीके जानता हूं। कभी-कभी इतना ही काफी होता है।

जब यह पूरी तरह से इस समस्या से बचने के लिए एक अलग दृष्टिकोण का उपयोग नहीं करता है। उदाहरण के लिए: यहां देखें कि राज्य पैटर्न को कैसे लागू किया जाता है । आपको लगता है कि वे एक परिपत्र संदर्भ के रूप में ऐसा करेंगे, लेकिन वे नहीं करते हैं। हर बार जब राज्य बदलते हैं तो वे नई वस्तुओं को क्रैंक करते हैं। कभी-कभी कचरे के कलेक्टर का दुरुपयोग करना आसान होता है, फिर यह पता लगाना कि मुर्गियों से अंडे कैसे निकलते हैं।


मुझे एक नए पैटर्न से परिचित कराने के लिए +1। पहले मैंने कभी फ्रीज-पिघल के बारे में सुना है।
रॉक एंथनी जॉनसन

a.freeze()ImmutableAप्रकार लौट सकता है । जो इसे मूल रूप से बिल्डर पैटर्न बनाते हैं।
ब्रायन चेन

@BryanChen यदि आप ऐसा करते हैं, तो bपुराने उत्परिवर्ती के संदर्भ को छोड़ दिया जाता है a। विचार यह है कि है a और bइससे पहले कि आप उन्हें प्रणाली के आराम करने के जारी एक दूसरे के अपरिवर्तनीय संस्करण के लिए ले जाना चाहिए।
कैंडिड_ऑरेंज

@RockAnthonyJohnson यह भी क्या एरिक Lippert कहा जाता है Popsicle अचल स्थिति
स्पॉट किया गया

1

कुछ स्मार्ट लोगों ने इस पर अपनी राय पहले ही दे दी थी, लेकिन मुझे लगता है कि यह जानने की जिम्मेदारी नहीं है कि इसके पड़ोसी क्या हैं।

मुझे लगता है कि यह जानना जिम्मेदारी है कि कमरे कहां हैं। अगर कमरे को वास्तव में अपने पड़ोसियों को यह जानने के लिए INeigbourFinder पास करना होगा।

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.