Сильно типизированные целые числа - PullRequest
5 голосов
/ 10 августа 2010

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

public void MyMethod(int useCaseId)
{
    // Do something with the useCaseId
}

public void SomeOtherMethod()
{
    int userId = 12;
    int useCaseId = 15;
    MyMethod(userId); // Ooops! Used the wrong value!
}

Эту ошибку будет трудно найти, поскольку нет ошибки во время компиляции, и вы не обязательно даже получите исключение во время выполнения. Вы просто получите «неожиданные результаты».

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

public enum UseCaseId { // Empty… }

public enum UserId { // Empty… }

public void MyMethod(UseCaseId useCaseId)
{
   // Do something with the useCaseId
}

public void SomeOtherMethod()
{
   UserId userId = (UserId)12;
   UseCaseId useCaseId = (UseCaseId)15;
   MyMethod(userId); // Compile error!!
}

Что ты думаешь?

Ответы [ 6 ]

6 голосов
/ 10 августа 2010

Если у вас возникнут проблемы с созданием новых типов для хранения UserId и UseCaseId, вы можете почти так же легко превратить их в простые классы и использовать оператор неявного преобразования из int, чтобы получить синтаксис Вы хотите:

public class UserId
{
    public int Id { get; private set; }

    public UserId(int id)
    {
       id_ = id;
    }

    public static implicit operator UserId(int id)
    {
        return new UserId(id);
    }

    public static void Main()
    {
        UserId id1 = new UserId(1);
        UserId id2 = 2;
    }
}

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

2 голосов
/ 24 сентября 2011

Я давно хотел сделать что-то похожее, и ваш пост побудил меня попробовать следующее:

 public class Id<T> {
    private readonly int _Id;

    private Id(int id) {
        _Id = id;
    }

    public static implicit operator int(Id<T> id) {
        return id._Id;
    }

    public static implicit operator Id<T>(int id) {
        return new Id<T>(id);
    }
}

, который я могу использовать следующим образом

public void MyMethod(Id<UseCase> useCaseId)
{
   // Do something with the useCaseId
}

public void SomeOtherMethod()
{
   Id<User> userId = 12;
   Id<UseCase> useCaseId = 15;
   MyMethod(userId); // Compile error!!
}

Я думаю, что передача этого типа объекта Id лучше, чем передача всего объекта домена, потому что это делает контракт более явным между вызывающей стороной и вызываемой стороной. Если вы передаете только идентификатор, вы знаете, что вызываемый не имеет доступа ни к какому другому свойству объекта.

2 голосов
/ 12 августа 2010

Если бы это был Haskell и я хотел бы сделать это, я мог бы сделать это так:

data UserId    = UserId    Int
data UseCaseId = UseCaseId Int

Таким образом, функции будут принимать UserId вместо Int, а создание UserId всегда явно, что-то вроде:

doSomething (UserId 12) (UseCaseId 15)

Это похоже на решение Найла С. о создании типа для обёртывания в Int. Однако было бы неплохо, если бы для каждого типа не потребовалось 10 строк.

1 голос
/ 13 декабря 2010

Поздно к игре, но FWIW ... этот проект на codeplex определил несколько "строго типизированных" скаляров, таких как угол, азимут, расстояние, широта, долгота, радиан и т. Д. Фактически, каждый из это структура с единственным скалярным членом и несколькими методами / свойствами / конструкторами для манипулирования ею «правильно». Не сильно отличается от того, что делает каждый из них классом, за исключением того факта, что это типы значений, а не ссылочные типы. Хотя я не использовал эту платформу, я вижу ценность того, чтобы сделать таких людей первоклассными гражданами.

Не знаю, является ли это в конечном счете хорошей идеей или нет, но кажется полезным иметь возможность писать код, подобный этому (аналогично вашему первоначальному примеру) с гарантией безопасности типов (и безопасности значений):

Angle a = new Angle(45); //45 degrees
SomeObject o = new SomeObject();
o.Rotate(a); //Ensure that only Angle can be sent to Rotate

или

Angle a = (Angle)45.0;

или

Radians r = Math.PI/2.0;
Angle a = (Angle)r;

Кажется, этот шаблон был бы наиболее полезен, если в вашем домене много скалярных «типов» со значениями семантики и потенциально много экземпляров этих типов. Моделирование каждого как структуры с одним скаляром дает очень компактное представление (сравнивая превращение каждого в полноценный класс). Хотя реализация этого уровня абстракции (а не просто использование «голых» скаляров для представления значений доменов) и дискретности может показаться затруднительной, результирующий API кажется гораздо проще использовать «правильно».

1 голос
/ 10 августа 2010

Лично я считаю, что не нужно быть честным.
Разработчик должен правильно реализовать логику, и вы не можете полагаться на ошибки времени компиляции для таких ошибок.

0 голосов
/ 10 августа 2010

Я бы предпочел проверить аргумент в MyMethod и вызвать соответствующее исключение в случае ошибки-условия.

public void MyMethod(int useCaseId)
{
    if(!IsValidUseCaseId(useCaseId))
    {
         throw new ArgumentException("Invalid useCaseId.");
    }
    // Do something with the useCaseId
}

public bool IsValidUseCaseId(int useCaseId)
{
    //perform validation here, and return True or False based on your condition.
}

public void SomeOtherMethod()
{
    int userId = 12;
    int useCaseId = 15;
    MyMethod(userId); // Ooops! Used the wrong value!
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...