是否可以在 C# 中创建"perfect forwarding"泛型集合?

Is it possible to create a "perfect forwarding" generic collection in C#?

本文关键字:perfect forwarding 集合 泛型 创建 是否      更新时间:2023-10-16

请考虑以下C++代码片段:

std::vector<Foo> bar;
bar.emplace_back(9001);    //Foo defines a constructor that takes int

这将创建一个类型为Foo的对象,传递任何构造函数参数,并将新对象存储在向量中。在功能上等同于bar.push_back(Foo(9001));,但效率更高,因为没有创建临时Foo

有没有办法在 C# 中实现类似的功能?

如果是这样,它会像C++那样提供任何真正的好处,还是只是句法糖

编辑:这个问题专门涉及C#(结构)中的值类型集合;我正在寻找的好处(除了简化的Add语法)类似于C++11emplace方法-无需构造,复制和销毁(GC)临时对象。

是的,这是可能的,但它是..繁琐。

在 .Net 中,结构总是被复制的。任何以任何方式传递结构的尝试都将导致复制:

SuperList x;
x.Add(new MyStruct());

此外,这个实际上可能会创建至少 3 个实例:

  • 您创建的
  • 复制到 Add 方法
  • 在 Add 方法将写入的列表数据存储区内复制

您现在可以使用工厂删除其中的一些

SuperList x;
x.Add(() => new MyStruct());

但随后:

  • 你得到了一个由工厂创建的
  • 并在工厂将写入的列表数据存储区内复制

因此,如果您尝试以任何方式将新实例传递到存储中,它将被复制。

我能想到的唯一不涉及制作任何副本的方法就是允许商店为您创建它。最简单的例子是..:

SuperList x;
x.AddNewItems(1);

不管商店如何实现它。假设它足够聪明,可以不做任何复制。例如,它可以使用一种 LinkedList<>并只附加一个新节点。

这样,通过要求商店为您创建对象,商店就有机会只创建项目。当然,它必须正确实现,但这是另一个话题。新项目可能会使用所有默认值创建,除非您使用更多参数和一些特定逻辑详细说明CreateItem,但是..假设使用默认值创建它是可以的。

现在怎么办?当然,现在在处理(即读取、修改等)该项目时存在问题:

class SuperList
{
MyStruct GetItem(int id);
}

好吧,这显然会返回一份。哎呀。

如果您使用的是 C# 7.0,您可以使用 ref 变量,也许,我还没有尝试过:

class SuperList
{
ref MyStruct GetItem(int idx);
}
ref MyStruct aRef = x.GetItem(x.Count-1);
aRef.Name = "mom";

但除非是 C#7,否则你不能并且必须绕开很长的路:

class SuperList
{
delegate void ItemAccessor(ref MyStruct y); // <- REF! no copy
void WorkWithItem(int idx, ItemAccessor func);
}
x.WorkWithItem(1, (ref MyStruct it) => {
it.Name="mom";
});

最后,由于我们可以创建一个商店并可以以某种方式访问其项目,因此您可以做任何您想将其包装在瓷层中的事情,以提供任何用户前端来创建和初始化这些对象。例如,您可以使用"构造函数"方法创建一个 API:

var x = new SuperList<T,U,...>( (ref item, T t, U u, ..) => {
item.First = t;
item.Second = u;
...
});
x.CreateItem(new T(), new U(), ...);

或者通过object[] args和反射来做到这一点。无论什么。使用或实施或两者兼而有之会很麻烦,我会考虑很多次这样做。class比使用struct方便得多。但如果真的需要,所有这些都是可能的。

啊,是的,还有问题的第二部分:

如果是这样,它会提供任何真正的好处,就像C++一样,还是只是句法糖?

当然,根据定义,为什么好处是:不复制。这意味着,如果对象很大,您将获得性能优势,这是显而易见的,假设复制需要大量时间

另一方面,如果对象很小,由于传递ref指针、额外的顺从、调用方法、创建委托实例等,您将受到性能影响。对于小型结构,可能会发现您构建的基础结构只需要更多时间(而不是空间!)来调用,而不是简单地来回复制该结构。这是您在进行此类优化时应考虑的事情。

我实际上不知道一个结构需要多大才能获得好处。复制结构很快,他们注定要这样做。它实际上几乎是原始的逐字节复制,处理器可以非常快速地完成。

我记得许多关于访问、GC'inc、复制等值类型和引用类型的性能测试。在互联网上搜索,并确保阅读他们测量的代码,因为有很多错误可能会扭曲结果。无论如何,对于复制structs,这完全取决于它们的字节大小和复制内存块的速度。

所以......有一个大的结构......让我们的智能列表有价值。在结构内放什么?海量数据?不,结构会很小,它会保留对大数组的引用:

struct X { public int[] f; }
static void Main()
{
X a;a.f = new []{5};
X b;b = a;
a.f[0]=4;
Console.WriteLine("Hello " + b.f[0]); // FOUR!
}

如果struct内的int[]实际上是一个引用,那么保留在结构中的几乎所有内容通常都是占用空间较小的小引用。您需要一个具有大量字段struct和/或其中大多数字段本身需要是繁重的非数组值类型。这不是你经常看到的。分解"大值类型"的想法并将结构分解为引用部分太容易了,正如您在int[]示例中所看到的那样。