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 !