C#/ F#

时间:2019-04-21 16:39:11

标签: c# generics f#

一段时间以来,我一直在尝试使此静态扩展之类的东西起作用:

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>

我从埃里克·利珀特(Eric Lippert)那里发现了一个暗示,即使用F#可能会有所帮助(因为我越来越绝望):

Generics: Why can't the compiler infer the type arguments in this case?

我在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. 由于C#将使用此F#方法,因此会使用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);
}

解决方案

非常感谢以下Zoran Horvat。该解决方案基于他的想法,没有它就不可能。我只是出于自己的目的对其进行了抽象,然后将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);

但是,由于该解决方案使用向下转换,因此缺少一些静态类型。如果将混合存储库向下转换为未实现的存储库,则会引发异常。并且由于对循环约束依赖关系的进一步限制,有可能在运行时打破它。有关完全静态类型的版本,请参见下面的Zoran答案。


替代解决方案

另一种基于Zoran答案的解决方案,它确实实现了静态类型输入:

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 个答案:

答案 0 :(得分:1)

我怀疑是因为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#相同的约束。

我找不到动态解决方案,该解决方案可以即时解决所有类型。但是,有一个技巧可以保留完整的静态键入功能,但是需要每个具体的存储库添加一个附加的属性getter。此属性不能继承或作为扩展附加,因为每种具体类型的返回类型都不同。这是演示此想法的代码(该属性简称为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属性getter的存储库可以按常规方式使用,但是扩展方法仅根据其FixTypes属性的结果来定义:

new PersonsRepository().FixTypes.FindBySelectorAsync(ageSelector);

答案 1 :(得分:0)

此存储库结构是否设计过度?存储库是只读的还是读写的。

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