目次

概要

C# では「すべての値型ValueTypeクラス(System名前空間)から派生する」というような、暗黙的な派生があります。

また、通常、値型(組み込み型や、構造体、列挙型)は他の型からの派生、他の型への派生ができませんが、 本項の「暗黙的な派生」だけは許されています。 ただ、内部的には、派生しているように見せかけるための特殊な変換処理が掛かっています。

ポイント
  • 全ての型は Object である
  • 全ての値型は ValueType である
  • 全ての列挙型は Enum である
  • 全てのデリゲートは Delegate である
  • 全ての配列は Array である

特殊な型

以下の型は、.NET/C# にとって特別な意味を持ちます。 いずれも System 名前空間中のクラスです。

型名 役割
Object 全ての型の共通の最上位の基底クラス。C# のキーワードの object はこのクラスの別名になっている。
ValueType 全ての値型(プリミティブ型構造体列挙型)の共通基底クラス。
Enum 全ての列挙型の共通基底クラス。ValueType クラスから派生。
Delegate 全てのデリゲートの共通基底クラス。
Array 全ての配列の共通基底クラス。

これらの型は「共通基底」として働きます。 基底クラスになっているので、派生しているどんな型でも受け取れる変数が作れます。

// 整数でも DateTime 構造体でも UriKind 列挙型でも入る変数
ValueType x;
x = 1;
x = DateTime.Now;
x = PlatformID.Unix;

// どんな型の配列でも入る変数
Array array;
array = new[] { 1, 2, 3 };
array = new[] { 1.2, 2.5, 3.9 };
array = new[] { "a", "b", "c" };

また、これらのクラスのメンバーは、派生型から呼べます。 例えば、Enumクラスが持っているHasFlagメソッドは任意の列挙型に対して使えます。 (ただし、このHasFlagの利用には、後述するパフォーマンス上の注意点があります。)

using System;

[Flags]
enum Flag
{
    X = 1,
    Y = 2,
    Z = 4,
}

class Program
{
    static void Main()
    {
        Flag f = Flag.X | Flag.Y;
        if (f.HasFlag(Flag.X)) // Flag 型に対して、Enum.HasFlag を呼んでる
        {
            // ...
        }
    }
}

ValueType, Enum とボックス化

前述の ValueTypeEnum はクラス(もちろん参照型)です。 これらに対して、値型である整数や列挙型の値を代入するとボックス化が起こります。

ボックス化については詳しくは「ボックス化」で説明しているのでそちらをご覧ください。 簡単にいうと、本来値の持ち方が全然違う型に対して、内部的な変換処理が働いています。 この変換処理はそれなりに重たい処理で、パフォーマンス的には避けたいものです。 (元から参照型なArray(配列)やDelegate(デリゲート型)の場合は特に問題になりません。 ValueTypeEnumだけの問題です。)

例えば以下のようなコードは、似たようなことに対して2つの書き方をしているだけですが、 パフォーマンス的にはだいぶ差があります。

// 値型だけを受け付けたいとき、ValueType で引数を受け取るとボックス化が起きる
static void A(ValueType value) { }

// ボックス化を避けたければこう書く
// where T : struct 制約付きのジェネリック メソッドを用意
static void B<T>(T value) where T : struct { }

static void Main()
{
    // 同じような呼び方をしていても、
    for (int i = 0; i < 10000; i++)
    {
        // こっちの方が倍は遅い
        A(1);
        A(TimeSpan.FromSeconds(1));
        A(PlatformID.Unix);
    }

    for (int i = 0; i < 10000; i++)
    {
        B(1);
        B(TimeSpan.FromSeconds(1));
        B(PlatformID.Unix);
    }
}

単にこのメソッドを呼び出すだけのベンチマークを取ると、2倍~3倍程度の差が付きます。 (手元の環境では、Aの方が167μ秒、Bの方が66μ秒でした。)

できる限りはジェネリクスを使う方がいいでしょう。

Enum.HasFlag でのボックス化

ちなみに、列挙型は、Enum型変数を経由しなくても、HasFlagメソッドを呼んだ時点でボックス化します。 なので長らく、このメソッドは「地雷」として有名だったんですが、 .NET Core 2.1では「Enum.HasFlagを見たら特別扱いして置き換える」と言うような最適化が掛かるようになったそうです。

例えば以下のようなコードは、.NET Core 2.0以前と2.1以降で実行速度に20倍以上の差があります。

[Flags]
enum X
{
    A = 1,
    B = 2,
    C = 4,
}

int Count(X x)
{
    var count = 0;
    if (x.HasFlag(X.A)) count++;
    if (x.HasFlag(X.B)) count++;
    if (x.HasFlag(X.C)) count++;
    return count;
}

Enum制約とDelegate制約

Ver. 7.3

本項で紹介しているような「特殊な基底クラス」は、これまでジェネリクスの型制約には指定できませんでした。

static void M<T>()
    where T : System.Array // エラーになる
{ }

C# 7.3 からはこの制限が少しだけ緩和されて、EnumDelegateの2つは制約にできるようになりました。

Enum制約を付けると列挙型だけを受け取れるジェネリック型・ジェネリック メソッドを作れます。

        static void EnumConstraint<T>(T x, T y)
            where T : struct, Enum
        {
            Console.WriteLine(x.HasFlag(y));

            // ちなみに、 == はダメ。Enum クラスに == はない。
        }

前述の.NET Core 2.1での最適化の話と併せると、 .NET Core 2.1以降では有用な機能でしょう。

一方、Delegate制約の方はデリゲートだけを受け取れます。 ちなみに、DelegateクラスにはさらにMulticastDelegateクラス(これもSystem名前空間)という派生クラスがいますが、この型も型制約として使えます。

static bool M<A>(A a, A b)
    where A : MulticastDelegate
{
    // Delegate は == 演算子を持ってる
    return a == b;
}

static object Invoke<A>(A a)
    where A : Delegate
{
    // Delegate.DynamicInvoke を呼ぶ
    return a.DynamicInvoke();
}

デリゲートの方は、Enum.HasFlagのような特殊な最適化が関わるわけではなく、 そこまで使い勝手は良くありません。 Expression.Lambda<TDelegate>のような一部のメソッドでのみ有効そうです。

更新履歴

ブログ