it-swarm-eu.dev

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

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

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

In dieser Frage geht es um verschiedene Dinge ...

Es ist für eine Struktur möglich, eine Schnittstelle zu implementieren, aber es gibt Bedenken hinsichtlich Casting, Wandlungsfähigkeit und Leistung. Weitere Informationen finden Sie in diesem Beitrag: http://blogs.msdn.com/abhinaba/archive/2005/10/05/477238.aspx

Im Allgemeinen sollten Strukturen für Objekte mit Werttypsemantik verwendet werden. Durch das Implementieren einer Schnittstelle in einer Struktur können Sie Boxing-Probleme bekommen, während die Struktur zwischen der Struktur und der Schnittstelle hin und her gewechselt wird. Infolge des Boxens verhalten sich Vorgänge, die den internen Status der Struktur ändern, möglicherweise nicht richtig.

45
Scott Dorman

Da diese Antwort von niemand anderem ausdrücklich angegeben wurde, möchte ich Folgendes hinzufügen:

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

Jede Variable des Schnittstellentyps, der zur Aufnahme einer Struktur verwendet wird, führt dazu, dass ein Box-Wert 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 für Sperrzwecke (auf jeden Fall eine immens schlechte Idee)
  • verwenden der Referenzgleichheitssemantik und Erwarten, dass sie für zwei Boxed-Werte derselben Struktur funktioniert.

Beides wäre unwahrscheinlich. Stattdessen haben Sie wahrscheinlich eine der folgenden Möglichkeiten:

Generika

Möglicherweise gibt es viele vernünftige Gründe, warum Strukturen Schnittstellen implementieren, damit sie in einem generischen Kontext mit Einschränkungen verwendet werden können . Bei dieser Verwendung sieht die Variable folgendermaßen aus:

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 class verwendet wird.
  2. Vermeiden Sie das Boxen von Strukturen, die auf diese Weise verwendet werden.

Dann ist this.a KEINE Schnittstellenreferenz, es wird also keine Kiste mit dem darin befindlichen Inhalt erstellt. Wenn der c # -Compiler die generischen Klassen kompiliert und Aufrufe der Instanzmethoden einfügen muss, die für Instanzen des Type-Parameters T definiert sind, kann er den constrained -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 die Schnittstelle implementiert, muss die Methode implementieren, wird kein Boxen stattfinden. Im obigen Beispiel wird der Aufruf von Equals() ohne Box auf this.a ausgeführt1.

Reibungsarme APIs

Die meisten Strukturen sollten eine primitivartige Semantik haben, bei der bitweise identische Werte als gleich angesehen werden2. Die Laufzeit liefert ein solches Verhalten im impliziten Equals(), aber dies kann langsam sein. Auch diese implizite Gleichheit wird not als Implementierung von IEquatable<T> Verfügbar gemacht und verhindert somit, dass Strukturen einfach als Schlüssel für Wörterbücher verwendet werden können, es sei denn, sie implementieren sie explizit selbst. Viele öffentliche Strukturtypen deklarieren daher häufig, dass sie IEquatable<T> Implementieren (wobei T sie selbst sind), um dies einfacher und leistungsfähiger zu machen und dem Verhalten vieler vorhandener Werte zu entsprechen Typen innerhalb der CLR BCL.

Alle Grundelemente in der BCL implementieren mindestens:

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

Viele implementieren auch IFormattable, weitere viele der vom System definierten Werttypen wie DateTime, TimeSpan und Guid implementieren viele oder alle diese ebenfalls. Wenn Sie einen ähnlich nützlichen Typ wie eine komplexe Zahlenstruktur oder Textwerte mit fester Breite implementieren, wird Ihre Struktur durch die Implementierung vieler dieser allgemeinen Schnittstellen (korrekt) nützlicher und benutzerfreundlicher.

Ausschlüsse

Wenn die Schnittstelle stark Wandlungsfähigkeit (wie ICollection) impliziert, ist die Implementierung offensichtlich eine schlechte Idee, da dies bedeuten würde, dass Sie die Struktur entweder wandlungsfähig gemacht haben (was zu einer Art von führt) Fehler, die bereits beschrieben wurden, wenn die Änderungen nicht am Original, sondern am Box-Wert vorgenommen wurden, oder Sie verwirren die Benutzer, indem Sie die Auswirkungen der Methoden wie Add() ignorieren oder Ausnahmen auslösen.

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

Zusammenfassung

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


Anmerkungen:

1: Beachten Sie, dass der Compiler dies verwenden kann, wenn er virtuelle Methoden für Variablen aufruft, die Known eines bestimmten Strukturtyps 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 beim Auflisten der Liste zu vermeiden (mit einigen interessanten Konsequenzen ). Die Semantik von foreach gibt jedoch an, dass, wenn der Enumerator IDisposable implementiert, Dispose() aufgerufen wird, sobald die Iteration abgeschlossen ist. Wenn dies offensichtlich durch einen Box-Aufruf geschieht, würde jeder Vorteil beseitigt, dass der Enumerator eine Struktur ist (in der Tat wäre es schlimmer). Schlimmer noch, wenn der Aufruf dispose den Zustand des Enumerators auf irgendeine Weise ändert, würde dies auf der Box-Instanz passieren und in komplexen Fällen könnten 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: nop 
 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: Aufruf von System.Collections.Generic.List.get_Current 
 IL_0018: stloc.1 
 IL_0019: ldloca.s 02 
 IL_001B: Aufruf von System.Collections.Generic.List.MoveNext 
 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: nop 
 IL_0034: endfinally 

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

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

161
ShuggyCoUk

In einigen Fällen kann es für eine Struktur hilfreich sein, eine Schnittstelle zu implementieren (wenn dies nie nützlich gewesen wäre, hätten die Ersteller von .net dies wahrscheinlich nicht vorgesehen). Wenn eine Struktur eine schreibgeschützte Schnittstelle wie IEquatable<T> implementiert, muss die Struktur in einem Speicherort (Variable, Parameter, Array-Element usw.) vom Typ IEquatable<T> in einem Kästchen abgelegt werden (jeder Strukturtyp definiert tatsächlich zwei Arten von Dingen: Ein Speicherorttyp, der sich als Werttyp und ein Heap-Objekttyp als 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 Boxing auszunutzen, indem sogenannte beschränkte Generika verwendet werden.

Wenn man zum Beispiel eine Methode CompareTwoThings<T>(T thing1, T thing2) where T:IComparable<T> hätte, könnte eine solche Methode thing1.Compare(thing2) aufrufen, ohne thing1 oder thing2 ankreuzen zu müssen. Wenn thing1 beispielsweise ein Int32 ist, weiß die Laufzeit, dass, wenn sie den Code für CompareTwoThings<Int32>(Int32 thing1, Int32 thing2) generiert. Da er den genauen Typ sowohl des Objekts, das die Methode hostet, als auch des Objekts, das als Parameter übergeben wird, kennt, muss er keines von beiden boxen.

Das größte Problem bei Strukturen, die Schnittstellen implementieren, besteht darin, dass sich eine Struktur, die an einem Speicherort des Schnittstellentyps Object oder ValueType (im Gegensatz zu einem Speicherort ihres eigenen Typs) gespeichert wird, wie folgt verhält ein Klassenobjekt. Bei schreibgeschützten Schnittstellen ist dies im Allgemeinen kein Problem, aber bei einer mutierenden Schnittstelle wie IEnumerator<T> kann es zu merkwürdiger 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

Mit der markierten Anweisung 1 wird enumerator1 mit einem Primer versehen, um das erste Element zu lesen. Der Status dieses Enumerators wird nach enumerator2 kopiert. Die markierte Anweisung Nr. 2 schiebt diese Kopie vor, um das zweite Element zu lesen, hat jedoch keinen Einfluss auf enumerator1. Der Status dieses zweiten Enumerators wird dann in enumerator3 kopiert, der durch die markierte Anweisung # 3 erweitert wird. Dann wird, da enumerator3 und enumerator4 beide Referenztypen sind, ein REFERENZ auf enumerator3 nach enumerator4 kopiert, sodass die markierte Anweisung effektiv vorrückt beide)enumerator3 und enumerator4.

Einige Leute versuchen vorzutäuschen, dass Werttypen und Referenztypen beide Arten von Object sind, aber das stimmt nicht wirklich. Reale Werttypen können in Object konvertiert werden, sind jedoch keine Instanzen davon. Eine Instanz von List<String>.Enumerator, die an einem Ort dieses Typs gespeichert ist, ist ein Werttyp und verhält sich wie ein Werttyp. Wenn Sie es an eine Position vom Typ IEnumerator<String> kopieren, wird es in einen Referenztyp konvertiert und es verhält sich wie ein Referenztyp. Das letztere ist eine Art von Object, das erstere jedoch nicht.

Übrigens noch ein paar Anmerkungen: (1) Im Allgemeinen sollten bei veränderlichen Klassentypen die Equals -Methoden die Referenzgleichheit testen, aber es gibt keine angemessene Möglichkeit für eine Boxstruktur, dies zu tun. (2) Trotz seines Namens ist ValueType ein Klassentyp und kein Wertetyp. Alle von System.Enum abgeleiteten Typen sind Werttypen, ebenso wie alle Typen, die von ValueType abgeleitet sind, mit Ausnahme von System.Enum, aber sowohl ValueType als auch System.Enum sind Klassentypen.

8
supercat

(Nun, ich habe nichts Wichtiges hinzuzufügen, aber ich habe noch keine Schnittfähigkeiten. Hier geht es weiter.)
Perfekt sicher. Das Implementieren von Schnittstellen auf Strukturen ist nicht illegal. Sie sollten sich jedoch fragen, warum Sie dies tun möchten.

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

Das einzig gültige Szenario, an das ich im Moment denken kann, ist in meinem Beitrag hier dargestellt . Wenn Sie den in einer Auflistung 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.

3
Gishu

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

Der einzige Grund, warum ich eine Struktur anstelle einer Klasse verwende, ist, dass es sich um einen Werttyp und keinen Referenztyp handelt, die Struktur jedoch nicht von einer Klasse erben kann. Wenn die Struktur eine Schnittstelle erbt und Sie Schnittstellen weitergeben, verlieren Sie den Werttyp der Struktur. Machen Sie es auch zu einer Klasse, wenn Sie Schnittstellen benötigen.

3
dotnetengineer

Ich denke, das Problem ist, dass es Boxen verursacht, weil Strukturen Werttypen sind, so gibt es eine leichte Leistungseinbuße.

Dieser Link weist darauf hin, dass möglicherweise andere Probleme vorliegen ...

http://blogs.msdn.com/abhinaba/archive/2005/10/05/477238.aspx

1
Simon Keep

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

0
Joseph Daigle

Es gibt sehr wenig Grund für einen Werttyp, eine Schnittstelle zu implementieren. Da Sie einen Werttyp nicht in Unterklassen unterteilen können, können Sie ihn immer als konkreten Typ bezeichnen.

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

Wenn Sie eine Schnittstelle implementieren, packen Sie die Struktur ein, sodass 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.

0
FlySwat