Почему и когда вы должны использовать вложенные классы в .net? Или нет?

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

Ссылка из архива Wayback на сообщение в блоге: web.archive.org/web/20141127115939/https://blogs.msmvps.com/‌…

iokevins 05.09.2019 00:20

Как науфал указывает, наш приятель Эрик Липперт ответил на дубликат этого вопроса здесь., ответ на вопрос начинается с "Используйте вложенные классы, когда вам нужен вспомогательный класс, который не имеет смысла вне класса; особенно когда вложенный класс может использовать частные детали реализации внешнего класса. Ваш аргумент о том, что вложенные классы бесполезны, также является аргументом о том, что частные методы бесполезны ..."

ruffin 06.09.2019 00:11
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
97
2
46 428
14
Перейти к ответу Данный вопрос помечен как решенный

Ответы 14

Ответ принят как подходящий

Используйте вложенный класс, когда вложенный класс полезен только для включающего класса. Например, вложенные классы позволяют писать что-то вроде (упрощенно):

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

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

Если бы класс TreeNode был внешним, вам пришлось бы либо сделать все поля public, либо создать несколько методов get/set для его использования. Во внешнем мире будет другой класс, загрязняющий их интеллект.

Чтобы добавить к этому: вы также можете использовать частичные классы в отдельных файлах, чтобы немного лучше управлять своим кодом. Поместите внутренний класс в отдельный файл (в данном случае SortedMap.TreeNode.cs). Это должно поддерживать ваш код в чистоте, а также сохранять его отдельно :)

Erik van Brakel 08.09.2008 03:27

Бывают случаи, когда вам нужно будет сделать вложенный класс общедоступным или внутренним, если он используется в типе возвращаемого значения общедоступного API или общедоступного свойства класса контейнера. Я не уверен, что это хорошая практика. В таких случаях может иметь смысл вытащить вложенный класс за пределы класса контейнера. Класс System.Windows.Forms.ListViewItem.ListViewSubItem в платформе .Net является одним из таких примеров.

RBT 25.04.2016 12:55

Если я правильно понимаю статью Кателин, она предлагает использовать вложенный класс, чтобы иметь возможность писать SomeEntity.Collection вместо EntityCollection <SomeEntity>. На мой взгляд, это противоречивый способ сэкономить вам время на вводе текста. Я почти уверен, что в реальных коллекциях приложений будет какая-то разница в реализации, поэтому вам все равно придется создавать отдельный класс. Я думаю, что использование имени класса для ограничения других возможностей класса - не лучшая идея. Это загрязняет интеллект и усиливает зависимости между классами. Использование пространств имен - это стандартный способ управления областью классов. Однако я считаю, что использование вложенных классов, таких как комментарий @hazzen, допустимо, если у вас нет множества вложенных классов, что является признаком плохого дизайна.

Это зависит от использования. Я редко когда-либо использую публичный вложенный класс, но все время использую частные вложенные классы. Частный вложенный класс может использоваться для подобъекта, который предназначен для использования только внутри родительского объекта. Примером этого может быть класс HashTable, содержащий частный объект Entry для внутреннего хранения данных.

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

Из учебника Sun по Java:

Зачем использовать вложенные классы? Есть несколько веских причин для использования вложенных классов, среди них:

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

Логическая группировка классов. Если класс полезен только для одного другого класса, то логично встроить его в этот класс и сохранить их вместе. Вложение таких «вспомогательных классов» делает их пакет более оптимизированным.

Повышенная инкапсуляция - рассмотрим два класса верхнего уровня, A и B, где B требуется доступ к членам A, которые в противном случае были бы объявлены частными. Скрывая класс B внутри класса A, члены A могут быть объявлены частными, и B может получить к ним доступ. Кроме того, сам B может быть скрыт от внешнего мира. <- это не относится к реализации вложенных классов C#, это применимо только к Java.

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

На самом деле это не применимо, поскольку вы не можете получить доступ к переменным экземпляра из включающего класса в C#, как в Java. Доступны только статические члены.

Ben Baron 02.09.2013 21:39

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

Alex 01.11.2013 16:25

@Alex Нет, это не так, в Java вложенный класс фактически захватывает экземпляр родительского класса при создании экземпляра - среди прочего, это означает, что он предотвращает сборку мусора родительского класса. Это также означает, что вложенный класс не может быть создан без родительского класса. Так что нет, это совсем не одно и то же.

Tomáš Zato - Reinstate Monica 26.10.2017 21:46

@ TomášZato Мое описание очень подходящее. Фактически существует неявная переменная родительского экземпляра во вложенных классах в Java, тогда как в C# вы должны явно передать экземпляр внутреннему классу. Следствием этого, как вы говорите, является то, что внутренние классы Java должны иметь родительский экземпляр, а C# - нет. В любом случае я хотел сказать, что внутренние классы C# также могут иметь доступ к своим родительским частным полям и свойствам, но для этого необходимо явно передать родительский экземпляр.

Alex 27.10.2017 14:12

Полностью ленивый и потокобезопасный одноэлементный шаблон

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

источник: https://csharpindepth.com/Articles/Singleton

Я часто использую вложенные классы, чтобы скрыть детали реализации. Пример из ответа Эрика Липперта здесь:

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
}

Еще одно использование вложенных классов, еще не упомянутое, - это разделение универсальных типов. Например, предположим, что кто-то хочет иметь несколько общих семейств статических классов, которые могут принимать методы с различным количеством параметров, а также значения для некоторых из этих параметров и генерировать делегаты с меньшим количеством параметров. Например, кто-то хочет иметь статический метод, который может принимать Action<string, int, double> и выдавать 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. Классификация данных
  2. Когда логика основного класса сложна и вы чувствуете, что вам требуются подчиненные объекты для управления классом
  3. Когда вы понимаете, что состояние и существование класса полностью зависит от включающего класса

Мне нравится вкладывать исключения, уникальные для одного класса, т.е. те, которые никогда не выбрасываются ни из какого другого места.

Например:

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

Это помогает поддерживать порядок в файлах вашего проекта и не содержать в себе сотни коротеньких небольших классов исключений.

Имейте в виду, что вам нужно будет протестировать вложенный класс. Если он частный, вы не сможете протестировать его изолированно.

Однако вы можете сделать его внутренним, в сочетании с атрибутом InternalsVisibleTo. Однако это было бы то же самое, что сделать частное поле внутренним только для целей тестирования, что я считаю плохой самостоятельной документацией.

Таким образом, вы можете захотеть реализовать только частные вложенные классы с низкой сложностью.

В дополнение к другим причинам, перечисленным выше, есть еще одна причина, по которой я могу думать не только о том, чтобы использовать вложенные классы, но и фактически общедоступные вложенные классы. Для тех, кто работает с несколькими универсальными классами, которые используют одни и те же параметры универсального типа, возможность объявить универсальное пространство имен будет чрезвычайно полезной. К сожалению, .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);
    }

}

Затем файлы в обозревателе решений Visual Studio будут организованы следующим образом:

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 или более параметров типа. Необходимость поддерживать синхронизацию этих параметров типа между девятью типами, которые все должны были знать параметры типа, была утомительной, особенно при добавлении нового параметра. Использование общих пространств имен делает этот код более управляемым и читаемым.

да в этом случае:

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

Почему? Добавьте контекст в код своего решения, чтобы помочь будущим читателям понять причину вашего ответа.

Grant Miller 23.06.2018 05:37

Основываясь на моем понимании этой концепции мы могли бы использовать эту функцию, когда классы концептуально связаны друг с другом. Я имею в виду, что некоторые из них представляют собой один элемент в нашем бизнесе, как сущности, существующие в мире 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();
        }


    }

Другие вопросы по теме