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


12

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

同時に、可変性がだれにも利益をもたらさない場合であっても、他のアプリケーションで不変型が表示されることはほとんどありません。

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

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

実生活からの例

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

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; }
}

一方、私は与えられた、このように書くとその取得WeatherAPIから、それは奇妙だろう変更:変更し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; } }
}

3
「より多くの作業が必要です」-引用が必要です。私の経験では作業が少なくて済みます。
コンラッドルドルフ14年

1
@KonradRudolph:より多くの作業によって、不変のクラスを作成するためにより多くのコードを記述する必要があります。私の質問の例はこれを示しており、可変クラスの場合は7行、不変クラスの場合は19行です。
Arseni Mourzenko

Visual Studioのコードスニペット機能を使用している場合は、使用することでコード入力を減らすことができます。カスタムスニペットを作成し、いくつかのキーを使用してIDEでフィールドとプロパティを同時に定義できます。不変の型はマルチスレッドに不可欠であり、Scalaなどの言語で広く使用されています。
メルトアカカヤ14

@Mert:コードスニペットはシンプルなものに最適です。フィールドとプロパティのコメントと正しい順序で完全なクラスを構築するコードスニペットを作成するのは簡単な作業ではありません。
Arseni Mourzenko

5
私は、不変バージョンがより多くの異なることをしているという例に同意しません。アクセサ{get; private set;}でプロパティを宣言することでインスタンスレベルの変数を削除できます。また、可変のプロパティにもコンストラクタが必要です。これらのフィールドはすべて常に設定する必要があり、それを強制しないのはなぜですか?これらの2つの完全に合理的な変更を行うことで、機能とLoCパリティが実現します。
Phoshi

回答:


16

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

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

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


11
「事前にインスタンス化の必要性すべての値を。」:あなたが浮かん初期化されていないフィールドを持つオブジェクトを持つことのリスクを受け入れない限り、また、可変タイプでは、ありません...
ジョルジオ

@Giorgio可変型の場合、デフォルトのコンストラクターはインスタンスをデフォルトの状態に初期化し、インスタンス化後にインスタンスの状態を後で変更できます。
ティア

8
不変の型の場合、同じデフォルトのコンストラクタを使用し、後で別のコンストラクタを使用してコピーを作成できます。デフォルト値が可変タイプに対して有効な場合、両方のケースで同じエンティティをモデリングしているため、これらは不変タイプに対しても有効である必要があります。それとも違いは何ですか?
ジョルジオ

1
考慮すべきもう1つの点は、型が表すものです。データコントラクトは、これらすべての点のために優れた不変型にはなりませんが、依存関係で初期化されるか、データのみを読み取り、操作を実行するサービス型は、操作が一貫して実行され、サービスの状態がそのリスクに変更されました。
ケビン14年

1
私は現在F#でコードを作成していますが、不変性がデフォルトです(したがって、実装が簡単です)。あなたのポイント3は大きなハードルだと思います。ほとんどの標準.Netライブラリを使用するとすぐに、フープをジャンプする必要があります。(そして、それらがリフレクションを使用し、したがって不変性をバイパスする場合... argh!)
クラン

5

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

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

不変性を中心にしない言語で作業している場合、実用的なアプローチは、すべてのデータ型を不変にすることではありません。同じ利点の多くを提供する潜在的にはるかに生産的な考え方は、副作用のないシステム内の機能の数を最大化することに集中することです。

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

// 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);

この時点で、メッシュには数十万以上のポリゴン、さらに多くの頂点とエッジ、複数のテクスチャマップ、モーフターゲットなどを備えた数百メガバイトのメモリが必要になる場合があります。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は、スレッドの安全性を単純化しただけではないため、わずかな費用が前払いされていても、実際に完済する不変の設計の努力を見つけました。また、非破壊編集(ユーザーが元のコピーを変更せずにメッシュ操作を階層化できるようにする)、アンドゥシステム(アンドゥシステムは、メモリを爆破せずに操作による変更の前にメッシュの不変のコピーを保存できるようになりました)も簡素化しました使用)、および例外の安全性(現在、上記の関数で例外が発生した場合、関数は元に戻らないため、すべての副作用をロールバックして元に戻す必要はありません)

これらの新しいデザインのメンテナンスコストを、可変性と副作用を引き起こす機能を中心とした以前のデザインと比較したため、これらのケースでは、これらの巨大なデータ構造を不変にするために必要な時間がコストよりも多くの時間を節約したと自信を持って言えます。前者の可変設計ははるかに多くの時間を要し、特に例外安全性など、開発者がクランチ時間中に無視することを本当に誘惑している分野では、はるかに人的ミスを起こしやすい傾向がありました。

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

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

不変のオーバーヘッド

概念的には、可変バージョンは常に効率性に優れています。不変のデータ構造に関連する計算オーバーヘッドは常にあります。しかし、私はそれを上記のケースでやりがいのある交換であり、オーバーヘッドを本質的に最小限に抑えることに集中できます。最適化は簡単だが正確性は難しくなるよりも、正確さが容易になり最適化が難しくなるタイプのアプローチを好む。最初から正しく機能しないコードをさらに調整する必要があるため、コードが完全に正しく機能することは、どれほど迅速に誤った結果を達成しても、それほど意気消沈することはありません。


3

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

もう1つの「問題」は、不変の型のみを使用できないことです。最後に、状態を記述する必要があり、それを行うには可変型を使用する必要があります。状態を変更しないと、作業を行うことはできません。

しかし、まだ一般的なルールは、可能な限り不変の型を使用し、本当にそうする理由がある場合にのみ型を可変にすることです...

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


1
どちらの問題もHaskellにはありません。
フローリアンマーゲイン

@FlorianMargaine詳しく説明していただけますか?
mrpyo

スマートコンパイラのおかげで、この遅延は正しくありません。Haskellでは、I / Oでさえ不変のAPIを介しています。
フローリアンマーゲイン

2
速度よりも根本的な問題は、不変オブジェクトが状態の変化中にIDを維持することが難しいことです。可変Carオブジェクトが特定の物理的な自動車の位置で継続的に更新される場合、そのオブジェクトへの参照があれば、その自動車の所在をすばやく簡単に見つけることができます。Car不変である場合、現在の居場所を見つけることはおそらくはるかに困難です。
supercat

コンパイラーが時々前のオブジェクトへの参照が残されていないことを理解するために、かなりスマートにコーディングする必要があります。特に大きなプログラムでは。そして、@ supercatが言うように、アイデンティティは確かに問題になる可能性があります。
マッケ14年

0

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

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

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

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


0

あなたの場合、答えは主にC#の不変性のサポートが不十分であるためです...

次の場合は素晴らしいでしょう:

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

  • メソッドを(変異してWith)自動的に利用できるようになります-これは、すでに見達成することができても

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

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


1
3番目の点について、ReSharperにMustUseReturnValueAttributeはまさにそれを行うカスタム属性があります。PureAttribute同じ効果があり、これにはさらに優れています。
セバスチャンレッド

-1

不変の型が他のアプリケーションで使用されることがほとんどないのはなぜですか?

無知?経験不足?

不変オブジェクトは今日では広く優れているとみなされていますが、比較的最近の開発です。最新の状態を維持していないエンジニア、または単に「知っていること」にこだわっているエンジニアは、それらを使用しません。そして、それらを効果的に使用するには、少しの設計変更が必要です。アプリが古い場合、またはエンジニアが設計スキルに乏しい場合、それらを使用するのは厄介な場合や面倒な場合があります。


「最新の状態に保たれていないエンジニア」:エンジニアは、非主流の技術についても学ぶべきだと言うことができます。不変性のアイデアは最近主流になりつつありますが、かなり古いアイデアであり、Scheme、SML、Haskellなどの古い言語でサポートされています(強制されていない場合)。したがって、主流の言語を超えて見慣れている人なら誰でも、30年前にそれについて学ぶことができたでしょう。
ジョルジオ

@Giorgio:一部の国では、多くのエンジニアが依然としてLINQなし、FPなし、イテレーターなし、ジェネリックなしでC#コードを記述しています。 、主流ではない言語を知っているとはほとんど思いません。
アルセニムルゼンコ

@MainMa:エンジニアという言葉をイタリック体で書いて良かった。
ジョルジオ

@Giorgio:私の国では、彼らはまた、建築家コンサルタント、および他の多くの価値のない用語と呼ばれ、イタリック体では書かれていません。私が現在働いている会社では、私はアナリスト開発者と呼ば、くだらないレガシーHTMLコードのためにCSSを書くことに時間を費やすことが期待されています。役職は非常に多くのレベルで邪魔です。
Arseni Mourzenko

1
@MainMa:同意します。エンジニアやコンサルタントなどの肩書きは、しばしば広く受け入れられている意味のない単なる流行語です。彼らはしばしば誰かまたは彼らの位置を実際よりも重要/名誉あるものにするために使われます。
ジョルジオ
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.