Wann sollte ich eine Struktur anstelle einer Klasse verwenden?

302

MSDN sagt, dass Sie Strukturen verwenden sollten, wenn Sie leichte Objekte benötigen. Gibt es andere Szenarien, in denen eine Struktur einer Klasse vorzuziehen ist?

Einige Leute haben das vielleicht vergessen:

  1. Strukturen können Methoden haben.
  2. Strukturen können nicht vererbt werden.

Ich verstehe die technischen Unterschiede zwischen Strukturen und Klassen, ich habe nur kein gutes Gefühl dafür, wann ich eine Struktur verwenden soll.

Esteban Araya
quelle
Nur eine Erinnerung - was die meisten Leute in diesem Zusammenhang vergessen, ist, dass in C # -Strukturen auch Methoden vorhanden sein können.
petr k.

Antworten:

295

MSDN hat die Antwort: Auswahl zwischen Klassen und Strukturen .

Grundsätzlich enthält diese Seite eine Checkliste mit 4 Elementen und die Verwendung einer Klasse, sofern Ihr Typ nicht alle Kriterien erfüllt.

Definieren Sie keine Struktur, es sei denn, der Typ weist alle folgenden Merkmale auf:

  • Es stellt logischerweise einen einzelnen Wert dar, ähnlich wie primitive Typen (Ganzzahl, Doppel usw.).
  • Die Instanzgröße ist kleiner als 16 Byte.
  • Es ist unveränderlich.
  • Es muss nicht häufig verpackt werden.
OwenP
quelle
1
Vielleicht fehlt mir etwas Offensichtliches, aber ich verstehe die Gründe für den "unveränderlichen" Teil nicht ganz. Warum ist das notwendig? Könnte es jemand erklären?
Tamas Czinege
3
Sie haben dies wahrscheinlich empfohlen, denn wenn die Struktur unveränderlich ist, spielt es keine Rolle, dass sie eher eine Wertesemantik als eine Referenzsemantik aufweist. Die Unterscheidung ist nur wichtig, wenn Sie das Objekt / die Struktur nach dem Erstellen einer Kopie mutieren.
Stephen C. Steel
@DrJokepu: In einigen Situationen erstellt das System eine temporäre Kopie einer Struktur und lässt dann zu, dass diese Kopie unter Bezugnahme auf Code übergeben wird, der sie ändert. Da die temporäre Kopie verworfen wird, geht die Änderung verloren. Dieses Problem ist besonders schwerwiegend, wenn eine Struktur über Methoden verfügt, die sie mutieren. Ich bin nachdrücklich anderer Meinung als die Vorstellung, dass Veränderlichkeit ein Grund ist, etwas zu einer Klasse zu machen, da veränderbare Strukturen trotz einiger Mängel in c # und vb.net eine nützliche Semantik bieten, die auf keine andere Weise erreicht werden kann; Es gibt keinen semantischen Grund, eine unveränderliche Struktur einer Klasse vorzuziehen.
Supercat
4
@Chuu: Beim Entwerfen des JIT-Compilers hat Microsoft beschlossen, den Code für das Kopieren von Strukturen zu optimieren, die 16 Byte oder kleiner sind. Dies bedeutet, dass das Kopieren einer 17-Byte-Struktur erheblich langsamer sein kann als das Kopieren einer 16-Byte-Struktur. Ich sehe keinen besonderen Grund zu der Annahme, dass Microsoft solche Optimierungen auf größere Strukturen ausdehnt, aber es ist wichtig zu beachten, dass 17-Byte-Strukturen zwar langsamer kopiert werden können als 16-Byte-Strukturen, es jedoch viele Fälle gibt, in denen große Strukturen effizienter sind als Objekte großer Klassen, bei denen der relative Vorteil von Strukturen mit der Größe der Struktur wächst .
Supercat
3
@Chuu: Das Anwenden der gleichen Verwendungsmuster auf große Strukturen wie auf Klassen führt zu ineffizientem Code. Die richtige Lösung besteht jedoch häufig darin, die Strukturen nicht durch Klassen zu ersetzen, sondern Strukturen effizienter zu verwenden. Vor allem sollte man vermeiden, Strukturen nach Wert zu übergeben oder zurückzugeben. Übergeben Sie sie als refParameter, wann immer dies sinnvoll ist. Übergeben Sie eine Struktur mit 4.000 Feldern als ref-Parameter an eine Methode, die eines ändert. Dies ist billiger als das Übergeben einer Struktur mit 4 Feldern nach Wert an eine Methode, die eine geänderte Version zurückgibt.
Supercat
53

Ich bin überrascht, dass ich bei keiner der vorherigen Antworten dies gelesen habe, was ich für den wichtigsten Aspekt halte:

Ich verwende Strukturen, wenn ich einen Typ ohne Identität möchte. Zum Beispiel ein 3D-Punkt:

public struct ThreeDimensionalPoint
{
    public readonly int X, Y, Z;
    public ThreeDimensionalPoint(int x, int y, int z)
    {
        this.X = x;
        this.Y = y;
        this.Z = z;
    }

    public override string ToString()
    {
        return "(X=" + this.X + ", Y=" + this.Y + ", Z=" + this.Z + ")";
    }

    public override int GetHashCode()
    {
        return (this.X + 2) ^ (this.Y + 2) ^ (this.Z + 2);
    }

    public override bool Equals(object obj)
    {
        if (!(obj is ThreeDimensionalPoint))
            return false;
        ThreeDimensionalPoint other = (ThreeDimensionalPoint)obj;
        return this == other;
    }

    public static bool operator ==(ThreeDimensionalPoint p1, ThreeDimensionalPoint p2)
    {
        return p1.X == p2.X && p1.Y == p2.Y && p1.Z == p2.Z;
    }

    public static bool operator !=(ThreeDimensionalPoint p1, ThreeDimensionalPoint p2)
    {
        return !(p1 == p2);
    }
}

Wenn Sie zwei Instanzen dieser Struktur haben, ist es Ihnen egal, ob es sich um ein oder zwei einzelne Daten im Speicher handelt. Sie kümmern sich nur um die Werte, die sie enthalten.

Andrei Rînea
quelle
4
Etwas abseits des Themas, aber warum löst man eine ArgumentException aus, wenn obj nicht ThreeDimensionalPoint ist? Sollten Sie in diesem Fall nicht einfach false zurückgeben?
Svish
4
Das ist richtig, ich war übermäßig eifrig. return falseist das, was dort hätte sein sollen und jetzt korrigiert.
Andrei Rînea
Ein interessanter Grund, eine Struktur zu verwenden. Ich habe Klassen mit GetHashCode und Equals erstellt, die ähnlich wie hier definiert sind, aber dann musste ich immer darauf achten, diese Instanzen nicht zu mutieren, wenn ich sie als Wörterbuchschlüssel verwendete. Wahrscheinlich wäre es sicherer gewesen, wenn ich sie als Strukturen definiert hätte. (Da dann der Schlüssel eine Kopie der Felder in dem Moment wäre, in dem die Struktur ein Wörterbuchschlüssel wurde , würde der Schlüssel unverändert bleiben, wenn ich später das Original ändere.)
ToolmakerSteve
In Ihrem Beispiel ist dies in Ordnung, da Sie nur 12 Bytes haben. Beachten Sie jedoch, dass Sie bei Verwendung vieler Felder in dieser Struktur, die 16 Bytes überschreiten, die Verwendung einer Klasse in Betracht ziehen und die Methoden GetHashCode und Equals überschreiben müssen.
Daniel Botero Correa
Ein Werttyp in DDD bedeutet nicht, dass Sie unbedingt einen Werttyp in C #
Darragh
26

Bill Wagner hat ein Kapitel darüber in seinem Buch "effektiv c #" ( http://www.amazon.com/Effective-Specific-Ways-Improve-Your/dp/0321245660 ). Er schließt mit dem folgenden Prinzip:

  1. Ist die Hauptverantwortung des Typdatenspeichers?
  2. Wird die öffentliche Schnittstelle vollständig durch Eigenschaften definiert, die auf die Datenelemente zugreifen oder diese ändern?
  3. Sind Sie sicher, dass Ihr Typ niemals Unterklassen haben wird?
  4. Sind Sie sicher, dass Ihr Typ niemals polymorph behandelt wird?

Wenn Sie alle 4 Fragen mit "Ja" beantworten: Verwenden Sie eine Struktur. Verwenden Sie andernfalls eine Klasse.

Bart Gijssens
quelle
1
Also ... sollten Datenübertragungsobjekte (DTOs) Strukturen sein?
Kreuzer
1
Ich würde ja sagen, wenn es die oben beschriebenen 4 Kriterien erfüllt. Warum muss ein Datenübertragungsobjekt auf eine bestimmte Weise behandelt werden?
Bart Gijssens
2
@cruizer hängt von Ihrer Situation ab. Bei einem Projekt hatten wir gemeinsame Prüfungsfelder in unseren DTOs und haben daher ein Basis-DTO geschrieben, von dem andere geerbt haben.
Andrew Grothe
1
Alle außer (2) scheinen ausgezeichnete Prinzipien zu sein. Müsste seine Argumentation sehen, um zu wissen, was er genau mit (2) meint und warum.
ToolmakerSteve
1
@ToolmakerSteve: Dafür musst du das Buch lesen. Denken Sie nicht, dass es fair ist, große Teile eines Buches zu kopieren / einzufügen.
Bart Gijssens
12

Ich würde Strukturen verwenden, wenn:

  1. Ein Objekt soll schreibgeschützt sein (jedes Mal, wenn Sie eine Struktur übergeben / zuweisen, wird sie kopiert). Nur-Lese-Objekte eignen sich hervorragend für die Multithread-Verarbeitung, da sie in den meisten Fällen nicht gesperrt werden müssen.

  2. Ein Objekt ist klein und kurzlebig. In einem solchen Fall besteht eine gute Chance, dass das Objekt auf dem Stapel zugewiesen wird, was viel effizienter ist als das Ablegen auf dem verwalteten Heap. Darüber hinaus wird der vom Objekt zugewiesene Speicher freigegeben, sobald er außerhalb seines Gültigkeitsbereichs liegt. Mit anderen Worten, es ist weniger Arbeit für Garbage Collector und der Speicher wird effizienter genutzt.

Pawel Pabich
quelle
10

Verwenden Sie eine Klasse, wenn:

  • Ihre Identität ist wichtig. Strukturen werden implizit kopiert, wenn sie als Wert an eine Methode übergeben werden.
  • Es wird einen großen Speicherbedarf haben.
  • Seine Felder benötigen Initialisierer.
  • Sie müssen von einer Basisklasse erben.
  • Sie benötigen polymorphes Verhalten;

Verwenden Sie eine Struktur, wenn:

  • Es verhält sich wie ein primitiver Typ (int, long, byte usw.).
  • Es muss einen kleinen Speicherbedarf haben.
  • Sie rufen eine P / Invoke-Methode auf, für die eine Struktur als Wert übergeben werden muss.
  • Sie müssen die Auswirkungen der Speicherbereinigung auf die Anwendungsleistung reduzieren.
  • Die Felder müssen nur mit ihren Standardwerten initialisiert werden. Dieser Wert wäre Null für numerische Typen, falsch für Boolesche Typen und null für Referenztypen.
    • Beachten Sie, dass Strukturen in C # 6.0 einen Standardkonstruktor haben können, mit dem die Felder der Struktur mit nicht standardmäßigen Werten initialisiert werden können.
  • Sie müssen nicht von einer Basisklasse erben (außer ValueType, von dem alle Strukturen erben).
  • Sie benötigen kein polymorphes Verhalten.
Yashwanth Chowdary Kata
quelle
5

Ich habe immer eine Struktur verwendet, wenn ich einige Werte gruppieren wollte, um Dinge von einem Methodenaufruf zurückzugeben, aber ich muss sie für nichts verwenden, nachdem ich diese Werte gelesen habe. Nur um die Dinge sauber zu halten. Ich neige dazu, Dinge in einer Struktur als "wegwerfbar" und Dinge in einer Klasse als nützlicher und "funktionaler" anzusehen.

Ryan Skarin
quelle
4

Wenn eine Entität unveränderlich sein soll, ist die Frage, ob eine Struktur oder eine Klasse verwendet werden soll, im Allgemeinen eher eine Frage der Leistung als der Semantik. Auf einem 32/64-Bit-System müssen für Klassenreferenzen 4/8 Byte gespeichert werden, unabhängig von der Informationsmenge in der Klasse. Das Kopieren einer Klassenreferenz erfordert das Kopieren von 4/8 Bytes. Auf der anderen Seite jeder andersDie Klasseninstanz hat zusätzlich zu den darin enthaltenen Informationen und den Speicherkosten der Verweise darauf 8/16 Byte Overhead. Angenommen, man möchte ein Array von 500 Entitäten, die jeweils vier 32-Bit-Ganzzahlen enthalten. Wenn es sich bei der Entität um einen Strukturtyp handelt, benötigt das Array 8.000 Byte, unabhängig davon, ob alle 500 Entitäten identisch, alle unterschiedlich oder irgendwo dazwischen sind. Wenn die Entität ein Klassentyp ist, benötigt das Array mit 500 Referenzen 4.000 Byte. Wenn diese Verweise alle auf unterschiedliche Objekte verweisen, benötigen die Objekte jeweils zusätzliche 24 Byte (12.000 Byte für alle 500), insgesamt 16.000 Byte - das Doppelte der Speicherkosten eines Strukturtyps. Wenn der Code hingegen eine Objektinstanz erstellt und dann einen Verweis auf alle 500 Array-Slots kopiert, betragen die Gesamtkosten für diese Instanz 24 Byte und 4, 000 für das Array - insgesamt 4.024 Bytes. Eine große Einsparung. Nur wenige Situationen würden so gut funktionieren wie die letzte, aber in einigen Fällen kann es möglich sein, einige Verweise auf genügend Array-Slots zu kopieren, damit sich eine solche Freigabe lohnt.

Wenn die Entität veränderbar sein soll, ist die Frage, ob eine Klasse oder eine Struktur verwendet werden soll, in gewisser Weise einfacher. Angenommen, "Thing" ist entweder eine Struktur oder eine Klasse mit einem ganzzahligen Feld namens x, und man führt den folgenden Code aus:

  Sache t1, t2;
  ...
  t2 = t1;
  t2.x = 5;

Möchte man, dass die letztere Aussage t1.x beeinflusst?

Wenn Thing ein Klassentyp ist, sind t1 und t2 äquivalent, was bedeutet, dass t1.x und t2.x ebenfalls äquivalent sind. Somit wirkt sich die zweite Anweisung auf t1.x aus. Wenn Thing ein Strukturtyp ist, sind t1 und t2 unterschiedliche Instanzen, was bedeutet, dass sich t1.x und t2.x auf unterschiedliche Ganzzahlen beziehen. Somit wirkt sich die zweite Anweisung nicht auf t1.x aus.

Veränderbare Strukturen und veränderbare Klassen verhalten sich grundlegend unterschiedlich, obwohl .net einige Besonderheiten im Umgang mit Strukturmutationen aufweist. Wenn Sie ein Verhalten vom Werttyp wünschen (was bedeutet, dass "t2 = t1" die Daten von t1 nach t2 kopiert, während t1 und t2 als unterschiedliche Instanzen belassen werden), und wenn Sie mit den Macken bei der Behandlung von Werttypen durch .net leben können, verwenden Sie eine Struktur. Wenn Sie eine Semantik vom Werttyp wünschen, die Macken von .net jedoch zu einer fehlerhaften Semantik vom Werttyp in Ihrer Anwendung führen würden, verwenden Sie eine Klasse und murmeln Sie.

Superkatze
quelle
3

Zusätzlich die hervorragenden Antworten oben:

Strukturen sind Werttypen.

Sie können niemals auf Nichts gesetzt werden .

Wenn Sie eine Struktur = Nothing festlegen, werden alle Wertetypen auf ihre Standardwerte gesetzt.

George Filippakos
quelle
2

wenn Sie nicht wirklich Verhalten brauchen, aber mehr Struktur als ein einfaches Array oder Wörterbuch benötigen.

Follow-up So denke ich über Strukturen im Allgemeinen. Ich weiß, dass sie Methoden haben können, aber ich mag es, diese allgemeine mentale Unterscheidung beizubehalten.

Jim Deville
quelle
Warum sagst du das? Strukturen können Methoden haben.
Esteban Araya
2

Wie @Simon sagte, bieten Strukturen eine Semantik vom Typ "Wert". Wenn Sie also ein ähnliches Verhalten wie ein integrierter Datentyp benötigen, verwenden Sie eine Struktur. Da Strukturen als Kopie übergeben werden, sollten Sie sicherstellen, dass sie klein sind (ca. 16 Byte).

Scott Dorman
quelle
2

Es ist ein altes Thema, wollte aber einen einfachen Benchmark-Test liefern.

Ich habe zwei CS-Dateien erstellt:

public class TestClass
{
    public long ID { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

und

public struct TestStruct
{
    public long ID { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

Benchmark ausführen:

  • Erstellen Sie 1 Testklasse
  • Erstellen Sie 1 TestStruct
  • Erstellen Sie 100 Testklassen
  • Erstellen Sie 100 TestStruct
  • Erstellen Sie 10000 TestClass
  • Erstellen Sie 10000 TestStruct

Ergebnisse:

BenchmarkDotNet=v0.12.0, OS=Windows 10.0.18362
Intel Core i5-8250U CPU 1.60GHz (Kaby Lake R), 1 CPU, 8 logical and 4 physical cores
.NET Core SDK=3.1.101
[Host]     : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT  [AttachedDebugger]
DefaultJob : .NET Core 3.1.1 (CoreCLR 4.700.19.60701, CoreFX 4.700.19.60801), X64 RyuJIT


|         Method |           Mean |         Error |        StdDev |     Ratio | RatioSD | Rank |    Gen 0 | Gen 1 | Gen 2 | Allocated |
|--------------- |---------------:|--------------:|--------------:|----------:|--------:|-----:|---------:|------:|------:|----------:|

|      UseStruct |      0.0000 ns |     0.0000 ns |     0.0000 ns |     0.000 |    0.00 |    1 |        - |     - |     - |         - |
|       UseClass |      8.1425 ns |     0.1873 ns |     0.1839 ns |     1.000 |    0.00 |    2 |   0.0127 |     - |     - |      40 B |
|   Use100Struct |     36.9359 ns |     0.4026 ns |     0.3569 ns |     4.548 |    0.12 |    3 |        - |     - |     - |         - |
|    Use100Class |    759.3495 ns |    14.8029 ns |    17.0471 ns |    93.144 |    3.24 |    4 |   1.2751 |     - |     - |    4000 B |
| Use10000Struct |  3,002.1976 ns |    25.4853 ns |    22.5920 ns |   369.664 |    8.91 |    5 |        - |     - |     - |         - |
|  Use10000Class | 76,529.2751 ns | 1,570.9425 ns | 2,667.5795 ns | 9,440.182 |  346.76 |    6 | 127.4414 |     - |     - |  400000 B |
Eduardas Šlutas
quelle
1

Hmm ...

Ich würde die Garbage Collection nicht als Argument für / gegen die Verwendung von Strukturen gegen Klassen verwenden. Der verwaltete Heap funktioniert ähnlich wie ein Stapel. Wenn Sie ein Objekt erstellen, wird es einfach oben auf dem Heap platziert, was fast so schnell ist wie das Zuweisen auf dem Stapel. Wenn ein Objekt nur von kurzer Dauer ist und einen GC-Zyklus nicht überlebt, ist die Freigabe kostenlos, da der GC nur mit Speicher arbeitet, auf den noch zugegriffen werden kann. (Durchsuchen Sie MSDN, es gibt eine Reihe von Artikeln zur .NET-Speicherverwaltung. Ich bin einfach zu faul, um nach ihnen zu suchen.)

Die meiste Zeit, in der ich eine Struktur benutze, mache ich mir dafür einen Kick, weil ich später herausfinde, dass eine Referenzsemantik die Dinge ein bisschen einfacher gemacht hätte.

Wie auch immer, diese vier Punkte im oben veröffentlichten MSDN-Artikel scheinen eine gute Richtlinie zu sein.


quelle
1
Wenn Sie manchmal eine Referenzsemantik mit einer Struktur benötigen, deklarieren Sie einfach class MutableHolder<T> { public T Value; MutableHolder(T value) {Value = value;} }, und dann ist a MutableHolder<T>ein Objekt mit veränderlicher Klassensemantik (dies funktioniert genauso gut, wenn Tes sich um eine Struktur oder einen unveränderlichen Klassentyp handelt).
Supercat
1

Strukturen befinden sich auf dem Stapel und nicht auf dem Heap. Daher sind sie threadsicher und sollten bei der Implementierung des Übertragungsobjektmusters verwendet werden. Sie möchten niemals Objekte auf dem Heap verwenden. Sie sind flüchtig. In diesem Fall möchten Sie den Aufrufstapel verwenden. Dies ist ein grundlegender Fall für die Verwendung einer Struktur. Ich bin überrascht, wie viele Antworten hier vorliegen.

Jack
quelle
-3

Ich denke, die beste Antwort ist einfach, struct zu verwenden, wenn Sie eine Sammlung von Eigenschaften benötigen, class, wenn es sich um eine Sammlung von Eigenschaften UND Verhaltensweisen handelt.

Lucian Gabriel Popescu
quelle
Strukturen können auch Methoden haben
Nithin Chandran
Natürlich, aber wenn Sie Methoden benötigen, liegt die Wahrscheinlichkeit bei 99%, dass Sie struct anstelle von class falsch verwenden. Die einzigen Ausnahmen, die ich gefunden habe, wenn es in Ordnung ist, Methoden in struct zu haben, sind Rückrufe
Lucian Gabriel Popescu