文字列は.NETで不変であることを考えると、なぜstring.Substring()
O(substring.Length
)時間ではなく、O()時間がかかるように設計されているのO(1)
でしょうか。
すなわち、もしあればトレードオフは何でしたか?
文字列は.NETで不変であることを考えると、なぜstring.Substring()
O(substring.Length
)時間ではなく、O()時間がかかるように設計されているのO(1)
でしょうか。
すなわち、もしあればトレードオフは何でしたか?
回答:
更新:私はこの質問がとても好きだったので、ブログに書きました。文字列、不変性、永続性をご覧ください
簡単に言えば、nが大きくならない場合、O(n)はO(1)です。 ほとんどの人は小さな文字列から小さな部分文字列を抽出するので、複雑さが漸近的に成長する方法はまったく無関係です。
長い答えは:
インスタンスでの操作により、少量のコピー(通常はO(1)またはO(lg n))でオリジナルのメモリを再利用できるように構築された不変のデータ構造または新しい割り当ては、「永続的」と呼ばれます不変のデータ構造。.NETの文字列は不変です。あなたの質問は本質的に「なぜそれらは永続的ではないのですか」ですか?
.NETプログラムの文字列に対して通常行われる操作を見ると、まったく新しい文字列を作成するだけでは、関連するすべての点で悪くなることはほとんどありません。複雑な永続データ構造を構築するための費用と難しさは、それだけでは採算が取れません。
人々は通常、「サブストリング」を使用して、少し長いストリング(おそらく数百文字)から短いストリング(例えば、10文字または20文字)を抽出します。コンマで区切られたファイルにテキスト行があり、姓である3番目のフィールドを抽出するとします。行はおそらく数百文字の長さで、名前は数十文字になります。50バイトの文字列割り当てとメモリコピーは、最新のハードウェアでは驚くほど高速です。既存の文字列の中央へのポインタと長さで構成される新しいデータ構造を作成することも、驚くほど高速であることは無関係です。「十分に速い」は、定義上十分に速いです。
抽出される部分文字列は通常、サイズが小さく、有効期間が短いです。ガベージコレクターはすぐにそれらを再利用する予定であり、そもそもヒープ上のスペースをあまり取りませんでした。したがって、ほとんどのメモリの再利用を促進する永続的な戦略を使用することもメリットではありません。これで、内部ポインタの処理について心配する必要があるため、ガベージコレクタの処理が遅くなります。
人々が文字列に対して通常行う部分文字列操作が完全に異なる場合、永続的なアプローチを採用することは理にかなっています。人々が通常100万文字の文字列を持ち、サイズが10万文字の範囲にある何千もの重複する部分文字列を抽出していて、それらの部分文字列がヒープ上で長期間存続している場合、永続的な部分文字列を使用するのが最適です。アプローチ; それは無駄で愚かです。しかし、ほとんどの基幹業務プログラマーは、そのようなことのように漠然とさえ何もしません。.NETは、Human Genome Projectのニーズに合わせて調整されたプラットフォームではありません。DNA解析プログラマーは、これらの文字列の使用特性に関する問題を毎日解決する必要があります。オッズはあなたがそうしないことは良いことです。使用シナリオに厳密に一致する独自の永続データ構造を構築する少数の人。
たとえば、私のチームは、入力時にC#およびVBコードのオンザフライ分析を行うプログラムを作成しています。これらのコードファイルの一部は巨大であるため、部分文字列を抽出したり、文字を挿入または削除したりするためのO(n)文字列操作を行うことはできません。私たちは、迅速かつ効率的に既存の文字列データの一括再利用するために私達を許可するテキストバッファへの編集を表現するための永続的な不変のデータ構造の束を構築していると、一般的な編集の際に、既存の語彙と構文解析を。これは解決するのが難しい問題であり、そのソリューションは、C#およびVBコード編集の特定のドメインに合わせて狭く調整されました。組み込みの文字列型がこの問題を解決することを期待するのは非現実的です。
string contents = File.ReadAllText(filename); foreach (string line in content.Split("\n")) ...
または他のバージョンのコード。つまり、ファイル全体を読み取ってから、さまざまな部分を処理します。文字列が永続的である場合、この種のコードはかなり高速になり、必要なメモリが少なくなります。各行をコピーするのではなく、常にメモリ内にファイルのコピーを1つだけ保持し、次に各行の一部を処理します。しかし、エリックが言ったように-それは典型的なユースケースではありません。
String
は永続的なデータ構造として実装されています(これは標準では指定されていませんが、私が知っているすべての実装でこれが行われます)。
正確ので、文字列は不変で、.Substring
元の文字列の少なくとも一部のコピーを作成する必要があります。nバイトのコピーを作成するには、O(n)時間かかります。
一定の時間に大量のバイトをコピーするとどう思いますか?
編集:Mehrdadは、文字列をまったくコピーせず、その一部への参照を維持することを推奨しています。
.Netで、誰かが.SubString(n, n+3)
(文字列の中央にある任意のnに対して)呼び出すマルチメガバイトの文字列について考えてみます。
では、1つの参照が4文字を保持しているという理由だけで、文字列全体をガベージコレクションにすることはできません。それはとんでもないスペースの無駄のようです。
さらに、部分文字列(部分文字列の内部にある場合もある)への参照を追跡し、最適なタイミングでコピーを試みて(上記のように)GCの無効化を回避すると、概念が悪夢になります。をコピーし.SubString
て、単純な不変モデルを維持する方がはるかに簡単で信頼性が高くなります。
編集: ここでは、より大きな文字列内の部分文字列への参照を維持することの危険性について少し読んでみましょう。
memcpy
O(n)を使用します。
char*
部分文字列を取得できます。
NULL
終了し。で説明したようにリッペルトの後、最初の4つのバイトは、文字列の長さを含みます。これが、スキートが指摘するように、\0
文字を含むことができる理由です。
Java(.NETとは対照的に)には2つの方法がSubstring()
あります。参照のみを保持するか、部分文字列全体を新しいメモリ位置にコピーするかを検討できます。
シンプルな.substring(...)
株式内部的に使用char
して、あなたのオリジナルのStringオブジェクトの配列、new String(...)
必要に応じて新しい配列にコピーすることができますが、(元1の妨げガベージコレクションを避けるため)。
この種の柔軟性は、開発者にとって最良のオプションだと思います。
.substring(...)
。
以前は、より大きな文字列を参照するためにJavaが使用されていましたが、
私はそれが改善できるように感じます:なぜ条件付きでコピーをしないのですか?
部分文字列が親のサイズの半分以上である場合、親を参照できます。それ以外の場合は、コピーを作成できます。これにより、多くのメモリのリークを回避しながら、大きなメリットを提供します。
char[]
(開始と終了への異なるポインターを使用)を使用することから、新しいを作成することに変更されたことString
です。これは明らかに、費用便益分析が新しいの作成に対する選好を示さなければならないことを示していますString
。
ここでの回答はいずれも「ブラケット問題」に対処していません。つまり、.NETの文字列はBStr(ポインタの「前」にメモリに格納されている長さ)とCStr(文字列は'\ 0')。
文字列「こんにちは」はこのように表されます
0B 00 00 00 48 00 65 00 6C 00 6F 00 20 00 74 00 68 00 65 00 72 00 65 00 00 00
(- ステートメントchar*
でaに割り当てられている場合fixed
、ポインターは0x48を指します。)
この構造により、文字列の長さの高速ルックアップが可能になり(多くのコンテキストで役立ちます)、ポインターをP / InvokeでWin32(または他の)APIに渡して、nullで終了する文字列を期待できます。
あなたが行う場合はSubstring(0, 5)
、あなたがコピーを作成する必要があるというルール「ああ、私は最後の文字の後にヌル文字があるだろうと約束しました」。最後に部分文字列を取得した場合でも、他の変数を破壊せずに長さを置く場所はありません。
ただし、「文字列の真ん中」について話したい場合もあり、必ずしもP / Invokeの動作を気にする必要はありません。最近追加されたReadOnlySpan<T>
構造を使用して、コピーなしの部分文字列を取得できます。
string s = "Hello there";
ReadOnlySpan<char> hello = s.AsSpan(0, 5);
ReadOnlySpan<char> ell = hello.Slice(1, 3);
ReadOnlySpan<char>
独立店舗の長さを「サブストリング」、そして、それは「\ 0」があることを値の終わりの後を保証するものではありません。「文字列のように」多くの方法で使用できますが、BStrまたはCStrの特性(どちらもはるかに少ない)がないため、「文字列」ではありません。P / Invokeを(直接)行っていない場合は、大きな違いはありません(呼び出すAPIにReadOnlySpan<char>
オーバーロードがない場合を除きます)。
ReadOnlySpan<char>
参照型のフィールドとして使用することはできません。そのため、ReadOnlyMemory<char>
(s.AsMemory(0, 5)
)もあります。これは、を持つ間接的な方法でありReadOnlySpan<char>
、同じ違いstring
があります。
以前の回答の回答/コメントのいくつかは、ガベージコレクターが5文字について話し続けている間、100万文字の文字列を保持しなければならないのは無駄であると説明しました。それがまさにReadOnlySpan<char>
アプローチで得られる行動です。短い計算をしているだけなら、ReadOnlySpanアプローチがおそらくより良いでしょう。しばらく保持する必要があり、元の文字列のごく一部のみを保持する場合は、適切な部分文字列を実行する(余分なデータを削除する)ほうがよいでしょう。途中のどこかに遷移点がありますが、それはあなたの特定の使用法に依存します。