タイダログ

もっと怠けますか? (y/n)

PowerShell から .NET のメソッドに [ref] で参照渡しをする方法

PowerShell から .NET のメソッドに引数を参照渡しするときに、何回調べても [ref] をつける箇所が分からなくなるので、今年の自由研究のテーマにしました。虫捕りと迷ったのですが、こちらにします。

.NET の [DateTime]::TryParseExact メソッドで検証します。

前提

作業環境は以下の通りです。

今回は .NET のメソッドを直接呼び出す方法と、function や scriptblock にして呼び出す方法を考えます。[ref] の部分に焦点を当てるため、その他の部分では変数を使いません。

変数 $parsedDateTime[DateTime]::MinValue を代入し、[ref] を付けたり付けなかったりしながら [DateTime]::TryParseExact(String, String, IFormatProvider, DateTimeStyles, DateTime) に渡します。

docs.microsoft.com

[DateTime]::TryParseExact(String, String, IFormatProvider, DateTimeStyles, DateTime)(英語)

docs.microsoft.com

[DateTime]::TryParseExact(String, String, IFormatProvider, DateTimeStyles, DateTime)(日本語)

検証したいこと

  1. どこに [ref] を付けたらエラーが出ないか
  2. [DateTime]::TryParseExact の引数 Result に .Value が必要かどうか
  3. function 等の仮引数に [DateTime] を指定していいかどうか

[ref] の位置をはっきりさせることが今回の主目的です。

それから、function に参照渡しした変数値にアクセスするには、こんな感じで .Value プロパティを使うとの情報が見つかるのですが、

Function Test([ref]$data)
{
    $data.Value = 3
}

参照パラメーターを受け入れる関数の記述 より

これに従うとうまく行かないことがあったので合わせて検証します。

ついでに、以下のように function 等の仮引数に [DateTime] を指定していいかどうかも確認します。

function TestByRef {
    param (
        [DateTime]
        Result # 仮引数
    )

   ...
}

検証するパターン

.NET

.NET のメソッドを直接呼び出すなら、まずこの2か所に [ref] が付く可能性があります。

[ref]$parsedDateTime = [DateTime]::MinValue # 変数

[DateTime]::TryParseExact(
    '2021-08-11 12:34:56',
    'yyyy-MM-dd HH-mm-ss',
    [System.Globalization.DateTimeFormatInfo]::InvariantInfo,
    [System.Globalization.DateTimeStyles]::None,
    [ref]$parsedDateTime # .NET の引数
)
  1. 変数
  2. .NET の引数

以上の2か所に [ref] を付けるかどうかと、.Value を付けるかどうかということで、次の8パターンを検証すればいいことになります。

パターン 変数 .NET .Value
1 - - -
2 - - .Value
3 - [ref] -
4 - [ref] .Value
5 [ref] - -
6 [ref] - .Value
7 [ref] [ref] -
8 [ref] [ref] .Value

function/scriptblock

function と scriptblock なら、この4か所に [ref] が付く可能性があります。

[ref]$parsedDateTime = [DateTime]::MinValue # 変数

function TestByRef {
    param (
        [ref]
        Result # 仮引数
    )

    [DateTime]::TryParseExact(
        '2021-08-11 12:34:56',
        'yyyy-MM-dd HH-mm-ss',
        [System.Globalization.DateTimeFormatInfo]::InvariantInfo,
        [System.Globalization.DateTimeStyles]::None,
        [ref]$parsedDateTime # .NET の引数
    )
}

TestByRef ([ref]$parsedDateTime) # 実引数
[ref]$parsedDateTime = [DateTime]::MinValue # 変数

[scriptblock]$testByRef = {
    param (
        [ref]
        Result # 仮引数
    )

    [DateTime]::TryParseExact(
        '2021-08-11 12:34:56',
        'yyyy-MM-dd HH-mm-ss',
        [System.Globalization.DateTimeFormatInfo]::InvariantInfo,
        [System.Globalization.DateTimeStyles]::None,
        [ref]$parsedDateTime # .NET の引数
    )
}

$testByRef.Invoke(([ref]$parsedDateTime)) # 実引数
  1. 変数
  2. .NET の引数
  3. 仮引数(param 節)
  4. 実引数(function/scriptblock に渡す引数)

以上の4か所の [ref].Value の有無、それと仮引数を [DateTime] 型にすることも考えて、次の64パターンを検証します。仮引数に [ref][DateTime] を両方付けるパターンも試します。以前やったことがあるので。

多いな……やっぱり虫捕りの方がよかったかなぁ……。

パターン 変数 .NET .Value 仮引数 [ref] 仮引数 [DateTime] 実引数
9 - - - - - -
10 - - - - - [ref]
11 - - - - [DateTime] -
12 - - - - [DateTime] [ref]
13 - - - [ref] - -
14 - - - [ref] - [ref]
15 - - - [ref] [DateTime] -
16 - - - [ref] [DateTime] [ref]
17 - - .Value - - -
18 - - .Value - - [ref]
19 - - .Value - [DateTime] -
20 - - .Value - [DateTime] [ref]
21 - - .Value [ref] - -
22 - - .Value [ref] - [ref]
23 - - .Value [ref] [DateTime] -
24 - - .Value [ref] [DateTime] [ref]
25 - [ref] - - - -
26 - [ref] - - - [ref]
27 - [ref] - - [DateTime] -
28 - [ref] - - [DateTime] [ref]
29 - [ref] - [ref] - -
30 - [ref] - [ref] - [ref]
31 - [ref] - [ref] [DateTime] -
32 - [ref] - [ref] [DateTime] [ref]
33 - [ref] .Value - - -
34 - [ref] .Value - - [ref]
35 - [ref] .Value - [DateTime] -
36 - [ref] .Value - [DateTime] [ref]
37 - [ref] .Value [ref] - -
38 - [ref] .Value [ref] - [ref]
39 - [ref] .Value [ref] [DateTime] -
40 - [ref] .Value [ref] [DateTime] [ref]
41 [ref] - - - - -
42 [ref] - - - - [ref]
43 [ref] - - - [DateTime] -
44 [ref] - - - [DateTime] [ref]
45 [ref] - - [ref] - -
46 [ref] - - [ref] - [ref]
47 [ref] - - [ref] [DateTime] -
48 [ref] - - [ref] [DateTime] [ref]
49 [ref] - .Value - - -
50 [ref] - .Value - - [ref]
51 [ref] - .Value - [DateTime] -
52 [ref] - .Value - [DateTime] [ref]
53 [ref] - .Value [ref] - -
54 [ref] - .Value [ref] - [ref]
55 [ref] - .Value [ref] [DateTime] -
56 [ref] - .Value [ref] [DateTime] [ref]
57 [ref] [ref] - - - -
58 [ref] [ref] - - - [ref]
59 [ref] [ref] - - [DateTime] -
60 [ref] [ref] - - [DateTime] [ref]
61 [ref] [ref] - [ref] - -
62 [ref] [ref] - [ref] - [ref]
63 [ref] [ref] - [ref] [DateTime] -
64 [ref] [ref] - [ref] [DateTime] [ref]
65 [ref] [ref] .Value - - -
66 [ref] [ref] .Value - - [ref]
67 [ref] [ref] .Value - [DateTime] -
68 [ref] [ref] .Value - [DateTime] [ref]
69 [ref] [ref] .Value [ref] - -
70 [ref] [ref] .Value [ref] - [ref]
71 [ref] [ref] .Value [ref] [DateTime] -
72 [ref] [ref] .Value [ref] [DateTime] [ref]

結果

.NET

パターン 変数 .NET .Value 結果
1 - - - 失敗
2 - - .Value 失敗
3 - [ref] - 成功
4 - [ref] .Value 失敗
5 [ref] - - ※1
6 [ref] - .Value 失敗
7 [ref] [ref] - 失敗
8 [ref] [ref] .Value ※1※2

※1 エラーは出ないが $parsedDateTime の型が [PSReference] 型のままになっている
※2 エラーは出ないが $parsedDateTime の中身が初期値のままになっている

パターン 1 のエラーメッセージに従えば解決です。

引数 '5' は System.Management.Automation.PSReference でなければなりません。[ref] を使用してください。

パターン 5 は成功しますが、変数 $parsedDateTime[PSReference] 型のままですので、[DateTime] 型としては扱えません。型変換も不可能なようです。たぶん。

というわけでパターン 3 一択です。

$parsedDateTime = [DateTime]::MinValue

[DateTime]::TryParseExact(
    '2021-08-11 12-34-56',
    'yyyy-MM-dd HH-mm-ss',
    [System.Globalization.DateTimeFormatInfo]::InvariantInfo,
    [System.Globalization.DateTimeStyles]::None,
    [ref]$parsedDateTime
) # True

$parsedDateTime # 2021年8月11日 12:34:56

function/scriptblock

検証用のスクリプトを function 用と scriptblock 用にそれぞれ64パターン作るスクリプトを書いて、その 64 * 2 = 128 のスクリプトスクリプトで実行した結果をスクリプトでまとめました(?)

こちらに御座います。
taidalog/PowerShell_Ref - GitHub

パターン 変数 .NET .Value 仮引数 [ref] 仮引数 [DateTime] 実引数 function の結果 scriptblock の結果
9 - - - - - - 失敗 失敗
10 - - - - - [ref] 成功 成功
11 - - - - [DateTime] - 失敗 失敗
12 - - - - [DateTime] [ref] 失敗 失敗
13 - - - [ref] - - 失敗 ※2
14 - - - [ref] - [ref] 成功 成功
15 - - - [ref] [DateTime] - 失敗 ※2
16 - - - [ref] [DateTime] [ref] 成功 失敗
17 - - .Value - - - 失敗 失敗
18 - - .Value - - [ref] 失敗 失敗
19 - - .Value - [DateTime] - 失敗 失敗
20 - - .Value - [DateTime] [ref] 失敗 失敗
21 - - .Value [ref] - - 失敗 失敗
22 - - .Value [ref] - [ref] 失敗 失敗
23 - - .Value [ref] [DateTime] - 失敗 失敗
24 - - .Value [ref] [DateTime] [ref] 失敗 失敗
25 - [ref] - - - - ※2 ※2
26 - [ref] - - - [ref] 失敗 失敗
27 - [ref] - - [DateTime] - ※2 ※2
28 - [ref] - - [DateTime] [ref] ※2 失敗
29 - [ref] - [ref] - - 失敗 失敗
30 - [ref] - [ref] - [ref] 失敗 失敗
31 - [ref] - [ref] [DateTime] - 失敗 失敗
32 - [ref] - [ref] [DateTime] [ref] 失敗 失敗
33 - [ref] .Value - - - 失敗 失敗
34 - [ref] .Value - - [ref] ※2 ※2
35 - [ref] .Value - [DateTime] - 失敗 失敗
36 - [ref] .Value - [DateTime] [ref] 失敗 失敗
37 - [ref] .Value [ref] - - 失敗 ※2
38 - [ref] .Value [ref] - [ref] ※2 ※2
39 - [ref] .Value [ref] [DateTime] - 失敗 ※2
40 - [ref] .Value [ref] [DateTime] [ref] ※2 失敗
41 [ref] - - - - - ※1 ※1
42 [ref] - - - - [ref] 失敗 失敗
43 [ref] - - - [DateTime] - 失敗 失敗
44 [ref] - - - [DateTime] [ref] 失敗 失敗
45 [ref] - - [ref] - - ※1 ※1
46 [ref] - - [ref] - [ref] 失敗 失敗
47 [ref] - - [ref] [DateTime] - ※1 失敗
48 [ref] - - [ref] [DateTime] [ref] 失敗 失敗
49 [ref] - .Value - - - 失敗 失敗
50 [ref] - .Value - - [ref] ※1 ※1
51 [ref] - .Value - [DateTime] - 失敗 失敗
52 [ref] - .Value - [DateTime] [ref] 失敗 失敗
53 [ref] - .Value [ref] - - 失敗 失敗
54 [ref] - .Value [ref] - [ref] ※1 ※1
55 [ref] - .Value [ref] [DateTime] - 失敗 失敗
56 [ref] - .Value [ref] [DateTime] [ref] ※1 失敗
57 [ref] [ref] - - - - 失敗 失敗
58 [ref] [ref] - - - [ref] 失敗 失敗
59 [ref] [ref] - - [DateTime] - ※1※2 失敗
60 [ref] [ref] - - [DateTime] [ref] 失敗 失敗
61 [ref] [ref] - [ref] - - 失敗 失敗
62 [ref] [ref] - [ref] - [ref] 失敗 失敗
63 [ref] [ref] - [ref] [DateTime] - 失敗 失敗
64 [ref] [ref] - [ref] [DateTime] [ref] 失敗 失敗
65 [ref] [ref] .Value - - - ※1※2 ※1※2
66 [ref] [ref] .Value - - [ref] ※1 ※1
67 [ref] [ref] .Value - [DateTime] - 失敗 失敗
68 [ref] [ref] .Value - [DateTime] [ref] 失敗 失敗
69 [ref] [ref] .Value [ref] - - ※1※2 ※1※2
70 [ref] [ref] .Value [ref] - [ref] ※1 ※1
71 [ref] [ref] .Value [ref] [DateTime] - ※1※2 失敗
72 [ref] [ref] .Value [ref] [DateTime] [ref] ※1 失敗

※1 エラーは出ないが $parsedDateTime の型が [PSReference] 型のままになっている
※2 エラーは出ないが $parsedDateTime の中身が初期値のままになっている

function と scriptblock で結果が変わる箇所があるのが意外でした。プログラムの仕組みをもっと勉強すればこういうのも理解できるんだろうな。

.NET の引数に [ref] を付けるとことごとく失敗するというのは大きな発見でした。真っ先に [ref] を付けたくなる箇所なのですけどね。

仮引数に [ref][DateTime] を両方付けるというカオスが許されるものなんですね。他の型が渡ってしまうのは [ValidateScript] でなんとかなるかな。

.Value を付けても失敗しました。なんでや。

まとめると、以下の3点に気を付ければいいようです。

  • function の仮引数に [DateTime]単独でつけてはいけない
  • function の実引数には [ref]付ける
  • それ以外は何も付けない

ということでパターン 10 でいいと思います。

function TestByRef {
    param (
        $Result
    )

    [DateTime]::TryParseExact(
        '2021-08-11 12-34-56',
        'yyyy-MM-dd HH-mm-ss',
        [System.Globalization.DateTimeFormatInfo]::InvariantInfo,
        [System.Globalization.DateTimeStyles]::None,
        $Result
    )
}


$parsedDateTime = [DateTime]::MinValue

TestByRef ([ref]$parsedDateTime) # True

$parsedDateTime # 2021年8月11日 12:34:56
[scriptblock]$testByRef = {
    param (
        $Result
    )

    [DateTime]::TryParseExact(
        '2021-08-11 12-34-56',
        'yyyy-MM-dd HH-mm-ss',
        [System.Globalization.DateTimeFormatInfo]::InvariantInfo,
        [System.Globalization.DateTimeStyles]::None,
        $Result
    )
}


$parsedDateTime = [DateTime]::MinValue

$testByRef.Invoke(([ref]$parsedDateTime)) # True

$parsedDateTime # 2021年8月11日 12:34:56

他のメソッドでも確認

他の .NET メソッドでも試しましょう。[Int32]::TryParse(String, Int32) メソッドを使います。

docs.microsoft.com

[Int32]::TryParse(String, Int32)(英語)

docs.microsoft.com

[Int32]::TryParse(String, Int32)(日本語)

$parsedInt = [Int]::MinValue

[Int]::TryParse('1', [ref]$parsedInt) # True

$parsedInt # 1
function TestIntTryParse {
    param (
        $Result
    )

    [int]::TryParse('1', $Result)
}


$parsedInt = [Int]::MinValue

TestIntTryParse ([ref]$parsedInt) # True

$parsedInt # 1
[scriptblock]$testIntTryParse = {
    param (
        $Result
    )

    [Int]::TryParse('1', $Result)
}


$parsedInt = [Int]::MinValue

$testIntTryParse.Invoke(([ref]$parsedInt)) # True

$parsedInt # 1

よし、全部クリア!

function への参照渡しについてわかったこと

カッコ必須です。

TestByRef ([ref]$parsedDateTime) # OK
TestByRef [ref]$parsedDateTime   # NG

値渡しと混在させてもOKです。

function TestByRef {
    param (
        [string]
        $s,

        $Result
    )

    [DateTime]::TryParseExact(
        $s,
        'yyyy-MM-dd HH-mm-ss',
        [System.Globalization.DateTimeFormatInfo]::InvariantInfo,
        [System.Globalization.DateTimeStyles]::None,
        $Result
    )
}

TestByRef '2021-08-11 12-34-56' ([ref]$parsedDateTime) # OK

名前付き引数で渡す例をあまり見ませんが、問題なくできます。

TestByRef -s '2021-08-11 12-34-56' -Result ([ref]$parsedDateTime) # OK

スプラッティングもできます。

$params = @{
    s = '2021-08-11 12-34-56'
    Result = ([ref]$parsedDateTime)
}

TestByRef @params # OK

パイプラインから渡す方法は上手くいきませんでした。

$parsedDateTime | %{TestByRef -s '2021-08-11 12-34-56' -Result  ([ref]$_)
# エラーは出ないが変換できない

感想

f:id:taidalog:20210812181344p:plain
おもしろかったです。またやろうと思いました。

思いついてから完成まで3日くらいかかりました。疲れたぜ。

検証するにあたってたくさんデバッグしましたので、これもひとつの「虫取り」ということで。

参考

about Ref - PowerShell | Microsoft Docs

ref について - PowerShell | Microsoft Docs

PowerShell - ファンクションに参照渡しで引数を受け渡す - ITLAB51.COM

PowerShellの再帰処理と参照渡し引数 - PS Work