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:
- używanie powstałego obiektu do blokowania (w każdym razie niezmiernie zły pomysł)
- używając semantyki równości odwołań i oczekując, że będzie działać dla dwóch wartości opakowanych z tej samej struktury.
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);
}
}
- Włącz użycie struktury jako parametru typu
- o ile żadne inne ograniczenie nie jest takie jak
new()
lub nie class
jest używane.
- Pozwól na unikanie boksowania na konstrukcjach używanych w ten sposób.
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:
Jeśli thisType jest typem wartości i thisType implementuje metodę, to ptr jest przekazywany niezmodyfikowany jako wskaźnik „this” do instrukcji metody wywołania, w celu implementacji metody przez thisType.
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>
(gdzie T
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ęc IEquatable
)
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 jak Add()
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)
;
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ępnie Dispose()
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:
IL_0001: newobj System.Collections.Generic.List..ctor
IL_0006: stloc.0
IL_0007: nie
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: wywołaj System.Collections.Generic.List.get_Current
IL_0018: stloc.1
IL_0019: ldloca.s 02
IL_001B: wywołanie 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: ograniczony. System.Collections.Generic.List.Enumerator
IL_002E: callvirt System.IDisposable.Dispose
IL_0033: nie
IL_0034: koniec końców
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.