こんにちは。開発のY.Mです。

プログラマ歴が長くなると、10年単位で使っていなかったプログラミング言語を再び使う時が来たりします。
前にも使っていたから大丈夫だ問題ないと思っていたら、知らない間に言語機能が拡張されていて書き方が様変わりしているのに、過去の知識のまま古臭いコード、いわゆる「レガシーコード」を書いてしまうなんて事が起こります。

VB.NETもそのひとつです。
私個人のVB.NETのイメージは「機能不足で思うようにプログラミングできない不自由な言語」でしたが、今や様々な機能拡張が図られ主要な機能はC#と遜色がありません。

本記事では「レガシーコードからの脱出」と称し、VB.NETのモダンな書き方を紹介していきます。

 

■ 「レガシーコード」と「モダンコード」

本記事では「今の時代にそぐわない古臭いコード」を「レガシーコード」と呼称します。
その「レガシーコードを近代的な書き方に改めたもの」を「モダンコード」と呼称します。

 

■ 対象となる読者

(1) VB6以前や初期のVB.NET(Visual Basic .NET 2003 辺り)での開発経験があり、長いブランクの後再び使い始めた方
(2) VB.NET以外の言語の経験があり、新たにVB.NETを使い始めた方

(1)の方は言わずもがな、(2)の方に向けてVB.NETでできる事と特有の罠について解説していきます。

 

■ 前提条件

本記事で取り上げているモダンコードは Visual Basic 2017 (Visual Studio 2017 Version 15.5 / .NET Framework 4.7.2)以上で動作します。
「Visual Basic 2017 って古すぎない?」とお思いかも知れませんが、主要な機能はその辺りで概ね整っています。マイクロソフトはC#の強化に注力しており、VB.NETのサポートは縮小傾向となっているため近年大きな変化はありません。

また、下記のコンパイルオプションが指定されている前提です。

Option Strict On   '厳密な型セマンティクスを強制適用する
Option Explicit On '変数の明示的な宣言を必要とする
Option Infer On    '変数宣言でローカル型推論を許可する

ちなみに、Legacy側のコードは敢えて冗長な書き方にしております。

 

では、始めましょう。

 

■ ローカル型推論

コンパイルオプションで Option Infer On を指定している場合、ローカル変数を宣言する際に初期値を代入していれば型宣言を省略できます。初期値を代入しない場合はこれまで通り型宣言する必要があります。

'Legacy
Dim value1 As Integer = 1
Dim value2 As String = "string"
Dim value3 As System.IO.FileAttributes = System.IO.File.GetAttributes("example.txt")


'Modern
Dim value1 = 1
Dim value2 = "string"
Dim value3 = System.IO.File.GetAttributes("example.txt")

注意点として、Nothing を初期値として代入すると変数は Object 型になってしまうので、変数を Object 型以外にしたい場合は型宣言を付けるようにします。
数値系の変数については 0 を初期値にすると Integer 型、0.0 を初期値にすると Double 型になります。厳密に型を指定したい場合は型宣言を付けるようにします。

Dim value4 = Nothing           '変数は Object 型
Dim value5 As String = Nothing '変数は String 型
Dim value6 = 0                 '変数は Integer 型
Dim value8 = 0.0               '変数は Double 型
Dim value7 As Double = 0       '変数は Double 型

 

■ オブジェクトの初期化

オブジェクトの初期化はオブジェクト初期化子 With { } を使用して簡略化できます。

Class Person
    Public Property Name As String
    Public Property Ruby As String
    Public Property Age As Integer
End Class


'Legacy
Dim person As Person
person = New Person()
person.Name = "田中一郎"
person.Ruby = "タナカイチロウ"
person.Age = 17


'Modern
Dim person As New Person With {
    .Name = "田中一郎",
    .Ruby = "タナカイチロウ",
    .Age = 17
}

VBに古くからある With ステートメントでも同じような書き方はできますが、入れ子で使うなど乱用すると可読性を落とす原因になります。
現在はこのオブジェクト初期化子然り、代わりの書き方はいくらでもあるので With ステートメントは忘れてしまって構いません。

 

■ 配列、コレクションの初期化

配列、コレクションの初期化は「コレクション初期化子」を使用して簡略化できます。
配列の場合は { } を使用します。

'Legacy
Dim strArray() As String
ReDim strArray(3)
strArray(1) = "Alfa"
strArray(2) = "Bravo"
strArray(3) = "Charlie"


'Modern
Dim strArray() = {"Alfa", "Bravo", "Charlie"}

コレクションの場合は From { } を使用します。

'Legacy
Dim strList As List(Of String)
strList = New List(Of String)
strList.Add("Alfa")
strList.Add("Bravo")
strList.Add("Charlie")


'Modern
Dim strList = New List(Of String) From {"Alfa", "Bravo", "Charlie"}

下記はコレクションとオブジェクトの初期化をまとめて行うパターンです。
Legacy側の長大な記述が、Modern側では簡潔かつ余計な変数を使用せず記述できています。

Class Person
    Public Property Name As String
    Public Property Ruby As String
    Public Property Age As Integer
End Class


'Legacy
Dim persons As List(Of Person)
persons = New List(Of Person)

Dim person1 As Person
person1 = New Person()
person1.Name = "田中一郎"
person1.Ruby = "タナカイチロウ"
person1.Age = 17
persons.Add(person1)

Dim person2 As Person
person2 = New Person()
person2.Name = "山田次郎"
person2.Ruby = "ダイゴウジガイ"
person2.Age = 18
persons.Add(person2)

Dim person3 As Person
person3 = New Person()
person3.Name = "坂井三郎"
person3.Ruby = "サカイサブロウ"
person3.Age = 19
persons.Add(person3)


'Modern
Dim persons = New List(Of Person) From {
    New Person With {.Name = "田中一郎", .Ruby = "タナカイチロウ", .Age = 17},
    New Person With {.Name = "山田次郎", .Ruby = "ダイゴウジガイ", .Age = 18},
    New Person With {.Name = "坂井三郎", .Ruby = "サカイサブロウ", .Age = 19}
}

コレクション初期化子で無名のコレクションを宣言して即時利用する事もできます。

Dim food = "カレー"
If {"トムヤムクン", "カレー", "麻婆豆腐"}.Contains(food) Then
    Console.WriteLine("から~い!")
End If

 

■ 暗黙的な行の継続 / 式の改行で _ が不要(条件あり)

先程のサンプルコードを見て気付かれたかと思いますが、式の途中で改行する際の _ は必要なくなっています。ただし条件があって、演算子や } , などの後で改行しなければなりません。間違っていればコンパイルエラーになるのですぐわかります。
また、改行した式の途中でコメントが書けるようになっています。ただし、行全体をコメントアウトするような事はできません。
Visual Basic 16.0 (Visual Studio 2019) 以降であれば行頭に _ を付ければ行全体のコメントが可能です。

Dim persons = New List(Of Person) From { '行の末尾にコメントはOK
    ',New Person With {.Name = "田中一郎", .Ruby = "タナカイチロウ", .Age = 17},    行全体のコメントは不可
    _ ',New Person With {.Name = "山田次郎", .Ruby = "ダイゴウジガイ", .Age = 18},  行頭に _ を付ければ行全体のコメント可能(Visual Basic 16.0 以降)
    New Person With {.Name = "坂井三郎", .Ruby = "サカイサブロウ", .Age = 19}
}

 

■ 引数の ByVal 不要

VB.NETでは関数の引数はデフォルトで値渡しとなっており、ByVal の記述は不要です。
参照渡しの場合はこれまでどおり ByRef を記述します。

'Legacy
Sub SubLegacy(ByVal arg1 As String, ByRef arg2 As String)

End Sub


'Modern
Sub SubModern(arg1 As String, ByRef arg2 As String)

End Sub

VB6やVBAではデフォルトが参照渡しであるため、そちらからVB.NETに移行してきた方は ByVal を付けてしまいがちです。付けていても間違いではないのですが、ByRef 引数と混在していると見分けにくくなってしまいますので不要な記述は省いた方がよいでしょう。

 

■ 自動実装プロパティ

クラスのプロパティ宣言で、Get、Set で特別な処理をする必要が無ければ Get、Set の記述は省略できます。

'Legacy
Public Class Legacy1

    Private _Value As String

    Public Property Value() As String
        Get
            Return _Value
        End Get
        Set(value As String)
            _Value = value
        End Set
    End Property

    Public Sub New()
        _Value = "DefaultValue"
    End Sub

End Class


'Modern
Public Class Modern1

    Public Property Value As String = "DefaultValue" 'Get/Setと初期値の設定が1行で書ける

End Class

Modern側では暗黙的に _Value のプライベート変数が宣言され、Value プロパティに対する Get、Set が割り当てられます。

読み取り専用プロパティの宣言には ReadOnly 修飾子を付けます。
Set に何も書かない事で読取専用にする実装方法がありますが、それだと誤って読み取り専用フィールドに値をセットしようとしてもコンパイルエラーにならないためコーディングミスの検知ができません。

'Legacy
Public Class Legacy2

    Private _ReadonlyValue As String

    Public Property ReadonlyValue() As String
        Get
            Return _ReadonlyValue
        End Get
        'Setで代入処理をしない事で読み取り専用にする。
        'この方法では読み取り専用フィールドに値をセットしようとしても
        'コンパイルエラーにならないためコーディングミスの検知ができない。
        Set(value As String)
        End Set
    End Property

    Public Sub New(ByVal value As String)
        _ReadonlyValue = value
    End Sub

End Class


'Modern
Public Class Modern2

    Public ReadOnly Property ReadonlyValue As String

    Public Sub New(value As String)
        ReadonlyValue = value 'コンストラクタ内では ReadonlyValue プロパティへ直接書き込み可能
    End Sub

    Public Sub SetReadonlyValue(value As String)
        _ReadonlyValue = value 'コンストラクタ以外では暗黙的に宣言される _ReadonlyValue プライベート変数へ書き込む
    End Sub

End Class

逆の WriteOnly 修飾子もあり、こちらはプロパティにパスワードなどの秘匿情報をセットし、クラス外部から参照できなくする用途などに使えます。

 

■ リソースの管理

ファイルの読み書きやテーブル参照など、処理が終わった後確実にリソースを開放する処理として慣例的に Try ~ Finally が使われてきました。

'Legacy
Dim sr As StreamReader = Nothing
Try
    sr = New StreamReader("xfile.txt")
    While Not sr.EndOfStream
        Dim line As String = sr.ReadLine()
        Console.WriteLine(line)
    End While
Finally
    If Not sr Is Nothing Then
        sr.Dispose()
    End If
End Try

しかし、Try ~ Finally は本来例外処理の構文であってリソース管理を目的とした構文ではありません。

リソース管理には Using ステートメントを使用します。
それによりリソース管理をしている事をコード上で明示することができ、記述も簡潔になります。

'Modern
Using sr = New StreamReader("xfile.txt")
    While Not sr.EndOfStream
        Dim line = sr.ReadLine()
        Console.WriteLine(line)
    End While
End Using

Using のブロック内で Return や例外が発生しても必ずオブジェクトに対して Dispose() が呼び出され、リソースが解放されます。

 

■ タプル(名前付きタプル)

関数の戻り値として複数の値を受け取りたい場合、ByRef 引数を渡して受け取る方法や、戻り値にクラスや構造体を指定する方法が思いつくでしょう。
こういう時は関数の戻り値に「タプル(名前付きタプル)」を使用する事で簡便に記述できます。

'Legacy
Function IsOndam(value As String, ByRef resultMessage As String) As Boolean

    If value.ToLower = "ondam" Then
        resultMessage = "○ンダムだ。"
        Return True
    Else
        resultMessage = "○ンダムではない。"
        Return False
    End If

End Function


Dim resultMessage As String
resultMessage = ""
Dim result = IsOndam("gangaru", resultMessage)
Console.WriteLine(resultMessage)


'Modern
Function IsOndam(value As String) As (Result As Boolean, Message As String)

    If value.ToLower = "ondam" Then
        Return (Result:=True, Message:="○ンダムだ。")
    Else
        Return (Result:=False, Message:="○ンダムではない。")
    End If

End Function


Dim result = IsOndam("gangaru")
Console.WriteLine(result.Message)

ByRef 引数を使わない事でどれが戻り値なのかが明確になります。またクラスや構造体を使用する場合のように別途宣言する必要がありません。
更にC#ではタプル内の値を直接変数へ代入できるのですが、残念ながらVB.NETではサポートされていません。

// C# Code
var (Result, Message) = IsOndam("gangaru");

関数の戻り値以外にもタプルの使い道は色々あります。
先のサンプルコードにあった persons を初期化する処理をクラスを使用せずタプルで書くとこんな感じになります。

Dim persons = New List(Of (Name As String, Ruby As String, Age As Integer)) From {
    (Name:="田中一郎", Ruby:="タナカイチロウ", Age:=17),
    (Name:="山田次郎", Ruby:="ダイゴウジガイ", Age:=18),
    (Name:="坂井三郎", Ruby:="サカイサブロウ", Age:=19)
}

見た感じのイメージは無名の構造体ですね。

 

■ 文字の連結、埋込

文字列に値を埋め込む方法には & での連結や String.Format() などがありますが、新たに「補間文字列」が利用できるようになっています。

'Legacy
Console.WriteLine("名前:" & person.Name & " フリガナ:" & person.Ruby & " 年齢:" & person.Age)
Console.WriteLine(String.Format("名前:{0} フリガナ:{1} 年齢:{2}", person.Name, person.Ruby, person.Age))


'Modern
Console.WriteLine($"名前:{person.Name} フリガナ:{person.Ruby} 年齢:{person.Age}")

文字列の ” の前に $ を記述し、文字列中で値を埋め込みたい場所に {} で囲って埋め込みます。
埋め込めるのは変数、定数、関数など値を返すもので、使用できない場合はコンパイルエラーになります。

{ } を普通に文字列として出力したい場合は {{ }} と記述します。
String.Format() 同様、埋め込む値のフォーマットを指定できます。

Console.WriteLine($"現在日時 {Date.Now:yyyy/MM/dd hh:mm:ss}")

: に続くフォーマット部分に定数や変数は指定できないようです。

 

■ 長文の初期化(ヒアドキュメント)

SQL文など長文の初期化には、愚直に文字列連結したり StringBuilder を使ったりしていましたが、Visual Basic 14.0 以降は文字列中で改行ができるようになったので記述が圧倒的に楽になりました。

'Legacy
Dim sql1 As String
sql1 = ""
sql1 = sql1 & "select" & vbCrLf
sql1 = sql1 & "  Name" & vbCrLf
sql1 = sql1 & " ,Ruby" & vbCrLf
sql1 = sql1 & " ,Age" & vbCrLf
sql1 = sql1 & "from persons" & vbCrLf
sql1 = sql1 & "where Age >= @Age" & vbCrLf
sql1 = sql1 & "order by Age" & vbCrLf

Dim sb As StringBuilder
sb = New StringBuilder()
sb.AppendLine("select")
sb.AppendLine("  Name")
sb.AppendLine(" ,Ruby")
sb.AppendLine(" ,Age")
sb.AppendLine("from persons")
sb.AppendLine("where Age >= @Age")
sb.AppendLine("order by Age")
Dim sql2 As String
sql2 = sb.ToString()


'Modern
Dim sql1 = "
    select
      Name
     ,Ruby
     ,Age
    from persons
    where Age >= @Age
    order by Age
"

改行ができるようになったのはよいのですが、行頭インデントの空白やタブがそのまま文字列に含まれる、改行コードはプログラムコードと同じものになる弊害があります。行頭の空白やタブを除くにはインデントを無くす必要があるので可読性が損なわれます。
HTMLやSQL文など、弊害の影響を受けにくい文字列の初期化には使えそうです。SQL文についてはSQLクライアントで動作確認した後そのままソースコードへコピペできるので、SQL文をコード化する際の記載ミスを減らせるのではないかと思います。

 

■ And と Or の罠

If文などで使用する And と Or はVB.NET以外の言語に習熟している方が引っ掛かる罠の代表格です。

Dim strArray() As String = {}

If strArray.Length > 0 And strArray(1) <> "" Then
    '処理
End If

上記の例は実行時に範囲エラーの例外が発生します。
And や Or では評価する必要のない後続のオペランド strArray(1) <> “” まで評価してしまうためです。

不要な後続のオペランドの評価をさせないためには AndAlso や OrElse を使用します。

If strArray.Length > 0 AndAlso strArray(1) <> "" Then
    '処理
End If

太古から続くBasic言語の And や Or の挙動を変える訳にはいかないので、AndAlso、OrElse を追加することで既存のコードへの影響を回避したのだと思われます。
特別な事情がない限りは AndAlso、OrElse を使うようにしましょう。

 

■ IIf 関数 → If 演算子

IIf 関数は If 演算子に置き換える事ができます。

'Legacy
Dim num1 = 1
Dim str2 = IIf(num1 = 1, "Yes", "No")


'Modern
Dim num1 = 1
Dim str1 = If(num1 = 1, "Yes", "No")

上記の例では I がひとつ減っただけで結果は同じです。

ですが次の例では話が違ってきます。
personA が Nothing の場合 personB の Name を取得するコードです。

Dim personA As Person = Nothing
Dim personB As New Person With {.Name = "山田次郎", .Ruby = "ダイゴウジガイ", .Age = 18}


'Legacy
Dim personName = IIf(personA Is Nothing, personB.Name, personA.Name)


'Modern
Dim personName = If(personA Is Nothing, personB.Name, personA.Name)

Legacy側のコードは実行時に例外が発生します。
IIf は関数であり、呼び出される際に偽の値である第3引数に指定された personA.Name も評価されます。personA は Nothing なのでNullオブジェクト参照の例外が発生します。

一方 If は演算子であり、ショートサーキット評価が使用されます。条件が真の場合は personB.Name のみが評価され、personA.Name は評価されず例外は発生しません。

If 演算子はVB.NETにおける三項演算子の位置付けです。
IIf 関数の方は使うべき理由がないので忘れてしまいましょう。

 

■ Null条件演算子とNull許容型

If 演算子の項での挙げたサンプルコードは personA、personB 両方が Nothing の場合はNullオブジェクト参照の例外が発生します。

Dim personA As Person = Nothing
Dim personB As Person = Nothing

Dim personName = If(personA Is Nothing, personB.Name, personA.Name)

コードをIf文に置き換える必要はなく「Null条件演算子」を用いることで例外は発生しなくなります。

Dim personName = If(personA Is Nothing, personB?.Name, personA?.Name)

プロパティ名の前にある . を ?. に書き換えるだけです。
短縮して下記のように記述する事も可能です。

Dim personName = If(personA?.Name, personB?.Name)

personA、personB 両方が Nothing の場合 personName に代入される値は Nothing になりますので、変数の型が「Null許容型」である必要があります。String型はデフォルトでNull許容型ですが、Integer型などは違いますので型宣言する際はNull許容型として宣言する必要があります。
Null許容型で型宣言するには元となる型の末尾に ? を付けるか、 Nullable(Of {元となる型}) で宣言します。

'例のため変数宣言を別に記述
Dim personAge As Integer?
Dim personAge As Nullable(Of Integer) 'こちらの書き方でも同じだが冗長
personAge = If(personA?.Age, personB?.Age)


'ローカル型推論では自動的に Integer? になる
Dim personAge = If(personA?.Age, personB?.Age)

通常の型とNull許容型は型の互換がありません。通常の型に代入する際は型キャストが必要です。

Dim personAge = If(personA?.Age, personB?.Age)

Dim personAge2 As Integer
personAge2 = CInt(personAge)

 

■ ジェネリック

ジェネリックのクラスについては既に実例を挙げていて、先のサンプルコードで使用している List クラスがジェネリックそのものです。
型宣言時に (Of に続けてリストに格納する値の型やクラスを指定します。

Dim strLlist As List(Of String) 'String 型のリスト
Dim intList As List(Of Integer) 'Integer 型のリスト
Dim persons As List(Of Person)  'Person クラスのリスト

ジェネリックを利用しない場合は各々の型やクラスに対して専用のクラスを用意しなくてはなりませんが、ジェネリックを利用すればひとつのクラスで様々な型やクラスに対応できるようになります。

ジェネリックはクラスだけでなく関数(メソッド)でも利用できます。
例として、Dictionary クラスに格納されている文字データをクラスにマッピングする処理があるとします。ジェネリックを使用しない場合はクラスごとにマッピング用の関数を用意する必要がありますが、ジェネリックを利用すれば共通の関数ひとつで処理する事も可能です(MappingData 関数内で使用している  For Each 文については後程説明します)。

'顧客情報
Class Customer
    Public Property Name As String
    Public Property EMail As String
    Public Property Phone As String
End Class

'商品情報
Class Merchandise
    Public Property Category As String
    Public Property Name As String
End Class


'Legacy ジェネリック不使用

'Dictionary を Customer クラスへマッピングする関数
Function MappingDataCustomer(data As Dictionary(Of String, String)) As Customer
    Dim result = New Customer

    result.Name = If(data.ContainsKey("Name"), data("Name"), Nothing)
    result.EMail = If(data.ContainsKey("EMail"), data("EMail"), Nothing)
    result.Phone = If(data.ContainsKey("Phone"), data("Phone"), Nothing)

    Return result
End Function

'Dictionary を Merchandise クラスへマッピングする関数
Function MappingDataMerchandise(data As Dictionary(Of String, String)) As Merchandise
    Dim result = New Merchandise

    result.Category = If(data.ContainsKey("Category"), data("Category"), Nothing)
    result.Name = If(data.ContainsKey("Name"), data("Name"), Nothing)

    Return result
End Function

Sub GenericLegacy()
    Dim customerData = New Dictionary(Of String, String) From {
        {"Name", "田中一郎"},
        {"EMail", "ichirotanaka@example.com"},
        {"Phone", "999-9999-9999"}
    }

    Dim merchandiseData = New Dictionary(Of String, String) From {
        {"Category", "食品"},
        {"Name", "白米"}
    }

    'クラスごとに関数を用意する必要あり
    Dim customer = MappingDataCustomer(customerData)
    Dim merchandise = MappingDataMerchandise(merchandiseData)
End Sub


'Modern ジェネリック使用

'Dictionary をジェネリックで指定したクラスへマッピングする関数
Function MappingData(Of T As {Class, New})(data As Dictionary(Of String, String)) As T
    Dim result = New T

    'Dictionary のキー値と名前が一致するクラスメンバに値を代入する
    For Each key In data.Keys
        Dim prop = result.GetType.GetProperty(key)
        prop?.SetValue(result, data(key))
    Next

    Return result
End Function

Sub GenericModern()
    Dim customerData = New Dictionary(Of String, String) From {
        {"Name", "田中一郎"},
        {"EMail", "ichirotanaka@example.com"},
        {"Phone", "999-9999-9999"}
    }

    Dim merchandiseData = New Dictionary(Of String, String) From {
        {"Category", "食品"},
        {"Name", "白米"}
    }

    '共通の関数ひとつで処理可能
    Dim customer = MappingData(Of Customer)(customerData)
    Dim merchandise = MappingData(Of Merchandise)(merchandiseData)
End Sub

 

■ ラムダ式(無名関数、匿名関数)

ラムダ式は無名関数、匿名関数とも呼ばれ、後述する ForEach メソッドや LINQ を利用する際に必要となってくる記法です。

Dim AddNya = Function(word As String) word & "にゃ"

Dim AddWan = Function(word As String)
                 If {"わ", "は"}.Contains(Strings.Right(word, 1)) Then
                     word = word.Substring(0, word.Length - 1)
                 End If
                 Return word & "わん"
             End Function

Console.WriteLine(AddNya("こんにちは"))
Console.WriteLine(AddWan("こんにちは"))

ラムダ式は名前を持たないので、変数へ代入するかそのまま関数やメソッドの引数として利用します(引数としての利用方法は後程説明します)。
ラムダ式内の式がひとつだけであれば AddNya のように1行で書けます。式の結果が戻り値となるため Return の記述は必要なく、型推論により型指定も不要です。
AddWan の方はラムダ式を複数行で書いた場合です。通常の関数と同じく制御構文が記述できます。戻り値に関しては Return の記述が必要です。
ラムダ式の実行はラムダ式を代入した変数名を関数名として指定します。

残念ながら、ラムダ式でジェネリックは使えません。

'こういう使い方はできない。
Dim OutputType = Sub(Of T)(value As T)
                     Console.WriteLine(value.GetType)
                 End Sub

 

■ For → For Each

VB.NETでも他の言語でお馴染みの For Each が使えます。

'Legacy
Dim i As Integer
For i = 0 To persons.Count - 1
    Console.WriteLine("名前:" & persons(i).Name)
    Console.WriteLine("フリガナ:" & persons(i).Ruby)
    Console.WriteLine("年齢:" & persons(i).Age)
Next


'Modern
For Each person In persons
    Console.WriteLine($"名前:{person.Name}")
    Console.WriteLine($"フリガナ:{person.Ruby}")
    Console.WriteLine($"年齢:{person.Age}")
Next

For Each ではリストの要素数やインデックスを意識しなくて済むのがよい所です。
他の言語の For Each に類する処理では要素の先頭から順番で処理されない場合がありますが、VB.NETの場合は先頭から処理されます(C#も同様です)。

また、List クラスや Dictionary クラスには ForEach メソッドが用意されています。

persons.ForEach(
    Sub(x)
        Console.WriteLine($"名前:{x.Name}")
        Console.WriteLine($"フリガナ:{x.Ruby}")
        Console.WriteLine($"年齢:{x.Age}")
    End Sub)

ForEach メソッドの引数には処理を行うコールバック関数のアドレスを指定します。先程紹介したラムダ式を使用し、処理の内容をその場に直接記述するのが一般的な使い方だと思います。
ForEach メソッドはループを Break で中断できませんが、要素すべてに対して処理をする目的であればできなくとも構いませんし、単純な処理であれば1行で記述できる利点があります。

余談ですが、条件に合う要素を抜き出す場合は ForEach メソッドではなく FindAll メソッドを使い、処理の目的がメソッド名でわかるようにする方がよいです。

For Each があるから For は必要なくなったわけではありません。
最後から処理する( For i = 100 To 0 Step -1 )、いくつかおきに処理する( For i = 0 To 100 Step 2 )などを For 以外で書こうとすると余計な処理が必要になり、かえってややこしくなります。
他にはパフォーマンス重視の処理で For が最速になる場合ですね。

 

■ LINQ(統合言語クエリ)

LINQ(リンク)は配列やリストのようなデータの集まりに対し、データの抽出や加工、集計などを行い別のデータの集まりを作る機能です。非常に大雑把ですが、データベーステーブルからSQLでデータを抽出するのと同じような事を、配列やリストに対してループ処理を書かずにできるものととらえてもらえればよいです。

先のサンプルコードにあった persons から18歳以上の名前を列挙する処理を例に取ってみます。

'Legacy
Dim selectedPersonNames() As String
Dim i As Integer
Dim cnt As Integer
cnt = 0
For i = 0 To persons.Count - 1
    If persons(i).Age >= 18 Then
        cnt = cnt + 1
        ReDim Preserve selectedPersonNames(cnt)
        selectedPersonNames(cnt - 1) = persons(i).Name
    End If
Next


'Modern
Dim selectedPersonNames = persons.Where(Function(x) x.Age >= 18).Select(Function(x) x.Name).ToArray

For 文のループで長々と書いていた処理が、LINQ のメソッド構文で書くと一気にコード量が減って1行で書けてしまいます。また、For 文に比べて処理の目的がはっきりします。Where で対象を絞り込み、Select で取得する値を指定している事が一目でわかります。

LINQはとても便利なのですが、先に挙げたラムダ式や型推論など前提となる知識がないと処理の内容が理解できません。使いこなすにはVB.NETの習熟度が一定以上必要ですが、本記事で挙げているモダンコードの内容を理解できるなら問題ないでしょう。

LINQ の書き方は上記のメソッド構文の他にクエリ構文もあります。クエリ構文の方は書き方がSQL文に近いので、SQLに習熟している方にはとっつきやすいと思います。

Dim selectedPersonNames = (
        From x In persons
        Where x.Age >= 18
        Select x.Name
    ).ToArray

LINQの注意点として、大量のデータを扱うと処理が極端に遅くなるおそれがあります。
全件を For Each でループさせているのと同じかそれ以上の時間がかかる事が想定されますので、LINQに対して処理の高速化は期待しない方がよいです。
データ量が多くなりそうならベンチマークを取って For や For Each の方が高速ならそちらを使う、他の条件(SQLなど)で可能な限りデータを絞り込んでからLINQを使うのがよいでしょう。

 


以上、様々なレガシーコードとモダンコードを紹介しました。
書いているうちに結構な分量になってしまい、読むのが大変だったかと思います。それでもVB.NETの拡張された機能を網羅しているわけではなく、細かな言語仕様にも触れておりません。
本記事は入口程度のものととらえて頂き、詳しくは言語リファレンス等を参照頂ければと思います。