Het correct implementeren van een deep copy is soms een vervelend werkje. We tonen hier een voorbeeld hoe je een deep copy op een elegante manier kan implementeren in C#. De details van de implementatie laten we aan de lezer om zelf te ontdekken.
Een groot probleem bij het implementeren van de interface ICLoneable zijn class members met een referentietype. Je moet er zelf voor zorgen dat ze op een correcte manier worden gekopieerd en dit doorheen de volledige object graph. Gebeurt dit niet correct dan krijg je een zogenaamde shallow copy met alle gevolgen van dien.
Er zijn een aantal verschillende manieren om een deep copy te implementeren. Een eerste manier is het aanmaken van een copy-constructor die je aanroept vanuit de methode Clone met het sleutelwoord this.
We geven hieronder een voorbeeld van een container die een lijst van IP-adressen stockeert. In .NET wordt een IP-adres opgeslagen in de klasse IPAddress. Het nadeel is dat ICLoneable en de copy-constructor niet geïmplementeerd zijn in deze klasse. We moeten dus naar een alternatieve manier zoeken om de objecten in de lijst te kopiëren. Hieronder zie je hoe we dit opgelost hebben.
class IPAddressContainer : ICloneable {
private List addresses;
public List Addresses {
get {
return addresses;
}
}
public IPAddressContainer() {
this.addresses = new List();
}
public IPAddressContainer(IPAddressContainer iac) {
this.addresses = new List();
foreach (IPAddress address in iac.addresses) {
this.addresses.Add(new IPAddress(address.GetAddressBytes()));
}
}
public object Clone() {
return new IPAddressContainer(this);
}
}
Deze implementatie van Clone maakt een diepe kopie van de objecten van IPAddressContainer. Het is echter duidelijk dat de implementatie van de copy-constructor erg foutgevoelig is. Ook de hoeveelheid implementatiewerk om een grote object graph te kopiëren is groot; je moet op elk niveau ICloneable op een correcte manier implementeren of een alternatief voorzien.
Er is echter nog een tweede manier om een deep copy te implementeren. We maken hier gebruik van een Stream naar het geheugen waarop we de ganse object graph serialiseren en deserialiseren. Het resultaat is een perfecte kopie van het oorspronkelijk object waarbij de twee kopieën geen enkele referentie meer gemeenschappelijk hebben.
[Serializable]
class IPAddressContainer : ICloneable {
private List addresses;
...
public object Clone() {
IPAddressContainer iac = null;
try {
using (var ms = new MemoryStream()) {
var bf = new BinaryFormatter();
bf.Serialize(ms, this);
ms.Position = 0;
iac = (IPAddressContainer)bf.Deserialize(ms);
}
} catch (Exception exp) {
System.Diagnostics.Debug.WriteLine(exp.Message);
}
return iac;
}
}
Deze implementatie is een elegant alternatief voor het implementeren van de deep copy doorheen de object graph. Je hoeft niet meer uit te zoeken hoe je objecten - zoals IPAddress - moet kopiëren. De implementatie van de deep copy gebeurt op één plaats in de object graph. Bovendien is het ook een universele manier in C# om object graphs op een correcte manier te kopiëren.
Er is echter ook een “maar” aan het verhaal. Zoals in het bovenstaande codevoorbeeld te zien is moet je elke klasse in de object graph van [Serializable] voorzien. Dit is echter een kleine moeite ten aanzien van de code die in het eerste voorbeeld voorzien moet worden.
Reacties
22 juni 2011, 16:36 (10 maanden geleden)
Zo elegant is het niet, zijn veel betere oplossingen hiervoor, met betere resultaten.
Kwa serialization zit je goed, alleen tegenwoordig kan je veel mooiere dingen met generics doen.
20 maart 2012, 14:59 (1 maand geleden)
@Michael: Artikel dateert van 2009 ;-)