Ruby 3.0の文字列型の新仕様が波紋を呼んでいるそうな。
immutableじゃなかった文字列型が、3.0からはimmutableになる。
今までも .freeze を付けることでimmutableにできたけれど、もちろん全てのコードがそうなっているわけではないから、Ruby3.0に移行するにあたって十分なコードリーディングとテストをしなければいけない。
これがC系のようなコンパイルのある言語なら、コンパイルエラーとして一発で検出できるからどうということはない。
気軽に書いていけることの代償に、こういう仕様の変更に弱いということが露呈してしまった。
書いてすぐ実行できることは、その時はいいけれど、その言語の進歩を阻害する。今良ければいいだけではなく、後々もいいということを目指さないといけないわけですな。
Ruby 3.0系と2.0系で断絶が起きる様子が見える……。あれ?なんとかいうWeb系言語でも見た断絶に似ている……。
immutableにするメリット
さて、文字列をimmutableにするということ自体には賛成です。それは、同じ文字列なら同じオブジェクトを使うという最適化ができるから。
特に、文字列オブジェクトを扱うことの多いWebな言語では、この最適化の効果は大きいはず。
ただ当然、そのオブジェクトがimmutableではなくてはいけない。ある場所で作った文字列オブジェクトが、全く別の場所で書き換えられる可能性があるから。そしてそういうバグを探すのは考えるだけでもジ、ジゴク……。
Rubyでも、先述の .freeze を使うと、この最適化ロジックが適用される。
C#の文字列型は?
もちろんC#でもこの仕組が用意されている。C#の文字列は元々immutableだから、エンジニアが意識せずとも、常にこの最適化が行われている。
String caching. Memory optimization and re-use
Optimizing C# String Performance
サンプルコードで実際にそうなっているのか調べてみたところ、確かなようだ。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
var textA = "abcde"; var textB = "xyz"; // This outputs "textA equals textB : False" Console.WriteLine( "textA equals textB : {0}", ReferenceEquals(textA, textB)); textB = "abcde"; // This outputs "textA equals textB : True" Console.WriteLine( "textA equals textB : {0}", ReferenceEquals(textA, textB)); |
このコードを実行して出力を見てみると、確かに同じ文字列なら同じオブジェクトが使われていることが分かる。
仕組みを考える
この文字列キャッシュの仕組みを考えてみた。
恐らく、文字列オブジェクトの生成毎に、すでに生成されている文字列かどうかを判定しているのだろうと思う。
その上で、既に使われている文字列なら過去に生成したオブジェクトを使い、使われていない文字列なら新たにオブジェクトを生成してstaticなキャッシングクラスに入れておくのだろう。
このstaticクラスは、文字列をキーに、文字列オブジェクトを値としたDictionaryだろうか?
そう考えると、文字列でなくとも使えそうなロジックに思えてくる。
追記 : コンパイル時に最適化しているそうです。
現状、immutableなクラスを作れる場合と作れない場合があるけれど、文字列型に対するキャッシュの仕組みは、そうしたクラスを扱う時に何か役に立つはず。
※C#におけるimmutable、それからC#7で検討されているimmutable classについてはこちらが詳しい。
2 Responses
__
最後のコードでTrueが出力されるのはコンパイル時最適化の結果です。文字列を実行時に外部から与えて最適化できないようにすれば同じ文字列に対し異なる参照が得られます。
実行時にも同じ参照を得たい場合はString.Intern()があります。
toach
自作でこのロジック組んでみたんですけど、どうしても速くならなかったのはそういうことなんですね。勉強になります。