Вывод типа на основе ограничений в C # / F # - PullRequest
2 голосов
/ 21 апреля 2019

Я пытался заставить это что-то вроде этого статического расширения работать некоторое время:

public static class MixedRepositoryExtensions {
    public static Task<TEntity> FindBySelectorAsync<TRepository, TEntity, TSelector>(
        this TRepository repository,
        TSelector selector)
        where TRepository : IReadableRepository<TEntity>, IListableRepository<TEntity>
        where TEntity : class, ISearchableEntity<TSelector>
        => repository.Entities.SingleOrDefaultAsync(x => x.Matches(selector));
}

Однако, насколько я понимаю, C # по своей природе не включает общие ограничения как часть процесса логического вывода, что приводит к следующей ошибке CS0411 при попытке вызвать его:

Аргументы типа для метода «MixedRepositoryExtensions.FindBySelectorAsync (TRepository, TSelector)» не могут быть выведены из использования. Попробуйте указать аргументы типа явно.

Пример вызова метода (где ProjectRepository расширяет и IReadableRepository и IListableRepository , а проект расширяет ISearchableEntity ):

await (new ProjectRepository()).FindBySelectorAsync(0);

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

Я также рассмотрел наследование двух интерфейсов в один интерфейс, например:

IReadableAndListableRepository<TEntity> : 
    IReadableRepository<TEntity>,
    IListableRepository<TEntity>

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

IUpdatableAndListableRepository<TEntity :
    IUpdatableRepository<TEntity>,
    IListableRepository<TEntity>

Я нашел здесь подсказку от Эрика Липперта о том, что использование F # может помочь (поскольку я отчаялся):

Обобщения: почему компилятор не может определить аргументы типа в этом случае?

Я немного поигрался с F #, но нашел немного документации об ограничении типов несколькими интерфейсами (или любыми конкретными интерфейсами в этом отношении) и не смог обойти несколько ошибок. Вот последняя попытка, которую я попробовал. Я понимаю, что метод не возвращает одно и то же значение, сейчас я просто пытался заставить ограничения играть хорошо. Извините, если это плохо сделано, я впервые играю с F #.

[<Extension>]
type MixedRepositoryExtensions() =
    [<Extension>]
    static member inline FindBySelectorAsync<'TSelector, 'TEntity when 'TEntity: not struct and 'TEntity:> ISearchableEntity<'TSelector>>(repository: 'TRepository when 'TRepository:> IReadableRepository<'TEntity> and 'TRepository:> IListableRepository<'TEntity>, selector: 'TSelector) = repository;

Однако эта реализация приводит к следующим ошибкам, обе ссылаются на строку, в которой определен FindBySelectorAsync:

FS0331: Неявное создание экземпляра универсальной конструкции в этой точке или около нее не может быть разрешено, поскольку она может разрешаться для нескольких не связанных типов, например, 'IListableRepository <' TEntity> 'и' IReadableRepository <'TEntity>'. Для устранения неоднозначности рассмотрите возможность использования аннотаций типов

FS0071: Несоответствие ограничения типа при применении типа по умолчанию 'IReadableRepository <' TEntity> 'для переменной вывода типа. Тип 'IReadableRepository <' TEntity> 'несовместим с типом' IListableRepository <'TEntity>'. Попробуйте добавить дополнительные ограничения типа

Итак, я думаю, мои вопросы:

  1. Есть ли в C # шаблон проектирования, который позволил бы мне использовать этот метод, не вызывая взрыва интерфейса?
  2. Если нет, то может ли решение F # решить эту проблему для меня? Или я лаю не на том дереве?
  3. Если F # может решить эту проблему для меня, как должна выглядеть сигнатура метода (потому что я знаю, что моя не верна, но я не могу найти хороший пример чего-то подобного в Интернете)?
  4. Поскольку этот метод F # будет использоваться C #, будет ли он использовать вывод F # или C #?
  5. Я пропускаю какую-то огромную причину, почему это было бы плохой идеей? До этого у меня были плохие идеи, и я не против того, чтобы мне не пытались это делать.

Интерфейсы

По запросу, вот основные интерфейсы, которые использовались в примерах:

public interface IRepository<TEntity>
    where TEntity : class {
}

public interface IReadableRepository<TEntity> :
    IRepository<TEntity>
    where TEntity : class {
    #region Read
    Task<TEntity> FindAsync(TEntity entity);
    #endregion
}

public interface IListableRepository<TEntity> :
    IRepository<TEntity>
    where TEntity : class {
    #region Read
    IQueryable<TEntity> Entities { get; }
    #endregion
}

public interface ISearchableEntity<TSelector> {
    bool Matches(TSelector selector);
}

Решение

Большое спасибо Зорану Хорвату ниже. Это решение основано на его идее и было бы невозможно без него. Я просто абстрагировал его немного дальше для своих целей и переместил метод FixTypes в методы расширения. Вот окончательное решение, к которому я пришел:

public interface IMixedRepository<TRepository, TEntity>
    where TRepository: IRepository<TEntity>
    where TEntity : class { }

public static class MixedRepositoryExtensions {
    public static TRepository AsMixedRepository<TRepository, TEntity>(
        this IMixedRepository<TRepository, TEntity> repository)
        where TRepository : IMixedRepository<TRepository, TEntity>, IRepository<TEntity>
        where TEntity : class
        => (TRepository)repository;
}

public static Task<TEntity> FindBySelectorAsync<TRepository, TEntity, TSelector>(
        this IMixedRepository<TRepository, TEntity> repository,
        TSelector selector)
        where TRepository : 
            IMixedRepository<TRepository, TEntity>, 
            IReadableRepository<TEntity>, 
            IListableRepository<TEntity>
        where TEntity : class, ISearchableEntity<TSelector>
        => repository.AsMixedRepository().Entities.SingleAsync(selector);

public class ProjectRepository :
    IMixedRepository<IProjectRepository, Project>,
    IReadableRepository<Project>,
    IListableRepository<Project>
{ ... }

Наконец, метод метода расширения может быть вызван через:

await (new ProjectRepository())
    .FindBySelectorAsync(0);

В этом решении, однако, отсутствует некоторая статическая типизация, так как он использует понижающее преобразование. Если вы преобразуете смешанный репозиторий как репозиторий, который он не реализует, это вызовет исключение. И из-за дополнительных ограничений на зависимости циклических ограничений, это можно нарушить во время выполнения. Для полностью статической типизированной версии см. Ответ Зорана ниже.


Альтернативное решение

Другое решение, основанное на ответе Зорана, которое обеспечивает статическую типизацию:

public interface IMixedRepository<TRepository, TEntity>
    where TRepository: IRepository<TEntity>
    where TEntity : class {
    TRepository Mixed { get; }
}

public static class MixedRepositoryExtensions {
    public static TRepository AsMixedRepository<TRepository, TEntity>(
        this IMixedRepository<TRepository, TEntity> repository)
        where TRepository : IMixedRepository<TRepository, TEntity>, IRepository<TEntity>
        where TEntity : class
        => repository.Mixed;
}

public static Task<TEntity> FindBySelectorAsync<TRepository, TEntity, TSelector>(
    this IMixedRepository<TRepository, TEntity> repository,
    TSelector selector)
    where TRepository : 
        IMixedRepository<TRepository, TEntity>, 
        IReadableRepository<TEntity>, 
        IListableRepository<TEntity>
    where TEntity : class, ISearchableEntity<TSelector>
    => repository.AsMixedRepository().Entities.SingleAsync(selector);

public class ProjectRepository :
    IMixedRepository<IProjectRepository, Project>,
    IReadableRepository<Project>,
    IListableRepository<Project>
{ 
    IProjectRepository IMixedRepository<IProjectRepository, Project>.Mixed { get => this; }
    ... 
}

Это можно назвать так же. Единственное отличие состоит в том, что вы должны применять его в каждом хранилище. Хотя это не такая большая проблема.

Ответы [ 2 ]

1 голос
/ 21 апреля 2019

Я подозреваю, что проблема возникает потому, что TEntity определяется только косвенно или, так сказать, транзитивно. Для компилятора единственный способ выяснить, что такое TEntity, - это изучить TRepository в глубину. Однако компилятор C # не проверяет типы подробно, а только наблюдает за их немедленной подписью.

Я считаю, что, удалив TRepository из уравнения, все ваши проблемы исчезнут:

public static class MixedRepositoryExtensions {
    public static Task<TEntity> FindBySelectorAsync<TEntity, TSelector>(
        this IReadableAndListableRepository<TEntity> repository,
        TSelector selector)
        where TEntity : class, ISearchableEntity<TSelector>
        => repository.Entities.SingleOrDefaultAsync(x => x.Matches(selector));
}

Когда вы применяете этот метод к конкретному объекту, реализующему интерфейс репозитория, его собственный параметр универсального типа будет использоваться для вывода сигнатуры метода FindBySelectorAsync.

Если проблема заключается в возможности указать список ограничений для репозитория в нескольких неравных методах расширения, то я думаю, что платформа .NET - это ограничение, а не сам C #. Поскольку F # также компилируется в байт-код, универсальные типы в F # будут подпадать под те же ограничения, что и C #.

Я не смог найти динамическое решение, которое решает все типы на лету. Однако есть одна хитрость, которая сохраняет все возможности статической типизации, но требует, чтобы каждый конкретный репозиторий добавлял один дополнительный метод получения свойства. Это свойство не может быть унаследовано или присоединено как расширение, поскольку оно будет отличаться типом возвращаемого значения в каждом конкретном типе. Вот код, который демонстрирует эту идею (свойство просто называется FixTypes):

public class EntityHolder<TTarget, TEntity>
{
    public TTarget Target { get; }

    public EntityHolder(TTarget target)
    {
        Target = target;
    }
}

public class PersonsRepository
    : IRepository<Person>, IReadableRepository<Person>,
      IListableRepository<Person>
{
    public IQueryable<Person> Entities { get; } = ...

    // This is the added property getter
    public EntityHolder<PersonsRepository, Person> FixTypes =>
        new EntityHolder<PersonsRepository, Person>(this);
}

public static class MixedRepositoryExtensions 
{
    // Note that method is attached to EntityHolder, not a repository
    public static Task<TEntity> FindBySelectorAsync<TRepository, TEntity, TSelector>(
        this EntityHolder<TRepository, TEntity> repository, TSelector selector)
        where TRepository : IReadableRepository<TEntity>, IListableRepository<TEntity>
        where TEntity : class, ISearchableEntity<TSelector>
        => repository.Target.Entities.SingleOrDefaultAsync(x => x.Matches(selector));
        // Note that Target must be added before accessing Entities
}

Репозиторий с определенным геттером свойства FixTypes может использоваться обычным способом, но метод расширения определяется только в результате его свойства FixTypes:

new PersonsRepository().FixTypes.FindBySelectorAsync(ageSelector);
0 голосов
/ 21 апреля 2019

Не слишком ли перепроектирована эта структура хранилища?Либо репозиторий доступен только для чтения, либо он предназначен для чтения и записи.

public interface IReadOnlyRepository<TEntity>
    where TEntity : class
{
    Task<TEntity> FindAsync(TEntity entity);
    IQueryable<TEntity> Entities { get; }
    // etc.
}

// The read-write version inherits from the read-only interface.
public interface IRepository<TEntity> : IReadOnlyRepository<TEntity>
    where TEntity : class
{
    void Update(TEntity entity);
    void Insert(TEntity entity);
    // etc.
}

Кроме того, вы можете избавиться от TSelector, изменив дизайн на

public interface ISelector<TEntity>
    where TEntity : class
{
    bool Matches(TEntity entity);
}

Теперь только одинтребуется параметр типа

public static class MixedRepositoryExtensions {
    public static Task<TEntity> FindBySelectorAsync<TEntity>(
        this IReadOnlyRepository<TEntity> repository,
        ISelector<TEntity> selector
    ) where TEntity : class
        => repository.Entities.SingleOrDefaultAsync(x => selector.Matches(x));
}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...