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を超えるような

VB.NETでもナウいCIサーバでテストをギュイーンしたい

はじめに

この記事はVisual Basic Advent Calendar 2018の15日目の記事です。

どうも、闇落ちJenkinsおじさんです。

オンプレのCIサーバといえばJenkinsが筆頭に上がりますが、Jenkinsのビルド環境が秘伝のタレ化してスレーブを追加する際に死ぬほどコンパイラやらSDKやらをぶち込まないとならず、自動化の裏側でソウルジェムをマッハで濁らせながらメンテナンスしている管理者も多いのではないでしょうか。

こんな時、コンテナでテスト環境は生成・破棄出来たらどんなに幸せでしょうか。オンプレで

コンテナは投げ捨てるもの

JenkinsでもPipelineを使えばDockerコンテナ上でビルド・テストを実行出来ますが、いかんせんGroovyで記述しなくてはならないというところが個人的にはつらみポイントでした。*1

という感じで色々迷っていたところ、drone.ioというオンプレにも展開できるDockerコンテナを使用したCIサーバを見つけたので試してみました。*2

Dockerパーリナイ

というわけで以下の図のようにLinuxインスタンスを生やしまくります。

f:id:jyuch:20181121220915p:plain

今回の検証に使用したソフトウェアは以下な感じです。

  • dnsmasq
    • 弊社は3桁以上の数字は覚えられないのでDNSサーバを立てます。
  • Gitea
    • Giteaを使うと割と簡単に構築できるそうなので今回はGiteaを使用しました。
  • drone
    • 今回の主役です。今回はサーバとエージェントを分ける構成で構築してみました。

今回はdnsmasq以外はすべてDocker上で構築します。これは別にコンテナ至上主義とかそういうのではなく、単純にそうしたほうが楽だから程度の理由です。

Gitea

まずはソースコードをホストするためにGiteaを立てます。

Giteaはデータ格納用にPostgres・MySQLSQL Server(とSQLite)が選べますが、マイクロソフト信者の私たちはもちろん最強のデータベースであるSQL Serverを選びますよね?

version: '3'
services:
  mssql:
    image: 'microsoft/mssql-server-linux:2017-CU12'
    volumes:
      - ./data:/var/opt/mssql/data
    ports:
      - '1433:1433'
    environment:
      - ACCEPT_EULA=Y
      - SA_PASSWORD=mssqllinux@1
      - MSSQL_LCID=1041
      - MSSQL_COLLATION=Japanese_CI_AS
version: "2"

networks:
  gitea:
    external: false

services:
  server:
    image: gitea/gitea:1.6
    environment:
      - USER_UID=1000
      - USER_GID=1000
    restart: always
    networks:
      - gitea
    volumes:
      - ./gitea:/data
    ports:
      - "80:3000"
      - "222:22"

f:id:jyuch:20181121222947p:plain

drone

droneもDockerで立てていきます。というよりはDockerでしか立てられません。

version: "2"

services:
  server:
    image: drone/drone:1.0.0-rc.1
    environment:
      - DRONE_GIT_ALWAYS_AUTH=false
      - DRONE_GITEA_SERVER=http://gitea.opabinia.co.jp/
      - DRONE_RPC_SECRET=2039906835c92213b78cce47d38be1e9
      - DRONE_SERVER_HOST=drone.opabinia.co.jp
      - DRONE_SERVER_PROTO=http
      - DRONE_TLS_AUTOCERT=false
    restart: always
    volumes:
      - ./drone:/data 
    ports:
      - "80:80"
      - "443:443"

f:id:jyuch:20181121224256p:plain

drone agent

こちらに至ってはOSインストールしてDockerインストールして以下のコマンドをコピペでぶち込めばそれで終わりです。

docker run \
  --volume=/var/run/docker.sock:/var/run/docker.sock \
  --env=DRONE_RPC_SERVER=http://drone.opabinia.co.jp \
  --env=DRONE_RPC_SECRET=2039906835c92213b78cce47d38be1e9 \
  --env=DRONE_RUNNER_CAPACITY=4 \
  --env=DRONE_RUNNER_NAME=dad96u1 \
  --restart=always \
  --detach=true \
  --name=agent \
  drone/agent:1.0.0-rc.1

プロジェクトの作成

まぁ、Linuxとかが出てきてお察しの通り、今回は.NET Coreで検証しています。droneのエージェントにはWindowsも使えるのでもしかしたら.NET Frameworkの方も行けるかもしれませんが今回は検証していないです。

Public Class Class1

    Public Function Add(x As Integer, y As Integer) As Integer
        Return x - y
    End Function

End Class

みたいなクラスがあって

Imports System
Imports Xunit
Imports ClassLibrary1

Public Class Class1Test

    <Fact>
    Sub Add_return_valid_value()
        Dim x = 10 : Dim y = 20
        Dim expected = x + y

        Dim c = New Class1()

        Assert.Equal(expected, c.Add(x, y))
    End Sub

End Class

みたいなテストを用意します。そして、手元でテストを回してばっちり失敗することを確認してから以下の.drone.ymlを追加してからプッシュします。

kind: pipeline
name: default

steps:
- name: classlibrary1
  image: microsoft/dotnet:2.1-sdk
  commands:
  - dotnet test

ちゃんと失敗しています。

f:id:jyuch:20181121230617p:plain

なおすと

f:id:jyuch:20181121230955p:plain

成功します。

おわりに

多分ドキュメントの整備が追い付いていないのか、ちょっとドキュメント類が不足しているなというところが率直な感想です。 また、開発途中だから無いのかそれともエンタープライズ版との差別化なのかは分かりませんが、ブラウザから操作できることが非常に限られている感じです。ブラウザからは現在いくつのエージェントがぶら下がっているのかも分かりません。*3

今回はクッソ簡単な例しか試していませんが、これが大規模になった場合、ビルド実行用のDockerイメージを用意したり、用意したDockerコンテナをローカルでホストするためリポジトリ(Sonatype Nexus OSSとか)を用意したり、独自ライブラリをNuGet経由で配布するためにパッケージを作ったり、それをホストするためのNuGetリポジトリを立てたりと想像するだけで大変そうです。

逆にその辺を解決できれば自動化インフラ管理者にとっての救済となるかもしれません。

おわり

github.com

*1:似たような理由でGradleもいまだに苦手意識ががが

*2:飛ぶほうのドローンが流行っているせいでググラビリティは割と低め

*3:エージェントについては意識するなという事?

VB.NETでもリアクティブしたい(Akka.NET)

はじめに

2018年は殺伐としたVB.NET界隈にAkka.NETの波が到来します。(しません)

という訳であまり需要がなさげなエントリですが*1、ライブラリ自体は結構成熟しているっぽいので実運用に突っ込んでもまぁ行けるんじゃないかなと思います。

Akkaとは

Akkaの概念自体については割と記事があるのでここでは割愛します。

インストールパッケージ

今回の記事で作成したプロジェクトでは以下のパッケージをNuGetでインストールします。

  • Akka(Akka本体)
  • Akka.Logger.Serilog(Serilogのログアダプタ)
  • Serilog(ロガー)
  • Serilog.Sinks.Console(Serilogのコンソールシンクの実装)

今回はロガーとしてSerilogを使用しますが、Serilog自体の説明は省略します。

メッセージとActorの実装

Akka.NETではActorはReceiveActor(もしくはUntypedActor)を継承して実装します。

ReceiveActorではコンストラクタ中で処理するメッセージの型とメッセージを処理するデリゲートを指定します。

Public Sub New(dest As IActorRef)
    _dest = dest
    Receive(Of String)(AddressOf ReceiveString)
    Receive(Of Greet)(AddressOf ReceiveGreet)
End Sub

また、メッセージは(Akkaに限らず)スレッド間を行き来するためイミュータブルになるように実装します。*2

本家のScalaではcase classで所望のクラスが簡単に量産できますが、VBではイミュータブルなクラスの定義がクッソ面倒*3なので弊社のプロダクト環境ではメタプログラミングとT4を駆使してコード生成をして対応しています。が本題ではないのでここでは触れません。

Public Class HelloActor
    Inherits ReceiveActor

    Private ReadOnly _logger As ILoggingAdapter = Context.GetLogger(New SerilogLogMessageFormatter())
    Private ReadOnly _dest As IActorRef

    Public Sub New(dest As IActorRef)
        _dest = dest
        Receive(Of String)(AddressOf ReceiveString)
        Receive(Of Greet)(AddressOf ReceiveGreet)
    End Sub

    Public Sub ReceiveString(msg As String)
        _dest.Tell(New Greet(msg))
    End Sub

    Public Sub ReceiveGreet(msg As Greet)
        _logger.Info("Returned {Message}", msg.Message)
    End Sub

End Class

Public Class EchoActor
    Inherits ReceiveActor

    Private ReadOnly _logger As ILoggingAdapter = Context.GetLogger(New SerilogLogMessageFormatter())

    Public Sub New()
        Receive(Of Greet)(AddressOf ReceiveGreet)
    End Sub

    Public Sub ReceiveGreet(msg As Greet)
        _logger.Info("Receive {Message}", msg.Message)
        Sender.Tell(New Greet($"You said {msg.Message}"))
    End Sub

End Class

Public Class Greet
    Public ReadOnly Property Message As String

    Public Sub New(msg As String)
        Message = msg
    End Sub

End Class

余談ですが、なんで

Receive(Of Greet)(AddressOf ReceiveGreet)

みたいな書き方をしているかなのですが、C#では

Receive<string>(message => {
  log.Info("Received String message: {0}", message);
  Sender.Tell(message);
});

みたいにラムダ式を使ってかなりきれいに書けますが、VBでは

Receive(Of Greet)(Sub(message)
                      _logger.Info("Returned {Message}", message.Message)
                  End Sub)

みたいになってお゛ぉ゛ぉ゛ぉ゛ぉ゛ぉ゛ぉ゛んってなるからです。 特に行数が10行を超えた時点でクッソ読みづらくなります。

ActorSystem

あとは本家と同じようにActorSystemを生成しーのActorを生成しーのトツギーノでリアクティブできます。

Module Program

    Dim conf As String = "
akka {
    loggers=[""Akka.Logger.Serilog.SerilogLogger, Akka.Logger.Serilog""]

    stdout-loglevel = off
    loglevel = DEBUG
    log-config-on-start = on
    actor {
        debug {
              receive = on 
              autoreceive = on
              lifecycle = on
              event-stream = on
              unhandled = on
        }
    }
}
"

    Sub Main()
        Dim logger = New LoggerConfiguration().
            WriteTo.Console().
            MinimumLevel.Debug().
            CreateLogger()
        Log.Logger = logger

        Dim system = ActorSystem.Create("vb-actor", conf)
        Dim echo = system.ActorOf(Of EchoActor)("echo")
        Dim hello = system.ActorOf(Props.Create(Function() New HelloActor(echo)))

        hello.Tell("hello")
        hello.Tell("hoge")

        Console.ReadLine()
        system.Terminate().Wait()
    End Sub

End Module
[23:09:14 INF] Receive hello
[23:09:14 INF] Receive hoge
[23:09:14 INF] Returned You said hello
[23:09:14 INF] Returned You said hoge

おわりに

Akka.NETはいかんせん日本語記事がほとんど存在しないので割と茨の道ですが、英語の記事自体は結構あるうえに本家のドキュメントも脳内で.NET向けにトランスパイルすれば割とそのまま適用できるので思ったよりは障壁は低いと思います。

なお、IntelliSenseの説明文がときたま「TBD」になっており、『はぁそうですか』ってなります。

f:id:jyuch:20180103233642p:plain

github.com

おわり

*1:執筆時点ではC#ですら日本語でヒットする記事は1件、VBに至っては英語ですらヒットしない

*2:マルチスレッドの大原則として不変な値は常にスレッドセーフというものがあります

*3:特にVB14以前