Por que / quando você deve usar classes aninhadas em .net? Ou não deveria?

93

Na postagem do blog de Kathleen Dollard de 2008 , ela apresenta um motivo interessante para usar classes aninhadas em .net. No entanto, ela também menciona que FxCop não gosta de classes aninhadas. Estou assumindo que as pessoas que escrevem as regras do FxCop não são estúpidas, então deve haver um raciocínio por trás dessa posição, mas não fui capaz de descobrir.

Eric Haskins
fonte
Link do arquivo de Wayback para a postagem do blog: web.archive.org/web/20141127115939/https://blogs.msmvps.com/…
iokevins
Como nawfal aponta , nosso amigo Eric Lippert respondeu a uma duplicata desta pergunta aqui , a resposta em questão começando com, " Use classes aninhadas quando você precisar de uma classe auxiliar que não faz sentido fora da classe; particularmente quando a classe aninhada pode fazer uso de detalhes de implementação privada da classe externa. Seu argumento de que classes aninhadas são inúteis também é um argumento de que métodos privados são inúteis ... "
ruffin

Respostas:

96

Use uma classe aninhada quando a classe que você está aninhando só for útil para a classe envolvente. Por exemplo, classes aninhadas permitem que você escreva algo como (simplificado):

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

Você pode fazer uma definição completa de sua classe em um só lugar, você não tem que pular nenhum obstáculo PIMPL para definir como sua classe funciona, e o mundo externo não precisa ver nada de sua implementação.

Se a classe TreeNode fosse externa, você teria que criar todos os campos publicou vários get/setmétodos para usá-la. O mundo exterior teria outra classe poluindo seu intelecto.

Hazzen
fonte
44
Para adicionar a isso: Você também pode usar classes parciais em arquivos separados para gerenciar seu código um pouco melhor. Coloque a classe interna em um arquivo separado (SortedMap.TreeNode.cs neste caso). Isso deve manter seu código limpo, ao mesmo tempo que mantém seu código separado :)
Erik van Brakel
1
Haverá casos em que você precisará tornar a classe aninhada pública ou interna se ela estiver sendo usada no tipo de retorno de uma API pública ou uma propriedade pública da classe contêiner. Não tenho certeza se é uma boa prática. Esses casos podem fazer mais sentido retirar a classe aninhada fora da classe contêiner. A classe System.Windows.Forms.ListViewItem.ListViewSubItem na estrutura .Net é um exemplo.
RBT
16

Do Tutorial de Java da Sun:

Por que usar classes aninhadas? Existem vários motivos convincentes para o uso de classes aninhadas, entre eles:

  • É uma maneira de agrupar logicamente as classes que são usadas apenas em um lugar.
  • Aumenta o encapsulamento.
  • As classes aninhadas podem levar a um código mais legível e sustentável.

Agrupamento lógico de classes - se uma classe for útil apenas para uma outra classe, então é lógico incorporá-la nessa classe e manter as duas juntas. Aninhar essas "classes auxiliares" torna seu pacote mais simplificado.

Encapsulamento aumentado - Considere duas classes de nível superior, A e B, onde B precisa de acesso aos membros de A que, de outra forma, seriam declarados privados. Ao ocultar a classe B dentro da classe A, os membros de A podem ser declarados privados e B pode acessá-los. Além disso, o próprio B pode ser escondido do mundo exterior. <- Isso não se aplica à implementação do C # de classes aninhadas, isso se aplica apenas ao Java.

Código mais legível e sustentável - aninhar pequenas classes em classes de nível superior coloca o código mais perto de onde ele é usado.

Esteban Araya
fonte
1
Isso realmente não se aplica, pois você não pode acessar variáveis ​​de instância da classe envolvente em C # como você pode em Java. Apenas membros estáticos estão acessíveis.
Ben Baron,
5
No entanto, se você passar uma instância da classe envolvente para a classe aninhada, a classe aninhada terá acesso total a todos os membros por meio dessa variável de instância ... então, na verdade, é como se Java tornasse a variável de instância implícita, enquanto em C # você precisa torne-o explícito.
Alex
@Alex Não, não é, em Java a classe aninhada realmente captura a instância da classe pai quando instanciada - entre outras coisas, isso significa que impede que o pai seja coletado como lixo. Isso também significa que a classe aninhada não pode ser instanciada sem a classe pai. Portanto, não, eles não são iguais.
Tomáš Zato - Reintegração de Monica
2
@ TomášZato Minha descrição é bastante apropriada, realmente. Há efetivamente uma variável de instância pai implícita em classes aninhadas em Java, enquanto em C #, você deve entregar explicitamente a instância à classe interna. Uma consequência disso, como você disse, é que as classes internas do Java devem ter uma instância pai, enquanto as do C # não. Meu ponto principal, em qualquer caso, foi que as classes internas do C # também podem acessar os campos e propriedades privados pais, mas que deve ser passado explicitamente à instância pai para poder fazer isso.
Alex
9

Padrão singleton totalmente preguiçoso e thread-safe

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

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

kay.one
fonte
5

Depende do uso. Eu raramente usaria uma classe aninhada Public, mas usaria classes aninhadas Private o tempo todo. Uma classe aninhada privada pode ser usada para um subobjeto que deve ser usado apenas dentro do pai. Um exemplo disso seria se uma classe HashTable contivesse um objeto Entry privado para armazenar dados apenas internamente.

Se a classe se destina a ser usada pelo chamador (externamente), geralmente gosto de torná-la uma classe autônoma separada.

Chris Dail
fonte
5

Além dos outros motivos listados acima, há mais um motivo pelo qual posso pensar não apenas em usar classes aninhadas, mas, de fato, classes públicas aninhadas. Para aqueles que trabalham com várias classes genéricas que compartilham os mesmos parâmetros de tipo genérico, a capacidade de declarar um namespace genérico seria extremamente útil. Infelizmente, .Net (ou pelo menos C #) não apóia a ideia de namespaces genéricos. Portanto, para atingir o mesmo objetivo, podemos usar classes genéricas para cumprir o mesmo objetivo. Veja as seguintes classes de exemplo relacionadas a uma entidade 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 as assinaturas dessas classes usando um namespace genérico (implementado por meio de classes aninhadas):

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

}

Então, por meio do uso de classes parciais, conforme sugerido por Erik van Brakel em um comentário anterior, você pode separar as classes em arquivos aninhados separados. Eu recomendo usar uma extensão do Visual Studio como NestIn para oferecer suporte ao aninhamento de arquivos de classe parcial. Isso permite que os arquivos de classe "namespace" também sejam usados ​​para organizar os arquivos de classe aninhados em uma pasta.

Por exemplo:

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

}

Os arquivos no explorador de soluções do Visual Studio seriam organizados da seguinte forma:

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

E você implementaria o namespace genérico como o seguinte:

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

}

E os arquivos seriam organizados no explorador de soluções da seguinte maneira:

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

O exemplo acima é um exemplo simples de uso de uma classe externa como um namespace genérico. Eu construí "namespaces genéricos" contendo 9 ou mais parâmetros de tipo no passado. Ter que manter esses parâmetros de tipo sincronizados entre os nove tipos que todos precisavam saber os parâmetros de tipo era tedioso, especialmente ao adicionar um novo parâmetro. O uso de namespaces genéricos torna esse código muito mais gerenciável e legível.

Tyree Jackson
fonte
3

Se entendi direito o artigo de Katheleen, ela propõe o uso de classe aninhada para poder escrever SomeEntity.Collection em vez de EntityCollection <SomeEntity>. Em minha opinião, é uma forma polêmica de economizar um pouco de digitação. Tenho certeza de que nas coleções de aplicativos do mundo real haverá alguma diferença nas implementações, portanto, você precisará criar classes separadas. Acho que usar o nome da classe para limitar o escopo de outra classe não é uma boa ideia. Ele polui o intellisense e fortalece as dependências entre as classes. O uso de namespaces é uma forma padrão de controlar o escopo das classes. No entanto, acho que o uso de classes aninhadas como no comentário @hazzen é aceitável, a menos que você tenha toneladas de classes aninhadas, o que é um sinal de projeto ruim.

aku
fonte
1

Costumo usar classes aninhadas para ocultar detalhes de implementação. Um exemplo da resposta de Eric Lippert aqui:

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

Esse padrão fica ainda melhor com o uso de genéricos. Veja esta questão para dois exemplos legais. Então eu acabo escrevendo

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

ao invés de

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

Também posso ter uma lista genérica de, Equality<Person>mas nãoEqualityComparer<Person, int>

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

enquanto que

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

não é possível. Esse é o benefício da classe aninhada herdada da classe pai.

Outro caso (da mesma natureza - ocultar a implementação) é quando você deseja tornar os membros de uma classe (campos, propriedades, etc.) acessíveis apenas para uma única classe:

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
}
Nawfal
fonte
1

Outro uso ainda não mencionado para classes aninhadas é a segregação de tipos genéricos. Por exemplo, suponha que se queira ter algumas famílias genéricas de classes estáticas que podem usar métodos com vários números de parâmetros, junto com valores para alguns desses parâmetros, e gerar delegados com menos parâmetros. Por exemplo, deseja-se ter um método estático que pode pegar um Action<string, int, double>e gerar a String<string, int>que chamará a ação fornecida passando 3.5 como o double; pode-se também desejar ter um método estático que pode tomar um an Action<string, int, double>e produzir um Action<string>, passando 7por inte 5.3como double. Usando classes aninhadas genéricas, pode-se fazer com que as invocações de método sejam algo como:

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

ou, porque os últimos tipos em cada expressão podem ser inferidos, embora os primeiros não possam:

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

Usar os tipos genéricos aninhados torna possível dizer quais delegados são aplicáveis ​​a quais partes da descrição geral do tipo.

supergato
fonte
1

As classes aninhadas podem ser usadas para as seguintes necessidades:

  1. Classificação dos dados
  2. Quando a lógica da classe principal é complicada e você sente que precisa de objetos subordinados para gerenciar a classe
  3. Quando você que o estado e a existência da classe dependem totalmente da classe envolvente
Rachin Goyal
fonte
0

Como nawfal mencionou a implementação do padrão Abstract Factory, esse código pode ser estendido para atingir o padrão Class Clusters que é baseado no padrão Abstract Factory.

Diimdeep
fonte
0

Gosto de aninhar exceções que são exclusivas de uma única classe, ou seja, aqueles que nunca são lançados de qualquer outro lugar.

Por exemplo:

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

Isso ajuda a manter seus arquivos de projeto organizados e não cheios de uma centena de pequenas classes de exceção atarracadas.

Eric Dand
fonte
0

Lembre-se de que você precisará testar a classe aninhada. Se for privado, você não poderá testá-lo isoladamente.

Você pode torná-lo interno, no entanto, em conjunto com o InternalsVisibleToatributo . No entanto, isso seria o mesmo que tornar um campo privado interno apenas para fins de teste, o que considero uma autodocumentação ruim.

Portanto, você pode querer implementar apenas classes aninhadas privadas envolvendo baixa complexidade.

tm1
fonte
0

sim 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);
        }
    }
}
Alex Martinez
fonte
Por quê? Adicione algum contexto ao código em sua solução para ajudar leitores futuros a entender o raciocínio por trás de sua resposta.
Grant Miller
0

Com base no meu entendimento desse conceito, poderíamos usar esse recurso quando as classes estiverem relacionadas umas às outras conceitualmente. Quero dizer, alguns deles são um item completo em nosso negócio, como entidades que existem no mundo DDD que ajudam um objeto raiz agregado a completar sua lógica de negócios.

Para esclarecer, vou mostrar isso por meio de um exemplo:

Imagine que temos duas classes como Order e OrderItem. Na classe de pedido, vamos gerenciar todos os orderItems e em OrderItem estamos mantendo dados sobre um único pedido para esclarecimento, você pode ver as classes abaixo:

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


    }
Hamid
fonte