105

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

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

9 に答える 9

198

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

構造体にインターフェイスを実装しても、悪影響はまったくありません。

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

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

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

ジェネリック

おそらく、インターフェースを実装する構造体の多くの合理的な理由は、それらが制約付きの汎用コンテキスト内で使用できるようにするためです。このように変数を使用すると、次のようになります。

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()orのような他の制約classが使用されていない限り。
  2. このように使用される構造体のボクシングを回避できるようにします。

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

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

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

低摩擦 API

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

BCL のすべてのプリミティブは、少なくとも次のように実装されています。

  • IComparable
  • IConvertible
  • IComparable<T>
  • IEquatable<T>(したがってIEquatable)

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

除外

明らかに、インターフェイスが可変性を強く暗示している場合(などICollection)、それを実装するのは悪い考えです。これは、構造体を可変にしたことを意味するためです (元の値ではなくボックス化された値で変更が発生した場合に、既に説明した種類のエラーが発生します)。 ) または、メソッドの意味を無視し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: いいえ         
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: いいえ         
IL_0034: endfinally  

したがって、IDisposable の実装によってパフォーマンスの問題が発生することはなく、Dispose メソッドが実際に何かを行う場合でも、(残念ながら) 列挙子の変更可能な側面が保持されます。

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

于 2009-08-17T18:17:03.553 に答える
52

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

構造体がインターフェースを実装することは可能ですが、キャスト、可変性、およびパフォーマンスに伴う懸念があります。詳細については、次の投稿を参照してください:https ://docs.microsoft.com/en-us/archive/blogs/abhinaba/c-structs-and-interface

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

于 2008-09-15T15:09:21.780 に答える
8

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

たとえば、メソッドがあった場合、そのようなメソッドは、またはをボックス化することなくCompareTwoThings<T>(T thing1, T thing2) where T:IComparable<T>呼び出すことができます。が. _ _ メソッドをホストするモノとパラメーターとして渡されるモノの両方の正確な型を認識できるため、どちらもボックス化する必要はありません。thing1.Compare(thing2)thing1thing2thing1Int32CompareTwoThings<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はどちらも参照型であるため、へのREFERENCEenumerator3は にコピーされるenumerator4ため、マークされたステートメントは との両方 enumerator3を効果的に進めenumerator4ます。

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

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

于 2013-01-14T16:34:31.010 に答える
3

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

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

于 2008-09-15T15:06:24.330 に答える
3

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

ただし、構造体へのインターフェイス参照を取得すると、それが BOXになります。そのため、パフォーマンスのペナルティなどがあります。

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

于 2008-09-15T15:14:58.317 に答える
1

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

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

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

于 2008-09-15T15:04:12.913 に答える
1

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

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

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

于 2008-09-15T15:04:52.553 に答える
0

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

于 2008-09-15T15:04:05.973 に答える
-10

構造体は、スタックに存在するクラスのようなものです。それらが「安全でない」べきである理由はわかりません。

于 2008-09-15T15:03:54.490 に答える