Ist es für Strukturen sicher, Schnittstellen zu implementieren?


88

Ich erinnere mich an etwas darüber, wie schlecht es für Strukturen ist, Schnittstellen in CLR über C # zu implementieren, aber ich kann anscheinend nichts darüber finden. Ist es schlimm? Gibt es unbeabsichtigte Konsequenzen?

public interface Foo { Bar GetBar(); }
public struct Fubar : Foo { public Bar GetBar() { return new Bar(); } }

Antworten:


45

In dieser Frage sind mehrere Dinge im Gange ...

Es ist möglich, dass eine Struktur eine Schnittstelle implementiert, es gibt jedoch Bedenken hinsichtlich Casting, Veränderbarkeit und Leistung. Weitere Informationen finden Sie in diesem Beitrag: https://docs.microsoft.com/en-us/archive/blogs/abhinaba/c-structs-and-interface

Im Allgemeinen sollten Strukturen für Objekte mit Werttypsemantik verwendet werden. Durch die Implementierung einer Schnittstelle in einer Struktur können Boxprobleme auftreten, wenn die Struktur zwischen der Struktur und der Schnittstelle hin und her geworfen wird. Infolge des Boxens verhalten sich Operationen, die den internen Status der Struktur ändern, möglicherweise nicht richtig.


3
"Aufgrund des Boxens verhalten sich Vorgänge, die den internen Status der Struktur ändern, möglicherweise nicht richtig." Geben Sie ein Beispiel und erhalten Sie die Antwort.

2
@ Will: Nicht sicher, worauf Sie sich in Ihrem Kommentar beziehen. Der Blog-Beitrag, auf den ich verwiesen habe, enthält ein Beispiel, das zeigt, wo das Aufrufen einer Schnittstellenmethode für die Struktur den internen Wert nicht ändert.
Scott Dorman

11
@ScottDorman: In einigen Fällen kann die Implementierung von Schnittstellen durch Strukturen dazu beitragen , das Boxen zu vermeiden . Paradebeispiele sind IComparable<T>und IEquatable<T>. Das Speichern einer Struktur Fooin einer Variablen vom Typ IComparable<Foo>würde Boxing erfordern. Wenn jedoch ein generischer Typ Tauf IComparable<T>einen beschränkt ist, kann er mit einem anderen verglichen werden, Tohne dass einer der beiden Boxen erforderlich ist und ohne dass Sie etwas Tanderes wissen müssen, als dass er die Einschränkung implementiert. Ein solches vorteilhaftes Verhalten wird nur durch die Fähigkeit von Strukturen ermöglicht, Schnittstellen zu implementieren. Das wurde gesagt ...
Supercat

3
... wäre es vielleicht schön gewesen, wenn es ein Mittel gegeben hätte, zu erklären, dass eine bestimmte Schnittstelle nur für Strukturen ohne Box als anwendbar angesehen werden sollte, da es einige Kontexte gibt, in denen es nicht möglich wäre, dass ein Klassenobjekt oder eine Box-Struktur die gewünschte Struktur hat Verhaltensweisen.
Supercat

2
"Strukturen sollten für Objekte mit Wertesemantik verwendet werden. ... Operationen, die den internen Status der Struktur ändern, verhalten sich möglicherweise nicht richtig." Ist das eigentliche Problem dort nicht die Tatsache, dass Wertesemantik und Mutatabilität nicht gut zusammenpassen?
jpmc26

178

Da sonst niemand diese Antwort ausdrücklich gegeben hat, werde ich Folgendes hinzufügen:

Das Implementieren einer Schnittstelle in einer Struktur hat keinerlei negative Konsequenzen.

Jede Variable des Schnittstellentyps, die zum Halten einer Struktur verwendet wird, führt dazu, dass ein Boxwert dieser Struktur verwendet wird. Wenn die Struktur unveränderlich ist (eine gute Sache), ist dies im schlimmsten Fall ein Leistungsproblem, es sei denn, Sie sind:

  • Verwenden des resultierenden Objekts zum Sperren (auf jeden Fall eine immens schlechte Idee)
  • Verwenden der Referenzgleichheitssemantik und Erwarten, dass sie für zwei Boxwerte aus derselben Struktur funktioniert.

Beides ist unwahrscheinlich, stattdessen führen Sie wahrscheinlich eine der folgenden Aktionen aus:

Generika

Möglicherweise gibt es viele vernünftige Gründe für Strukturen, die Schnittstellen implementieren, damit sie in einem generischen Kontext mit Einschränkungen verwendet werden können . Bei Verwendung auf diese Weise kann die Variable wie folgt aussehen:

class Foo<T> : IEquatable<Foo<T>> where T : IEquatable<T>
{
    private readonly T a;

    public bool Equals(Foo<T> other)
    {
         return this.a.Equals(other.a);
    }
}
  1. Aktivieren Sie die Verwendung der Struktur als Typparameter
    • solange keine andere Einschränkung wie new()oder classverwendet wird.
  2. Ermöglichen Sie das Vermeiden des Boxens bei Strukturen, die auf diese Weise verwendet werden.

Dann ist this.a KEINE Schnittstellenreferenz, daher verursacht es keine Box mit dem, was darin platziert ist. Wenn der c # -Compiler die generischen Klassen kompiliert und Aufrufe der Instanzmethoden einfügen muss, die für Instanzen des Typparameters T definiert sind, kann er den eingeschränkten Opcode verwenden:

Wenn thisType ein Werttyp ist und thisType eine Methode implementiert, wird ptr unverändert als 'this'-Zeiger auf eine Aufrufmethodenanweisung für die Implementierung der Methode durch thisType übergeben.

Dies vermeidet das Boxen und da der Werttyp implementiert wird, muss die Schnittstelle die Methode implementieren, so dass kein Boxen auftritt. Im obigen Beispiel erfolgt der Equals()Aufruf ohne Kästchen. A 1 .

Reibungsarme APIs

Die meisten Strukturen sollten eine primitive Semantik haben, bei der bitweise identische Werte als gleich 2 betrachtet werden . Die Laufzeit liefert ein solches Verhalten implizit Equals(), dies kann jedoch langsam sein. Auch diese implizite Gleichheit wird nicht als Implementierung von IEquatable<T>offengelegt und verhindert somit, dass Strukturen leicht als Schlüssel für Wörterbücher verwendet werden können, es sei denn, sie implementieren sie explizit selbst. Es ist daher üblich, dass viele öffentliche Strukturtypen deklarieren, dass sie implementiert werden IEquatable<T>(wo Tsind sie selbst), um dies einfacher und leistungsfähiger zu machen und mit dem Verhalten vieler vorhandener Werttypen innerhalb der CLR-BCL übereinzustimmen.

Alle Grundelemente in der BCL implementieren mindestens:

  • IComparable
  • IConvertible
  • IComparable<T>
  • IEquatable<T>(Und so IEquatable)

Viele implementieren auch IFormattable, viele der vom System definierten Werttypen wie DateTime, TimeSpan und Guid implementieren auch viele oder alle. Wenn Sie einen ähnlich "weit verbreiteten" Typ wie eine komplexe Zahlenstruktur oder einige Textwerte mit fester Breite implementieren, wird Ihre Struktur durch die korrekte Implementierung vieler dieser allgemeinen Schnittstellen nützlicher und benutzerfreundlicher.

Ausschlüsse

Wenn die Schnittstelle eine starke Veränderbarkeit (wie z. B. ICollection) impliziert, ist die Implementierung offensichtlich eine schlechte Idee, da dies bedeuten würde, dass Sie entweder die Struktur veränderbar gemacht haben (was zu den bereits beschriebenen Fehlern führt, bei denen die Änderungen am Boxed-Wert und nicht am Original auftreten ) oder Sie verwirren Benutzer, indem Sie die Auswirkungen der Methoden wie Add()oder Ausnahmen ignorieren .

Viele Schnittstellen implizieren KEINE Veränderlichkeit (wie z. B. IFormattable) und dienen als idiomatische Methode, um bestimmte Funktionen auf konsistente Weise verfügbar zu machen. Oft kümmert sich der Benutzer der Struktur nicht um den Boxaufwand für ein solches Verhalten.

Zusammenfassung

Wenn dies bei unveränderlichen Werttypen sinnvoll durchgeführt wird, ist die Implementierung nützlicher Schnittstellen eine gute Idee


Anmerkungen:

1: Beachten Sie, dass der Compiler dies möglicherweise verwendet, wenn er virtuelle Methoden für Variablen aufruft, von denen bekannt ist , dass sie von einem bestimmten Strukturtyp sind, in denen jedoch eine virtuelle Methode aufgerufen werden muss. Beispielsweise:

List<int> l = new List<int>();
foreach(var x in l)
    ;//no-op

Der von der Liste zurückgegebene Enumerator ist eine Struktur, eine Optimierung, um eine Zuordnung bei der Aufzählung der Liste zu vermeiden (mit einigen interessanten Konsequenzen ). Doch die Semantik von foreach , die angeben , ob die enumerator Geräte IDisposabledann Dispose()wird einmal aufgerufen werden , die Iteration abgeschlossen ist. Wenn dies offensichtlich durch einen Boxed Call geschieht, würde jeder Vorteil des Enumerators als Struktur beseitigt (tatsächlich wäre es schlimmer). Schlimmer noch, wenn dispose call den Status des Enumerators auf irgendeine Weise ändert, würde dies auf der Boxed-Instanz passieren und in komplexen Fällen können viele subtile Fehler auftreten. Daher ist die in dieser Art von Situation ausgestrahlte IL:

IL_0001: newobj System.Collections.Generic.List..ctor
IL_0006: stloc.0     
IL_0007: nein         
IL_0008: ldloc.0     
IL_0009: callvirt System.Collections.Generic.List.GetEnumerator
IL_000E: stloc.2     
IL_000F: br.s IL_0019
IL_0011: ldloca.s 02 
IL_0013: Rufen Sie System.Collections.Generic.List.get_Current auf
IL_0018: stloc.1     
IL_0019: ldloca.s 02 
IL_001B: Rufen Sie System.Collections.Generic.List.MoveNext auf
IL_0020: stloc.3     
IL_0021: ldloc.3     
IL_0022: brtrue.s IL_0011
IL_0024: Leave.s IL_0035
IL_0026: ldloca.s 02 
IL_0028: eingeschränkt. System.Collections.Generic.List.Enumerator
IL_002E: callvirt System.IDisposable.Dispose
IL_0033: Nein         
IL_0034: Endgültig  

Somit verursacht die Implementierung von IDisposable keine Leistungsprobleme und der (bedauerliche) veränderbare Aspekt des Enumerators bleibt erhalten, falls die Dispose-Methode tatsächlich etwas tut!

2: double und float sind Ausnahmen von dieser Regel, bei denen NaN-Werte nicht als gleich angesehen werden.


1
Die Website egheadcafe.com ist umgezogen, hat aber bei der Beibehaltung des Inhalts keine gute Arbeit geleistet. Ich habe es versucht, kann aber das Originaldokument von eggheadcafe.com/software/aspnet/31702392/… nicht finden, da mir die Kenntnisse des OP fehlen. (PS +1 für eine hervorragende Zusammenfassung).
Abel

2
Dies ist eine großartige Antwort, aber ich denke, Sie können sie verbessern, indem Sie die "Zusammenfassung" als "TL; DR" nach oben verschieben. Wenn Sie zuerst die Schlussfolgerung ziehen, weiß der Leser, wohin Sie mit den Dingen gehen.
Hans

Beim Umwandeln von a structin a sollte eine Compiler-Warnung angezeigt werden interface.
Jalal

8

In einigen Fällen kann es für eine Struktur gut sein, eine Schnittstelle zu implementieren (wenn dies niemals nützlich gewesen wäre, wäre es zweifelhaft, ob die Ersteller von .net dies vorgesehen hätten). Wenn eine Struktur eine schreibgeschützte Schnittstelle wie implementiert, erfordert das IEquatable<T>Speichern der Struktur an einem Speicherort (Variable, Parameter, Array-Element usw.) vom Typ IEquatable<T>, dass sie eingerahmt ist (jeder Strukturtyp definiert tatsächlich zwei Arten von Dingen: einen Speicher Standorttyp, der sich wie ein Werttyp verhält, und ein Heap-Objekttyp, der sich wie ein Klassentyp verhält, der erste ist implizit in den zweiten konvertierbar - "Boxing" - und der zweite kann durch explizite Umwandlung in den ersten konvertiert werden. "Unboxing"). Es ist jedoch möglich, die Implementierung einer Schnittstelle durch eine Struktur ohne Boxen auszunutzen, indem sogenannte Constrained Generics verwendet werden.

Wenn man CompareTwoThings<T>(T thing1, T thing2) where T:IComparable<T>beispielsweise eine Methode hätte , könnte eine solche Methode aufrufen, thing1.Compare(thing2)ohne boxen zu müssen thing1oder thing2. Wenn thing1dies z. B. ein ist Int32, weiß die Laufzeit, wann sie den Code für generiert CompareTwoThings<Int32>(Int32 thing1, Int32 thing2). Da es den genauen Typ sowohl der Sache kennt, die die Methode hostet, als auch der Sache, die als Parameter übergeben wird, muss es keine von beiden boxen.

Das größte Problem bei Strukturen, die Schnittstellen implementieren, besteht darin, dass sich eine Struktur, die an einem Ort des Schnittstellentyps gespeichert wird Object, oder ValueType(im Gegensatz zu einem Ort ihres eigenen Typs) als Klassenobjekt verhält. Für schreibgeschützte Schnittstellen ist dies im Allgemeinen kein Problem, aber für eine mutierende Schnittstelle wie IEnumerator<T>diese kann es zu einer seltsamen Semantik kommen.

Betrachten Sie beispielsweise den folgenden Code:

List<String> myList = [list containing a bunch of strings]
var enumerator1 = myList.GetEnumerator();  // Struct of type List<String>.IEnumerator
enumerator1.MoveNext(); // 1
var enumerator2 = enumerator1;
enumerator2.MoveNext(); // 2
IEnumerator<string> enumerator3 = enumerator2;
enumerator3.MoveNext(); // 3
IEnumerator<string> enumerator4 = enumerator3;
enumerator4.MoveNext(); // 4

Die markierte Anweisung Nr. 1 wird enumerator1das erste Element lesen. Der Status dieses Enumerators wird in kopiert enumerator2. Die markierte Anweisung Nr. 2 erweitert diese Kopie, um das zweite Element zu lesen, hat jedoch keine Auswirkungen enumerator1. Der Status dieses zweiten Enumerators wird dann kopiert und enumerator3durch die markierte Anweisung Nr. 3 erweitert. Dann, da enumerator3und enumerator4beide Referenztypen sind, wird eine REFERENCE to enumerator3kopiert enumerator4, sodass die markierte Anweisung beide enumerator3 und effektiv weiterentwickelt enumerator4.

Einige Leute versuchen vorzutäuschen, dass Werttypen und Referenztypen beide Arten von sind Object, aber das ist nicht wirklich wahr. Realwerttypen können in konvertiert werden Object, sind jedoch keine Instanzen davon. Eine Instanz, List<String>.Enumeratordie an einem Speicherort dieses Typs gespeichert ist, ist ein Werttyp und verhält sich wie ein Werttyp. Wenn Sie es an einen Ort des Typs kopieren, IEnumerator<String>wird es in einen Referenztyp konvertiert und es verhält sich wie ein Referenztyp . Letzteres ist eine Art Object, Ersteres jedoch nicht.

Übrigens noch ein paar Anmerkungen: (1) Im Allgemeinen sollten bei veränderbaren Klassentypen die EqualsMethoden die Referenzgleichheit testen, aber es gibt keine vernünftige Möglichkeit für eine Box-Struktur, dies zu tun. (2) ist trotz seines Namens ValueTypeein Klassentyp, kein Werttyp; Alle von abgeleiteten Typen System.Enumsind Werttypen, ebenso wie alle Typen, von denen ValueTypemit Ausnahme von abgeleitet ist System.Enum, aber beide ValueTypeund System.EnumKlassentypen.


3

Strukturen werden als Werttypen implementiert und Klassen sind Referenztypen. Wenn Sie eine Variable vom Typ Foo haben und eine Instanz von Fubar darin speichern, wird sie in einen Referenztyp "verpackt", wodurch der Vorteil der Verwendung einer Struktur in erster Linie zunichte gemacht wird.

Der einzige Grund, warum ich sehe, dass eine Struktur anstelle einer Klasse verwendet wird, ist, dass es sich um einen Werttyp und nicht um einen Referenztyp handelt, die Struktur jedoch nicht von einer Klasse erben kann. Wenn die Struktur eine Schnittstelle erbt und Sie Schnittstellen weitergeben, verlieren Sie diesen Werttyp der Struktur. Könnte es auch einfach zu einer Klasse machen, wenn Sie Schnittstellen benötigen.


Funktioniert das auch für Grundelemente, die Schnittstellen implementieren?
aoetalks

3

(Nun, ich habe nichts Wichtiges hinzuzufügen, aber noch keine Bearbeitungsfähigkeiten, also geht es weiter.)
Perfekt sicher. Nichts Illegales beim Implementieren von Schnittstellen auf Strukturen. Sie sollten sich jedoch fragen, warum Sie dies tun möchten.

Wenn Sie jedoch einen Schnittstellenverweis auf eine Struktur erhalten, wird diese BOX . Also Leistungseinbußen und so weiter.

Das einzig gültige Szenario, an das ich momentan denken kann, ist in meinem Beitrag hier dargestellt . Wenn Sie den in einer Sammlung gespeicherten Status einer Struktur ändern möchten, müssen Sie dies über eine zusätzliche Schnittstelle tun, die in der Struktur verfügbar gemacht wird.


Wenn eine Int32an eine Methode übergeben wird, die einen generischen Typ akzeptiert T:IComparable<Int32>(der entweder ein generischer Typparameter der Methode oder die Klasse der Methode sein kann), kann diese Methode die CompareMethode für das übergebene Objekt verwenden, ohne sie einzuschließen.
Supercat


0

Eine Struktur, die eine Schnittstelle implementiert, hat keine Konsequenzen. Zum Beispiel implementieren die eingebauten Systemstrukturen Schnittstellen wie IComparableund IFormattable.


0

Es gibt kaum einen Grund für einen Werttyp, eine Schnittstelle zu implementieren. Da Sie einen Werttyp nicht unterordnen können, können Sie ihn immer als konkreten Typ bezeichnen.

Wenn Sie natürlich nicht mehrere Strukturen haben, die alle dieselbe Schnittstelle implementieren, ist dies möglicherweise nur geringfügig nützlich, aber an diesem Punkt würde ich empfehlen, eine Klasse zu verwenden und es richtig zu machen.

Wenn Sie eine Schnittstelle implementieren, boxen Sie die Struktur natürlich so, dass sie sich jetzt auf dem Heap befindet und Sie sie nicht mehr als Wert übergeben können ... Dies bestätigt meine Meinung, dass Sie nur eine Klasse verwenden sollten in dieser Situation.


Wie oft geben Sie IComparable anstelle der konkreten Implementierung weiter?
FlySwat

Sie müssen nicht weitergeben, IComparableum den Wert zu boxen. Durch einfaches Aufrufen einer Methode, die IComparablemit einem Wertetyp erwartet , der sie implementiert, wird der Werttyp implizit eingerahmt.
Andrew Hare

1
@AndrewHare: Mit eingeschränkten Generika können Methoden IComparable<T>für Strukturen vom Typ Tohne Boxing aufgerufen werden.
Supercat

-10

Strukturen sind wie Klassen, die im Stapel leben. Ich sehe keinen Grund, warum sie "unsicher" sein sollten.


Außer ihnen fehlt die Vererbung.
FlySwat

7
Ich muss jedem Teil dieser Antwort widersprechen. Sie leben nicht unbedingt auf dem Stapel, und die Kopiersemantik unterscheidet sich stark von Klassen.
Marc Gravell

1
Sie sind unveränderlich, übermäßiger Gebrauch von Struktur wird Ihr Gedächtnis traurig machen :(
Teoman shipahi

1
@Teomanshipahi Übermäßige Verwendung von Klasseninstanzen macht Ihren Garbage Collector verrückt.
IllidanS4 will Monica am

4
Für jemanden mit mehr als 20.000 Wiederholungen ist diese Antwort einfach inakzeptabel.
Krythic
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.