c# - उत्परिवर्तनीय structs "बुराई" क्यों हैं?




immutability mutable (12)

उत्परिवर्तनीय structs बुरा नहीं हैं।

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

मैं इन पूरी तरह से वैध उपयोग-मामलों "बुराई" में एक अपरिवर्तनीय संरचना के उपयोग को नहीं बुलाऊंगा।

मैं इस बिंदु को देख सकता हूं कि सी # का वाक्यविन्यास किसी मान प्रकार या संदर्भ प्रकार के किसी सदस्य की पहुंच को अलग करने में मदद नहीं करता है, इसलिए मैं अपरिवर्तनीय structs को पसंद करने के लिए सभी हूं, जो परिवर्तनीय structs पर अपरिवर्तनीयता को लागू करता है।

हालांकि, अपरिवर्तनीय structs को "बुराई" के रूप में लेबल करने की बजाय, मैं भाषा को गले लगाने और अंगूठे के अधिक सहायक और रचनात्मक शासन की वकालत करने की सलाह दूंगा।

उदाहरण के लिए: "structs वैल्यू प्रकार हैं, जिन्हें डिफ़ॉल्ट रूप से कॉपी किया जाता है। यदि आप उन्हें कॉपी नहीं करना चाहते हैं तो आपको एक संदर्भ की आवश्यकता है" या "पहले पढ़ने वाले structs के साथ काम करने का प्रयास करें"

एसओ पर चर्चाओं के बाद मैंने पहले से ही कई बार टिप्पणी की है कि उत्परिवर्तनीय structs "बुराई" हैं (जैसे इस question के उत्तर में)।

सी # में उत्परिवर्तन और structs के साथ वास्तविक समस्या क्या है?


एक और जोड़े कोने के मामले हैं जो प्रोग्रामर दृष्टिकोण से अप्रत्याशित व्यवहार का कारण बन सकते हैं। उनमें से कुछ यहाँ।

  1. अपरिवर्तनीय मूल्य प्रकार और केवल पढ़ने वाले फ़ील्ड

// Simple mutable structure. 
// Method IncrementI mutates current state.
struct Mutable
{
    public Mutable(int i) : this() 
    {
        I = i;
    }

    public void IncrementI() { I++; }

    public int I {get; private set;}
}

// Simple class that contains Mutable structure
// as readonly field
class SomeClass 
{
    public readonly Mutable mutable = new Mutable(5);
}

// Simple class that contains Mutable structure
// as ordinary (non-readonly) field
class AnotherClass 
{
    public Mutable mutable = new Mutable(5);
}

class Program
{
    void Main()
    {
        // Case 1. Mutable readonly field
        var someClass = new SomeClass();
        someClass.mutable.IncrementI();
        // still 5, not 6, because SomeClass.mutable field is readonly
        // and compiler creates temporary copy every time when you trying to
        // access this field
        Console.WriteLine(someClass.mutable.I);

        // Case 2. Mutable ordinary field
        var anotherClass = new AnotherClass();
        anotherClass.mutable.IncrementI();

        //Prints 6, because AnotherClass.mutable field is not readonly
        Console.WriteLine(anotherClass.mutable.I);
    }
}

  1. परिवर्तनीय मूल्य प्रकार और सरणी

मान लें कि हमारे म्यूटेबल स्ट्रक्चर की एक सरणी है और हम उस सरणी के पहले तत्व के लिए incrementI विधि को कॉल कर रहे हैं। आप इस कॉल से क्या व्यवहार कर रहे हैं? क्या इसे सरणी के मूल्य या केवल एक प्रति को बदलना चाहिए?

Mutable[] arrayOfMutables = new Mutable[1];
arrayOfMutables[0] = new Mutable(5);

// Now we actually accessing reference to the first element
// without making any additional copy
arrayOfMutables[0].IncrementI();

//Prints 6!!
Console.WriteLine(arrayOfMutables[0].I);

// Every array implements IList<T> interface
IList<Mutable> listOfMutables = arrayOfMutables;

// But accessing values through this interface lead
// to different behavior: IList indexer returns a copy
// instead of an managed reference
listOfMutables[0].IncrementI(); // Should change I to 7

// Nope! we still have 6, because previous line of code
// mutate a copy instead of a list value
Console.WriteLine(listOfMutables[0].I);

इसलिए, उत्परिवर्तनीय structs बुराई नहीं हैं जब तक आप और बाकी टीम स्पष्ट रूप से समझें कि आप क्या कर रहे हैं। लेकिन बहुत से कोने के मामले हैं जब प्रोग्राम व्यवहार अपेक्षित से अलग होगा, जिससे त्रुटियों को समझने और कठोर समझने में कठिनाई हो सकती है।


कहां से शुरू करें; -पी

एरिक लिपर्ट का ब्लॉग हमेशा उद्धरण के लिए अच्छा है:

यह एक और कारण है कि उत्परिवर्तनीय मूल्य प्रकार बुरा क्यों हैं। हमेशा मूल्य प्रकार अपरिवर्तनीय बनाने की कोशिश करें।

सबसे पहले, आप आसानी से परिवर्तन खो देते हैं ... उदाहरण के लिए, चीजों को सूची से बाहर करना:

Foo foo = list[0];
foo.Name = "abc";

वह परिवर्तन क्या हुआ? कुछ भी उपयोगी नहीं है ...

गुणों के साथ ही:

myObj.SomeProperty.Size = 22; // the compiler spots this one

आपको करने के लिए मजबूर करना:

Bar bar = myObj.SomeProperty;
bar.Size = 22;
myObj.SomeProperty = bar;

कम गंभीर रूप से, एक आकार मुद्दा है; उत्परिवर्तनीय वस्तुओं में कई गुण होते हैं; फिर भी यदि आपके पास दो int , string , DateTime और bool साथ एक स्ट्रक्चर है, तो आप बहुत सारी मेमोरी के माध्यम से बहुत जल्दी जल सकते हैं। एक वर्ग के साथ, एकाधिक कॉलर एक ही उदाहरण के संदर्भ साझा कर सकते हैं (संदर्भ छोटे हैं)।


मूल्य प्रकार मूल रूप से अपरिवर्तनीय अवधारणाओं का प्रतिनिधित्व करते हैं। एफएक्स, यह एक पूर्णांक, वेक्टर इत्यादि जैसे गणितीय मान होने का कोई मतलब नहीं है और फिर इसे संशोधित करने में सक्षम हो। यह एक मूल्य के अर्थ को फिर से परिभाषित करने जैसा होगा। एक मूल्य प्रकार बदलने के बजाय, यह एक और अद्वितीय मूल्य असाइन करने के लिए और अधिक समझ में आता है। इस तथ्य के बारे में सोचें कि मूल्य प्रकारों की तुलना इसकी गुणों के सभी मूल्यों की तुलना करके की जाती है। मुद्दा यह है कि यदि गुण समान हैं तो यह उस मूल्य का वही सार्वभौमिक प्रतिनिधित्व है।

जैसा कि कोनराड का उल्लेख है कि यह किसी तारीख को बदलने की समझ में नहीं आता है, क्योंकि मान समय में अद्वितीय बिंदु का प्रतिनिधित्व करता है और समय वस्तु का उदाहरण नहीं है जिसमें कोई राज्य या संदर्भ-निर्भरता हो।

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


यदि आप किस स्ट्रक्चर के लिए चिपकते हैं (सी # में, विजुअल बेसिक 6, पास्कल / डेल्फी, सी ++ स्ट्रक्चर टाइप (या कक्षाएं) जब वे पॉइंटर्स के रूप में उपयोग नहीं किए जाते हैं), तो आप पाएंगे कि संरचना एक यौगिक चर से अधिक नहीं है । इसका अर्थ यह है कि: आप उन्हें एक सामान्य नाम के तहत चर के पैक किए गए सेट के रूप में देखेंगे (एक रिकार्ड वेरिएबल जो आप संदर्भ सदस्यों से)।

मुझे पता है कि बहुत से लोगों को ओओपी के लिए गहराई से इस्तेमाल किया जाएगा, लेकिन यह कहने का पर्याप्त कारण नहीं है कि ऐसी चीजें स्वाभाविक रूप से बुराई हैं, अगर सही तरीके से उपयोग की जाती हैं। कुछ ढांचे अतुलनीय हैं क्योंकि वे इरादे रखते हैं (यह पायथन के नामित मामले का मामला है), लेकिन यह विचार करने का एक और प्रतिमान है।

हां: structs में बहुत सारी मेमोरी शामिल है, लेकिन यह करने से यह अधिक स्मृति नहीं होगी:

point.x = point.x + 1

की तुलना में:

point = Point(point.x + 1, point.y)

मेमोरी खपत कम से कम वही होगी, या अतुलनीय मामले में और भी अधिक होगी (हालांकि यह मामला अस्थायी होगा, वर्तमान स्टैक के लिए, भाषा के आधार पर)।

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

एक अच्छा उदाहरण चाहते हैं? आसानी से फ़ाइलों को पढ़ने के लिए स्ट्रक्चर का उपयोग किया जाता है। पायथन में यह लाइब्रेरी है क्योंकि, क्योंकि यह ऑब्जेक्ट उन्मुख है और इसके लिए structs के लिए कोई समर्थन नहीं है, इसे इसे किसी अन्य तरीके से कार्यान्वित करना था, जो कुछ हद तक बदसूरत है। Structs लागू करने वाली भाषाओं में उस सुविधा है ... अंतर्निहित। Try reading a bitmap header with an appropriate struct in languages like Pascal or C. It will be easy (if the struct is properly built and aligned; in Pascal you would not use a record-based access but functions to read arbitrary binary data). So, for files and direct (local) memory access, structs are better than objects. As for today, we're used to JSON and XML, and so we forget the use of binary files (and as a side effect, the use of structs). But yes: they exist, and have a purpose.

They are not evil. Just use them for the right purpose.

If you think in terms of hammers, you will want to treat screws as nails, to find screws are harder to plunge in the wall, and it will be screws' fault, and they will be the evil ones.


यदि आपने कभी सी / सी ++ जैसी भाषा में प्रोग्राम किया है, तो structs mutable के रूप में उपयोग करने के लिए ठीक हैं। बस उन्हें रेफरी के साथ पास करें, आस-पास कुछ भी नहीं है जो गलत हो सकता है। मुझे लगता है कि एकमात्र समस्या सी # कंपाइलर के प्रतिबंध हैं और, कुछ मामलों में, मैं एक प्रतिलिपि के बजाय संरचना के संदर्भ का उपयोग करने के लिए बेवकूफ चीज को मजबूर करने में असमर्थ हूं (जैसे जब कोई संरचना सी # कक्षा का हिस्सा होती है )।

तो, परिवर्तनीय structs बुरा नहीं हैं, सी # उन्हें बुरा बना दिया है। मैं हर समय सी ++ में परिवर्तनीय structs का उपयोग करता हूं और वे बहुत सुविधाजनक और सहज हैं। इसके विपरीत, सी # ने मुझे ऑब्जेक्ट्स को संभालने के तरीके के कारण कक्षाओं के सदस्यों के रूप में पूरी तरह से structs को छोड़ दिया है। उनकी सुविधा ने हमें हमारी कीमत चुकानी पड़ी है।


सार्वजनिक उत्परिवर्तनीय क्षेत्रों या संपत्तियों के साथ संघर्ष बुरा नहीं हैं।

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

केवल-पढ़ने वाले संदर्भों में आत्म-उत्परिवर्तन विधियों के संचालन के साथ समस्याओं के बावजूद, म्यूटेबल structs अक्सर उत्परिवर्ती वर्ग प्रकारों से कहीं अधिक semantics प्रदान करते हैं। निम्नलिखित तीन विधि हस्ताक्षरों पर विचार करें:

struct PointyStruct {public int x,y,z;};
class PointyClass {public int x,y,z;};

void Method1(PointyStruct foo);
void Method2(ref PointyStruct foo);
void Method3(PointyClass foo);

प्रत्येक विधि के लिए, निम्नलिखित प्रश्नों का उत्तर दें:

  1. विधि मानते हुए किसी भी "असुरक्षित" कोड का उपयोग नहीं करता है, क्या यह foo को संशोधित कर सकता है?
  2. यदि विधि कहने से पहले 'foo' के बाहर कोई बाहरी संदर्भ मौजूद नहीं है, तो क्या बाहरी संदर्भ मौजूद हो सकता है?

उत्तर:

प्रश्न 1:
Method1() : नहीं (स्पष्ट मंशा)
Method2() : हाँ (स्पष्ट मंशा)
Method3() : हां (अनिश्चित इरादा)
प्रश्न 2:
Method1() : नहीं
Method2() : नहीं (जब तक असुरक्षित न हो)
Method3() : हाँ

विधि 1 foo को संशोधित नहीं कर सकता है, और कभी भी संदर्भ नहीं मिलता है। विधि 2 को foo के लिए एक अल्पकालिक संदर्भ प्राप्त होता है, जिसे किसी भी क्रम में, किसी भी क्रम में foo के फ़ील्ड को संशोधित करने तक इसका उपयोग कर सकते हैं, लेकिन यह उस संदर्भ को जारी नहीं रख सकता है। विधि 2 लौटने से पहले, जब तक यह असुरक्षित कोड का उपयोग नहीं करता है, तो किसी भी और सभी प्रतियां जो 'foo' संदर्भ से हो सकती हैं, गायब हो जाएंगी। विधि 3, विधि 2 के विपरीत, foo के लिए एक संवेदनात्मक रूप से संदर्भित संदर्भ प्राप्त करता है, और इसमें कोई जानकारी नहीं है कि यह इसके साथ क्या कर सकता है। यह foo को बिल्कुल भी नहीं बदल सकता है, यह foo को बदल सकता है और फिर वापस आ सकता है, या यह किसी अन्य थ्रेड को foo का संदर्भ दे सकता है जो इसे कुछ मनमाने ढंग से भविष्य में कुछ मनमाने तरीके से बदल सकता है। एक व्यवहार्य वर्ग ऑब्जेक्ट में विधि 3 क्या कर सकता है, इसे सीमित करने का एकमात्र तरीका यह है कि म्यूटेबल ऑब्जेक्ट को केवल पढ़ने-योग्य रैपर में encapsulate करना होगा, जो बदसूरत और बोझिल है।

संरचनाओं की Arrays अद्भुत semantics प्रदान करते हैं। आयत के प्रकार [500] को आयत दिया गया है, यह स्पष्ट और स्पष्ट है कि उदाहरण के लिए तत्व 123 को तत्व 456 में कॉपी करें और फिर कुछ समय बाद तत्व को 123 से 555 तक चौड़ाई निर्धारित करें, तत्व 456 को परेशान किए बिना। "RectArray [432] = RectArray [321 ]; ...; RectArray [123] .Width = 555; "। यह जानकर कि आयताकार एक पूर्णांक क्षेत्र वाला एक स्ट्रक्चर है जिसे चौड़ाई कहा जाता है, जिसे उपर्युक्त बयान के बारे में सभी को जानने की आवश्यकता होगी।

अब मान लें कि RectClass आयत के समान क्षेत्रों के साथ एक वर्ग था और एक RectClassArray [500] प्रकार RectClass पर एक ही ऑपरेशन करना चाहता था। शायद सरणी को उत्परिवर्तनीय RectClass वस्तुओं के 500 पूर्व-प्रारंभिक अपरिवर्तनीय संदर्भ रखने के लिए माना जाता है। उस स्थिति में, उचित कोड कुछ ऐसा होगा जैसे "RectClassArray [321] .SetBounds (RectClassArray [456]); ...; RectClassArray [321] .X = 555;"। शायद सरणी को उन उदाहरणों को पकड़ने के लिए माना जाता है जो बदलने जा रहे हैं, इसलिए उचित कोड "RectClassArray [321] = RectClassArray [456]; ...; RectClassArray [321] = नया RectClass (RectClassArray [321) ]); RectClassArray [321] .X = 555; " यह जानने के लिए कि किसी को क्या करना है, किसी को RectClass के बारे में बहुत कुछ पता होना चाहिए (उदाहरण के लिए यह एक प्रतिलिपि बनाने वाला, एक प्रतिलिपि विधि, आदि) और सरणी के इच्छित उपयोग का समर्थन करता है। एक संरचना का उपयोग करने के रूप में कहीं भी साफ के करीब।

यह सुनिश्चित करने के लिए, दुर्भाग्यवश किसी सरणी के अलावा किसी कंटेनर क्लास के लिए कोई स्ट्रक्चर सरणी के स्वच्छ अर्थशास्त्र की पेशकश करने का कोई अच्छा तरीका नहीं है। सबसे अच्छा कोई भी कर सकता है, अगर कोई संग्रह को स्ट्रिंग के साथ अनुक्रमित करना चाहता था, तो संभवतः एक सामान्य "ActOnItem" विधि प्रदान करना होगा जो सूचकांक, एक सामान्य पैरामीटर और एक प्रतिनिधि जो एक पारित होगा दोनों सामान्य पैरामीटर और संग्रह आइटम संदर्भ द्वारा। इससे लगभग उसी अर्थशास्त्र को स्ट्रक्चर सरणी के रूप में अनुमति मिल जाएगी, लेकिन जब तक vb.net और C # लोगों को एक अच्छा वाक्यविन्यास प्रदान करने के लिए पीछा नहीं किया जा सकता है, तो कोड उचित रूप से प्रदर्शन होने पर भी चिपचिपा दिखने वाला होगा (सामान्य पैरामीटर को पास करना होगा एक स्थिर प्रतिनिधि के उपयोग की अनुमति दें और किसी भी अस्थायी कक्षा के उदाहरण बनाने की आवश्यकता से बचें)।

व्यक्तिगत रूप से, मैं घृणा एरिक लिपर्ट एट अल पर peeved हूँ। परिवर्तनीय मूल्य प्रकारों के बारे में स्पू। वे उन जगहों पर उपयोग किए जाने वाले विशिष्ट संदर्भ प्रकारों की तुलना में अधिक क्लीनर अर्थशास्त्र प्रदान करते हैं। मूल्य प्रकारों के लिए .NET के समर्थन के साथ कुछ सीमाओं के बावजूद, ऐसे कई मामले हैं जहां परस्पर मूल्य प्रकार किसी अन्य प्रकार की इकाई से बेहतर फिट होते हैं।


According to the C# Cookbook (Jay Hilyard, et. al), we get a warning about using structs as they often lead to the IL generating boxing and un-boxing commands. This might have been fixed in later versions of C# IL generation but the stigma may persist. As it stands, there isn't a lot of need for structs in c# when you have classes. And for those who might argue with that I would say ... let the compiler figure it out.

The following can help prevent or eliminate boxing:

  1. Use classes instead of structures...This change can dramatically improve performance.

It doesn't have anything to do with structs (and not with C#, either) but in Java you might get problems with mutable objects when they are eg keys in a hash map. If you change them after adding them to a map and it changes its hash code , evil things might happen.


Personally when I look at code the following looks pretty clunky to me:

data.value.set ( data.value.get () + 1 ) ;

rather than simply

data.value++ ; or data.value = data.value + 1 ;

Data encapsulation is useful when passing a class around and you want to ensure the value is modified in a controlled fashion. However when you have public set and get functions that do little more than set the value to what ever is passed in, how is this an improvement over simply passing a public data structure around?

When I create a private structure inside a class, I created that structure to organize a set of variables into one group. I want to be able to modify that structure within the class scope, not get copies of that structure and create new instances.

To me this prevents a valid use of structures being used to organize public variables, if I wanted access control I'd use a class.


There are several issues with Mr. Eric Lippert's example. It is contrived to illustrate the point that structs are copied and how that could be a problem if you are not careful. Looking at the example I see it as a result of a bad programming habit and not really a problem with either struct or the class.

  1. A struct is supposed to have only public members and should not require any encapsulation. If it does then it really should be a type/class. You really do not need two constructs to say the same thing.

  2. If you have class enclosing a struct, you would call a method in the class to mutate the member struct. This is what I would do as a good programming habit.

A proper implementation would be as follows.

struct Mutable {
public int x;
}

class Test {
    private Mutable m = new Mutable();
    public int mutate()
    { 
        m.x = m.x + 1;
        return m.x;
    }
  }
  static void Main(string[] args) {
        Test t = new Test();
        System.Console.WriteLine(t.mutate());
        System.Console.WriteLine(t.mutate());
        System.Console.WriteLine(t.mutate());
    }

It looks like it is an issue with programming habit as opposed to an issue with struct itself. Structs are supposed to be mutable, that is the idea and intent.

The result of the changes voila behaves as expected:

1 2 3 Press any key to continue . । ।


When something can be mutated, it gains a sense of identity.

struct Person {
    public string name; // mutable
    public Point position = new Point(0, 0); // mutable

    public Person(string name, Point position) { ... }
}

Person eric = new Person("Eric Lippert", new Point(4, 2));

Because Person is mutable, it's more natural to think about changing Eric's position than cloning Eric, moving the clone, and destroying the original . Both operations would succeed in changing the contents of eric.position , but one is more intuitive than the other. Likewise, it's more intuitive to pass Eric around (as a reference) for methods to modify him. Giving a method a clone of Eric is almost always going to be surprising. Anyone wanting to mutate Person must remember to ask for a reference to Person or they'll be doing the wrong thing.

If you make the type immutable, the problem goes away; if I can't modify eric , it makes no difference to me whether I receive eric or a clone of eric . More generally, a type is safe to pass by value if all of its observable state is held in members that are either:

  • अडिग
  • reference types
  • safe to pass by value

If those conditions are met then a mutable value type behaves like a reference type because a shallow copy will still allow the receiver to modify the original data.

The intuitiveness of an immutable Person depends on what you're trying to do though. If Person just represents a set of data about a person, there's nothing unintuitive about it; Person variables truly represent abstract values , not objects. (In that case, it'd probably be more appropriate to rename it to PersonData .) If Person is actually modeling a person itself, the idea of constantly creating and moving clones is silly even if you've avoided the pitfall of thinking you're modifying the original. In that case it'd probably be more natural to simply make Person a reference type (that is, a class.)

Granted, as functional programming has taught us there are benefits to making everything immutable (no one can secretly hold on to a reference to eric and mutate him), but since that's not idiomatic in OOP it's still going to be unintuitive to anyone else working with your code.







mutable