tech.guitarrapc.cóm

Technical updates

PowerShell で オブジェクトの数を計測するんです?

オブジェクトの数を数える時によく使われるものといえば?

Measure-Objectでしょうか? Measure-Objectでしょうか?

今回はさらっとちょっとだけ見てみましょう。

個数のカウント

よく使うのはどれでしょうか?

  • .Lengthプロパティ?
  • .Countプロパティ?
  • `Measure-Object`` コマンドレット?
  • System.Linq.EnumerableSystem.Linq.Enumerableメソッド?

.Lengthプロパティは .NET通りなのでいいでしょう。

ここではPowerShellで奇妙に感じる.CountプロパティとCmdletである.Countを見てみます。

.Countプロパティって気持ち悪いけど便利

PowerShellのオブジェクトは、そこかしこで .Countプロパティを使って個数を数えることができます。*1 image

そう、たとえ配列でなくてもです。

気になるのが文字列System.Stringに対してですが、「インテリセンス上はLengthしかでず」これは文字長を取得します。*2

image

しかし、インテリセンスになくとも.Countが使える不思議。これは.Countであれば、必ず1になります。空文字.Countであってもです。

https://gist.github.com/guitarrapc/1e3498cec68800cd12e4

まぁ、便利は便利です。ICollectionインタフェースの.Countプロパティ同様に取得コストがほぼないので。

しかし、配列でもはいのに要素数が.Countで取れるのは気持ち悪いことこの上ない。

.Countプロパティの利用は罠がある

PowerShell 6.1からPSCustomObjectのCountプロパティが機能するようになっています。

https://blog.shibata.tech/entry/2018/10/07/172334

以下の内容はPowerShell 6.0以下のバージョンにおいて該当します。

大体のオブジェクトに対して.Countで個数が取れるのですが、数少ない例外が.Countです。

この型に対しては、.Countプロパティでの個数取得できません。

image

となった時に、.Lengthプロパティが使えるかというと同様にnullが返ってきます。

これを回避するには、いったん配列@()で包んでしまいます。

https://gist.github.com/guitarrapc/c75b35bb51747f2aac44

Measure-Object を使って個数を数える

ここでようやく、Measure-Object Cmdletが出てきます。

プロパティを使って取得できるなら、可能な限りそっちを使うのがふつーなのですが、取れないなら数えるしかないと。

よくあるパターンでPSCustomObject型を生成して個数を数えるとこうでしょうか?

https://gist.github.com/guitarrapc/daaf304d5aa8e2773aa0

Measure-Object の罠

正確には、パイプライン|の罠です。

PowerShellのパイプラインは、基本的にはオブジェクトを1つずつ渡します。*3

ということは? パイプラインに1000000オブジェクト渡すと、1000000オブジェクト渡しきるのを待たないといけないということです。これはひどい。

そのため、以下のように書いた日には18秒ぐらいぽけ~(())と待つことになります。やめましょう。

(1000000..1 | measure).Count

個数カウントの方法とベンチマーク

楽をしたいので、C# で良くやるようにPowerShellにも持ち込みます。

PowerShellの長所でもあり短所であるパイプラインです。コストがパイプラインにかかっているので、避ければいいのです。

ということで、System.Linq.Enumerableを使えばいいでしょう。それぞれのベンチをとってみます。

https://gist.github.com/guitarrapc/acd1b4a5a1a1f3d7fdbe

方法 1回目 2 回目 3回目 平均(ms)
.Count 261.2029 155.8287 145.4331 187.488
Measure-Object 19268.5511 19558.6135 19934.1167 19587.093
Linq.Enumerable.Count() 405.0513 357.8473 389.6365 384.18

当然の結果ですが、想定通りですね。

Min / Max の取得

この件がもっと影響を及ぼすのが、Min / Maxです。

Measure-Objectはとても便利なCmdletで、Min/Max/Average/Countをプロパティを指定して計測できます。

しかし、パイプライン越しの利用が主体になるため、オブジェクト個数に応じて時間がかかります。(1-1000程度ならmsで完了するのでいいのですが)

そういう意味では、System.Linq.Enumerableは、PowerShellからも使いやすいので是非使えばいいでしょう。

https://gist.github.com/guitarrapc/4f6146d48adef782cf3a

Min

方法 1回目 2 回目 3回目 平均(ms)
Measure-Object 22628.2811 22705.8116 23010.3391 22781.477
Linq.Enumerable.Min() 451.5155 431.6266 566.3141 483.152

Max

方法 1回目 2 回目 3回目 平均(ms)
Measure-Object 23102.14 22624.6367 22535.7175 22754.164
Linq.Enumerable.Max() 460.4299 548.7585 390.1181 466.435

Average

方法 1回目 2 回目 3回目 平均(ms)
Measure-Object 20982.5092 21166.3688 20576.0699 20908.315
Linq.Enumerable.Agerage() 382.4254 370.6433 360.0512 371.03

まとめ

.Countプロパティが使える時は素直に使いましょう。

Measure-Objectは量が十分抑えられる時には便利ですね。

Linq.Enumerableくぁわいい。

*1:実態は .Lengthプロパティです

*2:この場合は4が取得できます

*3:より正確にはひと塊ずつです。デフォルト1塊り、1オブジェクトなので一見1つずつになります。

PowerShell のString評価の方法と罠

以前Gistにまとめていたんですが、記事にするのを忘れていたのです。

PowerShellは、文字列の中に変数を埋め込む時にいくつかの手法があります。でもその評価の違いって以外と知られてないようなので、まとめておきましょう。

5つの方法

次に示す変数をString中に表示することを考えましょう。

https://gist.github.com/guitarrapc/3d1af4a26d023bb5c005

大きく5つの方法があります。

https://gist.github.com/guitarrapc/d26c72a9b9980589a09f

https://gist.github.com/guitarrapc/cfff0c9fc907e95dd6d8

https://gist.github.com/guitarrapc/77c1b0dc0ab0564c46d9

https://gist.github.com/guitarrapc/791da80f2ef99466dbd0

https://gist.github.com/guitarrapc/f4917aa0dbad9de8da32

このうち、私が良く使うのは、4のインデックス指定か、3の部分式です。5に関しては4と同じなので、普段は使いません。

インデックス指定のメリット

なぜインデックスを多用するかというと、モジュールやリソースを普段書くため、変更に強くするためです。

  1. 繰り返し利用時に何度も書かなくていい => "{0}-{1}-{0}" -f $hoge.hoge, $fuga
  2. 変数の変更に強い => 途中で当てる変数を変更したり、順序の変更がインデックス指定なので容易
  3. シングルクォート''の中にも変数を渡せる

過去、部分式を多用していた時期があったのですが、変数の変更やポジションの変更が面倒すぎてやめました。

インデックス指定の注意

さて、4のインデックス指定には注意があります。

このように{}で括っても評価できます。

https://gist.github.com/guitarrapc/85288a1450a15e1d7bb5

問題は、{}の間に改行が挟まるとパースに失敗します。

どんなシーンかというと、json中への埋め込みが苦手といえます。

https://gist.github.com/guitarrapc/1d903d0b7913569adbb3

対応方法

以下の2つがあり、私はほとんどの場合部分式を持ちいて回避しています。というか、部分式を用いるのはHere-Stringへの埋め込みとこのパターンぐらいです。

  1. 直を使う (但し、後ろの文字が変数の一部として評価される可能性が高いのでだめだめ)
  2. $() 部分式を使う

こういう意味では、部分式はほぼ影響を受けず使えるので強力な手段といえます。ただし、シングルクォート'への埋め込みができないのでインデックスを利用するなりしましょう。

まとめ

  • 機能としては部分式
  • だが、スクリプトなど(変更が発生しえる場合)にはインデックス指定が楽
  • ワンライナーなど単純な時は直もあり

Gistはこれです。