DOBON.NET プログラミング道: .NET Framework, VB.NET, C#, Visual Basic, Visual Studio, インストーラ, ...

クラスと構造体の使い分け

クラスと構造体には類似点が多いため、自作する時どちらにするか迷うこともあります。多くの場合はクラスで問題ありませんが、時には構造体の方がよいケースもあります。そこでここでは、どのようなケースでは構造体の方がよいのかについて考えます。

なおここではクラスと構造体の違いについてはあまり説明しません。クラスと構造体の違いについては、「クラスと構造体の違い」をご覧ください。

構造体を選択する時のガイドライン

どのような時に構造体を選択すべきかのガイドラインが、MSDNの「クラスまたは構造体の選択」にあります。以下はこのページからの引用です。

型のインスタンスが小さく有効期間が一般に短い場合や、型のインスタンスが一般に他のオブジェクト内に埋め込まれる場合は、クラスではなく、構造体を定義することを検討します。

型が次に挙げるすべての特性を持たない場合、構造体は定義しません。

  1. プリミティブ型 (整数、倍精度浮動小数点数など) に似た単一の値を論理的に表す。
  2. インスタンスのサイズが 16 バイト未満である。
  3. 変更できない。
  4. 頻繁にボックス化する必要がない。

これらの条件を 1 つ以上満たしていない場合は、構造体ではなく、参照型を作成します。 このガイドラインに従わない場合、パフォーマンスが低下する可能性があります。

非常に分かり難い言い回しがされていますが、結局このガイドラインが言っているのは、上記の4つの条件をすべて満たす時のみ構造体とし、それ以外はクラスにしろということです。

それでは、このガイドラインの意味をより深く考えてみましょう。

インスタンスのサイズが小さい

構造体(値型)はクラス(参照型)と違い、変数への代入やメソッドへの値渡しなどでインスタンスのコピーが行われます。インスタンスのサイズが大きいと、それだけコピーの負担も増えます。よって、構造体のインスタンスは小さくすべきです。

ガイドラインには、「16バイト未満」という具体的な数字が示されています。この数字の根拠として確かな情報を見つけることはできませんでしたが、「c# - Why should a .NET struct be less than 16 bytes?」や「.net - Why is struct better with being less than 16 bytes」等では、構造体のインスタンスをコピーする時、サイズが16バイト未満であれば最適化によってわずかな工程でコピーが行われるという指摘があります。また、「C#で何バイトなら構造体にして許容できるか?」等のベンチマークの結果を見ても、確かに16バイトというサイズは値型と参照型のパフォーマンスの優劣を分ける分岐点に見えます。

しかし、「C#. Struct design. Why 16 byte is recommended size?」によると、このガイドラインの出典元である本「Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries (Krzysztof Cwalina, Brad Abrams著)」の第2版には、メソッドに渡したり、コレクションクラスに(あるいは、から)コピーするつもりがないなら16バイト以上でもよいとする記述や、このガイドラインはさらなる調査のきっかけとして使うべきであるという記述もあるそうです。

有効期間が短い

「有効期限が短い」というのは、変数がメソッド内やブロック内だけで使用されるということでしょう。

値型のインスタンスは、ローカル変数であればスタックに置かれますが、参照型のフィールドや静的変数であればヒープに置かれます。ヒープに置かれたインスタンスがガベージコレクションで解放されるのに対して、スタックに置かれたインスタンスは変数が有効範囲を抜けるとすぐに解放されます。そのため割り当てと解放の負担が小さく、頻繁にインスタンスを生成、解放する場合に有利です。

不変

構造体が不変(パブリックなプロパティやメソッドによって内容を変えることができない)であるべきというのには、様々な混乱を回避する目的がありそうです。構造体は暗黙的にインスタンスのコピーを作成しますので、基のインスタンスを変更したつもりが、実はそのコピーを変更していたという間違いが起こりえます。

例として、メソッドに値を渡す場合を考えてみましょう。メソッドに値を渡す時、参照型なら参照渡し、値型なら値渡しになります。よって、例えば、値型のパラメータを参照型と勘違いして、メソッド内でパラメータの内容を変更した場合、そのメソッドに渡した引数の内容が全く変わらないというバグになってしまいます。

また、途中であるクラスを構造体に変更(もしくは、ある構造体をクラスに変更)した場合は、その型をパラメータや戻り値で使用しているすべてのメソッドを見直さなければならなくなります。

もし構造体が不変ならば、これらの心配はなくなります。

もっと分かりにくい例を紹介しましょう。以下のコードは、簡単なコンソールアプリケーションです。

VB.NET
コードを隠すコードを選択
Public Class Program
    '変更できる構造体
    Public Structure MutableStruct
        Private _value As Integer
        Public ReadOnly Property Value() As Integer
            Get
                Return Me._value
            End Get
        End Property

        'フィールドの値を変更する
        Public Sub SetValue(v As Integer)
            Me._value = v
        End Sub
    End Structure

    'クラス
    Public Class TestClass
        Private _mutableStruct As New MutableStruct()
        '変更できる構造体のプロパティ
        Public ReadOnly Property MutableStruct() As MutableStruct
            Get
                Return Me._mutableStruct
            End Get
        End Property
    End Class

    'エントリポイント
    Public Shared Sub Main()
        Dim c As New TestClass()
        '構造体の値を表示する。「0」と表示される。
        Console.WriteLine(c.MutableStruct.Value)
        '構造体の値を変更する。
        c.MutableStruct.SetValue(1)
        'もう一度構造体の値を表示する。「0」と表示される。
        Console.WriteLine(c.MutableStruct.Value)

        Console.ReadLine()
    End Sub
End Class
C#
コードを隠すコードを選択
using System;

public class Program
{
    //変更できる構造体
    public struct MutableStruct
    {
        private int _value;
        public int Value
        {
            get { return this._value; }
        }

        //フィールドの値を変更する
        public void SetValue(int v)
        {
            this._value = v;
        }
    }

    //クラス
    public class TestClass
    {
        private MutableStruct _mutableStruct = new MutableStruct();
        //変更できる構造体のプロパティ
        public MutableStruct MutableStruct
        {
            get { return this._mutableStruct; }
        }
    }

    //エントリポイント
    public static void Main()
    {
        TestClass c = new TestClass();
        //構造体の値を表示する。「0」と表示される。
        Console.WriteLine(c.MutableStruct.Value);
        //構造体の値を変更する。
        c.MutableStruct.SetValue(1);
        //もう一度構造体の値を表示する。「0」と表示される。
        Console.WriteLine(c.MutableStruct.Value);

        Console.ReadLine();
    }
}

この例では、可変の構造体「MutableStruct」と、MutableStruct型のプロパティを持つクラス「TestClass」を定義しています。そしてエントリポイント(Mainメソッド)では、TestClassのインスタンスを作成し、そのMutableStructプロパティのSetValueメソッドを呼び出すことで、MutableStructの内容を変更しようとしています。

このコードを実行すると、「MutableStruct.SetValue(1)」を呼び出した後の「MutableStruct.Value」は「1」を返すように思えます。ところが実際には「0」を返します。なぜこのようなことになるのかというと、値を変更したMutableStructはコピーされたものだからです。つまり、「c.MutableStruct」を別の変数に代入して、その変数の「SetValue(1)」を呼び出したのと同じことをしたわけです。

上記のMutableStruct構造体では、Valueプロパティが読み取り専用になっており、値の変更はメソッドで行っています。なぜこのようになっているのかは、実際にValueプロパティにsetを追加してみると分かります。Valueプロパティにsetを追加して、「c.MutableStruct.Value = 1」と書くと、「変数ではないため、'Program.TestClass.MutableStruct' の戻り値を変更できません。」というエラー(コンパイラエラーCS1612)が発生します。このようなエラーによって、上記のようなミスが起こりにくくなっています。

ただ、前述した本「Framework Design Guidelines: Conventions, Idioms, and Patterns for Reusable .NET Libraries」には、オブジェクトの一部として埋め込まれている値型の場合はこのような問題が起こりにくいため、可変でもよいという意見も載せられているようです。

なお構造体を変更できないようにするには、パブリックなプロパティやメソッドでフィールドの値が変更されることがないようにして、その代わりにパラメータを持つコンストラクタを作成してフィールドの値を設定できるようにします。

変更できない構造体の例を以下に示します。

VB.NET
コードを隠すコードを選択
Public Class Program
    '変更できない構造体
    Public Structure ImmutableStruct
        Private _value As Integer
        Public ReadOnly Property Value() As Integer
            Get
                Return Me._value
            End Get
        End Property

        '値を指定できるコンストラクタ
        Public Sub New(v As Integer)
            Me._value = v
        End Sub
    End Structure

    'クラス
    Public Class TestClass
        Private _immutableStruct As New ImmutableStruct()
        '変更できない構造体のプロパティ
        Public Property ImmutableStruct() As ImmutableStruct
            Get
                Return Me._immutableStruct
            End Get
            Set(value As ImmutableStruct)
                Me._immutableStruct = value
            End Set
        End Property
    End Class

    'エントリポイント
    Public Shared Sub Main()
        Dim c As New TestClass()
        '構造体の値を表示する。「0」と表示される。
        Console.WriteLine(c.ImmutableStruct.Value)
        '構造体の値を変更する。
        c.ImmutableStruct = New ImmutableStruct(1)
        'もう一度構造体の値を表示する。「1」と表示される。
        Console.WriteLine(c.ImmutableStruct.Value)

        Console.ReadLine()
    End Sub
End Class
C#
コードを隠すコードを選択
using System;

public class Program
{
    //変更できない構造体
    public struct ImmutableStruct
    {
        private int _value;
        public int Value
        {
            get { return this._value; }
        }

        //値を指定できるコンストラクタ
        public ImmutableStruct(int v)
        {
            this._value = v;
        }
    }

    //クラス
    public class TestClass
    {
        private ImmutableStruct _immutableStruct = new ImmutableStruct();
        //変更できない構造体のプロパティ
        public ImmutableStruct ImmutableStruct
        {
            get { return this._immutableStruct; }
            set { this._immutableStruct = value; }
        }
    }

    //エントリポイント
    public static void Main()
    {
        TestClass c = new TestClass();
        //構造体の値を表示する。「0」と表示される。
        Console.WriteLine(c.ImmutableStruct.Value);
        //構造体の値を変更する。
        c.ImmutableStruct = new ImmutableStruct(1);
        //もう一度構造体の値を表示する。「1」と表示される。
        Console.WriteLine(c.ImmutableStruct.Value);

        Console.ReadLine();
    }
}

ボックス化しない

ボックス化は負担が大きく、値型の大きなデメリットです。ボックス化が頻繁に行われるようであれば、構造体にすべきではありません。

つまり、例えば、ArrayListクラスHashtableクラスのような要素をObject型として扱うコレクションで使用するつもりならば、構造体にすべきではありません。ただし、ListクラスDictionaryクラスのようなジェネリックコレクションならばボックス化を防ぐことができますので、その限りではありません。

結論

このように、ガイドラインにはそれなりの理がありますが、構造体を選択するための条件はかなり厳しいので、このガイドラインを厳正に守るならば、構造体を選択できるケースはほとんどないでしょう。

しかし、.NET Frameworkで用意されている構造体を見てみると、このガイドラインが守られているとは思えません。例えば、プリミティブ型以外の構造体として真っ先に思いつくような、Point、Size、Rectangleといった構造体でさえガイドラインに反しています。

よってこのガイドラインは一つの基準として使用し、後は自分でベンチマーク等を行いどちらが良いか判断するのがよいのではないでしょうか。

配列で使用する

構造体を選択する大きなポイントとして、配列として使用するかがあげられます。通常は、要素数の多い配列では構造体のメリットが非常に大きく、構造体の使用を考慮する価値が十分にあります。

参照型の配列の場合、要素のインスタンスはメモリにバラバラに置かれます。一方値型の配列は、要素のインスタンスが一塊で(インラインで)メモリに置かれます。そのため、構造体の配列の方が要素の読み込みと書き込みの効率がよく、さらにガベージコレクションの負担が少ないです。

また、配列のすべての要素を初期化する時も、構造体の方が楽です。クラスの場合は、ループを回すなどしてすべての要素にNew演算子で作成したインスタンスを代入する必要があります。一方構造体は、New演算子でインスタンス化する必要がなく、配列を作成するだけで済みます。

PInvokeで使用する

PInvoke(プラットフォーム呼び出し)によってCやC++などで作成したアンマネージDLLの関数とデータのやり取りをする時は、構造体が必要になるケースが多いです。もしかすると、これが最もよくある構造体の使用法かもしれません。

注意:この記事では、基本的な事柄の説明が省略されているかもしれません。初心者の方は、特に以下の点にご注意ください。

  • .NET Tipsをご利用いただく際は、注意事項をお守りください。