Dans l'écosystème .NET, la notion de chaîne fortement typée ne désigne pas une fonctionnalité intégrée au framework, mais plutôt un patron de conception consistant à encapsuler une valeur textuelle dans une structure dédiée. Cette approche partage plusieurs avantages avec les énumérations — sécurité à la compilation, expressivité du code — tout en offrant une flexibilité et un contrôle accrus sur les valeurs acceptées.
Les limites des énumérations
Les types enum en C# permettent d'associer des noms explicites à un ensemble de constantes. Ils bénéficient d'une vérification à la compilation et améliorent grandement la lisibilité. Néanmoins, ils présentent des restrictions notables : impossible d'y attacher des méthodes, propriétés ou événements, et leur capacité à filtrer les entrées invalides reste faible.
Considérons cette énumération représentant des niveaux d'accès :
public enum AccessLevel
{
Guest,
Member,
Moderator,
Owner
}
Puis une méthode consommant ce type :
public string DescribeRole(AccessLevel level)
{
return level.ToString();
}
Rien n'empêche un développeur de passer une valeur hors plage :
var output = service.DescribeRole((AccessLevel)42);
Le code compile et s'exécute sans erreur, retournant simplement « 42 ». Tout branchement conditionnel reposant sur cette énumération produira alors un comportement imprévu. C'est précisément dans ce scénario qu'une chaîne fortement typée devient pertinente.
Mise en œuvre d'une chaîne fortement typée
Le patron consiste à créer une structure immuable (readonly struct) implémentant IEquatable<T>, munie d'un constructeur privé acceptant une chaîne. Les valeurs connues sont exposées sous forme de propriétés statiques en lecture seule. Il convient également de surcharger ToString() et les opérateurs d'égalité afin que le type se comporte de manière naturelle dans les expressions courantes.
Voici un exemple inspiré du code source .NET, illustrant la modélisation d'identifiants de protocoles réseau :
using System.Diagnostics.CodeAnalysis;
namespace App.Net.Protocols
{
public readonly struct ProtocolScheme : IEquatable<ProtocolScheme>
{
private static readonly ProtocolScheme Http = new("http");
private static readonly ProtocolScheme Https = new("https");
private static readonly ProtocolScheme Ftp = new("ftp");
private static readonly ProtocolScheme Ws = new("ws");
private static readonly ProtocolScheme Wss = new("wss");
private readonly string? _value;
internal ProtocolScheme(string? value)
{
_value = value;
}
public string? Value => _value;
public override string ToString() => _value ?? string.Empty;
public bool Equals(ProtocolScheme other)
=> string.Equals(_value, other._value, StringComparison.Ordinal);
public override bool Equals([NotNullWhen(true)] object? obj)
=> obj is ProtocolScheme scheme && Equals(scheme);
public override int GetHashCode()
=> _value is null ? 0 : StringComparer.Ordinal.GetHashCode(_value);
public static bool operator ==(ProtocolScheme left, ProtocolScheme right)
=> left.Equals(right);
public static bool operator !=(ProtocolScheme left, ProtocolScheme right)
=> !left.Equals(right);
public static implicit operator string?(ProtocolScheme scheme) => scheme._value;
}
}
Cette structure garantit que seules les valeurs prédéfinies sont utilisées dans le code applicatif. La comparaison reste intentionnellement ordinale et sensible à la casse, conformément aux conventions des identificateurs de protocole.
Le framework .NET applique lui-même ce patron dans plusieurs endroits, notamment avec System.Security.Cryptography.HashAlgorithmName, qui encapsule les noms d'algorithmes de hachage et expose des propriétés statiques comme MD5, SHA256, SHA3_512, etc. Cette classe ajoute également des utilitaires de conversion depuis des identifiants OID via les méthodes TryFromOid et FromOid.
Recommandations d'usage
Selon les Framwork Design Guidelines, plusieurs critères orientent le choix entre énumération et chaîne fortement typée :
- Ensemble fermé : si les valeurs sont parfaitement connues et qu'aucune extension n'est envisageable, une énumération suffit.
- Ensemble ouvert : lorsque les entrées peuvent être étendues par des types dérivés ou proviennent d'une source externe (versions de système d'exploitation, identifiants de protocole), la chaîne fortement typée est préférable.
- Besoin de comportement : si la logique de validation, de formatage ou de conversion doit accompagner la valeur, la structure offre un emplacement naturel pour ces membres.
La bibliothèque de contrôles HandyControl illustre ce dernier cas avec son type SystemVersionInfo, qui modélise des versions de systèmes d'exploitation — un ensemble par nature extensible — plutôt que de recourir à une énumération figée.