Сильно типизированный Guid как общая структура

Я уже дважды повторял одну и ту же ошибку в коде:

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);

Хорошо, я хочу предотвратить эти ошибки, поэтому я создал строго типизированные GUID:

[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();
    }
}

И еще один для 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();
    }
}

Эти структуры почти одинаковы, много дублирования кода. Не есть?

Я не могу придумать какой-либо элегантный способ решить эту проблему, кроме использования класса вместо структуры. Я бы предпочел использовать struct из-за нулевых проверок, меньшего объема памяти, никаких затрат на сборщик мусора и т. Д...

У вас есть идея, как использовать struct без дублирования кода?

Ответ 1

Во-первых, это действительно хорошая идея. Вкратце:

Я бы хотел, чтобы в С# было проще создавать дешевые типизированные обертки вокруг целых чисел, строк, идентификаторов и так далее. Мы очень "счастливы как строки" и "счастливы как целое" как программисты; многие вещи представлены в виде строк и целых чисел, которые могут отслеживать больше информации в системе типов; мы не хотим присваивать имена клиентов адресам клиентов. Некоторое время назад я написал серию постов в блоге (так и не закончил!) О написании виртуальной машины в OCaml, и одна из лучших вещей, которые я сделал, заключалась в том, чтобы обернуть каждое целое число в виртуальной машине типом, который указывает ее назначение. Это предотвратило так много ошибок! OCaml позволяет очень легко создавать маленькие типы обёрток; С# нет.

Во-вторых, я бы не стал слишком беспокоиться о дублировании кода. В основном это просто копирование-вставка, и вы вряд ли будете много редактировать код или делать ошибки. Потратьте свое время на решение реальных проблем. Небольшой скопированный код не имеет большого значения.

Если вы хотите избежать копирования, вставленного в код, то я бы предложил использовать такие шаблоны:

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();
    }
}

И теперь вы сделали. У вас есть типы Id<App> и Id<Payment> вместо AppId и PaymentId, но вы все равно не можете назначить Id<App> для Id<Payment> или Guid.

Кроме того, если вам нравится использовать AppId и PaymentId то в верхней части файла вы можете сказать

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

и так далее.

В-третьих, вам, вероятно, понадобится еще несколько функций в вашем типе; Я предполагаю, что это еще не сделано. Например, вам, вероятно, понадобится равенство, чтобы вы могли проверить, совпадают ли два идентификатора.

В-четвертых, имейте в виду, что default(Id<App>) прежнему дает вам идентификатор "пустой guid", поэтому ваша попытка предотвратить это на самом деле не работает; все еще будет возможно создать один. Существует не очень хороший способ обойти это.

Ответ 2

Мы делаем то же самое, это прекрасно работает.

Да, это много копировать и вставлять, но это именно то, для чего нужна генерация кода.

В Visual Studio вы можете использовать шаблоны T4 для этого. Вы в основном пишете свой класс один раз, а затем получаете шаблон, в котором говорите "Я хочу этот класс для App, Payment, Account,...", и Visual Studio сгенерирует вам один файл исходного кода для каждого.

Таким образом, у вас есть один единственный источник (шаблон T4), в который вы можете вносить изменения, если вы обнаружите ошибку в ваших классах, и она будет распространяться на все ваши идентификаторы, и вам не придется думать об их изменении.

Ответ 3

Вы можете использовать подклассы с другим языком программирования.