例子:
// Potentially large struct.
struct Foo
{
public int A;
public int B;
// etc.
}
Foo[] arr = new Foo[100];
如果 Foo 是一个 100 字节的结构,在执行以下语句的过程中会在内存中复制多少字节:
int x = arr[0].A
也就是说,将 arr[0] 评估为某个临时变量(Foo 实例的 100 字节副本),然后将 .A 复制到变量 x 中(4 字节副本)。
或者编译器、JITer 和 CLR 的某种组合能够优化这个语句,使得
A
的 4 个字节直接复制到x
.如果执行了优化,当项目保存在
List<Foo>
中时它是否仍然有效?或者当数组作为 IList<Foo>
传递时或 ArraySegment<Foo>
?
最佳答案
值类型是按值复制的——因此得名。因此,我们必须考虑什么时候必须对值进行复制。这归结为正确分析特定实体何时引用变量或值。如果它引用一个值,那么该值是从某个地方复制的。如果它引用一个变量,那么它只是一个变量,可以像任何其他变量一样对待。
假设我们有
struct Foo { public int A; public int B; }
暂时忽略这里的设计缺陷;公共(public)字段是一种不好的代码气味,可变结构也是如此。
如果你说
Foo f = new Foo();
怎么了?规范说:
f
被 build 。 temp
被 build 。 temp
用八字节的零填充。 temp
复制到 f
. 但实际情况并非如此。编译器和运行时足够聪明,可以注意到所需的工作流和“创建
f
并用零填充它”的工作流之间没有明显的区别,因此会发生这种情况。这是一个复制省略优化。EXERCISE:设计一个编译器不能复制省略的程序,并且输出清楚地表明编译器在初始化结构类型的变量时不执行复制省略。
现在如果你说
f.A = 123;
然后
f
被评估以产生一个变量——而不是一个值——然后从那个 A
被评估以产生一个变量,并且四个字节被写入该变量。如果你说
int x = f.A;
然后 f 被评估为一个变量,
A
被评估为变量,A
的值写入x
.如果你说
Foo[] fs = new Foo[1];
然后变量
fs
被分配,数组被分配并用零初始化,对数组的引用被复制到fs
.当你说fs[0].A = 123;
和之前一样。
f[0]
被评估为一个变量,所以 A
是一个变量,因此 123 被复制到该变量。当你说
int x = fs[0].A;
和以前一样:我们评估
fs[0]
作为变量,从该变量中获取 A
的值, 并复制它。但如果你说
List<Foo> list = new List<Foo>();
list.Add(new Foo());
list[0].A = 123;
那么你会得到一个编译器错误,因为
list[0]
是一个值,而不是一个变量。你不能改变它。如果你说
int x = list[0].A;
然后
list[0]
被评估为一个值 -- 生成存储在列表中的值的副本 -- 然后是 A
的副本制造于 x
.所以这里有一个额外的副本。练习:编写一个程序来说明
list[0]
是存储在列表中的值的副本。 正是出于这个原因,您应该 (1) 不要制作大结构,并且 (2) 使它们不可变。结构是按值复制的,这可能很昂贵,而且值不是变量,因此很难改变它们。
What makes array indexer return a variable but list indexer not? Is array treated in a special way?
是的。数组是非常特殊的类型,它深深地构建在运行时中,并且从版本 1 开始就存在。
这里的关键特性是数组索引器在逻辑上为数组中包含的变量生成别名;然后可以将该别名用作变量本身。
所有其他索引器实际上是一对 get/set 方法,其中 get 返回一个值,而不是一个变量。
Can I create my own class to behave the same as array in this regard
在 C# 7 之前,不在 C# 中。您可以在 IL 中执行此操作,但当然 C# 将不知道如何处理返回的别名。
C# 7 增加了方法返回变量别名的能力:
ref
返回。请记住,ref
(和 out
)参数将变量作为其操作数,并导致被调用者拥有该变量的别名。 C# 7 也为本地人和返回添加了执行此操作的能力。
关于c# - 结构类型数组的性能,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/43685417/