Excel (VBA) |
![]() ![]() |
(指定なし : 指定なし)
Static の挙動について
投稿日時: 22/12/26 15:50:36
投稿者: たらのり
|
---|---|
おつかれさまです
Public Sub Test() Dim ast() As String ' Array of string Dim i As Long ast = GetSameArrayAnytime() For i = LBound(ast) To UBound(ast) Debug.Print ast(i) Next i ast = GetSameArrayAnytime() For i = LBound(ast) To UBound(ast) ' (9) インデクスが有効範囲にありません Debug.Print ast(i) Next i End Sub ' 毎回同じ配列を返す Private Function GetSameArrayAnytime() As String() Static ast() As String Static bInit As Boolean If (Not bInit) Then ast = GetArray() ' ast は一度だけ初期化される bInit = True End If GetSameArrayAnytime = ast End Function ' 初期化のため一度だけ呼ばれる Private Function GetArray() As String() Dim ast(1 To 3) As String ast(1) = "a" ast(2) = "b" ast(3) = "c" GetArray = ast End Function 今回期待するのは、イミディエイトウィンドウに次のように 表示されることです: a b c a b c (実行時エラーのため、前半の 3字までしか表示されません) |
![]() |
投稿日時: 22/12/26 17:35:55
投稿者: taitani
|
---|---|
2回目でもう一度代入しようとしているからでは?
Public Sub Test() Dim ast() As String ' Array of string Dim i As Long ast = GetSameArrayAnytime() For i = LBound(ast) To UBound(ast) Debug.Print ast(i) Next i ' ast = GetSameArrayAnytime() For i = LBound(ast) To UBound(ast) ' (9) インデクスが有効範囲にありません Debug.Print ast(i) Next i End Sub 上記で期待された結果が出力されるはず。 |
![]() |
投稿日時: 22/12/26 18:06:38
投稿者: たらのり
|
---|---|
taitani さん、
Public Sub Test_2() Call CalledManyTimes ' Do something Call CalledManyTimes End Sub ' 何回も呼ばれる処理 Public Sub CalledManyTimes() Dim ast() As String ' Array of string Dim i As Long ast = GetSameArrayAnytime() For i = LBound(ast) To UBound(ast) Debug.Print ast(i) Next i End Sub 何度も呼ばれる処理で、つねに同一の配列を得る目的です。 処理化後の配列の状態は処理ごとに変化しますが、 同一(一回)の処理内では不変としたいつもりです。 |
![]() |
投稿日時: 22/12/26 18:16:16
投稿者: taitani
|
---|---|
うーん、私的に違和感がありますね。
引用: ということであれば、"Dim ast() As String" だけになるんじゃないかなって。 |
![]() |
投稿日時: 22/12/26 19:32:40
投稿者: 半平太
|
---|---|
>(たぶん)、Static の挙動はこのようなものなのでしょうか。
|
![]() |
投稿日時: 22/12/26 22:00:42
投稿者: たらのり
|
---|---|
taitani さん、
Function GetDic(ByBal key_ As String) As Scripting.Dictionary Static dic As Scripting.Dictionary if dic is Nothing Then set dic = new Scripting.Dictionary ' 登録処理 Enf if set GetDic = dic End function 今回の問題を含むコードには上と同等のコードを含みます。 同様の場面で Dictionayの変わりに配列を使用したことが あったかというと、定かではありません。 Staticを使用するのはスコープを限定して情報を保護したい ためでした。 半平太 さん、 レスをありがとうございます。 半平太 さんの環境でも、元のコードでは同様の減少が発生した のでしょうか(のですよね)。 書き方は別として、なんだかおかしい(直感に反する)なと… |
![]() |
投稿日時: 22/12/26 22:40:08
投稿者: 半平太
|
---|---|
>半平太 さんの環境でも、元のコードでは同様の現象が発生した
|
![]() |
投稿日時: 22/12/26 23:36:47
投稿者: hatena
|
---|---|
動的配列をStatic(静的)宣言するのがちょっと違和感があります。
' 毎回同じ配列を返す Private Function GetSameArrayAnytime() As String() Static ast As Variant ' ★ Static bInit As Boolean If (Not bInit) Then ast = GetArray() ' ast は一度だけ初期化される bInit = True End If GetSameArrayAnytime = ast End Function 半平太さんのコードの、 UBound(ast) とか IsArray(ast) というように関数の引数にすると「記憶域 スペース」確保することができるということになるのでしょうか。 まあ、内部的なことはMSの開発者にはしか分からないので、推測でなんとなく納得するしかないのですが。 |
![]() |
投稿日時: 22/12/26 23:55:55
投稿者: たらのり
|
---|---|
半平太 さん、
|
![]() |
投稿日時: 22/12/27 11:47:47
投稿者: 半平太
|
---|---|
>動的配列をStatic(静的)宣言するのがちょっと違和感があります
|
![]() |
投稿日時: 22/12/27 13:11:06
投稿者: sk
|
---|---|
現時点での結論としては「 VBA のバグの一種である」可能性が
たらのり さんの引用: GetSameArrayAnytime のプロシージャレベルで宣言されている それぞれの変数の状態をウォッチウィンドウで確認した限り、 戻り値として渡すまでは ast の型が String(1 To 3) で 保持されていますが、End Function ステートメントに入ったところで String() になる、つまり空の動的配列に初期化されているようです。 対して bInit の値は、1 回目の呼び出し後以降は True のまま 保持されています。 ちなみに GetSameArrayAnytime に Static キーワードを 付加しても同じ結果となります。 --------------------------------------------------------------- Private Static Function GetSameArrayAnytime() As String() Dim ast() As String Dim bInit As Boolean If (Not bInit) Then ast = GetArray() ' ast は一度だけ初期化される bInit = True End If GetSameArrayAnytime = ast End Function --------------------------------------------------------------- たらのり さんの引用: そして Test プロシージャ側で 2 回目の GetSameArrayAnytime が 呼び出された際に空の動的配列( 1 回目の時点で初期化された ast )が そのまま戻り値として返され、更にそれを LBound 関数に渡した際に 実行時エラー 9 が発生する、というのがこの現象の一連の流れです。 半平太 さんの引用: 上記のようにすると、何故か ast の型が String(1 To 3) のまま (当然配列の各要素の値も)保持されます。 なんと適当に行ラベルを挿入するだけでも有効。 --------------------------------------------------------------- Private Function GetSameArrayAnytime() As String() Static ast() As String Static bInit As Boolean If (Not bInit) Then ast = GetArray() ' ast は一度だけ初期化される bInit = True End If GetSameArrayAnytime = ast Exit_GetSameArrayAnytime: End Function --------------------------------------------------------------- hatena さんの引用: hatena さんの引用: 逆に GetSameArrayAnytime の戻り値の型を Variant 型に (もしくは型宣言を省略)しても ast の状態は保持されます。 --------------------------------------------------------------- Private Function GetSameArrayAnytime() As Variant Static ast() As String Static bInit As Boolean If (Not bInit) Then ast = GetArray() ' ast は一度だけ初期化される bInit = True End If GetSameArrayAnytime = ast End Function --------------------------------------------------------------- なお、ast が静的配列である場合は問題なく実行できます。 --------------------------------------------------------------- Private Function GetSameArrayAnytime() As String() Static ast(1 To 3) As String Static bInit As Boolean If (Not bInit) Then ast(1) = "a" ast(2) = "b" ast(3) = "c" bInit = True End If GetSameArrayAnytime = ast End Function --------------------------------------------------------------- 以上のことから、恐らく再現条件は次の 3 つを満たすこと。 ・Function プロシージャの戻り値の型が動的配列である。 ・プロシージャレベルで宣言された動的配列変数の属性が Static である。 ・Static 属性の動的配列をプロシージャの戻り値として返すコードの直後に End Function ステートメントがある。 こちらの環境で検証した限り、Excel 2010 でも Excel 2016 でも確認できたため、 今のところバージョンに依存しない不具合である可能性が高いように思われます。 |
![]() |
投稿日時: 22/12/28 17:30:25
投稿者: たらのり
|
---|---|
ちょっといたずらで、Static の領域がどうなっているのか、
' VBA Private Declare PtrSafe Sub DumpMem _ Lib "C:\Users\user\dev\dll\test\PeekStatic.dll" _ (ByVal p As LongPtr, ByVal sz As Long) Public Sub Test() Dim ast() As String Dim i As Long ast = GetSameArrayAnytime() For i = LBound(ast) To UBound(ast) Debug.Print ast(i) Next i ast = GetSameArrayAnytime() For i = LBound(ast) To UBound(ast) ' * (9) インデクスが有効範囲にありません Debug.Print ast(i) Next i End End Sub ' 毎回同じ配列を返す Private Function GetSameArrayAnytime() As String() Static dummy As Long Static ast() As String ' Array of string Static bInit As Boolean Call DumpMem(VarPtr(dummy), 16) ' ★ DLL呼び出し If (Not bInit) Then dummy = &HAAAAAAAA ast = GetArray() ' ast は一度だけ初期化される bInit = True End If Call DumpMem(VarPtr(dummy), 16) ' ★ DLL呼び出し GetSameArrayAnytime = ast ' ☆ '' Call DumpMem(VarPtr(dummy), 16) ' ★★ DLL呼び出し(しない) End Function ' 初期化のため一度だけ呼ばれる Private Function GetArray() As String() Dim ast(1 To 3) As String ast(1) = "a" ast(2) = "b" ast(3) = "c" GetArray = ast End Function /* peekstatic.c */ #include <windows.h> #include <stdio.h> void WINAPI DumpMem(const char *p, int sz) { char *buf; int i; buf = (char *) malloc(sz * 3 + 1); if (!buf) { MessageBox(NULL, "malloc() error.", "DLL", MB_OK | MB_ICONERROR); return; } if (!p) { MessageBox(NULL, "Argment is NULL.", "DLL", MB_OK | MB_ICONERROR); return; } for (i = 0; i < sz; i++) { sprintf(buf + i * 3, "%02X,", p[i] & 0xff); } buf[sz * 3] = '\0'; MessageBox(NULL, buf, "DLL", MB_OK | MB_ICONINFORMATION); free(buf); return; } ; peekstatic.def LIBRARY PeekStatic EXPORTS DumpMem DumpMem の表示結果ですが (16進数で表示) 1回目の呼び出し 00,00,00,00,00,00,00,00,00,00,00,00,00,00,00,00, … 初期化前 AA,AA,AA,AA,**,**,**,**,**,**,**,**,FF,FF,00,00, … 初期化後 2回目の呼び出し AA,AA,AA,AA,00,00,00,00,00,00,00,00,FF,FF,00,00, … ast 域が、、、 AA,AA,AA,AA,00,00,00,00,00,00,00,00,FF,FF,00,00, # 先頭の 4バイトは dummy As Long の領域 # つづく 8バイトは ast() As String (※) # つづく 2バイトは bInit As Boolean (True は -1(&HFFFF)) # # ※ SAFEARRAY構造体へのポインタ? (64ビットのアドレス) 1回目の呼び出しでは、5バイト目から 8バイトの領域に アドレス(らしきもの)が入りました(「**,**, …」の部分)。 2回目の呼び出しでは、dummy、bInit の値は保持されている ものの、アドレスらしき値は初期化されてしまっています。 ★★ のコードを入れると問題なく実行できてしまうのですが、 1回目の ☆ の行の代入処理の実行後、 sk さんが指摘されたとおり、End Function へ到達時点で ast の内容はすでに初期化されているようで、 半平太がおっしゃるように、Staticの属性を失念してしまう ような振る舞いとなっています(Dim と同様毎回初期値に)。 # だからどうした給湯室ネタか!! |
![]() |
投稿日時: 22/12/30 12:44:35
投稿者: たらのり
|
---|---|
こんにちは
|