c# - C#/F# 中基于约束的类型推断

标签 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 错误:

The type arguments for method 'MixedRepositoryExtensions.FindBySelectorAsync(TRepository, TSelector)' cannot be inferred from the usage. Try specifying the type arguments explicitly.

示例调用方法(其中 ProjectRepository 扩展了 IReadableRepository 和 IListableRepository 并且项目扩展了 ISearchableEntity):

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

我考虑过在所有调用者上显式定义它们,但是,这种方法并不理想,因为它会在很多地方使用并且有很多长命名类型。

我也考虑过像这样将两个接口(interface)继承为一个接口(interface):

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#,但发现很少有关于将类型约束到多个接口(interface)(或与此相关的任何特定接口(interface))的文档,并且无法克服一些错误。这是我最后一次尝试。我意识到该方法不会返回相同的值,我只是暂时尝试让约束很好地发挥作用。对不起,如果做得不好,这是我第一次玩 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: The implicit instantiation of a generic construct at or near this point could not be resolved because it could resolve to multiple unrelated types, e.g. 'IListableRepository <'TEntity>' and 'IReadableRepository <'TEntity>'. Consider using type annotations to resolve the ambiguity

FS0071: Type constraint mismatch when applying the default type 'IReadableRepository<'TEntity>' for a type inference variable. The type 'IReadableRepository<'TEntity>' is not compatible with the type 'IListableRepository<'TEntity>' Consider adding further type constraints

所以,我想我的问题是:

  1. 在 C# 中是否有一种设计模式可以让我在不引入接口(interface)爆炸的情况下使用这种方法?
  2. 如果没有,F# 更高级的推理能否为我解决这个问题?还是我找错了树?
  3. 如果 F# 可以为我解决这个问题,那么方法签名应该是什么样子(因为我知道我的不对,但我在网上找不到这样的好例子)?
  4. 既然这个F#方法会被C#使用,那么它是使用F#的推理,还是C#的推理?
  5. 我是否忽略了为什么这不是一个好主意的重要原因?众所周知,我以前有过糟糕的想法,我不反对被告知不要尝试这个。

接口(interface)

根据要求,以下是示例中使用的主要接口(interface):

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

这个也可以这样调用。唯一的区别是您必须在每个存储库中实现它。不过并没有那么痛苦。

最佳答案

我怀疑问题的发生是因为 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));
}

当您将此方法应用于实现存储库接口(interface)的具体对象时,它自己的泛型类型参数将用于推断 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);

关于c# - C#/F# 中基于约束的类型推断,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/55784662/

相关文章:

swift - 在扩展中创建通用排序函数

visual-studio - 转到 F# 库的定义

syntax - 在 F# 脚本文件中创建 C# 类的实例时出错

c# - 使用 AutoMapper 创建通用对象的新实例

f# - 未处理的异常 : System. ArgumentOutOfRangeException : Schema mismatch for feature column 'Features' : expected Vector<R4>, 得到 Vector<R8>

c# - 如何在 C# 中一次分配多个对象属性?

URL 查询字符串的 C# 正则表达式

c# - 将标签绑定(bind)到变量

c# - 使用 WCF 实现观察者模式

delphi - TDictionary<TVehicle, TPerson> 的 Equals 和 GetHashCode