VB のたまご

作成日: 2017/03/27, 更新日: 2017/03/27


イベントに関するメモリリークを学ぶ3

  •  前回の続きです。

  •  【回収できない不要メモリ】を生成しないような対策はどうすればいいのでしょうか。 もうすでに想像できているかもしれませんが、イベント登録をしたらイベント解除する!です。面倒くさがらずに確実に解放処理をする!です。 以下に、C# / VB それぞれ記載します。

  • スポンサーリンク


  •  C# の場合

  • using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace ConsoleApplication4
    {
        // 疑似ボタンコントロールのつもり、クリックイベントを持つ
        class DummyButton
        {
            public event EventHandler Click;
    
            public void RaiseClick()
            {
                var handler = Click;
                if (handler != null)
                    handler(this, EventArgs.Empty);
            }
        }
    }
    

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace ConsoleApplication4
    {
        // 疑似画面クラスのつもり、ボタンのクリックイベント処理をしたいため、イベントハンドラを持つ
        class DummyForm
        {
            public void DummyButton_Click(object sender, EventArgs e)
            {
                Console.WriteLine("DummyForm クラスの DummyButton_Click メソッドが呼ばれました");
            }
        }
    }
    

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace ConsoleApplication4
    {
        // 疑似画面クラスのつもり、ボタンのクリックイベント処理をしたいため、イベントハンドラを持つ
        // イベントの登録、解除を自分でやる
        class DummyForm2 : IDisposable
        {
            private DummyButton buttonControl = null;
    
            // コンストラクタ
            public DummyForm2(DummyButton buttonControl)
            {
                this.buttonControl = buttonControl;
                this.buttonControl.Click += DummyButton_Click;
            }
    
            // ボタンをクリックした時におこないたい処理
            public void DummyButton_Click(object sender, EventArgs e)
            {
                Console.WriteLine("DummyForm2 クラスの DummyButton_Click メソッドが呼ばれました");
            }
    
            // 破棄
            public void Dispose()
            {
                this.buttonControl.Click -= DummyButton_Click;
            }
            
        }
    }
    

    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Text;
    using System.Threading.Tasks;
    
    namespace ConsoleApplication4
    {
        class Program
        {
            static void Main(string[] args)
            {
                // 不要メモリ回収とその後のメモリ使用量を取得
                Action doClean = () => Console.WriteLine($"{GC.GetTotalMemory(true).ToString("N")} bytes");
    
    
                // イベントに関するメモリリークは、どうやって対応すればいいのでしょう?
                // プログラムを終了する
                // 現実的ではない解決方法なので NG
    
                // 単純に、イベント登録処理だけではなくイベント解除処理も書きます。
                {
                    var ctrl = new DummyButton();
                    for(var i = 0; i < 10; i++)
                    {
                        var frm = new DummyForm();
                        ctrl.Click += frm.DummyButton_Click;
                        ctrl.Click -= frm.DummyButton_Click;
                        doClean();
                    }
    
                    ctrl.RaiseClick();
                }
                Console.WriteLine("");
    
                // ちなみにラムダ式の場合
                {
                    // 同じイベントハンドラを書いてセットしたとしても、参照先が違うため、解除されない
                    var ctrl1 = new DummyButton();
                    ctrl1.Click += (sender, e) => Console.WriteLine("ラムダ");
                    ctrl1.Click -= (sender, e) => Console.WriteLine("ラムダ");
                    doClean();
                    ctrl1.RaiseClick();
                    Console.WriteLine("");
    
                    // 解除したい場合はいったん変数にセットしてから使う
                    EventHandler handler = (sender, e) => Console.WriteLine("ラムダ2");
                    var ctrl2 = new DummyButton();
                    ctrl2.Click += handler;
                    ctrl2.Click -= handler;
                    doClean();
                    ctrl2.RaiseClick();
                    Console.WriteLine("");
    
                }
    
                // イベント登録時にイベント解除もセットできない場合
                // 動的にイベント登録したいシナリオの場合は、大抵こっちなはず
    
                // for 文の中に宣言した変数を、クラス変数とかに退避しておき、後でイベント解除
                {
                    var handlerList = new List<DummyForm>();
                    var ctrl = new DummyButton();
                    for (var i = 0; i < 10; i++)
                    {
                        var frm = new DummyForm();
                        ctrl.Click += frm.DummyButton_Click;
                        handlerList.Add(frm);
                        // 後で手動での全解除なので今のタイミングでは以下は効かない
                        doClean();
                    }
    
                    // 全解除
                    handlerList.ForEach(x => ctrl.Click -= x.DummyButton_Click);
    
                    // イベントは実行される?
                    ctrl.RaiseClick();
    
                    Console.WriteLine("破棄した後");
                    doClean();
                    Console.WriteLine("");
                }
                Console.WriteLine("");
    
                // イベントハンドラを持つクラス側で、イベント登録とイベント解除を自分自身でおこなう機能を持たせる(IDisposable を実装)
                // イベント登録・・・インスタンス生成時に実行されるコンストラクタ内で
                // イベント解除・・・Dispose メソッドを呼んでその中で
                // ここでも前例と同じように「イベント解除処理を後から実行する」ことなので、面倒くささも前例と同じ
                // ただ、IDisposable インターフェースを実装したクラスなら何でも管理できるので、前例よりは柔軟性があるかも
                {
                    var disposableList = new List<IDisposable>();
                    var ctrl = new DummyButton();
                    for(var i = 0; i < 10; i++)
                    {
                        var frm = new DummyForm2(ctrl);
                        disposableList.Add(frm);
                        // 後で手動での全解除なので今のタイミングでは以下は効かない
                        doClean();
                    }
    
                    // 全解除
                    disposableList.ForEach(x => x.Dispose());
    
                    // イベントは実行される?
                    ctrl.RaiseClick();
    
                    Console.WriteLine("破棄した後");
                    doClean();
                    Console.WriteLine("");
    
                }
                Console.WriteLine("");
    
    
    
                Console.Read();
            }
        }
    }
    

    スポンサーリンク


  •  VB の場合

  • ' 疑似ボタンコントロールのつもり、クリックイベントを持つ
    Public Class DummyButton
    
        Public Event Click As EventHandler
    
        Public Sub RaiseClick()
    
            RaiseEvent Click(Me, EventArgs.Empty)
    
        End Sub
    
    End Class
    

    ' 疑似画面クラスのつもり、ボタンのクリックイベント処理をしたいため、イベントハンドラを持つ
    Public Class DummyForm
    
        Public Sub DummyButton_Click(sender As Object, e As EventArgs)
    
            Console.WriteLine("DummyForm クラスの DummyButton_Click メソッドが呼ばれました")
    
        End Sub
    
    End Class
    

    ' 疑似画面クラスのつもり、ボタンのクリックイベント処理をしたいため、イベントハンドラを持つ
    ' イベントの登録、解除を自分でやる
    Public Class DummyForm2
        Implements IDisposable
    
        Private _buttonControl As DummyButton = Nothing
    
        ' コンストラクタ
        Public Sub New(buttonControl As DummyButton)
    
            Me._buttonControl = buttonControl
            AddHandler Me._buttonControl.Click, AddressOf DummyButton_Click
    
        End Sub
    
        ' ボタンをクリックした時におこないたい処理
        Public Sub DummyButton_Click(sender As Object, e As EventArgs)
    
            Console.WriteLine("DummyForm2 クラスの DummyButton_Click メソッドが呼ばれました")
    
        End Sub
    
    #Region "IDisposable Support"
        Private disposedValue As Boolean ' 重複する呼び出しを検出するには
    
        ' IDisposable
        Protected Overridable Sub Dispose(disposing As Boolean)
            If Not disposedValue Then
                If disposing Then
                    ' TODO: マネージ状態を破棄します (マネージ オブジェクト)。
    
                    ' イベント解除
                    RemoveHandler Me._buttonControl.Click, AddressOf DummyButton_Click
    
                End If
    
                ' TODO: アンマネージ リソース (アンマネージ オブジェクト) を解放し、下の Finalize() をオーバーライドします。
                ' TODO: 大きなフィールドを null に設定します。
            End If
            disposedValue = True
        End Sub
    
        ' TODO: 上の Dispose(disposing As Boolean) にアンマネージ リソースを解放するコードが含まれる場合にのみ Finalize() をオーバーライドします。
        'Protected Overrides Sub Finalize()
        '    ' このコードを変更しないでください。クリーンアップ コードを上の Dispose(disposing As Boolean) に記述します。
        '    Dispose(False)
        '    MyBase.Finalize()
        'End Sub
    
        ' このコードは、破棄可能なパターンを正しく実装できるように Visual Basic によって追加されました。
        Public Sub Dispose() Implements IDisposable.Dispose
            ' このコードを変更しないでください。クリーンアップ コードを上の Dispose(disposing As Boolean) に記述します。
            Dispose(True)
            ' TODO: 上の Finalize() がオーバーライドされている場合は、次の行のコメントを解除してください。
            ' GC.SuppressFinalize(Me)
        End Sub
    #End Region
    
    End Class
    

    Module Module1
    
        Sub Main()
    
            ' 不要メモリ回収とその後のメモリ使用量を取得
            Dim doClean As Action = Sub() Console.WriteLine($"{GC.GetTotalMemory(True).ToString("N")} bytes")
    
    
            ' イベントに関するメモリリークは、どうやって対応すればいいのでしょう?
            ' プログラムを終了する
            ' 現実的ではない解決方法なので NG
    
            ' 単純に、イベント登録処理だけではなくイベント解除処理も書きます。
            With Nothing
    
                Dim ctrl = New DummyButton
                For i As Integer = 0 To 10
    
                    Dim frm = New DummyForm
                    AddHandler ctrl.Click, AddressOf frm.DummyButton_Click
                    RemoveHandler ctrl.Click, AddressOf frm.DummyButton_Click
                    doClean()
    
                Next
    
                ctrl.RaiseClick()
    
            End With
            Console.WriteLine()
    
            ' ちなみにラムダ式の場合
            With Nothing
    
                ' 同じイベントハンドラを書いてセットしたとしても、参照先が違うため、解除されない
                Dim ctrl1 = New DummyButton
                AddHandler ctrl1.Click, Sub(sender, e) Console.WriteLine("ラムダ")
                RemoveHandler ctrl1.Click, Sub(sender, e) Console.WriteLine("ラムダ")
                doClean()
                ctrl1.RaiseClick()
                Console.WriteLine()
    
                ' 解除したい場合はいったん変数にセットしてから使う
                Dim handler As EventHandler = Sub(sender, e) Console.WriteLine("ラムダ2")
                Dim ctrl2 = New DummyButton
                AddHandler ctrl2.Click, handler
                RemoveHandler ctrl2.Click, handler
                doClean()
                ctrl2.RaiseClick()
                Console.WriteLine()
    
            End With
            Console.WriteLine()
    
            ' イベント登録時にイベント解除もセットできない場合
            ' 動的にイベント登録したいシナリオの場合は、大抵こっちなはず
    
            ' for 文の中に宣言した変数を、クラス変数とかに退避しておき、後でイベント解除
            With Nothing
    
                Dim handlerList = New List(Of DummyForm)
                Dim ctrl = New DummyButton
                For i As Integer = 0 To 10
    
                    Dim frm = New DummyForm
                    AddHandler ctrl.Click, AddressOf frm.DummyButton_Click
                    handlerList.Add(frm)
                    ' 後で手動での全解除なので今のタイミングでは以下は効かない
                    doClean()
    
                Next
    
                ' 全解除
                handlerList.ForEach(Sub(x) RemoveHandler ctrl.Click, AddressOf x.DummyButton_Click)
    
                ' イベントは実行される?
                ctrl.RaiseClick()
    
                Console.WriteLine("破棄した後")
                doClean()
                Console.WriteLine()
    
            End With
            Console.WriteLine()
    
            ' イベントハンドラを持つクラス側で、イベント登録とイベント解除を自分自身でおこなう機能を持たせる(IDisposable を実装)
            ' イベント登録・・・インスタンス生成時に実行されるコンストラクタ内で
            ' イベント解除・・・Dispose メソッドを呼んでその中で
            ' ここでも前例と同じように「イベント解除処理を後から実行する」ことなので、面倒くささも前例と同じ
            ' ただ、IDisposable インターフェースを実装したクラスなら何でも管理できるので、前例よりは柔軟性があるかも
            With Nothing
    
                Dim disposableList = New List(Of IDisposable)
                Dim ctrl = New DummyButton
                For i As Integer = 0 To 10
    
                    Dim frm = New DummyForm2(ctrl)
                    disposableList.Add(frm)
                    ' 後で手動での全解除なので今のタイミングでは以下は効かない
                    doClean()
    
                Next
    
                ' 全解除
                disposableList.ForEach(Sub(x) x.Dispose())
    
                ' イベントは実行される?
                ctrl.RaiseClick()
    
                Console.WriteLine("破棄した後")
                doClean()
                Console.WriteLine()
    
            End With
            Console.WriteLine()
    
    
            Console.Read()
        End Sub
    
    End Module
    

    出力結果
    86,528.00 bytes
    90,816.00 bytes
    90,816.00 bytes
    90,816.00 bytes
    90,816.00 bytes
    90,828.00 bytes
    90,828.00 bytes
    90,828.00 bytes
    90,828.00 bytes
    90,828.00 bytes
    
    90,904.00 bytes
    ラムダ
    
    90,968.00 bytes
    
    91,100.00 bytes
    91,196.00 bytes
    91,248.00 bytes
    91,292.00 bytes
    91,368.00 bytes
    91,412.00 bytes
    91,456.00 bytes
    91,500.00 bytes
    91,608.00 bytes
    91,652.00 bytes
    破棄した後
    91,288.00 bytes
    
    
    91,408.00 bytes
    91,504.00 bytes
    91,556.00 bytes
    91,600.00 bytes
    91,676.00 bytes
    91,720.00 bytes
    91,764.00 bytes
    91,808.00 bytes
    91,916.00 bytes
    91,960.00 bytes
    破棄した後
    91,436.00 bytes
    

  •  このように、イベントの解放処理を明示的に命令することで、【回収できない不要メモリ】の生成を防ぎ、メモリリークへの成長を防ぐことにつながります。 += したら -= する / AddHandler したら RemoveHandler する、必ず2つで1つです。

  •  最後まで読んでいただき、ありがとうございました。

  • スポンサーリンク