sabato 1 dicembre 2012

Object Cloning usando IL in C#


una delle cose più fastidiose che succede spesso nella programmazione in c# è quando ci si imbatte in un oggetto da modificare mantenendo in copia l'originale.
Questa difficoltà si manifesta poiché quasi sempre gli oggetti riassegnati vengono legati all'oggetto sorgente per riferimento*. 

Per risolvere questo noioso problema solitamente nei nostri progetti implementiamo dei metodi di "Clone" ossia dei metodi che ci copiano gli oggetti ( veriabili, classi, e quant'altro ) su un altro contenitore non collegato al primo.

A volte però ci sono oggetti per cui abbiamo grosse difficoltà perché magari non sono serializzabili o altro ... ma ecco che vi posto un po di metodologie che vi aiuteranno nella quasi totalità dei casi di Clonazione. 

IL (Intermediate Language) può essere utilizzato per clonare oggetti e non è affatto male, e può essere abbastanza performante. 

definiamo una Classe Person : 


  public class Person 
  { 
  private int _id; 
  private string _name; 
  private string _firstName; 
  private string _field1, _field2, _field3; 

  public Person() 
  { 

  } 

  public int ID 
  { 
  get { return _id; } 
  set { _id = value ; } 
  } 

  public string Name 
  { 
  get { return _name; } 
  set { _name = value ; } 
  } 

  public string FirstName 
  { 
  get { return _firstName; } 
  set { _firstName = value ; } 
  } 
  } 




Test Code: Il codice riportato di seguito è un bel pezzo di codice, leggete i commenti e capirete cosa vi è dichiarato.


  class Program 
  { 
  /// <summary> 
  /// Delegate handler that's used to compile the IL to. 
  /// (This delegate is standard in .net 3.5) 
  /// </summary> 
  /// <typeparam name="T1">Parameter Type</typeparam> 
  /// <typeparam name="TResult">Return Type</typeparam> 
  /// <param name="arg1">Argument</param> 
  /// <returns>Result</returns> 
  public delegate TResult Func<T1, TResult>(T1 arg1); 
  /// <summary> 
  /// This dictionary caches the delegates for each 'to-clone' type. 
  /// </summary> 
  static Dictionary<Type, Delegate> _cachedIL = new Dictionary<Type, Delegate>(); 

  /// <summary> 
  /// The Main method that's executed for the test. 
  /// </summary> 
  /// <param name="args"></param> 
  static void Main( string [] args) 
  { 
  DoCloningTest(1000); 
  DoCloningTest(10000); 
  DoCloningTest(100000); 
  //Commented because the test takes long ;) 
  //DoCloningTest(1000000); 

  Console.ReadKey(); 
  } 

  /// <summary> 
  /// Do the cloning test and printout the results. 
  /// </summary> 
  /// <param name="count">Number of items to clone</param> 
  private static void DoCloningTest( int count) 
  { 
  // Create timer class. 
  HiPerfTimer timer = new HiPerfTimer(); 
  double timeElapsedN = 0, timeElapsedR = 0, timeElapsedIL = 0; 

  Console.WriteLine( "--> Creating {0} objects..." , count); 
  timer.StartNew(); 
  List<Person> personsToClone = CreatePersonsList(count); 
  timer.Stop(); 
  Person temp = CloneObjectWithIL(personsToClone[0]); 
  temp = null ; 
  Console.WriteLine( "\tCreated objects in {0} seconds" , timer.Duration); 

  Console.WriteLine( "- Cloning Normal..." ); 
  List<Person> clonedPersons = new List<Person>(count); 
  timer.StartNew(); 
  foreach (Person p in personsToClone) 
  { 
  clonedPersons.Add(CloneNormal(p)); 
  } 
  timer.Stop(); 
  timeElapsedN = timer.Duration; 

  Console.WriteLine( "- Cloning IL..." ); 
  clonedPersons = new List<Person>(count); 
  timer.StartNew(); 
  foreach (Person p in personsToClone) 
  { 
  clonedPersons.Add(CloneObjectWithIL<Person>(p)); 
  } 
  timer.Stop(); 
  timeElapsedIL = timer.Duration; 

  Console.WriteLine( "- Cloning Reflection..." ); 
  clonedPersons = new List<Person>(count); 
  timer.StartNew(); 
  foreach (Person p in personsToClone) 
  { 
  clonedPersons.Add(CloneObjectWithReflection(p)); 
  } 
  timer.Stop(); 
  timeElapsedR = timer.Duration; 

  Console.WriteLine(); 
  Console.ForegroundColor = ConsoleColor.Green; 
  Console.WriteLine( "----------------------------------------" ); 
  Console.WriteLine( "Object count:\t\t{0}" , count); 
  Console.WriteLine( "Cloning Normal:\t\t{0:00.0000} s" , timeElapsedN); 
  Console.WriteLine( "Cloning IL:\t\t{0:00.0000} s" , timeElapsedIL); 
  Console.WriteLine( "Cloning Reflection:\t{0:00.0000} s" , timeElapsedR); 
  Console.WriteLine( "----------------------------------------" ); 
  Console.ResetColor(); 
  } 

  /// <summary> 
  /// Create a list of persons with random data and a given number of items. 
  /// </summary> 
  /// <param name="count">Number of persons to generate</param> 
  /// <returns>List of generated persons</returns> 
  private static List<Person> CreatePersonsList( int count) 
  { 
  Random r = new Random(Environment.TickCount); 
  List<Person> persons = new List<Person>(count); 
  for ( int i = 0; i < count; i++) 
  { 
  Person p = new Person(); 
  p.ID = r.Next(); 
  p.Name = string .Concat( "Slaets_" , r.Next()); 
  p.FirstName = string .Concat( "Filip_" , r.Next()); 
  persons.Add(p); 
  } 
  return persons; 
  } 

  /// <summary> 
  /// Clone one person object with reflection 
  /// </summary> 
  /// <param name="p">Person to clone</param> 
  /// <returns>Cloned person</returns> 
  private static Person CloneObjectWithReflection(Person p) 
  { 
  // Get all the fields of the type, also the privates. 
  FieldInfo[] fis = p.GetType().GetFields(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic); 
  // Create a new person object 
  Person newPerson = new Person(); 
  // Loop through all the fields and copy the information from the parameter class 
  // to the newPerson class. 
  foreach (FieldInfo fi in fis) 
  { 
  fi.SetValue(newPerson, fi.GetValue(p)); 
  } 
  // Return the cloned object. 
  return newPerson; 
  } 

  /// <summary> 
  /// Generic cloning method that clones an object using IL. 
  /// Only the first call of a certain type will hold back performance. 
  /// After the first call, the compiled IL is executed. 
  /// </summary> 
  /// <typeparam name="T">Type of object to clone</typeparam> 
  /// <param name="myObject">Object to clone</param> 
  /// <returns>Cloned object</returns> 
  private static T CloneObjectWithIL<T>(T myObject) 
  { 
  Delegate myExec = null ; 
  if (!_cachedIL.TryGetValue( typeof (T), out myExec)) 
  { 
  // Create ILGenerator 
  DynamicMethod dymMethod = new DynamicMethod( "DoClone" , typeof (T), new Type[] { typeof (T) }, true ); 
  ConstructorInfo cInfo = myObject.GetType().GetConstructor( new Type[] { }); 

  ILGenerator generator = dymMethod.GetILGenerator(); 

  LocalBuilder lbf = generator.DeclareLocal( typeof (T)); 
  //lbf.SetLocalSymInfo("_temp"); 

  generator.Emit(OpCodes.Newobj, cInfo); 
  generator.Emit(OpCodes.Stloc_0); 
  foreach (FieldInfo field in myObject.GetType().GetFields(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic)) 
  { 
  // Load the new object on the eval stack...  (currently 1 item on eval stack) 
  generator.Emit(OpCodes.Ldloc_0); 
  // Load initial object (parameter) (currently 2 items on eval stack) 
  generator.Emit(OpCodes.Ldarg_0); 
  // Replace value by field value (still currently 2 items on eval stack) 
  generator.Emit(OpCodes.Ldfld, field); 
  // Store the value of the top on the eval stack into the object underneath that value on the value stack. 
  // (0 items on eval stack) 
  generator.Emit(OpCodes.Stfld, field); 
  } 

  // Load new constructed obj on eval stack -> 1 item on stack 
  generator.Emit(OpCodes.Ldloc_0); 
  // Return constructed object.  --> 0 items on stack 
  generator.Emit(OpCodes.Ret); 

  myExec = dymMethod.CreateDelegate( typeof (Func<T, T>)); 
  _cachedIL.Add( typeof (T), myExec); 
  } 
  return ((Func<T, T>)myExec)(myObject); 
  } 

  /// <summary> 
  /// Clone a person object by manually typing the copy statements. 
  /// </summary> 
  /// <param name="p">Object to clone</param> 
  /// <returns>Cloned object</returns> 
  private static Person CloneNormal(Person p) 
  { 
  Person newPerson = new Person(); 
  newPerson.ID = p.ID; 
  newPerson.Name = p.Name; 
  newPerson.FirstName = p.FirstName; 
  return newPerson; 
  } 
  } 

La cosa fondamentale che fa è, creare un metodo dinamico, ottenere il ILGenerator, creare codice nel metodo, compilarlo per un delegato, ed eseguire il delegato. 

Il delegato è in cache cosi che l'IL non viene generato ogni volta che una clonazione dovrebbe avvenire, quindi perdiamo un solo poco tempo, quando il primo oggetto viene clonato (IL deve essere creato e compilato in fase di esecuzione). 

Speriamo che questo articolo sia utile per alcune persone se è così, fatemelo sapere. 

Saluti

Nessun commento:

Posta un commento