Pourquoi la méthode d’extension Cast me lève l’exception InvalidCastException ?

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 :

[BuildActivity(HostEnvironmentOption.All)]
public class B
{
}
 
public class A
{
    public static explicit operator B(A a)
    {
        return new B();
    }
}

 

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 :

[BuildActivity(HostEnvironmentOption.All)]
A a = new A();
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 :

[BuildActivity(HostEnvironmentOption.All)]
var list = new List() { new A(), new A() };
list.Cast().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 :

[BuildActivity(HostEnvironmentOption.All)]
A a = new A();
object o = a;
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 :

[BuildActivity(HostEnvironmentOption.All)]
var list = new List() { new A(), new A() };
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 :

[BuildActivity(HostEnvironmentOption.All)]
public static class EnumarableExtensions
{
    public static MethodInfo GetMethod(Type toSearch, string methodName, Type returnType, BindingFlags bindingFlags)
    {
        return Array.Find(toSearch.GetMethods(bindingFlags), delegate(MethodInfo inf) { return ((inf.Name == methodName) && (inf.ReturnType == returnType)); });
    }
 
    public static IEnumerable DynamicCast(this IEnumerable list)
    {
        foreach (var obj in list)
        {
            Type ot = obj.GetType();
            MethodInfo meth = GetMethod(ot, "op_Implicit", typeof(T), BindingFlags.Static | BindingFlags.Public);
 
            if (meth == null)
            {
                meth = GetMethod(ot, "op_Explicit", typeof(T), BindingFlags.Static | BindingFlags.Public);
            }
 
            if (meth == null)
                yield return (T)obj;
            else
                yield return (T)meth.Invoke(null, new[] { obj });
        }
 
    }
}

Et notre appel se fait ainsi :

[BuildActivity(HostEnvironmentOption.All)]
var list = new List() { new A(), new A() };
var result = list.DynamicCast();

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

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s