Perché / quando dovresti usare le classi annidate in .net? O non dovresti?


93

Nel post del blog di Kathleen Dollard del 2008 , presenta un motivo interessante per utilizzare classi nidificate in .net. Tuttavia, afferma anche che a FxCop non piacciono le classi nidificate. Presumo che le persone che scrivono le regole di FxCop non siano stupide, quindi ci deve essere un ragionamento dietro quella posizione, ma non sono stato in grado di trovarlo.


Link all'archivio Wayback al post del blog: web.archive.org/web/20141127115939/https://blogs.msmvps.com/…
iokevins

Come sottolinea nawfal , il nostro amico Eric Lippert ha risposto a un duplicato di questa domanda qui , la risposta in questione che inizia con " Usa classi annidate quando hai bisogno di una classe helper che non ha significato al di fuori della classe; in particolare quando la classe annidata può fare uso di dettagli sull'implementazione privata della classe esterna .¶ Il tuo argomento secondo cui le classi annidate sono inutili è anche un argomento che i metodi privati ​​sono inutili ... "
ruffin

Risposte:


96

Usa una classe nidificata quando la classe che stai annidando è utile solo per la classe che la racchiude. Ad esempio, le classi annidate ti consentono di scrivere qualcosa come (semplificato):

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

Puoi fare una definizione completa della tua classe in un unico posto, non devi saltare attraverso alcun cerchio PIMPL per definire come funziona la tua classe e il mondo esterno non ha bisogno di vedere nulla della tua implementazione.

Se la classe TreeNode fosse esterna, dovresti creare tutti i campi publico creare una serie di get/setmetodi per usarla. Il mondo esterno avrebbe un'altra classe che inquina il loro senso intellettuale.


44
Per aggiungere a questo: puoi anche usare classi parziali in file separati per gestire un po 'meglio il tuo codice. Metti la classe interna in un file separato (SortedMap.TreeNode.cs in questo caso). Questo dovrebbe mantenere pulito il tuo codice, pur mantenendo il tuo codice separato :)
Erik van Brakel

1
Ci saranno casi in cui sarà necessario rendere la classe nidificata pubblica o interna se viene utilizzata nel tipo restituito di un'API pubblica o in una proprietà pubblica della classe contenitore. Tuttavia, non sono sicuro che sia una buona pratica. Tali casi potrebbero avere più senso estrarre la classe nidificata all'esterno della classe contenitore. La classe System.Windows.Forms.ListViewItem.ListViewSubItem in .Net framework è uno di questi esempi.
RBT

16

Dal tutorial Java di Sun:

Perché utilizzare le classi annidate? Esistono diversi motivi convincenti per l'utilizzo di classi nidificate, tra cui:

  • È un modo per raggruppare logicamente le classi che vengono utilizzate solo in un posto.
  • Aumenta l'incapsulamento.
  • Le classi annidate possono portare a codice più leggibile e gestibile.

Raggruppamento logico delle classi: se una classe è utile solo per un'altra classe, è logico incorporarla in quella classe e tenerle insieme. La nidificazione di queste "classi di supporto" rende il loro pacchetto più snello.

Incapsulamento aumentato: si consideri due classi di primo livello, A e B, in cui B necessita dell'accesso ai membri di A che altrimenti sarebbero dichiarati privati. Nascondendo la classe B all'interno della classe A, i membri di A possono essere dichiarati privati ​​e B può accedervi. Inoltre, B stesso può essere nascosto al mondo esterno. <- Questo non si applica all'implementazione di classi annidate in C #, questo si applica solo a Java.

Codice più leggibile e gestibile: l'annidamento di piccole classi all'interno di classi di primo livello colloca il codice più vicino a dove viene utilizzato.


1
Ciò non si applica realmente, poiché non è possibile accedere alle variabili di istanza dalla classe di inclusione in C # come è possibile in Java. Sono accessibili solo i membri statici.
Ben Baron

5
Tuttavia, se si passa un'istanza della classe che la racchiude nella classe nidificata, la classe nidificata ha pieno accesso a tutti i membri attraverso quella variabile di istanza ... quindi, in realtà, è come se Java rendesse implicita la variabile di istanza, mentre in C # devi rendilo esplicito.
Alex

@Alex No, in Java la classe nidificata cattura effettivamente l'istanza della classe genitore quando viene istanziata - tra le altre cose questo significa che impedisce al genitore di essere garbage collection. Significa anche che la classe nidificata non può essere istanziata senza la classe genitore. Quindi no, questi non sono affatto gli stessi.
Tomáš Zato - Ripristina Monica il

2
@ TomášZato La mia descrizione è abbastanza azzeccata, davvero. Esiste effettivamente una variabile di istanza padre implicita nelle classi nidificate in Java, mentre in C # è necessario consegnare esplicitamente l'istanza alla classe interna. Una conseguenza di ciò, come dici tu, è che le classi interne di Java devono avere un'istanza genitore, mentre quelle di C # no. Il mio punto principale in ogni caso era che le classi interne di C # possono anche accedere ai campi e alle proprietà private dei suoi genitori, ma che deve essere passato esplicitamente all'istanza genitore per poterlo fare.
Alex

9

Pattern singleton completamente pigro 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


5

Dipende dall'utilizzo. Raramente userei mai una classe nidificata pubblica ma uso sempre classi nidificate private. Una classe annidata privata può essere utilizzata per un oggetto secondario che deve essere utilizzato solo all'interno del genitore. Un esempio di ciò sarebbe se una classe HashTable contenesse un oggetto Entry privato per memorizzare i dati solo internamente.

Se la classe è pensata per essere utilizzata dal chiamante (esternamente), generalmente mi piace renderla una classe autonoma separata.


5

Oltre agli altri motivi sopra elencati, c'è un motivo in più a cui posso pensare non solo per utilizzare classi annidate, ma in realtà classi annidate pubbliche. Per coloro che lavorano con più classi generiche che condividono gli stessi parametri di tipo generico, la possibilità di dichiarare uno spazio dei nomi generico sarebbe estremamente utile. Sfortunatamente, .Net (o almeno C #) non supporta l'idea di spazi dei nomi generici. Quindi, per raggiungere lo stesso obiettivo, possiamo utilizzare classi generiche per raggiungere lo stesso obiettivo. Prendi le seguenti classi di esempio relative a un'entità logica:

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

Possiamo semplificare le firme di queste classi utilizzando uno spazio dei nomi generico (implementato tramite classi annidate):

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

}

Quindi, attraverso l'uso di classi parziali come suggerito da Erik van Brakel in un commento precedente, è possibile separare le classi in file nidificati separati. Consiglio di utilizzare un'estensione di Visual Studio come NestIn per supportare la nidificazione dei file di classe parziali. Ciò consente di utilizzare i file di classe "spazio dei nomi" per organizzare i file di classe nidificati in un modo simile a una cartella.

Per esempio:

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

}

I file in Visual Studio Solution Explorer sarebbero quindi organizzati in questo modo:

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

E implementeresti lo spazio dei nomi generico come segue:

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 i file sarebbero organizzati in Esplora soluzioni come segue:

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

Quanto sopra è un semplice esempio di utilizzo di una classe esterna come spazio dei nomi generico. In passato ho creato "spazi dei nomi generici" contenenti 9 o più parametri di tipo. Dover mantenere quei parametri di tipo sincronizzati tra i nove tipi che tutti avevano bisogno di conoscere i parametri di tipo era noioso, specialmente quando si aggiungeva un nuovo parametro. L'uso di spazi dei nomi generici rende quel codice molto più gestibile e leggibile.


3

Se ho capito bene l'articolo di Katheleen, propone di utilizzare la classe nidificata per poter scrivere SomeEntity.Collection invece di EntityCollection <SomeEntity>. Secondo me è un modo controverso per risparmiare un po 'di digitazione. Sono abbastanza sicuro che nel mondo reale le raccolte di applicazioni avranno qualche differenza nelle implementazioni, quindi dovrai comunque creare classi separate. Penso che l'uso del nome della classe per limitare l'ambito di un'altra classe non sia una buona idea. Inquina l'intellisenso e rafforza le dipendenze tra le classi. L'utilizzo degli spazi dei nomi è un modo standard per controllare l'ambito delle classi. Tuttavia, trovo che l'utilizzo di classi annidate come nel commento @hazzen sia accettabile a meno che tu non abbia tonnellate di classi annidate che è un segno di cattivo design.


1

Uso spesso classi annidate per nascondere i dettagli dell'implementazione. Un esempio dalla risposta di Eric Lippert qui:

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

Questo modello diventa ancora migliore con l'uso di farmaci generici. Vedi questa domanda per due fantastici esempi. Quindi finisco per scrivere

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

invece di

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

Inoltre posso avere un elenco generico di Equality<Person>ma nonEqualityComparer<Person, int>

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

mentre

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

non è possibile. Questo è il vantaggio della classe nidificata che eredita dalla classe genitore.

Un altro caso (della stessa natura - nascondere l'implementazione) è quando si desidera rendere i membri di una classe (campi, proprietà, ecc.) Accessibili solo per una singola 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
}

1

Un altro uso non ancora menzionato per le classi annidate è la segregazione dei tipi generici. Ad esempio, si supponga di voler disporre di alcune famiglie generiche di classi statiche che possono accettare metodi con vari numeri di parametri, insieme a valori per alcuni di questi parametri, e generare delegati con meno parametri. Ad esempio, si desidera avere un metodo statico che può prendere un Action<string, int, double>e restituire a String<string, int>che chiamerà l'azione fornita passando 3.5 come double; si può anche desiderare di avere un metodo statico che può prendere un an Action<string, int, double>e produrre un Action<string>, passando 7come inte 5.3come double. Usando classi nidificate generiche, si può fare in modo che le invocazioni dei metodi siano qualcosa del tipo:

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

oppure, perché i secondi tipi in ciascuna espressione possono essere dedotti anche se i primi non possono:

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

L'utilizzo dei tipi generici annidati rende possibile stabilire quali delegati sono applicabili a quali parti della descrizione generale del tipo.


1

Le classi annidate possono essere utilizzate per le seguenti esigenze:

  1. Classificazione dei dati
  2. Quando la logica della classe principale è complicata e ti senti come se avessi bisogno di oggetti subordinati per gestire la classe
  3. Quando lo stato e l'esistenza della classe dipendono completamente dalla classe che lo racchiude


0

Mi piace annidare le eccezioni che sono uniche per una singola classe, ad es. quelli che non vengono mai lanciati da nessun altro posto.

Per esempio:

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

Questo aiuta a mantenere i file di progetto in ordine e non pieni di centinaia di piccole classi di eccezioni tozze.


0

Tieni presente che dovrai testare la classe annidata. Se è privato, non sarai in grado di testarlo da solo.

Potresti renderlo interno, però, insieme InternalsVisibleToall'attributo . Tuttavia, questo sarebbe lo stesso che rendere interno un campo privato solo a scopo di test, che considero una cattiva documentazione personale.

Quindi, potresti voler implementare solo classi nidificate private che comportano una bassa complessità.


0

sì per questo 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);
        }
    }
}

Perché? Aggiungi un po 'di contesto al codice nella tua soluzione per aiutare i lettori futuri a comprendere il ragionamento alla base della tua risposta.
Grant Miller

0

Sulla base della mia comprensione di questo concetto, potremmo utilizzare questa funzionalità quando le classi sono correlate tra loro concettualmente. Voglio dire che alcuni di loro sono un elemento completo nella nostra attività come entità che esistono nel mondo DDD che aiutano un oggetto radice aggregato a completare la sua logica di business.

Per chiarire lo mostrerò tramite un esempio:

Immagina di avere due classi come Order e OrderItem. In order class, gestiremo tutti gli orderItems e in OrderItem conserviamo i dati su un singolo ordine per chiarimenti, puoi vedere le classi seguenti:

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


    }
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.