Convertir implicitement des delegates ayant la même signature
Hier soir, je tombe sur un tweet de Krzysztof Koźmic :
Do I care if it’s Predicate<int> or Func<int>? No, than why won’t C# compiler just live me alone and pick whatever it needs? I DONT CARE
Et là je me dis HAHA LOL MAIS N’IMPORTE QUOI Predicate<int>
et Func<int>
c’est très différent voilà une bonne occasion pour moi qui ne suis rien de reprendre gratuitement un des contributeurs principaux de Castle sur une étourderie. Et je lui fais remarquer, en gros :
Pardon, mais en fait, je crois que
Predicate<int>
etFunc<int>
sont des délégués bien différents :)
Et tac ! Bon, évidemment, Krzystzof Kzrysztof Krzysztof pensait bien à Func<int, bool>
qui partage effectivement la même signature que Predicate<int>
. Son commentaire, pertinent, est donc celui d’un programmeur pestant contre le fait que les délégués, en C#, et au niveau même de la CLI, soient des types nominatifs et non structurels.
Considérons les deux types suivants :
class Tweet { public string Message; } class Buzz { public string Message; }
Nous sommes alors ravis de ne pouvoir écrire :
Tweet tweet = new Buzz();
Car même si structurellement, les instances de Tweet
peuvent être considérées compatibles avec les instances de Buzz
, sémantiquement c’est une autre histoire. Les points en coordonnées cartésiennes ne sont pas des points en coordonnées polaires, les chiens ne sont pas des chats, etc.
Dans le CLR, tous les types sont nominatifs, et c’est très bien comme ça. Sauf quand ce n’est pas très bien ; Krzysztof et moi pensons par exemple aux délégués.
Le premier problème est qu’il est impossible de convertir implicitement une instance de delegate en un autre type de delegate structurellement compatible, e.g. : on ne peut pas « convertir » une instance de Func<int, bool>
en Predicate<int>
:
Func<int, bool> func = i => false; // La ligne ci-dessous lève une erreur de compilation : // Cannot implicitly convert type 'System.Func<int,bool>' to 'System.Predicate<int>' Predicate<int> predicate = func;
Cependant, il existe plusieurs façons de contourner le problème, dont une particulièrement acceptable :
EventHandler eventHandler = (s, e) => {}; Action<object, EventArgs> action; action = (s,e) => eventHandler(s,e); // OK, mais lourd. action = new Action<object, EventArgs>(eventHandler); // OK, mais verbeux. action = eventHandler.Invoke; // OK tout court.
Notons que les deux dernières lignes produisent le même code IL, puisqu’on laisse simplement le compilateur générer automatiquement l’appel au constructeur de Action<object, EventArgs>
à partir du method group Invoke
compatible. Quels que soient donc les types de délégués structurellement compatibles avec lesquels on joue, la « conversion », certes explicite, ne coutera que 7 caractères. C’est 133 de moins qu’un tweet.
Le second problème, plus gênant selon moi, est qu’il est impossible aujourd’hui de comparer les références de deux instances de délégués structurellement compatibles mais de types différents, alors que selon la sémantique de l’opérateur d’égalité de Delegate
, ça ne devrait pas être gênant :
Predicate<string> predicate1 = string.IsNullOrEmpty; Predicate<string> predicate2 = string.IsNullOrEmpty; Func<string, bool> func = string.IsNullOrEmpty; if (predicate1 == predicate2) // Vrai Console.WriteLine("predicate1 == predicate2"); if (predicate1.Equals(func)) // Faux Console.WriteLine("predicate1 == func");
Note : Une ancienne version de la spécification C# laisse entendre que c’était possible :
Note that delegates of different types can be considered equal by the above definition, as long as they have the same return type and parameter types.
Mais dans la dernière version spécifie explicitement que c’est impossible :
If the delegates have different runtime type they are never equal.
Bref.
Les plus curieux peuvent alors se demander pourquoi Microsoft n’a pas choisit de faire des delegates des types structurels. Lorsqu’on se pose une question de ce genre — et j’en profite, amis lecteurs, pour vous poser une petite devinette — à qui est-il intéressant de la soumettre :
- Eric Naulleau ?
- Eric Zemmour ?
- Eric Lippert ?
Bravo amis lecteurs, c’est effectivement à M. Zemmour que j’ai envoyé un mail. Voici donc un extrait de sa réponse :
In practice, almost everyone uses delegates structurally, and almost no one makes semantic distinctions between different kinds of delegate types. There’s no semantic reason why Predicate and Func couldn’t be used interchangeably; they are interesting solely for their structure. (Though of course it is nice if a predicate is a pure function!)
If we had a chance to do it all over again, I think we would make delegates into structural types. Also, note that the new « no PIA » feature of C# 4 is essentially structural typing on interfaces. We probably erred a little too far on the side of caution when making all types non-structural in CLR v1, and now we’re gradually weakening that. I would not be too surprised if in some future version of the CLR, it was easier to make structural delegate types, but, no promises. That’s just a speculation.
En deux mots, Eric Lippert (oui car en fait c’est Eric Lippert, hein), m’a répondu qu’il serait tenté de faire passer les delegates du côté des types structurels s’il en avait l’occasion, et qu’il ne serait pas forcément étonné si cela arrivait dans une version future du CLR.
Stay tuned !
Ça a le mérite de pouvoir se débattre. Avant l’arrivée des génériques et des délégués de la famille de Func et d’Action, l’avantage des délégués c’était la sémantique attachés à leur nom.
Imaginons un:
public delegate string Lolcatize (string s);
Et un:
public delegate string Normalize (string s);
Les deux ont une signature équivalente mais une sémantique entièrement différente. Et je n’ai certainement pas envie qu’ils soient égaux devant la loi de l’opérateur ==.
Et pourtant je suis le premier à remplacer des délégués de classe par des Func.
Oui, tout à fait, l’implémentation actuelle des delegates permet d’associer des sémantiques différentes à types de délégués ayant des signatures comparables, ce qui peut parfois être désirable. Autre extrait de la réponse Lippertienne :
Seulement rien n’empêche quelqu’un d’instancier le delegate à partir d’une fonction ou d’une méthode anonyme qui ne respecte cette sémantique…
Aussi, je viens de trouver sur cette page la confirmation qu’il y a bien eu un changement entre les premières versions du framework (1.0 et 1.1) et les versions ultérieures au niveau du comportement de l’opérateur d’égalité :
Je m’étais déjà posé la question effectivement.
Mais j’ai jamais chercher …
Comme dirait JB : « si ils cherchent c’est qu’ils n’ont pas trouvé… »
Merci Romain, je vais pouvoir passer une bonne nuit maintenant :p
Bonjour Romain, ça faisait longtemps.
Je prends toujours autant de plaisir à lire ton blog, et même si la plupart du temps je n’y comprend ABSOLUMENT RIEN, tu me fais bien rire quand même.
Keep going
Ton blog a l’air de passionner, même si moi aussi je n’y comprend rien ^^
Bonne continuation ! c u :)