.NETには「プリミティブ型」とかいうものがあるんですが、 何をもってプリミティブと言えるのか、 どういう型がプリミティブ型なのかというと、 なんかよくわからない存在です。

Type型にIsPrimitiveというプロパティがあって、こいつがtrueを返すものがプリミティブ型なんですが。 以下のコードを見ての通り、どういう基準なのかがパッと見でわからず。

using static System.Console;

class Program
{
    static void Main()
    {
        WriteLine(typeof(int).IsPrimitive);     // true
        WriteLine(typeof(bool).IsPrimitive);    // true
        WriteLine(typeof(double).IsPrimitive);  // true
        WriteLine(typeof(object).IsPrimitive);  // false!
        WriteLine(typeof(string).IsPrimitive);  // false!
        WriteLine(typeof(decimal).IsPrimitive); // false!
        WriteLine(typeof(System.IntPtr).IsPrimitive); // true!
    }
}

primitive

primitiveという単語の意味は、原始的とか基本的とかそんな意味なわけですが。 C#とかのプログラミング言語において「原始的」っていうのは、内部的に専用命令などを持っていて、 ユーザー定義のクラスや構造体ではできない何らかの特別扱いを受けているという意味になります。

特にC#の場合には、C#のレベルではなく、.NETランタイム的に、専用のIL (中間言語、Intermediate Language)命令を持っているかどうかが1つの基準なんですが… それにしても分類は結構あいまいで、よくわからなかったりします。

元々はJavaから

元々はJavaから来ているんですかね。Javaの場合は割かしプリミティブ型がはっきりしています。 以下の3つが一致していて、これこそがプリミティブ型です。

  • intとかbooleanみたいに、専用のキーワードがある
  • 言語的に許される唯一の値型
  • 中間言語(bytecode)的に専用命令を持ってる

C#というか.NETでよくわからなくなる理由は、

  • objectstringdecimalにも専用のキーワードがある
  • 構造体や列挙型があるのでいくらでも値型を作れる
  • 専用命令を持っているって意味ではstringもちょっとだけ命令を持っている

というあたり。

.NETのプリミティブ事情

.NET だと、

  • IsPrimitivetrueな型は以下の通り
    • Boolean, Byte, SByte, Int16, UInt16, Int32, UInt32, Int64, UInt64, IntPtr, UIntPtr, Char, Double, Single
    • C#のキーワードになってる型から、
      • decimal, object, stringは除く
      • IntPtr, UIntPtrを加える
  • objectstringはキーワードになってるけど参照型
  • 構造体や列挙型があるので、いくらでも値型を作れる
  • stringはプリミティブ型とかと比べると大して専用命令ない
    • けども、ないわけじゃない
    • 命令ではなくメモリの確保の仕方で言うと、stringは他のクラスと比べて相当特殊
      • この意味で言うと配列もかなり特殊
  • decimalに至っては全く専用命令ない
    • decimalが特別なのはC#的にリテラルがあることくらい
    • IL 的にはリテラルすらない、完全に普通の構造体

ということで、何が何やら。まあ大まかに言うと、プリミティブ型 = 「中間言語(IL)的に専用を持っていて、かつ、値型」ですかね。

IL的な扱い

せっかくなので、IL 的な扱いも見てみますか。 以下のようなコードをコンパイルしてみます。

class Program
{
    static void Main()
    {
        M(1, 2);
        M("a", "b");
        M(1.23m, 2.71m);
    }

    static int M(int x, int y) => x + y;
    static decimal M(decimal x, decimal y) => x + y;
    static string M(string x, string y) => x + y;
}

int

まずはintの場合。 メソッドM(int, int)の中身が

  IL_0000:  ldarg.0
  IL_0001:  ldarg.1
  IL_0002:  add

M(int, int)を呼び出す側が

  IL_0000:  ldc.i4.1
  IL_0001:  ldc.i4.2
  IL_0002:  call       int32 Program::M(int32,
                                        int32)

という感じです。 足し算用にaddという専用命令があったり、 定数読み込みのためにldc.i4.1 (load constant 4バイト整数の1という意味)という命令があったりします。

string

続いてstring メソッドM(string, string)の中身が

  IL_0000:  ldarg.0
  IL_0001:  ldarg.1
  IL_0002:  call       string [mscorlib]System.String::Concat(string,
                                                              string)

M(string, string)を呼び出す側が

  IL_0008:  ldstr      "a"
  IL_000d:  ldstr      "b"
  IL_0012:  call       string Program::M(string,
                                         string)

です。 連結のためには特に命令を持っているわけではなく、+演算子はConcatメソッド呼び出しに置き換わります。 一方で、値の読み込みのためにldstr (load stringの意味)命令は持っています。

微妙なライン… C#的には組み込み型(stringっていうキーワードがあり、リテラルとかが用意されてる特別な型)だし、 IL的にはプリミティブ型ではない、という割にはldstr命令とか持ってる…

decimal

最後にdecimal。 メソッドM(decimal, decimal)の中身が

  IL_0000:  ldarg.0
  IL_0001:  ldarg.1
  IL_0002:  call       valuetype [mscorlib]System.Decimal [mscorlib]System.Decimal::op_Addition(valuetype [mscorlib]System.Decimal,
                                                                                                valuetype [mscorlib]System.Decimal)

M(decimal, decimal)を呼び出す側が

  IL_0018:  ldc.i4.s   123
  IL_001a:  ldc.i4.0
  IL_001b:  ldc.i4.0
  IL_001c:  ldc.i4.0
  IL_001d:  ldc.i4.2
  IL_001e:  newobj     instance void [mscorlib]System.Decimal::.ctor(int32,
                                                                     int32,
                                                                     int32,
                                                                     bool,
                                                                     uint8)
  IL_0023:  ldc.i4     0x10f
  IL_0028:  ldc.i4.0
  IL_0029:  ldc.i4.0
  IL_002a:  ldc.i4.0
  IL_002b:  ldc.i4.2
  IL_002c:  newobj     instance void [mscorlib]System.Decimal::.ctor(int32,
                                                                     int32,
                                                                     int32,
                                                                     bool,
                                                                     uint8)
  IL_0031:  call       valuetype [mscorlib]System.Decimal Program::M(valuetype [mscorlib]System.Decimal,
                                                                     valuetype [mscorlib]System.Decimal)

です。 どこにも専用命令がないどころか、リテラルの1.23m2.71mすらも、 new decimal(123, 0, 0, false, 2)の意味のコンストラクター呼び出しに置き換わっています。 加算も、op_Additionメソッド呼び出しです。

正直なところ、decimalがC#的に特別扱いを受ける理由はリテラルだけだったりします。 あくまでC#上の特別扱いであって、IL上は他の構造体の扱いとまったく同じです。 なので、decimalはプリミティブではない。

もしかすると、C++みたいにユーザー定義リテラルが書ければ、 decimalなんていう組み込み型は要らなかったかもしれません。