📝

動的にクラス名を指定してインスタンス生成したい

2025/02/04に公開

ベースクラスから派生クラスを作って処理の細かい違いを派生クラス側で実装するということはよくやることですが、その場合に動的に派生クラス名を指定してインスタンス生成したくなることがあります。(あるよね?)

class BaseClass {
    $No;
}

class C1 : BaseClass {
}

class C2 : BaseClass {
}

というようなクラス定義がある時に、C1 か C2 のどちらかのインスタンスを生成したいわけです。

今まではクラス名のチェックも兼ねて switch でお茶を濁していたりしたのですが、ようやく重い腰をあげて調べたのでメモ。

結論

-as 一択。もうこれだけ。

code> $classname = 'C1'
code> @{} -as $classname

No
--


code> @{No=1} -as $classname

No
--
 1

-as とか思いつきもしなかったっすわ。

他の方法

New-Object

New-Object も名前(String)で行ける。

code> New-Object -TypeName $classname

No
--


code> New-Object -TypeName $classname -ArgumentList @{No=1}
New-Object: Cannot find an overload for "C1" and the argument count: "1".

初期化するならコンストラクターが必要、と。コンストラクターを書くのであればまぁいいですが、New-Object 自体の記述が圧倒的に長いので出番無いね。

New()

クラス名(string)ではなく、クラス型情報が必要なので下準備が要ります。初期化するならコンストラクターもな。

$ClassTable @{
    'C1' = [C1];
    'C2' = [C2];
}

code> $ClassTable.$classname::New()

No
--


code> $ClassTable.$classname::New(@{No=1})
MethodException: Cannot find an overload for "new" and the argument count: "1".

他に方法がないならこれで決まりなんだけど(短いし)、-as があるので出番なし。

Invoke-Expression

できなくはない。

code> $classname='C1'
code> Invoke-Expression "[$classname]::new()"

No
--


code> Invoke-Expression "[$classname]@{}"

No
--


code> Invoke-Expression "[$classname]@{No=2}"

No
--
 2

ここまでは良いんだけど、初期化したい時って値は別の変数に入ってたりするわけで。

code> $v = @{No=2}
code> Invoke-Expression "[$classname]$v"
Invoke-Expression: Unexpected token 'System.Collections.Hashtable' in expression or statement.
code> Invoke-Expression "[$classname]::new($v)"
Invoke-Expression: Missing ')' in method call.

もちろん変数v を展開すればできますが。

code> Invoke-Expression "[$classname]@{$($v.Keys |% {""$_ = $($v.$_)""})}"

No
--
 2

誰もやらんだろ。

-as 注意点

便利な -as ですが、余分な初期化データがあるとエラーではなく $null になるので要注意

code> @{No=1; 余計なもの=1} -as $classname
code>

必要なデータだけ使って初期化する例のコンストラクター置いておけば OK。

class BaseClassC {
    $No;
    BaseClassC() {}
    BaseClassC($o) {
        $oProp = $o -is [hashtable] ? $o.Keys : ($o |Get-Member -MemberType NoteProperty,Property| %{ $_.Name} )
        $this.GetType().GetProperties().Name |? { $_ -in $oProp } | %{ $this.$_ = $o.$_ }
    }

    [string] ToString() { return "[$($this.Gettype().Name)]{No=$($this.No)}"}
}

class C1 : BaseClassC {
    C1() : base() {}
    C1($o):  base($o) {}
}

code> $classname='C1'
coode> @{No=1; 余計なもの=1} -as $classname

No
--
 1

もちろんクラス名のチェックはした方が良いので、以下も忘れずに。

$classnames = 'C1,C2,のように派生クラスの名前のカンマ区切り' -split(',')
if ($classname -notin $classnames) { エラー処理 }

普段使いにする?

普段使い、つまり静的にクラス指定してインスタンス生成する時も -as でやるかというと、これはやらないと思います。

まず、 キャスト[クラス名]@{初期化パラメータ} が十分お手軽でわかりやすさとしても問題ないこと。

逆にキャストでは返ってこない $null が -as ではあり得るというのは厄介だと思うからです。例えば初期化パラメータに間違いがある時、キャストした場合には、

InvalidArgument: Cannot create object of type "C1". The property '余計' was not found for the 'C0' object. The settable properties are: [No <System.Object>], [Id <System.Object>].

ですが、-as で $null が返ってくればみんな大好きヌルポなので、その後のプロパティのアクセスでエラーですわ。

InvalidOperation: You cannot call a method on a null-valued expression.

例のコンストラクターが書いてあれば $null が返ってくることはないのですが、クラスを使う側からしたらそこまで依存できない(したくない)し、-as だから $nullチェックしなきゃって思うのが嫌。

動的にクラス名指定するときはやむなしとしても、キャスト [クラス名]@{初期化パラメータ} でやれば済むところに潜在的なトラブル要因を入れ込む必要もないですしね。

Discussion