¿Por qué / cuándo debería usar clases anidadas en .net? ¿O no deberías?


93

En la publicación del blog de Kathleen Dollard de 2008 , presenta una razón interesante para usar clases anidadas en .net. Sin embargo, también menciona que a FxCop no le gustan las clases anidadas. Supongo que las personas que escriben las reglas de FxCop no son estúpidas, por lo que debe haber un razonamiento detrás de esa posición, pero no he podido encontrarlo.


Enlace de Wayback Archive a la publicación del blog: web.archive.org/web/20141127115939/https://blogs.msmvps.com/…
iokevins

Como señala nawfal , nuestro amigo Eric Lippert respondió un duplicado de esta pregunta aquí , la respuesta en cuestión comienza con " Use clases anidadas cuando necesite una clase auxiliar que no tenga sentido fuera de la clase; particularmente cuando la clase anidada puede hacer uso de detalles de implementación privada de la clase externa. Su argumento de que las clases anidadas son inútiles es también un argumento de que los métodos privados son inútiles ... "
ruffin

Respuestas:


96

Use una clase anidada cuando la clase que está anidando solo sea útil para la clase adjunta. Por ejemplo, las clases anidadas le permiten escribir algo como (simplificado):

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

Puede hacer una definición completa de su clase en un solo lugar, no tiene que pasar por los aros de PIMPL para definir cómo funciona su clase, y el mundo exterior no necesita ver nada de su implementación.

Si la clase TreeNode fuera externa, tendría que crear todos los campos publico crear un montón de get/setmétodos para usarla. El mundo exterior tendría otra clase contaminando su intellisense.


44
Para agregar a esto: también puede usar clases parciales en archivos separados para administrar su código un poco mejor. Coloque la clase interna en un archivo separado (SortedMap.TreeNode.cs en este caso). Esto debería mantener su código limpio, al mismo tiempo que mantiene su código separado :)
Erik van Brakel

1
Habrá casos en los que necesitará hacer que la clase anidada sea pública o interna si se usa en el tipo de retorno de una API pública o una propiedad pública de la clase contenedora. Sin embargo, no estoy seguro de si es una buena práctica. Estos casos pueden tener más sentido para extraer la clase anidada fuera de la clase contenedora. La clase System.Windows.Forms.ListViewItem.ListViewSubItem en .Net framework es uno de esos ejemplos.
RBT

16

Del tutorial de Java de Sun:

¿Por qué utilizar clases anidadas? Hay varias razones convincentes para usar clases anidadas, entre ellas:

  • Es una forma de agrupar lógicamente clases que solo se utilizan en un lugar.
  • Aumenta la encapsulación.
  • Las clases anidadas pueden conducir a un código más legible y fácil de mantener.

Agrupación lógica de clases: si una clase es útil solo para otra clase, entonces es lógico incrustarla en esa clase y mantener las dos juntas. Anidar tales "clases auxiliares" hace que su paquete sea más ágil.

Mayor encapsulación: considere dos clases de nivel superior, A y B, donde B necesita acceso a miembros de A que de otra manera se declararían privados. Al ocultar la clase B dentro de la clase A, los miembros de A pueden declararse privados y B puede acceder a ellos. Además, el propio B puede ocultarse del mundo exterior. <- Esto no se aplica a la implementación de C # de clases anidadas, esto solo se aplica a Java.

Código más legible y fácil de mantener: anidar clases pequeñas dentro de clases de nivel superior coloca el código más cerca de donde se usa.


1
Esto realmente no se aplica, ya que no puede acceder a las variables de instancia desde la clase adjunta en C # como puede hacerlo en Java. Solo los miembros estáticos son accesibles.
Ben Baron

5
Sin embargo, si pasa una instancia de la clase adjunta a la clase anidada, la clase anidada tiene acceso completo a todos los miembros a través de esa variable de instancia ... así que en realidad, es como si Java hiciera implícita la variable de instancia, mientras que en C # tiene que hazlo explícito.
Alex

@Alex No, no lo es, en Java, la clase anidada en realidad captura la instancia de la clase principal cuando se crea una instancia; entre otras cosas, esto significa que evita que el padre sea recolectado como basura. También significa que no se puede crear una instancia de la clase anidada sin la clase principal. Entonces no, estos no son lo mismo en absoluto.
Tomáš Zato - Reincorpora a Monica

2
@ TomášZato Mi descripción es bastante acertada, de verdad. De hecho, existe una variable de instancia principal implícita en las clases anidadas en Java, mientras que en C #, debe entregar explícitamente la instancia a la clase interna. Una consecuencia de esto, como usted dice, es que las clases internas de Java deben tener una instancia principal, mientras que las de C # no. En cualquier caso, mi punto principal fue que las clases internas de C # también pueden acceder a los campos y propiedades privados de sus padres, pero que se debe pasar la instancia principal de forma explícita para poder hacerlo.
Alex

9

Patrón singleton completamente Lazy y seguro para subprocesos

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();
    }
}

fuente: http://www.yoda.arachsys.com/csharp/singleton.html


5

Depende del uso. Rara vez usaría una clase pública anidada, pero uso clases privadas anidadas todo el tiempo. Se puede usar una clase anidada privada para un subobjeto que está destinado a usarse solo dentro del padre. Un ejemplo de esto sería si una clase HashTable contiene un objeto Entry privado para almacenar datos solo internamente.

Si la clase está destinada a ser utilizada por la persona que llama (externamente), generalmente me gusta convertirla en una clase independiente separada.


5

Además de las otras razones enumeradas anteriormente, hay una razón más en la que puedo pensar no solo en usar clases anidadas, sino también en clases públicas anidadas. Para aquellos que trabajan con múltiples clases genéricas que comparten los mismos parámetros de tipo genérico, la capacidad de declarar un espacio de nombres genérico sería extremadamente útil. Desafortunadamente, .Net (o al menos C #) no es compatible con la idea de espacios de nombres genéricos. Entonces, para lograr el mismo objetivo, podemos usar clases genéricas para cumplir el mismo objetivo. Tome las siguientes clases de ejemplo relacionadas con una entidad lógica:

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>
{
}

Podemos simplificar las firmas de estas clases usando un espacio de nombres genérico (implementado a través de clases anidadas):

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 {}

}

Luego, mediante el uso de clases parciales como sugirió Erik van Brakel en un comentario anterior, puede separar las clases en archivos anidados separados. Recomiendo usar una extensión de Visual Studio como NestIn para admitir el anidamiento de archivos de clase parciales. Esto permite que los archivos de clase del "espacio de nombres" también se utilicen para organizar los archivos de clase anidados en una carpeta similar.

Por ejemplo:

Entity.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
{
}

Entity.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;
        }

    }

}

Entity.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;
        }

    }

}

Entity.IBaseBusiness.cs

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

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

}

Entity.IBaseDataAccess.cs

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

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

}

Los archivos en el explorador de soluciones de Visual Studio se organizarían así:

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

E implementaría el espacio de nombres genérico como el siguiente:

User.cs

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

User.DataObject.cs

partial class   User
{

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

}

User.DataObjectList.cs

partial class   User
{

    public  class   DataObjectList : BaseDataObjectList {}

}

User.IBusiness.cs

partial class   User
{

    public  interface   IBusiness : IBaseBusiness {}

}

User.IDataAccess.cs

partial class   User
{

    public  interface   IDataAccess : IBaseDataAccess {}

}

Y los archivos se organizarían en el explorador de soluciones de la siguiente manera:

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

Lo anterior es un ejemplo simple del uso de una clase externa como espacio de nombres genérico. He creado "espacios de nombres genéricos" que contienen 9 o más parámetros de tipo en el pasado. Tener que mantener esos parámetros de tipo sincronizados en los nueve tipos que todos necesitaban para conocer los parámetros de tipo era tedioso, especialmente al agregar un nuevo parámetro. El uso de espacios de nombres genéricos hace que el código sea mucho más manejable y legible.


3

Si entiendo bien el artículo de Katheleen, ella propone usar una clase anidada para poder escribir SomeEntity.Collection en lugar de EntityCollection <SomeEntity>. En mi opinión, es una forma controvertida de ahorrarle algo de escritura. Estoy bastante seguro de que, en el mundo real, las colecciones de aplicaciones tendrán alguna diferencia en las implementaciones, por lo que deberá crear una clase separada de todos modos. Creo que usar el nombre de la clase para limitar el alcance de otra clase no es una buena idea. Contamina el intellisense y fortalece las dependencias entre clases. El uso de espacios de nombres es una forma estándar de controlar el alcance de las clases. Sin embargo, encuentro que el uso de clases anidadas como en el comentario de @hazzen es aceptable a menos que tenga toneladas de clases anidadas, lo cual es una señal de mal diseño.


1

A menudo uso clases anidadas para ocultar los detalles de la implementación. Un ejemplo de la respuesta de Eric Lippert aquí:

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 { ... }
}

Este patrón se vuelve aún mejor con el uso de genéricos. Vea esta pregunta para ver dos ejemplos interesantes. Entonces termino escribiendo

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

en vez de

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

También puedo tener una lista genérica de Equality<Person>pero noEqualityComparer<Person, int>

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

donde como

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

no es posible. Ese es el beneficio de la clase anidada heredada de la clase principal.

Otro caso (de la misma naturaleza - implementación oculta) es cuando desea que los miembros de una clase (campos, propiedades, etc.) sean accesibles solo para una sola clase:

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

Otro uso aún no mencionado para las clases anidadas es la segregación de tipos genéricos. Por ejemplo, suponga que uno quiere tener algunas familias genéricas de clases estáticas que puedan tomar métodos con varios números de parámetros, junto con valores para algunos de esos parámetros, y generar delegados con menos parámetros. Por ejemplo, uno desea tener un método estático que pueda tomar an Action<string, int, double>y producir un String<string, int>que llame a la acción proporcionada pasando 3.5 como double; también se puede desear tener un método estático que pueda tomar an Action<string, int, double>y producir an Action<string>, pasando 7como inty 5.3como double. Usando clases anidadas genéricas, uno puede hacer arreglos para que las invocaciones de métodos sean algo como:

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

o, porque los últimos tipos en cada expresión se pueden inferir aunque los primeros no pueden:

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

El uso de tipos genéricos anidados permite saber qué delegados son aplicables a qué partes de la descripción general del tipo.


1

Las clases anidadas se pueden utilizar para las siguientes necesidades:

  1. Clasificación de los datos
  2. Cuando la lógica de la clase principal es complicada y siente que necesita objetos subordinados para administrar la clase
  3. Cuando usted que el estado y la existencia de la clase depende completamente de la clase que la encierra


0

Me gusta anidar excepciones que son exclusivas de una sola clase, es decir. los que nunca se tiran de ningún otro lugar.

Por ejemplo:

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()
    }
}

Esto ayuda a mantener los archivos de su proyecto ordenados y no llenos de un centenar de pequeñas clases de excepción.


0

Tenga en cuenta que deberá probar la clase anidada. Si es privado, no podrá probarlo de forma aislada.

Sin embargo, puede convertirlo en interno junto con el InternalsVisibleToatributo . Sin embargo, esto sería lo mismo que hacer que un campo privado sea interno solo con fines de prueba, lo que considero una mala auto documentación.

Por lo tanto, es posible que desee implementar solo clases anidadas privadas que involucren baja complejidad.


0

si para este caso:

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);
        }
    }
}

¿Por qué? Agregue algo de contexto al código en su solución para ayudar a los futuros lectores a comprender el razonamiento detrás de su respuesta.
Grant Miller

0

Basándome en mi comprensión de este concepto, podríamos usar esta característica cuando las clases están relacionadas entre sí conceptualmente. Quiero decir que algunos de ellos son un elemento completo en nuestro negocio, como entidades que existen en el mundo DDD que ayudan a un objeto raíz agregado a completar su lógica empresarial.

Para aclarar, voy a mostrar esto a través de un ejemplo:

Imagine que tenemos dos clases como Order y OrderItem. En la clase de pedido, vamos a administrar todos los artículos de pedido y en el artículo de pedido tenemos datos sobre un solo pedido para aclarar, puede ver las siguientes clases:

 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();
        }


    }
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.