为什么接口不能包含类型?
本文关键字:类型 包含 不能 接口 为什么 | 更新日期: 2023-09-27 18:10:17
我在c#中就遇到过一两次这样的问题。我可以写这样的代码
class Node
{
class Connection
{
public Connection(Node node, string label)
{
this.Node = node;
this.Label = label;
}
public Node Node { get; private set; }
public string Label { get; private set; }
};
IEnumerable<Connection> IncomingConnections() // ...
IEnumerable<Connection> OutgoingConnections() // ...
}
但是如果我写
interface INode
{
class Connection
{
public Connection(INode node, string label)
{
this.Node = node;
this.Label = label;
}
public INode Node { get; private set; }
public string Label { get; private set; }
};
IEnumerable<Connection> IncomingConnections();
IEnumerable<Connection> OutgoingConnections();
}
得到编译错误
错误CS0524: 'Connection':接口不能声明类型
我理解这个限制,但是我感兴趣的是为什么。我当然可以在c++"接口"中嵌套类型(这只是一个具有抽象成员的类,所以没有什么奇怪的),显然这在Java中也是可能的,参见c#接口不能声明类型问题。那么,既然c#从Java那里学到了一些东西,为什么它在这方面有所欠缺呢(如果确实缺乏的话)?
(如果这已经在其他地方解决了,请道歉。我还发现接口不能声明类型,为什么我不能在接口中放置委托?但他们似乎没有直接回答我的问题。
编辑
我想我应该加一点说明,在Java世界中,乍一看,是否可以在接口中嵌套一个类似乎是一个悬而未决的问题。见https://stackoverflow.com/a/9098321/834521。我不认为我问为什么同样的不能适用于c#是愚蠢的。
编辑
框架设计指南,第2版,章节4.9 pp115-117。
- 当嵌套类型需要访问封闭类型的私有成员时,请使用嵌套类型。
- 不要使用公共嵌套类型进行分组;
- 避免公开嵌套类型,除非你真的知道你在做什么。(主要动机:显式地创建嵌套类型会让不太熟练的开发人员感到困惑。然而,隐式创建,例如通过集合枚举器,是可以的。)
- 如果嵌套类型将在包含类型之外使用或实例化,则不要使用嵌套类型(这两者都主张嵌套类型与包含类型的独立性)。
- 不要将用作接口的成员。
为什么接口不能包含类型?
在深入讨论这个问题之前,让我先澄清几件事。
首先,CLR类型系统允许在接口内部嵌套类型。将来完全有可能创建一个支持接口、委托、类、结构和枚举在接口内声明的c#或VB或其他版本,并且它将运行在现有的CLR上。第二,对于"为什么c#语言没有实现X特性?"这样的问题,我会给出我通常的回答。对于x的所有值,答案都是一样的。为了实现功能,必须考虑、设计、指定、实现、测试和交付给客户。如果这六件事中的任何一件没有发生,那么就没有功能。功能X没有实现,因为其中一个或多个事情没有发生。
第三,c#编译器团队(我已经不在其中了)不必为而不是实现一个特性提供任何解释。功能需要花钱,预算是有限的,因此请求功能的人有责任证明其收益与成本之间的关系。第四,"为什么"很难回答,"为什么不"更难回答。
所以,说了这么多,我就拒绝你的问题,用一个我能回答的问题来代替它:
假设这个特性请求已经被提交给c#设计团队。你会有什么反对的理由?
该特性虽然在CLR中是合法的,但在CLS中是不合法的。c#中有很多特性在CLS中是不合法的,但由于CLS指南明确规定不要在接口中嵌套类型,因为大多数语言不支持它,因此在c#中实现该特性本质上是鼓励人们编写不能在其他语言中使用的库。建议的特性鼓励了一种不好的编程实践。
嵌套类型为您提供了三个主要优点。首先,它们可以访问其封闭类型的私有成员。这对于没有私有成员的接口来说不是一个好处。其次,它们提供了一种方便的方式来包含外部类型的特定私有实现细节。这对接口没有好处,因为接口可能没有私有嵌套类型,也没有定义上的实现细节。第三,它们提供了一种方便的方式将一种类型与另一种类型关联起来;但是,最好使用名称空间。
据我所知,没有其他人要求该功能。我们不要把钱花在一个几乎没有人想要的功能上,因为有很多客户确实想要的功能。
实现这个特性并不会使语言本身更强大或更有表现力。
实现这个功能并不是我所知道的一些更棒的功能的垫脚石。该功能与任何其他"主题"无关。这是一个"完美主义"的功能,消除了一个小的非正交性,而不是一个有用的功能。
对于缺少该功能存在一个简单的解决方案;只需将嵌套类型设置为顶级类型即可。
这就是对的情况。如果没有人为这个特性提出一个案例,那么它在设计委员会会议上最多只能持续五分钟。你愿意为这一特性提出一个建议吗?
我想补充一下,从c# 8.0开始,接口允许使用嵌套类型。
默认接口方法- c# 8.0规范建议| Microsoft Docs(强调添加)
所以下面的内容现在是合法的。接口的语法被扩展为允许:
- 成员声明,声明常量、操作符、静态构造函数和嵌套类型;
interface ISpectrum {
[Flags]
enum Palette { Red = 1, Green = 2, Blue = 4 }
Palette Color { get; }
}
这是否是一个好的实践已经在其他答案中讨论过了,但我个人认为特定于接口的enum有它的用途。
有趣的是,尽管这个变化被列为默认接口实现的一部分,但其中大部分需要新的运行时,即。net Core 3.0/。.NET Standard 2.1及以后版本,带有嵌套类型但没有任何实现的接口可以编译,并且可以在。NET Framework 4.8中使用,只要使用支持c# 8.0编译的Roslyn CSC。
我认为这是因为CLR一直以来都支持嵌套类型接口,正如Eric Lippert在这里的回答中所说的。
嵌套类型有意义的原因只有几个。主要原因是将它们定义为私有的,以便只有容器类可以访问它们。容器类将在自己的实现中使用这些私有类型。
由于接口不是实现,因此没有理由在其中嵌套类型。这将是无用的。这就像一个农民试图用一只小猫来帮助他耕地一样。从理论上讲,这是可能的,至少可以尝试一下,但它没有任何实际用途。
查看所提供的代码,我建议将Connection
类提升为顶级类型。如果您希望根据函数组织类型,这就是名称空间的作用。在项目中创建类型组织方式的文件夹结构,并更改名称空间以反映该结构。
来自c#规范第13.2节:
接口不能包含常量、字段、操作符、实例构造函数、析构函数或类型,也不能包含任何类型的静态成员。
嵌套类型是一种静态成员,因此不允许嵌套类型比将其作为特例更为一致。
虽然这篇文章的评论中已经提到了,但我想在这里重申一下,您可以使用VB。Net创建带有嵌套类型的接口,然后在c#中不受限制地使用该接口及其所有嵌套类型。此外,您可以使用ILDasm或ILSpy之类的工具将该代码导出到IL,然后使用Visual Studio ILSupport扩展将其编织回您的库中(如果您实际上不需要它直接在库中)。
至于为什么有人想要这样做,这里有两个原因。第一个是为一组耦合接口提供蓝图/模式定义,这些接口具有支持公共概念(如实体管理)所需的共享泛型类型参数。例如:
public interface IEntity
<
TIEntity,
TDataObject,
TDataObjectList,
TIBusiness,
TIDataAccess,
TPrimaryKeyDataType
>
where TIEntity : IEntity<TIEntity, TDataObject, TDataObjectList, TIBusiness, TIDataAccess, TPrimaryKeyDataType>
where TDataObject : IEntity<TIEntity, TDataObject, TDataObjectList, TIBusiness, TIDataAccess, TPrimaryKeyDataType>.BaseDataObject
where TDataObjectList : IEntity<TIEntity, TDataObject, TDataObjectList, TIBusiness, TIDataAccess, TPrimaryKeyDataType>.IDataObjectList
where TIBusiness : IEntity<TIEntity, TDataObject, TDataObjectList, TIBusiness, TIDataAccess, TPrimaryKeyDataType>.IBaseBusiness
where TIDataAccess : IEntity<TIEntity, TDataObject, TDataObjectList, TIBusiness, TIDataAccess, TPrimaryKeyDataType>.IBaseDataAccess
where TPrimaryKeyDataType : IComparable<TPrimaryKeyDataType>, IEquatable<TPrimaryKeyDataType>
{
public class BaseDataObject
{
public TPrimaryKeyDataType Id { get; set; }
}
public interface IDataObjectList : IList<TDataObject>
{
TDataObjectList ShallowClone();
}
public interface IBaseBusiness
{
void Delete(TPrimaryKeyDataType id);
TDataObjectList Load(TPrimaryKeyDataType id);
TDataObjectList Save(TDataObjectList items);
bool Validate(TDataObject item);
}
public interface IBaseDataAccess
{
void Delete(TPrimaryKeyDataType id);
TDataObjectList Load(TPrimaryKeyDataType id);
TDataObjectList Save(TDataObjectList items);
}
}
上面的问题也可以通过在容器接口之外的每个嵌套接口/类上重复泛型类型参数和约束来解决,但这很快就会变得混乱。另一个很好的选择是,如果。net支持名称空间上的泛型类型参数,但遗憾的是,目前还不支持。
在接口中嵌套类型的第二个原因是提供一个组合/混合接口来定义组合接口,以及一个嵌套的支持编写器类来提供接受混合实例来构造一个新的动态类来实现组合接口并转发对混合的调用的功能。例如:
public interface IComposite<TIComposite, TIMixin1, TIMixin2>
where TIComposite : class, IComposite<TIComposite, TIMixin1, TIMixin2>, TIMixin1, TIMixin2
where TIMixin1 : class
where TIMixin2 : class
{
public class Composer
{
public static TIComposite Create(TIMixin1 mixin1, TIMixin2 mixin2)
{
...
}
}
}
然后,您可以创建扩展上述iccomposite接口的接口,该接口已经为扩展的接口包含了适当的编写器。请注意,你还可能有iccomposite接口的其他变体来支持更高的混合计数(类似于Func和Action,因为。net目前不支持可变泛型类型参数)。
你可以在下面的例子中使用它:
public interface IPerson
{
string FirstName;
string LastName;
}
public interface ILock
{
object GetLock();
}
public interface ILockablePerson : IComposite<ILockablePerson, IPerson, ILockable>, IPerson, ILockable {}
public class Person : IPerson
{
public string FirstName { get; set; }
public string LastName { get; set; }
}
public class Lock : ILock
{
private
readonly object lock = new object();
public object GetLock() { return this.lock; }
}
public class UseLockablePerson
{
public void Main()
{
var lockablePerson = ILockablePerson.Composer.Create(new Person(), new Lock());
lock(lockablePerson.GetLock())
{
lockablePerson.FirstName = "Bob";
}
}
}
虽然可以在iccomposite接口之外创建composer类,但发现它的组合支持或意图并不像在智能感知中看到composer那么容易。在这种情况下,嵌套的Composer类实际上被用来为接口提供一个静态方法(Create)(这是另一个不支持的选项)。