从 C# 中的泛型继承的一个很好的理由



我知道这个主题被讨论过很多次:希望 .NET 泛型可以继承其中一个泛型参数类型的充分理由是什么?

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

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

简而言之,动机是使开发我称之为编译时顺序执行的东西变得更加容易,这在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;
}

由于每一行都返回一些接口,我们希望以这样的方式返回它们,以便开发人员不会在操作顺序上犯错误,并且在编译时将强制执行有效的用法,这也简化了 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 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 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<...> 来组合现有接口(或"后续步骤"(,这在状态机开发中非常有用。

当然,这个特定的问题可以通过在 C# 中添加"联接"接口的可能性来解决,但很明显,如果可以从泛型继承,那么问题根本不存在,而且很明显编译时顺序强制执行非常有用。

顺便说一句。对于语法,例如

    interface IInterface<T> : T {}
除了继承循环

之外,没有任何"假设"情况,继承循环可能在编译时检测到。

我认为至少对于接口,此功能是 100% 需要的

问候

你大大低估了这个功能的复杂性。就在我头顶上,如果我写怎么办

public class Fooer
{
    public void Foo();
}
public class Generic<T> : T
{
    public void Foo();
}
var genericFooer = new Generic<Fooer>();

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

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

更新:要记住的另一件事是,这将有很大的潜力破坏使用反射的现有代码。这将包括实体框架,IoC容器,自动映射器之类的东西,可能还有很多。如果这些示例中的任何一个例子没有通过这样的更改引入新的用例,这将导致错误或意外行为,我会感到惊讶。

当然,对于

任何语言变化来说,这在某种程度上都是正确的,但如此重大的更改可能会产生很大的影响。同样,这是一个很大的成本,必须与相对较小的收益相平衡。

更新2:如果我们将其限制为仅接口,我们仍然会遇到此问题:

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

这些是不兼容的,因为您不能有两个仅因返回类型而异的方法,即使在接口上也是如此。

我查看了评论,这是我的结论(与我的意见合并(。

  1. 允许从泛型继承并不像我想象的那么简单,即使只对接口也是如此。
  2. Fluent API的解决方案是必须的(可能不是通过泛型继承,而是通过稍后建议的"连接接口"的新语法(,因为它非常易于使用,并且它允许在编译时拒绝许多开发人员的错误 - 它的受欢迎程度只会增长。

我相信 1 和 2 是可能的,但目前我决定打印我描述的问题的最佳解决方案

所以,我把接口改成了这个("新语法"被评论了(

    public interface ITransactionBuilder
    {
        IRowBuilder UpdateTable(string tableName);
    }
    public interface IRowBuilder
    {
        IColumnBuilder Row(long rowid);
    }
    public interface INextColumnBuilder : IColumnBuilder, IRowBuilder, ITransactionBuilder, ITransactionHolder { }
    public interface IColumnBuilder
    {
        INextColumnBuilder Column(string columnName, Object columnValue);
        //I still want to write something like this here!!! But currently we may only to comment it :(
        //<IColumnBuilder, IRowBuilder, ITransactionBuilder, ITransactionHolder> Column(string columnName, Object columnValue)
    }
    public interface ITransactionHolder
    {
        ITransaction GetTransaction();
    }
    public interface ITransaction
    {
        //Execute func declaration
    }

因此,开销并不大。与"连接可能性"相比,我们只需要为返回类型声明虚拟接口,然后从实现器中继承它们,所以让我们看看 TransactionBuilder(实现器(...

    class TransactionBuilder : INextColumnBuilder
    //I want to write this: class TransactionBuilder : IColumnBuilder, IRowBuilder, ITransactionBuilder, ITransactionHolder
    //But then we will fail on "return this;" from Column() func, because class TransactionBuilder not inherits from INextColumnBuilder, and again interface joining should solve it...
    {
        public virtual IRowBuilder UpdateTable(string tableName)
        {
            m_currentTable = new TableHolder(tableName);
            m_commands.Add(new UpdateCommand(m_currentTable));
            return this;
        }
        public virtual IColumnBuilder Row(long rowid)
        {
            m_currentRow = new RowHolder(rowid);
            m_currentTable.AddRow(m_currentRow);
            return this;
        }
        public virtual INextColumnBuilder Column(string columnName, Object columnValue)
        //And the dream is: <IColumnBuilder, IRowBuilder, ITransactionBuilder, ITransactionHolder> Column(string columnName, Object columnValue)
        {
            m_currentRow.AddColumn(columnName, columnValue);
            return this;
        }
        public virtual ITransaction GetTransaction()
        {
            return new Transaction(m_commands);
        }
        private ICollection<ICommand> m_commands = new LinkedList<ICommand>();
        private TableHolder m_currentTable;
        private RowHolder m_currentRow;
    }

如您所见,我只有一个实现所有方法的类,我真的看不出有任何理由为每个接口实现许多类(因此,Ivaylo 试图解决不存在的问题;)(。我认为在接口级别拥有状态机就足够了,并且代码可能更简单和紧凑。

。代码中使用的其他类,只是为了完成图片...

    public interface ICommand 
    {
        //Apply func declaration
    }
    public class UpdateCommand : ICommand 
    {
        public UpdateCommand(TableHolder table) { /* ... */ }
        //Apply func implementation
    }
    public class TableHolder
    {
        public TableHolder(string tableName) { /* ... */ }
        public void AddRow(RowHolder row) { /* ... */ }
    }
    public class RowHolder
    {
        public RowHolder(long rowid) { /* ... */ }
        public void AddColumn(string columnName, Object columnValue) { /* ... */ }
    }
    public class Transaction : ITransaction
    {
        public Transaction(IEnumerable<ICommand> commands) { /* ... */ }
        //Execute func implementation
    }

希望 .NET 团队Microsoft的某个人会看到并喜欢它......

更新

我已经重新审视了这个问题,你最后的评论,我根据我对你的问题的解释提出了一个想法。

我同意你最后的说法,事实上,Join<...>接口和假设的IOddAndPrimeCounter : IOddCounter, IPrimeCounter接口之间没有区别。我对第 2 点的意思是,您将无法区分是将生成的连接接口调用为 T1 还是T2

在这种情况下,我建议我自己使用一种稍微不同的方法。我将保留两个接口IFirstColumnBuilderINextColumnBuilder,但稍微修改后者:

interface INextColumnBuilder : 
    IFirstColumnBuilder, ITransactionBuilder, IRowBuilder, ITransactionHolder {}

然后,我将选择IFirstColumnBuilderINextColumnBuilder的一个实现

,如下所示:
public class ColumnBulder : INextColumnBuilder // IFirstColumnBuilder is already implemented
{
}

然后,IRowBulder的典型实现将如下所示:

public class RowBulder : IRowBulder
{
    public IFirstColumnBuilder Column(...)
    {
         // do stuff
         return new ColumnBuilder(...);
    }
}

诀窍在于,虽然ColumnBuilder同时实现了IFirstColumnBuilderINextColumnBuilder,但rowBuilder.Column()的结果将只能访问IFirstColumnBuilder公开的方法。

我知道这并不能解决接口的问题,但你仍然只有一个实现,这意味着要编写的代码更少,接口中的重复代码更少。此外,您的呼叫链将按照您最初想要的方式受到限制。


原始响应

由于我对你的问题有一些想法,并且我自己也是 C# 开发人员,我想出了一些原因来解释为什么这可能不会在 CLR 中发生。

  1. 您的建议需要额外的编译时检查才能T是一个接口。在 C# 中,泛型可以是某些编译时约束的主题。例如

    public class ConstrainedList<T> : List<T> 
        where T: class, IEquatable<T>, new() { }
    

    class约束将T限制为仅引用类型,IEquatable<T>约束将仅允许实现IEquatable<T>接口的类型,new()约束意味着 T 必须具有公共无参数构造函数。如示例中所示,必须同时满足多个限制。

    对于您的建议,CLR 必须支持新的约束,让我们将其称为 interface ,以便T仅限于接口类型。此约束将与 CLR 团队需要实现的new()class约束不兼容,并将导致额外的编译时验证。这种更改是否向后兼容是有争议的,我认为它不会发生,因为隐藏在接口后面的class和值类型都表现为引用类型。因此,interface约束将以类似于class约束的方式起作用,但仍允许实现接口的值类型被视为可接受的 - 换句话说,约束将自相矛盾,并可能导致混淆和/或意外的代码行为。

  2. 我反对这个建议的第二个论点更实际。在 C# 和一般的 CLR 语言中,可以显式实现接口。(如果你不知道显式接口实现是什么以及如何使用它,我已经在文章末尾解释了(。当你有这个Join<T1, T2>接口,并且实现了第 1 点的内容时,那么当 T1T2 提供具有相同签名的方法时,您将遇到麻烦,并且它需要(并且具有(显式实现。所以,当你有这个:

    IColumnBuilder columnBuilder = ...; // Assume a valid column builder is set
    var joinResult = columnBuilder.Column("col", "val");
    joinResult.ConflictingMethod(); // Oops!
    

    如果ConflictingMethod()是在joinResult碰巧继承的T1T2中定义的,并且显式实现了T1T2,那么在这一行,编译器将不知道正在调用哪个实现(T1T2'(。

如果可能的话,上面的示例可能需要对 CLR 进行更重要且不向后兼容的更改;因此现在看来,这种情况发生的可能性更小。


关于显式接口的说明

这是一个非常有用的功能,如果你有一个必须实现多个接口的类,并且至少有两个接口强制执行具有相同签名的方法。问题是您可能需要为每个接口提供不同的实现。这里有一个虚拟的例子来说明这一点:

    public interface IOddNumberCounter
    {
        int Count(params int[] input);
    }
    public interface IPrimeNumberCounter
    {
        int Count(params int[] input);
    }

想象一个测试来验证这些:

    public void TestOddCounter(IOddNumberCounter counter)
    {
        AssertTrue(counter.Count(1, 2, 3, 15) == 3);
    }
    public void TestPrimeCounter(IPrimeNumberCounter counter)
    {
        AssertTrue(counter.Count(2, 3, 15, 16) == 2);
    }

现在,由于某种原因,您需要此类:

    public class OddAndPrimeCounter : IOddNumberCounter, IPrimeNumberCounter
    {
        public int Count(params int[] input)
        {
            // Now what ?
        }
    }

使测试通过OddAndPrimeCounter类的解决方案是显式实现至少一个(或两个(冲突方法。显式实现的语法略有不同:

    public class OddAndPrimeCounter : IOddNumberCounter, IPrimeNumberCounter
    {
        int IOddNumberCounter.Count(params int[] input)
        {
            //count the odds
        }
        int IPrimeNumberCounter.Count(params int[] input)
        {
            //count the primes
        }
    }

最新更新