C#

时间:2017-02-14 09:02:02

标签: c#

(这不是What is the difference between a field and a property in C#的副本 - 而是一个更微妙的问题)

我有一个不可变类Camera(来自this Ray Tracing示例),它可以正常工作:

public class Camera
{
    public Vector3D Pos; 
    public Vector3D Forward; 
    public Vector3D Up;
    public Vector3D Right;

    public  Camera(Vector3D pos, Vector3D lookAt)
    {
        Pos = pos;
        Forward = lookAt - pos;
        Forward.Normalize();
        Vector3D Down = new Vector3D(0, -1, 0);
        Right = Vector3D.CrossProduct(Forward, Down);
        Right.Normalize();
        Right *= 1.5;
        Up = Vector3D.CrossProduct(Forward, Right);
        Up.Normalize();
        Up *= 1.5;
    }
}

由于这是一个不可变的类,我想将字段更改为只读属性,这是我在整个应用程序中完成的,没有问题,因此:

public Vector3D Pos { get; private set; } 
public Vector3D Forward { get; private set; }
public Vector3D Up { get; private set; }
public Vector3D Right { get; private set; }

其余代码保持不变 - 但应用程序不再正常工作。经过调查,Forward,Up和Right的值不再标准化。 Normalize,在构造函数(上面)中调用3次是一个void方法。看起来在属性上而不是在字段上(至少在构造函数中)调用这样的方法不会导致更新属性的值。它似乎似乎是通过引用而不是通过值传递向量(结构),如果这有意义的话。

我能找到的唯一方法就是在设置属性值之前在构造函数中完成所有变换,如下所示:

public  Camera(Vector3D pos, Vector3D lookAt)
{
    var forward = lookAt - pos;
    forward.Normalize();
    var down = new Vector3D(0, -1, 0);
    var right = Vector3D.CrossProduct(forward, down);
    right.Normalize();
    right *= 1.5;
    var up = Vector3D.CrossProduct(forward, right);
    up.Normalize();
    up *= 1.5;
    Pos = pos;
    Forward = forward;
    Up = up;
    Right = right;
}

任何人都可以向我解释这里到底发生了什么?我了解如果您在get或set 中添加任何其他行为,属性和字段之间存在差异,但我不是。

3 个答案:

答案 0 :(得分:1)

这是可变值类型的众多问题之一,以及为什么应该避免它们。

在您的第一个场景中:

public Vector3D Forward; 
Forward.Normalize();

存储在变量Forward中的Vector3D本身的实例。因此,当您致电Forward()时,您正在改变Forward中存储的值,一切都按预期工作。

但是,在以下代码中:

private Vector3D forward;

public Vector3D Forward
{
     get { return forward; }
     private set { forward = value; }
}

Forward.Normalize();

Normalize在存储在字段forward中的值的副本上调用(请记住,C#中的默认行为是按值传递参数)。因为值类型的变量值本身就是值类型实例,所以您要做的是改变副本,而不是forward中存储的值。

在您的情况下,属性是自动生成的,但原理是相同的。自动生成的属性也有一个支持字段,编译器只是为您节省了所涉及的所有管道的麻烦。

答案 1 :(得分:0)

  

如果有任何意义,它几乎看起来好像是通过引用而不是通过值传递向量(结构)。

不,实际问题"是,它是通过价值传递的。您实际上是在您的财产的获取者创建的复制Normalize上调用Vector3D。原始值只能通过设置器进行更改(例如,通过包含作业的*=)。

  

我了解如果您在get或set中添加任何其他行为,则属性和字段之间存在差异,但我不是。

这是不正确的。总有一点不同。您的"内联属性"只是一个支持字段的包装器,然后由您的属性的getter和setter(如果提供)调用。如果您获得结构或基元类型,则只复制该值。

答案 2 :(得分:0)

原因是当您操作某个属性时,您正在操作该值的副本。假设你的类看起来像是这样的属性:

public class Camera
{
    public Vector3D Pos { get; private set; }
    public Vector3D Forward { get; private set; }
    public Vector3D Up { get; private set; }
    public Vector3D Right { get; private set; }

    public Camera(Vector3D pos, Vector3D lookAt)
    {
        Pos = pos;
        Forward = lookAt - pos;
        Forward.Normalize();
        Vector3D Down = new Vector3D(0, -1, 0);
        Right = Vector3D.CrossProduct(Forward, Down);
        Right.Normalize();
        Right *= 1.5;
        Up = Vector3D.CrossProduct(Forward, Right);
        Up.Normalize();
        Up *= 1.5;
    }
}

构造函数中对属性的调用实际上是调用名为get_Forward的方法。此方法返回调用Vector3D的{​​{1}}的副本。查看Normalize()的IL代码:

Forward.Normalize()

ldarg.0 call UserQuery+Camera.get_Forward stloc.1 ldloca.s 01 call System.Windows.Media.Media3D.Vector3D.Normalize 方法定义如下:

call UserQuery+Camera.get_Forward

指令ldarg.0 ldfld UserQuery+Camera.<Forward>k__BackingField ret 将字段的值压入堆栈,因为在这种情况下它是ldfld的实例,这个值被复制,从而产生一个新的System.ValueType实例获取调用它的Vector3D方法(Normalize()stloc.1指令存储并加载此副本)。这意味着该字段的原始值保持不变。