2008-09-07 10 views

Respuesta

77

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

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

se puede hacer una definición completa de su clase en un solo lugar, usted no tiene que saltar a través de aros cualquier PIMPL para definir cómo la clase funciona, y el mundo exterior no necesita ver nada de su implementación.

Si la clase TreeNode era externa, tendría que hacer todos los campos public o hacer un montón de métodos get/set para usarla. El mundo exterior tendría otra clase contaminando su intellisense.

+41

Para agregar a esto: También puede usar clases parciales en archivos separados para administrar su código un poco mejor. Ponga 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 :) –

+1

Habrá casos en los que tendrá que hacer que la clase anidada sea pública o interna si se está utilizando en el tipo de devolución de una API pública o una propiedad pública de la clase de contenedor. No estoy seguro de si es una buena práctica. Tales 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

3

Si entiendo el artículo de Katheleen correctamente, ella propone utilizar la clase anidada para poder escribir SomeEntity.Collection en lugar de EntityCollection < SomeEntity>. En mi opinión, es una forma controvertida de ahorrar algo de tipeo. Estoy bastante seguro de que en las colecciones de aplicaciones del mundo real habrá alguna diferencia en las implementaciones, por lo que tendrás que crear clases separadas de todos modos. Creo que usar el nombre de la clase para limitar el alcance de otra clase no es una buena idea. Contamina inteligentemente 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, considero que el uso de clases anidadas como en el comentario de @hazzen es aceptable a menos que tenga toneladas de clases anidadas, lo que es un signo de mal diseño.

4

Depende del uso. Raramente usaría una clase anidada pública pero usaría clases anidadas privadas todo el tiempo. Una clase anidada privada se puede usar para un subobjeto que se pretende usar solo dentro del padre. Un ejemplo de esto sería si una clase HashTable contiene un objeto de entrada privado para almacenar datos internamente solamente.

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

13

From Sun's Java Tutorial:

¿Por qué utilizar las clases anidadas? Hay varias razones de peso para el uso de clases anidadas, entre ellos:

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

Agrupamiento lógico de clases: si una clase es útil solo para otra clase, entonces es lógico incluirla en esa clase y mantener las dos juntas. Anidar tales "clases de ayuda" hace que su paquete sea más eficiente.

Encapsulación aumentada: considere dos clases de nivel superior, A y B, donde B necesita acceso a los miembros de A que, de lo contrario, 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, B puede ocultarse del mundo exterior. < - Esto no se aplica a la implementación de clases anidadas de C#, esto solo se aplica a Java.

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

+1

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

+4

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 realmente, es como si Java hiciera la variable de instancia implícita, mientras que en C# para hacerlo explícito. – Alex

+0

@Alex No, no, en Java la clase anidada realmente captura la instancia de la clase padre cuando se crea una instancia, entre otras cosas, esto significa que evita que se recolecte basura del padre. También significa que la clase anidada no puede crearse una instancia sin clase principal. Entonces no, estos no son lo mismo en absoluto. –

8

totalmente perezoso y flujos seguros patrón singleton

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

0

que a menudo utilizan las clases anidadas para ocultar los detalles de implementación. An example from Eric Lippert's answer here:

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 los genéricos. Vea this question para obtener dos ejemplos geniales. Así que termino de escribir

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

en lugar de

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

también puedo tener una lista genérica de Equality<Person> pero no EqualityComparer<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) 
     } 

i s no es posible. Ese es el beneficio de la clase anidada que hereda de la clase principal.

Otro caso (de la misma naturaleza - aplicación ocultando) es cuando se quiere hacer que los miembros de una clase (campos, propiedades, etc.) accesibles sólo 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 aún no mencionado uso de las clases anidadas son la segregación de los tipos genéricos. Por ejemplo, supongamos 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 puede tomar un Action<string, int, double> y producir un String<string, int> que llamará a la acción suministrada que pasa 3.5 como double; uno también puede desear tener un método estático que puede tomar un Action<string, int, double> y producir un Action<string>, pasando 7 como int y 5.3 como double. El uso de clases anidadas genéricos, se puede disponer que las invocaciones de métodos ser algo como:

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

o, debido a los últimos tipos en cada expresión se pueden inferir a pesar de que los anteriores no pueden:

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

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

0

Como nawfal mencionó la implementación del patrón Abstract Factory, ese código puede ser desviado para lograr Class Clusters pattern que se basa en el patrón Abstract Factory.

1

Las clases anidadas se puede utilizar para necesidades siguientes:

  1. Clasificación de los datos
  2. Cuando la lógica de la clase principal es complicado y te hacen sentir que necesita objetos subordinados para gestionar la clase
  3. Cuando usted que el estado y la existencia de la clase depende completamente de la clase adjunta
0

Me gusta anidar excepciones que son exclusivas de una sola clase, es decir. unos que nunca se arrojan desde 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 ordenada y no está lleno de un centenar de rechonchas clases de excepción poco su proyecto.

0

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

Podría hacerlo interno, sin embargo, in conjunction with the InternalsVisibleTo attribute. Sin embargo, esto sería lo mismo que hacer un campo privado interno solo para fines de prueba, lo cual considero una mala auto documentación.

Por lo tanto, es posible que desee implementar únicamente clases privadas anidadas de baja complejidad.

4

Además de las otras razones enumeradas anteriormente, hay una razón más por la que se me ocurre no solo utilizar clases anidadas, sino también clases anidadas públicas. 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 admite 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 relacionados 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 mediante el uso de un espacio de nombres genéricos (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, a través del uso de clases parciales según lo sugerido por Erik van Brakel en un comentario anterior, puede separar las clases en archivos separados anidados. Recomiendo usar una extensión de Visual Studio como NestIn para admitir el anidamiento de los archivos de clase parciales. Esto permite que los archivos de clase de "espacio de nombres" también se utilicen para organizar los archivos de clase anidados en una carpeta de forma 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 entonces serían organizados como tales:

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

Y se implementarían el espacio de nombres genéricos 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ía 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 sencillo de utilizar una clase externa como un espacio de nombres genéricos. Creé "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 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 ese código sea mucho más manejable y legible.

Cuestiones relacionadas