[[20180816134654]] 『ParamArrayに値・配列を渡した時の処理』(にゅるん) ページの最後に飛ぶ

[ 初めての方へ | 一覧(最新更新順) | 全文検索 | 過去ログ ]

 

『ParamArrayに値・配列を渡した時の処理』(にゅるん)

お世話になっております。

VBAでは、ParamArrayに対して配列を指定した時、ジャグ配列になってしまうようです。
そこで下記のコードのようにhoge(0)の型を見ることで、配列かどうかを判別してから処理を分ける案を考えました。

 Sub TestFunc1()
    Call Func1(1, "a", "b", "c")
    Call Func1(1, Array("x", "y", "z"))
End Sub

Sub Func1(i As Long, ParamArray hoge())

    If TypeName(hoge(0)) = "Variant()" Then
        Debug.Print hoge(0)(i)
        If UBound(hoge(0)) > i Then Call Func1(i + 1, hoge(0))
    Else
        Debug.Print hoge(i)
        If UBound(hoge) > i Then Call Func1(i + 1, hoge)
    End If
End Sub

しかし1行ならともかく、同じような処理のコードを2種類書くことになるため
共通処理をプロシャージャFunc2に抽出して処理を共通化することを考えました。
ところが、コンパイルエラー「ParamArrayの使い方が適切ではありません。」が出てしまいます。

 Sub TestFunc1kai()
    Call Func1kai(0, "a", "b", "c")
    Call Func1kai(0, Array("x", "y", "z"))
End Sub

Sub Func1kai(i As Long, ParamArray hoge())

    If TypeName(hoge(0)) = "Variant()" Then
        Call Func2(i, hoge())
        If UBound(hoge(0)) > i Then Call Func1(i + 1, hoge(0))
    Else
        Call Func2(i, hoge)
        If UBound(hoge) > i Then Call Func1(i + 1, hoge)
    End If
End Sub

Sub Func2(i As Long, hoge As Variant)

    Debug.Print hoge(i)
End Sub

このような場合、どのように書けば良いのでしょうか。
それともどちらの入力も受け付けられる仕様は望ましくないのですか?
(C#では普通に書いて使っていたと思うのですが・・・)

< 使用 Excel:Excel2016、使用 OS:Windows10 >


ParamArray は配列の配列なので、ジャグ配列ですね。 そういう引数にしているから、それしか受け付けなくなります。 値を配列で渡したいなら、普通に Variant型で受け取れば良いと思いますよ。

 Sub TestFunc1()
    Call Func1(1, "a", "b", "c")
    Call Func1(1, Array("x", "y", "z"))
 End Sub

 Sub Func1(iw As Long, cw1 As Variant, Optional cw2 As String, Optional cw3 As String)
    Dim i As Long

    If TypeName(cw1) = "Variant()" Then
        For i = 0 To UBound(cw1)
            Debug.Print cw1(i)
        Next i
    Else
        Debug.Print cw1, cw2, cw3
    End If
 End Sub
(???) 2018/08/16(木) 16:02

 ネット検索すると、こんなのありました。
           ↓
https://msdn.microsoft.com/ja-jp/vba/language-reference-vba/articles/invalid-paramarray-use
 ParamArray の使い方が正しくありません
  配列または ByRef Variant を想定している別のプロシージャに ParamArray が 引数として渡されました。
  ParamArray パラメーターをバリアント型 ( Variant ) に割り当てて、バリアントを渡します

 まぁ、これだけで全てが解決する訳じゃなさそうですけど、
 本当にやりたいことが分からないので・・

(半平太) 2018/08/16(木) 16:18


???様
 確かに力技で30個ほどOptionalを続けて、それらを配列に変換するコードを書けば実用上困らないレベルになるので案としては悪くないと思います。

 でももう少し自然な形で書く方法は無いものでしょうか。

 一応補足しておくと引数の数は決して3つ程度ということはありません。
いくつの場合でも対応出来るような書き方が知りたいです。

半平太様
>本当にやりたいことが分からないので・・
具体的に何をしたいという訳ではありません。

このような多数のデータを受け取る関数を作るときの技法が知りたいのです。
(上記ではコード量を減らすためにあえて再帰させていますが)

解説ページの意図が分かりにくいですね・・ちょっとジックリ読んでみます。
(にゅるん) 2018/08/16(木) 16:28


VBAではOptionalでひとつずつ引数省略の可否を指定するしかないのですが、引数を30個も並べるのは美しくないですね。(Intelの現在のCPUがどういうレジスタ構成をしているのか知りませんが、過去の他CPUの経験から、引数は4つまでにすべきというのが持論です。まぁ、インタプリンタ言語では意味がないかもですけど)

しかし、配列を使った指定のみ可能にすれば、配列の要素数が変わるだけで、引数は1個で済みますよ。 引数の個数を可変にするのを止めるだけかと思います。(汎用性が高い = 保守性が低い、不具合発生率が高い、作成時間がかかる、となるので、技術者はシンプルイズベストを目指すものと思っています)
(???) 2018/08/16(木) 16:54


 ※ VBAの配列はすべて「SAFEARRAY」です。対するC#などの配列はCArray。
   なので扱いが全く異なります。

 まず、SAFEARRAYの原理を理解することだと思います。
 下記はテスト材料としてのソースです。

 Declare PtrSafe Sub MoveMemory Lib "Kernel32" Alias "RtlMoveMemory" _
     (ByRef Destination As Any, _
      ByRef Source As Any, _
      Optional ByVal Length As Long = 4&)

 Sub TestFunc1kai()
     Call Func1kai(1, "a", "b", "c")
     Call Func1kai(1, Array("x", "y", "z"))

 End Sub

 Sub Func1kai(ByVal i As Long, ParamArray hoge())
     Dim v() As Variant, bnd As Long
     Dim ppsa As LongPtr, psa As LongPtr

     bnd = UBound(hoge)
     If bnd = -1 Then Exit Sub

     If (VarType(hoge(0)) And vbArray) = 0 Then
         MoveMemory ppsa, ByVal VarPtr(i) + 4
         MoveMemory psa, ByVal ppsa

         MoveMemory ByVal VarPtr(bnd) + 4, psa
         MoveMemory ByVal ppsa, 0&   'deref hoge()

     Else
         v() = hoge(0)

     End If

     'VARIANT型配列を作業用として扱う
     Call Func2(i, v())

 End Sub

 Sub Func2(i As Long, hoge() As Variant)
     Debug.Print hoge(i)
 End Sub
(Abyss2) 2018/08/16(木) 16:58

 当初旨く行ったコード
 Sub Func1(i As Long, ParamArray hoge()) 
     If TypeName(hoge(0)) = "Variant()" Then
         Debug.Print hoge(0)(i) ←--------------------------------- チャンと場合分けしているので添え字が書かれている
         If UBound(hoge(0)) > i Then Call Func1(i + 1, hoge(0))
     Else
         Debug.Print hoge(i)   ←--------------------------------- チャンと場合分けしているので添え字は無い
         If UBound(hoge) > i Then Call Func1(i + 1, hoge)
     End If
 End Sub

 トラブった(と思われる)コード
 Sub Func2(i As Long, hoge As Variant) 
     Debug.Print hoge(i)     ←---------------------------------場合分けがない。
 End Sub

 そうなると

 Sub Func1kai(i As Long, ParamArray hoge()) 
     If TypeName(hoge(0)) = "Variant()" Then
         Call Func2(hoge(0)(i)) ←--------------------------------- チャンと場合分けする
         If UBound(hoge(0)) > i Then Call Func1kai(i + 1, hoge(0))
     Else
         Call Func2(hoge(i))   ←--------------------------------- チャンと場合分けする
         If UBound(hoge) > i Then Call Func1kai(i + 1, hoge)
     End If
 End Sub

 Sub Func2(hoge As Variant) 
     Debug.Print hoge  ←---------------------------------シンプルに処理する。
 End Sub

(半平太) 2018/08/16(木) 17:12


皆様ありがとうございます。

???様
>シンプルイズベストを目指すものと思っています
仰る通りだと思います。
今回はC#で慣れた書き方なので、考え方自体は(私にとっては)シンプルなつもりです。
あまり複雑化しそうなら止めようと思います。

Abyss2様
>※VBAの配列はすべて「SAFEARRAY」です。対するC#などの配列はCArray。
>   なので扱いが全く異なります。
プログラム言語の根幹の部分の話ですね。
メモリを直接操作しているのでしょうか。
私は手を出さないほうが良さそうです。

半平太様
上記エラーが出たのは、Func1kaiの「Call Func2(i, hoge)」などの部分です。
メインの処理で配列全体を使わない場合は、提案頂いた方法にするのが良さそうですね。
(にゅるん) 2018/08/16(木) 17:34


せっかく色々な意見を頂いたところで申し訳ないのですが、MSのサイトの意味がわかったかもしれません。
>ParamArray パラメーターをバリアント型 ( Variant ) に割り当てて、バリアントを渡します

要するに一旦Variant変数にデータをコピーしてしまえば良いようです。

    Sub TestFunc1kai()
        Call Func1kai(0, "a", "b", "c")
        Call Func1kai(0, Array("x", "y", "z"))
        Call Func1kai(0, Split("d,e,f", ","))
    End Sub

    Sub Func1kai(i As Long, ParamArray hogehoge())
        Dim hoge As Variant
        If (VarType(hogehoge(0)) And 8192) = 8192 Then
            hoge = hogehoge(0)
        Else
            hoge = hogehoge
        End If
        '-----処理
        Debug.Print hoge(i)
        '-----
    End Sub
たったこれだけで全てが解決しました・・・。
繰り返しますが、皆様本当にありがとうございました。
(にゅるん) 2018/08/16(木) 17:40

VBAでも、Optional を使わずに個数可変に対応できるのですね。 変数2個で作成しているので、「Call Func1kai(0, "a", "b", "c")」は変数4個なのでエラーになるもの、と思い込んでいました。 良い情報をありがとうございます。

変数代入せずとも、以下のようにも書けますね。「処理」部分を1つだけ書くか、同じものを2つ書くかの違いが出ますが、代入を1回減らす効果があるかと思います。

 Sub Func1kai(i As Long, ParamArray hogehoge())
    Dim n As Long

    If VarType(hogehoge(0)) And vbArray Then
        For n = 0 To UBound(hogehoge(0))
            Debug.Print hogehoge(0)(n);
        Next n
    Else
        For n = 0 To UBound(hogehoge)
            Debug.Print hogehoge(n);
        Next n
    End If
    Debug.Print
 End Sub
(???) 2018/08/17(金) 09:29

いっそ、引数を1つにまとめても良い訳ですね。 面白い使い方ですが、有効な使いどころは思いつかず…。
 Sub Func1kai(ParamArray PA())
    Dim i As Long
    Dim j As Long

    For i = 0 To UBound(PA)
        If VarType(PA(i)) And vbArray Then
            For j = 0 To UBound(PA(i))
                Debug.Print PA(i)(j);
            Next j
        Else
            Debug.Print PA(i);
        End If
    Next i
    Debug.Print
 End Sub
(???) 2018/08/17(金) 09:48

???様
微妙に話が食い違うような気がすると思ったら、ParamArrayをご存知なかったのですね。

>変数代入せずとも、以下のようにも書けますね。「処理」部分を1つだけ書くか、
>同じものを2つ書くかの違いが出ますが、代入を1回減らす効果があるかと思います。
そうです。でも同じ処理を2つ書くのは元々出来ていたんですよ(笑)

>いっそ、引数を1つにまとめても良い訳ですね。 面白い使い方ですが、有効な使いどころは思いつかず…。
目的の異なる全ての引数を1つにまとめれば良いというものでもないと思いますが・・?

ParamArrayが有効な場面は別に珍しくないです。
カスタム版のSum、Min、Maxを作るときとか。

あとずっと違和感があったのですが、ふと気が付きました。
>If (VarType(hogehoge(0)) And 8192) = 8192 Then
>If VarType(hogehoge(0)) And vbArray Then
これは
If IsArray(hogehoge(0)) Then
で十分じゃないかと。
(にゅるん) 2018/08/17(金) 10:00


コメント返信:

[ 一覧(最新更新順) ]


YukiWiki 1.6.7 Copyright (C) 2000,2001 by Hiroshi Yuki. Modified by kazu.