.NETストリーム機能-CanXXXテストは安全ですか?
-
06-07-2019 - |
質問
.NETでは、クラスの機能をテストするためにかなり一般的なパターンが使用されます。ここでは例としてStreamクラスを使用しますが、このパターンを使用するすべてのクラスに問題が適用されます。
パターンは、CanXXXというブール型プロパティを提供して、機能XXXがクラスで利用可能であることを示すことです。たとえば、Streamクラスには、Read、Write、およびSeekメソッドを呼び出すことができることを示すCanRead、CanWrite、およびCanSeekプロパティがあります。プロパティ値がfalseの場合、それぞれのメソッドを呼び出すと、NotSupportedExceptionがスローされます。
ストリームクラスに関するMSDNドキュメントから:
基礎となるデータソースまたはリポジトリに応じて、ストリームはこれらの機能の一部のみをサポートする場合があります。アプリケーションは、CanRead、CanWrite、およびCanSeekプロパティを使用して、ストリームの機能を照会できます。
CanReadプロパティのドキュメント:
派生クラスでオーバーライドされると、現在のストリームが読み取りをサポートしているかどうかを示す値を取得します。
Streamから派生したクラスが読み取りをサポートしていない場合、Read、ReadByte、およびBeginReadメソッドの呼び出しはNotSupportedExceptionをスローします。
次の行に沿って多くのコードが記述されています:
if (stream.CanRead)
{
stream.Read(…)
}
たとえば、ストリームオブジェクトを何らかの方法でロックするための同期コードはありません—他のスレッドがそれまたはそれが参照するオブジェクトにアクセスしている可能性があります。 NotSupportedExceptionをキャッチするコードもありません。
MSDNのドキュメントには、プロパティ値が時間とともに変化することはできないと記載されていません。実際、CanSeekプロパティはストリームが閉じられるとfalseに変わり、これらのプロパティの動的な性質を示しています。そのため、上記のコードスニペットでRead()を呼び出してもNotSupportedExceptionがスローされないという契約上の保証はありません。
この潜在的な問題に苦しむ多くのコードがあることを期待しています。この問題を特定した人々がどのように対処したのだろうか。ここで適切な設計パターンは何ですか?
このパターン(CanXXX、XXX()ペア)の有効性に関するコメントも歓迎します。私にとって、少なくともStreamクラスの場合、これはやりすぎているクラス/インターフェースを表し、より基本的な部分に分割する必要があります。文書化された厳しい契約がないため、テストは不可能になり、実装はさらに難しくなります!
解決
オブジェクトの内部を知らなくても、" flag"プロパティは、オブジェクトが複数のスレッドで変更されているときに依存するには揮発性が高すぎます。
この質問は、ストリームよりも読み取り専用コレクションについてよく聞かれますが、同じデザインパターンの別の例であり、同じ引数が当てはまると思います。
明確にするために、.NETのICollectionインターフェイスにはIsReadOnlyプロパティがあります。これは、コレクションがそのコンテンツを変更するメソッドをサポートするかどうかのインジケーターとして使用することを目的としています。ストリームと同様に、このプロパティはいつでも変更でき、InvalidOperationExceptionまたはNotSupportedExceptionがスローされます。
これに関する議論は通常、次のように要約されます。
- 代わりにIReadOnlyCollectionインターフェースがないのはなぜですか?
- NotSupportedExceptionは良いアイデアです。
- 「モード」を持つことの長所と短所具体的な機能とは異なります。
モードはめったに良いことではありません。複数の「セット」を処理する必要があるためです。行動の;アプリケーションが複数の「セット」を処理する必要があるため、いつでもモードを切り替えることができるものを持つことはかなり悪いです。行動も。ただし、何かをより控えめな機能に分割できるからといって、必ずしもそうする必要があるわけではありません。特に、それを分解しても、目の前のタスクの複雑さが軽減されるわけではありません。
個人的な意見では、クラスの消費者が理解できると感じるメンタルモデルに最も近いパターンを選択する必要があります。あなたが唯一の消費者である場合、最も好きなモデルを選択してください。 StreamとICollectionの場合、これらの単一の定義を持つことは、同様のシステムでの長年の開発によって構築されたメンタルモデルにはるかに近いと思います。ストリームについて話すときは、読み取り可能か書き込み可能かではなく、ファイルストリームとメモリストリームについて話します。同様に、コレクションについて話すとき、「書き込み可能性」の観点からコレクションを参照することはめったにありません。
これに関する私の経験則:「モード」を持たずに、動作をより具体的なインターフェースに分解する方法を常に探します。シンプルなメンタルモデルを補完する限り、個別の動作を個別の物と考えることが難しい場合は、モードベースのパターンを使用して、非常に明確に文書化します。
他のヒント
さて、他の回答よりも役に立つと思われる別の試みがあります...
MSDNが CanRead
/ CanWrite
/ CanSeek
が時間とともにどのように変化するかについての具体的な保証を提供していないのは残念です。ストリームが読み取り可能な場合、閉じられるまで読み取り可能であると想定するのは合理的だと思います-同じことが他のプロパティにも当てはまります
場合によっては、ストリームを後でシーク可能にすることは合理的だと思います。たとえば、基になるデータの最後に到達するまで、読み込んだすべてをバッファリングし、シークを許可する場合がありますその後、クライアントがデータを再読み取りできるようにします。ただし、アダプターがその可能性を無視することは合理的だと思います。
これは、ほとんどの病理学的症例を除いてすべてを処理する必要があります。 (大混乱を引き起こすように設計されたストリーム!)これらの要件を既存のドキュメントに追加することは、実装の99.9%が既にそれに従うと疑っていますが、理論的には重大な変更です。それでも、接続で提案する価値があるかもしれません。
今、「機能ベース」を使用するかどうかの議論については、 API( Stream
など)およびインターフェイスベースのAPI ...私が見る根本的な問題は、.NETが、変数の実装への参照でなければならないことを指定する機能を提供しないことです。複数のインターフェース。たとえば、次のように書くことはできません。
public static Foo ReadFoo(IReadable & ISeekable stream)
{
}
これを許可した場合は 、それは合理的かもしれません-しかし、それなしでは、潜在的なインターフェースが爆発的に増えてしまいます:
IReadable
IWritable
ISeekable
IReadWritable
IReadSeekable
IWriteSeekable
IReadWriteSeekable
それは現在の状況よりも厄介だと思います-に加えて、 IReadable
と IWritable
のアイデアだけをサポートするだろうと思いますが既存の Stream
クラス。これにより、クライアントは必要なものを宣言的に表現しやすくなります。
コードコントラクトを使用すると、APIが提供するものと何を宣言できるか 確かに必要です:
public Stream OpenForReading(string name)
{
Contract.Ensures(Contract.Result<Stream>().CanRead);
...
}
public void ReadFrom(Stream stream)
{
Contract.Requires(stream.CanRead);
...
}
静的チェッカーがそれをどれだけ助けることができるか、またはストリームが閉じられたときにストリームが do 判読不能/書き込み不能になるという事実にどのように対処するかわかりません。
stream.CanReadは、基になるストリームに読み取りの可能性があるかどうかを確認するだけです。実際の読み取りが可能かどうかについては何も言いません(例:ディスクエラー)。
すべてが読み取りをサポートしているため、* Readerクラスのいずれかを使用した場合、NotImplementedExceptionをキャッチする必要はありません。 * WriterのみがCanRead = Falseを持ち、その例外をスローします。ストリームが読み取りをサポートしていることを知っている場合(StreamReaderを使用した場合など)、私見では追加のチェックを行う必要はありません。
読み取り中にエラーが発生すると例外が発生するため、例外をキャッチする必要があります(ディスクエラーなど)。
また、スレッドセーフとして文書化されていないコードはスレッドセーフではないことに注意してください。通常、静的メンバーはスレッドセーフですが、インスタンスメンバーはそうではありません-ただし、各クラスのドキュメントを確認する必要があります。
あなたの質問とそれに続くすべての解説から、あなたの問題は明快さと「正確さ」にあると推測しています。指定された契約の。指定された契約は、MSDNオンラインドキュメントに記載されているものです。
あなたが指摘したのは、契約書に前提条件を設けることを強制する文書に欠けているものがあるということです。より具体的には、ストリームの可読性プロパティのボラティリティについては何も述べられていないため、できるのは、 NotSupportedException
が可能であるという仮定だけです対応するCanReadプロパティの値が数ミリ秒(またはそれ以上)前であったものに関係なく、スローされます。
この場合、このインターフェースの意図に進む必要があると思います:
- 複数のスレッドを使用する場合、すべてのベットはオフになります。
- インターフェイスで何かを呼び出してストリームの状態を変更する可能性があるまで、
CanRead
の値は不変であると安全に想定できます。
上記にかかわらず、Read *メソッドは NotSupportedException をスローする可能性があります 。
同じ引数を他のすべてのCan *プロパティに適用できます。
このパターン(CanXXX、XXX()のペア)の有効性に関するコメントも歓迎します。
このパターンのインスタンスが表示された場合、通常は次のようになります。
-
パラメータのない
CanXXX
メンバーは、&#8230; を除き、常に同じ値を返します。
-
&#8230;
CanXXXChanged
イベントが存在する場合、パラメータなしのCanXXX
が異なる値を返す場合がありますそのイベントの発生の前後。ただし、イベントをトリガーしない限り変更されません。 -
パラメーター化された
CanXXX(&#8230;)
メンバーは、引数ごとに異なる値を返す場合があります。ただし、同じ引数の場合、同じ値を返す可能性があります。つまり、CanXXX(constValue)
は一定のままになる可能性があります。ここで注意しているのは、
stream.CanWriteToDisk(largeConstObject)
がtrue
を返す場合、常に> true
将来的には?おそらくそうではないので、パラメータ化されたCanXXX(&#8230;)
が同じ引数に対して同じ値を返すかどうかは、おそらくコンテキストに依存します。 -
XXX(&#8230;)
への呼び出しは、CanXXX
がtrue
。
そうは言っても、 Stream
がこのパターンを使用することには多少問題があることに同意します。少なくとも理論的には、実際にはそれほどではないとしても。
これは実際的な問題よりも理論的な問題のように聞こえます。ストリームが閉じられたため以外に、その他の読み取り/書き込みが不可能になる状況については本当に考えられません。
コーナーケースもあるかもしれませんが、それらが頻繁に表示されるとは思わないでしょう。大部分のコードがこれについて心配する必要はないと思います。
しかし、それは興味深い哲学的問題です。
編集:CanReadなどが有用であるかどうかの質問に対処しますが、それらはまだ有用だと思います-主に引数の検証のためです。たとえば、ある時点で読み取りたいストリームをメソッドが取得するからといって、メソッドの開始時にすぐに読み取りたいというわけではありませんが、それが引数の検証が理想的に実行される場所です。これは、パラメーターがnullであるかどうかを確認し、最初に間接参照したときに NullReferenceException
がスローされるのを待つのではなく、 ArgumentNullException
をスローすることと実際に違いはありません。
また、 CanSeek
はわずかに異なります:場合によっては、コードはシーク可能なストリームとシークできないストリームの両方にうまく対応できる場合がありますが、シーク可能な場合はより効率的です。
これは、「シーク可能性」に依存しています。などは一貫したままです-しかし、私が言ったように、それは実際の生活の中で本当のようです。
さて、これを別の方法で試してみましょう...
メモリ内で読み取り/シークを行い、十分なデータがあることを既に確認している場合、または事前に割り当てられたバッファ内に書き込みを行っている場合を除き、常に問題が発生する可能性があります 。ディスクが故障または一杯になり、ネットワークが崩壊するなど。これらのことは実際に発生します。したがって、障害を乗り切る方法でコーディングする必要があります(または、問題が発生しない場合は意識して無視することを選択します)。本当に重要です。)
コードがディスク障害の場合に正しいことを行うことができる場合、書き込み可能から書き込み不可に変わる FileStream
を生き残ることができる可能性があります。
Stream
にしっかりした契約がある場合、それらは信じられないほど弱くなければなりません-コードが常に機能することを証明するために静的チェックを使用することはできません。あなたができる最善のことは、失敗に直面して正しいことをしたことを証明することです。
Stream
はすぐに変更されるとは思わない。私はそれがより良く文書化される可能性があることを確かに受け入れますが、それが「完全に壊れている」という考えを受け入れません。実際に実際に使用できなかった場合は、もっと壊れます...そして、現在よりも壊れやすい場合は、論理的に完全ではありません。
フレームワークには、日付/時刻APIの状態が比較的悪いなど、はるかに大きな問題があります。彼らは最後の数バージョンで多く良くなりましたが、(たとえば) Joda Time 。組み込みの不変コレクションの欠如、言語の不変性に対する不十分なサポートなど-これらは本当の問題であり、実際の頭痛の種です。 Stream
に何年も費やすよりも、むしろ対処していると思います。これは、実生活ではほとんど問題を引き起こさない、やや手に負えない理論上の問題のようです。