概要
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 とボックス化
前述の ValueType
や Enum
はクラス(もちろん参照型)です。
これらに対して、値型である整数や列挙型の値を代入するとボックス化が起こります。
ボックス化については詳しくは「ボックス化」で説明しているのでそちらをご覧ください。
簡単にいうと、本来値の持ち方が全然違う型に対して、内部的な変換処理が働いています。
この変換処理はそれなりに重たい処理で、パフォーマンス的には避けたいものです。
(元から参照型なArray
(配列)やDelegate
(デリゲート型)の場合は特に問題になりません。
ValueType
とEnum
だけの問題です。)
例えば以下のようなコードは、似たようなことに対して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 からはこの制限が少しだけ緩和されて、Enum
とDelegate
の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>
のような一部のメソッドでのみ有効そうです。