VB.NETでReDim Preserveを使うくらいなら配列を使うのをやめたら?

はじめに

弊社のブログで現在一番アクセス数が多いのは(個人的には意外ですが)ReDimの記事です。

jyuch.hatenablog.com

この記事でもReDim Preserveには否定的な感じで書いていますが、現在でもこの意見は変わりません。

今北産業

  • 添え字操作がバグの温床になる
  • 配列の使用意図を読み取るのに文脈に読み解かないといけないから可読性がマッハで低下する
  • マジで気を付けないとパフォーマンス的にデメリットになるし、気を付けられる人はそもそもVBつkにゃーん

⇒定数として複数の値を定義したり、配列しか受け入れないメソッドへの引数で使う以外でVBで配列とReDim Preserveを使う価値はない

How about ReDim?

ReDimってどういう仕組みで配列の拡張または縮小を行っているのか確認してみましょう。

Dim a As String() = New String() {"Hello", "World"}
ReDim Preserve a(2)

のコードは以下のコードと等価です。

Dim a As String() = New String() {"Hello", "World"}
Dim b(2) As String

For i As Integer = 0 To a.GetLength(0)
    b(i) = a(i)
Next

a = b

単純に言ってしまうと、新しいサイズの配列を生成して値をコピーしているだけです。

ReDim Preserveに否定的な理由

そんじゃ、なんで弊社がReDim Preserve(というか配列そのもの)に否定的な意見*1なのかと言いますと

配列の添え字の制御が面倒(かつバグの温床になりやすい)

ReDim Preserveを使う場面って最初からデータの数が正確に予想できない場合がほとんどだと思います。

例えば以下のコードのように入力をバッファする場合、正確な数が予想できないため必要に応じて配列を拡張する必要があります。

Dim currentIndex = 0
Dim buffer(4) As String

While True
    Dim input = Console.ReadLine()

    If input = "q" Then
        Exit While
    End If

    If currentIndex > buffer.GetLength(0) Then
        ReDim Preserve buffer(buffer.GetLength(0) + 5)
    End If

    buffer(currentIndex) = input
    currentIndex += 1
End While

For Each it In buffer
    If it <> Nothing Then
        Console.WriteLine(it)
    End If
Next

入力された値をバッファし、最後に一気に出力しています。

それで、こちらがQueue(Of T)を使ったバージョン。

Dim buffer As New Queue(Of String)

While True
    Dim input = Console.ReadLine()

    If input = "q" Then
        Exit While
    End If

    buffer.Enqueue(input)
End While

For Each it In buffer
    Console.WriteLine(it)
Next

余計な変数が1つ消え、めんどくさい添え字計算も省略できています。*2

VBの主戦場たる生産性アプリケーションでは基本的にそこまでクリティカルな性能が求められることはなく*3、むしろコードでどこまでドメインを表現できるか・保守性の高いコードであるかが求められると考えています。

後述しますが、そのコードでのドメインの表現・保守性を考えると、配列の添え字を操作するよりも適切なコレクションクラスを用いてドメインのセマンティクスをコード上で直接表現したほうが保守性が高くなるのではないでしょうか。

使用意図が分かりずらい

添え字計算を駆使すれば、配列はキュー*4・スタック・リングバッファー等々のセマンティクスを表現できます。

表現できるのはいいのですが、その駆使された添え字計算について後から読まなくてはならないあなたの後任の事も考えてあげましょう。

つまるところ、ぐちゃぐちゃな添え字計算を読み取って使用意図をくみ取るのと、専用のコレクションが使われているのを読み取るのでは、将来あなたの後任にとってどちらが楽ですか?という事です。

多用するとパフォーマンス的に不利になりやすい

上でも述べましたが、ReDim Preserveは新しい領域を確保して値をコピーした後に古い領域をガベージコレクタにブン投げます。

特に配列のサイズが大きい*5ケースでReDim Preserveを多用するとメモリアロケーションガベージコレクションが多発してパフォーマンスに明らかな悪影響を及ぼします。

代替手段

ほとんどのケースでは

docs.microsoft.com

の中から適したコレクションクラスを選択するだけで要求を満足するはずです。

(古い記事では特に)配列の代替としてArrayListを推しているケースが多いですが、パフォーマンス・型安全の点からみてもジェネリックコレクションと比べたメリットが無いので2秒でゴミ箱にぶち込みましょう。

おわりに

  1. ジェネリックコレクションを知らないのなら、まずはそれを勉強しろ
  2. パフォーマンスを重要視したいのはわかるが、お前のアプリケーションでそこまでの最適化が本当に必要か胸に手を当てて考えてみろ
  3. そうでないならVB.NETでReDim Preserveを使うくらいなら配列を使うのをやめたら?

*1:ただし生産性アプリケーションの開発に限る

*2:ちなみに、配列版は実はバグがあります。探してみましょう!!

*3:もちろん速いに越したことはありませんが、求められる速度はプリミティブなメモリ操作が必要になるほどではないと考えています

*4:上のコードの様に

*5:具体的にいうと配列のサイズが86キロバイトを超えるケース。64ビットで動作していると仮定すると、文字列の配列で要素数が11008を超えるような