VB.NETでReDim Preserveを使うくらいなら配列を使うのをやめたら?
はじめに
弊社のブログで現在一番アクセス数が多いのは(個人的には意外ですが)ReDim
の記事です。
この記事でも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
を多用するとメモリアロケーションとガベージコレクションが多発してパフォーマンスに明らかな悪影響を及ぼします。
代替手段
ほとんどのケースでは
の中から適したコレクションクラスを選択するだけで要求を満足するはずです。
(古い記事では特に)配列の代替としてArrayList
を推しているケースが多いですが、パフォーマンス・型安全の点からみてもジェネリックコレクションと比べたメリットが無いので2秒でゴミ箱にぶち込みましょう。
おわりに
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インスタンスを生やしまくります。
今回の検証に使用したソフトウェアは以下な感じです。
- dnsmasq
- 弊社は3桁以上の数字は覚えられないのでDNSサーバを立てます。
- Gitea
- Giteaを使うと割と簡単に構築できるそうなので今回はGiteaを使用しました。
- drone
- 今回の主役です。今回はサーバとエージェントを分ける構成で構築してみました。
今回はdnsmasq以外はすべてDocker上で構築します。これは別にコンテナ至上主義とかそういうのではなく、単純にそうしたほうが楽だから程度の理由です。
Gitea
まずはソースコードをホストするためにGiteaを立てます。
Giteaはデータ格納用にPostgres・MySQL・SQL 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"
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"
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
ちゃんと失敗しています。
なおすと
成功します。
おわりに
多分ドキュメントの整備が追い付いていないのか、ちょっとドキュメント類が不足しているなというところが率直な感想です。 また、開発途中だから無いのかそれともエンタープライズ版との差別化なのかは分かりませんが、ブラウザから操作できることが非常に限られている感じです。ブラウザからは現在いくつのエージェントがぶら下がっているのかも分かりません。*3
今回はクッソ簡単な例しか試していませんが、これが大規模になった場合、ビルド実行用のDockerイメージを用意したり、用意したDockerコンテナをローカルでホストするためリポジトリ(Sonatype Nexus OSSとか)を用意したり、独自ライブラリをNuGet経由で配布するためにパッケージを作ったり、それをホストするためのNuGetリポジトリを立てたりと想像するだけで大変そうです。
逆にその辺を解決できれば自動化インフラ管理者にとっての救済となるかもしれません。
おわり
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); });
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」になっており、『はぁそうですか』ってなります。
おわり