Pourquoi / quand utiliser des classes imbriquées dans .net? Ou ne devriez-vous pas?


93

Dans le billet de blog 2008 de Kathleen Dollard , elle présente une raison intéressante d'utiliser des classes imbriquées dans .net. Cependant, elle mentionne également que FxCop n'aime pas les classes imbriquées. Je suppose que les gens qui écrivent des règles FxCop ne sont pas stupides, donc il doit y avoir un raisonnement derrière cette position, mais je n'ai pas été en mesure de le trouver.


Wayback Archive lien vers le billet de blog: web.archive.org/web/20141127115939/https://blogs.msmvps.com/…
iokevins

Comme le souligne Nawfal , notre copain Eric Lippert a répondu à un double de cette question ici , la réponse en question commençant par: " Utilisez des classes imbriquées lorsque vous avez besoin d'une classe d'assistance qui n'a pas de sens en dehors de la classe; en particulier lorsque la classe imbriquée peut utiliser détails de l'implémentation privée de la classe externe.¶ Votre argument selon lequel les classes imbriquées sont inutiles est également un argument selon lequel les méthodes privées sont inutiles ... "
ruffin le

Réponses:


96

Utilisez une classe imbriquée lorsque la classe que vous imbriquez n'est utile que pour la classe englobante. Par exemple, les classes imbriquées vous permettent d'écrire quelque chose comme (simplifié):

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

Vous pouvez créer une définition complète de votre classe en un seul endroit, vous n'avez pas besoin de parcourir les cerceaux PIMPL pour définir le fonctionnement de votre classe, et le monde extérieur n'a pas besoin de voir quoi que ce soit de votre implémentation.

Si la classe TreeNode était externe, vous devrez soit créer tous les champs, publicsoit créer un tas de get/setméthodes pour l'utiliser. Le monde extérieur aurait une autre classe polluant leur intelligence.


44
Pour ajouter à cela: vous pouvez également utiliser des classes partielles dans des fichiers séparés pour gérer un peu mieux votre code. Placez la classe interne dans un fichier séparé (SortedMap.TreeNode.cs dans ce cas). Cela devrait garder votre code propre, tout en gardant votre code séparé :)
Erik van Brakel

1
Il y aura des cas où vous devrez rendre la classe imbriquée publique ou interne si elle est utilisée dans le type de retour d'une API publique ou d'une propriété publique de la classe de conteneur. Je ne suis pas sûr que ce soit une bonne pratique. De tels cas peuvent avoir plus de sens pour extraire la classe imbriquée en dehors de la classe conteneur. La classe System.Windows.Forms.ListViewItem.ListViewSubItem dans le framework .Net en est un exemple.
RBT

16

À partir du didacticiel Java de Sun:

Pourquoi utiliser des classes imbriquées? Il existe plusieurs raisons impérieuses d'utiliser des classes imbriquées, parmi lesquelles:

  • C'est une façon de regrouper logiquement des classes qui ne sont utilisées qu'à un seul endroit.
  • Cela augmente l'encapsulation.
  • Les classes imbriquées peuvent conduire à un code plus lisible et maintenable.

Regroupement logique des classes: si une classe n'est utile qu'à une seule autre classe, il est logique de l'intégrer dans cette classe et de garder les deux ensemble. L'imbrication de ces "classes d'assistance" rend leur package plus simple.

Encapsulation accrue: considérez deux classes de niveau supérieur, A et B, où B a besoin d'accéder aux membres de A qui seraient autrement déclarés privés. En masquant la classe B dans la classe A, les membres de A peuvent être déclarés privés et B peut y accéder. De plus, B lui-même peut être caché du monde extérieur. <- Cela ne s'applique pas à l'implémentation en C # des classes imbriquées, cela ne s'applique qu'à Java.

Code plus lisible et maintenable: l'imbrication de petites classes dans des classes de niveau supérieur rapproche le code de l'endroit où il est utilisé.


1
Cela ne s'applique pas vraiment, car vous ne pouvez pas accéder aux variables d'instance à partir de la classe englobante en C # comme vous le pouvez en Java. Seuls les membres statiques sont accessibles.
Ben Baron le

5
Cependant, si vous passez une instance de la classe englobante dans la classe imbriquée, la classe imbriquée a un accès complet à tous les membres via cette variable d'instance ... donc vraiment, c'est comme si Java rend la variable d'instance implicite, alors qu'en C # vous devez le rendre explicite.
Alex

@Alex Non, ce n'est pas le cas, en Java, la classe imbriquée capture réellement l'instance de la classe parent lorsqu'elle est instanciée - entre autres, cela signifie qu'elle empêche le parent d'être récupéré. Cela signifie également que la classe imbriquée ne peut pas être instanciée sans classe parente. Donc non, ce ne sont pas du tout les mêmes.
Tomáš Zato - Réintégrer Monica

2
@ TomášZato Ma description est assez juste, vraiment. Il existe en fait une variable d'instance parent implicite dans les classes imbriquées en Java, tandis qu'en C #, vous devez explicitement remettre l'instance à la classe interne. Une conséquence de ceci, comme vous le dites, est que les classes internes de Java doivent avoir une instance parente, contrairement à C #. Mon point principal dans tous les cas était que les classes internes de C # peuvent également accéder aux champs et propriétés privés de ses parents, mais qu'il doit être transmis explicitement à l'instance parente pour pouvoir le faire.
Alex

9

Motif singleton entièrement paresseux et 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();
    }
}

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


5

Cela dépend de l'utilisation. J'utiliserais rarement une classe imbriquée publique, mais j'utiliserais tout le temps des classes imbriquées privées. Une classe imbriquée privée peut être utilisée pour un sous-objet destiné à être utilisé uniquement à l'intérieur du parent. Un exemple de cela serait si une classe HashTable contient un objet Entry privé pour stocker des données en interne uniquement.

Si la classe est destinée à être utilisée par l'appelant (en externe), j'aime généralement en faire une classe autonome distincte.


5

En plus des autres raisons énumérées ci-dessus, il y a une autre raison à laquelle je peux penser non seulement pour utiliser des classes imbriquées, mais en fait des classes imbriquées publiques. Pour ceux qui travaillent avec plusieurs classes génériques partageant les mêmes paramètres de type générique, la possibilité de déclarer un espace de noms générique serait extrêmement utile. Malheureusement, .Net (ou du moins C #) ne prend pas en charge l'idée d'espaces de noms génériques. Ainsi, pour atteindre le même objectif, nous pouvons utiliser des classes génériques pour atteindre le même objectif. Prenons l'exemple de classes suivantes liées à une entité logique:

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

Nous pouvons simplifier les signatures de ces classes en utilisant un espace de noms générique (implémenté via des classes imbriquées):

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

}

Ensuite, grâce à l'utilisation de classes partielles comme suggéré par Erik van Brakel dans un commentaire précédent, vous pouvez séparer les classes en fichiers imbriqués séparés. Je recommande d'utiliser une extension Visual Studio comme NestIn pour prendre en charge l'imbrication des fichiers de classe partiels. Cela permet aux fichiers de classe "namespace" d'être également utilisés pour organiser les fichiers de classe imbriqués dans un dossier de la même manière.

Par exemple:

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

}

Les fichiers de l'explorateur de solutions Visual Studio seraient alors organisés comme tels:

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

Et vous implémenteriez l'espace de noms générique comme suit:

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

}

Et les fichiers seraient organisés dans l'explorateur de solutions comme suit:

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

Ce qui précède est un exemple simple d'utilisation d'une classe externe comme espace de noms générique. J'ai construit des "espaces de noms génériques" contenant 9 paramètres de type ou plus dans le passé. Il était fastidieux de devoir garder ces paramètres de type synchronisés sur les neuf types dont tous avaient besoin pour connaître les paramètres de type, en particulier lors de l'ajout d'un nouveau paramètre. L'utilisation d'espaces de noms génériques rend ce code beaucoup plus gérable et lisible.


3

Si je comprends bien l'article de Katheleen, elle propose d'utiliser une classe imbriquée pour pouvoir écrire SomeEntity.Collection au lieu d'EntityCollection <SomeEntity>. À mon avis, c'est une façon controversée de vous éviter de taper. Je suis presque sûr que dans le monde réel, les collections d'applications auront une certaine différence dans les implémentations, vous devrez donc créer une classe séparée de toute façon. Je pense que l'utilisation du nom de classe pour limiter la portée d'une autre classe n'est pas une bonne idée. Il pollue l'intellisense et renforce les dépendances entre les classes. L'utilisation d'espaces de noms est un moyen standard de contrôler la portée des classes. Cependant, je trouve que l'utilisation de classes imbriquées comme dans @hazzen comment est acceptable à moins que vous n'ayez des tonnes de classes imbriquées, ce qui est un signe de mauvaise conception.


1

J'utilise souvent des classes imbriquées pour masquer les détails de l'implémentation. Un exemple de la réponse d'Eric Lippert ici:

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

Ce modèle devient encore meilleur avec l'utilisation de génériques. Voir cette question pour deux exemples sympas. Alors je finis par écrire

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

au lieu de

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

Je peux aussi avoir une liste générique de Equality<Person>mais pasEqualityComparer<Person, int>

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

tandis que

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

n'est pas possible. C'est l'avantage de la classe imbriquée héritant de la classe parent.

Un autre cas (de même nature - masquer l'implémentation) est celui où vous souhaitez rendre les membres d'une classe (champs, propriétés, etc.) accessibles uniquement pour une seule 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

Une autre utilisation non encore mentionnée pour les classes imbriquées est la ségrégation des types génériques. Par exemple, supposons que l'on veuille avoir des familles génériques de classes statiques qui peuvent prendre des méthodes avec différents nombres de paramètres, avec des valeurs pour certains de ces paramètres, et générer des délégués avec moins de paramètres. Par exemple, on souhaite avoir une méthode statique qui peut prendre un Action<string, int, double>et donner un String<string, int>qui appellera l'action fournie en passant 3.5 comme le double; on peut aussi souhaiter avoir une méthode statique qui peut prendre un an Action<string, int, double>et donner un an Action<string>, en passant 7comme le intet 5.3comme le double. En utilisant des classes imbriquées génériques, on peut faire en sorte que les invocations de méthode soient quelque chose comme:

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

ou, parce que les derniers types dans chaque expression peuvent être déduits même si les premiers ne le peuvent pas:

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

L'utilisation des types génériques imbriqués permet de dire quels délégués sont applicables à quelles parties de la description de type globale.


1

Les classes imbriquées peuvent être utilisées pour les besoins suivants:

  1. Classification des données
  2. Lorsque la logique de la classe principale est compliquée et que vous avez l'impression d'avoir besoin d'objets subordonnés pour gérer la classe
  3. Lorsque vous que l'état et l'existence de la classe dépendent entièrement de la classe englobante


0

J'aime imbriquer des exceptions qui sont uniques à une seule classe, c'est-à-dire. ceux qui ne sont jamais jetés d'un autre endroit.

Par exemple:

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

Cela permet de garder vos fichiers de projet bien rangés et non pleins d'une centaine de petites classes d'exception tronquées.


0

Gardez à l'esprit que vous devrez tester la classe imbriquée. S'il est privé, vous ne pourrez pas le tester de manière isolée.

Vous pouvez cependant le rendre interne en conjonction avec l' InternalsVisibleToattribut . Cependant, ce serait la même chose que de rendre un champ privé interne uniquement à des fins de test, ce que je considère comme une mauvaise auto-documentation.

Ainsi, vous souhaiterez peut-être implémenter uniquement des classes imbriquées privées impliquant une faible complexité.


0

oui pour ce cas:

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

Pourquoi? Ajoutez du contexte au code de votre solution pour aider les futurs lecteurs à comprendre le raisonnement derrière votre réponse.
Grant Miller

0

Sur la base de ma compréhension de ce concept, nous pourrions utiliser cette fonctionnalité lorsque les classes sont liées les unes aux autres sur le plan conceptuel. Je veux dire que certains d'entre eux sont un élément complet dans notre entreprise, comme des entités qui existent dans le monde DDD qui aident un objet racine agrégé à compléter sa logique métier.

Afin de clarifier, je vais montrer cela via un exemple:

Imaginez que nous ayons deux classes comme Order et OrderItem. Dans la classe de commande, nous allons gérer tous les orderItems et dans OrderItem, nous conservons des données sur une seule commande pour clarification, vous pouvez voir les classes ci-dessous:

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


    }
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.