Dlaczego / kiedy należy używać zagnieżdżonych klas w .net? A może nie powinieneś?

95

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źć.

Eric Haskins
źródło
Link do archiwum Wayback do wpisu na blogu: web.archive.org/web/20141127115939/https://blogs.msmvps.com/…
iokevins
Jak zauważył Nawfal , nasz kumpel Eric Lippert odpowiedział na powielenie tego pytania tutaj , odpowiedź na pytanie zaczyna się od: „ Użyj klas zagnieżdżonych, gdy potrzebujesz klasy pomocniczej, która jest bez znaczenia poza klasą; szczególnie gdy klasa zagnieżdżona może korzystać z prywatne szczegóły implementacji klasy zewnętrznej. Twój argument, że klasy zagnieżdżone są bezużyteczne, jest również argumentem, że metody prywatne są bezużyteczne ... "
ruffin

Odpowiedzi:

97

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, publicalbo kilka get/setmetod, aby z niej korzystać. Świat zewnętrzny miałby inną klasę zanieczyszczającą ich inteligencję.

hazzen
źródło
44
Aby dodać do tego: Możesz również użyć klas częściowych w oddzielnych plikach, aby nieco lepiej zarządzać swoim kodem. Umieść klasę wewnętrzną w osobnym pliku (w tym przypadku SortedMap.TreeNode.cs). Powinno to zachować czystość kodu, a jednocześnie zachować go oddzielnie :)
Erik van Brakel
1
Będą przypadki, w których trzeba będzie uczynić zagnieżdżoną klasę publiczną lub wewnętrzną, jeśli jest używana w zwracanym typie publicznego interfejsu API lub publicznej właściwości klasy kontenera. Nie jestem jednak pewien, czy to dobra praktyka. W takich przypadkach bardziej sensowne może być wyciągnięcie zagnieżdżonej klasy poza klasę kontenera. Jednym z takich przykładów jest klasa System.Windows.Forms.ListViewItem.ListViewSubItem w środowisku .Net.
RBT
16

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:

  • Jest to sposób na logiczne grupowanie klas, które są używane tylko w jednym miejscu.
  • Zwiększa hermetyzację.
  • Klasy zagnieżdżone mogą prowadzić do bardziej czytelnego i łatwiejszego w utrzymaniu kodu.

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.

Esteban Araya
źródło
1
To tak naprawdę nie ma zastosowania, ponieważ nie można uzyskać dostępu do zmiennych instancji z otaczającej klasy w C #, tak jak w Javie. Dostępne są tylko statyczne elementy członkowskie.
Ben Baron
5
Jeśli jednak przekażesz wystąpienie klasy otaczającej do klasy zagnieżdżonej, klasa zagnieżdżona będzie miała pełny dostęp do wszystkich elementów członkowskich poprzez tę zmienną wystąpienia ... więc tak naprawdę, to tak, jakby Java czyni zmienną wystąpienia niejawną, podczas gdy w C # musisz wyraź to.
Alex
@Alex Nie, w Javie zagnieżdżona klasa faktycznie przechwytuje instancję klasy nadrzędnej podczas tworzenia instancji - między innymi oznacza to, że zapobiega to gromadzeniu elementów bezużytecznych. Oznacza to również, że nie można utworzyć wystąpienia klasy zagnieżdżonej bez klasy nadrzędnej. Więc nie, to wcale nie jest to samo.
Tomáš Zato - Przywróć Monikę
2
@ TomášZato Mój opis jest naprawdę trafny. W praktyce istnieje niejawna zmienna wystąpienia nadrzędnego w klasach zagnieżdżonych w Javie, podczas gdy w C # trzeba jawnie przekazać klasę wewnętrzną wystąpienie. Konsekwencją tego, jak powiedziałeś, jest to, że wewnętrzne klasy Javy muszą mieć instancję nadrzędną, podczas gdy C # nie. W każdym razie moim głównym celem było to, że klasy wewnętrzne C # mogą również uzyskiwać dostęp do prywatnych pól i właściwości swoich rodziców, ale aby móc to zrobić, należy je przekazać do wystąpienia nadrzędnego.
Alex
9

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

kay.one
źródło
5

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ą.

Chris Dail
źródło
5

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.

Tyree Jackson
źródło
3

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.

aku
źródło
1

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
}
nawfal
źródło
1

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 jako double; można również chcieć mieć metodę statyczną, która może przyjąć an Action<string, int, double>i dać an Action<string>, przechodząc 7jako inti 5.3jako double. 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.

supercat
źródło
1

Zagnieżdżone klasy mogą być używane do następujących potrzeb:

  1. Klasyfikacja danych
  2. Gdy logika klasy głównej jest skomplikowana i czujesz, że do zarządzania klasą potrzebujesz obiektów podrzędnych
  3. Kiedy stwierdzisz, że stan i istnienie klasy w pełni zależy od otaczającej klasy
Rachin Goyal
źródło
0

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.

diimdeep
źródło
0

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.

Eric Dand
źródło
0

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 InternalsVisibleToatrybutem . 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.

tm1
źródło
0

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);
        }
    }
}
Alex Martinez
źródło
Czemu? Dodaj kontekst do kodu w swoim rozwiązaniu, aby pomóc przyszłym czytelnikom zrozumieć uzasadnienie Twojej odpowiedzi.
Grant Miller
0

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


    }
Hamid
źródło