SpecialNameAttribute クラス (System.Runtime.CompilerServices)
最近見っけた。
これ使うとメソッドやフィールドに IL レベルで specialname 修飾子を付けれる。
using System;
using System.Runtime.CompilerServices;
class Hoge
{
public Hoge(string value)
{
_value = value;
}
private string _value;
[SpecialName]
public static string op_Explicit(Hoge obj)
{
return obj._value;
}
}
こうすれば普通のメソッド定義と同じ形式で演算子のオーバーロードができる。
けど、同一プロジェクト内からは演算子として呼び出すことはできなくて、静的メソッドとして呼び出す必要あり。
これを使って何か面白いことできないかなーとか考えてたんだけど・・・無い!
.ctor なんて名前のメソッドは定義できないからコンストラクタは作れないし、get_XXX とか set_XXX とか add_XXX とか remove_XXX なんてメソッド作ったところでプロパティやイベントになるわけでもなし。まぁできたところで・・・何も面白くない。
interface との変換演算子とかジェネリックな変換演算子なんて作ってみたけどコンパイラが認識しないから実行されず。
強いて言えば C++/CLI で使える非静的な演算子を用意したりできるけど・・・無意味すぎ。
コンソール アプリケーションの作り方 ≪ ++C++; // 未確認飛行 C ブログ より
PowerShell って .NET Framework 4 で作ったアセンブリ読み込めないんですよねぇ・・・。アップデートして欲しい・・・
確か CLR のバージョンって指定できたなぁと思い調べてみたらできました。
これで .NET 4.0 の新しいクラスを使用したり、アセンブリを読み込んだりすることができます。
[手順]
PowerShell のインストールフォルダ (%windir%\system32\WindowsPowerShell\v1.0) に次のファイルを作成する。
powershell.exe.config
<?xml version ="1.0"?>
<configuration>
<startup useLegacyV2RuntimeActivationPolicy="true" >
<supportedRuntime version="v4.0.30319" />
</startup>
</configuration>
null許容型は参照型と値型のどちらになるの?
C# だとローカル変数への代入は値渡ししかできません。
参照渡しで代入を行うようなコードを C# っぽく書いてみます。
Sample.cs
class Program
{
static void Main()
{
int v0 = 0;
int& v1 = v0;
v0 = 5;
System.Console.WriteLine(v0);
System.Console.WriteLine(v1);
}
}
このコードが動作するとすれば、v1 への代入が参照渡しになるので v1 からも 5 が取得されるはずですが、当然ながらこのコードはそもそもコンパイルできません。
しかし、IL ならばローカル変数への参照渡しができます。
先ほどのコードと同等のコードを IL で書くと次のようになります。
Sample.il
.assembly extern mscorlib { }
.assembly sample { }
.class private abstract auto ansi sealed beforefieldinit Program
extends [mscorlib]System.Object
{
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
.locals init
(
[0] valuetype [mscorlib]System.Int32 v0,
[1] valuetype [mscorlib]System.Int32& v1
)
// v0 = 0;
ldc.i4.0
stloc.0
// v1 = v0;
ldloca.s v0
stloc.1
// v0 = 5;
ldc.i4.5
stloc.0
// Console.WriteLine(v0);
ldloc.0
call void [mscorlib]System.Console::WriteLine(int32)
// Console.WriteLine(v2);
ldloc.1
ldind.i4
call void [mscorlib]System.Console::WriteLine(int32)
ret
}
}
このコードを次のコマンドで exe ファイルとしてアセンブルします。
ilasm Sample.il
これを実行すると、v0 も v1 も 5 を返すことが確認できます。
つまり、v1 への参照渡しによる代入が可能であることが確認できます。
…まぁ、だからどうというわけでもないんですが。
IL で可能なので、C++/CLI でもできるかもしれませんね。
直交座標系とか極座標系とかはよくわかってないけど、とりあえず C# でもできます。
まぁ、2つのインスタンスのメモリ上のサイズが等しくない場合はマズいことになりかねませんけどね…。
Cartesian と Polar はサイズが等しいので大丈夫です。
// 追記1 (2009/01/28)
対象オブジェクトのフィールドを辿るとマネージヒープ上のオブジェクトへの参照が含まれている、という場合も、場合によってはマズいことになります。
// 追記2 (2009/01/28)
NyaRuRu さんより、「何が起きても不思議ではない」とご指摘頂きました。
今回のコードは動作しましたが、インスタンスサイズを揃えて、フィールドにマネージヒープ上のオブジェクトへの参照を含めないようにしたとしても、確実に大丈夫だと断言することはできません。また、今回のコードが如何なる時でも確実に動作すると断言することもできません。
using System;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
class Cartesian
{
public Cartesian(double x, double y)
{
this.X = x;
this.Y = y;
}
public double X { get; set; }
public double Y { get; set; }
public Polar ToPolar()
{
return new Polar(Math.Sqrt(X * X + Y * Y), Math.Atan2(Y, X));
}
}
class Polar
{
public Polar(double r, double theta)
{
this.R = r;
this.Theta = theta;
}
public double R { get; set; }
public double Theta { get; set; }
public Cartesian ToCartesian()
{
return new Cartesian(R * Math.Cos(Theta), R * Math.Sin(Theta));
}
}
class Program
{
static void Transmogrify(object a, object b)
{
Type aType = a.GetType();
Type bType = b.GetType();
BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
FieldInfo[] aFieldInfos = aType.GetFields(flags);
FieldInfo[] bFieldInfos = bType.GetFields(flags);
var aFields = aFieldInfos.Select(item => new { Key = item, Value = item.GetValue(a) }).ToList();
var bFields = bFieldInfos.Select(item => new { Key = item, Value = item.GetValue(b) }).ToList();
ConvertType(a, bType);
ConvertType(b, aType);
aFields.ForEach(item => item.Key.SetValue(b, item.Value));
bFields.ForEach(item => item.Key.SetValue(a, item.Value));
}
static unsafe void ConvertType(object target, Type newType)
{
IntPtr newTypeHandle = newType.TypeHandle.Value;
GCHandle targetGCHandle = GCHandle.Alloc(target, GCHandleType.Normal);
try
{
void* entryPointer = (void*)GCHandle.ToIntPtr(targetGCHandle);
void* targetPointer = *((void**)entryPointer);
IntPtr* typeHandlePointer = (IntPtr*)targetPointer;
*typeHandlePointer = newTypeHandle;
}
finally
{
targetGCHandle.Free();
}
}
static void Main(string[] args)
{
Polar pos1 = new Polar(Math.Sqrt(2), Math.PI / 4);
Polar pos2 = pos1;
Console.WriteLine(pos1.GetType().Name); //=> Polar
Transmogrify(pos1, pos1.ToCartesian());
Console.WriteLine(pos1.GetType().Name); //=> Cartesian
Cartesian pos1AsCart = pos1 as object as Cartesian;
Console.WriteLine(pos1AsCart.X); //=> 1
Console.WriteLine(pos1AsCart.Y); //=> 1
Console.WriteLine(pos2.GetType().Name); //=> Cartesian
Transmogrify(pos1, pos1AsCart.ToPolar());
Console.WriteLine(pos1.GetType().Name); //=> Polar
Console.WriteLine(pos1.R); //=> 1.4142135623731
Console.WriteLine(pos1.Theta / Math.PI); //=> 0.25
Console.ReadLine();
}
}
[関連]
C#と諸々 オブジェクトの型を破壊的に変換
C#と諸々 ガベージコレクションを開始するには
NoOptimization.cs
using System;
using System.Diagnostics;
static class Program
{
static void Main(string[] args)
{
TailCall(5);
Console.ReadLine();
}
static int TailCall(int i)
{
int frameCount = new StackTrace().FrameCount;
Console.WriteLine("FrameCount:{0}", frameCount);
if (i == 0)
{
return i;
}
i--;
return TailCall(i);
}
}
これと等価な IL コード。
NoOptimization.il
.assembly extern mscorlib { }
.assembly NoOptimization { }
.class private abstract auto ansi sealed beforefieldinit Program
extends [mscorlib]System.Object
{
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
.maxstack 8
ldc.i4.5
call int32 Program::TailCall(int32)
pop
call string [mscorlib]System.Console::ReadLine()
pop
ret
}
.method private hidebysig static int32 TailCall(int32 i) cil managed
{
.maxstack 2
.locals init (
[0] int32 frameCount)
newobj instance void [mscorlib]System.Diagnostics.StackTrace::.ctor()
callvirt instance int32 [mscorlib]System.Diagnostics.StackTrace::get_FrameCount()
stloc.0
ldstr "FrameCount:{0}"
ldloc.0
box int32
call void [mscorlib]System.Console::WriteLine(string, object)
ldarg.0
brtrue.s Label1
ldarg.0
ret
Label1:
ldarg.0
ldc.i4.1
sub
starg.s i
ldarg.0
call int32 Program::TailCall(int32)
ret
}
}
実行結果はこちら。末尾最適化が行われていないことがわかる。
FrameCount:2
FrameCount:3
FrameCount:4
FrameCount:5
FrameCount:6
FrameCount:7
続いて、IL コードに tail. プレフィックスを追加してみる。
追加する場所は、TailCall メソッドの下から 3 行目。
Optimization.il
.assembly extern mscorlib { }
.assembly Optimization { }
.class private abstract auto ansi sealed beforefieldinit Program
extends [mscorlib]System.Object
{
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
.maxstack 8
ldc.i4.5
call int32 Program::TailCall(int32)
pop
call string [mscorlib]System.Console::ReadLine()
pop
ret
}
.method private hidebysig static int32 TailCall(int32 i) cil managed
{
.maxstack 2
.locals init (
[0] int32 frameCount)
newobj instance void [mscorlib]System.Diagnostics.StackTrace::.ctor()
callvirt instance int32 [mscorlib]System.Diagnostics.StackTrace::get_FrameCount()
stloc.0
ldstr "FrameCount:{0}"
ldloc.0
box int32
call void [mscorlib]System.Console::WriteLine(string, object)
ldarg.0
brtrue.s Label1
ldarg.0
ret
Label1:
ldarg.0
ldc.i4.1
sub
starg.s i
ldarg.0
tail.
call int32 Program::TailCall(int32)
ret
}
}
実行結果はこちら。末尾最適化が行われていることがわかる。
FrameCount:2
FrameCount:2
FrameCount:2
FrameCount:2
FrameCount:2
FrameCount:2
さてここで、Optimization.il の TailCall メソッドに noinlining を付加して試してみる。
NoInlining.il
.assembly extern mscorlib { }
.assembly NoInlining { }
.class private abstract auto ansi sealed beforefieldinit Program
extends [mscorlib]System.Object
{
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
.maxstack 8
ldc.i4.5
call int32 Program::TailCall(int32)
pop
call string [mscorlib]System.Console::ReadLine()
pop
ret
}
.method private hidebysig static int32 TailCall(int32 i) cil managed noinlining
{
.maxstack 2
.locals init (
[0] int32 frameCount)
newobj instance void [mscorlib]System.Diagnostics.StackTrace::.ctor()
callvirt instance int32 [mscorlib]System.Diagnostics.StackTrace::get_FrameCount()
stloc.0
ldstr "FrameCount:{0}"
ldloc.0
box int32
call void [mscorlib]System.Console::WriteLine(string, object)
ldarg.0
brtrue.s Label1
ldarg.0
ret
Label1:
ldarg.0
ldc.i4.1
sub
starg.s i
ldarg.0
tail.
call int32 Program::TailCall(int32)
ret
}
}
実行結果はこちら。noinlining を付加しても末尾最適化が行われていることが確認できる。
FrameCount:2
FrameCount:2
FrameCount:2
FrameCount:2
FrameCount:2
FrameCount:2
では、Optimization.il の TailCall メソッドに reqsecobj を付加して試してみる。
ReqSecObj.il
.assembly extern mscorlib { }
.assembly ReqSecObj { }
.class private abstract auto ansi sealed beforefieldinit Program
extends [mscorlib]System.Object
{
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
.maxstack 8
ldc.i4.5
call int32 Program::TailCall(int32)
pop
call string [mscorlib]System.Console::ReadLine()
pop
ret
}
.method private hidebysig static reqsecobj int32 TailCall(int32 i) cil managed
{
.maxstack 2
.locals init (
[0] int32 frameCount)
newobj instance void [mscorlib]System.Diagnostics.StackTrace::.ctor()
callvirt instance int32 [mscorlib]System.Diagnostics.StackTrace::get_FrameCount()
stloc.0
ldstr "FrameCount:{0}"
ldloc.0
box int32
call void [mscorlib]System.Console::WriteLine(string, object)
ldarg.0
brtrue.s Label1
ldarg.0
ret
Label1:
ldarg.0
ldc.i4.1
sub
starg.s i
ldarg.0
tail.
call int32 Program::TailCall(int32)
ret
}
}
実行結果はこちら。noinlining とは異なり、reqsecobj を付加すると末尾最適化が行われなくなることが確認できる。
FrameCount:2
FrameCount:3
FrameCount:4
FrameCount:5
FrameCount:6
FrameCount:7
[関連記事]
JIT 最適化にも負けずに呼び出し元のメソッドを取得する方法
例えば、次のコードの実行結果を Debug ビルドと Release ビルド (非デバッグ実行) とで比較すると一目瞭然だ。
using System;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.CompilerServices;
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
Hoge();
Console.ReadLine();
}
static void Hoge()
{
Fuga();
}
[MethodImpl(MethodImplOptions.NoInlining)]
static void Fuga()
{
const int callerFrameIndex = 1;
StackFrame callerFrame = new StackFrame(callerFrameIndex);
MethodBase callerMethod = callerFrame.GetMethod();
Console.WriteLine(callerMethod.Name);
}
}
}
Debug ビルドでは Hoge と出力されるが、Release ビルド (非デバッグ実行) では Main と出力されることが確認できる。つまり、Release ビルド (非デバッグ実行) では JIT 最適化により Hoge メソッドがインライン展開されているわけである。
JIT 最適化によるインライン展開を抑止する方法としては、MethodImpl 属性 を付加して MethodImplOptions.NoInlining を指定するという方法が提供されている。しかし、この方法でインライン展開が抑止されるのは属性が付加されたメソッドのみである。そのため、呼び出し元のメソッド (上記の例なら Hoge メソッド) にこの属性を付加しなければならない。
また、この属性は絶対的なものではなく、64 bit CLR では無視されてしまうらしい。
また、インライン展開の抑止は、C# ・ IL レベルのメソッド呼び出しとコールスタックの一致を保証するわけではない。64 bit - CLR では、インライン展開以外に末尾最適化によっても、メソッド呼び出しとコールスタックの不一致が生じる場合があるのだが、NoInlining では末尾最適化は抑止されない。
デバッグ技 : .ini ファイルによる JIT コンパイラ制御
# このリンク先では ini ファイルを使用して インライン展開を抑止する方法 コールスタックの不一致 を防ぐ方法が紹介されているが、これはあくまでもデバッグ目的の手段である。
では、呼び出し元メソッドのコールスタックを維持する方法は無いだろうか。
実はある。
IL レベルで reqsecobj キーワードが付加されているメソッドでは、呼び出し元メソッドのコールスタックが維持されるのである。 (reqsecobj キーワードが付加されているメソッド自身のコールスタックも維持される。)
C# コンパイラには、このキーワードをメソッドに付加する方法が用意されていて、メソッドに DynamicSecurityMethodAttribute クラス (System.Security) を属性として付加すれば良い。
このクラスは mscorlib.dll の internal クラスであり僕らは本来使用することができないのだが、同名のクラスを自前で用意して使用すれば C# コンパイラは reqsecobj キーワードを付加してくれる。
先ほどのコードに、DynamicSecurity 属性を付けて、再び Release ビルド (非デバッグ実行) で実行してみると、見事にコールスタックが維持されることが確認できる。
using System;
using System.Diagnostics;
using System.Reflection;
using System.Security;
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
Hoge();
Console.ReadLine();
}
static void Hoge()
{
Fuga();
}
[DynamicSecurityMethod]
static void Fuga()
{
const int callerFrameIndex = 1;
StackFrame callerFrame = new StackFrame(callerFrameIndex);
MethodBase callerMethod = callerFrame.GetMethod();
Console.WriteLine(callerMethod.Name);
}
}
}
namespace System.Security
{
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
internal sealed class DynamicSecurityMethodAttribute : Attribute
{
}
}
見ての通り、呼び出し元メソッドである Hoge メソッドには属性の付加が一切不要である。
[修正履歴]
2008/10/07
NyaRuRu さんのコメントを元に記事本文を修正。
重要な削除箇所は取り消し線 + 文字色変更 (灰色)。
重要な追記箇所は背景色変更 (緑色)。
目印は付けていないが、何箇所か「インライン展開が抑止」という旨の記述を「コールスタックが維持」という記述に修正してある。
2008/10/08
冒頭の検証用コードで、Fuga メソッドまでインライン展開されてしまいエラーとなっていたので 、Fuga メソッドに MethodImpl(MethodImplOptions.NoInlining) 属性を付加。
と書きましたが、もうちょい詳しく書いときます。
# 今回の記事は Nullable 型に限らない話です。
[ Object クラスに定義されている非仮想メソッド ]
Object クラスには他にも非仮想メソッドとして MemberwiseClone メソッドが定義されていますが、このメソッドを呼び出す場合も同様にボックス化が発生します。
ポイントは次の 2 点です。
・ インスタンスメソッドは自分自身を取得できる
インスタンスメソッドでは、this キーワードにて自分自身を取得できます。しかしこれは、C# の話であって、IL レベルでは、this キーワードに相当する機能は用意されていません。
ではどうやって自分自身を取得するかというと、実はメソッドの 0 番目の引数として自分自身が渡されます。
・ 非仮想メソッドはオーバーライドできない
非仮想メソッドを派生クラスがオーバーライドすることはできません。つまり、GetType メソッドはオーバーライドされていないわけです。GetType メソッドは Object クラスに定義されています。
この 2 点を踏まえて考えてみてください。
GetType() メソッドの 0 番目の引数の型、つまり GetType メソッドにおける this の型は何型でしょう?
これがボックス化の理由です。
答えは Object 型ですね。
[ コンストラクタについて ]
値型は ValueType クラスを継承します。ValueType クラスはあくまでも参照型です。
値型にはコンストラクタを定義できます (C# の場合引数を持たないコンストラクタを値型に定義することはできません)。通常、コンストラクタは基底クラスのコンストラクタを呼び出さなければいけません。そしてコンストラクタは非仮想メソッド (正確には継承されないメソッド) です。ということは、コンストラクタが呼び出されるとボックス化が発生するのでしょうか?
答えは「発生しない」です。これは、値型のコンストラクタは基底クラスのコンストラクタを呼び出さないからです (これが CLR で決められているルールなのかはちょっとわからなかったです)。
[ ValueType クラスでオーバーライドされているメソッドについて ]
「プログラミング Microsoft .NET Framework 第2版」によると、ToString メソッドや Equals メソッド等の、ValueType クラスでオーバーライドされているメソッドに対してはボックス化が行われないと書いてあります。でも、ValueType クラスは参照型です。ホントにボックス化されないのでしょうか?
答えは「される」です。つまり、「プログラミング Microsoft .NET Framework 第2版」に書かれているのことは正しくないです。やはり、ValueType は参照型だからボックス化されるのです。
当然ですが、値型自身が更に ToString メソッドをオーバーライドしている場合は、ボックス化はされません。0 番目の引数の型は値型ですので。
以降、このことについて MSIL レベルで説明をします。
まず、Int32 の値に対して ToString メソッドを呼び出すコードを見てみます。
static void Main(string[] args)
{
int a = 10;
a.ToString();
}
このコードは次のような MSIL コードにコンパイルされます。
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
.maxstack 1
.locals init (
[0] int32 a)
L_0000: ldc.i4.s 10
L_0002: stloc.0
L_0003: ldloca.s a
L_0005: call instance string [mscorlib]System.Int32::ToString()
L_000a: pop
L_000b: ret
}
L_0003 と L_0005 をみると、ローカル変数 a のマネージポインタをプッシュして Int32.ToString メソッドを呼び出しています。これは特に変わったところもありません。ローカル変数 a に対して ToString メソッドを呼び出すごく普通の MSIL です。(ちなみに、マネージポインタをプッシュしている理由ですが、マネージポインタではなくローカル変数 a を直接プッシュしてしまうとローカル変数 a のコピーがプッシュされてしまうためです。)
では、次のようなコードはどうでしょうか。
static void Main(string[] args)
{
Hoge h = new Hoge();
h.ToString();
}
struct Hoge
{
}
このコード (の Main メソッド) は次のような MSIL コードにコンパイルされます。
.method private hidebysig static void Main(string[] args) cil managed
{
.entrypoint
.maxstack 1
.locals init (
[0] valuetype Program/Hoge h)
L_0000: ldloca.s h
L_0002: initobj Program/Hoge
L_0008: ldloca.s h
L_000a: constrained Program/Hoge
L_0010: callvirt instance string [mscorlib]System.Object::ToString()
L_0015: pop
L_0016: ret
}
L_0008, L_000a, L_0010 を見てください。
L_0008 については、先ほどのコードの L_0003 と同様ローカル変数 h のマネージポインタをプッシュしています。問題は次の L_000a と L_0010 です。
まず L_0010 ですが、Hoge.ToString を call 命令で呼び出すのではなく、 Object.ToString を callvirt 命令で呼び出しています。
もし、Hoge.ToString を call 命令で呼び出していれば、CLR は最初から Hoge.ToString を使用することを知ることができます。
しかし、Object.ToString を callvirt 命令で呼び出しているということは、遅延バインディング (実行時に変数 h の型を調べて適切なメソッドを判断) するということになります。
次に (一行戻って) L_000a ですが、この constrained が鍵を握っています。これは callvirt 命令にだけ付加することができるプリフィックスです。このプリフィックスでは、一緒に型を指定します (以降、thisType と記述)。
thisType が値型の場合、次の 2 つのケースでそれぞれ異なる動作をします。
- tisType が callvirt 命令で指定されているメソッドを実装していない場合
- tisType が callvirt 命令で指定されているメソッドを実装している場合
今見ている Hoge 構造体は、ToString をオーバーライドしていませんので 1 に該当します。
この場合、ローカル変数 h はボックス化されて ToString メソッドに渡されます。まぁ、先ほども言ったように、ValueType クラスは参照型ですから当然です。そして、Object.ToString を callvirt 命令で呼び出したことになります。つまり、メソッドの遅延バインディングが発生します。
では、Hoge 構造体で ToString メソッドをオーバーライドして再コンパイルしたとします。その場合も Main メソッドの MSIL コードは変わりません。すると 2 に該当することになります。
この場合、ローカル変数 h はボックス化されずに ToString メソッドに渡されます。これも適切な動作です。また、Hoge.ToString メソッドを call 命令で呼び出した場合と同じ動作をします。つまり、メソッドの遅延バインディングが発生しません。
なぜ、Int32 値の ToString メソッドを呼び出すコードでは、constrained プリフィックスが使用されなかったのでしょうか?
恐らくそれは、バージョン問題が発生しないからです。
今回の Hoge 構造体のように、ユーザー定義の構造体のインスタンスに対して、ValueType クラスでオーバーライドされているメソッドを呼び出す場合はバージョンの問題が発生する可能性があります。
たとえば、最初は Hoge.ToString を定義せずにコンパイルしたが、後から Hoge.ToString を定義 (オーバーライド) して再コンパイルした場合、利用側のコードはコンパイル前と後で異なる動作をしなければなりません。もし constrained プリフィックスがなければ、利用側のコードも再コンパイルしないといけなくなるわけです。
というわけで、constrained プリフィックスのおかげで、再コンパイルせずに動作を切り替えられるわけです。
# すごい疲れた
[ 参考 ]
OpCodes.Constrained フィールド (System.Reflection.Emit)
Type type = a.GetType();
Console.WriteLine(type.Name);
を実行すると、
Int32
って表示されるんだ。
type = a.GetType();
だと、
「オブジェクト参照がオブジェクト インスタンスに設定されていません。」
と、例外が発生する。
へー、そんな動きするんですね。
ちょっと調べてみましたところ、CLR の動作によるもののようでした。
まず、GetType メソッドは Object クラスで定義されている非仮想メソッドなので、GetType メソッドを値型に対して呼び出す際はボックス化が必要になります (Nullable 型は値型です) 。
そして、Nullable 型をボックス化する際、CLR は特別な動作をします。
Nullable 型が (論理的に) null である場合、ボックス化を行わず (正真正銘の) null を返します。
Nullable 型が (論理的に) null でない場合、Nullable 型が保持している生 (?) の値をボックス化して返します。
ということで、Nullable 型に対して GetType メソッドを呼び出しても、生の値、あるいは null に対して呼び出されてしまうというわけです。
# ちなみに、C# コンパイラが原因と予想して調べ始めたのですが、見事に外れました ^^;
ガベージコレクションを開始する際、CLR はまず、全てのスレッドを一時停止します。そして、それぞれのスレッドが実行中だったコードを確認し、「セーフポイント」に到達しているかどうかを調べます。
セーフポイントに到達しているスレッドは、ガベージコレクションが完了するまでそのまま一時停止にしておいて問題ありません。
セーフポイントに到達していないスレッドはそのまま一時停止にしておくわけにはいきません。しかし、ガベージコレクションの実行中は全てのスレッドを一時停止 しておく必要があります。そのため CLR は、スレッドの乗っ取りを行おうとします。簡単に言うと、現在実行中のメソッドの戻り先を書き換えてスレッドを再開させます。これにより、メソッドの完了後に特殊な関数を実行するようにし、この特殊な関数内でスレッドを一時停止するわけです。
ただし、乗っ取りを行おうとしてから 250 ミリ秒経過しても乗っ取れなかった (=メソッドが未完了) 場合、CLR は再びスレッドを一時停止し、再びセーフポイントに到達しているかどうかを調べます。到達している場合は、そのまま一時停止します。到達していない場合はスレッドの再開となりますが、前回乗っ取りを行おうとした時とは違うメソッドを実行していた場合は、再開前にそのメソッドの戻り先を書き換えます。再開後数ミリ秒待機しても乗っ取れなかった場合は、再びこの動作を繰り返します。
セーフポイントはコストが高いため、必要最低限の箇所にしか用意されません。具体的には、メソッド呼び出しを含まないループ構造にのみ用意されます。メソッド呼び出しを含まないループ構造では、いつまで経っても乗っ取りが出来ない可能性があるためです。
と、ここまでが前置きです。
これらを踏まえると、オブジェクトの型を破壊的に変換 のメソッドは、アンマネージポインタを取得してから型ハンドルの書き換えを行うまでの間にメソッド呼び出しを行わないようにすれば、ガベージコレクションによって問題を引き起こすことがなくなると思わます。
アンマネージポインタ取得してから型ハンドルの書き換えを行うまでの間のメソッド呼び出しは、Type.TypeHandle プロパティの呼び出しと、RuntimeTypeHandle.Value プロパティの呼び出しのみです。つまり、newType 引数から型ハンドルを取得する処理をアンマネージポインタ取得処理より前で行うように修正すれば良いわけです。
/// <summary>
/// 指定したオブジェクトを強制的に別の型に変換します。
/// このメソッドはオブジェクトの型を破壊的に変換するため、予期しない動作を引き起こす可能性があります。
/// </summary>
/// <param name="target">対象オブジェクト。</param>
/// <param name="newType">新しい型。</param>
static unsafe void ConvertType(object target, Type newType)
{
// 型ハンドルを取得
IntPtr newTypeHandle = newType.TypeHandle.Value;
// 対象オブジェクトに対する GC ハンドルを生成
GCHandle targetGCHandle = GCHandle.Alloc(target, GCHandleType.Normal);
try
{
// GC ハンドルの実際のエントリへのポインタを取得
void* entryPointer = (void*)GCHandle.ToIntPtr(targetGCHandle);
// エントリにはオブジェクトへの参照が格納されている
void* targetPointer = *((void**)entryPointer);
// オブジェクトへのポインタは、オブジェクトヘッダーの型ハンドル部分を参照している
IntPtr* typeHandlePointer = (IntPtr*)targetPointer;
// 型ハンドルを書き換えることで型を強制的に変換する
*typeHandlePointer = newTypeHandle;
}
finally
{
// GC ハンドルの解放
targetGCHandle.Free();
}
}
これで、アンマネージポインタ取得してから型ハンドルの書き換えを行うまでの間に、ガベージコレクションによってオブジェクトが移動することはなくなったはずです。
次の検証用コードで検証した結果、問題ありませんでした。
static void Main(string[] args)
{
bool run = true;
Action action =
delegate
{
while (run)
{
object o = new object();
Console.WriteLine("collect.");
GC.Collect();
}
};
action.BeginInvoke(null, null);
while (true)
{
object o = new object();
ConvertType(o, typeof(void));
Console.WriteLine(o.GetType());
if (o.GetType() != typeof(void))
{
Console.WriteLine("failed.");
run = false;
break;
}
}
Console.ReadLine();
}
ちなみに、修正前のメソッドで検証してみても、問題は中々発生しません。簡単に発生させるためには、オブジェクトの型ハンドルを書き換えている箇所にブレークポイントを貼ってデバック実行します。ブレークと再開を何回か繰り返せば簡単に発生します。
もちろん、修正後のメソッドならこの方法で検証しても問題が発生しません。
通常、オブジェクトに結びついている型を変更することはできません。例えば String オブジェクトを Object クラスにキャストしても、オブジェクト自体は String オブジェクトのままです。
オブジェクトはメモリ上のデータです。このデータには、オブジェクトヘッダーと各フィールドの値が含まれます。オブジェクトヘッダーには、同期ブロックインデックスと型ハンドルが含まれます。
この型ハンドルが、オブジェクトと結びついている型を示します。型ハンドルは、ローダーヒープ (アプリケーションドメイン内にロードされた型情報を格納している領域) 内にある型情報 (Type クラスのインスタンスとは別物) へのポインタです。Object.GetType メソッドは、この型ハンドルを元に Type クラスのインスタンスを取得して返却しています。
つまり、オブジェクトヘッダー内の型ハンドルを書き換えると、オブジェクトに結びついている型を変換することができるわけです。
オブジェクトヘッダーへの直接のアクセスはサポートされていません。ただし、アンマネージドコードを用いれば、アンマネージポインタからメモリを直接参照することができますので、これでアクセスすることが可能です。ちなみに、オブジェクトの参照 (マネージポインタ) は、オブジェクトヘッダー内の型ハンドルのアドレスを示すようになっています。
下記のメソッドは、オブジェクトに結びついている型を変換します。(ビルドするにはアンセーフコードの許可が必要です。)
/// <summary>
/// 指定したオブジェクトを強制的に別の型に変換します。
/// このメソッドはオブジェクトの型を破壊的に変換するため、予期しない動作を引き起こす可能性があります。
/// </summary>
/// <param name="target">対象オブジェクト。</param>
/// <param name="newType">新しい型。</param>
static unsafe void ConvertType(object target, Type newType)
{
// 対象オブジェクトに対する GC ハンドルを生成
GCHandle targetGCHandle = GCHandle.Alloc(target, GCHandleType.Normal);
try
{
// GC ハンドルの実際のエントリへのポインタを取得
void* entryPointer = (void*)GCHandle.ToIntPtr(targetGCHandle);
// エントリにはオブジェクトへの参照が格納されている
void* targetPointer = *((void**)entryPointer);
// オブジェクトへのポインタは、オブジェクトヘッダーの型ハンドル部分を参照している
IntPtr* typeHandlePointer = (IntPtr*)targetPointer;
// 型ハンドルを書き換えることで型を強制的に変換する
*typeHandlePointer = newType.TypeHandle.Value;
}
finally
{
// GC ハンドルの解放
targetGCHandle.Free();
}
}
以下は使用例です。
static void Main(string[] args)
{
object obj = new object();
Console.WriteLine("obj is a {0}", obj.GetType());
ConvertType(obj, typeof(IDisposable));
Console.WriteLine("obj is a {0}", obj.GetType());
Console.ReadLine();
}
以下は実行結果です。
obj is a System.Object
obj is a System.IDisposable
このメソッドを使えば、オブジェクトに結びついている型をどんな型にでも変換できます。抽象クラスやインターフェイスに変換することもできます。ただし、その結果予期せぬ動作を引き起こす可能性があります。例えば、変換前の型より変換後の型の方がサイズが大きいと、他のオブジェクトを破壊してしまう場合があります。
また、このメソッドには一つ欠点があります。オブジェクトへのポインタを取得してから型ハンドルの書き換えを行うまでの間に、ガベージコレクションによってオブジェクトが移動されてしまうと、型変換が正常に行われず、全く無関係なデータを型ハンドルで上書きしてしまう可能性があります。
Blittable 型と呼ばれる型に関しては、GC 時にオブジェクトが移動してしまわないよう固定化することができますが、非 Blittable 型では固定化ができません。
この問題を解決する方法は不明 こちら です。
言うまでもないと思いますが、これはとても危険でトリッキーなメソッドです。普通はまず使うことのないメソッドです。
このメソッドで何か面白いことができたら、是非教えてください。