.NET泛型应该继承一个泛型参数类型的好理由是什么?

18

这篇文章是在 这篇 的延续。

我试图理解是否只有我一个人需要 .NET 通用类型能够继承其中一个泛型参数类型的能力。

挑战在于收集支持此功能的充分理由,或者了解没有任何理由的情况。

我会将我的原因作为回答放到这个问题中 - 请见下文。

我请求各位在这篇文章中添加自己的原因作为回答。

如果您不同意此功能有用或没有好的理由支持它 - 请勿在这里发布任何内容,但您可以在最初引发此问题的原始帖子 这里 发布。

P.S.

一些 C++ 模式在 .NET 中是不相关的。例如,在他的优秀著作《现代 C++ 设计》中,Andrei Alexandrescu 描述了如何在编译时创建类型列表。当然,这种模式在 .NET 中是不相关的,因为如果我需要一个类型列表,我只需创建一个 List<Type> 并用类型填充它。因此,让我们试着提出与 .NET 框架相关的原因,而不是盲目地将 C++ 编程技术翻译成 C#。

P.P.S.

当然,这个讨论只是纯学术性的。即使有一百个支持此功能的充分理由被呈现出来,这也永远不会被实现。


这是我自己的感觉还是这似乎是对同一个问题的新方法:https://dev59.com/83I-5IYBdhLWcg3wc4Cw,但并没有提供任何新的东西? - IAbstract
这篇文章以短语“本文是上一篇文章的延续”开头,其中“上一篇文章”是指您在评论中提到的文章的超链接。两篇文章都是由同一个人 - 我创建的。第一篇文章是一般讨论,包括优缺点,而我希望在这里收集人们对于他们也错过了这个功能的指示,而不仅仅是我自己。 - mark
我认为你应该将自己的起始原因移动到答案中,而不是包含在问题中。现在这样做会让你的问题更加难以阅读。 - Brian
同意。我已经添加了自己的答案。 - mark
我认为应该允许这种方式用于接口(并且允许使用约束来指定它是一个接口)。现在我有一些代码,将会真正地利用到这个约束,就像这样: public interface ISomeInterface<TInterface> : TInterface where T : interface { } - TravisK
8个回答

8
时常会遇到一些实现问题,非常遗憾的是,C<T> 不能继承 T,这让我深感遗憾。不幸的是,我从未记录过这些问题,所以我只能描述最近遇到的问题 - 就是我现在遇到的问题。具体内容如下:
我们的系统可以通过元数据进行扩展,在运行时才可使用这些元数据。使用 Reflection.Emit 将元数据翻译成动态生成的类型。不幸的是,动态生成的类型必须满足以下条件:
  1. 它必须派生自其他类型,这种类型作为动态类型创建者的参数提供。该类型称为「祖先类型」,始终是一个静态编译类型。
  2. 它必须实现多个接口,例如我们的接口 IDynamicObjectSystem.ComponentModel.INotifyPropertyChangedCsla.Core.IMobileObject。请注意,此条件对祖先类型施加了某些约束。例如,祖先类型可能不实现 IDynamicObject 接口,除非所有接口方法都是抽象实现。还有其他限制,全部都必须检查。
  3. 它应该(而且确实)覆盖 object 方法中的 EqualsToStringGetHashCode 方法。
目前,我不得不使用 Reflection.Emit 来发出所有的 IL 代码以满足这些条件。当然,某些任务可以转发到静态编译代码。例如,object.Equals(object) 方法的覆盖调用了 DynamicObjectHelper(IDynamicObject, IDynamicObject),它是一个静态编译的 C# 代码,完成了比较两个动态对象相等性的大部分工作。但这只是个例外,大多数的实现都是发出的,这让人痛苦。
如果能编写一个通用类型,如 DynamicObjectBase<T>,它将与祖先类型一起实例化,并作为动态生成类型的实际基类型,那么不是很好吗?就我看来,通用类型 DynamicObjectBase<T> 可以在静态编译的 C# 代码中实现大部分动态类型要求。动态发出的类型将继承它,并可能只覆盖几个简单的虚拟方法。
总之,让 C<T> 继承自 T 将极大地简化发出动态类型的任务。

3
我正在使用GTK# 3。有一个“Widget”类定义,它是所有其他GTK#小部件的基础,例如Window、Label、Frame。不幸的是,框架使得改变小部件的背景颜色有点复杂,你需要重写Widget的一个方法来实现它(天啊…)。
因此,如果我想要一个Label,可以随意设置背景颜色,我应该这样做:
class BGLabel : Label
{
    private Color _bgColor;

    public Color BackgroundColor
    {
        get { return this._bgColor; }
        set { this._bgColor = value; this.QueueDraw(); /* triggers OnDraw */ }
    }

    protected override void OnDraw(...)
    {
        ... /* here we can use _bgColor to paint the background */
    }
}

现在,如果我想要将这个不错的功能应用到更多的小部件上,我需要为每个小部件都执行以上操作。如果“class C< T > : T”是可编译的,那么我只需要执行以下操作:

class C<T> : T where T : Widget
{
    private Color _bgColor;

    public Color BackgroundColor
    {
        get { return this._bgColor; }
        set { this._bgColor = value; this.QueueDraw(); /* triggers OnDraw */ }
    }

    protected override void OnDraw(...)
    {
        ... /* here we can use _bgColor to paint the background */
    }
}

我可以使用“C< xxx > bgw = new C< xxx >();”代替“BGxxx bgw = new BGxxx();”。


1

有一次我需要这样做的时候是因为我有数据库对象 (例如 MyEntity),还有相应的历史记录对象 (例如 MyEntityHistory)。这些实体完全共享相同的属性,除了历史记录对象上只有两个存在: ValidFromValidTo

所以,为了显示对象的当前状态以及历史数据,我可以在它们之间进行UNION查询,然后使用 Dapper 或 EF Core 的映射功能来将结果映射到 List<MyEntityHistory> 历史记录中。

当然,我想避免重复属性,所以理想情况下我会继承实体类来获取其属性。但是,我也想从某个基类中获取 ValidFromValidTo 属性,所以我最终会遇到菱形问题。一个帮助我的解决方案是定义一个像这样的历史记录类:

class MyEntityHistory : HistoryEntity<MyEntity>

其中HistoryEntity将被定义为:

class HistoryEntity<TEntity> : TEntity where TEntity: class
{
    public DateTime ValidFrom { get; set; }
    public DateTime ValidTo { get; set; }
}

很遗憾,无法实现HistoryEntity<TEntity>的这样一个实现。
请注意,通过组合嵌入实体类(MyEntity)不是一个选项,因为Dapper或EF Core的映射器无法处理嵌套对象。

0

我已经编写了CodeFirstWebFramework,它是一个提供所有工具的dll,用于编写数据库驱动的Web服务器,表会自动从代码中的类生成,并将URL粘合到AppModule类中的方法调用。

该DLL提供了一些基本的AppModule类-AdminModule和FileSender。这些实现了管理(登录、维护用户、更新设置),以及在URL与任何其他AppModule不相关时返回文件。

DLL的消费者可以重写抽象的AppModule类,以向其所有自己的AppModules添加其他功能(这些将派生自重写)。

我也希望他们能够覆盖AdminModule和/或FileSender。但是,在一些情况下,让他们能够覆盖其中一个模块(因此可以访问其中的默认代码),同时还要子类化自己的AppModule实现是非常有用的(甚至必要)。

我认为我有一个非常好的想法,如下所示:

在我的DLL中:

abstract class AppModule {
    // Contains base class implementation, including virtual Database property, code to 
    // retrieve files, code to call appropriate method depending on url, code to collect 
    // result and return it to the web browser, etc.
}

abstract class AdminModule<T> where T:AppModule, new() : T {
    // Contains implementation of Admin methods - create/edit users, 
    // edit settings, backup database, etc.
}

class AdminModule : AdminModule<AppModule> {
    // No code needed - inherits implementation from AdminModule<AppModule>
}

在消费者程序中(不同的命名空间):
abstract class AppModule : AppModule {
    // Contains additional properties and methods. Also overides some virtual methods,
    // e.g. returns a subclass of Database with additional methods, or retrieve files
    // from the database instead of the file system.
}

class AdminModule : AdminModule<AppModule> {
    // Additional code for Admin methods specific to this application
    // Does not need code for methods in base class - inherits implementation from 
    // AdminModule<AppModule>
}

在这种特定情况下,由于T是一个AppModule的限制,并且AdminModule<T>本身是抽象的,我看不出为什么到目前为止提出的任何反对意见都会适用。
编译器可以检查派生类中是否实现了任何抽象方法。构造AdminModule<T>没有危险,因为它是抽象的。
如果T是密封类、派生自AdminModule<T>或比AdminModule<T>的可访问性更低,则编写AdminModule<T>将导致编译时错误。

0
泛型的基本规则是“泛型类型的内容必须在泛型参数上定义明确”。让我们看看这如何适用于以下代码:
public abstract class AbstractBase
{
    public abstract string MyMethod();
}

public class SomeType<T> : T
{
}

public class SomeUsage
{
    void Foo()
    {
        // SomeType<AbstractBase> does not implement AbstractBase.MyMethod
        SomeType<AbstractBase> b = new SomeType<AbstractBase>();
    }
}

那么我们尝试实现MyMethod()

public class SomeType<T> : T
{
    public override string MyMethod()
    {
        return "Some return value";
    }
}

public class SomeUsage
{
    void Foo()
    {
        // SomeType<string> does not inherit a virtual method MyMethod()
        SomeType<string> b = new SomeType<string>();
    }
}

因此,让我们制定一个要求,即 T 必须派生自 AbstractBase

public abstract class DerivedAbstractBase : AbstractBase
{
    public abstract string AnotherMethod();
}

public class SomeType<T> : T
    where T : AbstractBase
{
    public override string MyMethod()
    {
        return "Some return value";
    }
}

public class SomeUsage
{
    void Foo()
    {
        // SomeType<DerivedAbstractBase> does not implement DerivedAbstractBase.AnotherMethod()
        SomeType<DerivedAbstractBase> b = new SomeType<DerivedAbstractBase>();
    }
}

概述:

如果考虑到基本类型的所有限制,你会发现从通用参数派生是毫无意义的,因为你已经非常受限了。


我真的不明白这里的问题。当一个人写 SomeType<AbstractBase> b = new SomeType<AbstractBase>(); 时,编译器应该将其解释为实例化抽象类型的尝试 - 然后编译失败。当然,可以从 SomeType<AbstractBase> 派生出来,然后被编译器强制实现 SomeType<AbstractBase> 的抽象 API,其中包括 AbstractBase 的抽象 API。 - mark
@mark:编译器不强制执行此操作 - CLI(底层虚拟机)会执行。 - Sam Harwell
1
当然,您是正确的。但这并不改变CLI在SomeType<AbstractBase>类型上强制执行适当语义时不会出现问题的事实 - 它将乐意拒绝对其进行实例化。 - mark
我认为这应该允许在接口中使用。公共接口 ISomeInterface<TInterface>:TInterface 其中 T: 接口 { }与上述SomeType不同,此代码没有实现MyMethod()的问题。现在我有一些代码可以真正利用这些约束。 - TravisK

0

嗯,这取决于情况。如果微软在语言层面上添加了它,那么对于涉及动态生成的代码场景来说,这就没有什么用处了。但是除此之外,是的,这将会非常有帮助。我已经为这个问题点赞了。 - mark
我认为这种类型组合的概念是我最感兴趣的用途。我说这个问题是否与其他语言中的“类装饰器”和“混合类”解决的问题相同? - binki
该链接要求我们登录,因此我无法查看它的内容。 - adrin

0

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

简要概述动机:是为了更轻松地开发我所谓的“编译时顺序强制执行”,这在 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();
    }

正如您所看到的,我们有两个列构建器的接口:“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 {}

除了可能在编译时检测到的继承循环之外,没有任何“如果”的情况。

我认为至少对于接口,这个功能是百分之百必要的。

敬礼


0

允许从类型参数继承的好处在于以下情况:

我想要一个抽象的ViewModel类,它继承自Entity类并具有一些额外的属性和方法。

public abstract class ViewModel<TEntity> : TEntity, IViewModel<TEntity> 
where TEntity : class, IEntity, new() {
    public void SomeMethod() {

    }
}

然后我可以创建一个特定的ViewModel
public class EmployeeViewModel : ViewModel<Employee> {

}

很好!它继承了实体的所有字段,并具有抽象视图模型的标准属性和方法!..
很遗憾现在不能做到。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接