[[20230201105935]] 『標準、クラス、ユーザーフォームそれぞれの変数の』(農民A) ページの最後に飛ぶ

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

 

『標準、クラス、ユーザーフォームそれぞれの変数の持ち方』(農民A)

 プロシージャ外に書かれた変数の持ち方について教えてほしいです
 標準モジュールのプロシージャ外に宣言された変数は基本的に自分で初期化したりブックを閉じたりエラーが発生したりしない限り入力された値を保持していると思いますが、ブックを閉じたりエラーが発生した際に保持していた値はちゃんと初期化されているのでしょうか?
 復帰時に再度メモリ内に容量をとって前回確保した分はそのまま放置でどんどんメモリを喰っていくような挙動にはなっていなんでしょうか?
 またクラスモジュールやUserFormは unload や Set クラス = Nothing するとプロシージャ外に書かれた変数の領域を解放すると思い込んでいるのですが、本当にそういう動作になっているんでしょうか?

< 使用 Excel:Microsoft365、使用 OS:Windows11 >


 >標準モジュールのプロシージャ外に宣言された変数は基本的に自分で初期化したり
 >ブックを閉じたりエラーが発生したりしない限り入力された値を保持していると思いますが
 過去にもこういう話合いがあったと思いますが、プロシジャから出た段階で保持されているかは保証されないです。
 いつ消えてもおかしくないです。
 なので、セルに直接書き込んだり、外部のテキストに保持させたりする方法が安全というのが一般的です。

 メモリと値の保持は別々に考えたほうが良いかと。
 メモリの開放については、詳しく知っているわけではないですが・・・
 マシンスペックが低かった時代は、ちゃんと解放したほうが良いみたいな記述はどっかで見ましたが
 現代において気にするほど占有することはないかと思います。

 >またクラスモジュールやUserFormは unload や Set クラス = Nothing するとプロシージャ外に書かれた変数の領域を解放する
 これは意味が分かりません。
 クラスもUserFormも一つのオブジェクトですので、インスタンスが終了するときには「値は保持していない」と思います。
 メモリについては先に述べた通りわかりません。

(稲葉) 2023/02/01(水) 12:45:45


 >クラスモジュールやUserFormは unload や Set クラス = Nothing するとプロシージャ外に書かれた変数の領域を解放する

 以下は、私の理解です。まちがってたら指摘してください。詳しい方。
 ・オブジェクト変数は、インスタンスへの参照です
 ・変数の領域とは別に、インスタンスが確保しているメモリ領域があります。
 ・変数にNothingを代入すると、変数そのものが確保しているメモリ漁期は解法されます。
 ・インスタンスは、インスタンスを参照してる変数などの総数(参照カウント)が0になると解放されます
 ・インスタンスを参照しているオブジェクト変数にNothingを代入すると、インスタンスの参照カウントが1つ減ります
 結論
 >クラスモジュールやUserFormは unload や Set クラス = Nothing するとプロシージャ外に書かれた変数の領域を解放する
 これは、必ずしも正しくない。 そのインスタンスを参照している変数などがただ一つであるときに限って正しい 

 '------------ Class1
    Private m_name
    Public Property Get name() As String
       name = m_name
    End Property
    Public Property Let name(newvalue As String)
       m_name = newvalue
    End Property

 '------------- 標準モジュール ----------------
 Sub test()
    Dim c1 As Class1, c2 As Class1
    Set c1 = New Class1
    Set c2 = c1   ' 同じインスタンスを参照する

    c1.name = "C1"
    Set c1 = Nothing
    Debug.Print c1.name    ' 参照が切れてるからエラー

    Debug.Print c2.name    ' 成功=インスタンスが残っている
    Set c2 = Nothing       ' これでClass1のインスタンスへの参照カウントは0になったはず
    Debug.Print c2.name    ' 参照がきれてるからエラー インスタンスが解放されているかどうかを調べる方法は???

 End Sub
(´・ω・`) 2023/02/01(水) 14:42:30

失礼します

標準モジュールのプロシージャ外に宣言された変数は基本的に自分で初期化したりブックを閉じたりエラーが発生したりしない限り入力された値を保持していると思いますが、ブックを閉じたりエラーが発生した際に保持していた値はちゃんと初期化されているのでしょうか? ブックを閉じたり・・・ですが、終了時に初期化とは?
メモリの解放と読めばよいのでしょうか?

復帰時に再度メモリ内に容量をとって前回確保した分はそのまま放置でどんどんメモリを喰っていくような挙動にはなっていなんでしょうか? 復帰時とは、
ブックを閉じて再度開くことと、
Excelのエラー終了から再度ブックを開くこと、
の2パターンを想定されてますか?
確認ですが、エラーとはどの程度のことを想定してますか?
コーディングミス程度のマクロエラー?
Excelが強制終了?
Windowsが強制終了?

またクラスモジュールやUserFormは unload や Set クラス = Nothing するとプロシージャ外に書かれた変数の領域を解放すると思い込んでいるのですが、本当にそういう動作になっているんでしょうか? (´・ω・`)さんのコードをお借りして、Class1のプロシージャ外に100MBとかのメモリを確保し、10個newするなどすれば1GBメモリ使用量が増えますよね。
10個newまたはnothingするsubを作成し、ステップ実行しながらタスクマネージャで全体(Excel?)のメモリ使用量をみていれば確認できませんかね。

(初心者) 2023/02/01(水) 16:52:49


 今回このような質問をした意図として
 私はプロシージャ外に変数を持つことを極端に嫌い、変数を共有する際も引数として渡すことがほとんどだったんですが
 それでは非効率だと思いなおしプロシージャの外に変数を宣言した際の動作について知ろうと質問させてもらいました
 しかし下手にグローバル変数を宣言すると意図しないところで変数を使用してしまったり(普段Explicitを入れてかつグローバル変数使わないのでエラーで気づける)初期化を忘れたりしないようにクラス内で複雑な処理を書いてプロシージャ内で宣言しながら使っていこうと思いましたがここら辺の動きを確認したく投稿しました

 初心者さんのお話ですが
 >>メモリの解放と読めばよいのでしょうか?
 ブックを閉じたときの動作では多分それでいいと思います
 (ブックを閉じたあともExcelアプリケーションが動作し続けている状態でのグローバル変数の扱いを把握していないので)

 >>復帰とは
 Excelを閉じて開くことを想定しています

 >>確認ですが、エラーとはどの程度のことを想定してますか?
 コーティングミス程度のものを想定しています
 コーティングミスでエラーが発生した際グローバル変数から値が消える時と消えないときがあるんですけどあれは何なんですかね?
 消えたものはどこに行くのか・・・

 >>UserFormは unload や Set クラス = Nothing 〜
 そうですねこちら確認しようと思います
(農民A) 2023/02/01(水) 17:22:19

コーティングミスでエラーが発生した際グローバル変数から値が消える時と消えないときがあるんですけどあれは何なんですかね?
消えたものはどこに行くのか・・・

ここでのエラーで前回値が残ってるのはたまたまとしか言えないと思います。
Windowsはそのまで詳しくないですが、例えばExcel終了時、変数等は初期化されないと思ってた方がいいと思います。
例えるなら、その土地の所有権を放棄したのです。
畑を作ってた状態で、所有権を放棄して、時が経ってその土地の所有権を取り戻したとして、畑の野菜等は残ってないのと同じと考えたらわかりやすいかな?
誰かがその土地の所有権を取得して、駐車場にしたら畑はなくなります。
前回値が残ってたのはたまたまその土地がずっと空き地だったんでしょう。
消えたものは消えたままです。

プログラムのスタックとか学ぶといいかも?です。
(初心者) 2023/02/01(水) 18:43:04


 >今回このような質問をした意図として

 グローバル変数の保持に関して、理解することは大事ですね。
 ただ、引数で渡せる変数を グローバル変数に置き換えることが
 効率化なのでしょうか。
 コーディング上、さして変わらないかと思いますが、
 農民Aさんのコードを見ていないので、なんとも。

 以前見かけた初学者のコードでは、シートのとあるセルの値何点かを
 それぞれ変数に代入し、その変数を引数で渡していました。
 シートを渡し、渡されたプロシージャ内で値を取得すれば、
 コードもスッキリで、イメージもしやすいですよね。

 プログラムの堅牢性から、元々の考えが正統かと思いますよ。
(tkit) 2023/02/02(木) 08:27:26

 プログラムのスタックとか学ぶといいかも
 そちら調べてみます

 tkitさん
 置き換えたいなとおもっているものはデータベースから取得したレコードセットやcsvやシートから取得した配列データ、あとは各動作を分岐させるための設定値等ですね
 配列は巨大なものを扱うときもある為引数で渡すと遅くなる
 設定値はせっかくクラス化するならプロパティで参照できるように持てばいいよねくらいの認識です
(農民A) 2023/02/02(木) 09:05:04

 なんだか私のコメントはよく無視されるんだけどまぁいいとして

 >配列は巨大なものを扱うときもある為引数で渡すと遅くなる
 配列を引数にするとき、基本ByRefだから、大きさは関係ないでしょう
 配列をVaiant型変数に入れて、ByValで渡せば遅くなるでしょうけど

 正しい知識に基づかないと正しい選択はできないですよ
(´・ω・`) 2023/02/02(木) 09:15:09

 (´・ω・`)さんの投稿いつも勉強にしてます!

 参照渡しの件も、↑で提示いただいたクラスで説明できそうだったので試したのですが、
 挙動がよくわからなくなりました・・・
 値渡しでクラスを渡せば、インスタンスが複製されるもんだと思っていたのですが、
  同じオブジェクトを別のインスタンス(値?)で参照しているだけなので、
  オブジェクトの書き換えを行うと元のインスタンスでも書き換わる
  ただし、Nothingした場合は、新たに参照に使ったインスタンスが解放されるだけなので、
  元のオブジェクトは呼び元のインスタンスが生きているので影響なし?

 参照渡しの場合は、書き換えられるのは当然として、Nothingしたときはオブジェクト毎削除される

 この辺り、認識として合ってます?

 '------------ Class1
    Private m_name
    Public Property Get name() As String
       name = m_name
    End Property
    Public Property Let name(newvalue As String)
       m_name = newvalue
    End Property

    Private Sub Class_Terminate()
        Debug.Print "クラスターミネート:"; m_name
    End Sub

'------------ 標準モジュール

    Sub test()
       Dim c1 As Class1, c2 As Class1
       Set c1 = New Class1
       Set c2 = c1   ' 同じインスタンスを参照する
       c1.name = "C1"
       値渡し c1
       Debug.Print "メインProc値渡し後:"; c1.name '←値渡し先で値を変えているのに、元のインスタンスにも反映される
       参照渡し c1
       Debug.Print "メインProc参照渡し後:"; c1.name '←参照渡し先で渡しているので、クラスの値が変わっているのはわかる
    End Sub
    Sub 参照渡し(ByRef c3 As Class1)
        c3.name = "C3"
        Debug.Print "参照渡し:"; c3.name

    End Sub
    Sub 値渡し(ByVal c4 As Class1)
        c4.name = "C4"
        Debug.Print "値渡し:"; c4.name
    End Sub 'この段階でクラスが解放されているわけではない・・・?

(稲葉) 2023/02/02(木) 10:35:49


 ちょっと話がそれてしまって元質問者さんには申し訳ないです

 なんか同じような議論が昔あったような気がうっすらするのですが、探せません

 Classとかオブジェクトの変数は、もともとが「参照」みたいなものですから、
 ByValでも参照先を保存しているメモリをコピーしているようなものなので、
 同じインスタンスを参照しますよね

 ByValだと何が出来ないか(何が保護されるか)というと、
 こんなテスト用マクロで分かるでしょうか。
 参照先のインスタンスを入れ替えることが出来ないということです

    Sub test()
       Dim c1 As Class1, c2 As Class1
       Set c1 = New Class1
       c1.name = "C1"
       Set c2 = New Class1
       c2.name = "C2"
       Debug.Print "C1="; c1.name; vbTab; "C2="; c2.name

       SwapByVal c1, c2
       Debug.Print "C1="; c1.name; vbTab; "C2="; c2.name

       SwapByRef c1, c2
       Debug.Print "C1="; c1.name; vbTab; "C2="; c2.name

    End Sub

    Sub SwapByVal(ByVal a As Class1, ByVal b As Class1)
       Dim tmp As Class1
       Set tmp = a
       Set a = b
       Set b = tmp
    End Sub

    Sub SwapByRef(ByRef a As Class1, ByRef b As Class1)
       Dim tmp As Class1
       Set tmp = a
       Set a = b
       Set b = tmp
    End Sub
(´・ω・`) 2023/02/02(木) 11:13:00

 >置き換えたいなとおもっているものはデータベースから取得したレコードセットやcsvやシートから取得した配列データ、
 >あとは各動作を分岐させるための設定値等ですね
 >配列は巨大なものを扱うときもある為引数で渡すと遅くなる

 実験しました。
 私の環境で、要素数100Mで0.1秒程でした。
 中身次第でもっと掛かりますね。

 Sub hogehoge()
    Dim src(100000, 1000) As Long
    Dim temp
    Dim t: t = Timer
    temp = src
    Debug.Print Timer - t
 End Sub

 (´・ω・`)さんの指摘に従いましょう。

 個人的に、データ、オブジェクトを引数で渡し、
 必要なデータの取得方法を工夫したり、
 プログレスバーを自作して、経過をみせたりしますね。

 まぁ、自分で使うのか、人に使ってもらうのかでも
 変わりますが。

(tkit) 2023/02/02(木) 13:15:59


 早速ありがとうございます!
 よくわかりました。
 参照を入れ替えることって滅多にないと思うので、通常はByValで行っても
 新たにインスタンスが発生するわけではないということですね。

 あとは当たり前ですが、配列に入れたものを別々にSwapしても、配列順番は入れ替わらない
 配列渡しは、配列でないVariant型を除き、値渡しができない

 (´・ω・`) 2023/02/02(木) 09:15:09のご指摘通り、Variant型の変数に入れた配列は値渡しができるが
 コピーになってしまうので、参照渡しが良い、という感じでしょうか。
    Sub test2()
        Dim cAry(1) As Class1
        Set cAry(0) = New Class1
        Set cAry(1) = New Class1
        cAry(0).name = 0
        cAry(1).name = 1

        '配列の一部を参照で渡しても、戻ってきたときには元の配列のまま
        SwapByRef cAry(0), cAry(1)
        Debug.Print "c0 ="; cAry(0).name; vbTab; "c1 ="; cAry(1).name

        SwapByVal cAry(0), cAry(1)
        Debug.Print "c0 ="; cAry(0).name; vbTab; "c1 ="; cAry(1).name

        'そもそも配列(配列になっていないVariant型を除く)の場合は、値渡しができない
        SwapByRefAry cAry
        Debug.Print "c0 ="; cAry(0).name; vbTab; "c1 ="; cAry(1).name
    End Sub
    Sub SwapByRefAry(ByRef ary() As Class1)
       Dim tmp As Class1
       Set tmp = ary(0)
       Set ary(0) = ary(1)
       Set ary(1) = tmp
    End Sub

     Sub test3()
        Dim tbl As Variant
        tbl = [{1,2,3,4}]
        tbl = 加算ByVal(tbl)
        Debug.Print Join(tbl, vbTab)

        tbl = [{1,2,3,4}]
        Call 加算ByRef(tbl)
        Debug.Print Join(tbl, vbTab)
    End Sub
    Function 加算ByVal(ByVal tbl As Variant) As Variant
        Dim i As Long
        For i = 1 To 4
            tbl(i) = tbl(i) + 10
        Next i
        加算ByVal = tbl
    End Function
    Sub 加算ByRef(ByRef tbl As Variant)
        Dim i As Long
        For i = 1 To 4
            tbl(i) = tbl(i) + 100
        Next i
    End Sub

 言葉のニュアンスは違和感あるかもしれませんが、動作に関して疑問解消されました。
 ありがとうございました。
(稲葉) 2023/02/02(木) 13:18:45

 >'配列の一部を参照で渡しても、戻ってきたときには元の配列のまま
 ちょっと話がよくわからないですが
 以下、実行したらちゃんとソートされませんか?

   Sub test3()
     Dim c() As Class1
     ReDim c(1 To 5)
     For i = 1 To 5
         Set c(i) = New Class1
         c(i).name = i
     Next
     For i = 1 To 5
         Debug.Print Format(i, """C""(0)="); c(i).name; vbTab;
     Next
     Debug.Print
     Sort c
     For i = 1 To 5
         Debug.Print Format(i, """C""(0)="); c(i).name; vbTab;
     Next
     Debug.Print
   End Sub

   Sub Sort(ByRef cary() As Class1)
     For i = LBound(cary) To UBound(cary) - 1
         For j = LBound(cary) + 1 To UBound(cary) - i + 1
             If cary(j).name > cary(j - 1).name Then
               SwapByRef cary(j), cary(j - 1)
             End If
         Next
     Next
   End Sub

   Sub SwapByRef(ByRef a As Class1, ByRef b As Class1)
       Dim tmp As Class1
       Set tmp = a
       Set a = b
       Set b = tmp
   End Sub
(´・ω・`) 2023/02/02(木) 14:28:10

 訂正事項
 > あとは当たり前ですが、配列に入れたものを別々にSwapしても、配列順番は入れ替わらない
 >>'配列の一部を参照で渡しても、戻ってきたときには元の配列のまま
 上記2点は誤り

 >以下、実行したらちゃんとソートされませんか?
 私の勘違いでした。
 ちゃんと入れ替わっておりました。 申し訳ないです。

(稲葉) 2023/02/02(木) 16:08:36


コメント返信:

[ 一覧(最新更新順) ]


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