En Kathleen Dollard's recent blog post, presenta una razón interesante para utilizar clases anidadas en .net. Sin embargo, ella también menciona que a FxCop no le gustan las clases anidadas. Supongo que las personas que escriben reglas FxCop no son estúpidas, por lo que debe haber un razonamiento detrás de esa posición, pero no he podido encontrarlo.¿Por qué/cuándo debería usar clases anidadas en .net? ¿O no deberías?
Respuesta
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.
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.
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.
¿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.
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. –
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
@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. –
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();
}
}
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
}
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.
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.
Las clases anidadas se puede utilizar para necesidades siguientes:
- Clasificación de los datos
- Cuando la lógica de la clase principal es complicado y te hacen sentir que necesita objetos subordinados para gestionar la clase
- Cuando usted que el estado y la existencia de la clase depende completamente de la clase adjunta
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.
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.
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.
- 1. Clases anidadas "públicas" o no
- 2. .NET XmlSerializer y clases anidadas en C#
- 3. ¿Por qué F # no admite clases anidadas?
- 4. ¿Usar clases anidadas para constantes?
- 5. ¿Por qué no debería usar AutoDual?
- 6. ¿Debo usar clases anidadas en este caso?
- 7. ¿Cuándo no deberías usar destructores virtuales?
- 8. Para usar colecciones genéricas anidadas o clases intermedias personalizadas?
- 9. Clases privadas anidadas
- 10. ¿Dónde y cómo usar clases anidadas?
- 11. ¿Por qué NO deberías establecer IGNORE_DUP_KEY en ON?
- 12. ¿Por qué debería usar enchufes que no bloquean o bloquean?
- 13. Java: clases anidadas no estáticas y instance.super()
- 14. Transacciones anidadas en .NET
- 15. ¿Por qué no debería usar Unity?
- 16. ¿Por qué no debería usar UNIVERSAL :: isa?
- 17. Reflexión para clases anidadas
- 18. Assembly.GetTypes() para clases anidadas
- 19. clases anidadas selectores
- 20. Plantillas y clases/estructuras anidadas
- 21. IObservable vs eventos simples o ¿Por qué debería usar IObservable?
- 22. Lambda con clases anidadas
- 23. ¿Puedes tener clases anidadas en PHP?
- 24. ¿Por qué deberías bloquear los hilos?
- 25. ¿Por qué alguien debería usar .NET Framework 4 Client Profile?
- 26. Debería usar document.createDocumentFragment o document.createElement
- 27. ¿Por qué debería usar glBindAttribLocation?
- 28. ¿Por qué no debería usar ID de estilo con CSS?
- 29. ¿Cuándo y por qué debería usar TStringBuilder?
- 30. ¿Por qué debería usar Flex?
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 :) –
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