Warum / wann sollten Sie verschachtelte Klassen in .net verwenden? Oder solltest du nicht?

93

In Kathleen Dollards Blogbeitrag von 2008 stellt sie einen interessanten Grund vor, verschachtelte Klassen in .net zu verwenden. Sie erwähnt jedoch auch, dass FxCop keine verschachtelten Klassen mag. Ich gehe davon aus, dass die Leute, die FxCop-Regeln schreiben, nicht dumm sind, also muss es Gründe für diese Position geben, aber ich konnte sie nicht finden.

Eric Haskins
quelle
Wayback-Archiv-Link zum Blog-Beitrag: web.archive.org/web/20141127115939/https://blogs.msmvps.com/…
iokevins
Wie nawfal betont, hat unser Kumpel Eric Lippert hier ein Duplikat dieser Frage beantwortet. Die fragliche Antwort beginnt mit " Verwenden Sie verschachtelte Klassen, wenn Sie eine Hilfsklasse benötigen, die außerhalb der Klasse bedeutungslos ist, insbesondere wenn die verschachtelte Klasse sie verwenden kann Details der privaten Implementierung der äußeren Klasse. Ihr Argument, dass verschachtelte Klassen nutzlos sind, ist auch ein Argument, dass private Methoden nutzlos sind ... "
Ruffin

Antworten:

96

Verwenden Sie eine verschachtelte Klasse, wenn die Klasse, die Sie verschachteln, nur für die einschließende Klasse nützlich ist. Mit verschachtelten Klassen können Sie beispielsweise Folgendes schreiben: (vereinfacht):

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

Sie können Ihre Klasse an einem Ort vollständig definieren. Sie müssen nicht durch PIMPL-Rahmen springen, um zu definieren, wie Ihre Klasse funktioniert, und die Außenwelt muss nichts von Ihrer Implementierung sehen.

Wenn die TreeNode-Klasse extern wäre, müssten Sie entweder alle Felder erstellen publicoder eine Reihe von get/setMethoden erstellen , um sie zu verwenden. Die Außenwelt würde eine andere Klasse haben, die ihre Intelligenz verschmutzt.

Hazzen
quelle
44
Hinzu kommt: Sie können auch Teilklassen in separaten Dateien verwenden, um Ihren Code etwas besser zu verwalten. Fügen Sie die innere Klasse in eine separate Datei ein (in diesem Fall SortedMap.TreeNode.cs). Dies sollte Ihren Code sauber halten und gleichzeitig Ihren Code getrennt halten :)
Erik van Brakel
1
Es wird Fälle geben, in denen Sie die verschachtelte Klasse öffentlich oder intern machen müssen, wenn sie im Rückgabetyp einer öffentlichen API oder einer öffentlichen Eigenschaft der Containerklasse verwendet wird. Ich bin mir nicht sicher, ob es eine gute Praxis ist. In solchen Fällen ist es möglicherweise sinnvoller, die verschachtelte Klasse außerhalb der Containerklasse herauszuziehen. Die System.Windows.Forms.ListViewItem.ListViewSubItem-Klasse im .Net-Framework ist ein solches Beispiel.
RBT
16

Aus dem Java-Tutorial von Sun:

Warum verschachtelte Klassen verwenden? Es gibt mehrere zwingende Gründe für die Verwendung verschachtelter Klassen, darunter:

  • Auf diese Weise können Klassen, die nur an einer Stelle verwendet werden, logisch gruppiert werden.
  • Es erhöht die Einkapselung.
  • Verschachtelte Klassen können zu besser lesbarem und wartbarem Code führen.

Logische Gruppierung von Klassen - Wenn eine Klasse nur für eine andere Klasse nützlich ist, ist es logisch, sie in diese Klasse einzubetten und die beiden zusammenzuhalten. Durch das Verschachteln solcher "Hilfsklassen" wird das Paket rationalisiert.

Erhöhte Kapselung - Betrachten Sie zwei Klassen der obersten Ebene, A und B, bei denen B Zugriff auf Mitglieder von A benötigt, die andernfalls als privat deklariert würden. Durch das Ausblenden von Klasse B in Klasse A können die Mitglieder von A als privat deklariert werden und B kann auf sie zugreifen. Außerdem kann B selbst vor der Außenwelt verborgen sein. <- Dies gilt nicht für die Implementierung verschachtelter Klassen durch C #, sondern nur für Java.

Besser lesbarer, wartbarer Code - Durch das Verschachteln kleiner Klassen in Klassen der obersten Ebene wird der Code näher an dem Ort platziert, an dem er verwendet wird.

Esteban Araya
quelle
1
Dies trifft nicht wirklich zu, da Sie in C # nicht wie in Java auf Instanzvariablen aus der einschließenden Klasse zugreifen können. Nur statische Mitglieder sind zugänglich.
Ben Baron
5
Wenn Sie jedoch eine Instanz der einschließenden Klasse an die verschachtelte Klasse übergeben, hat die verschachtelte Klasse über diese Instanzvariable vollen Zugriff auf alle Mitglieder. Es ist also so, als ob Java die Instanzvariable implizit macht, während dies in C # erforderlich ist mach es explizit.
Alex
@Alex Nein, ist es nicht, in Java erfasst die verschachtelte Klasse tatsächlich die Instanz der übergeordneten Klasse, wenn sie instanziiert wird. Dies bedeutet unter anderem, dass verhindert wird, dass die übergeordnete Klasse Müll sammelt. Dies bedeutet auch, dass die verschachtelte Klasse nicht ohne übergeordnete Klasse instanziiert werden kann. Also nein, diese sind überhaupt nicht gleich.
Tomáš Zato - Wiedereinsetzung Monica
2
@ TomášZato Meine Beschreibung ist wirklich ziemlich passend. In verschachtelten Klassen in Java gibt es effektiv eine implizite übergeordnete Instanzvariable, während Sie in C # der inneren Klasse die Instanz explizit übergeben müssen. Eine Konsequenz davon ist, wie Sie sagen, dass Javas innere Klassen eine übergeordnete Instanz haben müssen, während C # dies nicht tun. Mein Hauptpunkt war auf jeden Fall, dass die inneren Klassen von C # auch auf die privaten Felder und Eigenschaften der Eltern zugreifen können, dass jedoch die übergeordnete Instanz explizit übergeben werden muss, um dies tun zu können.
Alex
9

Völlig faul und threadsicheres Singleton-Muster

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

Quelle: http://www.yoda.arachsys.com/csharp/singleton.html

Kay One
quelle
5

Das hängt von der Nutzung ab. Ich würde selten eine öffentlich verschachtelte Klasse verwenden, aber die ganze Zeit private verschachtelte Klassen. Eine private verschachtelte Klasse kann für ein Unterobjekt verwendet werden, das nur innerhalb des übergeordneten Objekts verwendet werden soll. Ein Beispiel hierfür wäre, wenn eine HashTable-Klasse ein privates Eintragsobjekt enthält, um Daten nur intern zu speichern.

Wenn die Klasse vom Aufrufer (extern) verwendet werden soll, mache ich sie im Allgemeinen gerne zu einer separaten eigenständigen Klasse.

Chris Dail
quelle
5

Zusätzlich zu den anderen oben aufgeführten Gründen gibt es einen weiteren Grund, den ich mir vorstellen kann, nicht nur verschachtelte Klassen zu verwenden, sondern tatsächlich öffentlich verschachtelte Klassen. Für diejenigen, die mit mehreren generischen Klassen arbeiten, die dieselben generischen Typparameter verwenden, wäre die Möglichkeit, einen generischen Namespace zu deklarieren, äußerst nützlich. Leider unterstützt .Net (oder zumindest C #) die Idee generischer Namespaces nicht. Um dasselbe Ziel zu erreichen, können wir generische Klassen verwenden, um dasselbe Ziel zu erreichen. Nehmen Sie die folgenden Beispielklassen für eine logische Entität:

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>
{
}

Wir können die Signaturen dieser Klassen vereinfachen, indem wir einen generischen Namespace verwenden (implementiert über verschachtelte Klassen):

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 {}

}

Durch die Verwendung von Teilklassen, wie von Erik van Brakel in einem früheren Kommentar vorgeschlagen, können Sie die Klassen in separate verschachtelte Dateien aufteilen. Ich empfehle die Verwendung einer Visual Studio-Erweiterung wie NestIn, um das Verschachteln der Teilklassendateien zu unterstützen. Auf diese Weise können die Klassendateien "Namespace" auch verwendet werden, um die verschachtelten Klassendateien in einem Ordner zu organisieren.

Beispielsweise:

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

}

Die Dateien im Visual Studio Solution Explorer würden dann wie folgt organisiert:

Entity.cs
+   Entity.BaseDataObject.cs
+   Entity.BaseDataObjectList.cs
+   Entity.IBaseBusiness.cs
+   Entity.IBaseDataAccess.cs

Und Sie würden den generischen Namespace wie folgt implementieren:

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 {}

}

Und die Dateien würden im Lösungs-Explorer wie folgt organisiert:

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

Das Obige ist ein einfaches Beispiel für die Verwendung einer äußeren Klasse als generischen Namespace. Ich habe in der Vergangenheit "generische Namespaces" mit 9 oder mehr Typparametern erstellt. Es war mühsam, diese Typparameter über die neun Typen hinweg synchron zu halten, die alle zur Kenntnis der Typparameter erforderlich waren, insbesondere beim Hinzufügen eines neuen Parameters. Die Verwendung generischer Namespaces macht diesen Code weitaus übersichtlicher und lesbarer.

Tyree Jackson
quelle
3

Wenn ich Katheleens Artikel richtig verstehe, schlägt sie vor, eine verschachtelte Klasse zu verwenden, um SomeEntity.Collection anstelle von EntityCollection <SomeEntity> schreiben zu können. Meiner Meinung nach ist es eine kontroverse Möglichkeit, Ihnen das Tippen zu ersparen. Ich bin mir ziemlich sicher, dass Anwendungssammlungen in der Praxis einige Unterschiede bei den Implementierungen aufweisen. Daher müssen Sie ohnehin eine separate Klasse erstellen. Ich denke, dass es keine gute Idee ist, den Klassennamen zu verwenden, um den Umfang anderer Klassen einzuschränken. Es verschmutzt die Intelligenz und stärkt die Abhängigkeiten zwischen Klassen. Die Verwendung von Namespaces ist eine Standardmethode zur Steuerung des Klassenbereichs. Ich finde jedoch, dass die Verwendung verschachtelter Klassen wie im @ hazzen-Kommentar akzeptabel ist, es sei denn, Sie haben Tonnen verschachtelter Klassen, was ein Zeichen für schlechtes Design ist.

aku
quelle
1

Ich verwende oft verschachtelte Klassen, um Implementierungsdetails auszublenden. Ein Beispiel aus Eric Lipperts Antwort hier:

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 { ... }
}

Dieses Muster wird mit der Verwendung von Generika noch besser. In dieser Frage finden Sie zwei coole Beispiele. Also schreibe ich am Ende

Equality<Person>.CreateComparer(p => p.Id);

anstatt

new EqualityComparer<Person, int>(p => p.Id);

Auch kann ich eine generische Liste haben, Equality<Person>aber nichtEqualityComparer<Person, int>

var l = new List<Equality<Person>> 
        { 
         Equality<Person>.CreateComparer(p => p.Id),
         Equality<Person>.CreateComparer(p => p.Name) 
        }

wohingegen

var l = new List<EqualityComparer<Person, ??>>> 
        { 
         new EqualityComparer<Person, int>>(p => p.Id),
         new EqualityComparer<Person, string>>(p => p.Name) 
        }

Ist nicht möglich. Dies ist der Vorteil einer verschachtelten Klasse, die von einer übergeordneten Klasse erbt.

Ein anderer Fall (der gleichen Art - Implementierung ausblenden) besteht darin, dass Sie die Mitglieder einer Klasse (Felder, Eigenschaften usw.) nur für eine einzelne Klasse zugänglich machen möchten:

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
quelle
1

Eine andere Verwendung, die für verschachtelte Klassen noch nicht erwähnt wurde, ist die Trennung von generischen Typen. Angenommen, man möchte einige generische Familien statischer Klassen haben, die Methoden mit einer unterschiedlichen Anzahl von Parametern sowie Werte für einige dieser Parameter verwenden und Delegaten mit weniger Parametern generieren können. Zum Beispiel wünscht man sich eine statische Methode, die ein nehmen Action<string, int, double>und ein ergeben kann, String<string, int>das die bereitgestellte Aktion, die 3.5 übergibt, als das aufruft double; Man kann sich auch eine statische Methode wünschen, die ein an nehmen Action<string, int, double>und ein ergeben Action<string>kann und 7als das intund 5.3als das übergeht double. Mit generischen verschachtelten Klassen kann man festlegen, dass die Methodenaufrufe wie folgt aussehen:

MakeDelegate<string,int>.WithParams<double>(theDelegate, 3.5);
MakeDelegate<string>.WithParams<int,double>(theDelegate, 7, 5.3);

oder weil die letzteren Typen in jedem Ausdruck abgeleitet werden können, obwohl die ersteren nicht:

MakeDelegate<string,int>.WithParams(theDelegate, 3.5);
MakeDelegate<string>.WithParams(theDelegate, 7, 5.3);

Durch die Verwendung der verschachtelten generischen Typen kann festgestellt werden, welche Delegaten auf welche Teile der Gesamttypbeschreibung anwendbar sind.

Superkatze
quelle
1

Die verschachtelten Klassen können für folgende Anforderungen verwendet werden:

  1. Klassifizierung der Daten
  2. Wenn die Logik der Hauptklasse kompliziert ist und Sie das Gefühl haben, untergeordnete Objekte zum Verwalten der Klasse zu benötigen
  3. Wenn Sie, dass der Zustand und die Existenz der Klasse vollständig von der einschließenden Klasse abhängt
Rachin Goyal
quelle
0

Wie nawfal die Implementierung des Abstract Factory-Musters erwähnte, kann dieser Code erweitert werden, um ein Klassencluster- Muster zu erzielen , das auf dem Abstract Factory-Muster basiert.

diimdeep
quelle
0

Ich mag es, Ausnahmen zu verschachteln, die nur für eine einzelne Klasse gelten, d. H. diejenigen, die niemals von einem anderen Ort geworfen werden.

Beispielsweise:

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

Auf diese Weise bleiben Ihre Projektdateien aufgeräumt und nicht voll mit hundert kleinen Ausnahmeklassen.

Eric Dand
quelle
0

Denken Sie daran, dass Sie die verschachtelte Klasse testen müssen. Wenn es privat ist, können Sie es nicht isoliert testen.

Sie können es jedoch in Verbindung mit dem InternalsVisibleToAttribut intern machen . Dies wäre jedoch dasselbe, als würde ein privates Feld nur zu Testzwecken intern gemacht, was ich als schlechte Selbstdokumentation betrachte.

Daher möchten Sie möglicherweise nur private verschachtelte Klassen mit geringer Komplexität implementieren.

tm1
quelle
0

ja für diesen Fall:

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
quelle
Warum? Fügen Sie dem Code in Ihrer Lösung einen Kontext hinzu, damit zukünftige Leser die Gründe für Ihre Antwort verstehen.
Grant Miller
0

Basierend auf meinem Verständnis dieses Konzepts könnten wir diese Funktion verwenden, wenn Klassen konzeptionell miteinander in Beziehung stehen. Ich meine, einige von ihnen sind ein Element in unserem Geschäft, wie Entitäten, die in der DDD-Welt existieren und die einem aggregierten Stammobjekt helfen, seine Geschäftslogik zu vervollständigen.

Zur Verdeutlichung werde ich dies anhand eines Beispiels zeigen:

Stellen Sie sich vor, wir haben zwei Klassen wie Order und OrderItem. In der Auftragsklasse werden wir alle orderItems verwalten und in OrderItem speichern wir Daten zu einer einzelnen Bestellung zur Verdeutlichung. Sie können die folgenden Klassen sehen:

 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
quelle