web-dev-qa-db-fra.com

Guid fortement typé comme structure générique

Je fais déjà deux fois le même bug dans le code comme suit:

void Foo(Guid appId, Guid accountId, Guid paymentId, Guid whateverId)
{
...
}

Guid appId = ....;
Guid accountId = ...;
Guid paymentId = ...;
Guid whateverId =....;

//BUG - parameters are swapped - but compiler compiles it
Foo(appId, paymentId, accountId, whateverId);

OK, je veux éviter ces bogues, j'ai donc créé des GUID fortement typés:

[ImmutableObject(true)]
public struct AppId
{
    private readonly Guid _value;

    public AppId(string value)
    {            
        var val = Guid.Parse(value);
        CheckValue(val);
        _value = val;
    }      

    public AppId(Guid value)
    {
        CheckValue(value);
        _value = value;           
    }

    private static void CheckValue(Guid value)
    {
        if(value == Guid.Empty)
            throw new ArgumentException("Guid value cannot be empty", nameof(value));
    }

    public override string ToString()
    {
        return _value.ToString();
    }
}

Et un autre pour PaymentId:

[ImmutableObject(true)]
public struct PaymentId
{
    private readonly Guid _value;

    public PaymentId(string value)
    {            
        var val = Guid.Parse(value);
        CheckValue(val);
        _value = val;
    }      

    public PaymentId(Guid value)
    {
        CheckValue(value);
        _value = value;           
    }

    private static void CheckValue(Guid value)
    {
        if(value == Guid.Empty)
            throw new ArgumentException("Guid value cannot be empty", nameof(value));
    }

    public override string ToString()
    {
        return _value.ToString();
    }
}

Ces structures sont presque identiques, il y a beaucoup de duplication de code. N'est-ce pas?

Je ne peux pas trouver de façon élégante de le résoudre, sauf en utilisant la classe au lieu de struct. Je préfère utiliser struct, en raison de vérifications nulles, moins d'empreinte mémoire, pas de surcharge de garbage collector etc ...

Avez-vous une idée de comment utiliser struct sans dupliquer le code?

39
Tomas Kubes

Tout d'abord, c'est une très bonne idée. Un bref aparté:

Je souhaite que C # facilite la création de wrappers typés bon marché autour d'entiers, de chaînes, d'identifiants, etc. Nous sommes très "heureux de chaîne" et "heureux d'entier" en tant que programmeurs; beaucoup de choses sont représentées sous forme de chaînes et d'entiers qui pourraient avoir plus d'informations suivies dans le système de type; nous ne voulons pas attribuer de noms de clients à des adresses de clients. Il y a quelque temps, j'ai écrit une série d'articles de blog (jamais finis!) Sur l'écriture d'une machine virtuelle dans OCaml, et l'une des meilleures choses que j'ai faites a été d'encapsuler chaque entier de la machine virtuelle avec un type qui indique son but. Cela a empêché tant de bugs! OCaml facilite la création de petits types de wrapper; C # ne fonctionne pas.

Deuxièmement, je ne m'inquiéterais pas trop de la duplication du code. Il s'agit principalement d'un copier-coller facile et il est peu probable que vous modifiiez beaucoup le code ou fassiez des erreurs. Passez votre temps à résoudre de vrais problèmes. Un peu de code copié-collé n'est pas un gros problème.

Si vous voulez éviter le code copié-collé, je suggère d'utiliser des génériques comme celui-ci:

struct App {}
struct Payment {}

public struct Id<T>
{
    private readonly Guid _value;
    public Id(string value)
    {            
        var val = Guid.Parse(value);
        CheckValue(val);
        _value = val;
    }

    public Id(Guid value)
    {
        CheckValue(value);
        _value = value;           
    }

    private static void CheckValue(Guid value)
    {
        if(value == Guid.Empty)
            throw new ArgumentException("Guid value cannot be empty", nameof(value));
    }

    public override string ToString()
    {
        return _value.ToString();
    }
}

Et maintenant vous avez terminé. Vous avez les types Id<App> Et Id<Payment> Au lieu de AppId et PaymentId, mais vous ne pouvez toujours pas affecter un Id<App> À Id<Payment> ou Guid.

De plus, si vous aimez utiliser AppId et PaymentId alors en haut de votre fichier vous pouvez dire

using AppId = MyNamespace.Whatever.Id<MyNamespace.Whatever.App>

etc.

Troisièmement, vous aurez probablement besoin de quelques fonctionnalités supplémentaires dans votre type; Je suppose que ce n'est pas encore fait. Par exemple, vous aurez probablement besoin de l'égalité, afin de pouvoir vérifier si deux identifiants sont identiques.

Quatrièmement, sachez que default(Id<App>) vous donne toujours un identifiant "guid vide", donc votre tentative d'empêcher cela ne fonctionne pas réellement; il sera toujours possible d'en créer un. Il n'y a pas vraiment de bon moyen de contourner cela.

45
Eric Lippert

Nous faisons de même, cela fonctionne très bien.

Oui, c'est beaucoup de copier-coller, mais c'est exactement à cela que sert la génération de code.

Dans Visual Studio, vous pouvez utiliser des modèles T4 pour cela. Vous écrivez essentiellement votre classe une fois, puis vous avez un modèle dans lequel vous dites "Je veux cette classe pour App, Payment, Account, ..." et Visual Studio vous générera un fichier de code source pour chacun.

De cette façon, vous avez une seule source (le modèle T4) où vous pouvez apporter des modifications si vous trouvez un bogue dans vos classes et il se propagera à tous vos identificateurs sans que vous ayez à penser à les changer tous.

5
nvoigt