質問

構造体が C# 経由で CLR のインターフェイスを実装することがいかに悪いかについて何かを読んだ記憶があるようですが、それについては何も見つからないようです。悪いですか?そうすることで予期せぬ結果が生じることはありますか?

public interface Foo { Bar GetBar(); }
public struct Fubar : Foo { public Bar GetBar() { return new Bar(); } }
役に立ちましたか?

解決

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

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

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

他のヒント

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

実装する 構造体のインターフェイスにはマイナスの影響はまったくありません。

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

  • 結果のオブジェクトをロック目的に使用する (いずれにしても非常に悪いアイデア)
  • 参照等価セマンティクスを使用し、それが同じ構造体の 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() または class 使用されている。
  2. この方法で使用される構造体でのボックス化を回避できるようにします。

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

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

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

低摩擦 API

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

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

List によって返される列挙子は構造体であり、リストを列挙するときに割り当てを回避するための最適化です (いくつかの興味深いものがあります) 結果)。ただし、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:  call        System.Collections.Generic.List.get_Current
IL_0018:  stloc.1     
IL_0019:  ldloca.s    02 
IL_001B:  call        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:  constrained. System.Collections.Generic.List.Enumerator
IL_002E:  callvirt    System.IDisposable.Dispose
IL_0033:  nop         
IL_0034:  endfinally  

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

2:double と float はこの規則の例外であり、NaN 値は等しいとはみなされません。

場合によっては、構造体がインターフェイスを実装するのが良い場合もあります (これが役に立たなかった場合、.net の作成者がインターフェイスを提供したとは思えません)。構造体が次のような読み取り専用インターフェイスを実装している場合 IEquatable<T>, 、型の格納場所 (変数、パラメータ、配列要素など) に構造体を格納します。 IEquatable<T> ボックス化する必要があります (各構造体の型は実際には 2 種類のものを定義します:値型として動作する記憶場所型と、クラス型として動作するヒープオブジェクト型。1 つ目は 2 つ目 (「ボックス化」) に暗黙的に変換可能であり、2 つ目は明示的なキャスト (「アンボックス化」) を介して 1 つ目に変換できます。ただし、いわゆる制約付きジェネリックを使用すると、ボックス化せずにインターフェイスの構造の実装を悪用することは可能です。

たとえば、メソッドがあるとします。 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 どちらも参照型です。 参照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 が生成されます。 それ。したがって、パフォーマンスペナルティなどです。

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

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

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

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

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

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

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

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

構造体は、スタック内に存在するクラスとまったく同じです。彼らが「危険」である理由はわかりません。

ライセンス: CC-BY-SA帰属
所属していません StackOverflow
scroll top