提问者:小点点

编写迭代器方法时避免大结构参数的复制


我有一个很大的结构,我知道从剖析,是昂贵的复制。我正在使用in关键字传递这个结构的实例,效果很好。

现在我想把它作为一个参数传递给一个迭代器方法,而迭代器方法本身将值传递给其他迭代器方法--但是不允许使用in,这意味着每次将值传递给一个方法时都会复制该值。

这里的上下文是一个包含视频游戏保存状态的结构,迭代器方法是一个“加载”方法,它在Unity game engine(使用迭代器实现协同)中将保存数据的处理扩展到几个帧上。load方法比较复杂,因此需要将其分解为几种方法。

示例:

struct SaveData{
    // large data
}

// Async loading - can spread processing across frames (yay!) but copies lots of data (boo!)

IEnumerator LoadAsync(SaveData saveData) {// wish I could use 'in' here!
    // use some part of saveData
    yield return;
    // use more of saveData
    yield return InnerLoad(saveData); // wish I could use 'in' here!
}

IEnumerator InnerLoadAsync(SaveData saveData) {// wish I could use 'in' here!
    // use saveData
    yield return;
}


// Synchronous loading - very efficient (yay!) but blocks, causing an unacceptably long delay (boo!)

void LoadSynchronous(in SaveData saveData){
    // use some part of saveData
    // use more of saveData
    InnerLoadSynchronous(in saveData);
}

void InnerLoadSynchronous(in SaveData saveData){
    // use saveData
}

我理解为什么一般情况下in不允许用于迭代器(例如,如果迭代器/coroutine的持续时间超过值的所有者,该怎么办?)-所以我可以看到为什么最外层的迭代器函数需要一个副本了。但是对于内部调用,由于它们是用yield return调用的,所以内部迭代器的使用时间不会超过内部迭代器,所以似乎应该有某种方法在中使用

这里有没有我遗漏的语言特性,或者我可以使用一个很好的模式来解决它?我认为用一个外部类包装该类型是可行的,但这似乎有点混乱,当然仍然需要一个副本,因为我不能有refin成员。


共1个答案

匿名用户

但是对于内部调用,由于它们是用yield return调用的,内部迭代器的使用时间不会超过内部,所以似乎应该有一些方法可以使用。

你少了点什么。我们举一个简单的例子:

public class C
{
    public static void Main()
    {
        var enumerator = Outer(3);

        Console.WriteLine("Enumerating 1");
        enumerator.MoveNext();

        Console.WriteLine("Enumerating 2");
        enumerator.MoveNext();

        var innerEnumerator = (IEnumerator)enumerator.Current;

        Console.WriteLine("Enumerating Inner 1");
        innerEnumerator.MoveNext();
    }
    
    public static IEnumerator Outer(int i)
    {
        yield return null;
        Console.WriteLine("Yielding Inner");
        yield return Inner(i);
    }
    
    public static IEnumerator Inner(int i)
    {
        Console.WriteLine($"Inner {i}");
        yield break;   
    }
}

这将打印:

Enumerating 1
Enumerating 2
Yielding Inner
Enumerating Inner 1
Inner 3

(SharpLab)。

正如您所看到的,inner不是直接枚举的。编译器生成的inner实现将编译器生成的IEnumerable返回给outer的调用方,直到该调用方显式调用MoveNext时,才执行inner的主体。

但是,调用inner的时间要早得多。编译器生成的inner实现完全执行,并返回生成的IEnumerator,就在上面的产生inner之后。因此inner需要将变量i存储在编译器生成的类中的某个位置,这就是它不能是in的原因。