Oföränderligt objekt
I objektorienterad och funktionell programmering är ett oföränderligt objekt (oföränderligt objekt) ett objekt vars tillstånd inte kan ändras efter att det skapats. Detta till skillnad från ett föränderligt objekt (föränderligt objekt), som kan modifieras efter att det har skapats. I vissa fall anses ett objekt vara oföränderligt även om vissa internt använda attribut ändras, men objektets tillstånd verkar oföränderligt utifrån en extern synvinkel. Till exempel kan ett objekt som använder memoisering för att cachelagra resultaten av dyra beräkningar fortfarande betraktas som ett oföränderligt objekt.
Strängar och andra konkreta objekt uttrycks vanligtvis som oföränderliga objekt för att förbättra läsbarheten och körtidseffektiviteten i objektorienterad programmering . Oföränderliga objekt är också användbara eftersom de i sig är trådsäkra . Andra fördelar är att de är enklare att förstå och resonera kring och erbjuder högre säkerhet än föränderliga objekt.
Begrepp
Oföränderliga variabler
I imperativ programmering kallas värden i programvariabler vars innehåll aldrig ändras som konstanter för att skilja dem från variabler som kan ändras under exekvering. Exempel inkluderar omvandlingsfaktorer från meter till fot, eller värdet av pi till flera decimaler.
Skrivskyddade fält kan beräknas när programmet körs (till skillnad från konstanter, som är kända i förväg), men ändras aldrig efter att de initierats.
Svag vs stark oföränderlighet
Ibland talar man om att vissa fält av ett objekt är oföränderliga. Detta innebär att det inte finns något sätt att ändra dessa delar av objektets tillstånd, även om andra delar av objektet kan vara föränderliga ( svagt oföränderliga ) . Om alla fält är oföränderliga är objektet oföränderligt. Om hela objektet inte kan utökas med en annan klass kallas objektet starkt oföränderligt . Detta kan till exempel hjälpa till att explicit tvinga fram vissa invarianter om att vissa data i objektet förblir oförändrade under objektets livstid. På vissa språk görs detta med ett nyckelord (t.ex. const
i C++ , final
i Java ) som betecknar fältet som oföränderligt. Vissa språk vänder på det: i OKaml är fält för ett objekt eller post som standard oföränderliga och måste uttryckligen markeras med mutable
för att vara det.
Referenser till föremål
I de flesta objektorienterade språk kan objekt hänvisas till med hjälp av referenser . Några exempel på sådana språk är Java , C++ , C# , VB.NET och många skriptspråk , som Perl , Python och Ruby . I det här fallet spelar det roll om ett objekts tillstånd kan variera när objekt delas via referenser.
Referera vs kopiering av objekt
Om ett objekt är känt för att vara oföränderligt, är det att föredra att skapa en referens till det istället för att kopiera hela objektet. Detta görs för att spara minne genom att förhindra dataduplicering och undvika anrop till konstruktörer och destruktörer; det resulterar också i en potentiell ökning av exekveringshastigheten.
Referenskopieringstekniken är mycket svårare att använda för föränderliga objekt, för om någon användare av en föränderlig objektreferens ändrar den, ser alla andra användare av den referensen ändringen. Om detta inte är den avsedda effekten kan det vara svårt att meddela de andra användarna att de ska svara korrekt. I dessa situationer defensiv kopiering av hela objektet snarare än referensen vanligtvis en enkel men kostsam lösning. Observatörsmönstret är en alternativ teknik för att hantera förändringar av föränderliga objekt .
Kopiera-på-skriva
En teknik som blandar fördelarna med föränderliga och oföränderliga objekt, och som stöds direkt i nästan all modern hårdvara, är copy-on-write (COW). Med denna teknik, när en användare ber systemet att kopiera ett objekt, skapar det istället bara en ny referens som fortfarande pekar på samma objekt. Så snart en användare försöker modifiera objektet genom en viss referens, gör systemet en riktig kopia, tillämpar modifieringen på det och ställer in referensen att referera till den nya kopian. De andra användarna påverkas inte, eftersom de fortfarande hänvisar till det ursprungliga objektet. Därför, under COW, verkar alla användare ha en föränderlig version av sina objekt, även om de utrymmesbesparande och snabba fördelarna med oföränderliga objekt bevaras om användarna inte ändrar sina objekt. Kopiera-på-skriv är populärt i virtuella minnessystem eftersom det tillåter dem att spara minnesutrymme samtidigt som de fortfarande hanterar allt som ett applikationsprogram kan göra på rätt sätt.
Praktikant
Bruket att alltid använda referenser i stället för kopior av lika objekt kallas internering . Om internering används anses två objekt vara lika om och endast om deras referenser, vanligtvis representerade som pekare eller heltal, är lika. Vissa språk gör detta automatiskt: till exempel Python automatiskt in korta strängar . Om algoritmen som implementerar internering garanterat gör det i alla fall som det är möjligt, reduceras jämförande av objekt för jämlikhet till att jämföra deras pekare – en avsevärd hastighetsvinst i de flesta applikationer. (Även om algoritmen inte garanteras att vara heltäckande, finns det fortfarande möjlighet till en snabb förbättring av sökvägsfall när objekten är lika och använder samma referens.) Internering är i allmänhet bara användbart för oföränderliga objekt.
Trådsäkerhet
Oföränderliga objekt kan vara användbara i flertrådade applikationer. Flera trådar kan agera på data som representeras av oföränderliga objekt utan oro för att data ändras av andra trådar. Oföränderliga objekt anses därför vara mer trådsäkra än föränderliga objekt.
Kränker oföränderligheten
Oföränderlighet innebär inte att objektet som lagrats i datorns minne är oskrivbart. Snarare är oföränderlighet en kompileringstidskonstruktion som indikerar vad en programmerare kan göra genom objektets normala gränssnitt, inte nödvändigtvis vad de absolut kan göra (till exempel genom att kringgå typsystemet eller bryta mot const correctness i C eller C++ ).
Språkspecifika detaljer
I Python , Java och .NET Framework är strängar oföränderliga objekt. Både Java och .NET Framework har föränderliga versioner av sträng. I Java är dessa StringBuffer
och StringBuilder
(föränderliga versioner av Java String
) och i .NET är detta StringBuilder
(föränderlig version av .Net String
). Python 3 har en föränderlig strängvariant (bytes), som heter bytearray
.
Dessutom är alla de primitiva omslagsklasserna i Java oföränderliga.
Liknande mönster är Immutable Interface och Immutable Wrapper.
I rena funktionella programmeringsspråk är det inte möjligt att skapa föränderliga objekt utan att utöka språket (t.ex. via ett föränderligt referensbibliotek eller ett främmande funktionsgränssnitt ), så alla objekt är oföränderliga.
Ada
I Ada deklareras vilket objekt som helst som antingen variabel (dvs. föränderlig; vanligtvis den implicita standarden), eller konstant
(dvs. oföränderlig) via nyckelordet konstant .
typ Some_type är nytt heltal ; -- kan vara något mer komplicerat x : konstant Some_type := 1 ; -- oföränderlig y : Some_type ; -- föränderlig
Underprogramparametrar är oföränderliga i in- läge och föränderliga i in-ut- och ut -läge.
proceduren Do_it ( a : i heltal ; b : in ut heltal ; c : ut heltal ) börjar - a är oföränderlig b : = b + a ; c := a ; slut Gör_det ;
C#
I C# kan du framtvinga oföränderlighet av fälten i en klass med skrivskyddat
uttalande. Genom att upprätthålla alla fält som oföränderliga får du en oföränderlig typ.
class AnImmutableType { public readonly double _value ; public AnImmutableType ( double x ) { _value = x ; } public AnImmutableType Square () { returnera ny AnImmutableType ( _value * _value ); } }
C++
I C++ skulle en const-korrekt implementering av Cart
tillåta användaren att skapa instanser av klassen och sedan använda dem som antingen const
(oföränderliga) eller föränderliga, efter önskemål, genom att tillhandahålla två olika versioner av metoden items() .
(Observera att i C++ är det inte nödvändigt – och faktiskt omöjligt – att tillhandahålla en specialiserad konstruktör för konstinstanser
.)
class Cart { public : Cart ( std :: vektor < Artikel > objekt ) : items_ ( objekt ) {} std :: vektor < Artikel > & objekt ( ) { return items_ ; } const std :: vektor < Item >& items () const { return items_ ; } int ComputeTotalCost () const { /* return summan av priserna */ } private : std :: vector < Item > items_ ; };
Observera att när det finns en datamedlem som är en pekare eller referens till ett annat objekt, så är det möjligt att mutera objektet som pekas på eller refereras till endast inom en icke-konst-metod.
C++ ger också abstrakt (i motsats till bitvis) oföränderlighet via det mutable
nyckelordet, som låter en medlemsvariabel ändras inifrån en const-
metod.
0
klass Varukorg { public : Varukorg ( std :: vektor < Item > items ) : items_ ( items ) {} const std :: vektor < Item >& items () const { return items_ ; } int ComputeTotalCost () const { if ( total_cost_ ) { return * total_cost_ ; } int total_cost = ; för ( const auto & item : items_ ) { total_cost += item . Kostnad (); } total_cost_ = total_cost ; returnera total_kostnad ; } privat : std :: vektor < Objekt > items_ ; mutable std :: valfritt < int > total_cost_ ; };
D
I D finns det två typkvalificerare , const
och immutable
, för variabler som inte kan ändras. Till skillnad från C++'s const
, Java's final
och C#'s readonly
, är de transitiva och tillämpas rekursivt på allt som kan nås genom referenser till en sådan variabel. Skillnaden mellan const
och oföränderlig
är vad de gäller: const
är en egenskap hos variabeln: det kan lagligen existera föränderliga referenser till refererat värde, dvs värdet kan faktiskt ändras. Däremot oföränderlig
en egenskap hos det refererade värdet: värdet och allt som transitivt kan nås från det kan inte ändras (utan att bryta typsystemet, vilket leder till odefinierat beteende ). Alla referenser till det värdet måste markeras som const
eller oföränderliga
. I grund och botten för varje okvalificerad typ T är
const(T)
den disjunkta föreningen av T
(föränderlig) och oföränderlig(T
) .
class C { /*mutable*/ Object mField ; const Objekt cField ; oföränderligt objekt iField ; }
För ett föränderligt C
-objekt kan dess mField
skrivas till. För ett const(C)
-objekt kan mField
inte ändras, det ärver const
; iField
är fortfarande oföränderligt eftersom det är den starkare garantin. För en oföränderlig(C)
är alla fält oföränderliga.
I en funktion som denna:
void func ( C m , const C c , oföränderlig C i ) { /* innanför klammerparenteserna */ }
Inuti hängslen kan c
referera till samma objekt som m
, så mutationer till m
kan indirekt också ändra c .
Dessutom kan c
referera till samma objekt som i
, men eftersom värdet då är oföränderligt, finns det inga ändringar. Men m
och i
kan inte juridiskt referera till samma objekt.
På garantispråket har mutable inga garantier (funktionen kan ändra objektet), const
är en utåtriktad garanti för att funktionen inte kommer att ändra någonting, och immutable
är en dubbelriktad garanti (funktionen kommer inte att ändra värdet och den som ringer får inte ändra det).
Värden som är konstanta
eller oföränderliga
måste initieras genom direkt tilldelning vid deklarationspunkten eller av en konstruktor .
Eftersom const-
parametrar glömmer om värdet var föränderligt eller inte, fungerar en liknande konstruktion, inout , på sätt och vis som en variabel för information om förändringar.
En funktion av typen const(S) function(const(T))
returnerar const(S)
-skrivna värden för föränderliga, const och oföränderliga argument. Däremot returnerar en funktion av typen inout(S) function(inout(T))
S
för föränderliga T-
argument, const(S)
för const(T)
-värden och oföränderlig(S)
för oföränderliga(T)
-värden.
Att kasta oföränderliga värden till föränderliga orsakar odefinierat beteende vid förändring, även om det ursprungliga värdet kommer från ett föränderligt ursprung. Att casta föränderliga värden till oföränderliga kan vara lagligt när det inte finns några föränderliga referenser efteråt. "Ett uttryck kan konverteras från föränderligt (...) till oföränderligt om uttrycket är unikt och alla uttryck det transitivt refererar till är antingen unika eller oföränderliga." Om kompilatorn inte kan bevisa unikhet kan castingen göras explicit och det är upp till programmeraren att se till att inga föränderliga referenser finns.
Typsträngen är ett alias för immutable(char)[]
, dvs en maskinskriven minnesdel av oföränderliga tecken .
Att göra delsträngar är billigt, eftersom det bara kopierar och modifierar en pekare och en längd som är arkiverad, och säker, eftersom de underliggande data inte kan ändras. Objekt av typen const(char)[]
kan referera till strängar, men också till muterbara buffertar.
Att göra en ytlig kopia av ett const eller oföränderligt värde tar bort det yttre lagret av oföränderlighet: Att kopiera en oföränderlig sträng ( immutable(char[]) )
returnerar en sträng ( immutable(char)[]
). Den oföränderliga pekaren och längden kopieras och kopiorna är föränderliga. Den refererade datan har inte kopierats och behåller sin kvalificering, i exemplet oföränderlig
. Den kan tas bort genom att göra en depper kopia, t.ex. med dup
-funktionen.
Java
Ett klassiskt exempel på ett oföränderligt objekt är en instans av Java String
-klassen
String s = "ABC" ; s . toLowerCase ();
Metoden toLowerCase()
ändrar inte data "ABC" som s
innehåller. Istället instansieras ett nytt String-objekt och ges data "abc" under dess konstruktion. En referens till det här String-objektet returneras av metoden toLowerCase() .
För att få strängarna att
innehålla data "abc" behövs ett annat tillvägagångssätt:
s = s . toLowerCase ();
Nu refererar String s
till ett nytt String-objekt som innehåller "abc". Det finns inget i syntaxen för deklarationen av klassen String som framtvingar den som oföränderlig; snarare, ingen av String-klassens metoder påverkar någonsin data som ett String-objekt innehåller, vilket gör det oföränderligt.
Nyckelordet final
( detaljerad artikel ) används för att implementera oföränderliga primitiva typer och objektreferenser, men det kan inte i sig göra själva objekten oföränderliga. Se nedanstående exempel:
Primitiva typvariabler ( int
, long
, short
, etc.) kan tilldelas om efter att ha definierats. Detta kan förhindras genom att använda final
.
int i = 42 ; //int är en primitiv typ i = 43 ; // OK final int j = 42 ; j = 43 ; // kompilerar inte. j är slutgiltigt så kan inte tilldelas om
Referenstyper kan inte göras oföränderliga bara genom att använda det sista
nyckelordet. final
förhindrar endast omplacering.
final MyObject m = new MyObject (); //m är av referenstyp m . data = 100 ; // OK. Vi kan ändra tillståndet för objektet m (m är föränderligt och final ändrar inte detta faktum) m = new MyObject (); // kompilerar inte. m är slutgiltig så kan inte tilldelas om
Primitiva omslag ( heltal
, långt
, kort
, dubbelt
, flytande
, tecken
, byte
, booleskt
) är också alla oföränderliga. Oföränderliga klasser kan implementeras genom att följa några enkla riktlinjer.
JavaScript
I JavaScript är alla primitiva typer (Odefinierad, Null, Boolean, Number, BigInt, String, Symbol) oföränderliga, men anpassade objekt är i allmänhet föränderliga.
function doSomething ( x ) { /* ändras originalet om du ändrar x här? */ }; var str = 'en sträng' ; var obj = { an : 'objekt' }; göra Något ( str ); // strängar, siffror och booltyper är oföränderliga, funktion får en kopia doSomething ( obj ); // objekt skickas in genom referens och är föränderliga inuti funktionen doAnotherThing ( str , obj ); // `str` har inte ändrats, men `obj` kan ha ändrats.
För att simulera oföränderlighet i ett objekt kan man definiera egenskaper som skrivskyddade (skrivbar: falsk).
var obj = {}; Objekt . defineProperty ( obj , 'foo' , { värde : 'bar' , skrivbar : false }); obj . foo = 'bar2' ; // ignoreras tyst
Men tillvägagångssättet ovan låter fortfarande nya fastigheter läggas till. Alternativt kan man använda Object.freeze för att göra befintliga objekt oföränderliga.
var obj = { foo : 'bar' }; Objekt . frysa ( obj ); obj . foo = 'stänger' ; // kan inte redigera egenskapen, ignoreras tyst obj . foo2 = 'bar2' ; // kan inte lägga till egenskap, ignoreras tyst
Med implementeringen av ECMA262 har JavaScript möjlighet att skapa oföränderliga referenser som inte kan tilldelas om. Att använda en const-
deklaration betyder dock inte att värdet på den skrivskyddade referensen är oföränderligt, bara att namnet inte kan tilldelas ett nytt värde.
const ALWAYS_IMMUTABLE = sant ; försök { ALWAYS_IMMUTABLE = false ; } fånga ( fela ) { konsol . log ( "Kan inte omtilldela en oföränderlig referens." ) ; } const arr = [ 1 , 2 , 3 ]; arr . tryck ( 4 ); konsol . log ( arr ); // [1, 2, 3, 4]
Användningen av oföränderligt tillstånd har blivit en stigande trend i JavaScript sedan introduktionen av React , som gynnar Flux-liknande tillståndshanteringsmönster som Redux .
Perl
I Perl kan man skapa en oföränderlig klass med Moo-biblioteket genom att helt enkelt förklara alla attribut som läsbara:
paket Immutable ; använd Moo ; har värde => ( är => 'ro' , # skrivskyddad standard => 'data' , # kan åsidosättas genom att förse konstruktorn med # ett värde: Immutable->new(value => 'något annat'); ) ; 1 ;
Att skapa en oföränderlig klass brukade kräva två steg: för det första skapa accessorer (antingen automatiskt eller manuellt) som förhindrar modifiering av objektattribut, och för det andra förhindrar direkt modifiering av instansdata för instanser av den klassen (detta lagrades vanligtvis i en hash referens, och kan låsas med Hash::Utils lock_hash-funktion):
0
paket Immutable ; använd strikt ; använd varningar ; använd basen qw(Class::Accessor) ; # skapa skrivskyddade accessorer __PACKAGE__ -> mk_ro_accessors ( qw(värde) ) ; använd Hash::Util 'lock_hash' ; sub new { min $klass = skift ; returnera $klass om ref ( $klass ); die "Argument till nya måste vara nyckel => värdepar\n" om inte ( @_ % 2 == ); mina %defaults = ( värde => 'data' , ); min $obj = { %defaults , @_ , }; välsigna $obj , $klass ; # förhindra modifiering av objektdata lock_hash %$obj ; } 1 ;
Eller, med en manuellt skriven accessor:
0
paket Immutable ; använd strikt ; använd varningar ; använd Hash::Util 'lock_hash' ; sub new { min $klass = skift ; returnera $klass om ref ( $klass ); die "Argument till nya måste vara nyckel => värdepar\n" om inte ( @_ % 2 == ); mina %defaults = ( värde => 'data' , ); min $obj = { %defaults , @_ , }; välsigna $obj , $klass ; # förhindra modifiering av objektdata lock_hash %$obj ; } # skrivskyddad accessor subvärde { my $ self = shift ; if ( my $new_value = shift ) { # försöker sätta ett nytt värde tärning "Detta objekt kan inte ändras\n" ; } else { return $self -> { value } } } 1 ;
Pytonorm
I Python är vissa inbyggda typer (siffror, booleans, strängar, tupler, frysta set) oföränderliga, men anpassade klasser är i allmänhet föränderliga. För att simulera oföränderlighet i en klass, kan man åsidosätta attributinställning och radering för att skapa undantag:
class ImmutablePoint : """En oföränderlig klass med två attribut 'x' och 'y'.""" __slots__ = [ 'x' , 'y' ] def __setattr__ ( self , * args ): raise TypeError ( "Kan inte ändras oföränderlig instans." ) __delattr__ = __setattr__ def __init__ ( själv , x , y ): # Vi kan inte längre använda self.value = värde för att lagra instansdata # så vi måste uttryckligen kalla superklassen super () . __setattr__ ( 'x' , x ) super () . __setattr__ ( 'y' , y )
Standardbibliotekshjälparna collections.namedtuple
och typing.NamedTuple
, tillgängliga från Python 3.6 och framåt, skapar enkla oföränderliga klasser. Följande exempel är ungefär lika med ovanstående, plus några tuppelliknande funktioner:
från att skriva import NamedTuple import collections Point = samlingar . namedtuple ( 'Point' , [ 'x' , 'y' ]) # följande skapar en liknande namedtuple till ovanstående klass Point ( NamedTuple ): x : int y : int
Dataklasser
introducerades i Python 3.7 och tillåter utvecklare att emulera oföränderlighet med frusna instanser . Om en fryst dataklass byggs dataklasser
att åsidosätta __setattr__()
och __delattr__()
för att öka FrozenInstanceError
om de anropas.
från dataklasser importera dataklass @dataclass ( fryst = True ) klass Punkt : x : int y : int
Racket
Racket avviker väsentligt från andra Scheme- implementeringar genom att göra dess kärnpartyp ("cons celler") oföränderlig. Istället ger den en parallell föränderlig partyp, via mcons
, mcar
, set-mcar!
etc. Dessutom stöds många oföränderliga typer, till exempel oföränderliga strängar och vektorer, och dessa används flitigt. Nya strukturer är oföränderliga som standard, om inte ett fält specifikt förklaras föränderligt, eller hela strukturen:
( struktur foo1 ( x y )) ; alla fält oföränderliga ( struct foo2 ( x [ y #: föränderlig ]) ) ; ett föränderligt fält ( struct foo3 ( x y ) #: föränderligt ) ; alla fält är föränderliga
Språket stöder också oföränderliga hashtabeller, implementerade funktionellt och oföränderliga ordböcker.
Rost
Rusts ägarsystem tillåter utvecklare att deklarera oföränderliga variabler och skicka oföränderliga referenser. Som standard är alla variabler och referenser oföränderliga. Föränderliga variabler och referenser skapas uttryckligen med nyckelordet mut .
Konstanta objekt i Rust är alltid oföränderliga.
// konstanta objekt är alltid oföränderliga const ALWAYS_IMMUTABLE : bool = true ; struct Object { x : usize , y : usize , } fn main ( ) { // deklarera explicit en föränderlig variabel let mut mutable_obj = Object { x : 1 , y : 2 }; mutable_obj . x = 3 ; // okej låt mutable_ref = & mut mutable_obj ; mutable_ref . x = 1 ; // okej låt immutable_ref = & mutable_obj ; immutable_ref . x = 3 ; // fel E0594 // som standard, variabler är oföränderliga let immutable_obj = Objekt { x : 4 , y : 5 }; immutable_obj . x = 6 ; // error E0596 let mutable_ref2 = & mut immutable_obj ; // error E0596 let immutable_ref2 = & immutable_obj ; oföränderlig_ref2 . x = 6 ; // fel E0594 }
Scala
I Scala kan vilken entitet som helst (snävt, en bindning) definieras som föränderlig eller oföränderlig: i deklarationen kan man använda val
(värde) för oföränderliga entiteter och var
(variabel) för föränderliga. Observera att även om en oföränderlig bindning inte kan tilldelas om, kan den fortfarande hänvisa till ett föränderligt objekt och det är fortfarande möjligt att anropa muterande metoder för det objektet: bindningen är oföränderlig , men det underliggande objektet kan vara föränderligt.
Till exempel följande kodavsnitt:
val maxValue = 100 var currentValue = 1
definierar en oföränderlig entitet maxValue
(heltalstypen antas vid kompilering) och en föränderlig entitet som heter currentValue
.
Som standard är samlingsklasser som List
och Map
oföränderliga, så uppdateringsmetoder returnerar en ny instans istället för att mutera en befintlig. Även om detta kan låta ineffektivt, innebär implementeringen av dessa klasser och deras garantier för oföränderlighet att den nya instansen kan återanvända befintliga noder, vilket, särskilt när det gäller att skapa kopior, är mycket effektivt. [ bättre källa behövs ]
Se även
Den här artikeln innehåller en del material från Perl Design Patterns Book
externa länkar
- Oföränderliga objekt i C# med 3 enkla steg.
- Artikel Java teori och praktik: Att mutera eller inte mutera? av Brian Goetz, från IBM DeveloperWorks – sparad kopia på Internet Archive av Brian Goetz, från IBM DeveloperWorks – sparad kopia på Internet Archive
- Oföränderliga objekt från JavaPractices.com
- Oföränderliga objekt från Portland Pattern Repository
- Immutable.js av Facebook
- Oföränderliga strukturer i C# Arkiverad 2017-12-21 vid Wayback Machine opensource-projektet i Codeplex
- Oföränderliga samlingar i .NET officiella bibliotek av Microsoft
- Oföränderliga objekt i C# av Tutlane.com