dimanche 10 avril 2011

Je me devais de faire part de la découverte suivante.

En C#, l'opérateur as permet de combiner deux opérations communes: tester si un objet est convertible en un type, et effectuer la conversion à proprement parler. Si la conversion échoue, ou si la référence est déjà nulle, as retourne null. Ainsi:


var val = ...;

// les deux bouts suivants sont équivalents:
var covt1 = val as Type;
var covt2 = val is Type ? (Type)val : null;


Un bon exemple serait une redéfinition de la méthode Equals:

class T
{
// ...

public override bool Equals(object obj)
{
var other = obj as T;
if(other == null)
return false; // couvre obj == null et !(obj is T)

// utiliser other pour la comparaison
}
}


En ce sens, c'est un équivalent direct du dynamic_cast de C++, lorsque celui-ci est utilisé sur des pointeurs.

Il y a néanmoins un défaut avec ce opérateur: as retourne null quand la conversion échoue, et null n'est pas une valeur valide pour les types valeur (les struct). On ne peut donc pas utiliser as pour caster un struct.

En pratique, ce n'est pas trop problématique: les structs héritent tous de System.ValueType, sont implicitement sealed, et ne peuvent donc pas faire partie d'une hiérarchie de classe. Les downcastings de ce genre sont rares.

Mais il ne faut pas oublier un détail: un struct hérite également de System.Object, et il possible de faire un downcasting de object à un struct (une procédure appelée "unboxing"). Pour ces cas, donc, j'en étais réduit à l'approche verbeuse (is + cast).

Or, j'ai récemment découvert quelque chose d'intéressant: il est possible d'utiliser as avec un type valeur si celui-ci est nullable:

struct V
{
// ...

public override bool Equals(object obj)
{
var other = obj as V?; // le ? est un raccourci pour Nullable<V>
if(!other.HasValue)
return false;

// utiliser other.Value pour la comparaison
}
}


MSDN n'en parle pas, mais c'est très intéressant. Testé avec C# 4.0, donc ce n'est peut-être pas disponible pour les versions précédentes. Nullable ayant été introduit en .NET 2.0, il est certain que ça ne fonctionnera pas avant.

3 commentaires:

Patrice Roy a dit…

Les trucs comme «as» ou «instanceOf» ont leur utilité pour combler des manques structurels (faut être pragmatique dans la vie, après tout; parfois, «patcher» un peu, c'est pratique) mais j'aurais aimé connaître ton avis sur les raisons ou non d'utiliser ces trucs dans un design pensé comme tel a priori. À ton avis, dans un design (pas pour contourner les erreurs du passé ou celles de tiers), ces mécanismes font-ils partie de saines pratiques ou montrent-ils des problèmes structurels plus profonds? Ou, présenté autrement: as-tu des cas (et, idéalement, qui couvrent des langages à héritage simple et à héritage multiple) pour lesquels ces tests dynamiques de conversion potentielle de type font partie d'un design qui, selon toi, est solide?

Etienne de Martel a dit…

Dans un monde idéal, on ne devrait jamais avoir besoin de downcasting. Or, nous sommes loin d'être dans un monde idéal.

Prenons le cas de .NET. Eric Lippert parlait de GetHashCode récemment sur son blogue, et mentionnait que si les generics avaient été présents depuis la version 1, ils auraient pu s'en sortir avec une interface IHashable au lieu de mettre tout dans Object.

On peut tracer un parallèle avec Equals. Il existe l'interface IEquatable<T>, mais beaucoup de vieux code utilise le Equals de Object. Une redéfinition de Equals va donc obligatoirement nécessiter un downcast. Java a le même problème.

Le cast sert donc à contourner les erreurs du passé.

Autre exemple: les GUI. Lorsque je faisais du Qt, j'avais un cas où je devais mettre des formulaires (des objets d'une classe héritant de QFrame) dans un QStackWidget (une liste de contrôles avec un seul de visible à la fois). Puisque j'avais un traitement spécial à faire sur ces objets (les éléments du QStackWidget sont des QWidget *), j'avais deux choix: maintenir un conteneur quelconque en parallèle, ou downcaster. J'ai préféré la deuxième technique, et j'ai caché le cast dans une méthode pour pas trop que ça paraisse.

Le cast sert donc également à contourner les limitations d'un design qui n'est pas le nôtre (ce qui arrive très souvent lorsqu'un utilise un framework graphique).

Donc, un cast est une patch pour les erreurs commises par d'autres... mais est-ce que ce sont vraiment des erreurs?

Le problème de Equals est dû à un manque de temps (faut bien livrer à un moment donné): les generics ne sont arrivés que dans la version 2, et rendu là, il y avait beaucoup de legacy code d'écrit. Et si j'avais à développer un framework graphique, je représenterais probablement les contrôles comme des éléments d'un arbre (ce bon vieux composite pattern). Et ces éléments seraient des pointeurs sur une classe de base quelconque. Je ne vois vraiment pas comment je pourrais m'en tirer à coup de templates...

Tout ça revient au typage statique: il y a de nombreuses situations ou le type dynamique d'un objet doit être différent de son type statique. Le polymorphisme couvre la plupart des cas, mais parfois, il faut passer par des méthodes plus exotiques, et ça ne vaut pas toujours la peine de sortir un visitor pattern quand un cast règle le problème. Je ne dis pas qu'il faut toujours caster: une conversion explicite reste une patch, après tout. Mais il ne faut pas non plus oublier qu'on est payés pour résoudre des problèmes.

Les langages comme Python, Ruby ou Smalltalk évitent le problème en ayant pas de type statique. OCaml, langage au typage extrêmement fort, interdit tout simplement le downcasting.

C'est donc, encore une fois, le traditionnel combat entre la pureté et le pragmatisme. De quel bord on penche dépend de la proximité du deadline...

Patrice Roy a dit…

Allo!

C'est cool (et raisonnable) d'être pragmatique, t'en fais pas :) Je me demandais surtout si t'avais vu un cas où le «downcast» était une bonne idée de design (les patches, tu t.en doutes, je connais aussi).

Bonne journée!

Enregistrer un commentaire