tech.guitarrapc.cóm

Technical updates

そろそろ PowerShell の一次配列の罠と回避について一言いっておくか

タイトルは一度いってみたかっただけです、生意気言ってごめんなさい。

他の言語同様、PowerShell にも一次配列があります。こんなやつ。

gist.github.com

PowerShell は、型を持っているので Object[] 以外にも T[] (型の配列) などもあるのですが、他言語から見ると配列の扱いに癖があるように思います。まとまった記事にしたことなかったので、癖(挙動を知らなければ罠に思える)についてまとめます。

目次

TL;DR

PowerShell の一時配列は、

  • 左辺に右辺が合わせられる「暗黙の型変換」
  • @() を使うとTの一時配列がObjectに型変換される
  • += は要素が多くなると遅くなるので List[T] ã‚„ArrayListを使ってるなら .Add() を使いましょう
  • @(1,2,3) -eq 1などオペレーター(-eq)の配列に対する「フィルタリング」を防止するにはヨーダ記法1 -eq @(1,2,3)を使いましょう
  • PowerShell がはやしてるプロパティ (.Count) には注意
  • ジャグ配列内での配列維持には単項の, オペレーターを使いましょう

何がこまるの

配列と配列の比較をしよう思ったら違った!とかいうのはよくあるんじゃないでしょうか。

配列は特によく扱うデータ型ですが、その挙動が直感とずれることが多いと「この言語なに」となるでしょう。

この記事は認識にあるずれを明確にすることで、PowerShellを思ったように扱うことを目的にしています。

罠となるポイント

経験上、PowerShell で一次配列で困るっていうのはどれか*1に当てはまってきました。*2

「一言いう」とこうですね。

はまりポイント 一言 凶悪度*3
暗黙の型変換 仕方ない 3/10
簡略化された配列宣言 かき捨て以外は型に縛ってやる(やりすぎツライ 4/10
要素の連結がオペレータによっては遅い 仕様が古いんです。メソッド使って 1/10
標準出力での配列型の要素が単体な場合の自動的な型変換 知っててもほぼ回避できないから最悪ねっ! 10/10
オペレータの配列と単体での挙動の違い 初見で期待する動きじゃない、生まれ変わってこい 8/10
PowerShell がはやしてるプロパティ もはや生やしてるプロパティ使いたくない 6/10
ジャグ配列内での配列維持に,が必要 罠としか言えない 9/10

はじめにいっておきます。PowerShell は型を持っているといいますが、基本 Object | Object[] にしたがります。これを覚えておいてください。*4

順番にいきます。

暗黙の型変換

これは一次配列に限りません。が、どうも混同されているケースが多いようです。それぐらい厄介なわけですね。

PowerShell は、動的型付け言語なので実行時に型が決定されます。この暗黙の型変換には原則があるのですが、これを知らないと型で何かしらの操作をしようと期待した時に、期待と異なる型になってあわわわ。とか。

  • .GetType() メソッドでの型での判別が予期しない結果になる
  • function (関数) で型指定で受けようとしても意図した型でないものが渡ってきてエラーになる

暗黙の型変換のルール

これだけ覚えておいてください。

実行時にオペレータの左辺(左オペランド)と右辺(右オペランド)の型が違う場合、「左辺の型に右辺の型が変化」します。

変数の型は、.GetType() メソッドで調べられるので使っていきましょう。

いくつか例を見てみます。

シンプルな型変換例

まずは簡単な例です。

gist.github.com

一番下の$hoge の型を考えます。

この例では、左辺に System.String 型、右辺に System.Int32 型 の変数をAppend しています。そのため、ルール通り $hoge は System.String型 となります。

変数$b が System.Int32 型 (1) から System.String 型 ("1")に暗黙に型変換されたのですね。

暗黙の型変換の失敗例

先ほどの左辺と右辺を逆にしてみましょう。すると暗黙の型変換に失敗します。

gist.github.com

先ほどと同様に、左辺の System.Int32 型 に System.String 型 を暗黙に型変換しようとしたのですね。

int -> string は暗黙に型変換可能ですが、string -> int はできないのでエラーとなった訳です。

一次配列の型変換

さて、System.Int32 型を配列に入れたいと思って、@() で括ったらどんな型になるでしょうか?

gist.github.com

配列の中身は System.Int32 型ままですが、配列全体は System.Int32[] ではなく、System.Object[] となります。おそらく期待する型は System.Int32[] だと思うので違和感があるのではないでしょうか。

実際のところ、@() は、[object[]]() と同義です。知っておけば、そんなものか。ですが、シラナイと好ましくないと感じるでしょう。

gist.github.com

型をゆるふわにやって事故って、型をきっちりしようとしたらとても使いにくい。特に一次配列はなんでも Object[] にしたがるので扱いにくさが目についてしまいます。

回避策

左辺に合わせて右辺の型は暗黙に変換される。これを覚えておきましょう。インテリセンスでこういうの指摘してほしいですよね。

ScriptAnalyzer での静的な解析わんちゃんですね。

github.com

PowerShell Tools for Visual Studio は... ほげ。現状打つ手なしなのでPR投げてください。現在は Microsoft も開発に参加して実装速度が上がってます。

github.com

簡略化された配列宣言

次は 配列宣言です。

PowerShell で配列を宣言する方法はいくつかあります。

よくある簡略な方法

一番多く表現される方法は先ほどの、@() でしょう。

gist.github.com

先ほど説明した通り、System.Object[] になるので注意です。

明示的な宣言

他言語では、C# でいう var のような型推論を使っていてもインテリセンスがあるので困りません。しかし、PoweShell や PowerShell ISE のインテリセンスは型サポートが貧弱なのでさもすると型を把握できなくなります。さっきの例が System.Object[] になるのはその例です。

こういうときは、型を明示しておくと安心できるでしょう。(このあと説明する要素が単数の場合を除いては)

gist.github.com

単数を一次配列にする

これがはまりどころでもあり、回避策です。

gist.github.com

1 は System.Int32 型ですが、@(1) は System.Object[]型です。そう、簡略な方法でしめした@() で出力を囲むと配列扱いになります。

これ、この後も触れるのですが結構えげつないですが、効果のあるやり方です。PoweShell の覚えたくないけど便利なやり方 頻出トップ5に入りそうなぐらい。

要素の連結がオペレータによっては遅い

配列に要素を追加するときにどうしていますか?

もしかして、+= を使ってませんか?要素が増えたときに死にますよ、それ。

回避策

ジェネリクスの List[T] を使ってるなら .Add メソッドをつかってください。廃棄物のArrayListでも同様です。

詳しくは以前まとめたのでどうぞ。

tech.guitarrapc.com

標準出力での配列型の要素が単体な場合の自動的な型変換

私が思う PowerShell の配列の罠 で断然トップはこれです。えげつなさヤバイ。

gist.github.com

この挙動地雷以外のなんて呼べばいいんですかね。一次配列は結果が単数の場合、配列ではなくなってしまいます。*5

回避策

ここで、先ほどの単数を配列にする という @()が現れます。*6

gist.github.com

はい。

オペレータの配列と単体での挙動の違い

ここでいうオペレータは特に-eq | -ceq 演算子を指します。

このオペレータ、比較判定していると思いますよね?

gist.github.com

配列にやってみましょう。

配列 1,2,3,4,5,6,7,8,9,10 は、1とは違うので期待する動作は false が返ってくることですが....?

gist.github.com

知らない人にはまさかの 1 が返ってきます。

これは、-eq オペレータが配列に対してはフィルタオペレータだからです。

そのため、左辺の 1..10 の中に 1 が含まれていたので、該当した 1がとりだされました。

これ、if文の中でやっていると、 bool に対して 1 は暗黙に true に変換されるのでまさかの逆に結果になります。

gist.github.com

では、右辺が配列の左辺に含まれないものだった場合は?

gist.github.com

結果は空です。しかしこの結果がまた厄介です。

gist.github.com

なんとか判定したくても、さて。

gist.github.com

細かい解説ははぐれメタルセンセーを。

winscript.jp

null に関しては、上記ブログだけでは気づきにくいパターンもあります。例えばこれ、結果に気づけましたか?

gist.github.com

回避策

原則ルールを思いだしてください。左辺に暗黙の型変換されるのです。そして配列に対してでなければ、-eq は比較判定に使えます。

判定したい値を左辺に持ってくるのが回避策になります。

gist.github.com

null も左辺に持ってきましょう。

gist.github.com

PowerShell がはやしてるプロパティ

PowerShell は、.NET 標準の型に独自のプロパティやメソッドを生やしています。*7

gist.github.com

CodeProperty、ScriptProperty、NoteProperty、AliasProperty などとあったら.NET 標準ではなく PowerShell が生やしているプロパティです。

   TypeName: System.Int32

Name        MemberType   Definition                                                                                          
----        ----------   ----------                                                                                          
pstypenames CodeProperty System.Collections.ObjectModel.Collection`1[[System.String, mscorlib, Version=4.0.0.0, Culture=ne...
psadapted   MemberSet    psadapted {CompareTo, Equals, GetHashCode, ToString, GetTypeCode, GetType, ToBoolean, ToChar, ToS...
psbase      MemberSet    psbase {CompareTo, Equals, GetHashCode, ToString, GetTypeCode, GetType, ToBoolean, ToChar, ToSByt...
psextended  MemberSet    psextended {}                                                                                       
psobject    MemberSet    psobject {BaseObject, Members, Properties, Methods, ImmediateBaseObject, TypeNames, get_BaseObjec...
CompareTo   Method       int CompareTo(System.Object value), int CompareTo(int value), int IComparable.CompareTo(System.Ob...
Equals      Method       bool Equals(System.Object obj), bool Equals(int obj), bool IEquatable[int].Equals(int other)        
GetHashCode Method       int GetHashCode()                                                                                   
GetType     Method       type GetType()                                                                                      
GetTypeCode Method       System.TypeCode GetTypeCode(), System.TypeCode IConvertible.GetTypeCode()                           
ToBoolean   Method       bool IConvertible.ToBoolean(System.IFormatProvider provider)                                        
ToByte      Method       byte IConvertible.ToByte(System.IFormatProvider provider)                                           
ToChar      Method       char IConvertible.ToChar(System.IFormatProvider provider)                                           
ToDateTime  Method       datetime IConvertible.ToDateTime(System.IFormatProvider provider)                                   
ToDecimal   Method       decimal IConvertible.ToDecimal(System.IFormatProvider provider)                                     
ToDouble    Method       double IConvertible.ToDouble(System.IFormatProvider provider)                                       
ToInt16     Method       int16 IConvertible.ToInt16(System.IFormatProvider provider)                                         
ToInt32     Method       int IConvertible.ToInt32(System.IFormatProvider provider)                                           
ToInt64     Method       long IConvertible.ToInt64(System.IFormatProvider provider)                                          
ToSByte     Method       sbyte IConvertible.ToSByte(System.IFormatProvider provider)                                         
ToSingle    Method       float IConvertible.ToSingle(System.IFormatProvider provider)                                        
ToString    Method       string ToString(), string ToString(string format), string ToString(System.IFormatProvider provide...
ToType      Method       System.Object IConvertible.ToType(type conversionType, System.IFormatProvider provider)             
ToUInt16    Method       uint16 IConvertible.ToUInt16(System.IFormatProvider provider)                                       
ToUInt32    Method       uint32 IConvertible.ToUInt32(System.IFormatProvider provider)                                       
ToUInt64    Method       uint64 IConvertible.ToUInt64(System.IFormatProvider provider)                                       

特にGet-Process などはわかりやすいでしょう。試してみてください。

gist.github.com

細かく知りたい人へ

id:aetos382 さんの神まとめを見ればなんとなく理解が深まるでしょう。

tech.blog.aerie.jp

罠になりやすい例

PowerShell で、配列の長さを取るときに、良く用いられるのが 謎の.Count プロパティです。

でもこれ、Length プロパティの AliasProperty として PowerShell が配列にのみ設定しているんですね。

そのため、配列じゃない型には存在しません。

ここで、標準出力での配列型の要素が単体な場合の自動的な型変換 を思い出してください。そう、結果を @() で囲んで配列に強制変換する謎手法を使わないと、.Count が取れない!という状況になったりするんですね。*8

回避策

私あまり @() 好きじゃないので、結果が少ないとわかっているなら Measure-Object で数えることが多いです。

.Length プロパティアクセスと比べてコストが高いので嫌なんですが、数が少なければ誤差なので。

あるい型をしっかり配列か確認して、Lengthプロパティを使うといいでしょう。

ジャグ配列内での配列維持に,が必要

ジャグ配列は、その配列要素も配列である配列です。つまり@(@(1,2,3),@(4,5,6)) のようなものです。この例のジャグ配列要素へのインデックスアクセスはイメージ通りにできます。

gist.github.com

@(@()) は @() というまさかの罠

しかし、PowerShell は、@(@(<配列>)) は@(<配列>)とみなします。つまりこうなっちゃいます。

gist.github.com

解決策

そこで利用するのが、単項配列演算子の,です。,を対象配列の前に置くことでジャグ配列でも配列を維持します。

gist.github.com

厄介ですね!

まとめ

どれも知っていれば回避はできます。そういう問題ではない?そうですか。

まぁ気軽に2,3 行書いたり、シェル上でCmdlet を1つ実行するだけのシーンが多いと楽なんでしょうねぇ。*9

なんでこの記事書いたの

罠が多いという声がおおいので書けという天の声が聞こえました。

*1:あるいは複数

*2:他にもあったら教えてください

*3:個人の感想です

*4:なーにが型じゃ状態

*5:何をいってるんだ

*6:涙しかない

*7:ひっ

*8:PowreShell 3.0 以降は単体でも1が取れますが、Set-StrictMode -Latest するとエラーになり意図しない挙動となる可能性があります

*9:だが10000行とか書くと型を厳密に縛らないと死ぬ