しばたテックブログ

気分で書いている技術ブログです。

PowerShellのCountプロパティについてあれやこれや (2018年10月版)

3年ほど前に

blog.shibata.tech

というエントリを書いたのですが、その後新たに分かったことや新しいバージョンでの変更も発生したため、内容を更新してリライトしました。

便利な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独自のデータを付与できる機能になります。
詳細については牟田口先生の以下のエントリを参照してください。

winscript.jp

型データの定義が記載されている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すべてのオブジェクトの基本となるPSObjectDLRを使った動的オブジェクトとして定義されています。
これによりこれまでリフレクションによって行われていたメソッドやプロパティの解決が高速化され、PowerShell全体のパフォーマンス改善につながっています。

そしてDLRでは、主にエラーハンドリングのために、メソッドやプロパティの解決に失敗したときの処理を定義するフォールバック機能が提供されており、Countプロパティにおいては、

  1. オブジェクトのCount(またはLength)プロパティを取得しようとする

  2. Count(またはLength)プロパティが存在しないオブジェクトの場合、DLRのフォールバック(FallbackGetMember)が呼び出される

  3. フォールバック処理内で元のオブジェクトの種類に応じて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を返してしまいます。

tech.guitarrapc.com

実際に試してみると確かに$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)があったため削除されています。

github.com

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:前項で説明を端折りましたが、動的オブジェクトのメンバーフォールバックは配列の場合も考慮されいます