😺

デリゲートに登録したラムダ式を-=で消す

2025/02/13に公開2

前提

Unity/C#でデリゲートを使用する際、ラムダ式を用いることで、デリゲートのみで使う関数の宣言を減らし、
コードを簡潔にすることができます。

using UnityEngine;
using System;

public class DelegateSample : MonoBehaviour
{
    public Action MyAction = default;

    void Start()
    {
        MyDelegate += () => Debug.Log("Hello!");

        MyDelegate?.Invoke();
    }
}

このような書き方はよくしますが、デリゲートへの関数の二重登録を防ぐために、-= で登録を解除することもあります。
普通の関数は-=でデリゲートへの登録を解除できますが、ラムダ式に対して-=を使うことは許されるのか、直感的にはわからなかったため実験しました。

実験1

デリゲートに登録したラムダ式を-=で解除する

public class DelegateTest : MonoBehaviour
{
    public event Action MyAction = default;

    void Start()
    {
        MyAction += () => Debug.Log("action called");
        MyAction -= () => Debug.Log("action called");

        MyAction += () => Debug.Log("action called");

        MyAction?.Invoke();
    }
}

このコードを実行して、ログが一度だけ表示されれば、意図した通りにデリゲートからラムダ式が削除できていることになります。

結果は上のようになりました。2回出力されているので、ラムダ式は-=演算子で削除できていないようです。

実験2

実験2

もう少し詳しく調べてみます。
以下のソースコードを用いました。

using UnityEngine;
using System;

public class DelegateEquivalence : MonoBehaviour
{
    void Start()
    {
        Action action1 = DebugFunc;
        Action action2 = DebugFunc;
        Debug.Log($"method equality: {action1 == action2}");

        Action action3 = () => Debug.Log("action called");
        Action action4 = () => Debug.Log("action called");
        Debug.Log($"lambda equality: {action3 == action4}");
    }

    void DebugFunc()
    {
        Debug.Log("action called");
    }
}

デリゲートは等値演算子が使えるようなので、これを利用しました。
https://learn.microsoft.com/ja-jp/dotnet/csharp/language-reference/language-specification/expressions#12129-delegate-equality-operators

結果は以下のようになりました。
通常のメソッドをデリゲートに登録すると、デリゲートは同値と判定されますが、ラムダ式を登録すると、デリゲートは異なると判定されました。

実験3

実験3

UnityEventでも調べてみます。
AddListenerRemoveListenerを使っています。

using UnityEngine;
using UnityEngine.Events;

public class UnityEventTest : MonoBehaviour
{
    public UnityEvent MyEvent = new UnityEvent();

    void Start()
    {
        MyEvent.AddListener(() => Debug.Log("unityEvent called"));
        MyEvent.RemoveListener(() => Debug.Log("unityEvent called"));
        MyEvent.AddListener(() => Debug.Log("unityEvent called"));

        MyEvent?.Invoke();
    }
}

結果はやはり、以下のように2回ログが出力されました。
UnityEventでも、ラムダ式をRemoveListenerで削除することができないようです。

結論(だったもの)

ラムダ式をデリゲートに登録した場合、-=演算子で削除することはできません。
UnityEventでも同様です。

デリゲートに登録された全ての関数を削除して良いのであれば、MyAction = default;のようにデリゲートを初期化することで、全ての関数を削除できます。
UnityEventの場合は、MyEvent.RemoveAllListeners()を使えば良いでしょう。

私が調べた範囲では、デリゲートに登録した特定のラムダ式を指定し削除する方法はなさそうでした。個人的にそこまでラムダ式にこだわる理由はないので、素直に通常の関数として宣言し、デリゲートへ登録・解除すれば良いかなと考えています。

追記:コメントで、変数に入れて-=すれば消えると教えていただいたので、試してみました。
結果として、自分の無知を痛感することとなりました。

また、タイトルを
"デリゲートに登録したラムダ式は-=で消せない"
から
"デリゲートに登録したラムダ式を-=で消す"
へ変更しました

実験4

ラムダ式を一度変数に入れてから-=で削除・追加する。

using UnityEngine;
using System;

public class DelegateTest2 : MonoBehaviour
{
    private Action MyAction = default;

    void Start()
    {
        for (int i = 0; i < 3; i++)
        {
            SetDelegate();
        }
    }

    private void SetDelegate()
    {
        Debug.Log("set delegate");

        Action action = () => Debug.Log("action called");
        MyAction -= action;
        MyAction += action;

        MyAction?.Invoke();
    }
}

実験1の結果から、ラムダ式は宣言のたびに新しいものが生成されると考えていました。
従って上のコードでは、ループを回すたびに新しい、異なる関数が生成され、従って

        MyAction -= action;

の行では、前のループで代入された関数が消えことなく残ってしまい、MyActionには複数の関数が登録されてしまうと考えていました。

実際に実行してみると、以下のように、3回ループを回しても、各ループでは1回だけaction calledが出力されました。-=でラムダ式を消去するという、所望の動作ができていることがわかりました。

理由

https://ufcpp.net/study/csharp/sp2_anonymousmethod.html
こちらの記事は匿名関数についての解説ですが、おそらくラムダ式も同様の仕組みで動いていると思われます。

私はラムダ式が、実行時にインスタンスが作られるような物と考えていましたが、実態は関数であり、コンパイル時に普通の関数として展開されているそうです。

例えば、以下のようなコードは、

class MyClass : MonoBehaviour
{
    void Method()
    {
        Action action = () => { Debug.Log("Hello"); };
    }
}

コンパイル時に次のように展開されるわけです(逆コンパイル等をしたわけではないので、実際と幾分違いがあると思います。詳細は上記ページを参照してください。)

class MyClass : MonoBehaviour
{
    void Method()
    {
        Action action = AnonymousMethod;
    }

    private void AnonymousMethod()
    {
        Debug.Log("Hello");
    }
}

代入・消去のたびにラムダ式を宣言する場合

そう考えると、以下のように代入・消去のたびにラムダ式を宣言する場合(実験1に近い設定)には

public class MyClass : MonoBehaviour
{
    public event Action MyAction = default;

    void Start()
    {
        MyAction -= () => Debug.Log("action called");
        MyAction += () => Debug.Log("action called");

        MyAction?.Invoke();
    }
}

下に示すように、同型で実態が別の関数として展開されることになります。
結果、-=を使ってラムダ式を解除しようとしても、解除できません。

public class DelegateTest : MonoBehaviour
{
    private Action MyAction = default;

    void Start()
    {
        MyAction -= AnonymousMethod1;
        MyAction += AnonymousMethod2;

        MyAction?.Invoke();
    }

    void AnonymousMethod1()
    {
        Debug.Log("action called");
    }

    void AnonymousMethod2()
    {
        Debug.Log("action called");
    }
}

変数に入れてから代入・消去する場合

実験4のように、ラムダ式を変数に入れてから代入・消去する場合は、

public class MyClass3 : MonoBehaviour
{
    private Action MyAction = default;

    void Start()
    {
        Action action = () => Debug.Log("action called");
        MyAction -= action;
        MyAction += action;

        MyAction?.Invoke();
    }
}

以下のように展開されると考えられます。
結果、仮にStartメソッドが複数回呼ばれたとしても、参照されている関数はAnonymousMethodで不変なので、-=でラムダ式を解除することができます。

public class MyClass3 : MonoBehaviour
{
    private Action MyAction = default;

    void Start()
    {
        Action action = AnonymousMethod;
        MyAction -= action;
        MyAction += action;

        MyAction?.Invoke();
    }

    private void AnonymousMethod()
    {
        Debug.Log("action called");
    }
}

このように考えることで、実験2及び実験3の結果も説明できると思います。

まとめ

ラムダ式は、コンパイル時に普通の関数として展開されます。
従って、同じ記述のラムダ式を繰り返し書くと、それぞれが別の関数として扱われてしまい、デリゲートの追加・削除が意図通りに動作しないことがあります。

ラムダ式を複数回使用したい場合には、宣言は一箇所で行い、変数(デリゲート)に代入してから使用することで、追加・削除を問題なく行えるようになります。

指摘をいただくまで誤った情報を発信してしまい、申し訳ありませんでした🙇
誤りや説明不足があれば、コメントでご指摘いただければ幸いです。

Discussion

junerjuner

デリゲートに登録した特定のラムダ式を指定し削除する方法はなさそうでした。

変数に入れて -= すれば消えるのでは……?

おみずおみず

ご指摘をいただいて、もう一度ちゃんと調べたところ、おっしゃる通り変数に入れて-=すると消すことができました。
私の理解が不正確で、誤った情報を書いてしまい反省しています...
本文を更新しましたので、他に誤り等あれば、ご指摘いただけますと幸いです...!