Waarom zijn veranderlijke structs “slecht”?

Na de discussies hier op SO heb ik al verschillende keren de opmerking gelezen dat veranderlijke structs “slecht” zijn (zoals in het antwoord op deze vraag).

Wat is het eigenlijke probleem met veranderlijkheid en structs in C#?


Antwoord 1, autoriteit 100%

Structs zijn waardetypes, wat betekent dat ze worden gekopieerd wanneer ze worden doorgegeven.

Dus als je een kopie wijzigt, verander je alleen die kopie, niet het origineel en niet alle andere kopieën die in de buurt zouden kunnen zijn.

Als je struct onveranderlijk is, zijn alle automatische kopieën die resulteren uit het doorgeven van een waarde hetzelfde.

Als je het wilt veranderen, moet je dat bewust doen door een nieuwe instantie van de struct te maken met de gewijzigde gegevens. (geen kopie)


Antwoord 2, autoriteit 57%

Waar te beginnen ;-p

Eric Lipperts blogis altijd goed voor een offerte:

Dit is nog een reden waarom veranderlijk
waardetypes zijn slecht. Probeer altijd
maak waardetypes onveranderlijk.

Ten eerste raak je snel wijzigingen kwijt… bijvoorbeeld dingen uit een lijst halen:

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

wat is er veranderd? Niets nuttigs…

Hetzelfde met eigenschappen:

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

je dwingen te doen:

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

minder kritisch, er is een probleem met de grootte; veranderlijke objecten hebben meestalmeerdere eigenschappen; maar als je een struct hebt met twee ints, een string, een DateTimeen een bool, kun je heel snel veel geheugen doorbranden. Met een klas kunnen meerdere bellers een verwijzing naar dezelfde instantie delen (verwijzingen zijn klein).


Antwoord 3, autoriteit 25%

Ik zou niet zeggen slecht, maar veranderlijkheid is vaak een teken van overijver van de kant van de programmeur om een ​​maximum aan functionaliteit te bieden. In werkelijkheid is dit vaak niet nodig en dat maakt de interface weer kleiner, gebruiksvriendelijker en moeilijker verkeerd te gebruiken (= robuuster).

Een voorbeeld hiervan zijn lees/schrijf- en schrijf/schrijfconflicten in race-omstandigheden. Deze kunnen gewoon niet voorkomen in onveranderlijke structuren, omdat schrijven geen geldige bewerking is.

Ik beweer ook dat veranderlijkheid eigenlijk bijna nooit is nodig, denkt de programmeur gewoondat het mogelijkin de toekomst zal zijn. Het heeft bijvoorbeeld gewoon geen zin om een ​​datum te wijzigen. Maak liever een nieuwe datum op basis van de oude. Dit is een goedkope operatie, dus prestatie is geen overweging.


Antwoord 4, autoriteit 18%

Veranderlijke structuren zijn niet slecht.

Ze zijn absoluut noodzakelijk in omstandigheden met hoge prestaties. Bijvoorbeeld wanneer cacheregels en/of garbagecollection een bottleneck worden.

Ik zou het gebruik van een onveranderlijke struct in deze perfect geldige use-cases niet “kwaad” noemen.

Ik begrijp het punt dat de syntaxis van C# niet helpt om de toegang van een lid van een waardetype of van een referentietype te onderscheiden, dus ik ben er helemaal voor de voorkeur te geven aanonveranderlijke structs, die onveranderlijkheid afdwingen , over veranderlijke structuren.

In plaats van onveranderlijke structuren simpelweg als ‘slecht’ te bestempelen, zou ik adviseren om de taal te omarmen en te pleiten voor meer behulpzame en constructieve vuistregels.

Bijvoorbeeld: “structs zijn waardetypes, die standaard worden gekopieerd. je hebt een referentie nodig als je ze niet wilt kopiëren”of
“probeer eerst met alleen-lezen structs te werken”.


Antwoord 5, autoriteit 15%

Structs met openbare veranderlijke velden of eigenschappen zijn niet slecht.

Structmethoden (in tegenstelling tot eigenschapszetters) die “this” muteren, zijn enigszins slecht, alleen omdat .net geen manier biedt om ze te onderscheiden van methoden die dat niet doen. Struct-methoden die “dit” niet muteren, zouden zelfs op alleen-lezen structs moeten kunnen worden aangeroepen zonder dat defensief kopiëren nodig is. Methoden die “dit” muteren, zouden helemaal niet kunnen worden aangeroepen op alleen-lezen structs. Aangezien .net niet wil verbieden dat structmethoden die “dit” niet wijzigen, worden aangeroepen op alleen-lezen structs, maar niet wil toestaan ​​dat alleen-lezen structs worden gemuteerd, kopieert het defensief structs in lees- alleen contexten, misschien wel het slechtste van twee werelden.

Ondanks de problemen met de verwerking van zelf-muterende methoden in alleen-lezen contexten, bieden veranderlijke structs vaak semantiek die veel beter is dan die van veranderlijke klassen. Overweeg de volgende drie methodehandtekeningen:

struct PointyStruct {public int x,y,z;};
klasse PointyClass {public int x,y,z;};
void Method1(PointyStruct foo);
void Method2 (ref PointyStruct foo);
void Method3(PointyClass foo);

Beantwoord voor elke methode de volgende vragen:

  1. Ervan uitgaande dat de methode geen “onveilige” code gebruikt, zou het foo kunnen wijzigen?
  2. Als er geen externe verwijzingen naar ‘foo’ bestaan ​​voordat de methode wordt aangeroepen, kan er dan een externe verwijzing bestaan ​​daarna?

Antwoorden:

Vraag 1:
Method1(): nee (duidelijke bedoeling)
Method2(): ja (duidelijke bedoeling)
Method3(): ja (onzekere bedoeling)
Vraag 2:
Method1(): nee
Method2(): nee (tenzij onveilig)
Method3(): ja

Methode1 kan foo niet wijzigen en krijgt nooit een referentie. Methode2 krijgt een kortstondige verwijzing naar foo, waarmee het de velden van foo een willekeurig aantal keren kan wijzigen, in willekeurige volgorde, totdat het terugkeert, maar het kan die verwijzing niet vasthouden. Voordat Method2 terugkeert, zullen alle kopieën die mogelijk gemaakt zijn van de ‘foo’-referentie, zijn verdwenen, tenzij het onveilige code gebruikt. Methode3 krijgt, in tegenstelling tot Method2, een promiscue-deelbare verwijzing naar foo, en het is niet te zeggen wat het ermee zou kunnen doen. Het kan foo helemaal niet veranderen, het kan foo veranderen en dan terugkeren, of het kan een verwijzing naar foo geven naar een andere thread die het op een willekeurige toekomstige tijd op een willekeurige manier zou kunnen muteren. De enige manier om te beperken wat Method3 zou kunnen doen met een veranderlijk klasseobject dat erin wordt doorgegeven, is door het veranderlijke object in een alleen-lezen wrapper in te kapselen, wat lelijk en omslachtig is.

Arrays van structuren bieden prachtige semantiek. Gezien RectArray[500] van het type Rectangle, is het duidelijk hoe je b.v. kopieer element 123 naar element 456 en stel enige tijd later de breedte van element 123 in op 555, zonder element 456 te storen. “RectArray[432] = RectArray[321]; …; RectArray[123].Width = 555;” . Wetende dat Rectangle een struct is met een geheel getal dat Breedte wordt genoemd, zal je alles vertellen wat je moet weten over de bovenstaande uitspraken.

Veronderstel nu dat RectClass een klasse was met dezelfde velden als Rectangle en dat men dezelfde bewerkingen wilde uitvoeren op een RectClassArray[500] van het type RectClass. Misschien moet de array 500 vooraf geïnitialiseerde onveranderlijke verwijzingen naar veranderlijke RectClass-objecten bevatten. in dat geval zou de juiste code zoiets zijn als “RectClassArray[321].SetBounds(RectClassArray[456]); …; RectClassArray[321].X = 555;”. Misschien wordt aangenomen dat de array instanties bevat die niet zullen veranderen, dus de juiste code zou meer zijn als “RectClassArray[321] = RectClassArray[456]; …; RectClassArray[321] = New RectClass(RectClassArray[321 ]); RectClassArray[321].X = 555;” Om te weten wat men zou moeten doen, zou men veel meer moeten weten over zowel RectClass (bijvoorbeeld ondersteunt het een kopieerconstructor, een kopieer-van-methode, enz.) en het beoogde gebruik van de array. Nergens zo schoon als het gebruik van een struct.

Er is helaas geen goede manier voor een andere containerklasse dan een array om de schone semantiek van een struct-array te bieden. Het beste wat men kon doen, zou men willen dat een collectie geïndexeerd zou worden met b.v. een string, zou waarschijnlijk zijn om een ​​generieke “ActOnItem”-methode aan te bieden die een string voor de index, een generieke parameter en een gemachtigde zou accepteren die zowel de generieke parameter als het verzamelingsitem zou doorgeven. Dat zou bijna dezelfde semantiek mogelijk maken als struct-arrays, maar tenzij de vb.net- en C#-mensen kunnen worden overgehaald om een ​​mooie syntaxis aan te bieden, zal de code er onhandig uitzien, zelfs als het redelijk presteert (het doorgeven van een generieke parameter zou staat het gebruik van een statische gemachtigde toe en vermijdt de noodzaak om tijdelijke klasseninstanties aan te maken).

Persoonlijk erger ik me aan de haat Eric Lippert et al. spugen met betrekking tot veranderlijke waardetypes. Ze bieden veel schonere semantiek dan de promiscue referentietypes die overal worden gebruikt. Ondanks enkele beperkingen met de ondersteuning van .net voor waardetypes, zijn er veel gevallen waarin veranderlijke waardetypes beter passen dan enig ander soort entiteit.


Antwoord 6, autoriteit 8%

Er zijn een paar andere hoekgevallen die kunnen leiden tot onvoorspelbaar gedrag vanuit het oogpunt van de programmeur.

Onveranderlijke waardetypes en alleen-lezen velden

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

Veranderlijke waardetypes en array

Stel dat we een array hebben van onze Mutable-struct en dat we de IncrementI-methode aanroepen voor het eerste element van die array. Welk gedrag verwacht je van dit telefoontje? Moet het de waarde van de array veranderen of alleen een kopie?

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

Dus veranderlijke structuren zijn niet slecht zolang jij en de rest van het team duidelijk begrijpen wat je doet. Maar er zijn te veel hoekgevallen waarin het programmagedrag anders zou zijn dan verwacht, wat zou kunnen leiden tot subtiele, moeilijk te produceren en moeilijk te begrijpen fouten.


Antwoord 7, autoriteit 7%

Waardetypen vertegenwoordigen in feite onveranderlijke concepten. Fx, het heeft geen zin om een ​​wiskundige waarde zoals een geheel getal, vector enz. te hebben en deze vervolgens te kunnen wijzigen. Dat zou hetzelfde zijn als het herdefiniëren van de betekenis van een waarde. In plaats van een waardetype te wijzigen, is het logischer om een ​​andere unieke waarde toe te wijzen. Bedenk dat waardetypen worden vergeleken door alle waarden van hun eigenschappen te vergelijken. Het punt is dat als de eigenschappen hetzelfde zijn, het dezelfde universele representatie van die waarde is.

Zoals Konrad al zei, heeft het ook geen zin om een ​​datum te wijzigen, omdat de waarde dat unieke tijdstip vertegenwoordigt en niet een instantie van een tijdobject dat enige status- of contextafhankelijkheid heeft.

Hopelijk heb je hier wat aan. Het gaat natuurlijk meer om het concept dat je probeert vast te leggen met waardetypes dan om praktische details.


Antwoord 8, autoriteit 6%

Als je ooit in een taal als C/C++ hebt geprogrammeerd, zijn structs prima te gebruiken als veranderlijk. Geef ze gewoon door met ref, rond en er kan niets mis gaan. Het enige probleem dat ik tegenkom zijn de beperkingen van de C#-compiler en dat ik in sommige gevallen niet in staat ben om het stomme ding te dwingen een verwijzing naar de struct te gebruiken in plaats van een Copy (zoals wanneer een struct deel uitmaakt van een C#-klasse ).

Dus veranderlijke structs zijn niet slecht, C# heeft ze slecht gemaakt. Ik gebruik altijd veranderlijke structs in C++ en ze zijn erg handig en intuïtief. Daarentegen heeft C# ervoor gezorgd dat ik structs als leden van klassen volledig heb opgegeven vanwege de manier waarop ze met objecten omgaan. Hun gemak heeft ons het onze gekost.


Antwoord 9, autoriteit 5%

Als je je houdt aan waarvoor structs bedoeld zijn (in C#, Visual Basic 6, Pascal/Delphi, C++ structtype (of klassen) wanneer ze niet als aanwijzers worden gebruikt), zul je merken dat een structuur niet meer dan een samengestelde variabele. Dit betekent: u behandelt ze als een verpakte set variabelen, onder een algemene naam (een recordvariabele waaruit u naar leden verwijst).

Ik weet dat dit veel mensen die diep gewend zijn aan OOP in verwarring zou brengen, maar dat is niet genoeg reden om te zeggen dat zulke dingen inherent slecht zijn, als ze correct worden gebruikt. Sommige structuren zijn onveranderlijk zoals ze bedoeld zijn (dit is het geval van namedtuplevan Python), maar het is een ander paradigma om te overwegen.

Ja: structs brengen veel geheugen met zich mee, maar het zal niet precies meer geheugen zijn door te doen:

point.x = point.x + 1

vergeleken met:

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

Het geheugenverbruik zal minstens hetzelfde zijn, of zelfs meer in het onveranderlijke geval (hoewel dat geval tijdelijk zou zijn, voor de huidige stapel, afhankelijk van de taal).

Maar ten slotte zijn structuren structuren, geen objecten. In POO is de belangrijkste eigenschap van een object hun identiteit, die meestal niet meer is dan het geheugenadres. Struct staat voor datastructuur (geen echt object, en dus hebben ze toch geen identiteit), en data kunnen worden gewijzigd. In andere talen is record(in plaats van struct, zoals het geval is voor Pascal) het woord en heeft hetzelfde doel: alleen een gegevensrecordvariabele, bedoeld om te worden gelezen uit bestanden, gewijzigd en in bestanden gedumpt (dat is het belangrijkste gebruik en in veel talen kunt u zelfs gegevensuitlijning in het record definiëren, terwijl dat niet noodzakelijk het geval is voor correct genoemde Objecten).

Wil je een goed voorbeeld? Structuren worden gebruikt om bestanden gemakkelijk te lezen. Python heeft deze bibliotheekomdat het, aangezien het objectgeoriënteerd is en geen ondersteuning heeft voor structs, implementeer het op een andere manier, wat enigszins lelijk is. Talen die strucs implementeren, hebben die functie… ingebouwd. Probeer een bitmap-header te lezen met een geschikte struct in talen zoals Pascal of C. Het zal gemakkelijk zijn (als de struct correct is gebouwd en uitgelijnd; in Pascal zou u geen op records gebaseerde toegang gebruiken, maar functies om willekeurige binaire gegevens te lezen). Dus voor bestanden en directe (lokale) geheugentoegang zijn structs beter dan objecten. Vandaag zijn we JSON en XML gewend, en dus vergeten we het gebruik van binaire bestanden (en als neveneffect het gebruik van structs). Maar ja: ze bestaan, en hebben een doel.

Ze zijn niet slecht. Gebruik ze gewoon voor het juiste doel.

Als je in termen van hamers denkt, wil je schroeven als spijkers zien, want schroeven zijn moeilijker in de muur te steken, en het zal de schuld van de schroeven zijn, en zij zullen de slechte zijn.

>


Antwoord 10, autoriteit 4%

Stel je voor dat je een array van 1.000.000 structs hebt. Elke structuur die een eigen vermogen vertegenwoordigt met dingen als biedprijs, biedprijs (misschien decimalen) enzovoort, wordt gemaakt door C#/VB.

Stel je voor dat de array wordt gemaakt in een geheugenblok dat is toegewezen aan de onbeheerde heap, zodat een andere native codethread tegelijkertijd toegang kan krijgen tot de array (misschien een of andere hoogwaardige code die wiskunde doet).

Stel je voor dat de C#/VB-code luistert naar een marktfeed van prijswijzigingen, dat die code mogelijk toegang moet krijgen tot een element van de array (voor welke beveiliging dan ook) en vervolgens een of meer prijsveld(en) moet wijzigen.

Stel je voor dat dit tien- of zelfs honderdduizenden keren per seconde wordt gedaan.

Nou, laten we de feiten onder ogen zien, in dit geval willen we echt dat deze structs veranderlijk zijn, ze moeten wel omdat ze worden gedeeld door een andere native code, dus het maken van kopieën zal niet helpen; ze moeten zijn omdat het maken van een kopie van zo’n 120 bytes struct tegen deze snelheden waanzin is, vooral wanneer een update slechts een byte of twee kan beïnvloeden.

Hugo


11, Autoriteit 2%

Het heeft niets te maken met structuren (en niet met C #, ook), maar in Java krijgt u mogelijk problemen met mutable objecten wanneer ze b.v. Sleutels in een hash-kaart. Als u ze wijzigt na het toevoegen van ze aan een kaart en het verandert zijn Hash-code , slechte dingen kunnen gebeuren.


12, Autoriteit 2%

Er zijn veel voor- en nadelen aan mutable gegevens. Het nadeel van de miljoen dollar is aliasing. Als dezelfde waarde op meerdere plaatsen wordt gebruikt, en een van hen verandert, lijkt het op magische wijze aan de andere plaatsen die het gebruiken. Dit is gerelateerd aan, maar niet identiek aan, race-omstandigheden.

Het voordeel van de miljoen dollar is soms modulariteit. In de toestand kan u toestaan ​​dat u de wijziging van de code van de code verberft die er niet over hoeft te weten.

De kunst van de tolk gaat in een detail in deze trade, en geeft enkele voorbeelden.


13, Autoriteit 2%

Persoonlijk wanneer ik naar code kijk, ziet het volgende uiterlijk knuffelachtig aan mij:

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

in plaats van simpelweg

Data.Value ++; of data.value = gegevens. Value + 1;

Data-inkapseling is handig bij het passeren van een klasse en u wilt ervoor zorgen dat de waarde op een gecontroleerde manier wordt gewijzigd. Maar wanneer u echter openbare set hebt en functies krijgt die weinig meer doen dan de waarde instellen op wat er ooit is ingeschakeld, hoe is dit een verbetering van een openbare gegevensstructuur in de buurt?

Wanneer ik een privéstructuur in een klasse maak, creëerde ik die structuur om een ​​reeks variabelen in één groep te organiseren. Ik wil in staat zijn om die structuur binnen de klassingsbereik te wijzigen, geen kopieën van die structuur te krijgen en nieuwe instanties aan te maken.

Voor mij voorkomt dit een geldig gebruik van structuren die worden gebruikt om openbare variabelen te organiseren, als ik toegangscontrole wilde, zou ik een klasse gebruiken.


Antwoord 14

Ik geloof niet dat ze slecht zijn als ze correct worden gebruikt. Ik zou het niet in mijn productiecode plaatsen, maar ik zou het wel doen voor zoiets als mocks voor het testen van gestructureerde eenheden, waarbij de levensduur van een structeur relatief klein is.

Als je het Eric-voorbeeld gebruikt, wil je misschien een tweede exemplaar van die Eric maken, maar maak je aanpassingen, want dat is de aard van je test (dwz dupliceren, dan wijzigen). Het maakt niet uit wat er gebeurt met het eerste exemplaar van Eric als we Eric2 alleen gebruiken voor de rest van het testscript, tenzij je van plan bent hem als testvergelijking te gebruiken.

Dit zou vooral handig zijn voor het testen of wijzigen van verouderde code die oppervlakkig een bepaald object definieert (het punt van structs), maar door een onveranderlijke struct te hebben, voorkomt dit dat het irritant wordt gebruikt.

Other episodes