Сильная идентификация значений ID в С#

Есть ли способ сильно ввести значения идентификатора целочисленного идентификатора в С#?

Недавно я играл с Haskell и сразу вижу преимущества его сильной печати при применении к значениям ID, например, вы никогда не захотите использовать PersonId вместо ProductId.

Есть ли хороший способ создания Id-класса/структуры, который может использоваться для представления идентификаторов определенного типа?

У меня была следующая идея, но, к сожалению, она не является законной на многих уровнях. Вы не можете иметь абстрактную структуру, и неявные/явные операторы литья не будут унаследованы.

public abstract struct Id
{
    int _value;

   public Id(int value)
   {
      _value = value;
   }

   // define implicit Id to int conversion operator:
   public static implicit operator int(Id id) 
   {
      return _value;    
   }

   // define explicit int to Id conversion operator:
   public static explicit operator Id(int value) 
   {
      return new Id(value);
   }

   public bool Equals(object obj)
   {
      if(GetType() == obj.GetType()) 
      {
         Id other = (Id)obj;
         return other._value == _value;
      }
      return false;
   }

   public int GetHashCode()
   {
      return _value.GetHashCode();
   }
}

struct PersonId : Id { public PersonId(int value) : base(value) {} }
struct ProductId : Id { public ProductId(int value) : base(value) {} }

Существуют ли какие-либо допустимые способы выполнения чего-то подобного? Как еще мы можем доказать, что тип целочисленных идентификаторов не путается в большом приложении?

Ответы

Ответ 1

public interface IId { }

public struct Id<T>: IId {
    private readonly int _value;

    public Id(int value) {
        this._value = value;
    }

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

    public static explicit operator Id<T>(int value) {
        return new Id<T>(value);
    }
}

public struct Person { }  // Dummy type for person identifiers: Id<Person>
public struct Product { } // Dummy type for product identifiers: Id<Product>

Теперь вы можете использовать типы Id<Person> и Id<Product>. Типы Person и Product могут быть либо структурами, либо классами. Вы даже можете использовать фактические типы, идентифицируемые идентификатором, и в этом случае вам не нужны никакие типы фиктивных данных.

public sealed class Person {
    private readonly Id<Person> _id;
    private readonly string _lastName;
    private readonly string _firstName;

    // rest of the implementation...
}

Изменить: Добавлен интерфейс IId, предложенный smartcaveman.

Ответ 2

Хорошо, по моему опыту, перегрузка оператора в таких сценариях обычно не работает слишком хорошо и может привести к возникновению всех проблем. Кроме того, если вы предоставляете неявные операторы приведения в/из типа int, уверены ли вы, что оператор, например:

SomeCompany.ID = SomePerson.ID

по-прежнему будет считаться недействительным компилятором? Или компилятор может просто использовать ваши операторы трансляции и, таким образом, разрешить недопустимое присвоение через...?

Менее элегантное решение включает определение собственного типа объекта значения (как class) и доступ к фактическому идентификатору с помощью свойства Value:

sealed class PersonId
{
    readonly int value;
    public int Value { get { return value; } }

    public PersonId(int value)
    {
        // (you might want to validate 'value' here.)
        this.value = value;
    }

    // override these in order to get value type semantics:
    public override bool Equals(object other) { … }
    public override int GetHashCode() { … }
}

Теперь, когда вы пишете person.Id, вам нужно написать person.Id.Value, если вам действительно нужно необработанное целое число. Было бы даже лучше, если бы вы на самом деле пытались уменьшить доступ к необработанному целочисленному значению как можно меньше мест, например. где вы сохраняете объект (или загружаете его) из базы данных.

P.S.: В приведенном выше коде мне бы хотелось сделать PersonId a struct, так как это объект значения. Тем не менее, struct имеет одну проблему, которая является конструктором без параметров, который автоматически предоставляется компилятором. Это означает, что пользователь вашего типа может обойти все ваши конструкторы (где может произойти валидация), и вы можете получить недопустимый объект сразу после его создания. Таким образом, постарайтесь сделать PersonId максимально похожим на struct: объявив его sealed, переопределив Equals и GetHashCode и не предоставив конструктор без параметров.

Ответ 3

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

В основном идея состоит в создании идентификатора factory для каждого типа, возвращающего интерфейс для фактического экземпляра id.

public interface IPersonId
{
    int Value { get; }
}

public static class PersonId
{
    public static IPersonId Create(int id)
    {
        return new Id(id);
    }

    internal struct Id : IPersonId
    {
        public Id(int value)
        {
            this.Value = value;
        }

        public int Value { get; }

        /* Implement equality comparer here */
    }
}

Это немного работает, и он не будет работать с ORM, если только он не имеет доступа к внутреннему struct, поэтому, если это не проблема, идея должна быть безопасной. Это примерный черновик, и я сам не тестировал его полностью, но до сих пор я получаю типы значений для id и нет проблемы с конструктором без параметров struct.

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

public interface IId<T>
{
    int Value { get; }
}

public static class Id
{
    public static IId<T> Create<T>(int value)
    {
        return new IdImpl<T>(value);
    }

    internal struct IdImpl<T> : IId<T>
    {
        public IdImpl(int value)
        {
            this.Value = value;
        }

        public int Value { get; }

        /* Implement equality comparer here */
    } 
}

Тогда вы сможете сделать Id.Create<Person>(42).

Лично я не являюсь огромным фанатом этих дженериков, но, на мой взгляд, это действительно вопрос вкуса. Это действительно просто способ ограничения совместимости типов, который, на мой взгляд, должен быть явным. Преимущество, однако, состоит в том, что у вас будет только одно место для сравнения равенства, которое будет приятным и сухим.

Это определенно не идеально, и в большинстве (если не большинство) случаев использования он будет полностью переработан, просто обойти тот факт, что struct имеет конструктор без параметров без параметров. Кроме того, по очевидным причинам нет возможности выполнять проверки валидации на общую вариацию, а значение базового идентификатора ограничено int (что может быть изменено, конечно).

Ответ 4

Я не уверен, почему вы хотели бы, чтобы PersonID был классом сам по себе, PersonID был бы свойством лица типа int.

Person.PersonID \\<<int

Теперь я давно видел хороший шаблон, подобный этому:

interface BusinessObject<TKey,TEntity> //TKey as in ID
{
 TKey ID;
 TEntity SearchById(TKey id);
}

Итак, вы можете сделать это:

class Person : BusinessObject<Person, int> {}

ИЛИ

class FileRecord : BusinessObject<FileRecord, Guid> {}

Интересно, помогло ли это, но это пришло в голову.