Почему / когда вы должны использовать вложенные классы в.net? Или ты не должен?
В недавнем сообщении в блоге Кэтлин Доллард она представляет интересную причину использования вложенных классов в.net. Тем не менее, она также упоминает, что FxCop не любит вложенные классы. Я предполагаю, что люди, пишущие правила FxCop, не глупы, поэтому, должно быть, за этим стоит аргументация, но я не смог ее найти.
13 ответов
Используйте вложенный класс, когда вложенный класс полезен только для включающего класса. Например, вложенные классы позволяют писать что-то вроде (упрощенно):
public class SortedMap {
private class TreeNode {
TreeNode left;
TreeNode right;
}
}
Вы можете сделать полное определение вашего класса в одном месте, вам не нужно прыгать через любые обручи PIMPL, чтобы определить, как работает ваш класс, и внешнему миру не нужно ничего видеть в вашей реализации.
Если бы класс TreeNode был внешним, вам бы пришлось заполнить все поля public
или сделать кучу get/set
методы его использования. У внешнего мира был бы другой класс, загрязняющий их разум.
Зачем использовать вложенные классы? Есть несколько веских причин для использования вложенных классов, среди них:
- Это способ логической группировки классов, которые используются только в одном месте.
- Это увеличивает инкапсуляцию.
- Вложенные классы могут привести к более удобочитаемому и поддерживаемому коду.
Логическая группировка классов. Если класс полезен только для одного другого класса, то логично встраивать его в этот класс и сохранять их вместе. Вложение таких "вспомогательных классов" делает их пакет более упорядоченным.
Увеличенная инкапсуляция. Рассмотрим два класса верхнего уровня, A и B, где B необходим доступ к членам A, которые в противном случае были бы объявлены закрытыми. Скрывая класс B в классе A, члены A могут быть объявлены частными, и B может получить к ним доступ. Кроме того, сам 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
И вы реализуете общее пространство имен, как показано ниже:
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 или более параметров типа. Необходимость сохранять эти параметры типов синхронизированными между девятью типами, которые все должны были знать параметры типа, была утомительной, особенно при добавлении нового параметра. Использование общих пространств имен делает этот код намного более управляемым и читаемым.
Если я правильно понимаю статью Кэтелин, она предлагает использовать вложенный класс, чтобы иметь возможность писать SomeEntity.Collection вместо EntityCollection
Я часто использую вложенные классы, чтобы скрыть детали реализации. Пример из ответа Эрика Липперта здесь:
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);
Использование вложенных универсальных типов позволяет определить, какие делегаты применимы к каким частям общего описания типа.
Вложенные классы могут использоваться для следующих нужд:
- Классификация данных
- Когда логика основного класса сложна, и вы чувствуете, что вам нужны подчиненные объекты для управления классом
- Когда вы, что состояние и существование класса полностью зависит от окружающего класса
Помните, что вам нужно проверить вложенный класс. Если это личное, вы не сможете протестировать его изолированно.
Вы можете сделать это внутренним, однако, в сочетании с InternalsVisibleTo
атрибут Однако это было бы то же самое, что сделать внутреннее частное поле только для целей тестирования, что я считаю плохой самодокументированием.
Таким образом, вы можете захотеть реализовать только частные вложенные классы с низкой сложностью.
Основываясь на моем понимании этой концепции, мы могли бы использовать эту функцию, когда классы концептуально связаны друг с другом. Я имею в виду, что некоторые из них представляют собой один элемент в нашем бизнесе, как сущности, существующие в мире DDD, которые помогают объединенному корневому объекту завершить его бизнес-логику.
Чтобы пояснить, я покажу это на примере:
Представьте, что у нас есть два класса, например Order и OrderItem. В классе заказа мы собираемся управлять всеми элементами 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();
}
}
Мне нравится вкладывать исключения, которые являются уникальными для одного класса, т.е. те, которые никогда не выбрасывают из любого другого места.
Например:
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()
}
}
Это помогает сохранить ваши файлы проекта аккуратными и не полными сотен маленьких классных исключений.
Как nawfal упомянул реализацию шаблона Abstract Factory, этот код может быть расширен для достижения шаблона Class Clusters, который основан на шаблоне Abstract Factory.
Да для этого случая:
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);
}
}
}