Czy implementacja interfejsów przez struktury jest Bezpieczna?

Wydaje mi się, że pamiętam, jak czytałem coś o tym, jak to jest złe dla struktur implementujących interfejsy w CLR przez C#, ale nie mogę nic na ten temat znaleźć. Jest źle? Czy ma to niezamierzone konsekwencje?

public interface Foo { Bar GetBar(); }
public struct Fubar : Foo { public Bar GetBar() { return new Bar(); } }
Author: nawfal, 2008-09-15

9 answers

W tym pytaniu dzieje się kilka rzeczy...

Możliwe jest zaimplementowanie interfejsu przez strukturę, ale istnieją obawy związane z odlewaniem, zmiennością i wydajnością. Zobacz ten post po więcej szczegółów: http://blogs.msdn.com/abhinaba/archive/2005/10/05/477238.aspx

Ogólnie, struktury powinny być używane dla obiektów, które mają semantykę typu value. Implementując interfejs na struct możesz napotkać problemy związane z boksem, gdy struct jest rzucany pomiędzy strukturą a interfejsem. W wyniku boksu operacje, które zmieniają wewnętrzny stan struktury, mogą nie zachowywać się prawidłowo.

 37
Author: Scott Dorman,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-09-15 15:09:21

Ponieważ nikt inny wyraźnie nie udzielił tej odpowiedzi, dodam, co następuje:

Implementacja interfejsu na strukturze nie ma żadnych negatywnych konsekwencji.

Dowolna zmienna typu interface użyta do przechowywania struktury spowoduje użycie jej wartości pudełkowej. Jeśli struktura jest niezmienna (dobra rzecz), to jest to w najgorszym wypadku problem z wydajnością, chyba że:

  • używanie wynikowego obiektu do celów blokowania (an niezmiernie zły pomysł w jakikolwiek sposób)
  • używając semantyki równości odniesienia i oczekując, że będzie działać dla dwóch wartości z tej samej struktury.
Nie jest to jednak możliwe, ponieważ nie jest to możliwe.]}

Generyki

Być może wiele rozsądnych powodów dla struktur implementujących interfejsy jest po to, aby mogły być używane w ogólnym kontekście z ograniczenia. Przy użyciu w ten sposób zmienna tak:

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. Włącz użycie struktury jako parametru typu
    • tak długo, jak żadne inne ograniczenie jak new() lub class nie jest używane.
  2. pozwalają na unikanie boksu na konstrukcjach używanych w ten sposób.

Potem to.a nie jest odniesieniem do interfejsu, więc nie powoduje pudełka z tym, co jest w nim umieszczone. Następnie, gdy kompilator C# kompiluje klasy generyczne i musi wstawić wywołania metod instancji zdefiniowanych na przykład parametr typu T może używać constrained opcode:

Jeśli thisType jest typem wartości, a thistype implementuje metodę, to ptr jest przekazywany niemodyfikowany jako wskaźnik' this ' do instrukcji wywołania metody, w celu implementacji metody przez thisType.

Pozwala to uniknąć boksu, a ponieważ typ wartości implementuje interfejs to musi zaimplementować metodę, więc boks nie wystąpi. W powyższym przykładzie Equals() inwokacja odbywa się bez pola na tym.a1.

API o niskim tarciu

Większość struktur powinna mieć prymitywną semantykę, gdzie bitowo identyczne wartości są uważane za równe2. Runtime dostarczy takie zachowanie w implicit Equals(), ale może to być powolne. Również ta implicit equality jest Nie eksponowana jako implementacja IEquatable<T> i tym samym zapobiega używaniu struktur jako kluczy w słownikach, chyba że są one bezpośrednio implementowane siebie. Dlatego też często wiele typów struktur publicznych deklaruje, że implementują IEquatable<T> (gdzie T jest ich sobą), aby ułatwić i poprawić wydajność, a także zapewnić spójność z zachowaniem wielu istniejących typów wartości w CLR BCL.

Wszystkie prymitywy w BCL implementują co najmniej:

  • IComparable
  • IConvertible
  • IComparable<T>
  • IEquatable<T> (a więc IEquatable)

Wielu również wdraża IFormattable, ponadto wiele z Definiowane przez system typy wartości, takie jak DateTime, TimeSpan i Guid implementują wiele lub wszystkie z nich. Jeśli implementujesz podobny "powszechnie użyteczny" typ, jak struktura liczb zespolonych lub niektóre wartości tekstowe o stałej szerokości, implementacja wielu z tych wspólnych interfejsów (poprawnie) uczyni Twoją strukturę bardziej użyteczną i użyteczną.

Wyjątki

Oczywiście jeśli interfejs silnie implikuje zmienność (np. ICollection) to implementacja go jest złym pomysłem, ponieważ oznacza to, że albo zmieniłeś strukturę (co prowadzi do opisanych już błędów, gdzie modyfikacje występują na wartości pudełkowej, a nie na oryginalnej), albo dezorientujesz użytkowników ignorując implikacje metod, takich jak Add() lub rzucając wyjątki.

Wiele interfejsów nie implikuje zmienności (np. IFormattable) i służy jako idiomatyczny sposób eksponowania pewnych funkcji w spójny sposób. Często użytkownik struktury nie będzie dbał o każdy Boks nad głową za takie zachowanie.

Podsumowanie

Implementacja użytecznych interfejsów jest dobrym pomysłem, gdy odbywa się to rozsądnie, na niezmiennych typach wartości]}

Uwagi:

1: zauważ, że kompilator może tego użyć podczas wywoływania metod wirtualnych na zmiennych, które są } znane jako specyficzne struktury, ale w których wymagane jest wywołanie metody wirtualnej. Na przykład:

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

Wyliczenie zwracane przez Listę jest strukturą, optymalizacją, której należy unikać przydział przy wyliczaniu listy (z ciekawymi konsekwencjami ). Jednak semantyka foreach określa, że jeśli enumerator implementuje IDisposable, to Dispose() zostanie wywołane po zakończeniu iteracji. Oczywiście wystąpienie tego przez wywołanie boxed wyeliminowałoby jakąkolwiek korzyść z tego, że enumerator jest strukturą (w rzeczywistości byłoby gorzej). Co gorsza, jeśli wywołanie dispose modyfikuje w jakiś sposób stan enumeratora, to stanie się to na instancji pudełkowej i wielu subtelne błędy mogą być wprowadzane w złożonych przypadkach. Dlatego IL emitowane w takiej sytuacji wynosi:

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:  call        System.Collections.Generic.List.get_Current
IL_0018:  stloc.1     
IL_0019:  ldloca.s    02 
IL_001B:  call        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:  constrained. System.Collections.Generic.List.Enumerator
IL_002E:  callvirt    System.IDisposable.Dispose
IL_0033:  nop         
IL_0034:  endfinally  

Tak więc implementacja IDisposable nie powoduje żadnych problemów z wydajnością, a (godny ubolewania) zmienny aspekt enumeratora jest zachowany, jeśli metoda Disposable faktycznie cokolwiek zrobi!

2: double I float są wyjątkami od tej reguły, gdzie wartości NaN nie są uważane za równe.

 152
Author: ShuggyCoUk,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2014-09-08 12:09:46

W niektórych przypadkach może być dobrze, aby struktura zaimplementowała interfejs (jeśli nigdy nie była użyteczna, wątpliwe, że twórcy. net by ją zapewnili). Jeśli struktura implementuje interfejs tylko do odczytu, taki jak IEquatable<T>, przechowuje strukturę w miejscu przechowywania (zmienna, parametr, element tablicy, itd.) typu IEquatable<T> będzie wymagało, aby był boxed (każdy typ struct faktycznie definiuje dwa rodzaje rzeczy: Typ storage location, który zachowuje się jak typ wartości i typ heap-object, który zachowuje się jako typ klasy; pierwsza jest niejawnie zamieniana na drugą - "boks"- a druga może być zamieniana na pierwszą poprzez jawne odpisywanie - "unboxing"). Możliwe jest wykorzystanie implementacji interfejsu struktury bez boksu, jednak przy użyciu tak zwanych ograniczonych generyków.

Na przykład, jeśli ktoś ma metodę CompareTwoThings<T>(T thing1, T thing2) where T:IComparable<T>, taka metoda może wywołać thing1.Compare(thing2) bez konieczności boxowania thing1 lub thing2. Jeśli thing1 stanie się, np. Int32, Czas wykonania będzie wiedział, że gdy generuje kod dla CompareTwoThings<Int32>(Int32 thing1, Int32 thing2). Ponieważ będzie znał dokładny typ zarówno rzeczy hostującej metodę, jak i rzeczy przekazywanej jako parametr, nie będzie musiał blokować żadnej z nich.

Największy problem ze strukturami implementującymi interfejsy polega na tym, że struktura przechowywana w lokalizacji typu interface, Object lub ValueType (W przeciwieństwie do lokalizacji własnego typu) zachowuje się jak obiekt klasy. Dla interfejsów tylko do odczytu nie jest to generalnie problem, ale dla mutacji interfejs taki jak IEnumerator<T> może dać jakąś dziwną semantykę.

Rozważ 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

Zaznaczone polecenie # 1 będzie pierwsze enumerator1, aby odczytać pierwszy element. Stan tego wyliczenia zostanie skopiowany do enumerator2. Zaznaczona Instrukcja # 2 przesunie tę kopię, aby odczytać drugi element, ale nie wpłynie na enumerator1. Stan tego drugiego wyliczenia zostanie następnie skopiowany do enumerator3, który zostanie rozszerzony o zaznaczoną instrukcję #3. Wtedy, ponieważ enumerator3 i enumerator4 są obiema typami odniesienia, A odniesienie do enumerator3 zostanie następnie skopiowane do enumerator4, tak oznaczone oświadczenie skutecznie awansuje zarówno enumerator3 i enumerator4.

Niektórzy ludzie próbują udawać, że typy wartości i typy referencji są oba rodzaje Object, ale to nie do końca prawda. Rzeczywiste typy wartości są zamieniane na Object, ale nie są jego instancjami. Instancja List<String>.Enumerator, która jest przechowywana w miejscu tego typu, jest typem wartości i zachowuje się jak typ wartości; skopiowanie go do lokalizacji typu IEnumerator<String> przekonwertuje go na typ odniesienia, A zachowa się jako typ odniesienia . Ten ostatni jest rodzajem Object, ale ten pierwszy nie jest.

BTW, jeszcze kilka uwag: (1) Ogólnie rzecz biorąc, zmienne typy klas powinny mieć swoje metody Equals testujące równość odniesienia, ale nie ma przyzwoitego sposobu na to, aby struktura pudełkowa to zrobiła; (2) pomimo swojej nazwy, ValueType jest typem klasy, a nie typem wartości; wszystkie typy pochodzące z System.Enum są typami wartości, podobnie jak wszystkie typy typy, które wywodzą się z ValueType z wyjątkiem System.Enum, ale zarówno ValueType, jak i System.Enum są typami klasowymi.

 7
Author: supercat,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2013-01-14 16:34:31

Struktury są implementowane jako typy wartości, a klasy są typami odniesienia. Jeśli masz zmienną typu Foo i przechowujesz w niej instancję Fubar ,to "poleci ją" do typu referencyjnego, tym samym pokonując zaletę użycia struktury w pierwszej kolejności.

Jedynym powodem, dla którego widzę, że używam struktury zamiast klasy, jest to, że będzie to typ wartości, a nie Typ odniesienia, ale struktura nie może dziedziczyć po klasie. Jeśli masz struct dziedziczy interfejs, i przekazać wokół interfejsów, tracisz charakter typu wartości struktury. Równie dobrze możesz zrobić z tego klasę, jeśli potrzebujesz interfejsów.

 3
Author: dotnetengineer,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-09-15 15:06:24

(cóż nie mam nic ważnego do dodania, ale nie mam jeszcze umiejętności edycji, więc proszę bardzo..)
Całkowicie Bezpieczny. Nie ma nic nielegalnego w implementacji interfejsów na strukturach. Jednak powinieneś zapytać, dlaczego chcesz to zrobić.

Jednakże uzyskanie odniesienia do interfejsu do struktury spowoduje jego BOX . Więc kara wydajności i tak dalej.

Jedyny słuszny scenariusz, który mogę teraz wymyślić, to zilustrowany w moim poście tutaj . Gdy chcesz zmodyfikować stan struct przechowywany w zbiorze, musiałbyś to zrobić za pomocą dodatkowego interfejsu wyeksponowanego na strukturze.

 3
Author: Gishu,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:26:20

Myślę, że problem polega na tym, że powoduje Boks, ponieważ struktury są typami wartości, więc istnieje niewielka kara wydajności.

Ten link sugeruje, że mogą być z nim inne problemy...

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

 1
Author: Simon Keep,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-09-15 15:04:52

Nie ma żadnych konsekwencji dla struktury implementującej interfejs. Na przykład wbudowane struktury systemowe implementują interfejsy takie jak IComparable i IFormattable.

 0
Author: Joseph Daigle,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-09-15 15:04:05

Istnieje bardzo mały powód, dla którego typ wartości implementuje interfejs. Ponieważ nie można podklasować typu wartości, zawsze można odnieść się do niego jako konkretnego typu.

Chyba, że masz wiele struktur implementujących ten sam interfejs, może to być wtedy mało przydatne, ale w tym momencie zalecałbym użycie klasy i zrobienie tego dobrze.

Oczywiście, implementując interfejs, boksujesz strukturę, więc teraz siedzi na stercie i nie będziesz w stanie przejść to już według wartości...To naprawdę wzmacnia moją opinię, że powinieneś po prostu użyć klasy w tej sytuacji.

 0
Author: FlySwat,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-09-15 15:47:28

Struktury są jak Klasy, które żyją w stosie. Nie widzę powodu, dla którego powinny być "niebezpieczne".

 -5
Author: Sklivvz,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2008-09-15 15:03:54