Чому / коли слід використовувати вкладені класи у .net? Або не варто?


93

У публікації до блогу Кетлін Доллард 2008 року вона представляє цікаву причину для використання вкладених класів у .net. Однак вона також зазначає, що FxCop не любить вкладені класи. Я припускаю, що люди, які пишуть правила FxCop, не дурні, тому для цієї позиції повинні бути міркування, але я не зміг її знайти.


Архів зворотних посилань на повідомлення в блозі: web.archive.org/web/20141127115939/https://blogs.msmvps.com/…
iokevins

Як зазначає nawfal , наш приятель Ерік Ліпперт відповів на це дублікат цього запитання , відповідь на питання, що починається з " Використовуйте вкладені класи, коли вам потрібен безпомічний клас, який не має сенсу поза класом; особливо, коли вкладений клас може використовувати Деталі приватної реалізації зовнішнього класу. "Ваш аргумент про те, що вкладені класи є марними, також є аргументом того, що приватні методи марні ... "
ruffin

Відповіді:


96

Використовуйте вкладений клас, коли клас, який ви вкладаєте, корисний лише класу, що додається. Наприклад, вкладені класи дозволяють написати щось на зразок (спрощеного):

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

Ви можете зробити повне визначення свого класу в одному місці, вам не доведеться стрибати через будь-які обручі PIMPL, щоб визначити, як працює ваш клас, і зовнішній світ не повинен бачити нічого у вашій реалізації.

Якщо клас TreeNode був зовнішнім, вам доведеться або зробити всі поля, publicабо зробити купу get/setметодів для його використання. Зовнішній світ мав би інший клас, що забруднює їхню інтелігенцію.


44
Щоб додати до цього: Ви також можете використовувати часткові класи в окремих файлах, щоб трохи краще керувати кодом. Помістіть внутрішній клас у окремий файл (в цьому випадку SortedMap.TreeNode.cs). Це повинно підтримувати ваш код чистим, а також зберігати свій код відокремлено :)
Ерік ван Бракель

1
Будуть випадки, коли вам потрібно буде зробити вкладений клас загальнодоступним або внутрішнім, якщо він використовується у зворотному вигляді публічного API або публічного властивості класу контейнерів. Я не впевнений, чи це хороша практика. Такі випадки можуть мати більше сенсу витягувати вкладений клас поза класом контейнерів. System.Windows.Forms.ListViewItem.ListViewSubItem клас в .Net Framework є одним з таких прикладів.
RBT

16

З навчального посібника Java для Sun:

Навіщо використовувати вкладені класи? Існує кілька вагомих причин використання вкладених класів, серед них:

  • Це спосіб логічного групування класів, які використовуються лише в одному місці.
  • Це збільшує інкапсуляцію.
  • Вкладені класи можуть призвести до більш читабельного та збереженого коду.

Логічне групування класів - якщо клас корисний лише для одного іншого класу, то логічно вбудувати його в цей клас і тримати їх разом. Введення таких "допоміжних класів" робить їх пакет більш впорядкованим.

Посилена інкапсуляція - розгляньте два класи вищого рівня, A і B, де B потрібен доступ до членів A, які в іншому випадку будуть оголошені приватними. Приховуючи клас B в класі A, члени A можуть бути оголошені приватними, і B може отримати доступ до них. Крім того, сам Б може бути прихований від зовнішнього світу. <- Це не стосується реалізації C # вкладених класів, це стосується лише Java.

Більш читабельний, доступний для коду код - Вкладення малих класів у класах верхнього рівня розміщує код ближче до місця його використання.


1
Це насправді не застосовується, оскільки ви не можете отримати доступ до змінних екземплярів із класу, що додається в C #, як ви можете в Java. Доступні лише статичні члени.
Бен Барон

5
Однак, якщо ви передаєте екземпляр класу, що додається, до класу вкладених класів, вкладений клас має повний доступ до всіх членів через цю змінну екземпляра ... так що насправді це схоже на те, що Java робить змінну екземпляра прихованою, тоді як у C # ви повинні зробіть це явним.
Олексій

@Alex Ні, це не так, у Java вкладений клас насправді фіксує екземпляр батьківського класу, коли інстанціюється - серед іншого, це означає, що це не дозволяє батькові збирати сміття. Це також означає, що вкладений клас не може бути створений без батьківського класу. Так що ні, це зовсім не те саме.
Томаш Зато - Відновити Моніку

2
@ TomášZato Мій опис дуже влучний, насправді. Існує фактично неявна змінна батьківська інстанція в вкладених класах на Java, тоді як у C # ви повинні явно передавати внутрішній клас екземпляру. Наслідком цього, як ви кажете, є те, що внутрішні класи Java повинні мати батьківський екземпляр, тоді як C # - ні. Моя головна думка в будь-якому випадку полягала в тому, що внутрішні класи C # також можуть отримати доступ до приватних полів та властивостей своїх батьків, але що це має бути явно передано батьківському екземпляру, щоб мати можливість це робити.
Олексій

9

Повністю лінивий і безпечний однотонний візерунок

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

джерело: http://www.yoda.arachsys.com/csharp/singleton.html


5

Це залежить від використання. Я рідко коли-небудь використовував публічний вкладений клас, але весь час використовую приватні вкладені класи. Приватний вкладений клас може бути використаний для суб-об’єкта, який призначений для використання тільки всередині батьківського. Прикладом цього може бути, якщо клас HashTable містить приватний об'єкт Entry для зберігання даних лише внутрішньо.

Якщо клас призначений для використання абонентом (зовні), я, як правило, люблю робити його окремим окремим класом.


5

Окрім інших перелічених вище причин, є ще одна причина, по якій я можу думати не лише про використання вкладених класів, але і фактично для загальнодоступних класів. Для тих, хто працює з декількома загальними класами, які мають однакові параметри загального типу, можливість оголошення загального простору імен була б надзвичайно корисною. На жаль, .Net (або принаймні C #) не підтримує ідею загальних просторів імен. Тож для досягнення тієї ж мети ми можемо використовувати загальні класи для виконання тієї самої мети. Візьмемо такі приклади класів, що стосуються логічної сутності:

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

Ми можемо спростити підписи цих класів, використовуючи загальний простір імен (реалізований через вкладені класи):

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

}

Потім, використовуючи часткові класи, як запропонував Ерік ван Брекель у попередньому коментарі, ви можете розділити класи на окремі вкладені файли. Я рекомендую використовувати розширення Visual Studio на зразок NestIn для підтримки вкладання файлів часткового класу. Це дозволяє також використовувати файли класу «Простір імен» для організації вкладених файлів класу у папку.

Наприклад:

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

}

Файли в досліднику рішень візуальної студії будуть потім організовані як такі:

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

І ви б реалізували загальний простір імен на зразок наступного:

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

}

А файли будуть організовані в провіднику рішень таким чином:

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

Наведене є простим прикладом використання зовнішнього класу як загального простору імен. У минулому я створив "загальні простори імен", що містять 9 або більше параметрів типу. Необхідність зберігати параметри типу синхронізовані через дев'ять типів, що все, що потрібно знати параметри типу, було втомливим, особливо при додаванні нового параметра. Використання загальних просторів імен робить цей код набагато більш керованим і читабельним.


3

Якщо я правильно розумію статтю Кетлін, вона пропонує використовувати вкладений клас, щоб мати можливість писати SomeEntity.Collection замість EntityCollection <SomeEntity>. На мою думку, це суперечливий спосіб заощадити набравши текст. Я майже впевнений, що в реальному світі колекції додатків матимуть певну різницю у впровадженні, тому вам все одно потрібно буде створити окремий клас. Я думаю, що використання назви класу для обмеження обсягу інших класів не є хорошою ідеєю. Це забруднює інтелігенцію та посилює залежності між класами. Використання просторів імен є стандартним способом керування областями класів. Однак я вважаю, що використання вкладених класів, як у коментарі @hazzen, є прийнятним, якщо у вас є безліч вкладених класів, що є ознакою поганого дизайну.


1

Я часто використовую вкладені класи, щоб приховати деталі реалізації. Приклад відповіді Еріка Ліпперта тут:

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

Ця модель стає ще кращою при використанні дженериків. Дивіться це питання на двох класних прикладах. Тому я закінчую писати

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

замість

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

Також у мене може бути загальний список, Equality<Person>але ніEqualityComparer<Person, int>

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

де як

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

неможливо. Це перевага вкладеного класу у спадок від батьківського класу.

Інший випадок (такого ж характеру - приховування реалізації) - це коли ви хочете зробити членів класу (поля, властивості тощо) доступними лише для одного класу:

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

Ще одне використання, яке ще не згадується для вкладених класів, - це поділ родових типів. Наприклад, припустимо, що у нас є кілька родових сімейств статичних класів, які можуть приймати методи з різною кількістю параметрів, поряд із значеннями для деяких з цих параметрів, і генерувати делегати з меншою кількістю параметрів. Наприклад, хочеться статичного методу, який може приймати Action<string, int, double>і отримувати a, String<string, int>який буде викликати додану дію, що передає 3,5 як double; можна також побажати статичного методу, який може приймати ан Action<string, int, double>та виходити Action<string>, переходячи 7як intі 5.3як double. Використовуючи загальні вкладені класи, можна домовитись, щоб виклики методу були чимось на зразок:

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

або, оскільки останні типи в кожному виразі можна зробити, навіть якщо перші не можуть:

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

Використання вкладених загальних типів дає змогу визначити, які делегати застосовні до яких частин загального опису типу.


1

Вкладені класи можна використовувати для наступних потреб:

  1. Класифікація даних
  2. Коли логіка основного класу є складною і ви відчуваєте, що вам потрібні підпорядковані об'єкти для управління класом
  3. Коли ви, що стан і існування класу повністю залежить від класу, що додається


0

Мені подобається вкладати винятки, унікальні для одного класу, тобто. ті, які ніколи не кидаються з будь-якого іншого місця.

Наприклад:

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

Це допомагає зберегти файли проекту акуратними та не наповненими сотнями класів винятків.


0

Майте на увазі, що вам потрібно буде протестувати вкладений клас. Якщо він приватний, ви не зможете перевірити його ізольовано.

Ти можеш зробити це внутрішнім, хоча й у поєднанні з InternalsVisibleToатрибутом . Однак це було б те саме, що зробити приватне поле внутрішнім лише для цілей тестування, що я вважаю поганою самодокументацією.

Отже, ви можете хотіти реалізовувати лише приватні вкладені класи, що передбачають низьку складність.


0

так для цього випадку:

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

Чому? Додайте трохи контексту до коду у своє рішення, щоб допомогти майбутнім читачам зрозуміти міркування, що стоять за вашою відповіддю.
Грант Міллер

0

Спираючись на моє розуміння цієї концепції, ми могли б використовувати цю функцію, коли заняття концептуально пов'язані один з одним. Я маю на увазі, що деякі з них є повним предметом у нашому бізнесі, як такі, що існують у світі DDD, які допомагають об'єднаному кореневому об'єкту завершити його ділову логіку.

Щоб уточнити, я збираюся показати це на прикладі:

Уявіть, що у нас є два класи, такі як Order і OrderItem. Для класу замовлень ми будемо керувати усіма порядками порядку, а в OrderItem ми зберігаємо дані про єдине замовлення для уточнення, ви можете побачити нижче класи:

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


    }
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.