Er det trygt for structs å implementere grensesnitt?

stemmer
73

Jeg synes å huske å ha lest noe om hvordan det er dårlig for structs å implementere grensesnitt i CLR via C #, men jeg kan ikke synes å finne noe om det. Er det ille? Er det utilsiktede konsekvenser av å gjøre det?

public interface Foo { Bar GetBar(); }
public struct Fubar : Foo { public Bar GetBar() { return new Bar(); } }
Publisert på 15/09/2008 klokken 14:59
kilden bruker
På andre språk...                            


9 svar

stemmer
152

Siden ingen andre eksplisitt gitt dette svaret vil jeg legge til følgende:

Implementering av et grensesnitt på en struct har ingen negative konsekvenser overhodet.

Enhver variabel av den type grensesnitt benyttes for å holde en struct vil resultere i en eske verdien av den struct som brukes. Hvis struct er uforanderlig (bra), så dette er i verste fall et ytelsesproblem med mindre du er:

  • anvendelse av den resulterende gjenstand for låseformål (en uhyre dårlig idé hvilken som helst måte)
  • bruke referanse likestilling semantikk og forventer at det skal fungere for to eske verdier fra samme struct.

Begge disse ville være usannsynlig, i stedet er du sannsynligvis til å gjøre ett av følgende:

Generics

Kanskje mange fornuftige grunner til structs implementere grensesnitt er slik at de kan brukes i en generisk sammenheng med begrensninger . Når den brukes på denne måten variabelen slik:

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. Muliggjøre bruk av struct som en type parameter
    • så lenge ingen annen begrensning som new()eller classbrukes.
  2. Tillat å unngå boksing på structs brukes på denne måten.

Da this.a er ikke et grensesnitt referanse slik at det ikke medfører en boks med det som er plassert inn i den. Videre når den c # kompilatoren kompilerer generiske klasser og behov for å sette inn anrop av instans metoder definert på forekomster av den type som parameteren T den kan bruke begrenset opkode:

Hvis thisType er en verditype og thisType implementerer metoden da ptr føres umodifisert som 'denne' peker til en samtale metode instruksjon, for gjennomføring av fremgangsmåten ved thisType.

Dette unngår boksing og siden verditypen implementerer grensesnittet implementere metoden, og dermed ingen boksing vil skje. I eksemplet ovenfor Equals()påkallelse er gjort med ingen boks på this.a en .

Lav friksjon APIer

De fleste structs bør ha primitive-lignende semantikk hvor bitvis identiske verdier anses lik to . Kjøretiden vil levere slik oppførsel i den implisitte Equals(), men dette kan være treg. Også dette implisitt likestilling er ikke avslørt som en implementering av IEquatable<T>og dermed hindrer structs brukes enkelt som taster for Ordbøker mindre de eksplisitt gjennomføre det selv. Det er derfor vanlig at mange offentlige struct typer å erklære at de gjennomfører IEquatable<T>(der Ter dem selv) for å gjøre dette enklere og bedre resultater samt konsistent med oppførselen til mange eksisterende verdityper i CLR BCL.

Alle primitiver i BCL gjennomføre på et minimum:

  • IComparable
  • IConvertible
  • IComparable<T>
  • IEquatable<T>(Og slik IEquatable)

Mange også gjennomføre IFormattableytterligere mange av de systemdefinerte verdityper som Datetime, Timespan og Guid gjennomføre mange eller alle av disse også. Hvis du gjennomfører en tilsvarende 'mye nyttig' type som et komplekst tall struct eller noen fast bredde tekstlige verdier deretter implementere mange av disse felles grensesnitt (riktig) vil gjøre struct mer nyttig og brukervennlig.

utelukkelser

Selvfølgelig hvis grensesnittet antyder sterkt mutability (for eksempel ICollection) så implementere det er en dårlig idé som det ville bety tat du enten gjorde struct foranderlig (som fører til den slags feil beskrevet allerede der endringene skjer på eske verdi snarere enn den opprinnelige ), eller du forvirre brukerne ved å ignorere konsekvensene av metodene som Add()eller kaster unntak.

Mange grensesnitt innebærer ikke mutability (for eksempel IFormattable), og tjene som idiomatisk måte å eksponere visse funksjoner på en konsekvent måte. Ofte brukeren av struct vil ikke bry seg om noen bokse overhead for slik oppførsel.

Sammendrag

Når du er ferdig fornuftig, på uforanderlige verdityper, er gjennomføringen av nyttige grensesnitt en god idé


Merknader:

1: herunder bemerkes at kompilatoren kan bruke dette når påkalle virtuelle metoder på variabler som er kjent for å være av en spesiell struct type, men hvor det er nødvendig for å påkalle en virtuell metode. For eksempel:

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

Enumeratoren returneres av List er en struct, en optimalisering for å unngå en bevilgning når opplisting listen (med noen interessante konsekvenser ). Men semantikk foreach spesifisere at hvis enumeratoren implementerer IDisposableda Dispose()vil bli kalt når iterasjon er fullført. Selvfølgelig har dette skje gjennom en eske samtale ville eliminere noen nytte av enumeratoren være en struct (faktisk det ville være verre). Verre, hvis kast samtale endrer status for enumeratoren på noen måte da dette ville skje på eske forekomst og mange subtile feil kan bli innført i komplekse saker. Derfor IL slippes ut i denne typen situasjon er:

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

Dermed gjennomføringen av IDisposable ikke medfører noen ytelsesproblemer og (beklagelig) foranderlig aspekt av enumeratoren er bevart bør Kast metoden faktisk gjøre noe!

2: dobbelt og flyte finnes unntak til denne regelen der NaN verdiene regnes ikke like.

Svarte 17/08/2009 kl. 18:17
kilden bruker

stemmer
37

Det er flere ting som skjer i dette spørsmålet ...

Det er mulig for en struct å implementere et grensesnitt, men det er bekymringer som kommer rundt med støping, mutability og ytelse. Se dette innlegget for mer informasjon: http://blogs.msdn.com/abhinaba/archive/2005/10/05/477238.aspx

Generelt bør structs brukes for objekter som har verdi-type semantikk. Ved å implementere et grensesnitt på en struct kan du kjøre inn boksing bekymringer som struct er kastet frem og tilbake mellom struct og grensesnittet. Som et resultat av boksing, kan operasjoner som endrer den interne tilstanden til struct ikke oppfører seg ordentlig.

Svarte 15/09/2008 kl. 15:09
kilden bruker

stemmer
7

I noen tilfeller kan det være bra for en struct å implementere et grensesnitt (hvis det var aldri nyttig, er det tvilsomt skaperne av .net ville ha gitt for det). Hvis en struct implementerer en skrivebeskyttet grensesnitt lignende IEquatable<T>, lagring struct i et lagringssted (variabel, parameter, gruppeelement, etc.) av type IEquatable<T>vil kreve at det være eske (hver struct typen som definerer to typer ting: en lagrings plassering type som oppfører seg som et verditype og en haug-objekttype som oppfører seg som en klasse type; den første er implisitt kan omdannes til den annen - "bokse" - og den andre kan omdannes til den første via eksplisitt cast-- "unboxing"). Det er mulig å utnytte en struktur implementering av et grensesnitt uten boksing, men ved hjelp av det som kalles begrenset generika.

For eksempel, hvis man hadde en metode CompareTwoThings<T>(T thing1, T thing2) where T:IComparable<T>, kan en slik metode ring thing1.Compare(thing2)uten å måtte boksen thing1eller thing2. Hvis thing1skjer for å være, for eksempel, en Int32vil kjøre-tid vet at når det genererer koden for CompareTwoThings<Int32>(Int32 thing1, Int32 thing2). Siden det vil vite den eksakte type både ting hosting metoden og ting som blir sendt som en parameter, vil det ikke ha å bokse en av dem.

Det største problemet med structs som implementerer grensesnitt er at en struct som blir lagret på et sted av grensesnittet type, Objecteller ValueType(i motsetning til en plassering av sin egen type) vil oppføre seg som en klasse objekt. For skrivebeskyttet grensesnitt dette er vanligvis ikke et problem, men for en mute grensesnitt som IEnumerator<T>det kan gi noen merkelige semantikk.

Tenk for eksempel følgende kode:

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

Merket statement # 1 vil prime enumerator1å lese det første elementet. Delstaten som enumeratoren vil bli kopiert til enumerator2. Merket statement # 2 vil fremme denne kopien til å lese andre element, men vil ikke påvirke enumerator1. Tilstanden til at andre enumeratoren vil da bli kopiert til enumerator3, som vil bli fremmet av merket statement # 3. Deretter, fordi enumerator3og enumerator4er begge referansetyper, en REFERANSE til enumerator3vil da bli kopiert til enumerator4, så merket uttalelsen vil effektivt fremme både enumerator3 og enumerator4.

Noen prøver å late som om verdityper og referansetyper er begge typer Object, men det er egentlig ikke sant. Ekte verdityper er konvertible til Object, men er ikke forekomster av det. Et eksempel på List<String>.Enumeratorhvilken er lagret på et sted av denne typen er en verdi-type og oppfører seg som et verditype; kopiere den til et sted av type IEnumerator<String>vil konvertere den til en referansetype, og det vil oppføre seg som en referansetype . Sistnevnte er en slags Object, men det tidligere ikke.

BTW, et par flere notater: (1) Generelt foranderlig klassetyper bør ha sin Equalsmetoder test referansen likestilling, men det er ingen anstendig måte for en eske struct å gjøre det; (2) til tross for navnet, ValueTypeer en klasse type, ikke en verdi type; alle typer avledet fra System.Enumer verdityper, som er alle typer som er avledet fra ValueType, med unntak av System.Enum, men begge deler ValueTypeog System.Enumer klasse-typer.

Svarte 14/01/2013 kl. 16:34
kilden bruker

stemmer
3

(Vel fikk ikke noe stort å legge til, men har ikke redigering prowess ennå så her går ..)
helt trygt. Ingenting ulovlig med å implementere grensesnitt på structs. Men du bør spørsmålet hvorfor du ønsker å gjøre det.

Imidlertid skaffe et grensesnitt henvisning til en struct vil BOX den. Så ytelse straff og så videre.

Den eneste gyldige scenario som jeg kan tenke på akkurat nå er illustrert i mitt innlegg her . Når du ønsker å endre en struct tilstand lagres i en samling, vil du være nødt til å gjøre det via en ekstra interface eksponert på struct.

Svarte 15/09/2008 kl. 15:14
kilden bruker

stemmer
3

Structs er implementert som verdityper og klasser er referansetyper. Hvis du har en variabel av type Foo, og du lagrer en forekomst av Fubar i den, vil den "boksen det" opp i en referansetype, og dermed beseiret fordelen med å bruke en struct i første omgang.

Den eneste grunnen jeg ser til å bruke en struct i stedet for en klasse er fordi det vil være en verditype og ikke en referansetype, men struct kan ikke arve fra en klasse. Hvis du har struct arve et grensesnitt, og du passerer rundt grensesnitt, mister du at verditype natur struct. Kan like godt bare gjøre det en klasse hvis du trenger grensesnitt.

Svarte 15/09/2008 kl. 15:06
kilden bruker

stemmer
1

Jeg tror problemet er at det fører til boksing fordi structs er verdityper, så det er en liten ytelsen.

Denne koblingen antyder at det kan være andre problemer med det ...

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

Svarte 15/09/2008 kl. 15:04
kilden bruker

stemmer
0

Det er svært liten grunn for en verditype for å implementere et grensesnitt. Siden du ikke kan underklasse en verditype, kan du alltid se det som sin konkrete type.

Med mindre selvfølgelig, har du flere structs alle som gjennomfører samme grensesnitt, kan det være marginalt nyttig da, men på det punktet jeg vil anbefale å bruke en klasse og gjør det riktig.

Selvfølgelig, ved å implementere et grensesnitt, er du boksing struct, så det nå sitter på haugen, og du vil ikke være i stand til å passere det verdi lenger ... Dette forsterker virkelig min mening at du bør bare bruke en klasse i denne situasjonen.

Svarte 15/09/2008 kl. 15:04
kilden bruker

stemmer
0

Det er ingen konsekvenser for en struct som implementerer et grensesnitt. For eksempel den innebygde system structs implementere grensesnitt som IComparableog IFormattable.

Svarte 15/09/2008 kl. 15:04
kilden bruker

stemmer
-5

Structs er akkurat som klasser som lever i bunken. Jeg ser ingen grunn til at de skal være "usikker".

Svarte 15/09/2008 kl. 15:03
kilden bruker

Cookies help us deliver our services. By using our services, you agree to our use of cookies. Learn more