タグ別アーカイブ: 新しいクラスを作る

新しいクラスを作る(7) ― PInvoke

概要

New-Class.ps1 を使い、PowerShell から Win32API を呼び出します。

用例

Beep 関数を使って音を鳴らします。

PS> New-Class Class11 {

>>     PInvokeMethod public, static stdcall bool kernel32.dll Beep([UInt32], [UInt32])

>> }

>>

 

PS> [Class11]::Beep(262, 500)

方法

PInvokeMethod は次のように指定します。

PInvokeMethod <属性> <呼び出し規約> <戻り値の型> <ライブラリ名> <メソッド名> <パラメータ型リスト> <キャラクタセット> <エントリ名>

<属性> には、static を含める必要があります。Win32API はオブジェクトインスタンスを受け取らないからです。

<呼び出し規約> は、ほとんどの場合 stdcall で構いません。特に指定のある場合は変更してください。

<ライブラリ名> には、その API 関数が収められているダイナミックリンクライブラリを指定します。

<メソッド名> は、通常、その API 関数と同じものを使ってください。もし別の名前を使いたい場合には、<エントリ名> に API 関数の名前を指定してください。

<キャラクタセット> は、auto, ansi, unicode から選択してください。デフォルトは auto です。


新しいクラスを作る(6) ― インターフェース

概要

New-Class.ps1 でインターフェースを実装する方法を解説します。

用例

IDisposable インターフェースを実装するクラス Class10 を作成します。

PS> New-Class Class10 -Interface([IDisposable]) {
>>     Method public, virtual void Dispose @() {
>>         Write-Host 'Now disposed.'
>>     }
>> }
>>

NameSpace Name BaseType
——— —- ——–
  Class10 System.Object

PS> $o = New-Object Class10
PS> $o.Dispose()
Now disposed.

方法

用例のように、「-Interface」パラメータに実装するインターフェースの配列を渡し、必要なメソッドを実装します。メソッドは「public, virtual」でなくてはなりません。


新しいクラスを作る(5) ― メソッド

概要

次のスクリプトにより、PowerShell で使える新しいクラスを作ることができます。詳しい作り方を 新しいクラスを作る(1) より少しずつ解説していますので、順にご覧ください。今回は、メソッドの作り方を解説します。

ダウンロード

メソッドの定義

メソッドは次のように定義します。

Method <属性> <戻り値の型> <メソッド名> <パラメータ型リスト> <実装>

<属性> には、 public, private, static のいずれかを指定できます。複数指定する場合には、「public, static」のように、カンマで区切って指定します。

<戻り値の型> には、型名を指定します。<実装> スクリプトブロック内で出力されたオブジェクトは、この型にキャストされ、戻り値となります。

<パラメータ型リスト> は、たとえば「([int], [string])」のように指定します。この場合、int 型の第一引数は $args[1] に代入され、 string 型の第二引数は $args[2] に代入されます。$args[0] には、オブジェクトインスタンスが代入されます。

用例

次の用例では、int 型の引数を二つ受取り、その合計を返すメソッド Plus を持つクラス Class9 を定義します。

PS> New-Class Class9 {
>>     Method public int Plus([int], [int]) {
>>         $args[1] + $args[2]
>>     }
>> }
>>

NameSpace Name BaseType
——— —- ——–
  Class9 System.Object

PS> $o = New-Object Class9
PS> $o.Plus(1234, 567)
1801


新しいクラスを作る(4) ― プロパティ

プロパティの定義

プロパティは、次のように定義します。

Property <型> <名前> <実装>

<型>は、Type で、<名前> は、string、<実装>は ScriptBlock です。<実装> の中には Get-Value と Set-Value を定義します。$args[0] には、オブジェクトのインスタンスが格納されており、Set-Value 中の $args[1] には、プロパティに設定すべき値が格納されています。次の用例をご覧ください。

PS> New-Class Class7 {
>>     Field private string FMessage
>>     Property string Message {
>>         Get-Value {
>>             Get-PrivateField $args[0] FMessage
>>         }
>>         Set-Value {
>>             Set-PrivateField $args[0] FMessage $args[1]
>>         }
>>     }
>> }
>>
NameSpace Name BaseType
——— —- ——–
  Class7 System.Object
 
PS> $o = New-Object Class7
PS> $o.Message = 'Hello World!'
PS> $o.Message
Hello World!

Set-Value が無い場合、プロパティは読み取り専用になります。

PS> New-Class Class8 {
>>     Property string Message {
>>         Get-Value { '読み取り専用!' }
>>     }
>> }
>>
NameSpace Name BaseType
——— —- ——–
  Class8 System.Object
PS> $o = New-Object Class8
PS> $o.Message = 'Hello World!'
"Message" は ReadOnly のプロパティです。 発生場所 行:1 文字:4 + $o.M <<<< essage = ‘Hello World!’
PS> $o.Message
読み取り専用!

ダウンロード

上記用例を実行するには、以下のスクリプトが必要です。


プライベートメンバにアクセスする

概要

新しいクラスを作る(3) ― コンストラクタにて、プライベートフィールドへのアクセスが冗長になることを書きました。色々と考えたのですが、他の場面で使うこともあるかもしれないと思い、外部スクリプトにすることにしました。

ダウンロード

用例

$o に「FMessage」というプライベートフィールドを持つオブジェクトが入っている時、Get-PrivateField および Set-PrivateField は、次のように使います。

PS> Set-PrivateField $o 'FMessage' 'Hello World!'
PS> Get-PrivateField $o 'FMessage'
Hello World!

また、$o が、「Add」という int 型の引数を 2 つ取るメソッドを持っている時、Invoke-PrivateMethod は、次のように使います。

PS> Invoke-PrivateMethod $o 'Add' (3, 5)
8

これらは、静的フィールドおよび静的メソッドに対しても使うことができます。


新しいクラスを作る(3) ― コンストラクタ

コンストラクタの定義

コンストラクタは、次のようなコマンドで定義します。 Constructor <パラメータリスト> <実装> <パラメータリスト>は、Type 型の配列で、<実装>はスクリプトブロックです。実際に作ってみましょう。

PS> New-Class Class3 {
>>     Constructor ([string]) {
>>         $args[0].Message = $args[1]
>>     }
>>     Field public string Message
>> }
>>
NameSpace Name BaseType
———– —- ———-
  Class3 System.Object

Class3 は、string 型のパブリックフィールド「Message」を持ち、そのコンストラクタは、string 型の引数を受け取って、それを Message に代入します。ここで、$args[0] には、このクラスによって作られるオブジェクトのインスタンスが代入されており、$args[1..] には、コンストラクタに渡される引数が代入されています。今回の場合、引数は一つだけですので、$args.Length は 2 です。 試してみましょう。

PS> $o = New-Object Class3('Hello World!')
PS> $o.Message
Hello World!

ちゃんと Message に「Hello World!」が代入されています。

Private なフィールドへのアクセス

New-Class.ps1 は、PowerShell でクラスを記述しやすくするために、コンストラクタやメソッドなどをスクリプトブロックで実装する仕様になっています。 すると、ここに一つ問題点が出てきます。それは、PowerShell のスクリプトブロックというのは、あくまでオブジェクトから見れば「よそ者」であるということです。したがって、Private なメンバにシームレスにアクセスすることができません。例えば、次のようにコンストラクタ中で Private なフィールドへ値を代入しようとすると、エラーが発生します。

PS> New-Class Class4 {
>>     Constructor ([string]) {
>>         $args[0].Message = $args[1]
>>     }
>>     Field private string Message
>> }
>>
NameSpace Name BaseType
——— —- ——–
  Class4 System.Object
PS> $o = New-Object Class4('Hello World!')
New-Object : "1" 個の引数を指定して ".ctor" を呼び出し中に例外が発生しました: "このオブジェクトにプロパティ ‘Message’ が見つかりません。プロパティが存在し、設定可能なことを確認してください。" 発生場所 行:1 文字:16 + $o = New-Object <<<< Class4(‘Hello World!’)

そのため、次のような冗長な書き方をしなくてはなりません。

PS> New-Class Class5 {
>>     Constructor ([string]) {
>>         $args[0].GetType().GetField('Message', ('NonPublic', 'Instance')).SetValue($args[0], $args[1])
>>     }
>>     Field private string Message
>> }
>>
NameSpace Name BaseType
——— —- ——–
  Class5 System.Object
PS> $o = New-Object Class5('Hello World!')
PS> $o.GetType().GetField('Message', ('NonPublic', 'Instance')).GetValue($o)
Hello World!

何かいい方法がないか模索中です。

複数のコンストラクタ

コンストラクタはもちろん複数作ることができます。次の例の Class6 は 3 通りの方法でインスタンス化できます。引数が必要ない時、「Constructor {}」としたくなりますが、「Constructor @() {}」と、空の配列を渡すのを忘れないようにしてください。

PS> $c = New-Class Class6 {
>>     Constructor @() {}
>>     Constructor ([string]) {}
>>     Constructor ([string], [int]) {}
>> }
>>
PS> $o = New-Object $c
PS> $o = New-Object $c('Hello World!')
PS> $o = New-Object $c('Hello World!', 100)

新しいクラスを作る(2) ― フィールド

概要

前回の 新しいクラスを作る(1)でご紹介した New-Class.ps1を使って、フィールドを持つクラスを作ってみます。

フィールドの実装

新しいクラス「Class1」を作成し、string 型のパブリックフィールド「Message」を実装してみましょう。

PS> New-Class Class1 {Field public string Message}
NameSpace Name BaseType
———– —- ———-
  Class1 System.Object

オブジェクトを作って Get-Member に渡してみます。

PS> $o = New-Object Class1
PS> $o | Get-Member
 
TypeName: Class1
 
Name MemberType Definition
—- ———- ———-
Equals Method System.Boolean Equals(Object obj)
GetHashCode Method System.Int32 GetHashCode()
GetType Method System.Type GetType()
ToString Method System.String ToString()
Message Property System.String Message {get;set;}

確かに「Message」というメンバが増えています。「Property」と表示されていますが、これは PowerShell での表示上の問題です。別の方法で確かめてみましょう。

PS> [Class1].GetProperties()
PS> [Class1].GetFields()
System.String Message

GetProperties() では何も表示されませんが、GetFields() で「System.String Message」と表示され、確かにフィールドであることがわかります。 実際に使ってみましょう。

PS> $o.Message = 'Hello World!'
PS> $o.Message
Hello World!

Field コマンドの解説と静的メンバの実装

クラスメンバは、スクリプトブロックの中にコマンドを記述することで定義します。上記では、「Field public string Message」というコマンドを与え、フィールドを実装しました。まず、「Field」というのが、「フィールドを作成する」というコマンドです。続く「public」「string」「Message」が、その引数で、それぞれ「属性」「型」「名前」をあらわします。 属性は、「public」の他に、「private」「static」が使えます。属性を 2 つ以上使いたい場合には、「(‘public’, ‘static’)」のように、文字列の配列にして渡します。 それでは、Message フィールドを静的フィールドにしてみましょう。

PS> New-Class Class2 {Field ('public', 'static') string Message}
NameSpace Name BaseType
———– —- ———-
  Class2 System.Object

試してみます。

PS> [Class2]::Message = 'Hello World!'
PS> [Class2]::Message
Hello World!

これでフィールドが作れるようになりました。


新しいクラスを作る(1)

新しいクラスが作りたい理由

PowerShell を使っていて時々不便に感じるのが、新しいクラスを作れないことです。

たとえば、System.Collections.ArrayList に CSV ファイルを読み込み、任意の列でソートしようと思えば、 IComparer インターフェースを実装するクラスを新しく作らなくてはなりませんが、その手段がありません。.NET Framework は、新しいクラスが簡単に作れることを前提に設計されていたので、それができないと、思わぬところでつまづくことがあります。

一番頻繁に感じるのが、新しいデリゲートクラスが作れないことでしょう。デリゲートはありとあらゆる場所で、便利な、あるいは必須のコールバックとして使われています。

また、クラスが作れないことによって、Win32API を呼び出すような PInvoke メソッドが実装できない困難もあります。

もちろん、逃げ道もあります。コマンドレットを高級言語で実装する方法や、デリゲートならば、「Windows PowerShell で delegate」 のような方法、その他も 「PowerShell で C# の実行」 や、 「メモリリークのない C# の実行」 のように、他の言語をコンパイルして実行することでどうにかなります。

しかし、それでも「新しいクラスが簡単に作れたらなあ」と思うことはしばしばです。「思うならできるようにしよう」ということで作ったのが、次のスクリプトです。

ダウンロード

このスクリプトでできること

  • 新しいクラスの作成
  • コンストラクタ・メソッド・プロパティ・フィールドをスクリプトブロックで実装
  • インターフェースの追加
  • PInvoke メソッドの定義

使用法

New-Class.ps1 の使用法については、これから段階を追って説明していきたいと思います。今回は簡単に、System.Object から何の機能追加もなしに継承したクラスを作ります。 次のように、「New-Class <文字列> <スクリプトブロック>」のように起動し、「文字列」には新しいクラスの名前を、「スクリプトブロック」にはクラスの定義を指定します。今回は機能追加しないので、スクリプトブロックの中身は空です。

PS> New-Class Flamework.Object {}
NameSpace Name BaseType
———– —- ———-
Flamework Object System.Object

新しいクラスができたので、オブジェクトを作ってみましょう。

PS> $o = New-Object Flamework.Object
PS> $o.ToString()
Flamework.Object
PS> $o.GetType() -eq [System.Object]
False

ちゃんとできました。

実装

このスクリプトは、メモリ内に動的アセンブリを定義し、System.Reflection.Emit.TypeBuilder で新しい型を作成しています。

動的アセンブリはメモリリークの原因になると言ったじゃないか!」ですって?確かに、スクリプトを起動するたびに動的アセンブリを作っていたのでは、使わないアセンブリがどんどんたまっていきます。しかし、このスクリプトは、二回目以降の起動では、前回のアセンブリを再利用します。また、一度クラスを作ると、PowerShell が終了するまで同じ名前で作ることはできないので、同じ名前、同じ機能で異なるバージョンのクラスが次々に作られることもありません。

クラスの実装には System.Reflection.Emit.ILGenerator によって得られた IL を使い、各メソッドの内部からスクリプトブロックを起動しています。