Aller au contenu

# Sérialiser vos objets en chaînes de caractères avec l'interface IFormatting

Introduction

Dans une application, un même objet ne s'affiche pas toujours de la même manière. Dans une liste de contacts, nous préférons parfois « DUPONT, Jean ». Dans un message, « Jean Dupont » paraît plus naturel. Pour un prix, la langue du lecteur change aussi la présentation du nombre.

En .NET, l'interface IFormattable permet de définir ces représentations au sein du type, puis de les sélectionner directement dans un appel à ToString, une chaîne interpolée ou string.Format.

Présentation de l'interface

IFormattable expose une seule méthode :

string ToString(string? format, IFormatProvider? formatProvider);

Le paramètre format indique la représentation souhaitée. Le paramètre formatProvider fournit les conventions de mise en forme, généralement une culture (fr-FR, en-US, etc.). La classe décide elle-même des formats qu'elle accepte : la lettre A peut signifier « annuaire » pour notre classe, mais elle n'a pas cette signification pour tous les types .NET.

Cette distinction est essentielle : le format choisit quoi montrer, tandis que le fournisseur détermine comment écrire certaines valeurs, notamment les nombres et les dates.

Un premier exemple : afficher un utilisateur

Nous allons proposer trois représentations d'un utilisateur : C pour son nom complet, A pour un annuaire et I pour une forme abrégée. En l'absence de format, C sera utilisé.

public sealed class User : IFormattable
{
    public string Prenom { get; }
    public string Nom { get; }

    public User(string prenom, string nom)
    {
        if (string.IsNullOrWhiteSpace(prenom))
            throw new ArgumentException("Le prénom est obligatoire.", nameof(prenom));

        if (string.IsNullOrWhiteSpace(nom))
            throw new ArgumentException("Le nom est obligatoire.", nameof(nom));

        Prenom = prenom;
        Nom = nom;
    }

    public override string ToString() => ToString(null, null);

    public string ToString(string? format, IFormatProvider? formatProvider)
    {
        return format?.ToUpperInvariant() switch
        {
            null or "" or "C" => $"{Prenom} {Nom}",
            "A" => $"{Nom.ToUpperInvariant()}, {Prenom}",
            "I" => $"{Prenom[0]}. {Nom}",
            _ => throw new FormatException($"Format inconnu : '{format}'.")
        };
    }
}

Voici les appels correspondants :

var user = new User("Jean", "Dupont");

Console.WriteLine(user);                         // Jean Dupont
Console.WriteLine(user.ToString("A", null));      // DUPONT, Jean
Console.WriteLine($"{user:I}");                   // J. Dupont
Console.WriteLine(string.Format("{0:A}", user));  // DUPONT, Jean

La surcharge ToString() est redéfinie pour déléguer à la représentation par défaut de IFormattable. Sans cette redéfinition, un appel ordinaire à ToString() ne bénéficierait pas automatiquement de notre choix de format. Ici, formatProvider n'est pas utilisé, car ces trois représentations ne contiennent ni nombre ni date à adapter à une culture.

Un deuxième exemple : présenter un prix selon la culture

Avec un prix, les deux paramètres deviennent utiles. Nous définissons D pour une description complète et M pour le montant seul. Le fournisseur de format est transmis au decimal lors de sa mise en forme.

using System.Globalization;

public sealed class PrixProduit : IFormattable
{
    public string Libelle { get; }
    public decimal Montant { get; }

    public PrixProduit(string libelle, decimal montant)
    {
        Libelle = libelle;
        Montant = montant;
    }

    public override string ToString() => ToString(null, null);

    public string ToString(string? format, IFormatProvider? formatProvider)
    {
        formatProvider ??= CultureInfo.CurrentCulture;
        string montant = Montant.ToString("N2", formatProvider);

        return format?.ToUpperInvariant() switch
        {
            null or "" or "D" => $"{Libelle} : {montant}",
            "M" => montant,
            _ => throw new FormatException($"Format inconnu : '{format}'.")
        };
    }
}
var prix = new PrixProduit("Clavier", 1234.5m);
var france = CultureInfo.GetCultureInfo("fr-FR");
var etatsUnis = CultureInfo.GetCultureInfo("en-US");

Console.WriteLine(prix.ToString("D", france));
// Clavier : 1 234,50

Console.WriteLine(prix.ToString("D", etatsUnis));
// Clavier : 1,234.50

Console.WriteLine(string.Format(france, "{0:M}", prix));
// 1 234,50

N2 est un format numérique standard : il affiche deux décimales et des séparateurs conformes à la culture reçue. L'espace utilisé comme séparateur de milliers pour fr-FR peut varier dans sa représentation Unicode selon l'environnement. Ce prix n'affiche pas de devise : le symbole monétaire ne doit pas être déduit de la culture lorsque la devise est une donnée métier. Si un produit est facturé en euros, il faut conserver cette information explicitement.

Comment .NET utilise-t-il notre implémentation ?

Lorsqu'une opération de mise en forme reçoit notre objet et un spécificateur comme A, elle peut appeler son implémentation de IFormattable.ToString en lui transmettant ce spécificateur et le fournisseur de format disponible. Ainsi, string.Format(france, "{0:M}", prix) appelle la mise en forme de PrixProduit avec M et la culture française.

Une chaîne interpolée telle que $"{user:I}" permet aussi de demander un format. En revanche, elle n'introduit pas à elle seule une culture précise : si la culture du résultat compte, transmettez-la explicitement, par exemple à string.Format ou à ToString.

Les avantages

  • Une API familière : les types personnalisés s'utilisent comme les types .NET déjà formatables.

  • Des règles regroupées dans le type : les appelants n'ont pas à répéter la construction de chaque représentation.

  • Une écriture concise : les formats fonctionnent notamment dans les chaînes interpolées et la mise en forme composite.

  • Une prise en charge des cultures : les nombres et les dates peuvent être présentés selon les conventions demandées.

Les points de vigilance

Documentez les codes acceptés et choisissez un format par défaut stable. Une exception FormatException signale clairement un code inconnu. Si vous avez besoin d'une longue liste d'options combinables, une méthode dédiée ou un objet de configuration sera plus facile à comprendre qu'une multiplication de lettres de format.

Enfin, un texte formaté est destiné à l'affichage. Il ne remplace pas les propriétés métier ni un format de sérialisation pour échanger ou conserver les données.