web-dev-qa-db-ja.com

構造体がインターフェイスを実装するのは安全ですか?

構造体がC#を介してCLRにインターフェイスを実装するのがいかに悪いかについて読んだことを覚えているようですが、それについて何も見つけられないようです。悪いですか?そうすることの意図しない結果はありますか?

public interface Foo { Bar GetBar(); }
public struct Fubar : Foo { public Bar GetBar() { return new Bar(); } }
83
Will

この質問にはいくつかのことが起こっています...

構造体がインターフェイスを実装することは可能ですが、キャスト、可変性、およびパフォーマンスに関連する懸念があります。詳細については、この投稿を参照してください: http://blogs.msdn.com/abhinaba/archive/2005/10/05/477238.aspx

一般に、構造体は値型のセマンティクスを持つオブジェクトに使用する必要があります。構造体にインターフェイスを実装することにより、構造体とインターフェイスの間で構造体が前後にキャストされるため、ボクシングの問題に直面する可能性があります。ボクシングの結果として、構造体の内部状態を変更する操作が適切に動作しない場合があります。

45
Scott Dorman

他の誰もこの回答を明示的に提供していないので、次を追加します。

実装構造体のインターフェイスは、マイナスの影響は一切ありません。

構造体を保持するために使用されるインターフェイスタイプの変数は、その構造体のボックス化された値が使用される結果になります。構造体が不変(良いこと)である場合、以下の場合を除き、これは最悪の場合パフォーマンスの問題です。

  • 結果のオブジェクトをロック目的に使用する(とにかく非常に悪い考え)
  • 参照等価セマンティクスを使用し、同じ構造体からの2つのボックス化された値に対して機能することを期待します。

これらの両方が発生する可能性は低く、代わりに次のいずれかを実行する可能性があります。

ジェネリック

おそらく、インターフェイスを実装する構造体の合理的な理由の多くは、constraints を指定したgenericコンテキスト内で使用できるようにするためです。。この方法で変数を使用すると、次のようになります。

_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. 型パラメーターとして構造体の使用を有効にします
    • new()classなどの他の制約が使用されていない限り。
  2. この方法で使用される構造体でのボクシングの回避を許可します。

この場合、this.aはインターフェイス参照ではないため、そこに配置されたもののボックスは作成されません。さらに、c#コンパイラがジェネリッククラスをコンパイルし、TypeパラメーターTのインスタンスで定義されたインスタンスメソッドの呼び出しを挿入する必要がある場合、 constrained opcodeを使用できます。

ThisTypeが値型で、thisTypeがメソッドを実装する場合、thisTypeによるメソッドの実装のために、ptrが、メソッドの呼び出し命令への「this」ポインターとして変更されずに渡されます。

これにより、ボクシングが回避され、値の型がインターフェイスを実装しているため、mustメソッドが実装されるため、ボクシングは発生しません。上記の例では、Equals()呼び出しはthis.aのボックスなしで実行されます。1

低摩擦API

ほとんどの構造体は、ビット単位の同一の値が等しいと見なされるプリミティブのようなセマンティクスを持つ必要があります2。ランタイムは、暗黙のEquals()でそのような動作を提供しますが、これは遅くなる可能性があります。また、この暗黙的な等価性はnot _IEquatable<T>_の実装として公開されているため、構造体が明示的に実装されない限り、辞書のキーとして簡単に使用できなくなります。したがって、多くの既存の値型の動作との整合性を保つために、多くのパブリック構造体型が_IEquatable<T>_(Tは自己)を実装することを宣言するのが一般的です。 CLR BCL内。

BCLのすべてのプリミティブは、少なくとも以下を実装します。

  • IComparable
  • IConvertible
  • _IComparable<T>_
  • _IEquatable<T>_(したがってIEquatable

多くはIFormattableも実装しています。さらに、DateTime、TimeSpan、Guidなどのシステム定義の値タイプの多くは、これらの多くまたはすべてを実装しています。複素数構造体や固定幅のテキスト値のような同様に「広く役立つ」型を実装している場合、これらの一般的なインターフェイスの多くを(正しく)実装すると、構造体がより便利で使いやすくなります。

除外

明らかに、インターフェイスがmutabilityICollectionなど)を強く示唆している場合、それを実装することは悪い考えです。変更は元の値ではなくボックス化された値で既に行われています)、またはAdd()などのメソッドの意味を無視するか、例外をスローすることでユーザーを混乱させます。

多くのインターフェイスは、可変性(IFormattableなど)を意味せず、特定の機能を一貫した方法で公開する慣用的な方法として機能します。多くの場合、構造体のユーザーは、そのような動作のオーバーヘッドを気にしません。

概要

賢明に行われる場合、不変の値型では、有用なインターフェースの実装は良い考えです


ノート:

1:特定の構造体型であるが、仮想メソッドを呼び出す必要があるknownの変数で仮想メソッドを呼び出すときに、コンパイラがこれを使用する場合があることに注意してください。例えば:

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

リストによって返される列挙子は構造体であり、リストを列挙するときに割り当てを回避するための最適化です(興味深い consequences を使用)。ただし、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:endfinally 

したがって、IDisposableの実装はパフォーマンスの問題を引き起こさず、Disposeメソッドが実際に何らかの処理を行った場合、列挙子の(残念な)変更可能な側面が保持されます!

2:doubleとfloatは、NaN値が等しいと見なされないこのルールの例外です。

161
ShuggyCoUk

場合によっては、構造体がインターフェイスを実装するとよい場合があります(役に立たなかった場合、.netの作成者が提供してくれたのではないかと疑います)。構造体がIEquatable<T>などの読み取り専用インターフェイスを実装する場合、IEquatable<T>型の格納場所(変数、パラメーター、配列要素など)に構造体を格納するには、ボックス化する必要があります(各構造体タイプは実際に2種類を定義します:値型として振る舞う保管場所型とクラス型として振る舞うヒープオブジェクト型;最初のものは暗黙的に2番目に変換できます-「ボクシング」-そして2番目は明示的なキャストによって最初に変換できます-「ボックス化解除」)。ただし、制約付きジェネリックと呼ばれるものを使用して、ボクシングなしで構造のインターフェイスの実装を悪用することは可能です。

たとえば、メソッドCompareTwoThings<T>(T thing1, T thing2) where T:IComparable<T>がある場合、そのようなメソッドはthing1またはthing2をボックス化することなくthing1.Compare(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によって進められます。次に、enumerator3enumerator4が両方とも参照型であるため、[〜#〜] reference [〜#〜] to enumerator3enumerator4にコピーされるため、マークされたステートメントは効果的に進みます両方enumerator3およびenumerator4

一部の人々は、値型と参照型の両方がObjectの種類であるふりをしようとしますが、実際はそうではありません。実数値型はObjectに変換できますが、そのインスタンスではありません。その型の場所に格納されているList<String>.Enumeratorのインスタンスは値型であり、値型として動作します。タイプIEnumerator<String>の場所にコピーすると、参照タイプに変換され、参照タイプとして動作します。後者はObjectの一種ですが、前者はそうではありません。

ところで、さらにいくつかのメモ:(1)一般に、可変クラス型はEqualsメソッドで参照の等価性をテストする必要がありますが、ボックス化された構造体がそうする適切な方法はありません。 (2)名前にもかかわらず、ValueTypeはクラス型であり、値型ではありません。 System.Enumから派生したすべての型は、System.Enumを除くValueTypeから派生したすべての型と同様に、値型ですが、ValueTypeSystem.Enumは両方ともクラス型です。

8
supercat

(追加する大きなものは何もありませんでしたが、編集能力はまだないので、ここに行きます。)
完全に安全。構造体にインターフェイスを実装することは違法ではありません。しかし、なぜそれをしたいのか疑問に思うべきです。

ただし、構造体へのインターフェイス参照を取得するとBOXになります。パフォーマンスの低下など。

私が今考えることができる唯一の有効なシナリオは、 ここの私の投稿に示されている です。コレクションに保存されている構造体の状態を変更する場合は、構造体で公開されている追加のインターフェイスを介して行う必要があります。

3
Gishu

構造体は値型として実装され、クラスは参照型です。 Foo型の変数があり、その中にFubarのインスタンスを格納すると、参照型に「ボックス化」されるため、最初に構造体を使用する利点が無効になります。

クラスの代わりに構造体を使用する唯一の理由は、参照型ではなく値型になりますが、構造体はクラスから継承できないためです。構造体がインターフェイスを継承している場合、インターフェイスをやり取りすると、構造体の値型の性質が失われます。インターフェイスが必要な場合は、クラスにすることもできます。

3
dotnetengineer

問題は、構造体が値型であるためにボクシングが発生し、パフォーマンスがわずかに低下することだと思います。

このリンクは、他の問題がある可能性を示唆しています...

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

1
Simon Keep

インターフェースを実装する構造体への影響はありません。たとえば、組み込みのシステム構造体は、IComparableIFormattableなどのインターフェイスを実装します。

0
Joseph Daigle

値型がインターフェースを実装する理由はほとんどありません。値型をサブクラス化することはできないため、常にその具体型として参照できます。

もちろん、同じインターフェースをすべて実装する複数の構造体がない限り、それはわずかに役立つかもしれませんが、その時点でクラスを使用して正しく実行することをお勧めします。

もちろん、インターフェイスを実装することで、構造体をボックス化するため、ヒープ上に配置され、値で渡すことができなくなります...これは、クラスを使用するだけでよいという私の意見を実際に補強しますこの状況で。

0
FlySwat