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é :

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 :

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 :

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 :

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é! :)