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

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

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

§1 配列フィールドと初期化

次のコードでは、行数と列数、各セルのデータをそれぞれフィールドで持つマトリックスを表す構造体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;
  }
}

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

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;
  }
}

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

コンストラクタで構造体フィールドの初期値を設定する例
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;
  }
}

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

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

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



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

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

代入による構造体のコピーと配列フィールドの内容
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();
  }
}
実行結果
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();
  }
}
実行結果
m1.Data: 0, 1, 2, 3, 4, 5, 
m2.Data: 6, 1, 2, 3, 4, 5, 

§3 固定長の配列フィールド

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

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

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

using System;

// 固定長の配列フィールドを持つ構造体
struct Int128 {
  // sample.cs(7,15): error CS0270: 配列のサイズは変数宣言の中で指定できません ('new'を使用して初期化してください)
  public byte[16] Data;

  // sample.cs(10,19): error CS0650: 不適切な配列の宣言子: マネージ配列を宣言するには、次元指定子を変数の識別子の前に指定します。固定サイズ バッファー フィールドを宣言するには、フィールド型の前に fixed キーワードを使用します。
  // sample.cs(10,20): error CS0270: 配列のサイズは変数宣言の中で指定できません ('new' を使用して初期化してください)
  public byte Data[16];
}

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;
  }
}

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

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