委托,动作和内存分配

12

我目前正在处理一些需要最小内存分配的代码。

我注意到如果我使用一个方法作为参数,编译器会更改代码并创建一个新的Action,但如果我使用匿名方法,编译器会创建一个委托块。我知道新的Action会分配内存,但是对于委托,我不太确定它是否会分配内存。当使用委托时,委托会分配内存吗?

我的代码:

bool workfinished = false;

void DoWork()
{
    MyMethod(CallBack1Work, ()=>{ workfinished = false;});
}

void MyMethod(Action callback1, Action callback2)
{
}

void CallBack1Work()
{
}

编译器版本:

bool workfinished = false;

void DoWork()
{
    MyMethod(new Action( CallBack1Work ), delegate{ workfinished = false;});
}

void MyMethod(Action callback1, Action callback2)
{
}

void CallBack1Work()
{
}
        
void DoWork_b01()
{
    workfinished = false;
}

请看这个链接:http://diditwith.net/2007/03/19/TheProblemWithDelegates.aspx 还有这个链接:https://dev59.com/h0fRa4cB1Zd3GeqP6BEC - huMpty duMpty
你能否将 MyMethod 修改为接受一个 Action<ContainingClass> 的参数?这样可以提供更多的缓存选项... - Jon Skeet
那是一个选项,我不太关心内存的数量,我更关心生成垃圾的情况,比如如果DoWork方法被调用了一百万次,会分配多少内存。我需要运行测试,但从专家那里听到这些总是很好的 :) - TimD
我也在尝试最小化分配。我观察到在Debug模式下会分配一个Action<T>,但在Release模式下不会。(我使用的是Visual Studio 2015,在64位Windows上运行.NET 4.5.2。) - yoyo
2个回答

22

如果您使用一个不捕获任何变量的lambda表达式,编译器将生成一个静态字段来缓存它。因此,您可以将Action更改为Action<YourClass>并使用this进行调用。所以:

class YourClass
{
    private bool workFinished;

    public void DoWork()
    {
        MyMethod(instance => instance.Callback1Work(),
                 instance => instance.workFinished = false);
    }

    private void MyMethod(Action<YourClass> callback1,
                          Action<YourClass> callback2)
    {
        // Do whatever you want here...
        callback1(this);
        // And here...
        callback2(this);
    }

    private void Callback1Work()
    {
       // ...
    }
}

这将仅在第一次在任何实例上调用DoWork时创建委托实例。然后,这些委托将被缓存以供所有实例的未来调用。

诚然,这都是一个实现细节。您始终可以使其更清晰:

class YourClass
{
    private static readonly Action<YourClass> Callback1 = x => x.Callback1Work();
    private static readonly Action<YourClass> Callback2 = x => x.workFinished = false;

    private bool workFinished;

    public void DoWork()
    {
        MyMethod(Callback1, Callback2);
    }

    ... code as before ...
}

在你采取任何这些措施之前,值得实际对代码进行分析和基准测试。

另一种选择是坚持使用Action,但为委托创建实例变量-只要在同一个实例上多次调用DoWork,那么你就没问题了:

class YourClass
{
    private readonly Action foo;
    private readonly Action bar;

    private bool workFinished;

    public YourClass()
    {
        foo = Callback1Work;
        bar = () => workFinished = false;
    }

    public void DoWork()
    {
        MyMethod(foo, bar);
    }

    public void MyMethod(Action callback1, Action callback2)
    {
        ...
    }

    private void Callback1Work()
    {
        ...
    }
}

11
无论你是显式地使用new SomeDelegate,还是省略它,无论你使用lambda表达式、delegate关键字还是传递一个方法组,或者任何你没有展示的可能的解决方案,都会创建一个委托对象。编译器通常可以推断出它应该在那里,因此它不会强制你打出来;但是无论如何,委托的创建仍然会发生。(嗯,技术上讲,你可以传递null并且不分配对象,但是那么你永远无法完成任何工作,所以我认为可以忽略这种情况。)
每个选项之间唯一真正不同的内存分配是,在给定的匿名方法块中,你正在闭合一个变量(workfinished)。为了创建该闭包,运行时将生成自己的类型来存储闭包的状态,创建该类型的实例,并将其用于委托,因此所有使用匿名方法的解决方案都会创建一个新对象。(虽然只是很小的一个对象,在大多数情况下不会特别昂贵。)

6
值得注意的是,当没有捕获上下文时,编译器会缓存和重用单个委托实例以用于 lambda 表达式。 - Marc Gravell
谢谢,我一直以为delegate关键字实际上是创建一个新的delegate对象,但我并不100%确定。 - TimD
是的,让人困惑的是有一个方法正在被创建,所以我不确定它是一次性分配还是什么。看起来会像callback2_b01这样。 - TimD
@TimD:每次调用将会分配一个内存单元。虽然很小,但还是存在的。 - Jon Skeet
2
我提供了一种替代方案,该方案尚未被展示,不会在每次调用时创建一个委托... - Jon Skeet

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接