以下の内容はhttps://tech.guitarrapc.com/entry/2015/09/05/012733より取得しました。


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

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

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

https://gist.github.com/guitarrapc/5bbdeaaab8b8eb232193

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

概要

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()メソッドで調べられるので使っていきましょう。

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

シンプルな型変換例

まずは簡単な例です。

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

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

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

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

暗黙の型変換の失敗例

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

https://gist.github.com/guitarrapc/6028d04981a63a3517bf

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

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

一次配列の型変換

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

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

配列の中身はSystem.Int32型ままですが、配列全体はSystem.Int32[]ではなく、System.Int32[]となります。期待する型はSystem.Int32[]なので違和感があります。

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

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

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

回避策

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

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

https://github.com/PowerShell/PSScriptAnalyzer

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

https://github.com/Microsoft/poshtools

簡略化された配列宣言

次は配列宣言です。

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

よくある簡略な方法

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

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

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

明示的な宣言

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

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

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

単数を一次配列にする

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

https://gist.github.com/guitarrapc/5bd9fdbc2f05754160ec

1System.Int32型ですが、@(1)System.Object[]型です。@(1)のように出力を囲むと配列扱いになります。

@()で配列とするのは効果的ですが、型的には困ったものです。

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

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

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

回避策

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

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

https://tech.guitarrapc.com/entry/2013/09/22/100203

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

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

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

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

回避策

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

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

はい。

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

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

このオペレータ、比較判定しているつもりが、実はフィルタリングをしています。

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

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

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

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

これは、-eqオペレータが配列に対してはフィルタオペレータだからです。そのため、左辺の1..10の中に1が含まれていたので、該当した1がとりだされました。

if文の中でやっていると、 フィルタ結果の1がが[bool]1となってtrueと判定されます。

https://gist.github.com/guitarrapc/10546562a48a30334d9c

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

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

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

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

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

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

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

https://winscript.jp/powershell/222

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

https://gist.github.com/guitarrapc/03935f5b41466093e0c5

回避策

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

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

https://gist.github.com/guitarrapc/6a0512645697dcf9575a

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

https://gist.github.com/guitarrapc/76a4f82644e96624a64b

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

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

https://gist.github.com/guitarrapc/7c4af9ccc52dd9293e02

CodePropertyScriptPropertyNotePropertyAliasProperty などとあったら.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などはわかりやすいでしょう。試してみてください。

https://gist.github.com/guitarrapc/273e2c6765b565c90d5f

細かく知りたい人へ

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

https://tech.blog.aerie.jp/entry/2013/12/23/173004

罠になりやすい例

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

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

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

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

回避策

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

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

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

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

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

https://gist.github.com/guitarrapc/9d5a346629507f82d341

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

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

https://gist.github.com/guitarrapc/045bf8890cb64f90e6b0

解決策

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

https://gist.github.com/guitarrapc/8249a731c743c7e54f55

厄介ですね!

まとめ

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

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

なんでこの記事書いたの

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

牟田口大介 (@mutaguchi) September 3, 2015

*1:あるいは複数

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

*3:個人の感想です

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

*5:何をいってるんだ

*6:涙しかない

*7:ひっ

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

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




以上の内容はhttps://tech.guitarrapc.com/entry/2015/09/05/012733より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14