目次

キーワード

概要

C# や Java などのプログラミング言語では、 コンピュータのメモリ上の任意の場所に自由にアクセスするための手段、 すなわち、ポインターの利用が禁止もしくは制限されています。

ポインターは、その自由さから、非常に有用であると同時に、 危険なものでもあり、バグの原因になりやすいという問題がありました。 そのため、C# や Java などの言語では、 ポインターの代替となる物を用意し、 必要最小限の機能のみを提供する事によって、 簡単でかつ堅牢なプログラミング環境を提供しています。

ただし、C# では、 C言語などの既存のプログラミング言語との相互運用性や、 プログラムの実行効率向上のために、 ポインターを完全に廃止するのではなく、 unsafe コンテキストと呼ばれる特別な場面でのみポインターを利用できるようにしています。

ポイント
  • unsafe キーワードの付いたメソッド内や、unsafe ブロック内限定で、ポインターなどの低レベル機能が使える。

  • unsafe を使うためには、コンパイル時に /unsafe オプションを付ける必要がある。

  • C言語などとの相互運用のためにあるもの。それ以外の用途(パフォーマンス向上など)に使うのは最終手段。

ポインターとは

( 「コンピュータの基礎知識」に、 もう少し詳しいポインターの説明を書いたので、 そちらも参照してみてください → 「メイン・メモリ」。 )

プログラム中で使用する変数の値はメモリに記憶されています。 図1に示すように、メモリ上には値を格納するための領域が一直線に並んで、 それぞれにアドレス(変数の所在地を示す番号)が付いています。 そして、アドレスを格納するために用いるのがポインター(アドレスを指し示す変数という意味)です。

アドレス
アドレス

ポインターを説明するために、C# の前身であるC++によるポインターの利用例を示します。 C++では、変数の宣言するとき、 型名の後に * を付けるとポインター変数になります。 また、変数の前に & を付けることで、 その変数のアドレスを取り出すことができます。 逆に、ポインターの参照先の値を読み書きするには、 ポインター変数の前に * を付けます。

// 注: C++ です。

int* p; // ポインターの宣言
int n = 30;

p = &n;     // ポインター p に n のアドレスを代入
cout << *p; // p の参照先 (n) の値 (30) を読み出す
*p = 20;    // p の参照先 (n) の値を 20 に書き換える
cout << n;  // n は 20 に書き換わっている

このように、ポインターを使うことで他の変数を参照することが出来ます。 さらに、ポインターにはあくまでメモリ上のアドレスがそのまま数値として格納されていて、 +, --, ++, -- などの演算子を使って値を自由に変更できます。 この自由さのため、ポインターは正しく使用すれば非常に強力な道具になりますが、 ほんのちょっとした不注意からプログラマの意図しない動作を起こすことがあり、 扱いの難しいものとなっていました。

このような問題を解決するため、 C# や Java などのプログラミング言語では、 ポインターの代替となる機能を提供し、 ポインターの使用を禁止もしくは制限しています。

ここでは、ポインターの詳細についてはこれ以上触れませんが、 従来のプログラミング言語においてポインターがどのような場面で使用されいたのかと、 C# においてどのような機能で代替出来るのかだけ、 以下に簡単にまとめます。

ポインターを必要とする場面 代替機能
動的確保 参照型の変数は常に動的に確保される。
動的配列 C# の配列は元々動的。
引数の参照渡し 参照変数を使うもしくは ref、out キーワードを使う。
配列に対する反復処理の効率化 for や foreach に対してコンパイラーが最適化を掛けて、効率のいいコードにする。

unsafe コード

従来のプログラミング言語でポインターを必要としていた場面のほとんどは、 他の機能で代替することが出来るため、 C# や Java 言語にとってポインターは必須なものではありません。 そのため、Java 言語ではポインターを完全に廃止しています。 しかし、C# ではプログラムの効率化と従来のプログラミング言語との相互運用を目的として、 制限付きながらポインターの使用可能にしてあります。

まず、ポインター使用における制限ですが、 C# では unsafe キーワードを用いて宣言されたメソッドもしくはブロック内(このようなコードを unsafe コードと呼びます)でしかポインターを使用できません。 メソッドに unsafe 修飾子を付けることでそのメソッド内部は unsafe コードとなり、 そのメソッド内でポインターを使用できるようになります。 また、unsafe{} と言うように、ブロックの手前に unsafe キーワードを付けることで、そのブロック内部でのポインター使用が可能になります。

unsafe void UnsafeMethod()
{
  // unsafe メソッド。
  // ポインターが使用可能。
}

void SafeMethod()
{
  // ポインター使用不可。

  unsafe
  {
    // unsafe ブロック。
    // ブロック内でのみポインター使用可能。
  }
}

さらに、プログラム内で unsafe キーワードを使用するためには、 コンパイル時に /unsafe オプションを付ける必要があります 。 前節で述べたように、ポインターの使用は危険を伴うため、 C# ではこのような強い制限を設けています。

unsafe コード限定機能

unsafe コード内では以下の機能が利用可能となります。

  • ポインターの使用。

  • 配列の静的確保(stackalloc)。

  • sizeof 演算子

  • アドレス固定(fixed)。

詳しくは次節以降で説明していきます。

アンマネージ型

ちなみに、これらの機能を利用できる型は、以下のような条件を全て満たす型に限られます。 このような型をアンマネージ型(unmaged type)と呼びます。

  • 値型である。

  • 構造体の場合、アンマネージ型しかメンバーに含まない。

  • 非ジェネリック。

つまり、ガベージ コレクションによって管理される(managed)参照(クラスなど、参照型のインスタンス)を含まない (含む可能性が全くない)型に対してだけ、ポインター、sizeof 演算子、stackalloc などの機能が利用できます。

ポインター

C# では、C++ 言語と似た文法でポインターを使用できます。 すなわち、& 演算子を用いてアドレスの取り出し、 * 演算子を用いてポインターの指している先を参照、 +, --, ++, -- などの演算子を使ってアドレスの値を計算できます。

ちなみに、& を アドレス取得式(address-of expression)、* を間接参照式(pointer indirection expression)と呼びます。

using System;

class UnsafeTest
{
  static void Main()
  {
    unsafe
    {
      int n;
      int* pn = &n;        // n のアドレスをポインター pn に代入。
      byte* p = (byte*)pn; // 違う型のポインターに無理やり代入可能。

      *p = 0x78; // n の最初の1バイト目に 0x78 を代入
      ++p;
      *p = 0x56; // n の2バイト目に 0x56 を代入
      ++p;
      *p = 0x34; // n の3バイト目に 0x34 を代入
      ++p;
      *p = 0x12; // n の4バイト目に 0x12 を代入

      Console.Write("{0:x}\n", n); // n の値を16進数で表示。
    }
  }
}
12345678

また、ポインターには -> というポインター専用演算子と、配列と同じ [] によるアクセスが使えます。

演算子 説明
-> ポインター メンバー アクセス(pointer member access)。p->x で、 (*p).x と同じ意味。
[] ポインター要素アクセス(pointer element access)。p[i]*(p + i) と同じ意味。

例えば以下のように使います。

using System;

struct Point
{
    public short X;
    public short Y;
}

class Program
{
    unsafe static void Main()
    {
        var p = new Point();

        // アンマネージ型の変数にはポインターを使える
        // & でアドレス取得(ポインター化)
        // 型推論(var)も効く
        var pp = &p;

        // int 型のポインターに無理やり代入
        // p のある位置に無理やり int の値を書き込み
        int* pi = (int*)pp;
        *pi = 0x00010002;

        // -> で構造体のポインターのメンバーにアクセス
        Console.WriteLine(pp->X); // (*pp).X と同じ意味 = 2
        Console.WriteLine(pp->Y); // (*pp).Y と同じ意味 = 1

        // byte 型のポインターに無理やり代入
        byte* pb = (byte*)pp;

        // ポインターには配列と同じように [] が使える
        Console.WriteLine(pb[0]); // *(pb + 0) と同じ意味 = 2
        Console.WriteLine(pb[1]); // *(pb + 1) と同じ意味 = 0
        Console.WriteLine(pb[2]); // *(pb + 2) と同じ意味 = 1
        Console.WriteLine(pb[3]); // *(pb + 3) と同じ意味 = 0
    }
}

スタック上への配列の確保(stackalloc)

C# で通常使用している配列はヒープ領域にメモリを確保しています(参考: 「[雑記] スタックとヒープ」 )。 しかしながら、ヒープ領域への読み書きは、スタック領域と比べ、少しですが効率が悪くなります。 そのため、C# では unsafe コード内限定で、配列をスタック上に確保するための構文を用意しています。

スタック上への配列確保は以下に示すように、 stackalloc キーワードを用いて行います。

型名* 変数名 = stackalloc 型名[配列長];

変数の型が 型名[] から 型名* に、 インスタンスの作成方法が new 型名[配列長] から stackalloc 型名[配列長] に代わっていますが、通常の配列と似たような構文で使用できます。 ただし、stackalloc を用いた場合、配列長は定数でなければなりません。

サンプル
using System;

class UnsafeTest
{
  static void Main()
  {
    unsafe
    {
      const int N = 10;
      const int MAX = 99;
      int* x = stackalloc int[N]; // 配列をスタック上に確保
      Random rand = new Random();

      // 配列 x に乱数を代入
      for(int i=0; i<N; ++i)
      {
        x[i] = rand.Next(MAX);
        Console.Write("{0}, ", x[i]);
      }
      Console.Write('\n');

      // 配列 x を整列
      for(int i=0; i<N; ++i)
        for(int j=i+1; j<N; ++j)
          if(x[i] > x[j])
          {
            int tmp = x[i];
            x[i] = x[j];
            x[j] = tmp;
          }

      // 整列結果を出力
      for(int i=0; i<N; ++i)
      {
        Console.Write("{0}, ", x[i]);
      }
      Console.Write('\n');
    }
  }
}
56, 67, 82, 23, 86, 78, 27, 92, 39, 13,
13, 23, 27, 39, 56, 67, 78, 82, 86, 92,

sizeof 演算子

unsafe コード内では、sizeof 演算子で構造体の領域サイズを取得できます。 (通常(unsafe コードの外では)、sizeof 演算子でサイズを取得できるのは int や char など、C# の規格上サイズが決まっている数値型のみです。)

using System;

class Program
{
    unsafe struct X
    {
        byte x;
        int y;
    }

    static void Main()
    {
        unsafe
        {
            Console.WriteLine(sizeof(X));
        }
    }
}
8

構造体のメンバーのレイアウトは、必ずしも隙間なく並ぶわけではなく、 メンバーのアドレスが4の倍数になるように隙間が作られたりします。 (32ビット CPU では、4バイト単位で値を読み書きするため、その方が実行効率がいい。 CPU の種類によって、最適な間隔は変わります。) 上記の例でも、1バイトの x と、4バイトの y の間に隙間が空いて、X 構造体全体では8バイトの領域を占めています。

アドレス固定(fixed)

.NET Framework 向けに作成されたコードは、 メモリの確保・解放は全て .NET Framework によって管理されているため、 managed (管理された)コードと呼ばれています。 即ち、.NET Framework は生成したインスタンスがまだどこかで使われているかどうかを自動的に判別して、 不要になったメモリ領域を自動的に解放します。

unsafe コードも managed コードの一部分なので、 unsafe コード中のオブジェクトも .NET Framework の管理下に置かれます。 ところが、このようなメモリの自動管理とポインターはあまり相性の良いものではありません。 .NET Framework によりインスタンスの格納されているメモリ領域が削除されてしまったり、 移動されてしまう可能性があり、 ポインターの参照先が気が付いたら消えていたということが有り得るからです。

したがって、ポインターを使う場合、しばらくの間メモリ領域の削除や移動を停止してもらう(アドレスを固定する)必要があります。 そのための構文として、C# には fixed 文というものがあります。 fixed 文は以下のような形で書かれます。

fixed(型名* 変数名 = アドレス取得式) 実行したい文

fixed 文中でアドレスを取得したインスタンスは格納先のメモリ領域が移動・削除されなくなり、 アドレスが変化しないことが保障されます。 例えば、参照型のメンバーのアドレスをポインターに代入する場合、以下のようにします。

// Complex クラスは re, im というdouble 型のメンバーを持っているものとする。
Complex c = new Complex(1, 0);
fixed(double* p = &c.re)
{
  *p = 10;
}
Conosle.Write("({0}, {1})\n", c.re, c.im); // (10, 0) と表示される

また、C# では配列を暗黙的にポインターに変換し、 ポインターを介して配列の内容を変更することが出来ます。 この場合にも、fixed 文を使用してアドレスを固定しなければなりません。

int[] x = new int[10];
fixed(int* px = x)
{
  // ポインター px を介して配列 x の内容を更新できる。
}
サンプル
using System;

class UnsafeTest
{
  static void Main()
  {
    unsafe
    {
      const int N = 5;
      int[] x = new int[N];
      for(int i=0; i<N; ++i)
        x[i] = i;

      // 配列 x をポインター px に代入する。
      fixed(int* px = x)
      {
        // ポインターを介して配列 x の内容を変更。
        for(int* p = px; p != px + 10; ++p)
          *p = (*p) * (*p);
      }

      // 結果出力。
      for(int i=0; i<N; ++i)
        Console.Write("{0}, ", x[i]);
      // 0, 1, 4, 9, 16, と表示される。
    }
  }
}

固定長バッファ

Ver. 2.0

C# 2.0 で、unsafe な構造体のメンバーとして、 C 言語の配列風の固定長バッファを定義できるようになりました。

固定長バッファは、以下のように、fixed キーワードを用いて定義します。 通常の C# の配列と異なり、型名[] ではなく、 変数名[要素数] と書きます。

[System.Runtime.InteropServices.StructLayout(
  System.Runtime.InteropServices.LayoutKind.Sequential,
  Pack=1)]
unsafe struct Header
{
  public Int16 Source;
  public Int16 Destination;
  public Byte  Type;
  fixed byte reserved[3];
  public Int32 Data;
}

固定長配列は、主に unmanaged コードとの相互運用時に用いられます。 その名前の通り、バッファ長は固定で、実行時にサイズを決めたり、 サイズを変えたりすることはできません。

余談: C++/CLI

C# の unsafe の目的は、実行効率の向上と既存言語との相互運用性ですが、 この目的のためなら、C# で unsafe を使う以外に、 C++/CLI を使うという選択肢もあります。

C++/CLI は、C++ を .NET Framework 向けに修正したもので、 Microsoft の提供する .NET 言語の中で唯一、 native コードと managed コードを混在させてのプログラミングができる言語です。 C++/CLI を使うことで、既存の(native の) C++ 資源を .NET Framework から利用することも割と容易です。

native なコードは、通常の C++ と同じ構文で記述でき、 managed な部分に関しては、 ref、delegate、property などのいくつかの拡張キーワードや、 TypeName^ や TypeName^% などの追加の記号を使って記述します。

C# と比べるとお世辞にも書きやすいとは言えない言語ですが、 native / managed 混在プログラムを書きたい場合には最良の選択肢となるでしょう。

unsafeコードはどのくらいunsafeか

unsafeコードが名前通りunsafe(安全でない)なところを、一例出しておきます。

通常、C#の文字列(string型)は書き換えできません。 書き換えれないようなすることで、同じメモリ領域を複数の場所から参照しても安全に使えます(どこか別のあずかり知らぬところで書き換わってる心配がない)。

ですが、unsafeコードを使うと、文字列を書き換えれてしまいます。 例えば以下のようなコードが書けます。

using System;

class Program
{
    static void Main()
    {
        // C# の string は書き換えできないはず
        var s1 = "-----";

        // 参照型なので、同じインスタンスを見てる
        // 書き換えれないからこそ、インスタンスの共有が安全
        var s2 = s1;

        // 実際には、C# の string は書き換えれる
        unsafe
        {
            fixed (char* c = s1)
            {
                c[2] = 'X';
            }
        }

        Console.WriteLine(s1); // --X--
        Console.WriteLine(s2); // 同じものを見てるので、こちらにも書き換えの影響が出てて --X--
    }
}

無制限にやられてしますと結構怖いコードです。 このように、unsafeコードの利用には注意が必要です。|

更新履歴

ブログ