从 C# 中的泛型继承的一个很好的理由
本文关键字:一个 很好 理由 泛型 继承 | 更新日期: 2023-09-27 18:25:22
我知道这个主题被讨论过很多次:希望 .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();
}
这些是不兼容的,因为您不能有两个仅因返回类型而异的方法,即使在接口上也是如此。
我查看了评论,这是我的结论(与我的意见合并(。
- 允许从泛型继承并不像我想象的那么简单,即使只对接口也是如此。
- 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
。
在这种情况下,我建议我自己使用一种稍微不同的方法。我将保留两个接口IFirstColumnBuilder
和INextColumnBuilder
,但稍微修改后者:
interface INextColumnBuilder :
IFirstColumnBuilder, ITransactionBuilder, IRowBuilder, ITransactionHolder {}
然后,我将选择IFirstColumnBuilder
和INextColumnBuilder
的一个实现
public class ColumnBulder : INextColumnBuilder // IFirstColumnBuilder is already implemented
{
}
然后,IRowBulder
的典型实现将如下所示:
public class RowBulder : IRowBulder
{
public IFirstColumnBuilder Column(...)
{
// do stuff
return new ColumnBuilder(...);
}
}
诀窍在于,虽然ColumnBuilder
同时实现了IFirstColumnBuilder
和INextColumnBuilder
,但rowBuilder.Column()
的结果将只能访问IFirstColumnBuilder
公开的方法。
我知道这并不能解决接口的问题,但你仍然只有一个实现,这意味着要编写的代码更少,接口中的重复代码更少。此外,您的呼叫链将按照您最初想要的方式受到限制。
原始响应
由于我对你的问题有一些想法,并且我自己也是 C# 开发人员,我想出了一些原因来解释为什么这可能不会在 CLR 中发生。
您的建议需要额外的编译时检查才能
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
约束的方式起作用,但仍允许实现接口的值类型被视为可接受的 - 换句话说,约束将自相矛盾,并可能导致混淆和/或意外的代码行为。我反对这个建议的第二个论点更实际。在 C# 和一般的 CLR 语言中,可以显式实现接口。(如果你不知道显式接口实现是什么以及如何使用它,我已经在文章末尾解释了(。当你有这个
Join<T1, T2>
接口,并且实现了第 1 点的内容时,那么当T1
和T2
提供具有相同签名的方法时,您将遇到麻烦,并且它需要(并且具有(显式实现。所以,当你有这个:IColumnBuilder columnBuilder = ...; // Assume a valid column builder is set var joinResult = columnBuilder.Column("col", "val"); joinResult.ConflictingMethod(); // Oops!
如果
ConflictingMethod()
是在joinResult
碰巧继承的T1
和T2
中定义的,并且显式实现了T1
和T2
,那么在这一行,编译器将不知道正在调用哪个实现(T1
或T2
'(。
如果可能的话,上面的示例可能需要对 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
}
}