constなしで生きる方法は?

多くの場合、オブジェクトをメソッドに渡すときに、「ここでは、このオブジェクトを保持しますが、変更する権利はありません」と伝えたいと思います。 利点は明らかです。コードの信頼性が高くなるだけでなく、コードも読みやすくなります。 関心のあるオブジェクトがどのように、どこで変化するかを追跡するために、各メソッドの実装に進む必要はありません。 さらに、渡された引数の不変性がメソッドのシグネチャで示されている場合は、シグネチャ自体によって、ある程度の正確さで、実際に何を行うかをすでに想定できます。 別のプラスは、スレッドセーフです。 オブジェクトが読み取り専用であることを知っています。

C / C ++では、これらの目的のためにconstキーワードが存在します。 多くの人は、そのようなメカニズムはあまりにも信頼性が低いと言いますが、C#ではそのようなメカニズムはありません。 そして、多分それは将来のバージョンに現れるでしょう(開発者はこれを否定しません)が、今はどうですか?





1.不変オブジェクト


C#で最も有名な類似オブジェクトは文字列です。 オブジェクト自体の変更につながる単一のメソッドはありませんが、新しいメソッドの作成のみになります。 そして、パフォーマンスを思い出すまで、それらのすべてが素晴らしく美しいようです(使いやすく信頼性があります)。 たとえば、文字の配列全体をコピーせずに部分文字列を見つけることができますが、たとえば、文字列内の文字を置き換える必要がある場合はどうでしょうか? しかし、このような文字列の数千の配列を処理する必要がある場合はどうでしょうか? いずれの場合も、新しい行オブジェクトが作成され、配列全体がコピーされます。 古い行はもう必要ありませんが、行自体はこれについて何も知らず、データのコピーを続けます。 メソッドを呼び出す開発者のみが、オブジェクト引数を変更する権利を与える場合と与えない場合があります。 さらに、不変オブジェクトの使用はメソッドのシグネチャに反映されません。 私たちはどうですか?



2.インターフェース


1つのオプションは、オブジェクトを変更するすべてのメソッドを除外する読み取り専用オブジェクトのインターフェイスを作成することです。 また、このオブジェクトが汎用の場合、共分散もインターフェイスに追加できます。 ベクトルを使用した例では、次のようになります。



interface IVectorConst<out T> { T this[int nIndex] { get; } } class Vector<T> : IVectorConst<T> { private readonly T[] _vector; public Vector(int nSize) { _vector = new T[nSize]; } public T this[int nIndex] { get { return _vector[nIndex]; } set { _vector[nIndex] = value; } } } void ReadVector(IVectorConst<int> vector) { ... }
      
      







(ところで、VectorとIVectorConst(またはIVectorReader-好きなように)の間に、反変のIVectorWriterを追加することもできます。)



そして、すべては問題ありませんが、ReadVectorがVectorへのダウンキャストを行い、変更するのを止めるものは何もありません。 ただし、C ++からconstをリコールする場合、このメソッドは、ポインター変換を禁止しない同様に信頼性の低いconstよりも信頼性が高くなります。 これで十分な場合は停止できますが、そうでない場合は先に進みます。



3.「アダプター」による定数オブジェクトの分離


前述のダウンキャストを禁止する方法は1つだけです。VectorがIVectorConstを継承しないようにする、つまり分離するようにしてください。 これを行う1つの方法は、VectorConst“ Adapter”を作成することです(この方法を思い出してくれたosmirnovに感謝します。これがないと記事は不完全になります)。 次のようになります。



 interface IVector<in T> { T this[int nIndex] { set; } } interface IVectorConst<out T> { T this[int nIndex] { get; } } class VectorConst<T> : IVectorConst<T> { private readonly Vector<T> _vector; public VectorConst(Vector<T> vector) { _vector = vector; } public T this[int nIndex] { get { return _vector[nIndex]; } } } class Vector<T> : IVector<T> { private readonly T[] _vector; public Vector(int nSize) { _vector = new T[nSize]; } public T this[int nIndex] { get { return _vector[nIndex]; } set { _vector[nIndex] = value; } } public IVectorConst<T> AsConst { get { return new VectorConst<T>(this); } } }
      
      







ご覧のとおり、定数オブジェクト(VectorConst)はメインのオブジェクトから完全に分離されており、ダウンキャストはできません。 誰かにそれを与えると、私たちのベクトルが変わらないことを確信して、私たちは平和に眠ることができます。



VectorConstには独自の実装が含まれておらず(すべてがベクター内にあります)、単にベクターインスタンスにリダイレクトします。 しかし、ここでのすべてが私たちが望むほどスムーズではありません... VectorConstを呼び出すとき、すでに1つの呼び出しではなく、2つの呼び出しがあります。 パフォーマンス。 さらに、メインオブジェクトのインターフェイスを変更するたびに、IVectorConstおよびVectorConstでメソッドを追加/編集する必要があります。新しい適切なメソッドが表示されても、コンパイラーはこれを強制しません。これを覚えておく必要があり、これは余分な頭痛です。 しかし、これらの不利な点がそれほど重大でない場合、このアプローチはおそらく最適です。 特に、メインオブジェクトが既に書き込まれており、それを書き換えることが不可能または不適切な場合。



4.定数オブジェクトの実際の分離


ベクトルを使用した同じ例では、次のようになります。



 interface IVectorConst<out T> { T this[int nIndex] { get; } } interface IVector<in T> { T this[int nIndex] { set; } } struct VectorConst<T> : IVectorConst<T> { private readonly T[] _vector; public VectorConst(T[] vector) { _vector = vector; } public T this[int nIndex] { get { return _vector[nIndex]; } } } struct Vector<T> : IVector<T> { private readonly T[] _vector; private readonly VectorConst<T> _reader; public Vector(int nSize) { _reader = new VectorConst<T>(_vector = new T[nSize]); } public T this[int nIndex] { set { _vector[nIndex] = value; } } public VectorConst<T> Reader { get { return _reader; } } public static implicit operator VectorConst<T>(Vector<T> vector) { return vector._reader; } }
      
      







これでVectorConstは分離されるだけでなく、ベクター自体の実装が2つの部分に分割されます。 私たちがt.zで支払わなければならなかったすべて。 パフォーマンスとは、_vectorへのリンクとメモリ内の追加リンクをコピーすることによるVectorConst構造体の初期化です。 VectorConstがメソッドに渡されると、プロパティの呼び出しと同じコピーが作成されます。 したがって、パフォーマンスの点では、これはT []のインスタンスをメソッドに渡すこととほぼ同じですが、変更からの保護(達成された)があると言えます。 そして、VectorConstを受け入れるメソッドにVectorインスタンスを渡すときに、明示的にReaderプロパティを再度呼び出さないように、Vectorに変換演算子を追加できます。



 public static implicit operator VectorConst<T>(Vector<T> vector) { return vector._reader; }
      
      







ただし、オブジェクトを直接使用する場合、Readerプロパティを呼び出さずにはできません。



 var v = new Vector<int>(5); v[0] = 0; Console.WriteLine(v.Reader[0]);
      
      







また、IVectorConstの共分散を使用する必要がある場合(変換演算子が存在するにもかかわらず)、これなしではできません。



 class A { } class B : A { } private static void ReadVector(IVectorConst<A> vector) { ... } var vector = new Vector<B>(); ReadVector(vector.Reader);
      
      







そして、これがこのアプローチの主な、そしておそらく唯一のマイナスです:場合によってはReaderを呼び出す必要があるため、その使用はやや珍しいです。 しかし、C#には引数のconstがありませんが、いずれにしても、何かを犠牲にしなければなりません。



多くはおそらくこれらすべてが共通の真実であると言うでしょう。 しかし、この記事とこれらのシンプルなテンプレートは、誰かにとって役に立つかもしれません。



All Articles