组合迭代器方法时避免复制大型结构参数

问题描述

我有一个从分析中得知的大型结构,复制起来很昂贵。我正在使用 in 关键字传递这个结构的实例,效果很好。

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

这里的上下文是一个包含视频游戏保存状态的结构体,迭代器方法一个“加载”方法,它在 Unity 游戏引擎(它使用迭代器实现协程)中将保存数据的处理分布在多个帧上.加载方法比较复杂,需要分解为多种方法

示例:

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 不允许用于迭代器(例如,如果迭代器/协程比值的所有者更持久呢?) - 所以我可以理解为什么最外层迭代器函数需要一个副本。但是对于内部调用,由于它们是用 yield return 调用的,因此内部迭代器不会比内部迭代器更持久,因此似乎应该有某种方法可以使用 in

这里是否有我遗漏的任何语言功能,或者我可以使用一个很好的模式来解决它?我认为用外部类包装类型会起作用,但它看起来有点混乱,当然仍然需要一个副本,因为我不能有 refin 成员。

解决方法

但是对于内部调用,由于它们是通过 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,就在上面的 Yielding Inner 之后。所以 Inner 需要将变量 i 存储在编译器生成的类中的某处,这就是它不能是 in 的原因。