C# 在编写迭代器方法时避免复制大型结构参数

C# 在编写迭代器方法时避免复制大型结构参数,c#,iterator,C#,Iterator,我有一个大型结构,我知道从分析,是昂贵的复制。我使用关键字中的传递此结构的实例,效果非常好 现在我想把它作为一个参数传递给迭代器方法,迭代器方法本身将值传递给其他迭代器方法,但是不允许在中使用,这意味着每次将值传递给方法时都会复制该值 这里的上下文是一个包含视频游戏保存状态的结构,迭代器方法是一种“加载”方法,它在Unity游戏引擎(使用迭代器实现协同程序)中将保存数据的处理扩展到多个帧。荷载法很复杂,因此需要将其分解为几种方法 例如: struct SaveData{ // large

我有一个大型结构,我知道从分析,是昂贵的复制。我使用关键字中的
传递此结构的实例,效果非常好

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

这里的上下文是一个包含视频游戏保存状态的结构,迭代器方法是一种“加载”方法,它在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
}
我理解为什么一般情况下,
中的
不允许迭代器使用(例如,如果迭代器/协同程序超过了值的所有者怎么办?),因此我可以理解为什么最外层的迭代器函数需要一个副本。但是对于内部调用,因为它们是用
产生返回的
调用的,所以内部迭代器不会比内部迭代器更久,所以似乎应该有某种方法在
中使用

这里有没有我缺少的语言特性,或者我可以用一个很好的模式来解决这个问题?我认为用外部类包装这个类型会有用,但是它看起来有点混乱,当然仍然需要一个副本,因为我不能在
成员中拥有
ref

但是对于内部调用,因为它们是用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
()

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


但是,
internal
调用得更早。编译器生成了完整执行的
内部
的实现,并在上面生成内部
之后返回生成的
IEnumerator
。因此
internal
需要将变量
i
存储在编译器生成的类中的某个位置,这就是为什么它不能在

如果它太大,并且到处都通过ref传递它,那么基本上将它视为存储在托管堆之外的某个位置的引用类型。为什么不能将其转换为引用类型?这其中的每一点都在喊“值类型是错误的选择”。如果您需要它成为值类型,那么您必须将
Load
方法作为正确的状态机来实现,而不是依赖编译器为您生成状态机。然后每次调用时都可以将SaveData中的
传递到状态机的
MoveNext
方法中。@madreflection-“存储在托管堆以外的地方”是一句非常有用的话!你说得对,因为它太大了,把它放在书堆上太疯狂了。所以很明显,我应该用引用类型来包装它,我可以不使用“in”来传递它。不过,这并不像在任何地方都使用引用类型那么简单——外部SaveData结构将包含许多部分,如果它们都是引用类型,这将导致堆碎片(这是一个实际观察到的问题,因此我对结构如此着迷!)。我的目标是使用少量的大面积内存。@canton7谢谢!我根本没有考虑过这一点——我将检查Unity是否支持接受参数到
MoveNext
的协同程序,或者我是否可以自己调用IEnumerator方法。我有一种感觉,是Unity调用了
MoveNext
,但可能有一种方法可以绕过它。
Unity
确实调用了
MoveNext
,并且没有办法让C编译器生成一个迭代器方法,该迭代器方法有一个接受任何参数的
MoveNext
方法。我认为没有必要把
SaveData
放在包装类中:为什么不把它变成一个类呢?没有人建议它的所有字段也成为类。这非常有趣,谢谢!不知何故,我已经使用IEnumerator很多年了,但我没有考虑到第一次调用实际上不会执行任何代码!我刚刚检查了Unity的功能,发现它在启动协同程序时总是调用一次
MoveNext
,这可能就是我不知道迭代器的原因。在任何情况下-似乎给出了您的答案,在语言中没有任何方法可以实现这一点,相反,我必须通过包装我的类型来解决它。这是一个众所周知的问题,这意味着如果您从迭代器方法中抛出例如
ArgumentExceptions
,除非有人真的试图列举它们,否则它们不会被抛出,这很糟糕。这就是为什么大多数
IEnumerable
-返回方法将执行参数检查,然后执行
返回内部(args)
,其中
internal
是另一种使用
产生返回的方法,以确保在调用out方法时运行参数检查,而不是当
IEnumerable
第一次被列举为有趣的东西时——谢谢!