3年ほど前に
というエントリを書いたのですが、その後新たに分かったことや新しいバージョンでの変更も発生したため、内容を更新してリライトしました。
便利なCountプロパティ
たとえばコマンドを実行して出力した結果の件数を取得したい場合、
([コマンド]).Count
とするとその件数を取得することができます。
具体例として以下の様にコマンドを実行するとC:\Program Files
配下にあるEXEファイルの総数を取得することができます。
# Countプロパティを使うと、評価した結果(System.IO.FileInfo[])の要素数を取得することができます。 PS C:\> (dir -Path "C:\Program Files" -File -Filter "*.exe" -Recurse).Count 815
Countプロパティの実体
System.Array.Count?
このCount
、要はコマンドを実行した結果(この場合はSystem.IO.FileInfo[]
)の要素数を取得しているだけなのですが、しかしながら、.NETの配列(System.Array
)にはLength
プロパティはあれどもCount
プロパティはありません。
それでもPowerShellではCount
プロパティを利用することができています。
バージョン間の差異
この謎めいたCount
プロパティはPowerShellのバージョンによってその裏にある実体が異なり、その挙動も異なります。
大きくはPowerShell 2.0までとPowerShell 3.0以降に分かれ、加えてPowerShell Core 6.0で若干の仕様変更が入り現在に至っています。
以降、バージョンごとの実体について触れていきます。
PowerShell 2.0までCountプロパティの実体
PowerShell 2.0までのCount
プロパティの実体はSystem.Array
に追加された型データになります。
型データとは.NETの型にPowerShell独自のデータを付与できる機能になります。
詳細については牟田口先生の以下のエントリを参照してください。
型データの定義が記載されているC:\Windows\System32\WindowsPowerShell\v1.0\types.ps1xml
を見るとCount
プロパティは以下の様に定義されており、System.Array.Length
に対するAliasProperty
となっていることがわかります。
# C:\Windows\System32\WindowsPowerShell\v1.0\types.ps1xmlより抜粋 <Type> <Name>System.Array</Name> <Members> <AliasProperty> <Name>Count</Name> <ReferencedMemberName>Length</ReferencedMemberName> </AliasProperty> </Members> </Type>
Get-Member
で空の配列@()
のメンバを確認してみるとこんな結果を返します。
PS C:\> Get-Member -InputObject @() -View Extended
TypeName: System.Object[]
Name MemberType Definition
---- ---------- ----------
Count AliasProperty Count = Length
この型データにより本来System.Array
には存在しないCount
プロパティを利用することができます。
制限事項
PowerShell 2.0までのCount
プロパティはSystem.Array
に対するものなので配列以外では使うことができません。
例えば以下の様なコードは$null
を返し、配列以外ではCount
プロパティが存在しないことがわかります。
# PowerShell 2.0 PS C:\> $PSVersionTable.PSVersion Major Minor Build Revision ----- ----- ----- -------- 2 0 -1 -1 PS C:\> (123).Count PS C:\>
配列でないオブジェクトに対してCount
プロパティを使いたいときは配列化してやる必要があります。
# PowerShell 2.0 # 要素数1の配列化してCountプロパティを取得 PS C:\> @(123).Count 1
PowerShell 3.0 ~ 5.1のCountプロパティの実体
PowerShell 2.0では配列にしかCount
プロパティが使えませんでしたが、PowerShell 3.0からは配列でないスカラ値や$null
に対してもCount
プロパティが使える様に機能が拡張されています。
# PowerShell 3.0 以降 PS C:\> $PSVersionTable.PSVersion Major Minor Build Revision ----- ----- ----- -------- 5 1 17763 1 # スカラ値のCountは 1 PS C:\> (123).Count 1 # $nullのCountは 0 PS C:\> ($null).Count 0
この変更については公にはWindows PowerShell Blogのこの記事で触れられる程度の情報しかなく、機能の実体については謎でした。
しかしながらPowerShellがオープンソースとなったため実際にソースを確認してみたところ、このCount
プロパティは動的オブジェクトのメンバーフォールバックとして実装されていることが分かりました。
PowerShellは3.0ではランタイムが.NET 4ベースで刷新され、PowerShellすべてのオブジェクトの基本となるPSObject
が
DLRを使った動的オブジェクトとして定義されています。
これによりこれまでリフレクションによって行われていたメソッドやプロパティの解決が高速化され、PowerShell全体のパフォーマンス改善につながっています。
そしてDLRでは、主にエラーハンドリングのために、メソッドやプロパティの解決に失敗したときの処理を定義するフォールバック機能が提供されており、Count
プロパティにおいては、
オブジェクトの
Count
(またはLength
)プロパティを取得しようとするCount
(またはLength
)プロパティが存在しないオブジェクトの場合、DLRのフォールバック(FallbackGetMember
)が呼び出されるフォールバック処理内で元のオブジェクトの種類に応じて
1
または0
を返す
といった動作をしています。
(これがすべてではありませんが)ソースコードとしてはこのあたりの処理が参考になります。
参考として以下にフォールバックの一部を列挙しておきます。
// // PowerShell 6.0時点の Binders.cs より一部引用 // /// <summary> /// Return the binding result when no property exists. /// </summary> private DynamicMetaObject PropertyDoesntExist(DynamicMetaObject target, BindingRestrictions restrictions) { // ・・・ 省略 ・・・ // As part of our effort to hide how a command can return a singleton or array, we want to allow people to iterate // over singletons with the foreach statement (which has worked since V1) and a for loop, for example: // for ($i = 0; $i -lt $x.Length; $i++) { $x[$i] } // If $x is a singleton, we want to return 1 for the length so code like this works correctly. // We do not want this magic to show up in Get-Member output, tab completion, intellisense, etc. if (Name.Equals("Length", StringComparison.OrdinalIgnoreCase) || Name.Equals("Count", StringComparison.OrdinalIgnoreCase)) { // $null.Count should be 0, anything else should be 1 var resultCount = PSObject.Base(target.Value) == null ? 0 : 1; return new DynamicMetaObject( Expression.Condition( Compiler.IsStrictMode(2), ThrowPropertyNotFoundStrict(), ExpressionCache.Constant(resultCount).Cast(typeof(object))), restrictions); } // ・・・ 省略 ・・・ }
ちなみに配列に対しては従来通り型データによるAliasProperty
が使われています。
制限事項
PowerShell 3.0で配列以外にもCount
プロパティが使える様になったのですが、ぎたぱそ先生の以下のエントリによるとPScustomObject
に対してはCount
プロパティは$null
を返してしまいます。
実際に試してみると確かに$null
を返します。
# PowerShell 3.0 ~ 6.0 PS C:\> $PSVersionTable.PSVersion Major Minor Build Revision ----- ----- ----- -------- 5 1 17763 1 PS C:\> ([PScustomObject]@{hoge = "hoge"}).Count PS C:\>
このためPScustomObject
に対してCount
プロパティを使用する際は注意が必要です。
PowerShell 6.0以降のCountプロパティの実体
PowerShell Core 6.0においてもCount
プロパティの実体が動的オブジェクトのメンバーフォールバックであることは変わらないのですが、いくつか挙動が変更されています。
AliasPropertyの削除 (PowerShell Core 6.0より)
最初に説明したSystem.Array.Length
に対するAliasProperty
ですが、これはPowerShell Core 6.0で削除されました。
理由としてはこのAliasProperty
が無くても問題がない*1点と、ConvertTo-Json
等の処理で不要なCount
プロパティが露出してしまう問題(#3222)があったため削除されています。
PSCustomObjectに対するCount (PowerShell Core 6.1より)
前項で触れたPScustomObject
に対するCount
ですが、PowerShell Core 6.1で挙動が改善されPScustomObject
でもCount
プロパティが取得できる様に改善されています。
PS C:\> $PSVersionTable.PSVersion Major Minor Patch PreReleaseLabel BuildLabel ----- ----- ----- --------------- ---------- 6 1 0 PS C:\> ([PScustomObject]@{hoge = "hoge"}).Count 1
【補足】PowerShell 4.0のWhere、ForEachメソッドについて
最後にちょっとだけおまけを。
PowerShell 4.0から配列などのコレクションオブジェクトに対してWhere()
およびForEach()
メソッドが使える様に機能拡張されています。
# PowerShell 4.0以降 PS C:\> @(1,2,3).Where({$_ -eq 2}) 2 PS C:\> @(1,2,3).ForEach({$_ * 2}) 2 4 6
これらのメソッドも動的オブジェクトのメンバーフォールバックとして実装されいます。
ソースコードとしてはこのあたりが参考になると思いますので興味のある方はご覧ください。
*1:前項で説明を端折りましたが、動的オブジェクトのメンバーフォールバックは配列の場合も考慮されいます