9

这让我在 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:“连接”:接口无法声明类型

我理解限制,但我感兴趣的是为什么. 我当然可以在 C++“接口”中嵌套类型(这只是一个具有抽象成员的类,所以不足为奇),显然它在 Java 中也是可能的,请参阅Interfaces Cannot Declare Type Issue C#。那么鉴于 C# 从 Java 中学到了一些东西,为什么它在这方面缺乏(如果确实缺乏的话)?

(抱歉,如果这已经在其他地方得到解决。我还发现接口不能声明类型为什么我不能在接口中放置一个委托?但他们似乎没有直接解决我的问题。)

编辑

我想我只是添加一个注释,说在 Java 世界中,乍一看似乎是一个悬而未决的问题,即是否可以在接口中嵌套一个类。请参阅https://stackoverflow.com/a/9098321/834521。我不认为我在问为什么同样不能适用于 C# 是愚蠢的。

编辑

框架设计指南,第 2 版,第 4.9 节 pp115-117的简要总结/引述。

  • 当嵌套类型需要访问封闭类型的私有成员时,请使用嵌套类型。
  • 不要使用公共嵌套类型进行分组;为此使用名称空间。
  • 除非您真的知道自己在做什么,否则请避免公开嵌套类型。(主要动机:显式创建嵌套类型会使技术水平较低的开发人员感到困惑。但是隐式创建,例如通过集合枚举器,是可以的。)
  • 如果嵌套类型将在包含类型之外使用或实例化,则不要使用嵌套类型(这两者都主张嵌套类型与包含类型的独立性)。
  • 不要用作接口的成员。
4

5 回答 5

31

为什么接口不能包含类型?

在深入探讨这个问题之前,让我先澄清几件事。

首先,CLR 类型系统确实允许在接口内嵌套类型。完全有可能创建一个 C# 或 VB 或任何支持在接口内声明的接口、委托、类、结构和枚举的版本,并且它将在现有的 CLR 上运行。

其次,我将就“为什么 C# 语言不实现功能 X?”形式的问题向您提供我通常的回击。对于 X 的所有值,答案都是相同的。为了实现一个特性,必须:考虑、设计、指定、实施、测试并交付给客户。如果这六件事中的任何一件都没有发生,那么就没有特征。未实施功能 X 是因为其中一项或多项未发生。

第三,C# 编译器团队(我不再参与)不必为实现某个功能提供任何解释。功能需要花钱,预算是有限的,因此请求该功能的人有责任证明其收益与成本的合理性。

第四,“为什么”的问题很难回答,“为什么不”的问题更难回答。

所以,话虽如此,我会拒绝你的问题,并用我可以回答的问题代替它:

假设此功能请求已提交给 C# 设计团队。你会提出什么反对意见?

  • 该功能虽然在 CLR 中是合法的,但在 CLS 中是不合法的。C# 中有很多特性在 CLS 中是不合法的,但是由于 CLS 指南明确指出不要在接口中嵌套类型,因为大多数语言不支持它,所以在 C# 中实现该特性本质上是在鼓励人们编写不能可用于其他语言。建议的特性会鼓励不良的编程习惯

  • 嵌套类型为您提供三个主要优势。首先,它们可以访问其封闭类型的私有成员。这对没有私有成员的接口没有好处。其次,它们提供了一种方便的方法来包含外部类型的特定私有实现细节。这对接口没有好处,接口可能没有私有嵌套类型,并且根据定义没有实现细节。第三,它们提供了一种将一种类型与另一种类型关联起来的便捷方式;但是,这最好通过命名空间来完成。

  • 据我所知,没有其他人要求该功能。当客户确实需要很多功能时,我们不要将钱花在几乎没有人想要的功能上。

  • 实现该功能并不会使语言本身以任何方式更强大或更具表现力。

  • 实现该功能并不是我所知道的一些更棒的功能的垫脚石。该功能与任何其他“主题”无关。这是一个“完成主义者”功能,它消除了一个小的非正交性,而不是一个有用的功能。

  • 对于缺少该功能,有一个简单的解决方法;只需将嵌套类型设为顶级类型即可。

反对的情况就是这样。如果没有人为该功能提出一个案例,它在设计委员会会议上的持续时间可能不会超过五分钟。您是否愿意为该功能提出案例?

于 2013-04-22T19:17:17.450 回答
3

想补充一点,从 C# 8.0 开始,接口允许使用嵌套类型。

默认接口方法——C# 8.0 规范建议 | Microsoft Docs (强调)

接口的语法扩展为 permit

  • 声明常量、运算符、静态构造函数和嵌套类型的成员声明;

所以像下面这样的东西现在是合法的。

interface ISpectrum {
    [Flags]
    enum Palette { Red = 1, Green = 2, Blue = 4 }
    Palette Color { get; }
}

这是否是一种好的做法已在其他答案中讨论过,但我个人发现特定于接口的枚举有其用途。

有趣的是,尽管此更改被列为默认接口实现的一部分,但其中大部分都需要新的运行时,即 .NET Core 3.0/.NET Standard 2.1 及更高版本,具有嵌套类型但没有任何实现的接口确实可以编译并可用于.NET Framework 4.8,只要使用支持 C# 8.0 中编译的 Roslyn CSC。

我认为这是因为 CLR 一直支持接口中的嵌套类型,正​​如 Eric Lippert 在此处的答案中所说的那样。

于 2020-05-01T02:52:59.177 回答
2

嵌套类型有意义的原因只有几个。主要原因是将它们定义为私有的,以便只有容器类可以访问它们。容器类将在其自己的实现中使用这些私有类型。

由于接口不是实现,因此没有充分的理由在其中嵌套类型。这将是无用的。这就像一个农民试图用一只小猫帮他犁地。从理论上讲,这至少可以尝试,但它没有任何实际用途。

查看提供的代码,我建议将该Connection类提升为顶级类型。如果您想根据功能组织您的类型,这就是命名空间的用途。在您的项目中创建一个文件夹结构来说明类型的组织方式,并更改命名空间以反映这一点。

于 2013-04-22T16:17:37.230 回答
1

从 C# 规范,第 13.2 节:

接口不能包含常量、字段、运算符、实例构造函数、析构函数或类型,接口也不能包含任何类型的静态成员。

嵌套类型是一种静态成员,因此不允许嵌套类型比让它们成为特例更一致。

于 2013-04-22T16:19:01.873 回答
1

虽然在这篇文章的评论中提到了它,但我想在此重申,您可以使用 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)
        {
            ...
        }

    }

}

然后,您可以创建扩展上述 IComposite 接口的接口,该接口已经包含扩展接口的适当编写器。请注意,您还可能拥有 IComposite 接口的其他变体来支持更多数量的 mixin(类似于 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";
        }
    }

}

虽然可以在 IComposite 接口之外创建 Composer 类,但发现其组合支持或意图并不像在 IntelliSense 中看到 Composer 那样容易。在这种情况下,嵌套的 Composer 类实际上用于为接口提供静态方法 (Create)(这是另一个不受支持的选项)。

于 2015-07-22T19:29:02.087 回答