【问题标题】:A good reason to inherit from generics in C#从 C# 中的泛型继承的一个很好的理由
【发布时间】:2014-10-02 09:12:27
【问题描述】:

我知道这个主题被讨论过很多次: 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;
}

由于每一行都返回了一些接口,所以我们希望这样返回,开发者不会弄错操作顺序,并且在编译时会强制执行有效的使用,这也使得实现和使用更简单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 个用于 Column builder “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% 需要的

问候

【问题讨论】:

  • 你的问题到底是什么?
  • 我明白你的意思。没有不好的感觉,但是,我认为这篇文章与 SO 无关。根据网站的使用条款,您应该提出一个可以提供有效解决方案的可挽救问题。您的帖子更像是对 CLR 开发团队的功能请求,我怀疑您是否会在本网站范围内以及您撰写的帖子的生命周期内获得可行的解决方案。
  • 其实我只是想听听其他人对我的想法有何看法?我是 c# 新手,大约 4 周,来自 c++ 世界。
  • 我不确定减少大型流畅接口所需的接口数量实际上是添加如此复杂(在实现方面)功能的一个很好的理由。如果您真的想这样做,请使用一些元编程方法(例如 T4)从 DSL 生成流畅的接口。

标签: c# templates generics inheritance


【解决方案1】:

您大大低估了此功能的复杂性。就在我的头上,如果我写怎么办

public class Fooer
{
    public void Foo();
}

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

var genericFooer = new Generic<Fooer>();

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

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

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

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

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

public interface IFooer
{
    public void Foo();
}

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

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

【讨论】:

  • 我同意这个特性对类有问题,可能它应该只对接口“开放”,所以我们可以写 interface I1: T1 where T: interface。如果所有类型都是接口,那有什么问题?
  • 好吧,那么请看下面 Ivaylo Slavov 的回答,如果您将其限制在接口上,它解决了实现该功能的问题。
  • 是的,也许你是对的,对于简单流畅的 API,添加仅支持来自组合接口的类型声明会很容易......我希望这不是那么复杂
  • 我在帖子中回答了我的结论以及我是如何解决的。
【解决方案2】:

我查看了 cmets,这是我的结论(与我的观点合并)。

  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 团队的人会看到并喜欢它...

【讨论】:

  • 如果有任何现实世界的 DSL,其中像 INextColumnBuilder 这样的组合接口的数量在大小或复杂性方面是整个流畅 api 代码的重要组成部分,我真的会感到惊讶。我理解理论上这个数字可能非常大,但我的估计是它几乎总是不会。而且在它非常大的情况下,无论如何,整体代码可能会非常复杂
【解决方案3】:

更新

根据我对您问题的解释,我重新审视了这个问题,您的最后一个 cmets 和我想出了一个想法。

我同意你最后的说法,Join&lt;...&gt; 接口和假设的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&lt;T&gt; 约束将仅允许实现 IEquatable&lt;T&gt; 接口的类型,new() 约束意味着 T 必须具有公共无参数构造函数。多个限制,如示例中的,必须同时满足。

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

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

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

    如果ConflictingMethod()joinResult 恰好继承自的T1T2 中定义,并且T1T2 被显式实现,那么在这一行编译器将不会知道正在调用哪个实现(T1's 或 T2's)。

如果可能的话,上述示例可能需要对 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
        }
    }

【讨论】:

  • 我不明白“class OddAndPrimeCounter : IOddNumberCounter, IPrimeNumberCounter”中的相同方法导致的现有问题与“class OddAndPrimeCounter : Join”有何不同其他接口通过泛型,只是另一种声明接口的方式吗?
  • 当你有Join&lt;IOddNumberCounter, IPrimeNumberCounter&gt;接口的实例时,调用Count(...)时会使用哪个逻辑?我的意思是,编译器如何知道要调用连接接口的哪个确切成员?
  • 我认为答案与 IOddAndPrimeCounter 相同,其中“公共接口 IOddAndPrimeCounter : IOddNumberCounter, IPrimeNumberCounter”...而且这是一个模棱两可的调用错误,但这种情况在 C# 中并不是什么新鲜事...
  • @EvgenyMamontov,请参阅我的更新,虽然我找不到使您的泛型建议起作用的方法,但我为实现代码中的代码样板提出了一种解决方法。
  • 我在帖子中回答了我的结论以及我是如何解决的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-12-19
  • 2011-05-01
  • 1970-01-01
相关资源
最近更新 更多