配列をクラスや構造体のフィールドとして格納することは出来ますが、.NET Frameworkにおいて構造体内に配列のフィールドを用意する場合は、C言語等での構造体とは異なる次の点を意識しておく必要があります。

  1. (他の種類のフィールドと同様に)構造体内にある配列のフィールドには初期化子を指定できない
  2. 配列は参照型である
  3. 配列のフィールドを固定長にすることはできない

このそれぞれについて詳しく見ていきます。

配列フィールドと初期化

次のコードでは、行数と列数、各セルのデータをそれぞれフィールドで持つマトリックスを表す構造体Matrixを作成しています。 配列をフィールドに持つ構造体の扱いは言語によって若干異なりますが、いずれも初期化子によって配列フィールドに初期値を設定することは出来ず、また初期化されていないフィールドを参照しようとするとエラーとなります。

using System;

// 配列フィールドを持つ構造体
struct Matrix {
  public int Column;
  public int Row;
  public int[] Data;

  // 初期化子を指定しようとすると、コンパイルエラーとなる
  // sample.cs(12,16): error CS0573: 'Matrix.Data': 構造体にインスタンスフィールド初期化子を指定することはできません。
  //public int[] Data = {0, 1, 2, 3, 4, 5};
}

class Sample {
  static void Main()
  {
    Matrix m;

    // フィールド変数が初期化されていない状態なので、コンパイルエラーとなる
    // sample.cs(22,5): error CS0170: フィールド 'Data' は、割り当てられていない可能性があります。
    m.Data[0] = 6;
  }
}
Imports System

' 配列フィールドを持つ構造体
Structure Matrix
  Public Column As Integer
  Public Row As Integer
  Public Data As Integer()

  ' 初期化子を指定しようとすると、コンパイルエラーとなる
  ' E:\sample.vb(11) : error BC31049: 構造体メンバー上の初期化子は、'Shared' メンバーおよび定数にのみ有効です。
  'Public Data As Integer() = {0, 1, 2, 3, 4, 5}
End Structure

Class Sample
  Shared Sub Main()
    Dim m As Matrix

    ' 配列フィールドはNothingに初期化されるため、実行時にNullReferenceExceptionがスローされる
    ' E:\sample.vb(19) : warning BC42104: 変数 'Data' は、値が割り当てられる前に使用されています。Null 参照の例外が実行時に発生する可能性があります。
    m.Data(0) = 6
  End Sub
End Class

そのため、構造体内にある配列フィールドを参照する場合は、次のように事前に配列を確保して代入しておくようにする必要があります。

using System;

// 配列のフィールドを持つ構造体
struct Matrix {
  public int Column;
  public int Row;
  public int[] Data;
}

class Sample {
  static void Main()
  {
    Matrix m;

    m.Column = 2;
    m.Row = 3;
    m.Data = new int[6]; // 配列を作成し、フィールドに設定

    // 設定した配列に値を設定
    m.Data[0] = 6;
  }
}
Imports System

' 配列のフィールドを持つ構造体
Structure Matrix
  Public Column As Integer
  Public Row As Integer
  Public Data As Integer()
End Structure

Class Sample
  Shared Sub Main()
    Dim m As Matrix

    m.Column = 2
    m.Row = 3
    m.Data = New Integer(5) {} ' 配列を作成し、フィールドに設定

    ' 設定した配列に値を設定
    m.Data(0) = 6
  End Sub
End Class

もしくは、引数をとるコンストラクタを用意しておき、構造体を使用する場合はそれを使って初期化するようにします。

コンストラクタで構造体フィールドの初期値を設定する例
using System;

// 配列のフィールドを持つ構造体
struct Matrix {
  public int Column;
  public int Row;
  public int[] Data;

  // コンストラクタ
  public Matrix(int column, int row)
  {
    // 各フィールドに初期値を設定する
    this.Column = column;
    this.Row = row;
    this.Data = new int[column * row];
  }
}

class Sample {
  static void Main()
  {
    // コンストラクタを使って初期化
    Matrix m = new Matrix(2, 3);

    // コンストラクタで初期化済みの配列フィールドに値を設定
    m.Data[0] = 6;
  }
}
コンストラクタで構造体フィールドの初期値を設定する例
Imports System

' 配列のフィールドを持つ構造体
Structure Matrix
  Public Column As Integer
  Public Row As Integer
  Public Data As Integer()

  ' コンストラクタ
  Public Sub New(ByVal column As Integer, ByVal row As Integer)
    MyClass.Column = column
    MyClass.Row = row
    MyClass.Data = New Integer(column * row - 1) {}
  End Sub
End Structure

Class Sample
  Shared Sub Main()
    ' コンストラクタを使って初期化
    Dim m As New Matrix(2, 3)

    ' コンストラクタで初期化済みの配列フィールドに値を設定
    m.Data(0) = 6
  End Sub
End Class

当然、このようにコンストラクタを用意していても、それを使わずに構造体を初期化した場合は、配列フィールドには何も設定されておらずヌル参照の状態となります。 例えば、次の例では構造体に暗黙的に用意されるコンストラクタ(デフォルトコンストラクタ)を使って変数を初期化していますが、初期化の結果配列フィールドにはヌル参照が設定されるため、実行するとNullReferenceExceptionがスローされます。

デフォルトコンストラクタを使って構造体変数を初期化した場合
using System;

// 配列のフィールドを持つ構造体
struct Matrix {
  public int Column;
  public int Row;
  public int[] Data;

  // コンストラクタ
  public Matrix(int column, int row)
  {
    // 各フィールドに初期値を設定する
    this.Column = column;
    this.Row = row;
    this.Data = new int[column * row];
  }
}

class Sample {
  static void Main()
  {
    // デフォルトコンストラクタを使って初期化
    Matrix m = new Matrix();

    // 配列フィールドはnullに初期化されているため、実行時にNullReferenceExceptionがスローされる
    m.Data[0] = 6;
  }
}
デフォルトコンストラクタを使って構造体変数を初期化した場合
Imports System

' 配列のフィールドを持つ構造体
Structure Matrix
  Public Column As Integer
  Public Row As Integer
  Public Data As Integer()

  ' コンストラクタ
  Public Sub New(ByVal column As Integer, ByVal row As Integer)
    MyClass.Column = column
    MyClass.Row = row
    MyClass.Data = New Integer(column * row - 1) {}
  End Sub
End Structure

Class Sample
  Shared Sub Main()
    ' デフォルトコンストラクタを使って初期化
    Dim m As New Matrix

    ' 配列フィールドはNothingに初期化されているため、実行時にNullReferenceExceptionがスローされる
    m.Data(0) = 6
  End Sub
End Class

さらに、デフォルトコンストラクタは暗黙的に用意されるため、引数のないコンストラクタを構造体に実装することもできません。 構造体に引数のないコンストラクタを実装するとコンパイルエラーとなります。 従って、配列をフィールドに持ち、かつヌル参照以外に初期化させたい場合は、デフォルトコンストラクタを使用した初期化をしないようにするか、構造体ではなくコンストラクタによる初期化を強制できるクラスを使用するようにする必要があります。

その他、C#では固定長の配列フィールドを使うことによりnull以外に初期化された配列フィールドを宣言することができます。

構造体の代入と配列フィールドのコピー

構造体自体は値型ですが、配列は参照型であるため構造体内における配列フィールドも当然参照型です(値型と参照型)。 構造体変数は代入によって各フィールドがコピーされますが、その結果配列フィールドが参照する配列はコピー元とコピー先で同一のインスタンスとなります。 そのため、コピー元配列もしくはコピー先配列への変更は相互に影響します。

代入による構造体のコピーと配列フィールドの内容
using System;

// 配列のフィールドを持つ構造体
struct Matrix {
  public int Column;
  public int Row;
  public int[] Data;
}

class Sample {
  static void Main()
  {
    // 1つ目の構造体変数を作成し、各フィールドを設定
    Matrix m1;

    m1.Column = 2
    m1.Row = 3
    m1.Data = new int[] {0, 1, 2, 3, 4, 5}

    // 1つ目の変数を2つ目の変数に代入
    Matrix m2 = m1;

    // 配列フィールドの値を変更
    m2.Data[0] = 6;

    // それぞれの配列フィールドの要素を列挙
    Console.Write("m1.Data: ");
    foreach (int d1 in m1.Data) {
      Console.Write("{0}, ", d1);
    }
    Console.WriteLine();

    Console.Write("m2.Data: ");
    foreach (int d2 in m2.Data) {
      Console.Write("{0}, ", d2);
    }
    Console.WriteLine();
  }
}
代入による構造体のコピーと配列フィールドの内容
Imports System

' 配列のフィールドを持つ構造体
Structure Matrix
  Public Column As Integer
  Public Row As Integer
  Public Data As Integer()
End Structure

Class Sample
  Shared Sub Main()
    ' 1つ目の構造体変数を作成し、各フィールドを設定
    Dim m1 As Matrix

    m1.Column = 2
    m1.Row = 3
    m1.Data = New Integer() {0, 1, 2, 3, 4, 5}

    ' 1つ目の変数を2つ目の変数に代入
    Dim m2 As Matrix = m1

    ' 配列フィールドの値を変更
    m2.Data(0) = 6

    ' それぞれの配列フィールドの要素を列挙
    Console.Write("m1.Data: ")
    For Each d1 As Integer In m1.Data
      Console.Write("{0}, ", d1)
    Next
    Console.WriteLine()

    Console.Write("m2.Data: ")
    For Each d2 As Integer In m2.Data
      Console.Write("{0}, ", d2)
    Next
    Console.WriteLine()
  End Sub
End Class
実行結果
m1.Data: 6, 1, 2, 3, 4, 5, 
m2.Data: 6, 1, 2, 3, 4, 5,

このように、構造体変数では代入によって配列フィールドのインスタンスとその内容までコピーされるわけではない(簡易コピーが行われる)という点に注意が必要です。


配列を含め参照型をフィールドに持つ型をどのように複製するかという点についてはオブジェクトの複製で詳しく解説していますが、ここではその一つの例として、コピーコンストラクタを用意して複製を可能にする方法について紹介します。 この例で使用しているArray.Cloneメソッドは配列を複製して同じ内容の配列を作成するメソッドです。 詳しくは配列操作 §.複製 (Clone)で解説しています。

コピーコンストラクタを使った構造体のコピーと配列フィールドの内容
using System;

// 配列のフィールドを持つ構造体
struct Matrix {
  public int Column;
  public int Row;
  public int[] Data;

  // コピーコンストラクタ
  public Matrix(Matrix source)
  {
    this.Column = source.Column;
    this.Row = source.Row;

    // 配列フィールドは、フィールドに設定されている配列を複製して初期化する
    this.Data = (int[])source.Data.Clone();
  }
}

class Sample {
  static void Main()
  {
    // 1つ目の構造体変数を作成し、各フィールドを設定
    Matrix m1;

    m1.Column = 2;
    m1.Row = 3;
    m1.Data = new int[] {0, 1, 2, 3, 4, 5};

    // コピーコンストラクタを使って1つ目の変数の内容をコピーしたものを2つ目の変数に代入
    Matrix m2 = new Matrix(m1);

    // 配列フィールドの値を変更
    m2.Data[0] = 6;

    // それぞれの配列フィールドの要素を列挙
    Console.Write("m1.Data: ");
    foreach (int d1 in m1.Data) {
      Console.Write("{0}, ", d1);
    }
    Console.WriteLine();

    Console.Write("m2.Data: ");
    foreach (int d2 in m2.Data) {
      Console.Write("{0}, ", d2);
    }
    Console.WriteLine();
  }
}
コピーコンストラクタを使った構造体のコピーと配列フィールドの内容
Imports System

' 配列のフィールドを持つ構造体
Structure Matrix
  Public Column As Integer
  Public Row As Integer
  Public Data As Integer()

  ' コピーコンストラクタ
  Public Sub New(ByVal source As Matrix)
    MyClass.Column = source.Column
    MyClass.Row = source.Row

    ' 配列フィールドは、フィールドに設定されている配列を複製して初期化する
    MyClass.Data = DirectCast(source.Data.Clone(), Integer())
  End Sub
End Structure

Class Sample
  Shared Sub Main()
    ' 1つ目の構造体変数を作成し、各フィールドを設定
    Dim m1 As Matrix

    m1.Column = 2
    m1.Row = 3
    m1.Data = New Integer() {0, 1, 2, 3, 4, 5}

    ' コピーコンストラクタを使って1つ目の変数の内容をコピーしたものを2つ目の変数に代入
    Dim m2 As New Matrix(m1)

    ' 配列フィールドの値を変更
    m2.Data(0) = 6

    ' それぞれの配列フィールドの要素を列挙
    Console.Write("m1.Data: ")
    For Each d1 As Integer In m1.Data
      Console.Write("{0}, ", d1)
    Next
    Console.WriteLine()

    Console.Write("m2.Data: ")
    For Each d2 As Integer In m2.Data
      Console.Write("{0}, ", d2)
    Next
    Console.WriteLine()
  End Sub
End Class
実行結果
m1.Data: 0, 1, 2, 3, 4, 5, 
m2.Data: 6, 1, 2, 3, 4, 5, 

固定長の配列フィールド

構造体内の配列フィールドに初期値を設定できないのと同様、配列フィールドを固定長にすることも出来ません。 例えば、C言語で記述した次のような構造体を考えます。

struct Int128 {
  unsigned char Data[16];
};

これと同様の構造体を作成することを考えた場合、次のようなフィールド宣言はいずれもコンパイルエラーとなります。

Imports System

' 固定長の配列フィールドを持つ構造体
Structure Int128
  ' E:\sample.vb(6) : error BC31043: 構造体メンバーとして宣言された配列を初期サイズで宣言することはできません。
  Public Data(15) As Byte

  ' E:\sample.vb(9) : error BC30638: 配列の範囲を、型指定子に記述することはできません。
  Public Data As Byte(15)
End Structure

C#では、このような配列フィールドを宣言するためにfixedステートメントを使うことが出来ます。

fixedステートメントを使って固定長配列を宣言・使用する例
// csc /unsafe+ sample.cs
using System;

// 固定長の配列フィールドを持つ構造体
unsafe struct Int128 {
  // 16バイト(128ビット)のサイズを持つフィールド
  public fixed byte Data[16];
}

class Sample {
  static void Main()
  {
    Int128 val = new Int128();

    unsafe {
      // fixedなフィールドを参照する場合は、unsafeコンテキストの内側で行う必要がある
      val.Data[0] = 0xff;
      val.Data[15] = 0x00;
    }
  }
}

ただし、fixedステートメントを使う構造体はunsafeな構造体でなければならず、また固定長配列のフィールドを参照する場合はunsafeコンテキスト内で行わなければなりません。 また、配列の型もプリミティブ型のうちbyte, short, int, long, sbyte, ushort, uint, ulong, char, float, double, boolのいずれかに限定されます。 従って構造体内に固定長の構造体配列フィールドを宣言することも出来ません。

さらに、fixedステートメントでは1次元の固定長配列のみが宣言でき、2次元以上の固定長配列は宣言することが出来ません。 固定長配列を宣言するにはコード中でunsafeコンテキストを使用するため、コンパイルオプション(/unsafe+)でアンセーフコードの使用を許可する必要があります。

VBではunsafeやfixedのようなステートメントが用意されていないため、構造体内に固定長の配列フィールドを宣言することは出来ません。 ただ、VBFixedArrayAttribute属性でフィールドをマークすることで配列フィールドが固定長であるように扱われることを期待することも出来ますが、ここではその詳細については触れません。


fixedステートメントで固定長配列の宣言を行う以外にも、別の方法で固定長配列の代用を行うことも出来ます。 次の例では、固定長配列を宣言する代わりに、配列を同型の複数フィールドに展開して宣言しています。 これは愚直なやり方ですが、そもそも配列は同型の値が複数個集まったものであり、このようにすることで確保されるフィールドのサイズが固定長となることを保証できるという点や、またアンセーフコードを一切使わないという点でも、この方法は有効な方法です。

using System;

// 固定長配列の代わりに同じサイズ分だけフィールドを並べた構造体
struct Int128 {
  // 16バイト(128ビット)分のサイズを持つフィールド群
  public byte Data0;
  public byte Data1;
  public byte Data2;
  public byte Data3;
  public byte Data4;
  public byte Data5;
  public byte Data6;
  public byte Data7;

  public byte Data8;
  public byte Data9;
  public byte Data10;
  public byte Data11;
  public byte Data12;
  public byte Data13;
  public byte Data14;
  public byte Data15;
}

class Sample {
  static void Main()
  {
    Int128 val = new Int128();

    val.Data0 = 0xff;
    val.Data15 = 0x00;
  }
}
Imports System

' 固定長配列の代わりに同じサイズ分だけフィールドを並べた構造体
Structure Int128
  ' 16バイト(128ビット)分のサイズを持つフィールド群
  Public Data0 As Byte
  Public Data1 As Byte
  Public Data2 As Byte
  Public Data3 As Byte
  Public Data4 As Byte
  Public Data5 As Byte
  Public Data6 As Byte
  Public Data7 As Byte

  Public Data8 As Byte
  Public Data9 As Byte
  Public Data10 As Byte
  Public Data11 As Byte
  Public Data12 As Byte
  Public Data13 As Byte
  Public Data14 As Byte
  Public Data15 As Byte
End Structure

Class Sample
  Shared Sub Main()
    Dim val As New Int128()

    val.Data0 = &hFF
    val.Data15 = &h00
  End Sub
End Class

このようにして複数のフィールドを固定長配列の代用とした場合、フィールド変数が多くなり扱いづらくなりますが、配列となるフィールド群を別途共用体として構成することでより簡便に扱えるようにも出来ます。 共用体を構成する方法についてはフィールドのレイアウト・オフセット §.共用体の実装で解説しています。

固定長の配列フィールドを扱う場合は、同時にバイト配列やポインタと構造体の相互変換も行うことがありますが、そういった方法についてはBinaryReader・BinaryWriterでの構造体の読み書きで解説しています。