Quelques méthodes pour cloner des objets en .NET

Parfois pour certains traitements, il est nécessaire de cloner l’instance d’un objet en une ou plusieurs autres instances distinctes. Suivant la complexité de l’objet ou suivant le nombre de copies nécessaires, cette copie peut être plus ou moins complexe à effectuer. D’autre part, dans le cas où on veut dupliquer un objet en plusieurs instances, il peut être nécessaire d’optimiser la duplication pour qu’elle soit la plus rapide possible. Enfin, suivant le type des objets à dupliquer, on peut vouloir rendre générique le procédé de duplication pour qu’il puisse s’adapter à plusieurs types d’objets.

Le but de cet article est de présenter quelques procédés de duplication d’instances d’un objet.

Quelque soit le procédé choisi pour dupliquer une instance d’un objet:

  • On souhaite que les instances dupliquées comporte des valeurs identiques concernant les champs (données membres privées) et les propriétés. De même que pour les champs, les valeurs des propriétés doivent être identiques même si l’accès en écriture à la propriété est privé.
  • Il faudrait que le procédé de duplication minimise les changements nécessaires à l’objet pour qu’il puisse être dupliqué: présence d’un constructeur sans paramètres, présence de l’attribut Serializable ou données membres accessibles dont l’accès en lecture et écriture est publique.
Choix de la méthode pour effectuer la duplication:

Il n’y a pas de méthodes parfaites pour effectuer la duplication d’objets, la méthode choisie dépend du contexte:

  • Possibilité d’adapter les objets à dupliquer: certaines méthodes nécessitent d’adapter les objets à dupliquer, par exemple, en ajoutant un constructeur sans paramètres ou en ajoutant un attribute particulier. La méthode choisie dépend de la façon dont on peut intervenir sur l’objet pour le rendre duplicable.
  • Nombre d’objets à dupliquer: si on doit dupliquer un grand nombre d’objets, on peut chercher à optimiser la vitesse de duplication.
  • Hétérogénéïté des objets: si les types des objets sont très différents, on peut privilégier une méthode générique plutôt qu’une méthode qui nécessite de modifier les classes à dupliquer.
  • Complexité des objets: suivant les couches d’abstractions des objets à dupliquer, on peut privilégier des méthodes plus génériques.

Dans tous les cas, une bonne approche est d’utiliser des tests pour vérifier le comportement de la méthode choisie.

On distingue 2 familles de méthodes pour dupliquer des objets:

  • Des méthodes nécessitant de modifier les objets à dupliquer: ces méthodes sont généralement faciles à mettre en œuvre. Elles sont possibles si on peut modifier l’implémentation des objets et si les modifications n’impactent pas trop d’objets.
  • Des méthodes génériques: ces méthodes ne nécessitent pas de connaître les objets à dupliquer. Elles sont à privilégier si on ne peut pas modifier l’implémentation des objets ou si la duplication s’effectue sur des objets de types très hétérogènes.

Quelques définitions en préambule

“Shallow copy” et “deep copy”

On distingue 2 types de duplication:

  • Shallow copy (i.e. copie superficielle): ce sont avant tout des copies simples et rapides d’un objet. Dans un contexte C#, ce type de duplication copie les valeurs des membres qui sont de type valeur (struct et enum). Pour les objets de type référence (classes, interfaces et delegates), seule la référence est dupliquée ce qui signifie que la référence d’origine et la référence dupliquée pointe vers le même objet.
    Les objets de type System.String sont des objets de type référence (car ils dérivent de Object). Toutefois sachant que les strings sont immutables (c’est-à-dire qu’il n’est pas possible de les modifier sans créer une nouvelle instance), dans le cadre d’une copie superficielle, elles sont dupliquées complêtement au même titre que les objets de type valeur.
  • Deep copy (i.e. copie en profondeur): tous les membres de l’objet d’origine sont dupliqués vers des valeurs et des instances distinctes quelque soit la complexité du membre.

Interface System.ICloneable

A partir du framework 2.0, l’interface System.ICloneable permet de décorer les objets de façon à ce qu’ils implémentent la méthode Clone():

public interface ICloneable 
{ 
    object Clone(); 
}

Cette interface n’indique pas si la copie est superficielle ou en profondeur, le choix est laissé au soin du développeur. Le 2e inconvénient de cette interface est qu’elle impose que l’objet cloné est de type Object. Il faut donc effectuer un cast sur le résultat de ICloneable.Clone() pour l’utiliser:

ICloneable cloneableObject = new CloneableObject(); 
CloneableObject objectClone = (CloneableObject)cloneableObject.Clone();

Méthodes nécessitant de modifier les objets à dupliquer

Implémenter ICloneable.Clone()

La méthode la plus directe pour dupliquer un objet est de le prévoir dans l’implémentation de cet objet. On peut alors implémenter la méthode ICloneable.Clone().

Par exemple, si on considère la classe:

public class Car 
{ 
    private string brand; 
    private string modelName; 
    private string reference; 
    private decimal price; 
    private Engine engine; 
    private Person owner; 
 
    public string Brand { get { return this.brand; } } 
    public string ModelName { get { return this.modelName; } } 
    public string Reference { get { return this.reference; } } 
    public decimal Price { get { return this.price; } } 
    public Engine Engine { get { return this.engine; } } 
    public Person Owner { get { return this.owner; } } 
 
 
    public Car(string brand, string modelName, string reference,  
        decimal price, Engine engine, Person owner) 
    { 
        this.brand = brand; 
        this.modelName = modelName; 
        this.reference = reference; 
        this.price = price; 
        this.engine = engine; 
        this.owner = owner; 
    } 
}

avec:

public class Engine  
{ 
    private string reference; 
    private string serialNumber; 
 
    public int Power { get; set; } 
    public string Reference { get { return this.reference; } } 
    public string SerialNumber { get { return this.serialNumber; } } 
 
    public Engine(int power, string reference, string serialNumber) 
    { 
        this.Power = power; 
        this.reference = reference; 
        this.serialNumber = serialNumber; 
    } 
}

Et:

public struct Person 
{ 
    public string FirstName { get; set; } 
    public string LastName { get; set; } 
}

On peut implémenter ICloneable.Clone() dans la classe Car:

public class Car : ICloneable 
{ 
    // [...] 
 
    public object Clone() 
    { 
        return new Car(this.brand, this.modelName, this.reference, 
            this.price, this.engine, this.Owner); 
    } 
}

Les membres brand, modelName, reference et price seront bien dupliqués mais pas engine (puisqu’on ne fait que dupliquer les références). Il faut donc implémenter aussi ICloneable.Clone() pour la classe Engine:

public class Engine : ICloneable 
{ 
    // [...] 
 
    public object Clone() 
    { 
        return new Engine(this.power, this.reference, this.serialNumber); 
    } 
}

Et modifier Car.Clone() en conséquence:

public class Car : ICloneable 
{ 
    // [...] 
 
    public object Clone() 
    { 
        return new Car(this.brand, this.modelName, this.reference, this.price,  
            this.engine.Clone() as Engine, this.Owner); 
    } 
}

La structure Person est un objet de type valeur donc lorsqu’on écrit this.Owner, on obtient déjà une copie de l’objet d’origine. Il n’est donc pas nécessaire d’implémenter une duplication explicite.

L’intéret de cette méthode est sa simplicité. Si on rajoute un membre dans une classe et qu’on modifie le constructeur, on sera obligé de modifier l’implémentation de la fonction ICloneable.Clone() correspondante.
En revanche, à chaque modification des membres d’une des classes, reporter la modification dans la fonction ICloneable.Clone() peut s’avérer fastidieux. Les méthodes ICloneable.Clone() impose un cast systématique lorsqu’elles sont utilisées.

Enfin, dans le cas où on implémente des tests pour vérifier la duplication, le moindre changement dans les membres des classes va imposer de modifier aussi les tests des duplications.

Duplication dans le constructeur

Une autre méthode est de prévoir la duplication directement dans le constructeur.

Par exemple, pour les classes Engine et Car:

public class Engine  
{ 
    // [...] 
 
    public Engine(Engine templateEngine) 
        : this(templateEngine.Power, templateEngine.reference, templateEngine.SerialNumber) 
    {  
    } 
} 
 
public class Car 
{ 
    // [...] 
 
    public Car(Car templateCar) : 
        this(templateCar.Brand, templateCar.ModelName, templateCar.Reference, 
        templateCar.Price, new Engine(templateCar.Engine), templateCar.Owner) 
    { 
    } 
}

De même cette méthode est facile à mettre en œuvre et n’impose pas un cast à son utilisation.

Object.MemberwiseClone()

Cette fonction permet d’effectuer une shallow copy d’un objet de type référence. Elle va donc créer une nouvelle instance et y copier les champs non statiques en effectuant une copie bit à bit des membres de type valeur. Comme indiqué plus haut, une shallow copy ne duplique pas les membres de type référence.
Object.MemberwiseClone() est protected, elle ne peut donc pas être appelée directement à l’extérieur de la classe à dupliquer.

Si on utilise cette méthode pour la classe Car, on peut réimplémenter la méthode ICloneable.Clone():

public class Car : ICloneable 
{ 
    private string brand; 
    private string modelName; 
    private string reference; 
    private decimal price; 
    private Engine engine; 
    private Person owner; 
 
    // [...] 
 
    public object Clone() 
    { 
        return this.MemberwiseClone(); 
    } 
}

Avec MemberwiseClone(), la structure Person est dupliquée au même titre que les autres objets de type valeur. En revanche, la classe Engine n’est pas dupliquée, seule la référence est dupliquée.

Ainsi si on exécute le code suivant:

var car = new Car("Ford", "Mustang", "GT", 3000m, new Engine(300, "BigEngine", "FE34F3"), 
    new Person() { FirstName = "Paul", LastName = "Ryan" }); 
 
var clonedCopy = car.Clone() as Car; // en utilisant MemberwiseClone() 
clonedCopy.Engine.Power = 340; 
 
var originalPower = car.Engine.Power; 
var clonedPower = clonedCopy.Engine.Power;

originalPower et clonedPower ont la même valeur.

L’intérêt de Object.MemberwiseClone() est qu’il n’est pas nécessaire d’implémenter un constructeur sans paramètre pour dupliquer un objet toutefois cette méthode comporte quelques inconvénients:

  • Elle n’effectue qu’une shallow copy, donc on utilise l’implémentation précédente de Car et qu’on exécute Car.Clone(), les membres brand, modelName, reference et price seront correctement dupliqués dans la nouvelle instance. En revanche, les membres Car.engine et Car.owner ne seront pas dupliqués car ce sont des objets de type référence. Ces membres dans la classe dupliquée pointeront vers les mêmes objets que l’objet d’origine.
  • Dans le cas où le développeur ne sait pas que Object.MemberwiseClone() n’effectue qu’une shallow copy et qu’il rajoute des membres de type référence, l’exécution pourra mener à des comportements inattendus car, dans le cas de la classe Car, le membre engine pointe vers le même objet que l’instance d’origine.

Méthodes génériques

Ces méthodes sont plus génériques car elles peuvent être utilisées sans avoir une connaissance des membres des objets à dupliquer à la compilation. La découverte des membres de l’objet à dupliquer se fait directement à l’exécution.

Reflection

On peut utiliser la reflection pour effectuer certains traitements:

  • Vérifier qu’une classe satisfait l’interface ICloneable pour exécuter la méthode ICloneable.Clone() qu’elle contient,
  • Accéder à la méthode Object.MemberwiseClone() d’une classe pour l’exécuter,
  • Pouvoir accéder aux données membres privées d’une classe de façon à en dupliquer la valeur dans la copie.

Les inconvénients de la reflection sont la lenteur d’exécution et il n’est pas autorisé de l’exécuter dans un environnement partial trust.

Instanciation de la copie avec Activator.CreateInstance()

Une première approche consiste à instancier la classe sans utiliser Object.MemberwiseClone() avec System.Activator.CreateInstance(). Le gros inconvénient de cette méthode est qu’elle nécessite d’implémenter un constructeur sans paramètres.

Dans le cadre de la classe Car (définie plus haut), il faudrait modifier l’objet de cette façon:

public class Car 
{ 
    private string brand; 
    private string modelName; 
    private string reference; 
    private decimal price; 
    private Engine engine; 
    private Person owner; 
 
    public string Brand { get { return this.brand; } } 
    public string ModelName { get { return this.modelName; } } 
    public string Reference { get { return this.reference; } } 
    public decimal Price { get { return this.price; } } 
    public Engine Engine { get { return this.engine; } } 
    public Person Owner { get { return this.owner; } } 
 
    public Car() 
    { 
    } 
 
    public Car(string brand, string modelName, string reference, 
        decimal price, Engine engine, Person owner) 
    { 
        this.brand = brand; 
        this.modelName = modelName; 
        this.reference = reference; 
        this.price = price; 
        this.engine = engine; 
        this.owner = owner; 
    } 
}

L’instanciation de la copie se fait en exécutant:

var originalCar = new Car(); 
Type carType = originalCar.GetType(); 
object copiiedCar = Activator.CreateInstance(carType);

“Shallow copy” avec Object.MemberwiseClone()

Exécuter la fonction MemberwiseClone() en utilisant la reflection permet d’éviter d’implémenter un constructeur sans paramètres:

MethodInfo memberwiseCloneMethod = typeof(Car).GetMethod("MemberwiseClone", 
    BindingFlags.Instance | BindingFlags.NonPublic); 
car copiiedCar = (Car)memberwiseCloneMethod.Invoke(input, null);

Copie des propriétés

Comme indiqué plus haut Object.MemberwiseClone() effectue une shallow copy d’un objet. Les objets de type référence ne sont pas vraiment dupliqués.

Une solution consiste à utiliser la reflection pour parcourir les membres de la classe:

  • On écarte de ce parcours les membres de type valeur qui sont des types primitifs ainsi que les objets System.String puisqu’ils sont pris en charge par MemberwiseClone(). En revanche on parcourt les structures car même si elles sont des objets de type valeur, elles peuvent contenir des des membres de type référence.
  • On parcourt d’abord les propriétés publiques et protected de la classe et des éventuelles classes mère (en cas d’héritage) pour effectuer la copie.
  • On parcourt ensuite les propriétés privés et les champs pour exécuter récursivement la copie.

Pour vérifier si un objet est de type primitif et qu’il n’est pas une System.String:

public static bool IsPrimitive(Type type) 
{ 
    if (type == typeof(String)) return true; 
    return type.IsValueType & type.IsPrimitive; 
}

Pour parcourir les propriétés publiques et protected de la classe et des classes mère, on utilise les BindingFlags suivant:

  • BindingFlags.Instance: permet de sélectionner des membres instanciables (données membres).
  • Bindings.NonPublic: sélectionner les membres private et protected.
  • Bindings.Public: permet de sélectionner les membres public.
  • Binding.FlattenHierarchy: permet de sélectionner les champs, les méthodes, les évènements et les propriétés public et protected de la classe et des classes mère (dans le cas d’un héritage). Les membres privés et les types encapsulés (i.e. Nested types) ne sont pas retournées.

Avec Binding.FlattenHierarchy, il n’est pas nécessaire de parcourir récursivement les propriétés de la classe et des classes mère car ce paramètre renvoie toutes les propriétés public et protected héritées.

Pour parcourir les données membres privés, on utilise les BindingFlags BindingFlags.Instance et Bindings.NonPublic et on filtre ensuite pour ne garder que les membres privés. Le parcours doit s’effectuer sur la classe mais aussi récursivement sur les membres privés des classes mère.

On peut effectuer le parcours des membres avec les méthodes suivantes:

private static readonly MethodInfo memberwiseCloneMethod = 
    typeof(Object).GetMethod("MemberwiseClone", BindingFlags.NonPublic |  
        BindingFlags.Instance); 
 
public static Object Copy(Object objectToCopy) 
{ 
    return InternalCopy(objectToCopy); 
} 
 
private static Object InternalCopy(Object objectToCopy) 
{ 
    var objectTypeToCopy = objectToCopy.GetType(); 
    var cloneObject = memberwiseCloneMethod.Invoke(objectToCopy, null); 
 
    CopyNonPrivateMembers(objectTypeToCopy, objectToCopy, cloneObject); 
    CopyPrivateFieldsForObjectBaseType(objectTypeToCopy, objectToCopy, cloneObject); 
 
    return cloneObject; 
} 
 
private static void CopyPrivateFieldsForObjectBaseType(Type objectTypeToCopy, 
    object objectToCopy, object objectCopy) 
{ 
    if (objectTypeToCopy.BaseType != null) 
    { 
        CopyPrivateFieldsForObjectBaseType(objectTypeToCopy.BaseType, objectToCopy, objectCopy); 
        CopyPrivateMembers(objectTypeToCopy.BaseType, objectToCopy, objectCopy); 
    } 
} 
 
private static void CopyNonPrivateMembers(Type objectTypeToCopy, 
    object objectToCopy, object objectCopy) 
{ 
    BindingFlags nonPrivateMemberFlags = BindingFlags.Instance | BindingFlags.NonPublic |  
        BindingFlags.Public | BindingFlags.FlattenHierarchy; 
    CopyMembers(objectTypeToCopy, objectToCopy, objectCopy, nonPrivateMemberFlags, false); 
} 
 
private static void CopyPrivateMembers(Type objectTypeToCopy, object objectToCopy, 
    object objectCopy) 
{ 
    BindingFlags privateMemberFlags = BindingFlags.Instance | BindingFlags.NonPublic; 
    CopyMembers(objectTypeToCopy, objectToCopy, objectCopy, privateMemberFlags, true); 
} 
 
private static void CopyMembers(Type objectTypeToCopy, object objectToCopy, 
    object objectCopy, BindingFlags bindingFlags, bool getPrivateMembers) 
{ 
    foreach (FieldInfo fieldInfo in objectTypeToCopy.GetFields(bindingFlags)) 
    { 
        if (getPrivateMembers && !fieldInfo.IsPrivate) continue; 
        var originalFieldValue = fieldInfo.GetValue(objectToCopy); 
        var clonedFieldValue = InternalCopy(originalFieldValue); 
        fieldInfo.SetValue(objectCopy, clonedFieldValue); 
    } 
}
Implémentation complète d’une duplication par reflection

On peut trouver une implémentation complète de la duplication par reflection dans le projet net-object-deep-copy de Burtsev Alexey sur Github.

Sérialisation

La duplication d’objets en utilisant la sérialisation se fait en deux temps:

  • On sérialise l’objet à dupliquer
  • Puis on désérialise l’objet sérialisé.

On obtient alors une copie en profondeur (i.e. deep copy) de l’objet d’origine. Pour utiliser cette méthode, il faut que la classe à dupliquer soit sérialisable et qu’elle soit décorée avec l’attribut: [Serializable].

Il est possible d’utiliser tous les types de “serializer” (binaire, Soap, JSON, XML, etc…). Toutefois pour que la duplication soit rapide et provoque le moins d’erreur possible, utiliser le sérialisation binaire permet d’avoir un bon compromis.

L’intérêt de cette méthode est qu’elle est très simple à mettre en œuvre et son implémentation est rapide. En revanche, c’est la méthode de duplication la plus lente.

Sérialisation simple

L’implémentation la plus simple de la sérialisation consiste à sérialiser tout l’objet.

Une duplication par serialisation binaire peut s’implémenter de cette façon:

using System.IO; 
using System.Runtime.Serialization; 
using System.Runtime.Serialization.Formatters.Binary; 
 
public static T Clone<T>(T objectToCopy) 
{ 
    if (!typeof(T).IsSerializable) 
    { 
        throw new InvalidOperationException("The type shall be serializable."); 
    } 
 
    // Don't serialize a null object, simply return the default for that object 
    if (Object.ReferenceEquals(objectToCopy, null)) 
    { 
        return default(T); 
    } 
 
    IFormatter formatter = new BinaryFormatter(); 
    using (Stream stream = new MemoryStream()) 
    { 
        formatter.Serialize(stream, objectToCopy); 
        stream.Seek(0, SeekOrigin.Begin); 
        return (T)formatter.Deserialize(stream); 
    } 
}

Cette méthode peut s’avérer compliquée à mettre en œuvre si on ne maîtrise pas tous les types des membres de l’objet à dupliquer en particulier s’ils ne sont pas sérialisables.

Serialization surrogate

Comme indiqué plus haut, la sérialisation est parfois impossible car certains membres de l’objet à dupliquer peuvent ne pas être sérialisables. Dans ce cas, on souhaiterait avoir un comportement différent suivant si un membre est sérialisable ou non.

La solution consiste à utiliser un serialization surrogate (i.e. “assistant de sérialisation”). Un serialization surrogate doit satisfaire System.Runtime.Serialization.ISerializationSurrogate:

  • GetObjectData(): permet d’indiquer dans un objet de type SerializationInfo les données nécessaires pour sérialiser l’objet.
  • SetObjectData(): utilise les données présentes dans un objet de type SerializationInfo pour construire l’objet.

Si on considère les objets suivants:

public class Car 
{ 
    public string Brand { get; set; } 
    public string ModelName { get; set; } 
    public string Reference { get; set; } 
    public decimal Price { get; set; } 
    public Engine Engine { get; set; } 
    public Person Owner { get; set; } 
} 
public class Engine 
{ 
    public int Power { get; set; } 
    public string Reference { get; set; } 
    public string SerialNumber { get; set; } 
} 
public class Person 
{ 
    public string FirstName { get; set; } 
    public string LastName { get; set; } 
}

On peut définir des serialization surrogates particuliers pour la classe Car et la classe Engine:
CarSerializationSurrogate: il assiste pour la sérialisation de la classe Car.
EngineSerializationSurrogate: il assiste pour la sérialisation de la classe Engine.
Il n’y a pas de serialization surrogate pour la classe Person. D’autre part, on n’implémente rien de particulier pour cette classe.

L’implémentation des serialization surrogates est:

public class CarSerializationSurrogate : ISerializationSurrogate 
{ 
    private const string BrandValueName = "Brand"; 
    private const string ModelNameValueName = "ModelName"; 
    private const string ReferenceValueName = "Reference"; 
    private const string PriceValueName = "Price"; 
    private const string EngineValueName = "Engine";

    public void GetObjectData(object obj, SerializationInfo info, StreamingContext context) 
    { 
        Car car = (Car)obj; 
        info.AddValue(BrandValueName, car.Brand); 
        info.AddValue(ModelNameValueName, car.ModelName); 
        info.AddValue(ReferenceValueName, car.Reference); 
        info.AddValue(PriceValueName, car.Price); 
        info.AddValue(EngineValueName, car.Engine); 
    } 
 
    public object SetObjectData(object obj, SerializationInfo info, 
       StreamingContext context, ISurrogateSelector selector) 
    { 
        Car car = (Car)obj; 
        car.Brand = info.GetString(BrandValueName); 
        car.ModelName = info.GetString(ModelNameValueName); 
        car.Reference = info.GetString(ReferenceValueName); 
        car.Price = info.GetDecimal(PriceValueName); 
        car.Engine = (Engine)info.GetValue(EngineValueName, typeof(Engine)); 
        return car; 
    } 
} 
 
public class EngineSerializationSurrogate : ISerializationSurrogate 
{ 
    private const string PowerValueName = "Power"; 
    private const string ReferenceValueName = "Reference"; 
    private const string SerialNumberValueName = "SerialNumber";

    public void GetObjectData(object obj, SerializationInfo info, StreamingContext context) 
    { 
        Engine engine = (Engine)obj; 
        info.AddValue(PowerValueName, engine.Power); 
        info.AddValue(ReferenceValueName, engine.Reference); 
        info.AddValue(SerialNumberValueName, engine.SerialNumber); 
    } 
 
    public object SetObjectData(object obj, SerializationInfo info, 
        StreamingContext context, ISurrogateSelector selector) 
    { 
        Engine engine = (Engine)obj; 
        engine.Power = info.GetInt32(PowerValueName); 
        engine.Reference = info.GetString(ReferenceValueName); 
        engine.SerialNumber = info.GetString(SerialNumberValueName); 
        return engine; 
    } 
}

Pour prendre en compte les serialization surrogates lors de la sérialisation:

public class Serializer 
{ 
    public static Car CloneWithSerializationSurrogates(Car carToCopy) 
    { 
        var surrogateSelector = new SurrogateSelector(); 
        surrogateSelector.AddSurrogate(typeof(Car), 
            new StreamingContext(StreamingContextStates.All), 
            new CarSerializationSurrogate()); 
        surrogateSelector.AddSurrogate(typeof(Engine), 
            new StreamingContext(StreamingContextStates.All), 
            new EngineSerializationSurrogate());

        IFormatter formatter = new BinaryFormatter(); 
        formatter.SurrogateSelector = surrogateSelector;

        using (Stream stream = new MemoryStream()) 
        { 
            formatter.Serialize(stream, carToCopy); 
            stream.Seek(0, SeekOrigin.Begin); 
            return (Car)formatter.Deserialize(stream); 
        } 
    }
}

Si on exécute le code suivant:

var originalCar = new Car { 
    Brand = "Ford", ModelName = "Mustang", Reference = "GT",  
    Price = 3000m,  
    Engine = new Engine { Power = 300, Reference = "BigEngine",   
        SerialNumber = "FE34F3" }, 
    Owner = new Person { FirstName = "Paul", LastName = "Ryan" } 
}; 
Car duplicatedCar = Serializer.CloneWithSerializationSurrogates(originalCar);

Tous les membres de la classe originalCar seront sérialisés et correctement dupliqués à l’exception du membre Owner puisqu’on a omis la prise en compte de ce membre dans les fonctions CarSerializationSurrogate.GetObjectData() et CarSerializationSurrogate.SetObjectData().

Serialization surrogate selector

L’implémentation précédente permettait de choisir le serialization surrogate à utiliser suivant le type d’objet à sérialiser. Il est toutefois possible d’avoir une implémentation particulière quant au choix du serialization surrogate à utiliser, ce choix se fait un serialization surrogate selector (i.e. “sélectionneur d’assistant de sérialisation”).

Un serialization surrogate selector doit satisfaire l’interface System.Runtime.Serialization.ISurrogateSelector.

Les surrogate selectors sont organisés en chaîne, c’est-à-dire si un surrogate selector ne peut pas sélectionner un serialization surrogate alors il fournit un autre surrogate selector qui sera sollicité pour sélectionner un serialization surrogate et ainsi de suite. L’interface ISurrogateSelector permet d’organiser cette chaîne:

  • ChainSelector(): permet d’indiquer une instance d’un surrogate selector qui sera le suivant dans la chaine si le surrogate selector actuel ne peut sélectionner un serialization surrogate.
  • GetNextSelector(): retourne le surrogate selector suivant dans la chaîne. S’il n’y a pas de surrogate selector suivant, alors cette fonction renvoie null.
  • GetSurrogate(): fournit le serialization surrogate qui assistera la sérialisation. S’il le surrogate selector ne sélectionne pas de serialization surrogate alors cette fonction renvoie null.

Une implémentation intéressante d’un surrogate selector serait de fournir un serialization surrogate particulier seulement si le membre est sérialisable.

Ainsi, dans le cas où l’objet à sérialiser est de type System.String, un type primitif ou si il est sérialisable, on ne fournit pas de serialization surrogate. Le comportement est normal et les objets sont sérialisés. Dans le cas où les objets ne sont pas sérialisables et s’ils sont des classes ou des structures alors on fournit un serialization surrogate particulier.

L’implémentation du surrogate selector est:

public class SurrogateSelectorForNonSerialiazableType : ISurrogateSelector 
{ 
    private ISurrogateSelector nextSelector; 
    private ISerializationSurrogate serializationSurrogateForNonSerializableType; 
    public SurrogateSelectorForNonSerialiazableType( 
        ISerializationSurrogate serializationSurrogateForNonSerializableType) 
    { 
        this.serializationSurrogateForNonSerializableType = 
            serializationSurrogateForNonSerializableType; 
    } 
    #region ISurrogateSelector members 
    public void ChainSelector(ISurrogateSelector selector) 
    { 
        this.nextSelector = selector; 
    } 
    public ISurrogateSelector GetNextSelector() 
    { 
        return this.nextSelector; 
    } 
    public ISerializationSurrogate GetSurrogate(Type type, StreamingContext context, 
        out ISurrogateSelector selector) 
    { 
        selector = null; 
        ISerializationSurrogate surrogate = null; 
        if (!IsKnownType(type) && (type.IsClass || type.IsValueType)) 
        { 
            selector = this; 
            surrogate = this.serializationSurrogateForNonSerializableType; 
        } 
        return surrogate; 
    } 
    #endregion 
    private static bool IsKnownType(Type type) 
    { 
        return type == typeof(string) || type.IsPrimitive 
            || type.IsSerializable; 
    } 
}

Une implémentation du serialization surrogate pourrait être:

public class SerializationSurrogateForNonSerializableType : ISerializationSurrogate 
{ 
    private static IEnumerable<FieldInfo> GetSerializableFieldInfos(object obj) 
    { 
        return obj.GetType().GetFields(BindingFlags.Instance | 
            BindingFlags.Public | BindingFlags.NonPublic) 
            .Where(fi => fi.FieldType == typeof(string) || fi.FieldType.IsPrimitive || 
                fi.FieldType.IsSerializable || fi.FieldType.IsClass); 
    }

    public void GetObjectData(object obj, SerializationInfo info, StreamingContext context) 
    { 
        foreach (var fi in GetSerializableFieldInfos(obj)) 
        { 
            info.AddValue(fi.Name, fi.GetValue(obj)); 
        } 
    }

    public object SetObjectData(object obj, SerializationInfo info, StreamingContext context, 
        ISurrogateSelector selector) 
    { 
        foreach (var fi in GetSerializableFieldInfos(obj)) 
        { 
            fi.SetValue(obj, info.GetValue(fi.Name, fi.FieldType)); 
        } 
        return obj; 
    } 
}

La duplication est effectuée en exécutant:

public static Car Clone(Car carToCopy) 
{ 
     
    IFormatter formatter = new BinaryFormatter(); 
    formatter.SurrogateSelector = new SurrogateSelector(); 
    formatter.SurrogateSelector.ChainSelector( 
        new SurrogateSelectorForNonSerialiazableType( 
            new SerializationSurrogateForNonSerializableType())); 
    using (Stream stream = new MemoryStream()) 
    { 
        formatter.Serialize(stream, carToCopy); 
        stream.Seek(0, SeekOrigin.Begin); 
        return (Car)formatter.Deserialize(stream); 
    } 
}
Implémentation complête d’une méthode de duplication par sérialisation:

On peut trouver une implémentation plus complète de la duplication par sérialisation en utilisant un surrogate selector dans A Generic Method for Deep Cloning in C# 3.0.

Expression Trees

Il existe plusieurs exemples de code de duplication générique en utilisant des arbres d’expressions (i.e. expression trees).

L’intérêt des expression trees est de pouvoir construire dynamiquement une suite d’instructions. On peut ensuite, compiler cette suite d’instructions en code IL afin de l’exécuter.

La méthode duplication par reflection décrite plus haut s’exécute en découvrant les types et caractéristiques des membres de l’objet à dupliquer. De façon à effectuer une copie en profondeur de l’objet (i.e. deep copy), l’algorithme va parcourir récursivement ses membres afin de les dupliquer. S’il y a des duplications à effectuer sur 10 objets de même type, l’algorithme parcourera l’arbre des 10 objets de la même façon. Parcourir les arbres d’objets par reflection est couteux en performance. Dans le cas où un type d’objet est connu, une optimisation pourrait être de copier la valeur des membres directement avec leur nom et non en parcourant systématiquement tous les membres.

Utiliser les expression trees permet d’optimiser la méthode de duplication par reflection puisqu’on peut générer une expression effectuant la duplication pour chaque type d’objet. On peut ensuite compiler cette expression et l’exécuter à chaque fois qu’on détecte un objet de même type. La duplication est ainsi plus rapide puisqu’on exécute directement du code IL et ensuite, on ne fait un parcours systématique de tous les membres de l’objet. La duplication se fait en copiant les valeurs avec les noms des membres directement.

Les méthodes de duplication par expression tree, sont plus rapides que les méthodes utilisant la reflection pure et la sérialisation à partir du moment où les expressions ont été générées. Ces méthodes sont intéressantes lorsqu’il y a beaucoup d’objets de même type à dupliquer.

La contrepartie de l’utilisation des expression trees est qu’elles sont difficilement débugables et que le code pour générer les expressions est complexes.

Comme pour la méthode par reflection, la première étape de la duplication par expression trees est d’appeler la copie superficielle avec Object.MemberwiseClone(). Ainsi à titre d’exemple, l’exécution de cette méthode par reflection se fait de la façon suivante:

MethodInfo memberwiseCloneMethod = typeof(Object).GetMethod("MemberwiseClone",
    BindingFlags.Instance | BindingFlags.NonPublic); 
 
ExampleClass output = (ExampleClass)memberwiseCloneMethod.Invoke(input, null);

En utilisant les expression trees, l’implémentation devient:

ParameterExpression inputParameter = Expression.Parameter(
    typeof(ExampleClass)); 
ParameterExpression outputParameter = Expression.Parameter(typeof(ExampleClass)); 
 
MethodInfo memberwiseCloneMethod = typeof(Object).GetMethod("MemberwiseClone", 
    BindingFlags.Instance | BindingFlags.NonPublic); 
 
Expression lambdaExpression = Expression.Assign(outputParameter, 
    Expression.Convert(
        Expression.Call(inputParameter, memberwiseCloneMethod), 
        typeof(ExampleClass)));

Ainsi l’intérêt est qu’après compilation de l’expression tree, l’exécution est plus rapide puisqu’il n’y a plus l’étape de reflection pour récupérer les informations sur la méthode Object.MemberwiseClone().

D’une façon générale, tous les traitements effectués pour la méthode de duplication par reflection sont adaptées de la même façon pour utiliser les expression trees. Il faut donc être familié, dans un premier temps, avec les implémentations de duplication par reflection pour comprendre plus facilement les implémentation avec les expression trees.

Quelques implémentations de duplication avec des expression trees:

L’implémentation suivante de la duplication par expression trees: Fast Deep Copy by expression trees (C#) s’inspire de cette méthode de duplication par reflection: https://github.com/Burtsev-Alexey/net-object-deep-copy.

Il existe d’autres implémentations de la duplication par expression trees: FastClone, Fast Deep Cloning ou le projet CloneExtensions sur GitHub.

Tester la méthode de duplication

Avant de procéder à l’implémentation d’une méthode pour dupliquer, une bonne approche est d’implémenter un test qui permettrait de vérifier que:

  • La duplication produit bien une instance distincte de même type,
  • Les données membres de l’objet d’origine sont correctement dupliquées.

Dans le cas d’une méthode générique, il faut vérifier le comportement pour la duplication:

  • De membres privés et publics
  • De membres readonly
  • Des classes et des structures
  • Dans le cas d’héritage
  • Des tableaux d’objets
Références:

Duplication par reflection:
Project net-object-deep-copy sur Github: https://github.com/Burtsev-Alexey/net-object-deep-copy

Duplication par sérialisation binaire:

Duplication avec des expression trees:

Environnement partial trust:

Autres:

Leave a Reply