C’est un peu un piège, bien entendu, une classe “sealed” on ne peut en hériter... Pourtant le besoin existe. Par exemple une String différenciée. Comment contourner l’interdiction du Framework ? Est-ce possible ?
Pourquoi ?
C’est la première question, et la plus importante peut-être. Pourquoi vouloir créer des types descendant de classes sealed ? En quoi cela peut-il être utile ?
Si je parle d’utilité c’est bien parce que le code doit répondre à cet impératif, tout code sans exception. On code pour faire quelque chose d’utile. Sinon coder n’a pas de sens.
Prenons la classe String. Le Framework ne permet pas de la création de classes en héritant et pour bloquer toute velléité en ce sens, la classe String est celée (sealed). Les concepteurs du Framework ont définitivement fermé cette porte. Mais ils en ont ouvert une autre : les extensions de classe. Cela permet d’étendre les possibilités de toute classe, même sealed, donc de string aussi.
Cela serait parfait si le besoin d’hériter d’une classe se limitait à vouloir lui ajouter des méthodes... Or ce n’est pas la seule motivation envisageable !
Prenons un cas concret : vous créez un logiciel qui pour autoriser la saisie de nombreux paramètres de classes différentes utilise une PropertyGrid (comme celle de Windows Forms qui peut s’utiliser sans problème sous WPF). Au sein d’un tel mécanisme vous pouvez généralement définir vos propres éditeurs personnalisés, qui dépendent du type de la valeur. Par exemple, pour une propriété de type Color vous pourrez écrire un éditeur offrant un nuancier Pantone et une “pipette”. Cela sera plus agréable à vos utilisateur que de taper à l’aveugle un code hexadécimal pour définir une couleur.
Imaginons une seconde que parmi ces paramètres qui seront saisis dans une PropertyGrid (ou son équivalent WPF ou UWP) il se trouve certaines chaines de caractères définissant par exemple le nom d’un fichier externe.
Dans un tel cas vous souhaitez qu’en plus d’un simple éditeur de string s’affiche aussi un petit bouton ellipse “...” qui permettra à l’utilisateur de browser les disques pour directement sélectionner un nom de fichier existant. Peut-être même la zone gèrera-t-elle le drag’n drop depuis l’explorateur.
Hélas... Soit vous enregistrez le nouvel éditeur pour le nom d’une propriété précise (ce qui est très contraignant et source de bogues), soit vous l’enregistrez pour son type, String, et dès lors ce seront toutes les strings qui bénéficieront du browser de fichiers, ce qui n’a aucun sens !
Que ne serait-il pas plus facile de définir juste “public class NomDeFichier : string {} “ et Hop ! l’affaire serait jouée !
L’éditeur serait enregistré pour le type “NomDeFichier”, les noms de fichiers dans les paramètres ne seraient plus de type “string” mais de type “NomDeFichier” et tout irait pour le mieux dans le meilleur des mondes.
Donc voici concrètement un cas qui montre l’utilité évidente de créer des classes héritant de string (ou d’autres classes sealed), même totalement vides, juste pour créer une CLASSification, à la base même de la programmation objet malgré tout...
Je ne doute pas qu’éclairez par cet exemple vous en trouviez d’autres, même totalement différents.
En tout cas nous avons répondu à la première question. C’est utile, et puis la programmation objet se base sur l’héritage pour régler de nombreux problèmes, il y a donc une légitimité naturelle à vouloir hériter d’une classe. “sealed” est un peu frustrant. C’est presque un contre-sens dans un monde objet. La justification du code plus efficace produit par une classe sealed me semble assez artificielle et ne se défendant que difficilement. Mais C# est ainsi fait, la perfection n’existe pas. Heureusement la grande souplesse du langage permet de contourner assez facilement ce genre de problème !
Comment ?
Je vous l’ai déjà dit : ce n’est pas possible, n’insistez pas ! ...
Mais comme ce billet n’existerait pas si je n’avais pas une solution à vous proposer, vous vous dites qu’il doit y avoir un “truc”.
La classe string est sealed. Donc il n’y a pas de “truc” magique. Pas de moyen de bricoler le Framework non plus. Je vous ai dit que ce n’est pas possible !
Mais il y a une solution, un peu moins directe mais tout à fait raisonnable et “propre”.
Elle consiste tout simplement à développer une autre classe qui n’hérite de rien.
Hou là ! Réinventer le type string juste pour une raison de classification semble carrément overkilling !
C’est vrai, et nous ne nous lancerons pas sur une voie aussi complexe. En revanche on peut être rusé et tenter d’en écrire le moins possible tout en se faisant passer par une string...
En fait c’est assez facile mais cela utilise des éléments syntaxiques peu utilisés comme les opérateurs implicites.
L’astuce consiste à créer une classe “normale” n’héritant de rien, et possédant une seule propriété, Value, de type string (ou d’un autre type sealed dont on souhaiterait hériter).
C’est sûr que ce n’est pas compliqué à écrire mais cela ne règle pas la question. Il n’est pas possible de faire passer notre classe pour string. Partout il faudra changer ‘x = “toto”’ par ‘x.Value = “toto”’ et ce n’est pas du tout ce qu’on cherche !
C’est oublier les opérateurs “implicit” qui permettent de convertir une instance d’une classe en d’autres types (et réciproquement). Implicitement. C’est à dire sans avoir à écrire quoi que ce soit dans le code qui utilise la dite classe à convertir.
Pour commencer nous aurons ainsi un code qui ressemble à cela :
public class MyString : IEquatable<MyString>, IConvertible
{
private string value;
public MyString() { }
public MyString(string value)
{
this.value = value;
}
public string Value
{
get { return value; }
set { this.value = value; }
}
public override string ToString() { return value; }
public static implicit operator MyString(string str)
{ return new MyString(str); }
public static implicit operator string(MyString myString)
{ return myString.value; } ...
Le type MyString déclare une propriété Value de type string, mais surtout elle déclare deux opérateurs implicites : l’un permettant de convertir une string en MyString, et l’autre s’occupant du sens inverse.
C’est presque tout. Ca marche. Je peux écrire ‘MyString x = “toto”’ et l’inverse aussi (affecter à une variable de type string directement une variable de type MyString).
Dans la réalité il faudra s’occuper d’autres détail, comme les opérateurs d’égalité par exemple, ou bien les conversions de type (interface IConvertible), etc.
Mais la majorité de ce code peut être directement vampirisé de la classe string puisque la valeur Value est de ce type et que notre classe ne contient rien d’autre à convertir.
On en arrive à un code final de ce type (en supposant comme dans l’exposé plus haut qu’on souhaite avoir une string personnalisée pour saisir le nom d’un dictionnaire, Donc une DictionaryNameString) :
public class DictionaryNameString : IEquatable<DictionaryNameString>, IConvertible
{
private string value;
public DictionaryNameString() { }
public DictionaryNameString(string value)
{
this.value = value;
}
public string Value
{
get { return value; }
set { this.value = value; }
}
public override string ToString() { return value; }
public static implicit operator DictionaryNameString(string str)
{
return new DictionaryNameString(str);
}
public static implicit operator string(DictionaryNameString dictionary)
{ return dictionary.value; }
public bool Equals(DictionaryNameString other)
{
if (ReferenceEquals(null, other)) return false;
return ReferenceEquals(this, other) || Equals(other.value, value);
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
return obj.GetType() == typeof(DictionaryNameString) &&
Equals((DictionaryNameString)obj);
}
public override int GetHashCode()
{
return (value != null ? value.GetHashCode() : 0);
}
public static bool operator ==(DictionaryNameString left, DictionaryNameString right)
{ return Equals(left, right); }
public static bool operator !=(DictionaryNameString left, DictionaryNameString right)
{ return !Equals(left, right); }
#region IConvertible Members
public TypeCode GetTypeCode() { return TypeCode.String; }
public bool ToBoolean(IFormatProvider provider)
{ return Convert.ToBoolean(value, provider); }
public byte ToByte(IFormatProvider provider)
{ return Convert.ToByte(value, provider); }
public char ToChar(IFormatProvider provider)
{ return Convert.ToChar(value, provider); }
public DateTime ToDateTime(IFormatProvider provider)
{ return Convert.ToDateTime(value, provider); }
public decimal ToDecimal(IFormatProvider provider)
{ return Convert.ToDecimal(value, provider); }
public double ToDouble(IFormatProvider provider)
{ return Convert.ToDouble(value, provider); }
public short ToInt16(IFormatProvider provider)
{ return Convert.ToInt16(value, provider); }
public int ToInt32(IFormatProvider provider)
{ return Convert.ToInt32(value, provider); }
public long ToInt64(IFormatProvider provider)
{ return Convert.ToInt64(value, provider); }
public sbyte ToSByte(IFormatProvider provider)
{ return Convert.ToSByte(value, provider); }
public float ToSingle(IFormatProvider provider)
{ return Convert.ToSingle(value, provider); }
public string ToString(IFormatProvider provider)
{ return value; }
public object ToType(Type conversionType, IFormatProvider provider)
{ return Convert.ChangeType(value, conversionType, provider); }
public ushort ToUInt16(IFormatProvider provider)
{ return Convert.ToUInt16(value, provider); }
public uint ToUInt32(IFormatProvider provider)
{ return Convert.ToUInt32(value, provider); }
public ulong ToUInt64(IFormatProvider provider)
{ return Convert.ToUInt64(value, provider); }
#endregion
}
Et voici une classe “string” personnalisée, utilisable comme string et offrant globalement les mêmes services dans 99% des cas (affectations dans un sens ou dans l’autre, conversions).
Petit plus : notre classe n’est pas “sealed”... Il suffit de l’appeler “MyStringBase” et d’hériter ensuite de cette classe pour se créer des tas de types “string” personnalisés.
En dehors de l’exemple que je donnais, on peut imaginer de nombreux cas où faire un “if (variable is MySpecialString)...” pourra simplifier beaucoup les choses. Tout en conservant une écriture simple et limpide, un code propre et maintenable.
Conclusion
C# est un langage fort subtil, il sait créer des blocages qui évitent les grosses boulettes (comme le fait qu’il n’autorise pas l’héritage multiple ou l’existence des classes sealed) mais en contrepartie il offre de nombreuses portes de sortie permettant de façon élégante de faire ce qu’on veut. Reste à bien le maîtriser pour y arriver, et ça c’est un sacré challenge, plus on travaille avec ce langage, plus on comprend qu’on est loin de tout en savoir !
Stay Tuned !