でも実は、コンストラクタを実行せずにインスタンスを生成することができます。
方法はとても簡単です。
生成したいインスタンスの型情報を渡して FormatterServices クラス (System.Runtime.Serialization) の GetUninitializedObject メソッド を呼び出すだけです。
このクラスは、通常シリアル化で使用されるクラスですが、インスタンス化したいクラスに SerializableAttribute クラス (System) が付加されている必要はありません。
それと面白いことに、Void 構造体 (System) をインスタンス化することができます。 (Activator じゃできないのに)
ただし、String クラス (System)、 ContextBoundObject クラス (System) はこの方法でインスタンス化することができません。
下記のコードでは、コンストラクタの処理中に例外をスローするクラスのインスタンスを生成できることが確認できます。
using System;
using System.Runtime.Serialization;
class Program
{
static void Main()
{
Hoge h = (Hoge)FormatterServices.GetUninitializedObject(typeof(Hoge));
}
}
public class Hoge
{
public Hoge()
{
throw new NotSupportedException();
}
}
で、配列が定義される際、どうも Address メソッドというものが定義されるようです。開発者がこのメソッドを呼び出すことは普通はできません。
PowerShell で以下のコードを実行すれば、この Address メソッドを確認できます。
$addressMethod = [Int32[]].GetMethod("Address");
$addressMethod.ToString(); # Int32& Address(Int32)
このメソッドは Int32 型のパラメータを一つ持っているようです。戻り値は、なんと Int32 の ByRef 型です。つまり、Int32 値への参照を返します。
動作は何となく予想が付きます。たぶん、指定したインデックス番号の位置にある要素への参照を返すメソッドでしょう。
一応検証してみます。
using System;
using System.Reflection;
using System.Reflection.Emit;
public static void Main()
{
DynamicMethod addressInvoker = new DynamicMethod(string.Empty, typeof(int), null);
ILGenerator invokerILGenerator = addressInvoker.GetILGenerator();
invokerILGenerator.DeclareLocal(typeof(int[])); // Int32[] 型のローカル変数を宣言
invokerILGenerator.Emit(OpCodes.Ldc_I4, 2); // 2 を Int32 型としてスタックにプッシュ (要素数)
invokerILGenerator.Emit(OpCodes.Newarr, typeof(int)); // 要素数 2 の Int32[] オブジェクトを生成
invokerILGenerator.Emit(OpCodes.Stloc_0); // 0 番目のローカル変数に設定
invokerILGenerator.Emit(OpCodes.Ldloc_0); // 0 番目のローカル変数をスタックにプッシュ
invokerILGenerator.Emit(OpCodes.Ldc_I4, 0); // 0 を Int32 型としてスタックにプッシュ (配列のインデックス)
invokerILGenerator.Emit(OpCodes.Ldc_I4, 1234); // 1234 を Int32 型としてスタックにプッシュ (要素 0 の値)
invokerILGenerator.Emit(OpCodes.Stelem_I4); // 配列の 0 番目の要素に 1234 を設定
invokerILGenerator.Emit(OpCodes.Ldloc_0); // 0 番目のローカル変数をスタックにプッシュ
invokerILGenerator.Emit(OpCodes.Ldc_I4, 1); // 1 を Int32 型としてスタックにプッシュ (配列のインデックス)
invokerILGenerator.Emit(OpCodes.Ldc_I4, 5678); // 1234 を Int32 型としてスタックにプッシュ (要素 0 の値)
invokerILGenerator.Emit(OpCodes.Stelem_I4); // 配列の 0 番目の要素に 5678 を設定
MethodInfo addressMethod = typeof(int[]).GetMethod("Address");
invokerILGenerator.Emit(OpCodes.Ldloc_0); // 0 番目のローカル変数をスタックにプッシュ
invokerILGenerator.Emit(OpCodes.Ldc_I4, 0); // 0 を Int32 型としてスタックにプッシュ (引数)
invokerILGenerator.Emit(OpCodes.Callvirt, addressMethod); // Int32[].Address メソッドを呼び出す
invokerILGenerator.Emit(OpCodes.Ldind_I4); // 戻り値である参照が指す値をスタックにプッシュ
invokerILGenerator.Emit(OpCodes.Ret); // 返却
int result = (int)addressInvoker.Invoke(null, null);
}
なぜわざわざ動的メソッドを作って呼び出しを行っているのかというと、戻り値が ByRef 型のメソッドは MethodInfo.Invoke で呼び出すことができないからです。
この動的メソッド内では、まず、要素数が 2 の配列を生成し、0 番目に 1234、1 番目に 5678 を設定します。そして、Address メソッドに 0 を渡して呼び出します。最後にその戻り値である参照が示す値を取得して呼び出し元に返却します。
このコードを実行すると、予想通り 1234 が取得できます。
で、ここからが本題です。
このメソッドは何のためにあるのでしょうか?
次のコードを見てください。
static void Main()
{
int[] array = { 0, 1, 2 };
Method1(ref array[0]);
Console.WriteLine(array[0]);
}
static void Method1(ref int arg1)
{
arg1 = 10;
}
普通に考えると、プロパティやインデクサを参照パラメータにそのまま渡すことはできないので、このコードはコンパイルエラーになりそうです。しかし、このコードはコンパイルが通りますし、コンソールにはちゃんと 10 が出力されます。
ということで、答えはメソッドの ref パラメータに配列の要素の参照を渡すためです。
といっても、実は Address メソッドは上記のコードでは使われません。Address メソッドは下限が 0 の 1 次元配列に対しては使われないからです。このような配列のことを SZ 配列 (SZArray) と呼びます。SZ は「single-dimension, zero-base」の略です。
SZ 配列ではない配列の要素を参照渡するコードは、Address メソッドにて参照を取得するよう、コンパイルされます。
では、SZ 配列ではどのようにして参照を取得するのでしょうか?
SZ 配列には特別な IL 命令が使用できます。newarr, ldelem, ldelema, ldlen, stelem などの命令がそうです。これらの命令は SZ 配列にしか使用できません。この内の ldelema 命令は配列の要素の参照を取得するための命令です。つまり、SZ 配列の要素を参照渡しするコードは、ldelema 命令にて参照を取得するよう、コンパイルされるわけです。
Address メソッドの他にも、配列には Get メソッド (指定したインデックスの要素の値を取得) と Set メソッド (指定したインデックスの要素に値を設定) が自動的に定義されます。これらも SZ 配列ではない配列で使用され、SZ 配列では ldelem 命令と stelem 命令が使用されます。
SZ 配列では、このように、特別な IL 命令が使用されるため、パフォーマンスが向上するようです。
Windows PowerShell Get-Enjoy コンテスト結果発表
で、僕の作品が選考委員特別賞を頂きました!
プロトタイプチェーンというのが僕の作品です。マニアックな作品と、お褒めの言葉を頂きました (笑)
ダウンロードはこちらからです。
Prototype.zip
実は、コンテスト向けの作品は当初全く違う作品を作ろうとしていました。
スクリプトを書いている内にクラスが使いたくなってきて、Add-Member コマンドレットを使って疑似的にクラスのようなものを書き始めました。
気付いたら、疑似的なクラスを作るための機構に凝りだしてしまいました。
で、作品の路線を変更することにしました。
プロトタイプチェーンこそ備わっていませんが、オブジェクトへ動的にメンバーを追加したりできる PowerShell は、プロトタイプベースオブジェクト指向の流れを汲んでいるのだと思います。
ならば、プロトタイプチェーンを実装しよう。PowerShell にできないことは (あまり) ないはずだ!とその時考えたわけです。
そういえば、僕が初めて PowerShell に触った時、真っ先にスクリプトブロックに Prototype プロパティが備わっているかどうかを試しました。
JavaScript でプロトタイプベースオブジェクト指向を知った僕は、PowerShell にも当然のようにプロトタイプチェーンが備わっているだろうと (勝手に) 思い込んでいたので、備わっていないとわかった時には結構テンションが下がりました。
あの時の落胆をバネに、この作品を完成させることができたような気がしないでもないです。
PowerShell でプロセスを実行する関数
PowerShell で NUnit を実行する関数
また、以下の 2 つの PowerShell 関数を変更しました。
変更箇所については各記事を参照してください。
PowerShell で Sandcastle プロジェクトをビルドする関数
PowerShell で VS のソリューションをリビルドするための関数
ダウンロードはこちらから。
YokoKen.PowerShell.Scripts
NUnit を使用して記述されたユニットテストを実行する関数。
この関数は内部で Invoke-Process 関数を使用する。
[ パラメータ ]
testAssemblyPaths
ユニットテストアセンブリのパスの配列。
xmlOutputDirectoryPath
XML 出力ファイルの出力先ディレクトリのパス。
version
NUnit のバージョン。
timeoutMilliseconds
プロセスの実行時間に対するタイムアウト値 (単位:ミリ秒)。
無制限に待機する場合、-1 または Int32.MaxValue を指定。
省略した場合は -1。
[ 戻り値 ]
戻り値については Invoke-Process 関数を参照。
[ Invoke-NUnit ]
function global:Invoke-NUnit
{
param ([string[]] $testAssemblyPaths, [string]$xmlOutputDirectoryPath, [string]$version, [int]$timeoutMilliseconds = [System.Threading.Timeout]::Infinite)
trap { break; }
if ("$testAssemblyPaths".Length -eq 0) { throw "引数 testAssemblyPaths が null または空の配列です。"; }
if ([string]::IsNullOrEmpty($xmlOutputDirectoryPath)) { throw "引数 xmlOutputDirectoryPath が null または空の文字列です。"; }
if ([string]::IsNullOrEmpty($version)) { throw "引数 version が null または空の文字列です。"; }
$testAssemblyPathsText = [string]::Empty;
$testAssemblyPaths | % { $testAssemblyPathsText += "`"$_`" "; };
$nunitConsolePath = "$Env:ProgramFiles\NUnit $version\bin\nunit-console.exe";
$nunitConsoleArgs = "$testAssemblyPathsText /xml=`"$xmlOutputDirectoryPath\TestResult.xml`" /nologo /nodots";
return Invoke-Process $nunitConsolePath $nunitConsoleArgs $timeoutMilliseconds;
}
[ 使用例 ]
$testAssemblyPaths =
@(
"C:\work\Hoge\Project1.Test\bin\Release\Project1.Test.dll",
"C:\work\Hoge\Project2.Test\bin\Release\Project2.Test.dll"
);
$xmlOutputDirectoryPath = "C:\work\Hoge";
$result = Invoke-NUnit $testAssemblyPaths $xmlOutputDirectoryPath "2.4.6";
if (!$result.IsSuccess)
{
Write-Host "ユニットテストが失敗しました。" -ForegroundColor "Red";
}
【 ダウンロード 】
自作の PowerShell 関数は、以下の記事からまとめてダウンロードできます。
YokoKen.PowerShell.Scripts
PowerShell でプロセスを実行する関数。
[ パラメータ ]
processPath
プロセス (実行可能ファイル) のパス。
processArgs
プロセスを実行する際に渡す引数。
省略可能。
timeoutMilliseconds
プロセスの実行時間に対するタイムアウト値 (単位:ミリ秒)。
無制限に待機する場合、-1 または Int32.MaxValue を指定。
省略した場合は -1。
[ 戻り値 ]
プロセスの実行結果に関する情報を格納するハッシュテーブル。
各キーの説明を以下に記述する。
IsSuccess
プロセスが正常終了した場合は true、それ以外の場合は false。
プロセスが正常終了したかどうかは、プロセスの終了コードを元に判断される。
IsComplete
プロセスがタイムアウトせずに完了した場合は true、それ以外の場合は false。
Message
プロセスが実行中に出力したメッセージ。
ErrorMessage
プロセスが実行中に出力したエラーメッセージ。
[ Invoke-Process 関数 ]
function global:Invoke-Process
{
param ([string] $processPath, [string]$processArgs, [int]$timeoutMilliseconds = [System.Threading.Timeout]::Infinite)
trap
{
if ($() -ne $process)
{
$process.Dispose();
}
break;
}
if ([String]::IsNullOrEmpty($processPath)) { throw "引数 processPath が null または空の文字列です。"; }
$process = New-Object "System.Diagnostics.Process";
$process.StartInfo = New-Object "System.Diagnostics.ProcessStartInfo" @($processPath, $processArgs);
$process.StartInfo.WorkingDirectory = (Get-Location).Path;
$process.StartInfo.RedirectStandardOutput = $True;
$process.StartInfo.RedirectStandardError = $True;
$process.StartInfo.UseShellExecute = $False;
$process.Start() | Out-Null;
$message = $process.StandardOutput.ReadToEnd();
$errorMessage = $process.StandardError.ReadToEnd();
$complete = $process.WaitForExit($timeoutMilliseconds);
if (!$complete)
{
$process.Kill();
}
$result =
@{
"IsSuccess" = ($process.ExitCode -eq 0);
"IsComplete" = $complete;
"Message" = $message;
"ErrorMessage" = $errorMessage;
};
$process.Dispose();
return $result;
}
[ 使用例 ]
$result = Invoke-Process "$Env:ProgramFiles\Microsoft Visual Studio 9.0\Common7\IDE\devenv.com" "/rebuild Release `"C:\work\Hoge\Hoge.sln`"";
Write-Host $result.Message;
if (!$result.IsSuccess)
{
Write-Host "ソリューションのリビルドに失敗しました。" -ForegroundColor "Red";
}
【 ダウンロード 】
自作の PowerShell 関数は、以下の記事からまとめてダウンロードできます。
YokoKen.PowerShell.Scripts