構造体とは既存のデータ型を組み合わせた構造を持つ型のことです。 構造体では複数の値(データ型)を組み合わせることにより、それらに意味を持たせた一つの型を作成することができます。 VB6以前でユーザー定義型と呼ばれていたものは、VB.NETでは構造体という名称に変わり、クラスと同様に構造体がプロシージャ(メソッド)を持つことができるようになっているなど、機能も大幅に強化されています。

構造体の宣言

まずは構造体の宣言について、その構文を見てみます。 ここでは長方形の座標を扱うRECT構造体に相当する構造体を例に挙げています。 比較のため、同等の型をVB6のユーザー定義型、C言語の構造体として宣言した場合についても併記しています。

VB.NETでの構造体の宣言
Structure Rect
  Dim Left As Integer
  Dim Top As Integer
  Dim Right As Integer
  Dim Bottom As Integer
End Structure
VB6以前でのユーザー定義型の宣言
Type RECT
  Left As Long
  Top As Long
  Right As Long
  Bottom As Long
End Type
C言語での構造体の宣言
struct RECT
{
  long Left;
  long Top;
  long Right;
  long Bottom;
};

(上記のコードについて、VB6以前ではLongが32ビット、VB.NETではInteger32ビットの整数を表すという違いがあります。)

VB.NETではTypeEnd Typeではなく、StructureEnd Structureを用いて構造体を宣言します。 また、VB.NETの構造体ではフィールド(構造体内の変数・メンバ変数)やプロシージャ(メソッド)を含むすべてのメンバにアクセシビリティを設定できるようになっています。 そのためメンバはDimまたはPublicPrivateなどのアクセス修飾子を用いて宣言します。 なお、Dimで宣言した構造体のメンバはPublicと同じアクセス範囲となります。 そのため、上記の例にあるフィールド(構造体内の変数)はすべてPublicとなります。

このように宣言した構造体を実際に使う場合は次のようになります。 構造体変数.メンバ名の形式で構造体内の各メンバにアクセスできるようになっています。 このように、使用する場合の構文についてはVB6と概ね変わりありません。

' 構造体型変数の宣言
Dim r As Rect

' 各メンバに値を設定する場合
r.Left = 0
r.Top = 0
r.Right = 320
r.Bottom = 240

Withステートメントを使用することにより、メンバの参照時に構造体変数の記述を省略することもできます。

' 構造体型変数の宣言
Dim r As Rect

' 変数rの記述を省略してメンバを参照する
With r
  .Left = 0
  .Top = 0
  .Right = 320
  .Bottom = 240
End With

構造体を宣言する際の命名基準について、VB.NETでは構造体の場合もクラスと同様の命名方法、つまりすべて大文字からなる名前は付けないようにガイドラインで定められています。 (例えば、RECTよりはRectが推奨される) ただ、あくまで指針であるので、わかりやすさが維持される限りは自由に名前を付けることができます。

ちなみに、長方形の座標を扱う構造体はSystem.Drawing名前空間にRectangle構造体として既に.NET Frameworkに用意されています。 そのため、Rectangle構造体が使用できるのであれば上記のような構造体をわざわざ独自に宣言する必要はありません。

メソッド

VB.NETでの構造体の仕様はVB6のユーザー定義型よりもC++の構造体に近くなっていて、クラスに近い機能を持っています。 その一つとして、VB.NETの構造体はメソッドを持つことができるようになっています。

例えば次のサンプルでは、先ほどのRect構造体に対してメンバの値を設定するためのメソッドを追加しています。

Structure Rect
  Dim Left As Integer
  Dim Top As Integer
  Dim Right As Integer
  Dim Bottom As Integer

  ' 構造体の各メンバ変数に対して一度に値を設定するためのメソッド
  Sub SetRect(ByVal left As Integer, ByVal top As Integer, ByVal right As Integer, ByVal bottom As Integer)
    Me.Left = left
    Me.Top = top
    Me.Right = right
    Me.Bottom = bottom
  End Sub
End Structure

Module Sample
  Sub Main()
    ' 構造体型変数の宣言
    Dim r As Rect

    ' メソッドを使って各メンバに値を設定する
    r.SetRect(0, 0, 320, 240)
  End Sub
End Module

このように、VB.NETでは構造体にメソッドを持たせることができます。 メソッド以外にも、コンストラクタプロパティを構造体に追加することもできます。

なお、上記の例におけるMeキーワードは、構造体自身がメンバを参照するためのキーワードです。 このメソッドでのMeは、メソッドの引数leftと構造体メンバのLeftを区別するために用いています。 このような区別をする必要がなければMeは不要です。 例えば次のように構造体メンバとは異なる引数名にすればMeキーワードは不要となり、わざわざ明示する必要はなくなります。

Sub SetRect(ByVal l As Integer, ByVal t As Integer, ByVal r As Integer, ByVal b As Integer)
  Left = l
  Top = t
  Right = r
  Bottom = b
End Sub

値を返さないメソッドであるSubプロシージャだけでなく、当然値を返すメソッドであるFunctionプロシージャを構造体に持たせることも可能です。

' 長方形の幅を返すメソッド
Function GetWidth() As Integer
  Return Right - Left
End Function

' 長方形の高さを返すメソッド
Function GetHeight() As Integer
  Return Bottom - Top
End Function

ただ、この場合はメソッドよりもプロパティにした方が設計的には優れているかもしれません。 プロパティについては後述します

構造体とクラスの共通点・相違点

構造体とクラスは多くの共通する機能を持っていますが、一方で複製時のコストなど実行時のパフォーマンスに影響するような大きな違いもあります(値型と参照型)。 そういった違いを意識せずにクラスと構造体の選択を安易に行うと、一見すると不可思議なエラーに遭遇する場合もあります。 そのため、機能は似ていたとしても構造体とクラスは適切に使い分ける必要があります。

構造体とクラスの共通点・相違点に関してはクラスでの解説も合わせて参照してください。

コンストラクタ (初期値の設定)

構造体にNewという名前のメソッドを作成すると、それはコンストラクタとして扱われます。 コンストラクタはNewステートメントで構造体変数を宣言するときに呼び出される特殊なメソッドで、主にメンバ変数の初期化を行うために用います。

次のコードは先ほどのRect構造体に、宣言時に初期化できるようなコンストラクタを追加した例です。

Structure Rect
  Dim Left As Integer
  Dim Top As Integer
  Dim Right As Integer
  Dim Bottom As Integer

  ' コンストラクタ
  Sub New(ByVal l As Integer, ByVal t As Integer, ByVal r As Integer, ByVal b As Integer)
    Left = l
    Top = t
    Right = r
    Bottom = b
  End Sub

  ' 通常のメソッド
  Sub SetRect(ByVal l As Integer, ByVal t As Integer, ByVal r As Integer, ByVal b As Integer)
    Left = l
    Top = t
    Right = r
    Bottom = b
  End Sub
End Structure

変数宣言時にこのコンストラクタを使用する場合は次のようにNewキーワードを使用します。

' コンストラクタを使用した例
Dim r As New Rect(160, 120, 640, 480)

' 次のコードは上のコードと同等で、同じ結果となります
Dim r As Rect

r.SetRect(160, 120, 640, 480)

クラスのコンストラクタとは異なり、構造体におけるコンストラクタは常に1つ以上の引数を取る必要があります。 構造体においては、引数のないコンストラクタは暗黙のコンストラクタとして自動的に用意されるため、そのようなコンストラクタを作成することはできません。

コンストラクタをもつ構造体の場合でも、作成したコンストラクタを使用せず初期化することが出来ます。 この場合、暗黙のコンストラクタが呼び出されます。 ここまでの例で明らかなように、必要がなければコンストラクタを用意しなくてもよく、またコンストラクタが無くても暗黙のコンストラクタによって構造体のインスタンスを作成することができます。

' コンストラクタを使用しない(暗黙のコンストラクタによって初期化する)例
Dim r As New Rect

Console.WriteLine("({0}, {1})-({2}, {3})", r.Left, r.Top, r.Right, r.Bottom)
実行結果
(0, 0)-(0, 0)

なお、暗黙のコンストラクタが呼び出される場合、構造体内の各フィールドは0など(参照型ならNothing)のデフォルト値に初期化されます。

型とそのデフォルト値については型の種類・サイズ・精度・値域 §.型のデフォルト値を参照してください。

Newキーワードを使用せずに単に変数として宣言した場合もこれと同じ動作となります。 言い換えると、明示的にコンストラクタを呼び出さない場合は、変数として宣言した時点で暗黙のコンストラクタにより初期化されるとも言えます。

' 構造体変数を宣言する
' (この際、暗黙のコンストラクタにより初期化される)
Dim r As Rect

Console.WriteLine("({0}, {1})-({2}, {3})", r.Left, r.Top, r.Right, r.Bottom)
実行結果
(0, 0)-(0, 0)

オブジェクト初期化子 (初期化と値の設定)

コンストラクタのない構造体や、暗黙のコンストラクタを使って構造体を初期化する際、オブジェクト初期化子を使うと特定のメンバ変数に初期値を設定することができます。 オブジェクト初期化子はVB9(VB2008)で導入されたもので、次の例のようにWithキーワードを使って構造体の初期化と値の設定を同時に行うことができます。

' オブジェクト初期化子を使ってメンバ変数の値を設定する例
Dim r As New Rect With {.Left = 240, .Right = 480}

Console.WriteLine("({0}, {1})-({2}, {3})", r.Left, r.Top, r.Right, r.Bottom)
実行結果
(240, 0)-(480, 0)

このように、Newキーワードを使った変数宣言に続けてWith {.メンバ変数 = , ...}と記述することで構造体の特定のメンバ変数に値を設定することができます。 オブジェクト初期化子は引数のあるコンストラクタと同時に使用することもできます。

オブジェクト初期化子が使用できないVB8(VB2005)以前の場合は、Withステートメントを使うことができます。 オブジェクト初期化子とWithステートメントによる値の設定はどちらも同等です。

' Withステートメントを使ってメンバ変数の値を設定する例
Dim r As New Rect

With r
    .Left = 240
    .Right = 480
End With

Console.WriteLine("({0}, {1})-({2}, {3})", r.Left, r.Top, r.Right, r.Bottom)
実行結果
(240, 0)-(480, 0)

ゼロクリア (再初期化)

既に値が代入・設定されている構造体変数を初期化するには、Nothingを代入します。 Nothingを代入することで、構造体内の各フィールドには0など(参照型ならNothing)のデフォルト値が設定された状態になります。 これにより、構造体に対してゼロクリアに相当する再初期化を行うことが出来ます。 (型とそのデフォルト値については型の種類・サイズ・精度・値域 §.型のデフォルト値を参照してください)

Structure Rect
  Dim Left As Integer
  Dim Top As Integer
  Dim Right As Integer
  Dim Bottom As Integer

  Sub New(ByVal l As Integer, ByVal t As Integer, ByVal r As Integer, ByVal b As Integer)
    Left = l
    Top = t
    Right = r
    Bottom = b
  End Sub
End Structure

Module Sample
  Sub Main()
    ' 構造体変数を宣言し、初期値を設定
    Dim r As New Rect(160, 120, 640, 480)

    Console.WriteLine("({0}, {1})-({2}, {3})", r.Left, r.Top, r.Right, r.Bottom)

    ' Nothing(構造体の初期値)を代入して変数を再初期化
    r = Nothing

    Console.WriteLine("({0}, {1})-({2}, {3})", r.Left, r.Top, r.Right, r.Bottom)
  End Sub
End Module
実行結果
(160, 120)-(640, 480)
(0, 0)-(0, 0)

構造体変数をNothing(何も参照していない状態・ヌル参照)にすることは出来ないことから、Nothingを代入することで変数を再初期化するのは一見すると予想と反する動作に見える点で注意が必要です。 Nothingの代入は、実際にはフィールドをすべてデフォルト値に設定するというよりは、暗黙のコンストラクタで初期化された値を代入して上書きすると言った方が適切でしょう。

次のように暗黙のコンストラクタを使って再初期化しても結果は同じで、両者は見た目・記述の違い程度でしかないため、コーディング規約や読みやすさなどを考慮して適切な方を選ぶとよいでしょう。

' 構造体変数を宣言し、初期値を設定
Dim r As New Rect(160, 120, 640, 480)

Console.WriteLine("({0}, {1})-({2}, {3})", r.Left, r.Top, r.Right, r.Bottom)

' 暗黙のコンストラクタで初期化された値を代入して変数を再初期化
r = New Rect

また、あらかじめ初期値に相当する値を定数として用意しておいて、それを代入するという方法も取れます。 .NET Frameworkでも、Rectangle.EmptyPoint.EmptyDecimal.Zeroなどといったフィールドがそのような方法にも使えるよう用意されています。 Nothingの代入を行うより、こういったフィールドを使用する方がコードの意図がより明確にすることができます。

次の例は先のコードに追記してEmptyというフィールドを用意し、その値を代入することで変数を初期化しています。

Structure Rect
  Dim Left As Integer
  Dim Top As Integer
  Dim Right As Integer
  Dim Bottom As Integer

  Sub New(ByVal l As Integer, ByVal t As Integer, ByVal r As Integer, ByVal b As Integer)
    Left = l
    Top = t
    Right = r
    Bottom = b
  End Sub

  ' 初期値となる値の定数 (初期値で初期化された読み取り専用の値)
  Public Shared ReadOnly Empty As Rect = New Rect
End Structure

Module Sample
  Sub Main()
    ' 構造体変数を宣言し、初期値を設定
    Dim r As New Rect(160, 120, 640, 480)

    Console.WriteLine("({0}, {1})-({2}, {3})", r.Left, r.Top, r.Right, r.Bottom)

    ' 構造体の初期値の定数を代入して変数を初期化
    r = Rect.Empty

    Console.WriteLine("({0}, {1})-({2}, {3})", r.Left, r.Top, r.Right, r.Bottom)
  End Sub
End Module

ヌル許容型を使用すると、構造体型変数の初期化ではなく実際にNothingを代入するようにすることもできます。 詳しくはヌル許容型を参照してください。

プロパティ

プロパティはすでにVBを使ったことがある方であれば概要は分かると思います。 プロパティは値の取得・設定をメソッド形式で行う代わりに、メンバ変数と同じ記述で行えるようにするためのものです。 VB.NETでは構造体にもプロパティを持たせることができます。 プロパティの構文などについてはプロパティで解説しています。

例えば先に例を挙げたGetWidth()およびGetHeight()メソッドを読み取り専用プロパティとして書き換えると次のようになります。

' 長方形の幅を返すプロパティ
ReadOnly Property Width() As Integer
  Get
    Return Right - Left
  End Get
End Property

' 長方形の高さを返すプロパティ
ReadOnly Property Height() As Integer
  Get
    Return Bottom - Top
  End Get
End Property

次の例は、このようにして作成したプロパティを実際に使用する例です。

Structure Rect
  Dim Left As Integer
  Dim Top As Integer
  Dim Right As Integer
  Dim Bottom As Integer

  Sub SetRect(ByVal l As Integer, ByVal t As Integer, ByVal r As Integer, ByVal b As Integer)
    Left = l
    Top = t
    Right = r
    Bottom = b
  End Sub

  ' 読み取り専用のプロパティ
  ReadOnly Property Width() As Integer
    Get
      Return Right - Left
    End Get
  End Property

  ' 読み取り専用のプロパティ
  ReadOnly Property Height() As Integer
    Get
      Return Bottom - Top
    End Get
  End Property
End Structure

Module Sample
  Sub Main()
    ' 構造体型変数の宣言
    Dim r As Rect

    ' 各メンバに値を設定する
    r.SetRect(160, 120, 640, 480)

    ' プロパティを使って値を取得する
    Console.WriteLine("Width: {0}, Height: {1}", r.Width, r.Height)
  End Sub
End Module
実行結果
Width: 480, Height: 360

配列フィールド

VB.NETでは構造体内に固定長の配列フィールドを作成することができません。 例えばC言語で記述された以下のような構造体をVB.NETで記述することはできません。 配列フィールドにサイズを指定しようとしてもコンパイルエラーとなります。

struct FixedLength
{
  // サイズ16の固定長配列フィールド
  unsigned char Data[16];
};

また、配列フィールドに初期化子を指定して割り当てを行うこともできません。 初期化子を指定しようとしてもコンパイルエラーとなります。 従って、コンストラクタを使って初期化するなどしない限り、配列フィールドは常にNothingで初期化されます。

Structure FixedLength

  ' error BC31049: 構造体メンバー上の初期化子は、'Shared' メンバーおよび定数にのみ有効です。
  Public Data As Integer() = {0, 1, 2, 3, 4, 5}

End Structure

こういった構造体内に配列フィールドを作成する場合の注意点などについては構造体と配列フィールドで個別に詳しく解説しているのでそちらを参照してください。 また、構造体内に固定長の領域を作成する方法などについてはフィールドのレイアウト・オフセットを参照してください。

バイト単位での操作

VB.NETでは任意の構造体とバイト配列を相互に変換したり、構造体を直接読み書きするメソッドが用意されていません。 そのため、構造体の内容をファイルに保存したりバイト配列から構造体データを読み込んだりする場合には、独自にコードを記述する必要があります。

そのような処理を実現する方法についてはBinaryReader・BinaryWriterでの構造体の読み書きシリアライズの基本などで解説しています。