Let There Be Code RSS 2.0
# Friday, March 04, 2011

Avec l’un de mes collègues, nous avons récemment eu un débat autour de cette question, que nous avons fini par élucider. Voici le contexte : nous avons une classe A et une classe B, aucun héritage n’existe entre ces 2 classes, par contre nous redéfinissons l’opérateur de cast explicite de cette manière :

   1:  public class B
   2:  {
   3:  }
   4:   
   5:  public class A
   6:  {
   7:      public static explicit operator B(A a)
   8:      {
   9:          return new B();
  10:      }
  11:  }

Une fois que nous avons défini ces 2 classes, il est tout à fait possible d’effectuer un cast entre un objet A et un objet B :

   1:  A a = new A();
   2:   
   3:  B b = (B)a;

 

Maintenant que ce passe-t-il si on instancie une liste d’objets A et que l’on appelle la méthode d’extension Cast<T> comme ceci :

   1:  var list = new List<A>() { new A(), new A() };
   2:  list.Cast<B>().ToList();

 

Et bien à l’exécution, notre opérateur de cast explicite n’est pas appelé. Par contre une exception InvalidCastException est levée. La première réponse que l’on m’a donné était de dire que la méthode d’extension Cast<T> ne fait pas un cast…

Utilisons notre outil préféré (ou presque puisqu’il va bientôt devenir payant… ;)) Reflector. En reflectant la méthode Cast<T>, on peut voir qu’elle fait appel à la classe CastIterator. Celle-ci est tout simplement un itérateur sur notre collection qui dans la méthode MoveNext affecte à l’objet courant l’objet casté :

image

Donc oui la méthode Cast<T> fait un cast… d’après le code C#.  Car en fait, plus précisément, elle fait une opération de type unbox.any, comme le montre le code IL suivant :

image

C’est justement là qu’est notre problème. La classe CastIterator ne connait pas notre type A et elle itère sur une liste de type IEnumerable. Donc pour la classe CastIterator, nos objets sont de type System.Object. A cet endroit, un cast est effectué entre un objet de type System.Object et un type TResult. Du coup il fait un unbox (équivalent à l’opérateur castclass).

Décompilons maintenant le code écrit au tout début de ce post, qui caste notre variable de type A en type B :

image

Nous voyons bien que le compilateur a trouvé notre opérateur explicite et donc l’appelle pour effectuer la conversion.

Pour reproduire ce qu’il se passe au niveau du CastIterator, il suffit d’écrire ceci :

   1:  A a = new A();
   2:   
   3:  object o = a;
   4:   
   5:  B b = (B)o;

Et ici, le compilateur fait appel à l’opérateur castclass et non à notre opérateur de cast explicite :

image

Pour palier ce problème nous avons 2 solutions. La 1ère consiste à effectuer le cast soit même dans un Select :

   1:  var list = new List<A>() { new A(), new A() };
   2:  var result = list.Select(a => (B) a);

La seconde solution consiste a utiliser le mécanisme de Reflection pour retrouver l’existence de l’opérateur. On peut ainsi définir la méthode d’extension suivante :

   1:  public static class EnumarableExtensions
   2:  {
   3:      public static MethodInfo GetMethod(Type toSearch, string methodName, Type returnType, BindingFlags bindingFlags)
   4:      {
   5:          return Array.Find(toSearch.GetMethods(bindingFlags), delegate(MethodInfo inf) { return ((inf.Name == methodName) && (inf.ReturnType == returnType)); });
   6:      }
   7:   
   8:      public static IEnumerable<T> DynamicCast<T>(this IEnumerable list)
   9:      {
  10:          foreach (var obj in list)
  11:          {
  12:              Type ot = obj.GetType();
  13:              MethodInfo meth = GetMethod(ot, "op_Implicit", typeof(T), BindingFlags.Static | BindingFlags.Public);
  14:   
  15:              if (meth == null)
  16:              {
  17:                  meth = GetMethod(ot, "op_Explicit", typeof(T), BindingFlags.Static | BindingFlags.Public);
  18:              }
  19:   
  20:              if (meth == null)
  21:                  yield return (T)obj;
  22:              else
  23:                  yield return (T)meth.Invoke(null, new[] { obj });
  24:          }
  25:   
  26:      }
  27:  }

Et notre appel se fait ainsi :

   1:  var list = new List<A>() { new A(), new A() };
   2:  var result = list.DynamicCast<B>();

 

Et voilà un mystère d’élucidé! :)

Friday, March 04, 2011 7:00:00 AM (Romance Standard Time, UTC+01:00)  #    Voir Commentaires
C#
Archive
<March 2011>
SunMonTueWedThuFriSat
272812345
6789101112
13141516171819
20212223242526
272829303112
3456789
About the author/Disclaimer

Disclaimer
The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.

© Copyright 2012
Benoît Laut
Sign In
All Content © 2012, Benoît Laut
DasBlog theme 'Business' created by Christoph De Baene (delarou)