为什么/何时应该在 .net 中使用嵌套类?还是你不应该?

Why/when should you use nested classes in .net? Or shouldn't you?

提问人:Eric Haskins 提问时间:9/8/2008 最后编辑:ruffinEric Haskins 更新时间:10/23/2020 访问量:53217

问:

Kathleen Dollard 2008 年的博客文章中,她提出了一个在 .net 中使用嵌套类的有趣原因。但是,她也提到 FxCop 不喜欢嵌套类。我假设编写 FxCop 规则的人并不愚蠢,所以这个立场背后一定有原因,但我没能找到它。

嵌套 FXCop.NET class nested fxcop

评论

0赞 iokevins 9/5/2019
博客文章的 Wayback Archive 链接:web.archive.org/web/20141127115939/https://blogs.msmvps.com/...
0赞 ruffin 9/6/2019
正如 nawfal 所指出的,我们的好友 Eric Lippert 在这里回答了这个问题的重复,有问题的答案是这样开头的,“当你需要一个在类外毫无意义的帮助类时,请使用嵌套类;特别是当嵌套类可以利用外部类的私有实现细节时。¶你认为嵌套类是无用的论点也是私有方法无用的论点......"

答:

110赞 hazzen 9/8/2008 #1

当嵌套的类仅对封闭类有用时,请使用嵌套类。例如,嵌套类允许您编写类似(简化)的内容:

public class SortedMap {
    private class TreeNode {
        TreeNode left;
        TreeNode right;
    }
}

你可以在一个地方对你的类进行完整的定义,你不必跳过任何PIMPL的箍来定义你的类是如何工作的,外界也不需要看到你的任何实现。

如果 TreeNode 类是外部的,则必须创建所有字段或创建一堆方法来使用它。外面的世界会有另一个阶级污染他们的智力。publicget/set

评论

45赞 Erik van Brakel 9/8/2008
除此之外,您还可以在单独的文件中使用分部类来更好地管理代码。将内部类放在单独的文件中(在本例中为 SortedMap.TreeNode.cs)。这应该使你的代码保持干净,同时也使你的代码保持独立:)
1赞 RBT 4/25/2016
在某些情况下,如果嵌套类用于公共 API 的返回类型或容器类的公共属性,则需要将其设置为公共或内部。不过,我不确定这是否是一种好的做法。在这种情况下,将嵌套类拉出容器类可能更有意义。.Net 框架中的 System.Windows.Forms.ListViewItem.ListViewSubItem 类就是这样一个示例。
3赞 aku 9/8/2008 #2

如果我没看错 Katheleen 的文章,她建议使用嵌套类来编写 SomeEntity.Collection 而不是 EntityCollection<SomeEntity>。在我看来,这是为您节省一些打字的有争议的方式。我很确定在现实世界中,应用程序集合在实现上会有一些差异,因此无论如何您都需要创建单独的类。我认为使用类名来限制其他类范围不是一个好主意。它污染了智能感知并加强了类之间的依赖关系。使用命名空间是控制类范围的标准方法。但是,我发现像@hazzen注释中那样使用嵌套类是可以接受的,除非您有大量的嵌套类,这是设计不佳的标志。

5赞 Chris Dail 9/8/2008 #3

这取决于用法。我很少使用公共嵌套类,而是一直使用私有嵌套类。私有嵌套类可用于仅在父级内部使用的子对象。例如,如果 HashTable 类包含一个私有 Entry 对象,该对象仅在内部存储数据。

如果该类旨在由调用者(外部)使用,我通常喜欢将其设置为一个单独的独立类。

17赞 Esteban Araya 9/8/2008 #4

来自 Sun 的 Java 教程:

为什么要使用嵌套类? 使用嵌套类有几个令人信服的理由,其中包括:

  • 这是一种对仅在一个地方使用的类进行逻辑分组的方法。
  • 它增加了封装。
  • 嵌套类可以生成更具可读性和可维护性的代码。

类的逻辑分组 - 如果一个类仅对另一个类有用,则将其嵌入到该类中并将两者放在一起是合乎逻辑的。嵌套这样的“帮助程序类”使它们的包更加精简。

增加封装 - 考虑两个顶级类,A 和 B,其中 B 需要访问 A 的成员,否则这些成员将被声明为私有。通过将 B 类隐藏在 A 类中,A 的成员可以被声明为私有,B 可以访问它们。此外,B本身可以隐藏在外界之外。<- 这不适用于 C# 的嵌套类实现,这仅适用于 Java。

代码更具可读性、可维护性 - 将小类嵌套在顶级类中,使代码更接近其使用位置。

评论

1赞 Ben Baron 9/3/2013
这实际上并不适用,因为您不能像在 Java 中那样从 C# 中的封闭类访问实例变量。只能访问静态成员。
5赞 Alex 11/1/2013
但是,如果将封闭类的实例传递到嵌套类中,则嵌套类可以通过该实例变量对所有成员进行完全访问...所以实际上,这就像 Java 使实例变量隐式化,而在 C# 中,你必须让它显式化。
0赞 Tomáš Zato 10/27/2017
@Alex 不,事实并非如此,在 Java 中,嵌套类在实例化时实际上会捕获父类实例 - 除其他外,这意味着它可以防止父类被垃圾回收。这也意味着如果没有父类,就无法实例化嵌套类。所以不,这些根本不一样。
2赞 Alex 10/27/2017
@TomášZato 我的描述非常贴切,真的。在 Java 的嵌套类中实际上有一个隐式父实例变量,而在 C# 中,您必须显式地将实例交给内部类。正如您所说,这样做的结果是 Java 的内部类必须有一个父实例,而 C# 的则没有。无论如何,我的主要观点是 C# 的内部类也可以访问其父级私有字段和属性,但必须显式传递父实例才能做到这一点。
10赞 kay.one 7/6/2009 #5

完全惰性和线程安全的单例模式

public sealed class Singleton
{
    Singleton()
    {
    }

    public static Singleton Instance
    {
        get
        {
            return Nested.instance;
        }
    }
    
    class Nested
    {
        // Explicit static constructor to tell C# compiler
        // not to mark type as beforefieldinit
        static Nested()
        {
        }

        internal static readonly Singleton instance = new Singleton();
    }
}

来源: https://csharpindepth.com/Articles/Singleton

1赞 nawfal 4/19/2013 #6

我经常使用嵌套类来隐藏实现细节。埃里克·利珀特(Eric Lippert)的回答中的一个例子是:

abstract public class BankAccount
{
    private BankAccount() { }
    // Now no one else can extend BankAccount because a derived class
    // must be able to call a constructor, but all the constructors are
    // private!
    private sealed class ChequingAccount : BankAccount { ... }
    public static BankAccount MakeChequingAccount() { return new ChequingAccount(); }
    private sealed class SavingsAccount : BankAccount { ... }
}

使用泛型后,这种模式会变得更好。有关两个很酷的例子,请参阅此问题。所以我最终写了

Equality<Person>.CreateComparer(p => p.Id);

而不是

new EqualityComparer<Person, int>(p => p.Id);

我也可以有一个通用列表,但不是Equality<Person>EqualityComparer<Person, int>

var l = new List<Equality<Person>> 
        { 
         Equality<Person>.CreateComparer(p => p.Id),
         Equality<Person>.CreateComparer(p => p.Name) 
        }

其中作为

var l = new List<EqualityComparer<Person, ??>>> 
        { 
         new EqualityComparer<Person, int>>(p => p.Id),
         new EqualityComparer<Person, string>>(p => p.Name) 
        }

是不可能的。这就是从父类继承的嵌套类的好处。

另一种情况(性质相同 - 隐藏实现)是当您希望使类的成员(字段、属性等)仅对单个类可访问时:

public class Outer 
{
   class Inner //private class
   {
       public int Field; //public field
   }

   static inner = new Inner { Field = -1 }; // Field is accessible here, but in no other class
}
1赞 supercat 9/3/2013 #7

嵌套类的另一个尚未提及的用途是泛型类型的分离。例如,假设想要一些泛型的静态类族,这些类可以采用具有不同数量参数的方法以及其中一些参数的值,并生成具有较少参数的委托。例如,人们希望有一个静态方法,它可以接受 an 并产生 a,它将调用传递 3.5 的所提供的操作作为 ;人们可能还希望有一个静态方法,它可以接受一个 an 并产生一个 ,作为 和 作为 传递。使用泛型嵌套类,可以安排方法调用如下所示:Action<string, int, double>String<string, int>doubleAction<string, int, double>Action<string>7int5.3double

MakeDelegate<string,int>.WithParams<double>(theDelegate, 3.5);
MakeDelegate<string>.WithParams<int,double>(theDelegate, 7, 5.3);

或者,因为可以推断每个表达式中的后一种类型,即使前一种不能:

MakeDelegate<string,int>.WithParams(theDelegate, 3.5);
MakeDelegate<string>.WithParams(theDelegate, 7, 5.3);

使用嵌套泛型类型可以判断哪些委托适用于整个类型描述的哪些部分。

0赞 diimdeep 10/31/2013 #8

正如 nawfal 提到的抽象工厂模式的实现,该代码可以被调整为实现基于抽象工厂模式的类集群模式

1赞 Rachin Goyal 4/27/2014 #9

嵌套类可用于以下需求:

  1. 数据分类
  2. 当主类的逻辑很复杂,并且您觉得需要从属对象来管理该类时
  3. 当你认为类的状态和存在完全取决于封闭类时
0赞 Eric Dand 12/20/2014 #10

我喜欢嵌套单个类独有的异常,即。那些永远不会从任何其他地方扔下来的。

例如:

public class MyClass
{
    void DoStuff()
    {
        if (!someArbitraryCondition)
        {
            // This is the only class from which OhNoException is thrown
            throw new OhNoException(
                "Oh no! Some arbitrary condition was not satisfied!");
        }
        // Do other stuff
    }

    public class OhNoException : Exception
    {
        // Constructors calling base()
    }
}

这有助于保持项目文件整洁,而不是充满一百个粗短的小异常类。

0赞 tm1 3/10/2015 #11

请记住,您需要测试嵌套类。如果它是私有的,您将无法单独测试它。

但是,您可以将其InternalsVisibleTo 属性结合使用。但是,这与仅出于测试目的在内部创建私有字段相同,我认为这是糟糕的自我文档。

因此,您可能只想实现涉及低复杂性的私有嵌套类。

5赞 Tyree Jackson 4/1/2015 #12

除了上面列出的其他原因之外,我还能想到另一个原因,不仅使用嵌套类,而且实际上使用公共嵌套类。对于那些使用共享相同泛型类型参数的多个泛型类的人来说,声明泛型命名空间的能力将非常有用。不幸的是,.Net(或至少 C#)不支持泛型命名空间的概念。因此,为了实现相同的目标,我们可以使用泛型类来实现相同的目标。以以下与逻辑实体相关的示例类为例:

public  class       BaseDataObject
                    <
                        tDataObject, 
                        tDataObjectList, 
                        tBusiness, 
                        tDataAccess
                    >
        where       tDataObject     : BaseDataObject<tDataObject, tDataObjectList, tBusiness, tDataAccess>
        where       tDataObjectList : BaseDataObjectList<tDataObject, tDataObjectList, tBusiness, tDataAccess>, new()
        where       tBusiness       : IBaseBusiness<tDataObject, tDataObjectList, tBusiness, tDataAccess>
        where       tDataAccess     : IBaseDataAccess<tDataObject, tDataObjectList, tBusiness, tDataAccess>
{
}

public  class       BaseDataObjectList
                    <
                        tDataObject, 
                        tDataObjectList, 
                        tBusiness, 
                        tDataAccess
                    >
:   
                    CollectionBase<tDataObject>
        where       tDataObject     : BaseDataObject<tDataObject, tDataObjectList, tBusiness, tDataAccess>
        where       tDataObjectList : BaseDataObjectList<tDataObject, tDataObjectList, tBusiness, tDataAccess>, new()
        where       tBusiness       : IBaseBusiness<tDataObject, tDataObjectList, tBusiness, tDataAccess>
        where       tDataAccess     : IBaseDataAccess<tDataObject, tDataObjectList, tBusiness, tDataAccess>
{
}

public  interface   IBaseBusiness
                    <
                        tDataObject, 
                        tDataObjectList, 
                        tBusiness, 
                        tDataAccess
                    >
        where       tDataObject     : BaseDataObject<tDataObject, tDataObjectList, tBusiness, tDataAccess>
        where       tDataObjectList : BaseDataObjectList<tDataObject, tDataObjectList, tBusiness, tDataAccess>, new()
        where       tBusiness       : IBaseBusiness<tDataObject, tDataObjectList, tBusiness, tDataAccess>
        where       tDataAccess     : IBaseDataAccess<tDataObject, tDataObjectList, tBusiness, tDataAccess>
{
}

public  interface   IBaseDataAccess
                    <
                        tDataObject, 
                        tDataObjectList, 
                        tBusiness, 
                        tDataAccess
                    >
        where       tDataObject     : BaseDataObject<tDataObject, tDataObjectList, tBusiness, tDataAccess>
        where       tDataObjectList : BaseDataObjectList<tDataObject, tDataObjectList, tBusiness, tDataAccess>, new()
        where       tBusiness       : IBaseBusiness<tDataObject, tDataObjectList, tBusiness, tDataAccess>
        where       tDataAccess     : IBaseDataAccess<tDataObject, tDataObjectList, tBusiness, tDataAccess>
{
}

我们可以通过使用泛型命名空间(通过嵌套类实现)来简化这些类的签名:

public
partial class   Entity
                <
                    tDataObject, 
                    tDataObjectList, 
                    tBusiness, 
                    tDataAccess
                >
        where   tDataObject     : Entity<tDataObject, tDataObjectList, tBusiness, tDataAccess>.BaseDataObject
        where   tDataObjectList : Entity<tDataObject, tDataObjectList, tBusiness, tDataAccess>.BaseDataObjectList, new()
        where   tBusiness       : Entity<tDataObject, tDataObjectList, tBusiness, tDataAccess>.IBaseBusiness
        where   tDataAccess     : Entity<tDataObject, tDataObjectList, tBusiness, tDataAccess>.IBaseDataAccess
{

    public  class       BaseDataObject {}

    public  class       BaseDataObjectList : CollectionBase<tDataObject> {}

    public  interface   IBaseBusiness {}

    public  interface   IBaseDataAccess {}

}

然后,通过使用 Erik van Brakel 在前面的评论中建议的分部类,您可以将类分隔为单独的嵌套文件。我建议使用 Visual Studio 扩展(如 NestIn)来支持嵌套分部类文件。这允许“命名空间”类文件也用于在类似文件夹中组织嵌套的类文件。

例如:

实体 .cs

public
partial class   Entity
                <
                    tDataObject, 
                    tDataObjectList, 
                    tBusiness, 
                    tDataAccess
                >
        where   tDataObject     : Entity<tDataObject, tDataObjectList, tBusiness, tDataAccess>.BaseDataObject
        where   tDataObjectList : Entity<tDataObject, tDataObjectList, tBusiness, tDataAccess>.BaseDataObjectList, new()
        where   tBusiness       : Entity<tDataObject, tDataObjectList, tBusiness, tDataAccess>.IBaseBusiness
        where   tDataAccess     : Entity<tDataObject, tDataObjectList, tBusiness, tDataAccess>.IBaseDataAccess
{
}

实体.BaseDataObject,.cs

partial class   Entity<tDataObject, tDataObjectList, tBusiness, tDataAccess>
{

    public  class   BaseDataObject
    {

        public  DataTimeOffset  CreatedDateTime     { get; set; }
        public  Guid            CreatedById         { get; set; }
        public  Guid            Id                  { get; set; }
        public  DataTimeOffset  LastUpdateDateTime  { get; set; }
        public  Guid            LastUpdatedById     { get; set; }

        public
        static
        implicit    operator    tDataObjectList(DataObject dataObject)
        {
            var returnList  = new tDataObjectList();
            returnList.Add((tDataObject) this);
            return returnList;
        }

    }

}

实体.BaseDataObjectList,.cs

partial class   Entity<tDataObject, tDataObjectList, tBusiness, tDataAccess>
{

    public  class   BaseDataObjectList : CollectionBase<tDataObject>
    {

        public  tDataObjectList ShallowClone() 
        {
            var returnList  = new tDataObjectList();
            returnList.AddRange(this);
            return returnList;
        }

    }

}

实体.IBase业务.cs

partial class   Entity<tDataObject, tDataObjectList, tBusiness, tDataAccess>
{

    public  interface   IBaseBusiness
    {
        tDataObjectList Load();
        void            Delete();
        void            Save(tDataObjectList data);
    }

}

实体.IBaseDataAccess.cs

partial class   Entity<tDataObject, tDataObjectList, tBusiness, tDataAccess>
{

    public  interface   IBaseDataAccess
    {
        tDataObjectList Load();
        void            Delete();
        void            Save(tDataObjectList data);
    }

}

然后,Visual Studio 解决方案资源管理器中的文件将按如下方式组织:

Entity.cs
+   Entity.BaseDataObject.cs
+   Entity.BaseDataObjectList.cs
+   Entity.IBaseBusiness.cs
+   Entity.IBaseDataAccess.cs

您将实现如下所示的通用命名空间:

用户 .cs

public
partial class   User
:
                Entity
                <
                    User.DataObject, 
                    User.DataObjectList, 
                    User.IBusiness, 
                    User.IDataAccess
                >
{
}

用户.数据对象.cs

partial class   User
{

    public  class   DataObject : BaseDataObject 
    {
        public  string  UserName            { get; set; }
        public  byte[]  PasswordHash        { get; set; }
        public  bool    AccountIsEnabled    { get; set; }
    }

}

用户.数据对象列表 .cs

partial class   User
{

    public  class   DataObjectList : BaseDataObjectList {}

}

用户.IBusiness.cs

partial class   User
{

    public  interface   IBusiness : IBaseBusiness {}

}

用户.IData访问.cs

partial class   User
{

    public  interface   IDataAccess : IBaseDataAccess {}

}

这些文件将在解决方案资源管理器中组织,如下所示:

User.cs
+   User.DataObject.cs
+   User.DataObjectList.cs
+   User.IBusiness.cs
+   User.IDataAccess.cs

以上是使用外部类作为泛型命名空间的简单示例。我过去构建了包含 9 个或更多类型参数的“通用命名空间”。必须使这些类型参数在九种类型之间保持同步,而所有类型都需要知道类型参数,这很繁琐,尤其是在添加新参数时。泛型命名空间的使用使该代码更易于管理和可读。

0赞 Alex Martinez 6/23/2018 #13

对于这种情况,是的:

class Join_Operator
{

    class Departamento
    {
        public int idDepto { get; set; }
        public string nombreDepto { get; set; }
    }

    class Empleado
    {
        public int idDepto { get; set; }
        public string nombreEmpleado { get; set; }
    }

    public void JoinTables()
    {
        List<Departamento> departamentos = new List<Departamento>();
        departamentos.Add(new Departamento { idDepto = 1, nombreDepto = "Arquitectura" });
        departamentos.Add(new Departamento { idDepto = 2, nombreDepto = "Programación" });

        List<Empleado> empleados = new List<Empleado>();
        empleados.Add(new Empleado { idDepto = 1, nombreEmpleado = "John Doe." });
        empleados.Add(new Empleado { idDepto = 2, nombreEmpleado = "Jim Bell" });

        var joinList = (from e in empleados
                        join d in departamentos on
                        e.idDepto equals d.idDepto
                        select new
                        {
                            nombreEmpleado = e.nombreEmpleado,
                            nombreDepto = d.nombreDepto
                        });
        foreach (var dato in joinList)
        {
            Console.WriteLine("{0} es empleado del departamento de {1}", dato.nombreEmpleado, dato.nombreDepto);
        }
    }
}

评论

0赞 Grant Miller 6/23/2018
为什么?在解决方案中为代码添加一些上下文,以帮助将来的读者理解答案背后的原因。
0赞 Hamid 8/3/2020 #14

基于我对这个概念的理解 当类在概念上相互关联时,我们可以使用此功能。 我的意思是,其中一些是我们业务中的完整项目,就像存在于 DDD 世界中的实体一样,它们有助于聚合根对象以完成其业务逻辑。

为了澄清这一点,我将通过一个示例来展示这一点:

想象一下,我们有两个类,如 Order 和 OrderItem。 在 order 类中,我们将管理所有 orderItems,在 OrderItem 中,我们将保存有关单个订单的数据 为了清楚起见,您可以查看以下类:

 class Order
    {
        private List<OrderItem> _orderItems = new List<OrderItem>();

        public void AddOrderItem(OrderItem line)
        {
            _orderItems.Add(line);
        }

        public double OrderTotal()
        {
            double total = 0;
            foreach (OrderItem item in _orderItems)
            {
                total += item.TotalPrice();
            }

            return total;
        }

        // Nested class
        public class OrderItem
        {
            public int ProductId { get; set; }
            public int Quantity { get; set; }
            public double Price { get; set; }
            public double TotalPrice() => Price * Quantity;
        }
    }

    class Program
    {

        static void Main(string[] args)
        {
            Order order = new Order();

            Order.OrderItem orderItem1 = new Order.OrderItem();
            orderItem1.ProductId = 1;
            orderItem1.Quantity = 5;
            orderItem1.Price = 1.99;
            order.AddOrderItem(orderItem1);

            Order.OrderItem orderItem2 = new Order.OrderItem();
            orderItem2.ProductId = 2;
            orderItem2.Quantity = 12;
            orderItem2.Price = 0.35;
            order.AddOrderItem(orderItem2);

            Console.WriteLine(order.OrderTotal());
            ReadLine();
        }


    }