Hériter d’une classe Sealed ?

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 ?

imageC’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 !