【入門編】VB.NETでのReadOnlyDictionaryとImmutable Collections:スレッドセーフで変更不可能なコレクションによる堅牢なドメインモデル設計 – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク

VB.NETで「変更不可」な世界へ!ReadOnlyDictionaryとImmutable Collectionsで堅牢なプログラムを作る方法

こんにちは!Visual Basic (VB / VB.NET)の世界へようこそ!
マクロの記録から一歩進んで、もっとパワフルで安全なプログラムを作りたい皆さんを、この私が全力でサポートします。今回は、VB.NETのちょっと踏み込んだ、でもとっても実用的なテクニック、「ReadOnlyDictionary」と「Immutable Collections」について、基礎からじっくり解説していきますね。

「え、なんか難しそう…?」と思ったあなた、大丈夫!ここでは、まるで秘密基地を作るみたいに、一つ一つ丁寧に、そして分かりやすく、その魅力と使い方を紐解いていきますよ。

なぜ「変更不可」が重要なのか?~プログラムの「安定性」を守る鍵~

皆さんは、プログラムの中でデータ(値)を扱うとき、こんな経験はありませんか?

  • 「あれ?さっきまでこの値、こうだったはずなのに、いつの間にか変わってる!」
  • 「複数の場所から同じデータを使っているんだけど、片方で書き換えたら、もう片方も意図せず変わってしまって、原因が特定できない…」
  • 「たくさんの人が同時にプログラムを使っているんだけど、データが壊れないか心配…」

これらはすべて、プログラムの中の「データが意図せず変更されてしまう」ことから起こる、よくある問題です。特に、たくさんの人が同時にプログラムを使うような場面(マルチスレッド環境)では、この問題はさらに深刻になりがちです。まるで、みんなで一つの粘土をこねているようなもので、誰かが触っただけで形が変わってしまう、そんなイメージですね。

そこで登場するのが、今回ご紹介する 「変更不可(Immutable)」 なデータ構造です。

変更不可(Immutable)とは?

「Immutable」という言葉、初めて聞く人もいるかもしれませんね。「Immutable」とは、「不変」「変更不可能」という意味です。
つまり、Immutableなデータ構造とは、一度作ったら、その中身を後から変更できないデータ構造のことなんです。

「え、変更できないなんて不便じゃない?」と思うかもしれません。でも、これが実はプログラムを「堅牢(けんろう)」、つまり壊れにくく、安定させるための強力な武器になるんです。

変更不可のメリット:

  • 意図しない変更を防ぐ: 一度作ったら変更できないので、プログラムのどこかで勝手に値が変わってしまう、という心配がなくなります。
  • スレッドセーフ(Thread-safe): 複数の処理が同時に同じデータにアクセスしても、お互いの処理を邪魔したり、データを壊したりする心配がありません。ロック処理(後述)が不要になることが多いので、プログラムがシンプルになり、パフォーマンスも向上することがあります。
  • デバッグが楽になる: データが突然変わる心配がないので、「いつ、どこで、なぜ値が変わったのか」という原因究明が格段に楽になります。
  • コードの意図が明確になる: 「このデータは変更されないんだな」とコードを読む人がすぐに理解できるため、コードの可読性が高まります。

まるで、一度書いたら消せない「石板」に記録するようなイメージでしょうか。間違ったことを書いてしまったら、新しい石板を作るしかありませんが、その分、記録が消える心配はありません。

VB.NETの「変更不可」コレクション:ReadOnlyDictionaryとImmutable Collections

VB.NETでは、この「変更不可」なデータ構造を実現するために、いくつか便利な仕組みが用意されています。今回は、特に実務でよく使う `ReadOnlyDictionary` と、より汎用的な Immutable Collections に焦点を当ててみましょう。

1. ReadOnlyDictionary: 読み取り専用の辞書

`ReadOnlyDictionary` は、その名の通り、読み取り専用の辞書(Dictionary) です。
通常の `Dictionary` は、キーと値のペアを追加したり、削除したり、値を変更したりできますよね。しかし、`ReadOnlyDictionary` は、一度作成されたら、その中身を一切変更することができません。

どんな時に使う?

  • 設定情報や、プログラムの実行中に変更されるべきではないデータなどを保持したいとき。
  • 外部から渡された `Dictionary` を、意図せず変更されないように保護したいとき。
  • マルチスレッド環境で、複数のスレッドから安全に参照させたいデータ(ただし、初期化時や更新時は別途考慮が必要です)。

実際のコードを見てみよう!

まずは、通常の `Dictionary` を使って、後から変更できてしまう例を見てみましょう。

.net
‘ 通常のDictionaryの例
Dim normalDictionary As New Dictionary(Of String, Integer)()
normalDictionary.Add(“Apple”, 100)
normalDictionary.Add(“Banana”, 150)

Console.WriteLine($”Original Apple price: {normalDictionary(“Apple”)}”) ‘ 出力: Original Apple price: 100

‘ ここで、意図せず値が変更されてしまう可能性がある!
normalDictionary(“Apple”) = 120 ‘ 値を変更できてしまう
Console.WriteLine($”Modified Apple price: {normalDictionary(“Apple”)}”) ‘ 出力: Modified Apple price: 120

このコードだと、`normalDictionary(“Apple”)` の値を簡単に変更できてしまいます。

では、`ReadOnlyDictionary` を使ってみましょう。

.net
‘ ReadOnlyDictionaryの例
‘ まず、変更可能なDictionaryを作成します。
Dim modifiableDictionary As New Dictionary(Of String, Integer)()
modifiableDictionary.Add(“Apple”, 100)
modifiableDictionary.Add(“Banana”, 150)

‘ mutableDictionary を ReadOnlyDictionary に変換します。
‘ Dictionary.AsReadOnly() メソッドを使います。
Dim readOnlyDictionary As System.Collections.ObjectModel.ReadOnlyDictionary(Of String, Integer) = modifiableDictionary.AsReadOnly()

‘ 値の参照は通常通りできます。
Console.WriteLine($”Apple price: {readOnlyDictionary(“Apple”)}”) ‘ 出力: Apple price: 100
Console.WriteLine($”Banana price: {readOnlyDictionary(“Banana”)}”) ‘ 出力: Banana price: 150

‘ — ここからが重要 —
‘ 以下のコードはエラーになります!
‘ 変更しようとすると、InvalidOperationException が発生します。
‘ readOnlyDictionary(“Apple”) = 120 ‘ <-- この行は実行するとエラーになる! ' Dictionary に要素を追加しようとすると、NotSupportedException が発生します。 ' readOnlyDictionary.Add("Orange", 200) ' <-- この行も実行するとエラーになる! ' Dictionary から要素を削除しようとすると、NotSupportedException が発生します。 ' readOnlyDictionary.Remove("Apple") ' <-- この行も実行するとエラーになる! 【解説】

1. `New Dictionary(Of String, Integer)()` で、まず普段使っている `Dictionary` を作成します。
2. `modifiableDictionary.AsReadOnly()` というメソッドを使います。これがポイント!このメソッドは、元の `Dictionary` を「読み取り専用」のビューとして返してくれます。元の `modifiableDictionary` 自体は変更可能ですが、`readOnlyDictionary` として参照している間は変更できません。
3. `readOnlyDictionary(“Apple”) = 120` のように値を変更しようとしたり、`.Add()` や `.Remove()` で要素を操作しようとすると、例外(エラー)が発生します。これにより、意図しない変更が防がれます。

【陥りやすいエラー】

`ReadOnlyDictionary` は「ビュー」であることが多いです。つまり、元の `Dictionary` を参照しています。
もし、元の `modifiableDictionary` がプログラムの別の場所で変更されてしまうと、`readOnlyDictionary` の内容も変わってしまいます。
「完全に変更されないデータ」を保証したい場合は、後述する Immutable Collections の利用を検討しましょう。

2. Immutable Collections: 完全な「変更不可」の世界

VB.NET(.NET Framework 4.5 以降、または .NET Core/.NET 5+)では、`System.Collections.Immutable` 名前空間に、より強力な Immutable Collections が用意されています。これらは、作られた瞬間から、その内容が絶対に変わらない データ構造です。

Immutable Collections には、`ImmutableArray` (配列), `ImmutableList` (リスト), `ImmutableDictionary` (辞書), `ImmutableHashSet` (ハッシュセット) など、様々な種類があります。

どんな時に使う?

  • プログラムの実行中に絶対に値が変わっては困る、重要なデータ。
  • 複数のスレッドで安全に共有したいデータ(初期値として渡す場合など)。
  • 関数型プログラミングの考え方を取り入れたい場合。

実際のコードを見てみよう!

Immutable Dictionary を使って、変更不可能な辞書を作成してみましょう。

.net
‘ ImmutableDictionaryの例
‘ System.Collections.Immutable 名前空間をインポートする必要があります。
‘ Imports System.Collections.Immutable

‘ ImmutableDictionary を作成するには、Builder パターンを使うのが一般的です。
‘ まず、Builder を作成します。
Dim builder As ImmutableDictionary(Of String, Integer).Builder = ImmutableDictionary.CreateBuilder(Of String, Integer)()

‘ Builder に要素を追加します。
builder.Add(“Apple”, 100)
builder.Add(“Banana”, 150)

‘ Builder を使用して、ImmutableDictionary を作成します。
‘ ToImmutable() メソッドを使います。
Dim immutableDictionary As ImmutableDictionary(Of String, Integer) = builder.ToImmutable()

‘ 値の参照は通常通りできます。
Console.WriteLine($”Apple price: {immutableDictionary(“Apple”)}”) ‘ 出力: Apple price: 100
Console.WriteLine($”Banana price: {immutableDictionary(“Banana”)}”) ‘ 出力: Banana price: 150

‘ — ここからがImmutableの真骨頂 —
‘ immutableDictionary から直接要素を変更したり、追加したりすることはできません!
‘ 以下のコードはコンパイルエラー、または実行時エラーになります。

‘ immutableDictionary(“Apple”) = 120 ‘ <-- コンパイルエラー! ' immutableDictionary.Add("Orange", 200) ' <-- コンパイルエラー! ' では、どうやって「変更」するのか? ' ImmutableCollections では、「変更」という操作は、 ' 元のコレクションを基に「新しい」コレクションを作成する、という形になります。 ' 例えば、Apple の価格を 120 に変更したい場合: Dim newImmutableDictionary As ImmutableDictionary(Of String, Integer) = immutableDictionary.SetItem("Apple", 120) ' 元の immutableDictionary は一切変更されていません! Console.WriteLine($"Original Apple price after SetItem: {immutableDictionary("Apple")}") ' 出力: Original Apple price after SetItem: 100 ' 新しく作成された newImmutableDictionary は変更されています。 Console.WriteLine($"New Apple price: {newImmutableDictionary("Apple")}") ' 出力: New Apple price: 120 Console.WriteLine($"New Banana price: {newImmutableDictionary("Banana")}") ' 出力: New Banana price: 150 【解説】

1. `ImmutableDictionary.CreateBuilder(Of String, Integer)()` で、Immutable Dictionary を作成するための「工房」である Builder を作成します。
2. Builder に対して `.Add()` で要素を追加していきます。この時点では Builder は変更可能です。
3. `builder.ToImmutable()` を呼び出すことで、変更不可能な `ImmutableDictionary` が生成されます。
4. `immutableDictionary(“Apple”) = 120` や `.Add()` は、Immutable なので直接実行できません。コンパイルエラーになります。
5. もし内容を変更したい場合は、`immutableDictionary.SetItem(“Apple”, 120)` のように、元のコレクションを基に新しいコレクションを作成します。元の `immutableDictionary` はそのまま残り、新しく `newImmutableDictionary` が作られます。

【スレッドセーフのイメージ】

Immutable Collections は、作られたら絶対に変わらないため、複数のプログラム(スレッド)から同時に参照しても、誰かが勝手に書き換える心配がありません。まるで、完成した彫刻をみんなで鑑賞するようなものです。彫刻自体は変わらないので、誰が見ても同じ状態です。

3. Immutable Collections のその他の便利なメソッド

Immutable Collections には、他にも便利なメソッドがたくさんあります。

  • `.Add()` / `.AddRange()`: 新しい要素を追加した「新しい」コレクションを生成します。
  • `.Remove()` / `.RemoveRange()`: 指定した要素を削除した「新しい」コレクションを生成します。
  • `.Clear()`: 全ての要素を削除した「新しい」空のコレクションを生成します。
  • `.AddRange()`: 複数の要素を一度に追加した「新しい」コレクションを生成します。

これらのメソッドは、すべて元のコレクションを変更せず、新しいコレクションを返す、という点がImmutable Collections の最大の特徴です。

どこで使う?実践的なシナリオ

  • 設定値の管理: アプリケーションの起動時に読み込んだ設定値を、`ImmutableDictionary` で保持しておけば、実行中に設定値が意図せず変更されるのを防げます。
  • ドメインモデルの表現:

例えば、ある商品の情報を表現するクラスがあったとします。このクラスのプロパティが、一度設定されたら変更されないように設計することで、オブジェクトの状態が常に予測可能になります。
.net
‘ 例:変更不可な商品クラス
Public Class Product
Public Property Id As Integer
Public Property Name As String
Public Property Price As Decimal
‘ ReadOnly Property で、外部からの変更を防ぐ
Public ReadOnly Property Tags As ImmutableHashSet(Of String)

‘ コンストラクタで初期化
Public Sub New(id As Integer, name As String, price As Decimal, tags As IEnumerable(Of String))
Me.Id = id
Me.Name = name
Me.Price = price
‘ ImmutableHashSet の Builder を使って初期化
Me.Tags = ImmutableHashSet.CreateRange(tags)
End Sub

‘ 値を変更したい場合は、新しいインスタンスを生成して返すメソッドを作る
Public Function WithPrice(newPrice As Decimal) As Product
Return New Product(Me.Id, Me.Name, newPrice, Me.Tags)
End Function
End Class

この `Product` クラスでは、`Tags` プロパティは `ImmutableHashSet` になっており、一度設定されたら変更できません。また、`Price` を変更したい場合でも、直接 `product.Price = newPrice` とはせず、`WithPrice` メソッドのように、新しい `Product` インスタンスを生成する形をとります。これにより、オブジェクトの状態管理が非常にシンプルになります。

  • キャッシュデータ:

頻繁にアクセスするけれど、あまり更新されないデータをキャッシュとして保持する場合、`ImmutableDictionary` や `ImmutableList` を使うと、キャッシュデータが壊れる心配がなく、スレッドセーフなアクセスが保証されます。

まとめ:VB.NETで「安全」と「予測可能性」を手に入れよう!

今回は、VB.NETにおける `ReadOnlyDictionary` と Immutable Collections という、「変更不可」なデータ構造について解説しました。

  • `ReadOnlyDictionary`: 既存の `Dictionary` を読み取り専用のビューとして提供し、意図しない変更を防ぎます。
  • Immutable Collections: 作られたら絶対に内容が変わらない、真に「変更不可」なデータ構造を提供し、スレッドセーフと予測可能性を高めます。

これらのテクニックを理解し、適切に活用することで、皆さんの VB.NET プログラムは、より堅牢で、より安全で、そしてより開発しやすいものになるはずです。

最初は少し戸惑うかもしれませんが、一度この「変更不可」の考え方に慣れると、プログラムのバグが減り、コードが読みやすくなるのを実感できるはずです。「ここをクリアすれば、Visual Basic (VB / VB.NET)の基本はバッチリですよ」と、自信を持って言えるようになります。

ぜひ、皆さんの開発に取り入れてみてくださいね!応援しています!

タイトルとURLをコピーしました