c# - 在 C# 中继承泛型的一个很好的理由

标签 c# templates generics inheritance

我知道这个主题被讨论过很多次: What are the good reasons to wish that .NET generics could inherit one of the generic parameter types?

但现在我认为它真的需要......

我将尝试用简单的例子来解释为什么我们需要继承泛型。

简而言之,动机是为了让我称之为 Compile Time Order Enforcement 的东西的开发变得更加容易,这在 ORM 框架中非常流行。

假设我们正在构建数据库框架。

以下是如何使用此类框架构建交易的示例:

public ITransaction BuildTransaction(ITransactionBuilder builder)
{
    /* Prepare the transaction which will update specific columns in 2 rows of table1, and one row in table2 */
    ITransaction transaction = builder
        .UpdateTable("table1") 
            .Row(12)
            .Column("c1", 128)
            .Column("c2", 256)
            .Row(45)
            .Column("c2", 512) 
        .UpdateTable("table2")
            .Row(33)
            .Column("c3", "String")
        .GetTransaction();

    return transaction;
}

由于每一行都返回一些接口(interface),我们希望以这样的方式返回它们,开发人员不会在操作顺序上出错,并且在编译时将强制使用有效的用法,这也使得实现和使用更加简单TransactionBuilder,因为开发人员不会犯这样的错误:

    { 
        ITransaction transaction = builder
            .UpdateTable("table1") 
            .UpdateTable("table2")  /*INVALID ORDER: Table can't come after Table, because at least one Row should be set for previous Table */
    }
    // OR
    {
        ITransaction transaction = builder
            .UpdateTable("table1") 
                .Row(12)
                .Row(45) /* INVALID ORDER: Row can't come after Row, because at least one column should be set for previous row */
    }

现在让我们看看今天的 ITransactionBuilder 接口(interface),没有从泛型继承,它将在编译时强制执行所需的顺序:

    interface ITransactionBuilder
    {
        IRowBuilder UpdateTable(string tableName);
    }
    interface IRowBuilder
    {
        IFirstColumnBuilder Row(long rowid);
    }
    interface IFirstColumnBuilder
    {
        INextColumnBuilder Column(string columnName, Object columnValue);
    }
    interface INextColumnBuilder : ITransactionBuilder, IRowBuilder, ITransactionHolder
    {
        INextColumnBuilder Column(string columnName, Object columnValue);
    }
    interface ITransactionHolder
    {
        ITransaction GetTransaction();
    }
    interface ITransaction
    {
        void Execute();
    }

如您所见,我们有 2 个用于列生成器“IFirstColumnBuilder”和“INextColumnBuilder”的接口(interface),它们实际上不是必需的,请记住这是编译时状态机的非常简单的示例,而在更复杂的问题中,不必要的接口(interface)将急剧增长。

现在让我们假设我们可以从泛型继承并准备这样的接口(interface)

interface Join<T1, T2> : T1, T2 {}
interface Join<T1, T2, T3> : T1, T2, T3 {}
interface Join<T1, T2, T3, T4> : T1, T2, T3, T4 {} //we use only this one in example

然后,我们可以在不影响顺序的情况下,将我们的界面重写为更直观的风格和单列构建器

interface ITransactionBuilder
{
    IRowBuilder UpdateTable(string tableName);
}
interface IRowBuilder
{
    IColumnBuilder Row(long rowid);
}
interface IColumnBuilder
{
    Join<IColumnBuilder, IRowBuilder, ITransactionBuilder, ITransactionHolder> Column(string columnName, Object columnValue);
}
interface ITransactionHolder
{
    ITransaction GetTransaction();
}
interface ITransaction
{
    void Execute();
}

所以我们使用 Join<...> 来组合现有接口(interface)(或“后续步骤”),这在状态机开发中非常有用。

当然,这个具体问题可以通过在 C# 中添加“加入”接口(interface)的可能性来解决,但很明显,如果可以从泛型继承,那么问题就根本不存在了,而且很明显编译执行时间顺序是非常有用的事情。

顺便说一句。对于像这样的语法

    interface IInterface<T> : T {}

除了可以在编译时检测到的继承循环外,没有任何“假设”情况。

我认为至少对于接口(interface)来说,这个特性是 100% 需要的

问候

最佳答案

您大大低估了此功能的复杂性。就在我的脑海里,如果我写的话会怎样

public class Fooer
{
    public void Foo();
}

public class Generic<T> : T
{
    public void Foo();
}

var genericFooer = new Generic<Fooer>();

现在我遇到了成员签名冲突的问题。

支持像您描述的流畅 api 的大量接口(interface)只是用例的一小部分,正如我在评论中提到的那样,元编程相对容易支持。我不确定是否有人创建了一个 T4 模板来将 DSL 转换为接口(interface)声明,但这肯定是可行的。我认为在创建接口(interface)时并没有考虑到在有限状态机中表示每个转换组合。这种改变当然不值得,我敢肯定实现起来会非常复杂,并且会出现各种像这样的奇怪情况。

更新:另一件要记住的事情是,这很可能会破坏使用反射的现有代码。这将包括诸如 Entity Framework、IoC 容器、Automapper 之类的东西,可能还有很多。如果这些示例中的任何一个没有通过像这样会导致错误或意外行为的更改引入新的用例,我会感到惊讶。

当然,这在某种程度上对于任何语言更改都是正确的,但是如此重要的更改可能会产生很大的影响。同样,这是一笔巨大的成本,必须与相对较小的 yield 相平衡。

更新 2:如果我们只限制在接口(interface)上,我们仍然会遇到这个问题:

public interface IFooer
{
    public void Foo();
}

public interface IGeneric<T> : T
{
    public int Foo();
}

这些是不兼容的,因为即使在一个接口(interface)上,您也不能有两个仅在返回类型上不同的方法。

关于c# - 在 C# 中继承泛型的一个很好的理由,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/26157721/

相关文章:

generics - 在 Kotlin 中从映射中过滤空键和值

Java泛型

c# - 软删除实体的导航属性

c# - Unity/C#:按值进行类特化(相当于 C++ std::array<int>)

c# - 如何限制事件一次触发多次

bash - 是否可以仅使用命令行从模板创建新的 git 存储库?

c++ - 复制构造函数调用另一个类的复制构造函数

java - 为什么这里不进行隐式转换?

javascript - 使用 Asp.net 和 angularjs 的 Odata

c# - 立即窗口中的 F#