構造体がC#を介してCLRにインターフェイスを実装するのがいかに悪いかについて何かを読んだことを覚えているようですが、それについては何も見つからないようです。悪いですか?そうすることの意図しない結果はありますか?
public interface Foo { Bar GetBar(); }
public struct Fubar : Foo { public Bar GetBar() { return new Bar(); } }
回答:
この質問ではいくつかのことが起こっています...
構造体がインターフェースを実装することは可能ですが、キャスト、可変性、およびパフォーマンスに伴う懸念があります。詳細については、この投稿を参照してください:https://docs.microsoft.com/en-us/archive/blogs/abhinaba/c-structs-and-interface
一般に、構造体は、値型のセマンティクスを持つオブジェクトに使用する必要があります。構造体にインターフェースを実装することにより、構造体が構造体とインターフェースの間で前後にキャストされるときに、ボクシングの懸念に遭遇する可能性があります。ボックス化の結果、構造体の内部状態を変更する操作が正しく動作しない場合があります。
IComparable<T>
とIEquatable<T>
。構造体Foo
を型の変数に格納するにIComparable<Foo>
はボックス化が必要ですが、ジェネリック型T
がIComparable<T>
1つに制約されている場合はT
、どちらかをボックス化する必要がなくT
、制約を実装すること以外は何も知らなくても、構造体を別の型と比較できます。このような有利な動作は、構造体がインターフェースを実装する能力によってのみ可能になります。...言われたこと
他の誰もこの答えを明示的に提供しなかったので、私は以下を追加します:
構造体にインターフェースを実装しても、悪影響はありません。
任意の変数構造体を保持するために使用されるインターフェイスタイプのは、使用されているその構造体のボックス化値をもたらすであろう。構造体が不変である場合(良いこと)、次の場合を除いて、これは最悪の場合パフォーマンスの問題です。
これらの両方が発生する可能性は低く、代わりに次のいずれかを実行する可能性があります。
おそらく、構造体がインターフェースを実装する多くの合理的な理由は、それらが制約付きの一般的なコンテキスト内で使用できるようにするためです。この方法で使用すると、次のような変数が使用されます。
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()
またはのような他の制約class
が使用されていない限り。その場合、this.aはインターフェイス参照ではないため、そこに配置されたもののボックスは発生しません。さらに、c#コンパイラが汎用クラスをコンパイルし、TypeパラメータTのインスタンスで定義されたインスタンスメソッドの呼び出しを挿入する必要がある場合、制約付きオペコードを使用できます。
thisTypeが値型であり、thisTypeがメソッドを実装する場合、thisTypeによるメソッドの実装のために、ptrは呼び出しメソッド命令への「this」ポインタとして変更されずに渡されます。
これによりボクシングが回避され、値型が実装されているため、インターフェイスはメソッドを実装する必要があるため、ボクシングは発生しません。上記の例ではEquals()
、呼び出しはthis.aにはボックスで行われている1。
ほとんどの構造体は、ビット単位で同一の値が等しいと見なされるプリミティブのようなセマンティクスを持つ必要があります2。ランタイムは暗黙的にそのような動作を提供しますがEquals()
、これは遅くなる可能性があります。また、この暗黙の同等性はの実装として公開されていないIEquatable<T>
ため、構造体が明示的に実装されていない限り、構造体が辞書のキーとして簡単に使用されることはありません。多くの公共構造体のタイプは、彼らが実装することを宣言するのはそのため一般的ですIEquatable<T>
(ここで、T
彼ら自身がある)これを簡単に行うために、より良いCLR BCL内の多くの既存の値型の動作と一貫性だけでなく、実行します。
BCLのすべてのプリミティブは、少なくとも次のものを実装します。
IComparable
IConvertible
IComparable<T>
IEquatable<T>
(したがってIEquatable
)多くは実装も行ってIFormattable
おり、DateTime、TimeSpan、Guidなどのシステム定義値タイプの多くはこれらの多くまたはすべても実装しています。複素数構造体や固定幅のテキスト値など、同様に「広く役立つ」タイプを実装している場合、これらの一般的なインターフェイスの多くを(正しく)実装すると、構造体がより便利で使いやすくなります。
インターフェースを強く示唆した場合に明らかに可変性を(などICollection
)、それは修正ではなく、オリジナルよりも箱入り値に発生した場所、あなたがいずれかのエラーの種類につながる(構造体の可変を作っすでに述べたTAT意味するだろうとして、それを実装することは悪い考えです)または、のようなメソッドの影響を無視しAdd()
たり、例外をスローしたりして、ユーザーを混乱させます。
多くのインターフェースは(などのIFormattable
)可変性を意味せず、一貫した方法で特定の機能を公開する慣用的な方法として機能します。多くの場合、構造体のユーザーは、そのような動作のためにボクシングのオーバーヘッドを気にしません。
賢明に行われる場合、不変の値型では、有用なインターフェースの実装は良い考えです
1:コンパイラは、特定の構造体タイプであることがわかっているが、仮想メソッドを呼び出す必要がある変数で仮想メソッドを呼び出すときにこれを使用する場合があることに注意してください。例えば:
List<int> l = new List<int>();
foreach(var x in l)
;//no-op
リストによって返される列挙子は構造体であり、リストを列挙するときに割り当てを回避するための最適化です(いくつかの興味深い結果があります)。しかしforeachのの意味は、列挙子が実装する場合に指定することをIDisposable
、その後Dispose()
の繰り返しが完了すると呼び出されます。明らかに、これがボックス化された呼び出しを通じて発生することは、列挙子が構造体であるという利点を排除します(実際にはもっと悪いでしょう)。さらに悪いことに、dispose呼び出しが何らかの方法で列挙子の状態を変更すると、これはボックス化されたインスタンスで発生し、複雑なケースで多くの微妙なバグが発生する可能性があります。したがって、この種の状況で放出される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:System.Collections.Generic.List.get_Currentを呼び出します IL_0018:stloc.1 IL_0019:ldloca.s 02 IL_001B: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:制約付き。System.Collections.Generic.List.Enumerator IL_002E:callvirt System.IDisposable.Dispose IL_0033:nop IL_0034:最終的に
したがって、IDisposableの実装によってパフォーマンスの問題が発生することはなく、Disposeメソッドが実際に何かを実行した場合でも、列挙子の(残念な)変更可能な側面が保持されます。
2:doubleとfloatは、NaN値が等しいと見なされないこのルールの例外です。
struct
と、コンパイラの警告が表示されますinterface
。
場合によっては、構造体がインターフェースを実装するのが良いかもしれません(もしそれが役に立たなかったとしたら、.netの作成者がそれを提供したかどうかは疑わしいです)。構造体がのような読み取り専用インターフェースを実装している場合、構造体IEquatable<T>
をタイプの格納場所(変数、パラメーター、配列要素など)に格納するには、IEquatable<T>
ボックス化する必要があります(各構造体タイプは実際には2種類のものを定義します:ストレージ値型として動作するロケーション型とクラス型として動作するヒープオブジェクト型。最初の型は暗黙的に2番目の「ボクシング」に変換可能であり、2番目の型は明示的なキャストによって最初の型に変換できます。 「箱から出す」)。構造のインターフェースの実装をボックス化せずに利用することは可能ですが、いわゆる制約付きジェネリックを使用します。
たとえば、メソッドがある場合CompareTwoThings<T>(T thing1, T thing2) where T:IComparable<T>
、そのようなメソッドはthing1.Compare(thing2)
ボックスthing1
またはを使用せずに呼び出すことができますthing2
。thing1
たとえば、が発生した場合Int32
、ランタイムは、のコードを生成するときにそれを認識しますCompareTwoThings<Int32>(Int32 thing1, Int32 thing2)
。メソッドをホストしているものとパラメーターとして渡されているものの両方の正確なタイプを知っているので、どちらもボックス化する必要はありません。
インターフェイスを実装する構造体の最大の問題は、インターフェイスタイプ、、、Object
またはValueType
(独自のタイプの場所ではなく)の場所に格納される構造体がクラスオブジェクトとして動作することです。読み取り専用インターフェースの場合、これは一般に問題ではありませんが、そのような変更インターフェースのIEnumerator<T>
場合、奇妙なセマンティクスが生じる可能性があります。
たとえば、次のコードについて考えてみます。
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
マークされたステートメント#1はenumerator1
、最初の要素を読み取るために準備を整えます。その列挙子の状態はにコピーされenumerator2
ます。マークされたステートメント#2は、そのコピーを進めて2番目の要素を読み取りますが、には影響しませんenumerator1
。次に、その2番目の列挙子の状態がにコピーされenumerator3
、マークされたステートメント#3によって進められます。ので、次に、enumerator3
およびenumerator4
両方の参照型、あるREFERENCEにenumerator3
、その後にコピーされますがenumerator4
、そのマークの文が効果的に進めるの両方を enumerator3
してenumerator4
。
値型と参照型はどちらも種類であると偽ろうとする人もいますがObject
、それは実際には真実ではありません。実数値型はに変換可能ですがObject
、そのインスタンスではありません。List<String>.Enumerator
その型の場所に格納されているインスタンスは値型であり、値型として動作します。タイプの場所にコピーIEnumerator<String>
すると、参照タイプに変換され、参照タイプとして動作します。後者は一種ですObject
が、前者はそうではありません。
ところで、さらにいくつかの注意事項:(1)一般に、可変クラスタイプは、Equals
メソッドが参照の同等性をテストする必要がありますが、ボックス化された構造体がそうするための適切な方法はありません。(2)その名前にもかかわらValueType
ず、値型ではなくクラス型です。から派生したすべての型は、を除いてSystem.Enum
から派生したすべての型と同様に値型ですが、とは両方ともクラス型です。ValueType
System.Enum
ValueType
System.Enum
構造体は値型として実装され、クラスは参照型です。Foo型の変数があり、その中にFubarのインスタンスを格納すると、参照型に「ボックス化」されるため、最初に構造体を使用する利点が失われます。
クラスの代わりに構造体を使用する唯一の理由は、それが参照型ではなく値型になるためですが、構造体はクラスから継承できません。構造体にインターフェイスを継承させ、インターフェイスを渡すと、構造体の値型の性質が失われます。インターフェイスが必要な場合は、クラスにすることもできます。
(追加する主要なものは何もありませんが、まだ編集能力がないので、ここに行きます。)
完全に安全です。構造体にインターフェースを実装することに違法なことは何もありません。ただし、なぜそれを実行したいのか疑問に思う必要があります。
ただし、構造体へのインターフェイス参照を取得するとBOXになりますなります。したがって、パフォーマンスのペナルティなど。
私が今考えることができる唯一の有効なシナリオは、ここの私の投稿に示されています。コレクションに格納されている構造体の状態を変更する場合は、構造体に公開されている追加のインターフェイスを介して変更する必要があります。
Int32
ジェネリック型T:IComparable<Int32>
(メソッドのジェネリック型パラメーターまたはメソッドのクラスのいずれか)を受け入れるメソッドにを渡すと、そのメソッドは、Compare
ボックス化せずに、渡されたオブジェクトでメソッドを使用できます。
問題は、構造体が値型であるためにボクシングが発生し、パフォーマンスがわずかに低下することだと思います。
このリンクは、他の問題がある可能性があることを示唆しています...
http://blogs.msdn.com/abhinaba/archive/2005/10/05/477238.aspx
値型がインターフェースを実装する理由はほとんどありません。値型はサブクラス化できないため、いつでも具体的な型として参照できます。
もちろん、複数の構造体がすべて同じインターフェースを実装している場合を除いて、それはわずかに役立つかもしれませんが、その時点で、クラスを使用して正しく実行することをお勧めします。
もちろん、インターフェースを実装することで、構造体をボックス化するので、それはヒープ上に置かれ、値で渡すことができなくなります...これは、クラスを使用するだけでよいという私の意見を本当に補強しますこの状況では。
IComparable
値をボックス化するために渡す必要はありません。それIComparable
を実装する値型で期待するメソッドを呼び出すだけで、値型を暗黙的にボックス化できます。
IComparable<T>
に型の構造体でメソッドを呼び出すことができますT
。
構造体は、スタックに存在するクラスのようなものです。それらが「安全でない」べきである理由はわかりません。