W poście na blogu Kathleen Dollard z 2008 roku przedstawia interesujący powód, dla którego warto używać zagnieżdżonych klas w .net. Jednak wspomina również, że FxCop nie lubi klas zagnieżdżonych. Zakładam, że ludzie piszący zasady FxCop nie są głupi, więc musi być rozumowanie za tym stanowiskiem, ale nie byłem w stanie go znaleźć.
95
Odpowiedzi:
Użyj klasy zagnieżdżonej, gdy zagnieżdżona klasa jest przydatna tylko dla klasy otaczającej. Na przykład klasy zagnieżdżone pozwalają napisać coś takiego (uproszczonego):
public class SortedMap { private class TreeNode { TreeNode left; TreeNode right; } }
Możesz stworzyć pełną definicję swojej klasy w jednym miejscu, nie musisz przeskakiwać przez żadne obręcze PIMPL, aby zdefiniować, jak działa Twoja klasa, a świat zewnętrzny nie musi widzieć nic z Twojej implementacji.
Gdyby klasa TreeNode była zewnętrzna, należałoby albo utworzyć wszystkie pola,
public
albo kilkaget/set
metod, aby z niej korzystać. Świat zewnętrzny miałby inną klasę zanieczyszczającą ich inteligencję.źródło
Z samouczka Java firmy Sun:
Dlaczego warto używać klas zagnieżdżonych? Istnieje kilka istotnych powodów, dla których warto używać klas zagnieżdżonych, między innymi:
Logiczne grupowanie klas - jeśli klasa jest przydatna tylko dla jednej innej klasy, logiczne jest osadzenie jej w tej klasie i trzymanie obu razem. Zagnieżdżanie takich „klas pomocniczych” sprawia, że ich pakiet jest bardziej uproszczony.
Zwiększona hermetyzacja - rozważ dwie klasy najwyższego poziomu, A i B, w których B potrzebuje dostępu do elementów członkowskich A, które w przeciwnym razie zostałyby uznane za prywatne. Ukrywając klasę B w klasie A, członkowie A mogą zostać uznani za prywatnych i B może uzyskać do nich dostęp. Ponadto samo B można ukryć przed światem zewnętrznym.<- Nie dotyczy to implementacji klas zagnieżdżonych w języku C #, dotyczy to tylko języka Java.Bardziej czytelny i łatwiejszy w utrzymaniu kod - zagnieżdżanie małych klas w klasach najwyższego poziomu umieszcza kod bliżej miejsca, w którym jest używany.
źródło
W pełni leniwy i bezpieczny dla wątków wzorzec singletonowy
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(); } }
źródło: https://csharpindepth.com/Articles/Singleton
źródło
To zależy od zastosowania. Rzadko używałbym zagnieżdżonej klasy Public, ale cały czas używam zagnieżdżonych klas Private. Prywatna klasa zagnieżdżona może być używana dla podobiektu, który ma być używany tylko wewnątrz obiektu nadrzędnego. Przykładem może być sytuacja, w której klasa HashTable zawiera prywatny obiekt Entry do przechowywania danych tylko wewnętrznie.
Jeśli klasa ma być używana przez wywołującego (zewnętrznie), generalnie lubię uczynić ją oddzielną, samodzielną klasą.
źródło
Oprócz innych powodów wymienionych powyżej jest jeszcze jeden powód, dla którego przychodzi mi do głowy nie tylko użycie klas zagnieżdżonych, ale w rzeczywistości publicznych klas zagnieżdżonych. Dla tych, którzy pracują z wieloma klasami generycznymi, które mają te same parametry typu ogólnego, możliwość zadeklarowania ogólnej przestrzeni nazw byłaby niezwykle przydatna. Niestety .Net (lub przynajmniej C #) nie obsługuje idei ogólnych przestrzeni nazw. Aby osiągnąć ten sam cel, możemy użyć klas ogólnych, aby osiągnąć ten sam cel. Weźmy następujące przykładowe klasy związane z jednostką logiczną:
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> { }
Możemy uprościć podpisy tych klas, używając ogólnej przestrzeni nazw (zaimplementowanej za pośrednictwem klas zagnieżdżonych):
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 {} }
Następnie, korzystając z klas częściowych, zgodnie z sugestią Erika van Brakela we wcześniejszym komentarzu, można podzielić klasy na oddzielne zagnieżdżone pliki. Zalecam używanie rozszerzenia Visual Studio, takiego jak NestIn, do obsługi zagnieżdżania częściowych plików klas. Dzięki temu pliki klas „przestrzeni nazw” mogą być również używane do organizowania zagnieżdżonych plików klas w sposób podobny do folderu.
Na przykład:
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); } }
Pliki w eksploratorze rozwiązań Visual Studio byłyby wtedy zorganizowane w następujący sposób:
Entity.cs + Entity.BaseDataObject.cs + Entity.BaseDataObjectList.cs + Entity.IBaseBusiness.cs + Entity.IBaseDataAccess.cs
I można zaimplementować ogólną przestrzeń nazw w następujący sposób:
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 {} }
Pliki zostaną zorganizowane w eksploratorze rozwiązań w następujący sposób:
User.cs + User.DataObject.cs + User.DataObjectList.cs + User.IBusiness.cs + User.IDataAccess.cs
Powyższe jest prostym przykładem użycia klasy zewnętrznej jako ogólnej przestrzeni nazw. W przeszłości zbudowałem „ogólne przestrzenie nazw” zawierające 9 lub więcej parametrów typu. Konieczność synchronizowania tych parametrów typu w dziewięciu typach, które wszystkie musiały znać parametry typu, była żmudna, szczególnie podczas dodawania nowego parametru. Zastosowanie ogólnych przestrzeni nazw sprawia, że kod ten jest znacznie łatwiejszy w zarządzaniu i czytelny.
źródło
Jeśli dobrze rozumiem artykuł Katheleen, proponuje ona użycie klasy zagnieżdżonej, aby móc napisać SomeEntity.Collection zamiast EntityCollection <SomeEntity>. Moim zdaniem jest to kontrowersyjny sposób, aby zaoszczędzić trochę pisania. Jestem prawie pewien, że w prawdziwym świecie kolekcje aplikacji będą miały pewną różnicę w implementacjach, więc i tak będziesz musiał utworzyć oddzielną klasę. Myślę, że używanie nazwy klasy do ograniczania innych zakresów klas nie jest dobrym pomysłem. Zanieczyszcza inteligencję i wzmacnia zależności między klasami. Używanie przestrzeni nazw to standardowy sposób kontrolowania zakresu klas. Jednak uważam, że użycie zagnieżdżonych klas, takich jak komentarz @hazzen, jest dopuszczalne, chyba że masz mnóstwo zagnieżdżonych klas, co jest oznaką złego projektu.
źródło
Często używam klas zagnieżdżonych, aby ukryć szczegóły implementacji. Przykład z odpowiedzi Erica Lipperta tutaj:
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 { ... } }
Ten wzorzec staje się jeszcze lepszy dzięki zastosowaniu generyków. Zobacz to pytanie, aby zobaczyć dwa fajne przykłady. Więc w końcu piszę
Equality<Person>.CreateComparer(p => p.Id);
zamiast
new EqualityComparer<Person, int>(p => p.Id);
Mogę też mieć ogólną listę,
Equality<Person>
ale nieEqualityComparer<Person, int>
var l = new List<Equality<Person>> { Equality<Person>.CreateComparer(p => p.Id), Equality<Person>.CreateComparer(p => p.Name) }
natomiast
var l = new List<EqualityComparer<Person, ??>>> { new EqualityComparer<Person, int>>(p => p.Id), new EqualityComparer<Person, string>>(p => p.Name) }
nie jest możliwe. Taka jest korzyść z dziedziczenia klasy zagnieżdżonej po klasie nadrzędnej.
Innym przypadkiem (o tym samym charakterze - ukrywanie implementacji) jest sytuacja, w której chcesz udostępnić składowe klasy (pola, właściwości itp.) Tylko dla jednej klasy:
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 }
źródło
Innym zastosowaniem, o którym jeszcze nie wspomniano, dla klas zagnieżdżonych jest segregacja typów ogólnych. Na przykład załóżmy, że ktoś chce mieć kilka ogólnych rodzin klas statycznych, które mogą przyjmować metody z różnymi liczbami parametrów wraz z wartościami niektórych z tych parametrów i generować delegatów z mniejszą liczbą parametrów. Na przykład, ktoś chce mieć statyczną metodę, która może przyjmować a
Action<string, int, double>
i dawać a,String<string, int>
która wywoła podaną akcję, przekazując 3.5 jakodouble
; można również chcieć mieć metodę statyczną, która może przyjąć anAction<string, int, double>
i dać anAction<string>
, przechodząc7
jakoint
i5.3
jakodouble
. Używając ogólnych klas zagnieżdżonych, można ustawić wywołania metod w następujący sposób:MakeDelegate<string,int>.WithParams<double>(theDelegate, 3.5); MakeDelegate<string>.WithParams<int,double>(theDelegate, 7, 5.3);
lub, ponieważ te ostatnie typy w każdym wyrażeniu można wywnioskować, nawet jeśli pierwsze nie mogą:
MakeDelegate<string,int>.WithParams(theDelegate, 3.5); MakeDelegate<string>.WithParams(theDelegate, 7, 5.3);
Użycie zagnieżdżonych typów ogólnych umożliwia określenie, którzy delegaci mają zastosowanie do których części ogólnego opisu typu.
źródło
Zagnieżdżone klasy mogą być używane do następujących potrzeb:
źródło
Jak wspominał Nawfal o implementacji wzorca Abstract Factory, kod ten można ustawić w celu uzyskania wzorca Class Clusters, który jest oparty na wzorcu Abstract Factory.
źródło
Lubię zagnieżdżać wyjątki, które są unikalne dla jednej klasy, tj. takie, które nigdy nie są wyrzucane z innego miejsca.
Na przykład:
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() } }
Pomaga to w utrzymaniu porządku w plikach projektu i nie zawiera setek krótkich klas wyjątków.
źródło
Pamiętaj, że musisz przetestować zagnieżdżoną klasę. Jeśli jest prywatny, nie będzie można go przetestować w izolacji.
Możesz jednak uczynić go wewnętrznym, w połączeniu z
InternalsVisibleTo
atrybutem . Jednak byłoby to tym samym, co uczynienie pola prywatnego wewnętrznym tylko do celów testowych, co uważam za złą dokumentację własną.Dlatego możesz chcieć zaimplementować tylko prywatne klasy zagnieżdżone o niskiej złożoności.
źródło
tak w tym przypadku:
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); } } }
źródło
Opierając się na moim zrozumieniu tej koncepcji, moglibyśmy użyć tej funkcji, gdy klasy są ze sobą powiązane koncepcyjnie. Chodzi mi o to, że niektóre z nich są kompletną jedną pozycją w naszej firmie, podobną do encji, które istnieją w świecie DDD, które pomagają zagregowanemu obiektowi głównemu uzupełnić logikę biznesową.
Aby to wyjaśnić, pokażę to na przykładzie:
Wyobraź sobie, że mamy dwie klasy, takie jak Order i OrderItem. W klasie order będziemy zarządzać wszystkimi orderItems, aw OrderItem przechowujemy dane o pojedynczym zamówieniu w celu wyjaśnienia, możesz zobaczyć poniższe klasy:
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(); } }
źródło