Det är ofta nödvändigt att göra en kopia av en värde i Ruby. Även om detta kan verka enkelt, och det är för enkla objekt, så snart du måste göra en kopia av en data struktur med flera array eller hashes på samma objekt, hittar du snabbt att det finns många fallgropar.
Objekt och referenser
För att förstå vad som händer, låt oss titta på någon enkel kod. Först ska tilldelningsoperatören använda en POD (Plain Old Data) -typ Rubin.
a = 1
b = a
a + = 1
sätter b
Här gör uppdragsoperatören en kopia av värdet på en och tilldela den till b med uppdragsoperatören. Eventuella ändringar av en kommer inte att återspeglas i b. Men hur är det med något mer komplicerat? Tänk på detta.
a = [1,2]
b = a
a << 3
sätter b.inspect
Innan du kör ovanstående program, försök att gissa vad utdata blir och varför. Detta är inte samma sak som föregående exempel, ändringar gjorda till en återspeglas i b, men varför? Detta beror på att Array objektet är inte en POD-typ. Uppdragsoperatören gör inte en kopia av värdet utan kopierar helt enkelt
referens till Array-objektet. De en och b variabler är nu referenser till samma Array-objekt kommer alla ändringar i någon av variablerna att ses i det andra.Och nu kan du se varför det kan vara svårt att kopiera icke-triviala objekt med referenser till andra objekt. Om du helt enkelt gör en kopia av objektet kopierar du bara referenser till de djupare objekten, så din kopia kallas en "grund kopia."
Vad Ruby tillhandahåller: dup och klon
Ruby tillhandahåller två metoder för att göra kopior av objekt, inklusive en som kan göras för att göra djupa kopior. De Objekt # dup metoden kommer att göra en ytlig kopia av ett objekt. För att uppnå detta dup metoden kommer att ringa initialize_copy metod för den klassen. Vad detta exakt gör beror på klassen. I vissa klasser, till exempel Array, kommer det att initialisera en ny matris med samma medlemmar som den ursprungliga matrisen. Detta är dock inte en djup kopia. Tänk på följande.
a = [1,2]
b = a.dup
a << 3
sätter b.inspect
a = [[1,2]]
b = a.dup
a [0] << 3
sätter b.inspect
Vad har hänt här? De Array # initialize_copy metoden kommer verkligen att göra en kopia av en array, men den kopian är i sig själv en grund kopia. Om du har några andra icke-POD-typer i din grupp använder du dup kommer bara att vara en delvis djup kopia. Det kommer bara att vara så djup som den första matrisen, alla djupare arrayer, hashes eller andra objekt kopieras bara grunt.
Det finns en annan metod värd att nämna, klona. Klonmetoden gör samma sak som dup med en viktig skillnad: det förväntas att objekt kommer att åsidosätta denna metod med en som kan göra djupa kopior.
Så i praktiken vad betyder detta? Det betyder att var och en av dina klasser kan definiera en klonmetod som kommer att göra en djup kopia av det objektet. Det betyder också att du måste skriva en klonmetod för varje klass du gör.
A Trick: Marshalling
"Marshalling" av ett objekt är ett annat sätt att säga "serialisera" ett objekt. Med andra ord, förvandla objektet till en karaktärsström som kan skrivas till en fil som du kan "avmarkera" eller "avläsna" senare för att få samma objekt. Detta kan utnyttjas för att få en djup kopia av alla objekt.
a = [[1,2]]
b = Marshal.load (Marshal.dump (a))
a [0] << 3
sätter b.inspect
Vad har hänt här? Marshal.dump skapar en "dumpning" av det kapslade arrayet lagrat i en. Denna dumpning är en binär teckensträng avsedd att lagras i en fil. Den innehåller hela innehållet i matrisen, en fullständig djup kopia. Nästa, Marshal.load gör motsatsen. Den analyserar denna binära teckenuppsättning och skapar en helt ny matris med helt nya arrayelement.
Men detta är ett trick. Det är ineffektivt, det fungerar inte på alla objekt (vad händer om du försöker klona en nätverksanslutning på detta sätt?) Och det är förmodligen inte väldigt snabbt. Det är emellertid det enklaste sättet att göra djupa kopior utan anpassade initialize_copy eller klona metoder. Samma sak kan också göras med metoder som to_yaml eller to_xml om du har bibliotek laddade för att stödja dem.