⚠️

Object[]への自動型変換にご注意

に公開

久しぶりに PowerShell の型変換にハマりましたので、戒めと備忘録を兼ねて残しておきますね。

普段 PowerShell コードで配列を取り扱う時にその配列が特定の型、例えば Single[] なのか Object[] なのかというのはあまり気にしていないのではないでしょうか? 配列の中身は気にしていても全体は気にしない。それがスクリプト言語のメリットでもあるわけですが、普段気にかけていないこともありハマる時には沼になりました。

条件がトリッキーなので例によって誰得な話ではあります。

こんな場合は要注意

  • クラスで書く
  • 外部ライブラリを使う
  • 外部ライブラリが標準にないデータ型を使う

# やっぱり誰得ですね

背景

一応背景も残しておくと、FFT で音データの解析をしようってことになり、テスト用のコードが動いたので実利用向けにクラス使って書き直すと動かないんですわ。ハマること数日、原因は Object[] への自動型変換だったと(泣)

なぜクラスだとハマりやすいのか

おっさんさんも記事にしていますが PowerShell のクラスはパーサーから挙動が別で、標準にない型の使用は厄介です。

https://zenn.dev/notstrings/articles/b524f7467b8b7f

今回は FFT に MathNet を使うのですが、これが複素数型 Complex32 を定義して使うのでこれが通りません。

error.ps1
Add-Type -AssemblyName パス\MathNet.Numerics.dll

$c = [MathNet.Numerics.Complex32]::New(0.0, 0.0)
Write-Host "$($c.GetType().Name)"

[Test]::New().Run()

class Test {
    Run() {
        $c = [MathNet.Numerics.Complex32]::New(0.0, 0.0)
        Write-Host "[in class] $($c.GetType().Name)"
    }
}
実行結果
>.\error.ps1
Line |
  10 |          $c = [MathNet.Numerics.Complex32]::New(0.0, 0.0)
     |                ~~~~~~~~~~~~~~~~~~~~~~~~~~
     | Unable to find type [MathNet.Numerics.Complex32].

実行前のパーサーのエラーなので Add-Type とか using module とか無力なんすよ。

関数にして外に出す

func.ps1
Add-Type -AssemblyName パス\MathNet.Numerics.dll

function NewComplex($r) { [MathNet.Numerics.Complex32]::New($r, 0.0) }

[Test]::New().Run()

class Test {
    Run() {
        $c = NewComplex(1)
        Write-Host "[in class] $($c.GetType().Name) $c"
    }
}

このような時には Invoke-Expression かクラスの外に出すかが定石ですが、今回は関数にします。

実行結果
> .\func.ps1
[in class] Complex32 (1, 0)

クラス内のコードが実行されるときには Add-Type されているので問題なし、と。マジでパーサー何とかしてほしいのだが。

Object[] の落とし穴

ここでやっと本題の Object[] の話で、FFT には Complex32[] を渡すことになっていてここでハマりました。

例によって、$fftdata = [MathNet.Numerics.Complex32[]]::New($SampleCount) という様なコードはクラス内では使えません。これができれば問題はなかったのですがね。

ともあれできないので、先ほどの関数を使うわけですが

実行結果
> $fftdata = 1 .. 10 |%{ NewComplex(0) }
> $fftdata.GetType().Name
Object[]

と、ここで Object[] への自動型変換ですよ。

これの厄介なところは、実行してもエラーにならず、この変数にデータを設定して FFT してもエラーにならないことです。単に結果がおかしくなるだけ。

渡しているデータがおかしくなっているのかと Export-Csv して確認しても問題なし。そりゃそうですよね PowerShell 側でよろしくやってくれてますから。

今回は結果がめちゃくちゃになるので気づけたのですが、微妙に違うとかだと気づきもしないかもしれません。

この型変換は標準の型でも同じで、

実行結果
> $a = 0.1
> $a.GetType().Name
Double
> $a = 1 .. 10 |%{ 0.1 }
> $a.GetType().Name
Object[]

まぁ普通にコーディングしている分には Object[] で困りませんけどね。冒頭でも言ってますが、Object[] なのか <T>[] なのかとか気にもしませんし。いろいろな型が混じることもあるわけで便利ですしね。

Object[] への型変換はクラスとは関係ないので関数を使って外だしにしても解決できません。

実行結果
> function NewComplexArray($n) { [MathNet.Numerics.Complex32[]]::New($n) }
> $ca = NewComplexArray 10
> $ca.GetType().Name
Object[]

力ずくで解決

グローバル変数を使ってもよかったのですが、今回はデータだけを保持するクラスを作っていたので、力技で設定します。

余談:変数の関数スコープがかったるい

ちょっと複雑な処理をしようとすると、関数に小分けすることになりますが、ローカル変数はありがたいものの、関数間でのデータのやり取りはかったるい場合が多いです。配列をやり取りするときはトラぶりやすいですし。クラスで書くとプロパティ(いわゆるインスタンス変数)として共有できるのもクラスを使う理由になるくらい。

関数で書く(書かざるを得ない)場合でも、クラスとして必要な変数を纏めておくと便利だったりします。

class CommonVars {
    $Var1;
}

$CV = [CommonVars]@{}

関数内からも同名のローカル変数がなければ $global:$script: なしで参照できるので、

function setVar1($n) {
    $CV.Var1 = $n
}

というように、本来ならいちいちスコープ指定しないとダメな値を書き込むケースも OK。関数内での暗黙のグローバル変数参照は多様すると毒で、ついうっかり変数の書き込みもしてローカル変数を生成してしまいバグになったりしますが、この方式だとそれもないのもメリット。

自堕落すぎる? まぁ否定はしない。

代入する間にいろいろ挟むとややこしいということで、直接関数内で変数を設定します。

function InitComplexArray($obj, $n) {
    $obj.FFTData = [MathNet.Numerics.Complex32[]]::New($n)
}

class Dataset {
    $SampleCount;
    $FFTData;
}
実行結果
> $v = [DataSet]@{}
> InitComplexArray $v 100
> $v.FFTData.GetType().Name
Complex32[]

見苦しいことこの上もなし。でも背に腹ですわ。

他の方法

蛇足ですが他の方法も一応残しておきます。メモ用。

[Ref]

関数の中で特定プロパティに書き込むというのもアレなので、[ref] で書き換えたほうがよかったかもしれません。好きではないのでやりませんが。

Invoke-Expression お前もか

困った時の助っ人 Invoke-Expression も型変換してくれます。

> $v = Invoke-Expression "[MathNet.Numerics.Complex32[]]::New(10)"
> $v.GetType().Name
Object[]

やってできなくはない。

> Invoke-Expression "`$v = [MathNet.Numerics.Complex32[]]::New(10)"
> $v.GetType().Name
Complex32[]

Discussion