web-dev-qa-db-ja.com

不変型の欠点は何ですか?

クラスのインスタンスの変更が予期されていないときに、私はますます不変タイプを使用しているように見えます。より多くの作業が必要ですが(下記の例を参照)、マルチスレッド環境での型の使用が容易になります。

同時に、他のアプリケーションで不変タイプを目にすることはめったにありません。たとえ、可変性が誰にとってもメリットがないとしてもです。

質問:不変タイプが他のアプリケーションでほとんど使用されないのはなぜですか?

  • これは、不変タイプのコードを書くのが長くなるためです。
  • または、何かが欠けていて、不変の型を使用するときにいくつかの重要な欠点がありますか?

実際の例

そのようなRESTful APIからWeatherを取得するとします。

public Weather FindWeather(string city)
{
    // TODO: Load the JSON response from the RESTful API and translate it into an instance
    // of the Weather class.
}

一般的に表示されるのは次のとおりです(コードを短縮するために新しい行とコメントが削除されています)。

public sealed class Weather
{
    public City CorrespondingCity { get; set; }
    public SkyState Sky { get; set; } // Example: SkyState.Clouds, SkyState.HeavySnow, etc.
    public int PrecipitationRisk { get; set; }
    public int Temperature { get; set; }
}

一方、APIからWeatherを取得し、それを変更することは奇妙なことです。つまり、TemperatureまたはSkyを変更しても、現実の世界で天気を変えるのも、CorrespondingCityを変えるのも意味がありません。

public sealed class Weather
{
    private readonly City correspondingCity;
    private readonly SkyState sky;
    private readonly int precipitationRisk;
    private readonly int temperature;

    public Weather(City correspondingCity, SkyState sky, int precipitationRisk,
        int temperature)
    {
        this.correspondingCity = correspondingCity;
        this.sky = sky;
        this.precipitationRisk = precipitationRisk;
        this.temperature = temperature;
    }

    public City CorrespondingCity { get { return this.correspondingCity; } }
    public SkyState Sky { get { return this.sky; } }
    public int PrecipitationRisk { get { return this.precipitationRisk; } }
    public int Temperature { get { return this.temperature; } }
}
12

私はC#とObjective-Cでプログラミングします。私は不変の型付けが本当に好きですが、実際には、次の理由により、主にデータ型の使用を制限せざるを得ませんでした。

  1. 実装労力可変型との比較。不変型の場合、すべてのプロパティの引数を必要とするコンストラクターが必要になります。あなたの例は良いものです。 10個のクラスがあり、それぞれに5〜10個のプロパティがあると想像してみてください。物事をより簡単にするために、C#のStringBuilderまたはUriBuilderや、WeatherBuilder場合。私が設計する多くのクラスはそのような努力に値しないので、これが私にとっての主な理由です。
  2. 消費者の使いやすさ。不変タイプは、可変タイプに比べて使用が困難です。インスタンス化では、すべての値を初期化する必要があります。不変性は、ビルダーを使用せずにインスタンスをメソッドに渡してその値を変更できないことも意味します。ビルダーが必要な場合、欠点はmy(1)にあります。
  3. 言語のフレームワークとの互換性。多くのデータフレームワークは、動作するために変更可能な型とデフォルトのコンストラクタを必要とします。たとえば、不変の型でネストされたLINQ-to-SQLクエリを実行することはできません。また、WindowsフォームのTextBoxなどのエディターで編集するプロパティをバインドすることもできません。

つまり、不変性は、値のように動作する、またはプロパティが少数しかないオブジェクトに適しています。不変にする前に、クラスを不変にした後、必要な労力とクラス自体の使いやすさを考慮する必要があります。

16
tia

一般に、不変性を中心にしない言語で作成された不変タイプは、作成に多くの開発時間を費やす傾向があり、必要な変更を表現するためにオブジェクトの「ビルダー」タイプが必要な場合に使用する可能性があります(これは、作業はもっと多くなりますが、これらの場合は事前にコストがかかります)。また、この言語によって不変の型を本当に簡単に作成できるかどうかに関係なく、重要なデータ型には常に処理とメモリのオーバーヘッドが必要になる傾向があります。

副作用のない関数を作成する

不変性を中心に展開しない言語で作業している場合、実用的なアプローチは、すべてのデータ型を不変にすることではありません。同じ利点の多くをもたらす潜在的にはるかに生産性の高い考え方は、ゼロの副作用を引き起こすシステム内の関数の数を最大化することに焦点を当てることです。

簡単な例として、次のような副作用を引き起こす関数があるとします。

// Make 'x' the absolute value of itself.
void make_abs(int& x);

次に、初期化後の代入などの演算子がその関数に副作用を回避させるのを禁止する不変の整数データ型は必要ありません。私たちはこれを簡単に行うことができます:

// Returns the absolute value of 'x'.
int abs(int x);

これで、関数はxやその範囲外の何かに干渉しなくなりました。この些細なケースでは、インダイレクション/エイリアスに関連するオーバーヘッドを回避することで、いくつかのサイクルを削った可能性もあります。少なくとも、2番目のバージョンは、最初のバージョンよりも計算コストが高くないはずです。

完全にコピーするには高価なもの

もちろん、関数が副作用を引き起こさないようにしたい場合、ほとんどの場合、これは簡単なことではありません。複雑な実際の使用例は、次のようになる可能性があります。

// Transforms the vertices of the specified mesh by
// the specified transformation matrix.
void transform(Mesh& mesh, Matrix4f matrix);

この時点で、メッシュには数百メガバイトのメモリが必要で、10万を超えるポリゴン、さらに多くの頂点とエッジ、複数のテクスチャマップ、モーフターゲットなどが必要になる可能性があります。これを行うためだけにメッシュ全体をコピーするのは非常にコストがかかります。 transform関数は次のように副作用がありません。

// Returns a new version of the mesh whose vertices been 
// transformed by the specified transformation matrix.
Mesh transform(Mesh mesh, Matrix4f matrix);

そして、これらの場合、全体をコピーすることは通常、壮大なオーバーヘッドになり、Meshを永続的なデータ構造に変更し、類似の「ビルダー」を使用して不変の型を変更すると便利です。一意ではないカウントパーツを単純に浅くコピーおよび参照できるようにするためのバージョン。それはすべて、副作用のないメッシュ関数を記述できることに重点が置かれています。

永続的なデータ構造

そして、すべてをコピーするのが非常に高価であるこれらのケースでは、スレッドの安全性を単純化するだけではなかったので、わずかなコストの前払いがあったとしても、不変のMeshを設計する努力が実際に報われました。また、非破壊編集(ユーザーが元のコピーを変更せずにメッシュ操作をレイヤー化できるようにする)、アンドゥシステム(アンドゥシステムは、メモリを消費することなく、操作による変更前のメッシュの不変のコピーを保存できるようになりました)も簡素化しました使用)、および例外安全性(上記の関数で例外が発生した場合、関数は最初から何も起こさなかったため、ロールバックしてすべての副作用を取り消す必要はありません)。

これらの場合、これらの多額のデータ構造を不変にするのに必要な時間は、コストよりも多くの時間を節約できたと自信を持って言えます。これは、これらの新しいデザインのメンテナンスコストを、可変性と副作用を引き起こす機能を中心とした以前のデザインと比較したためですまた、前者の変更可能な設計ははるかに時間がかかり、特に例外の安全性など、開発者が非常に時間をかけて無視しがちな領域では、人的ミスがはるかに発生しやすかった。

したがって、これらの場合、不変のデータ型は本当に効果があると思いますが、システムのほとんどの機能に副作用がないようにするために、すべてを不変にする必要はありません。多くのものは、完全にコピーするのに十分安いです。また、実際のアプリケーションの多くは、あちこちで副作用を引き起こす必要があります(少なくともファイルの保存と同様)。ただし、通常、副作用のない関数がはるかに多くあります。

不変のデータ型をいくつか持つことのポイントは、大量のデータ構造を小さな部分だけを完全に左右に深くコピーするという形で、壮大なオーバーヘッドを招くことなく、副作用のない最大数の関数を書き込めることを確認することです。それらの修正が必要です。それらのケースで永続的なデータ構造を使用することで、最終的には最適化の詳細になり、副作用が発生しないように関数を記述できるようになり、そのために莫大なコストを支払う必要がなくなります。

不変のオーバーヘッド

概念的には、可変バージョンは常に効率の面で優れています。不変のデータ構造に関連する計算オーバーヘッドは常に存在します。しかし、私が上記で説明したケースでは、それは価値のある交換であることがわかりました。そして、オーバーヘッドを本質的に最小限に抑えることに集中できます。私は、最適化は簡単だが正確さが難しくなるよりも、正確さが容易になり最適化が難しくなるようなタイプのアプローチを好みます。正しくない結果がどれほど早く得られても、そもそも正しく機能しないコードをさらに調整する必要がある場合に、完全に正しく機能するコードを使用することは、それほど意気消沈しません。

5
user204677

私が考えることができる唯一の欠点は、理論的には不変データの使用が可変データよりも遅くなる可能性があることです-新しいインスタンスを作成して以前のインスタンスを収集するのは、既存のインスタンスを変更するよりも遅くなります。

もう1つの「問題」は、不変タイプしか使用できないことです。結局のところ、状態を説明する必要があり、そのために変更可能な型を使用する必要があります。状態を変更しないと、作業を行うことはできません。

しかし、それでも一般的なルールは、できる限り不変の型を使用し、実際にそうする理由がある場合にのみ型を可変にすることです...

そして、「不変型が他のアプリケーションでめったに使用されないのはなぜですか?」という質問に答えるために-私は本当にそうだとは思わない...どこにいてもクラスをできるだけ不変にすることを誰もが推奨しているように見えます...例: http://www.javapractices.com/topic/TopicAction.do?Id=29

3
mrpyo

物事が変化する可能性のある実際のシステムをモデル化するには、可変状態をどこかで何らかの方法でエンコードする必要があります。オブジェクトが変更可能な状態を保持できる主な方法は3つあります。

  • 不変オブジェクトへの可変参照の使用
  • 可変オブジェクトへの不変参照の使用
  • 可変オブジェクトへの可変参照の使用

Firstを使用すると、オブジェクトが現在の状態の不変のスナップショットを簡単に作成できます。 2番目を使用すると、オブジェクトが現在の状態のライブビューを簡単に作成できます。 3番目を使用すると、不変のスナップショットやライブビューの必要性がほとんどない場合に、特定のアクションをより効率的にすることができます。

不変オブジェクトへの可変参照を使用して格納された状態の更新は、可変オブジェクトを使用して格納された状態の更新よりも遅いことが多いという事実を超えて、可変参照を使用すると、状態の安価なライブビューを構築する可能性を忘れる必要があります。ライブビューを作成する必要がない場合は、問題ありません。ただし、ライブビューを作成する必要がある場合、不変の参照を使用できないと、ビューでのすべての操作が--読み取りと書き込みの両方-他の場合よりもはるかに遅くなります。不変のスナップショットの必要性がライブビューの必要性を超える場合、不変のスナップショットのパフォーマンスの向上によりライブビューのパフォーマンスへの影響が正当化される可能性がありますが、ライブビューが必要でスナップショットが不要な場合は、可変オブジェクトへの不変の参照を使用する方法ですトーゴ。

0
supercat

あなたの場合、答えは主にC#が不変性をサポートしていないためです...

以下の場合に最適です。

  • 特に明記されていない限り(つまり、 'mutable'キーワードを使用して)、デフォルトではすべてが不変です。不変タイプと可変タイプを混在させると混乱を招きます

  • 変異メソッド(With)は自動的に利用可能になります-これはすでに達成可能ですが、 With を参照してください

  • 特定のメソッド呼び出しの結果を言う方法があります(つまり、ImmutableList<T>.Add)は破棄できないか、少なくとも警告が表示されます

  • そして主に、コンパイラが要求された場所でできるだけ不変性を保証できる場合( https://github.com/dotnet/roslyn/issues/159 を参照)

0
kofifus