当通过 in 参数传递方法时,可变结构体是否被复制?

Are mutable structs copied when passed through methods via in parameters?

提问人:Ray 提问时间:11/4/2019 最后编辑:Ray 更新时间:6/20/2022 访问量:433

问:

我想尽量减少数学库中结构体的复制,并阅读有关 C# 7.2 修饰符的信息,尤其是将其与可变结构一起使用时的警告。in

碰巧我有这个可变结构:

public struct Quaternion
{
    public float W;
    public float X;
    public float Y;
    public float Z;
}

到目前为止,该库有这样的方法,其中参数通过以下方式传递:ref

public static void Dot(ref Quaternion left, ref Quaternion right, out float result)
    => result = left.W * right.W + left.X * right.X + left.Y * right.Y + left.Z * right.Z;

从 MSDN 文档中,我了解到,如果我将这些参数更改为参数,只要我只访问可变结构的字段,就不会发生防御性复制,因为编译器会看到我没有修改可变结构:in

public static void Dot(in Quaternion left, in Quaternion right, out float result)
    => result = left.W * right.W + left.X * right.X + left.Y * right.Y + left.Z * right.Z;

第一个问题:我对这种行为的理解正确吗?

第二个,愚蠢的问题:如果在其中一个接受 struct 作为 in 参数的方法中,如果我调用另一个接受它们作为 in 参数的方法,编译器会复制它吗?举个例子:

public static void Lerp(in Quaternion start, in Quaternion end, float amount,
    out Quaternion result)
{
    float inv = 1.0f - amount;
    if (Dot(start, end) >= 0.0f) // will 2 copies be created here?
    {
        result.W = inv * start.W + amount * end.W;
        result.X = inv * start.X + amount * end.X;
        result.Y = inv * start.Y + amount * end.Y;
        result.Z = inv * start.Z + amount * end.Z;
    }
    else
    {
        result.W = inv * start.W - amount * end.W;
        result.X = inv * start.X - amount * end.X;
        result.Y = inv * start.Y - amount * end.Y;
        result.Z = inv * start.Z - amount * end.Z;
    }
    result.Normalize();
}

我很确定它不应该创建副本 - 否则我将如何阻止来自调用端的副本?但我不确定,我最好在制造混乱之前先问问。


补遗

我想更改为:refin

  • (static) readonly字段(例如特定的常数四元数)不能作为参数传递。ref
  • 我无法指定运算符参数,但我可以使用 .refin
  • 在呼叫站点上不断指定是丑陋的。ref
  • 我知道我必须在任何地方更改呼叫站点,但这没关系,因为这个库只会在内部使用。
c# 结构 可变 C#-7.2 中 in参数

评论

0赞 Marc Gravell 11/4/2019
在一般情况下,可能会导致多个副本,因为运行时需要强制执行调用方法等操作不会产生副作用。如果类型被标记为 ,则它信任不更改类型的方法 - 但是:您的方案是非值类型。在您的情况下,您不是在调用方法 - 只是访问字段;所以。。。它变得不那么清晰了inreadonly structreadonly
0赞 Zohar Peled 11/4/2019
为什么一开始就使用可变结构?我见过的所有关于结构的最佳实践建议都谈到了使它们不可变......
0赞 500 - Internal Server Error 11/4/2019
您关心的是性能还是语义?
1赞 Ray 11/5/2019
@ZoharPeled 我过去使用的 C# 3D 数学库通常不使用不可变结构,这也可能是因为它们当时在 C# 中不存在。我实际上在 Matrix 类型上尝试过这个。仍然有太多的代码只是想改变矩阵的一个字段,所以我保留了可变的字段。不过,它可能只适用于四元数,但为了这个问题,假设我必须坚持使用用户期望的可变四元数。
0赞 Ray 11/5/2019
@500-InternalServerError 性能在这里更为重要。如果不能保证不复制它们,我可能只能忍受.我看到了一个机会,可以在这里使用一个新功能,语义不那么冗长,而且显然没有性能成本,因为我只访问字段。ref

答:

0赞 Ray 6/18/2022 #1

如注释中所述,如果运行时不能保证传递的实例不被修改,则使用 for 可变结构的参数可以创建防御性副本。如果在该实例上调用属性、索引器或方法,则可能很难保证这一点。in

因此,每当您不打算修改这样的实例时,您应该通过使它们明确说明这一点。这样做的好处是,如果您尝试修改其中的实例,也会导致编译失败。readonly

特别注意关键字在以下示例中的位置:readonly

public struct Vec2
{
    public float X, Y;

    // Properties
    public readonly float Length
    {
        get { return MathF.Sqrt(LengthSq); }
    }
    public readonly float LengthSq => X * X + Y * Y;

    // Indexers (syntax the same for properties if they also have setter)
    public float this[int index]
    {
        readonly get => index switch
        {
            0 => X,
            1 => Y,
            _ => throw ...
        };
        set
        {
            switch (index)
            {
                case 0: X = value; break;
                case 1: Y = value; break;
                default: throw ...
            }
        }
    }

    // Methods
    public readonly override int GetHashCode() => HashCode.Combine(X, Y);
}

现在,每当您有一个与修饰符一起使用的方法时,您都可以安全地调用上述内容,而无需进行复制。Vec2in

(此功能是在 C# 8.0 中引入的,当我提出问题时不可用。)