Wydaje mi się, że czytałem coś o tym, jak złe jest dla struktur implementowanie interfejsów w CLR za pośrednictwem C #, ale nie mogę znaleźć nic na ten temat. To jest złe? Czy istnieją niezamierzone konsekwencje takiego postępowania?
public interface Foo { Bar GetBar(); }
public struct Fubar : Foo { public Bar GetBar() { return new Bar(); } }
IComparable<T>
iIEquatable<T>
. Przechowywanie strukturyFoo
w zmiennej typuIComparable<Foo>
wymagałoby zapakowania, ale jeśli typ ogólnyT
jest ograniczony doIComparable<T>
jednego, można go porównać z innymT
bez konieczności umieszczania żadnego z nich w pudełku i bez konieczności posiadania wiedzy na tematT
innych rzeczy niż to, że implementuje ograniczenie. Takie korzystne zachowanie jest możliwe tylko dzięki zdolności struktur do implementowania interfejsów. Powiedziawszy to ...Ponieważ nikt inny wyraźnie nie udzielił tej odpowiedzi, dodam, co następuje:
Implementacja interfejsu w strukturze nie ma żadnych negatywnych konsekwencji.
Każda zmienna typu interfejsu używana do przechowywania struktury spowoduje, że zostanie użyta wartość pudełkowa tej struktury. Jeśli struktura jest niezmienna (dobrze), w najgorszym przypadku jest to problem z wydajnością, chyba że:
Oba byłyby mało prawdopodobne, zamiast tego prawdopodobnie wykonasz jedną z następujących czynności:
Generics
Być może wiele rozsądnych powodów, dla których struktury implementujące interfejsy są takie, że mogą być używane w ogólnym kontekście z ograniczeniami . Użyta w ten sposób zmienna taka jak ta:
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); } }
new()
lub nieclass
jest używane.Wtedy this.a NIE jest odwołaniem do interfejsu, więc nie powoduje umieszczenia w nim pudełka. Ponadto, gdy kompilator C # kompiluje klasy generyczne i musi wstawić wywołania metod instancji zdefiniowanych w instancjach parametru Type T, może użyć ograniczonego opcode:
Pozwala to uniknąć boksowania, a ponieważ typ wartości jest implementowany, interfejs musi implementować metodę, dlatego nie wystąpią żadne opakowania. W powyższym przykładzie
Equals()
wywołanie jest wykonywane bez ramki. A 1 .API o niskim współczynniku tarcia
Większość struktur powinna mieć semantykę prymitywną, w której identyczne wartości bitowe są uważane za równe 2 . Środowisko wykonawcze zapewni takie zachowanie w sposób niejawny,
Equals()
ale może to być powolne. Również ta niejawna równość nie jest ujawniana jako implementacja,IEquatable<T>
a zatem zapobiega łatwemu używaniu struktur jako kluczy dla słowników, chyba że jawnie zaimplementują je samodzielnie. W związku z tym często zdarza się, że wiele typów struktur publicznych deklaruje, że implementująIEquatable<T>
(gdzieT
są one siebie), aby ułatwić i poprawić wydajność, a także zachować spójność z zachowaniem wielu istniejących typów wartości w CLR BCL.Wszystkie prymitywy w implementacji BCL to minimum:
IComparable
IConvertible
IComparable<T>
IEquatable<T>
(A więcIEquatable
)Wiele z nich również implementuje
IFormattable
, a ponadto wiele typów wartości zdefiniowanych w systemie, takich jak DateTime, TimeSpan i Guid, implementuje również wiele lub wszystkie z nich. Jeśli implementujesz podobnie „szeroko użyteczny” typ, jak struktura liczb zespolonych lub niektóre wartości tekstowe o stałej szerokości, to zaimplementowanie wielu z tych wspólnych interfejsów (poprawnie) sprawi, że twoja struktura będzie bardziej użyteczna i użyteczna.Wyłączenia
Oczywiście, jeśli interfejs silnie implikuje zmienność (taką jak
ICollection
), to zaimplementowanie go jest złym pomysłem, ponieważ oznaczałoby to, że albo dokonałeś mutacji struktury (co prowadzi do tego rodzaju błędów opisanych już, gdzie modyfikacje występują na wartości pudełkowej, a nie oryginalnej ) lub dezorientujesz użytkowników, ignorując konsekwencje metod takich jakAdd()
lub zgłaszanie wyjątków.Wiele interfejsów NIE implikuje zmienności (na przykład
IFormattable
) i służy jako idiomatyczny sposób na wyeksponowanie pewnych funkcji w spójny sposób. Często użytkownik struktury nie będzie przejmował się jakimkolwiek narzutem związanym z boksowaniem za takie zachowanie.Podsumowanie
Jeśli jest to zrobione rozsądnie, na niezmiennych typach wartości, dobrym pomysłem jest implementacja przydatnych interfejsów
Uwagi:
1: Zwróć uwagę, że kompilator może tego użyć podczas wywoływania metod wirtualnych na zmiennych, o których wiadomo, że mają określony typ struktury, ale w przypadku których wymagane jest wywołanie metody wirtualnej. Na przykład:
List<int> l = new List<int>(); foreach(var x in l) ;//no-op
Moduł wyliczający zwracany przez List jest strukturą, optymalizacją mającą na celu uniknięcie alokacji podczas wyliczania listy (z interesującymi konsekwencjami ). Jednak semantyka foreach określić, że jeśli narzędzia wyliczający
IDisposable
następnieDispose()
zostanie wywołana po zakończeniu iteracji. Oczywiście, gdyby to nastąpiło za pośrednictwem wywołania pudełkowego, wyeliminowałoby to jakąkolwiek korzyść z faktu, że moduł wyliczający jest strukturą (w rzeczywistości byłoby gorzej). Gorzej, jeśli wywołanie dispose modyfikuje w jakiś sposób stan modułu wyliczającego, to mogłoby się to zdarzyć w pudełkowej instancji i wiele subtelnych błędów może zostać wprowadzonych w złożonych przypadkach. Dlatego IL emitowany w tego rodzaju sytuacji to:Dlatego implementacja IDisposable nie powoduje żadnych problemów z wydajnością, a (niestety) zmienny aspekt modułu wyliczającego zostaje zachowany, gdyby metoda Dispose faktycznie coś zrobiła!
2: double i float są wyjątkami od tej reguły, gdzie wartości NaN nie są uważane za równe.
źródło
struct
do plikuinterface
.W niektórych przypadkach dobrze byłoby, gdyby struktura zaimplementowała interfejs (gdyby nigdy nie był przydatny, wątpliwe jest, aby twórcy .net by to zapewnili). Jeśli struktura implementuje interfejs tylko do odczytu, taki jak
IEquatable<T>
przechowywanie struktury w miejscu przechowywania (zmienna, parametr, element tablicy itp.),IEquatable<T>
Będzie wymagało, aby była zapakowana (każdy typ struktury faktycznie definiuje dwa rodzaje rzeczy: magazyn typ lokalizacji, który zachowuje się jak typ wartości i typ obiektu sterty, który zachowuje się jak typ klasy; pierwszy jest niejawnie konwertowany na drugi - „pudełko” - a drugi może być konwertowany na pierwszy przez jawne rzutowanie - „unboxing”). Możliwe jest wykorzystanie implementacji interfejsu przez strukturę bez boksu, jednak przy użyciu tak zwanych ograniczonych typów generycznych.Na przykład, gdyby ktoś miał metodę
CompareTwoThings<T>(T thing1, T thing2) where T:IComparable<T>
, taka metoda mogłaby wywołaćthing1.Compare(thing2)
bez konieczności zaznaczaniathing1
lubthing2
. Jeślithing1
tak się stanie, np. An, środowiskoInt32
wykonawcze będzie wiedziało, że generuje kod dlaCompareTwoThings<Int32>(Int32 thing1, Int32 thing2)
. Ponieważ będzie znał dokładny typ obiektu hostującego metodę i tego, który jest przekazywany jako parametr, nie będzie musiał zaznaczać żadnego z nich.Największym problemem związanym ze strukturami, które implementują interfejsy, jest to, że struktura, która jest przechowywana w lokalizacji typu interfejsu
Object
lubValueType
(w przeciwieństwie do lokalizacji własnego typu) zachowuje się jak obiekt klasy. W przypadku interfejsów tylko do odczytu na ogół nie stanowi to problemu, ale w przypadku interfejsu z mutacją, takiego jakIEnumerator<T>
ten, może dać dziwną semantykę.Rozważmy na przykład następujący kod:
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
Zaznaczona instrukcja nr 1 spowoduje
enumerator1
odczytanie pierwszego elementu. Stan tego modułu wyliczającego zostanie skopiowany doenumerator2
. Zaznaczona instrukcja nr 2 przesunie tę kopię do odczytania drugiego elementu, ale nie wpłynie na niąenumerator1
. Stan tego drugiego modułu wyliczającego zostanie następnie skopiowany doenumerator3
, który zostanie przesunięty o zaznaczoną instrukcję nr 3. Wtedy, ponieważenumerator3
ienumerator4
są oba typy referencyjną, mającej naenumerator3
zostanie skopiowany doenumerator4
, tak oznakowane oświadczenie będzie skutecznie awansować zarównoenumerator3
ienumerator4
.Niektórzy próbują udawać, że typy wartości i typy referencyjne są jednymi i drugimi
Object
, ale to nieprawda. Rzeczywiste typy wartości można zamieniać naObject
, ale nie są to instancje. Instancja,List<String>.Enumerator
która jest przechowywana w lokalizacji tego typu, jest typem wartości i zachowuje się jak typ wartości; skopiowanie go do lokalizacji typuIEnumerator<String>
spowoduje przekonwertowanie go na typ referencyjny i będzie zachowywał się jak typ referencyjny . Ten ostatni jest rodzajemObject
, ale ten pierwszy nie.Przy okazji, jeszcze kilka uwag: (1) Ogólnie rzecz biorąc, zmienne typy klas powinny mieć swoje
Equals
metody testujące równość odwołań, ale nie ma na to przyzwoitego sposobu, aby to zrobić w strukturze pudełkowej; (2) pomimo swojej nazwyValueType
jest typem klasowym, a nie wartościowym; wszystkie typy pochodne odSystem.Enum
są typami wartości, podobnie jak wszystkie typy, które pochodzą od,ValueType
z wyjątkiemSystem.Enum
, ale obaValueType
iSystem.Enum
są typami klasowymi.źródło
Struktury są implementowane jako typy wartości, a klasy są typami referencyjnymi. Jeśli masz zmienną typu Foo i przechowujesz w niej instancję Fubar, będzie ona „opakowywać” ją w typ referencyjny, co niweczy korzyści wynikające z używania struktury w pierwszej kolejności.
Jedynym powodem, dla którego widzę użycie struktury zamiast klasy, jest to, że będzie to typ wartości, a nie typ referencyjny, ale struktura nie może dziedziczyć z klasy. Jeśli struktura dziedziczy interfejs i przekazujesz interfejsy dookoła, tracisz naturę struktury typu wartości. Równie dobrze może po prostu uczynić z niej klasę, jeśli potrzebujesz interfejsów.
źródło
(Cóż, nie mam nic ważnego do dodania, ale nie mam jeszcze umiejętności edycji, więc proszę ...)
Całkowicie bezpieczny. Nie ma nic nielegalnego w implementowaniu interfejsów w strukturach. Jednak powinieneś zapytać, dlaczego chcesz to zrobić.
Jednak uzyskanie referencji interfejsu do struktury spowoduje jej BOX . A więc spadek wydajności i tak dalej.
Jedyny prawidłowy scenariusz, o którym mogę teraz pomyśleć, jest zilustrowany w moim poście tutaj . Jeśli chcesz zmodyfikować stan struktury przechowywany w kolekcji, musisz to zrobić za pośrednictwem dodatkowego interfejsu uwidocznionego w strukturze.
źródło
Int32
do metody, która akceptuje typ ogólnyT:IComparable<Int32>
(który może być albo parametrem typu ogólnego metody, albo klasą metody), ta metoda będzie mogła użyćCompare
metody na przekazanym obiekcie bez pakowania w ramki.Myślę, że problem polega na tym, że powoduje to boksowanie, ponieważ struktury są typami wartości, więc istnieje niewielki spadek wydajności.
Ten link sugeruje, że mogą występować z nim inne problemy ...
http://blogs.msdn.com/abhinaba/archive/2005/10/05/477238.aspx
źródło
Struktura implementująca interfejs nie ma żadnych konsekwencji. Na przykład wbudowane struktury systemu implementują interfejsy takie jak
IComparable
iIFormattable
.źródło
Istnieje bardzo niewiele powodów, dla których typ wartości powinien implementować interfejs. Ponieważ nie można podklasować typu wartości, zawsze można odwołać się do niego jako do konkretnego typu.
O ile oczywiście nie masz wielu struktur implementujących ten sam interfejs, może to być wtedy marginalnie przydatne, ale w tym momencie zalecałbym użycie klasy i zrobienie tego dobrze.
Oczywiście, implementując interfejs, pakujesz strukturę, więc teraz siedzi na stercie i nie będziesz już w stanie przekazać jej przez wartość ... To naprawdę wzmacnia moją opinię, że powinieneś po prostu użyć klasy w tej sytuacji.
źródło
IComparable
wartości w ramce. Po prostu wywołując metodę, która oczekujeIComparable
z typem wartości, który ją implementuje, niejawnie zapakujesz typ wartości.IComparable<T>
wywoływanie metod na strukturach typuT
bez pakowania.Struktury są jak klasy żyjące na stosie. Nie widzę powodu, dla którego miałyby być „niebezpieczne”.
źródło