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


93

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

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

回答:


45

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

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

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


3
「ボックス化の結果、構造体の内部状態を変更する操作が正しく動作しない可能性があります。」例を挙げて答えを見つけてください。

2
@ウィル:コメントで何を指しているのかわからない。私が参照したブログ投稿には、構造体でインターフェイスメソッドを呼び出しても実際には内部値が変更されないことを示す例があります。
スコットドーマン

12
@ScottDorman:場合によっては、構造体にインターフェースを実装させるとボクシングを回避できる場合があります。首相の例ですIComparable<T>IEquatable<T>。構造体Fooを型の変数に格納するにIComparable<Foo>はボックス化が必要ですが、ジェネリック型TIComparable<T>1つに制約されている場合はT、どちらかをボックス化する必要がなくT、制約を実装すること以外は何も知らなくても、構造体を別の型と比較できます。このような有利な動作は、構造体がインターフェースを実装する能力によってのみ可能になります。...言われたこと
supercat

3
...特定のインターフェイスがボックス化されていない構造にのみ適用可能であると見なされるべきであると宣言する手段があれば、クラスオブジェクトまたはボックス化された構造が目的のオブジェクトを持つことができないコンテキストがあるため、それは良かったかもしれません。行動。
スーパーキャット2013

2
「構造体は、値型のセマンティクスを持つオブジェクトに使用する必要があります。...構造体の内部状態を変更する操作は正しく動作しない可能性があります。」値型のセマンティクスと可変性がうまく混ざっていないという事実が本当の問題ではありませんか?
jpmc26 2017

184

他の誰もこの答えを明示的に提供しなかったので、私は以下を追加します:

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

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

  • 結果のオブジェクトをロックの目的で使用する(とにかく非常に悪い考え)
  • 参照等式セマンティクスを使用し、同じ構造体からの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がメソッドを実装する場合、thisTypeによるメソッドの実装のために、ptrは呼び出しメソッド命令への「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)、それは修正ではなく、オリジナルよりも箱入り値に発生した場所、あなたがいずれかのエラーの種類につながる(構造体の可変を作っすでに述べた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値が等しいと見なされないこのルールの例外です。


1
サイトegheadcafe.comは移動しましたが、そのコンテンツを保持することでうまくいきませんでした。試しましたが、opの知識が不足しているeggheadcafe.com/software/aspnet/31702392/…の元のドキュメントが見つかりません。(優れた要約についてはPS +1)。
アベル

2
これは素晴らしい答えですが、「概要」を「TL; DR」として一番上に移動することで改善できると思います。最初に結論を出すことは、読者があなたが物事をどこに向かっているのかを知るのに役立ちます。
ハンス

をにキャストするstructと、コンパイラの警告が表示されますinterface
ジャラル

8

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

たとえば、メソッドがある場合CompareTwoThings<T>(T thing1, T thing2) where T:IComparable<T>、そのようなメソッドはthing1.Compare(thing2)ボックスthing1またはを使用せずに呼び出すことができますthing2thing1たとえば、が発生した場合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両方の参照型、あるREFERENCEenumerator3、その後にコピーされますがenumerator4、そのマークの文が効果的に進めるの両方を enumerator3してenumerator4

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

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


3

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

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


インターフェイスを実装するプリミティブでもこのように機能しますか?
aoetalks

3

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

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

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


Int32ジェネリック型T:IComparable<Int32>(メソッドのジェネリック型パラメーターまたはメソッドのクラスのいずれか)を受け入れるメソッドにを渡すと、そのメソッドは、Compareボックス化せずに、渡されたオブジェクトでメソッドを使用できます。
スーパーキャット2013年


0

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


0

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

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

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


具体的な実装の代わりに、どのくらいの頻度でIComparableを渡しますか?
FlySwat 2008

IComparable値をボックス化するために渡す必要はありません。それIComparableを実装する値型で期待するメソッドを呼び出すだけで、値型を暗黙的にボックス化できます。
Andrew Hare

1
@AndrewHare:制約付きジェネリックスを使用すると、ボックス化せずIComparable<T>に型の構造体でメソッドを呼び出すことができますT
スーパーキャット2013年

-10

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


彼らが継承を欠いていることを除いて。
FlySwat 2008

7
私はこの答えのすべての部分に反対しなければなりません。それら必ずしもスタック上に存在するとは限らず、コピーセマンティクスはクラスとは大きく異なります。
MarcGravell

1
それらは不変であり、構造体の過度の使用はあなたの記憶を悲しませます:(
Teoman shipahi 2014

1
@Teomanshipahiクラスインスタンスを過度に使用すると、ガベージコレクターが狂ってしまいます。
IllidanS4は

4
20k以上の担当者がいる人にとって、この答えは受け入れられません。
クリシック2016年
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.