Dot.Blog

C#, XAML, WinUI, WPF, Android, MAUI, IoT, IA, ChatGPT, Prompt Engineering

C# : créer des descendants du type String

[new:30/09/2011]C’est un peu un piège, bien entendu, la classe String est “sealed” et il est donc impossible d’en hériter, comme d’autres classes de base du Framework... Pourtant le besoin existe. Pourquoi vouloir des chaines de caractères descendant de string (ou d’autres de base) ? Comment contourner l’interdiction du Framework ? Répondre à ces questions est le thème du jour !

Pourquoi ?

C’est la première question, et la plus importante peut-être. Pourquoi vouloir créer des types descendant de string (ou d’autres types de base 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.

Le Framework ne permet pas de la création de classes héritant de string ou , 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...

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, il en existe certaines implémentations pour Silverlight et celle de WF 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 Silverlight) il se trouve certaines chaines de caractères définissant par exemple le nom d’un fichier externe.

Dans un tel cas vous souhaitez que plutôt qu’un simple éditeur de string s’affiche aussi un petit bouton “...” 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 justifiant pas. 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.

La solution est toute autre.

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 (le nom de la classe est un peu long mais correspond à un cas réel) :

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

Je parle moins souvent de C# qu’il y a quelques années car les nouveautés se font rares, le langage est stabilisé et commence à être bien connu. Mais ce n’est pas une raison pour ne pas rappeler certaines de ses possibilités qui sont loin d’être toutes maitrisées et encore moins utilisées fréquemment. Même les choses les moins exotiques.

En 2009 j’avais eu l’honneur d’être nommé MVP C#. Depuis j’ai eu des distinctions plus spécialisées comme la dernière, MVP Silverlight. Mais je n’oublie pas pour autant la beauté de C# et son incroyable efficacité. Porter la bonne parole sur ce langage particulièrement agréable à utiliser est toujours un plaisir, même si je n’ai plus de titre à y gagner !

L’été ayant été studieux (vu le temps...) j’ai deux ou trois articles assez gros en réserve. J’aurais le plaisir de vous présenter en 110 pages Jounce ou de vous parler Design. C’est en cours de relecture. D’autres billets seront certainement publiés d’ici la mise en ligne de ces articles car il faut vous laisser le temps de lire le dernier sortir sur MEF et Silverlight qui atteint les 73 pages malgré tout ! En tout cas, autant de raisons de ...

... Stay Tuned !

Faites des heureux, PARTAGEZ l'article !

Commentaires (4) -

  • nom

    04/09/2011 18:21:22 | Répondre

    Salut,
    dans ce contexte c'est plutot 'scellée' que 'celée.'
    L'ajout de l'opérateur + serait le bienvenue Smile

    Cdt

  • Olivier

    04/09/2011 19:21:20 | Répondre

    il est vrai que scellée s'adapte mieux au contexte, les blogs sont des grands pourvoyeurs de coquilles, le mien n'y échappe pas hélas (mais quand j'en lis d'autres, je suis quand même rassuré sur mon orthographe Smile ).
    Merci de la correction (bien que je doute que les lecteurs, qui écrivent eux mêmes de plus en plus en langage SMS soient nombreux à voir la différence Smile ).

    Pour l'opérateur "+" c'est vrai que ça serait mieux. Il faut bien que je vous laisse un peu de boulot quand même ! Smile

  • nom

    06/09/2011 00:32:27 | Répondre

    >>les blogs sont des grands pourvoyeurs de >>coquilles,
    Les blogs non, mais le manque de temps c'est probable, car une simple consultation de dictionnaire peut faire l'affaire Wink
    bien
    >>Il faut bien que je vous laisse un peu de boulot quand même !
    Pourvoyeur d'idées, t'a trouver la planque Smile

    Je reconnais qu'il y a du travail en amont.

  • Olivier

    06/09/2011 15:23:34 | Répondre

    Tu sais, s'il fallait consulter le dictionnaire à chaque mot, il y aurait par force moins de billets et donc moins d'information partagée.
    Il ne faut pas confondre les buts. Toute action à un but. Ici mon but est de partager de l'information. Ce n'est pas le blog d'un grammairien ni d'un étymologiste.
    C'est un peu comme le but des Resto du coeur, c'est de partager de la nourriture. Il serait mal venu pour un demandeur de se plaindre qu'ils ne fournissent pas de rince-doigts ...
    Et puis la simple consultation d'un dictionnaire n'est pas suffisante, c'est mal connaître les problèmes de l'écriture en général. Il y a les phrases reconstruites, les échanges de place de tel ou tel point qui laisse un bout de phrase au singulier alors qu'il devrait passer au pluriel, etc... La relecture par l'auteur est malheureusement d'une efficacité limitée. Il lit ce qu'il veut dire et non pas ce qui est écrit, laissant passer de nombreuses coquilles. Pour avoir écrit trois livres assez gros, je peux t'assurer que même la relecture par un relecteur professionnel puis une troisième relecture par l'auteur laissent encore passer des coquilles...
    Après c'est un choix : Moine copiste, mais seule la Bible existait comme livre, en très petit nombre d'exemplaires lus et relus; ou bien une société de partage de l'information qui semble bien incompatible avec les rigueurs qu'imposent notre grammaire et notre orthographe tirant le lourd fardeau des siècles passés et ses longues listes d'exceptions à des règles tout aussi nombreuses.

    Mais je te suis reconnaissant de reconnaître qu'il y a du "travail en amont".

    D'autant qu'il y a quelques années, les informations publiées par Dot.Blog ne se seraient trouvées que dans des journaux très chers et à diffusion limitée (comme le fut Pascalissime par exemple), alors qu'ici, l'accès à l'information est gratuit (et sans un seul bandeau de pub)...

    J'aimerai, moi aussi, sois en convaincu, que tout soit parfait, que chaque billet soit l'expression à la fois d'une maîtrise parfaite de la technique informatique évoquée et de celle d'une magistrale et brillante maîtrise de notre langue si sophistiquée. J'aimerai. Il me reste à trouver un mécène qui alimentera mon compte en banque pour me permettre de me consacrer uniquement à la création  du blog parfait : sujets passionnants, explications lumineuses, mise en page à faire pâlir les meilleurs designers, et langage soutenu portant le label "lu et approuvé par Me Capello"... Si tu connais quelqu'un, n'hésite pas à me mettre en contact !

Ajouter un commentaire