一部の言語(C#またはJava)には不変の文字列があり、他の言語(Rubyなど)には可変の文字列があります。それらのデザインの選択の背後にある理由は何ですか?
4 に答える
不変の文字列が優れている理由の1つは、Unicodeのサポートが容易になることです。最新のUnicodeは、固定サイズのデータセルに効率的に適合できなくなり、文字列インデックスとメモリアドレス間の1対1の対応が失われ、可変文字列に利点がもたらされます。
これまで、ほとんどの欧米のアプリケーションはシングルバイト文字(さまざまなASCIIベースのコーディングまたはEBCDIC ...)を使用していたため、通常、文字列をバイトバッファとして扱うことで効率的に処理できました(従来のCアプリケーションのように)。
Unicodeがかなり新しいときは、最初の16ビット以外はそれほど必要ではなかったため、JavaはそのString
s(およびStringBuffer
s)に2バイト文字を使用していました。これは2倍のメモリを使用し、16ビットを超えるUnicode拡張から発生する可能性のある問題を無視しましたが、当時は便利でした。
現在、Unicodeはそれほど新しいものではなく、最もよく使用される文字は16ビットに収まりますが、基本多言語面が存在するすべてであるかのように見せかけることはできません。Unicodeのサポートを正直に主張したい場合は、可変長文字またはさらに大きな(32ビット?)文字セルが必要です。
可変長文字を使用すると、O(1)時間で任意の長さの文字列にインデックスを付けることができなくなります。追加情報がなければ、最初から数えてN番目の文字が何であるかを把握する必要があります。これにより、可変文字列バッファの主な利点である、サブストリングをシームレスに変更できるという利点も失われます。
幸い、ほとんどの文字列操作では、実際にはこのインプレース変更機能は必要ありません。字句解析、構文解析、および検索はすべて、最初から最後まで、順次、反復的に進行します。置換文字列は元の文字列と同じ長さである必要はないため、一般的な検索と置換は、そもそもインプレースではありませんでした。
多数のサブストリングを連結することも、効率的にするために実際にインプレースで変更する必要はありません。ただし、(他の人が指摘しているように)N個の部分部分文字列のそれぞれに新しい文字列を割り当てることで、単純な連結ループを簡単にO(N ^ 2)にすることができるため、さらに注意する必要があります。
単純な連結を回避する1つの方法は、連結を効率的に行うように設計された可変オブジェクトStringBuffer
またはオブジェクトを提供することです。ConcatBuffer
別の方法は、(効率的に)連結される文字列のシーケンスにイテレータを取り込む不変の文字列コンストラクタを含めることです。
ただし、より一般的には、参照によって効率的に連結する不変の文字列ライブラリを作成することは可能です。この種の弦は、「ロープ」または「コード」と呼ばれることが多く、それを構成する基本的な弦よりも少なくとも少し重いことを示していますが、連結の目的では、必要がないため、はるかに効率的です。データを完全に再コピーします!
上記のウィキペディアのリンクでは、「ロープ」データ構造は連結するO(log N)であると述べていますが、岡崎による独創的な論文「純粋関数型データ構造」は、O(1)時間で連結を行う方法を示しています。
少なくともJavaの場合、文字列を不変にする理由の一部は、セキュリティとスレッドセーフのためでした。Javaは、実行時の安全性を重視しています(元々、セットトップボックスとWebブラウザがホストシステムを危険にさらすことなくリモートコンテンツをダウンロードして実行できるように設計されていました)。セキュリティを強化するために、文字列は不変であり、サブクラス化することはできません。つまり、Javaランタイムは、文字列の値が一定のままであることを保証しながら、ユーザーから文字列を渡したり受け取ったりできます(つまり、攻撃者は文字列をサブクラス化できず、有効な文字列のように見えるものを関数に渡しますが、次に、後で値を変更して間違ったデータにアクセスするか、複数のスレッドを使用して、ある時点で文字列が正しく見えるようにしますが、後で変更します)。
さらに、文字列をロックする必要がないため、不変性はマルチスレッドシステムで効率の利点をもたらします。また、開始点と終了点が異なっていても、多くの文字列が同じ基本的な文字配列を共有できるため、部分文字列操作を簡単に実装できます。
あなたがそれについて考えるならば、すべての基本的なデータ型は不変です。整数10を11に変更せず、10を11に置き換えます。文字列を基本的で不変にすることで、他の方法では不可能なプーリングやその他の最適化が可能になります。
短所に関しては、不変の文字列には、経済的な追加、並べ替え、およびその他の同様の操作を可能にするために、補完的な可変データ構造(つまり、文字列バッファー)が必要です。
不変の構造に対して実行されるこのような操作には、不当な量のリソースが必要になります。
Luaでのプログラミングには、この問題に関するすばらしい説明があります。
さらに反映するために、一部の言語(Common Lispなど)には非破壊機能と破壊機能の両方があり、その他の言語には不変リストと可変リスト(Python)の両方があります。
Common Lispに関する本を引用するには:
割り当てが危険に満ちている場合は、言語からそれを省略してみませんか?表現力と効率性という2つの理由があります。割り当ては、共有データを変更するための最も明確な方法です。また、割り当てはバインドよりも効率的です。バインディングは新しいストレージロケーションを作成し、ストレージを割り当てます。これにより、追加のメモリが消費されるか(バインディングがスコープ外にならない場合)、ガベージコレクターに負担がかかります(バインディングが最終的にスコープ外になる場合)。
ただし、反例として、多くのJavaScript(不変の文字列を持つ)インタープリターは、実装レベルで文字列を可変配列として扱います。
同様に、Clojureにはトランジェントがあります。これは、不変のデータ構造に対するエレガントな純粋関数のように見えますが、内部では効率のために可変状態を使用しています。