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

StackOverflow https://stackoverflow.com/questions/48872

  •  09-06-2019
  •  | 
  •  

Вопрос

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

Это было полезно?

Решение

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

И вы бы реализовали общее пространство имен следующим образом:

Пользователь.cs

public
partial class   User
:
                Entity
                <
                    User.DataObject, 
                    User.DataObjectList, 
                    User.IBusiness, 
                    User.IDataAccess
                >
{
}

Пользователь.DataObject.cs

partial class   User
{

    public  class   DataObject : BaseDataObject 
    {
        public  string  UserName            { get; set; }
        public  byte[]  PasswordHash        { get; set; }
        public  bool    AccountIsEnabled    { get; set; }
    }

}

Пользователь.DataObjectList.cs

partial class   User
{

    public  class   DataObjectList : BaseDataObjectList {}

}

Пользователь.IBusiness.cs

partial class   User
{

    public  interface   IBusiness : IBaseBusiness {}

}

Пользователь.IDataAccess.cs

partial class   User
{

    public  interface   IDataAccess : IBaseDataAccess {}

}

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

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

Выше приведен простой пример использования внешнего класса в качестве общего пространства имен.Раньше я создавал «универсальные пространства имен», содержащие 9 или более параметров типа.Необходимость синхронизировать эти параметры типов для всех девяти типов, которые все должны были знать параметры типа, была утомительной, особенно при добавлении нового параметра.Использование общих пространств имен делает этот код гораздо более управляемым и читабельным.

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

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

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

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
}

Как науфал упомянутую реализацию шаблона «Абстрактная фабрика», этот код можно расширить для достижения Шаблон кластеров классов который основан на шаблоне «Абстрактная фабрика».

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

Например:

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

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

да, для этого случая:

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);
        }
    }
}
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top