作为属性访问和作为字段访问结构体的区别及修改方式

5

好的,我将开始我的问题,说我理解可变结构背后的恶意,但我正在使用SFML.net并使用许多Vector2f和类似的结构体。

我不明白的是为什么我可以在一个类中拥有并更改一个字段的值,却无法在同一个类中进行相同的属性操作。

看看这段代码:

using System;

namespace Test
{
    public struct TestStruct
    {
        public string Value;
    }

    class Program
    {
        TestStruct structA;
        TestStruct structB { get; set; }

        static void Main(string[] args)
        {
            Program program = new Program();

            // This Works
            program.structA.Value = "Test A";

            // This fails with the following error:
            // Cannot modify the return value of 'Test.Program.structB'
            // because it is not a variable
            //program.structB.Value = "Test B"; 

            TestStruct copy = program.structB;
            copy.Value = "Test B";

            Console.WriteLine(program.structA.Value); // "Test A"
            Console.WriteLine(program.structB.Value); // Empty, as expected
        }
    }
}

注意:我将构建自己的类来覆盖相同的功能并保持可变性,但我无法看到为什么我可以做一件事却不能做另一件事的技术原因。

2个回答

13

当你访问一个字段时,你正在访问实际的结构体。当你通过属性访问它时,你调用了一个返回存储在该属性中的内容的方法。对于值类型结构体来说,你将得到结构体的副本。显然,该副本不是变量,不能被更改。

C#语言规范5.0的第1.7节“结构体”说:

对于类,两个变量可能引用同一对象,因此对一个变量的操作可能会影响另一个变量引用的对象。对于结构体,每个变量都有自己的数据副本,一个变量上的操作不可能影响另一个变量。

这解释了为什么会收到结构体的副本而无法修改原始结构体。然而,它并没有描述为什么不允许修改。

规范的第11.3.3节:

当结构体的属性或索引器是赋值目标时,与属性或索引器访问相关联的实例表达式必须分类为变量。如果实例表达式分类为值,则会在编译时出错。这在§7.17.1中有进一步描述。

因此,从get访问器返回的"thing"是值而不是变量。这解释了错误消息中的措辞。

规范还包含一个示例,位于第7.17.1节,与你的代码几乎完全相同:

考虑以下声明:

struct Point
{
    int x, y;
    public Point(int x, int y) {
        this.x = x;
        this.y = y;
    }
    public int X {
        get { return x; }
        set { x = value; }
    }
    public int Y {
        get { return y; }
        set { y = value; }
    }
}
struct Rectangle
{
    Point a, b;
    public Rectangle(Point a, Point b) {
        this.a = a;
        this.b = b;
    }
    public Point A {
        get { return a; }
        set { a = value; }
    }
    public Point B {
        get { return b; }
        set { b = value; }
    }
}

在这个例子中

Point p = new Point();
p.X = 100;
p.Y = 100;
Rectangle r = new Rectangle();
r.A = new Point(10, 10);
r.B = p;

对于 p 和 r 是变量的情况,p.X,p.Y,r.A 和 r.B 的赋值是允许的。然而,在这个例子中:

Rectangle r = new Rectangle();
r.A.X = 10;
r.A.Y = 10;
r.B.X = 100;
r.B.Y = 100;

所有的作业都是无效的,因为r.A和r.B不是变量。


谢谢你,Abel。你的解释(返回一个值就像复制它一样)正是我在寻找的技术细节。 - NemoStein

7
尽管属性看起来像变量,但每个属性实际上都是一个get方法和/或set方法的组合。通常情况下,属性get方法会返回某个变量或数组槽中的副本,而put方法会将其参数复制到该变量或数组槽中。如果想执行类似于someVariable = someObject.someProeprty;someobject.someProperty = someVariable; 这样的操作,则不必担心这些语句最终会被执行为var temp=someObject.somePropertyBackingField; someVariable=temp;var temp = someVariable; someObject.somePropertyBackingField = temp;。与此相反,有一些使用字段可以完成但无法使用属性完成的操作。
如果一个对象George公开了一个名为Field1的字段,那么代码可以将George.Field作为ref或out参数传递给另一个方法。此外,如果Field1的类型是一个具有公开字段的值类型,则尝试访问这些字段将访问存储在George内部的结构的字段。如果Field1具有公开的属性或方法,则访问这些属性或方法将导致将George.Field1作为ref参数传递给这些方法,就像它是一个ref参数一样。
如果George公开了一个名为Property1的属性,则访问Property1并不是赋值运算符的左侧,将调用“get”方法并将其结果存储在临时变量中。尝试读取Property1的字段将从临时变量中读取该字段。尝试在Property1上调用属性getter或方法将将该临时变量作为ref参数传递给该方法,然后在方法返回后将其丢弃。在方法或属性getter或方法内部,this将引用临时变量,并且方法对this所做的任何更改都将被丢弃。
由于对于临时变量的字段写入没有意义,因此禁止尝试写入属性字段。此外,当前版本的C#编译器会猜测属性设置器可能会修改this并因此禁止任何使用属性设置器即使它们实际上不会修改底层结构 [例如,ArraySegment包括一个索引get方法而不是索引set方法的原因是,如果尝试说例如thing.theArraySegment [3] = 4;,编译器会认为正在尝试修改由theArraySegment属性返回的结构,而不是修改其内封装的数组的引用所在的数组]。 如果可以指定特定结构方法将修改this并且不应在结构属性上调用它们,那将非常有用,但目前还不存在这种机制。
如果想要写入属性中包含的字段,最好的模式通常是:
var temp = myThing.myProperty; // Assume `temp` is a coordinate-point structure
temp.X += 5;
myThing.myProperty = temp;

如果 myProperty 的类型旨在封装一组相关但独立的固定值(例如点的坐标),最好将这些变量公开为字段。尽管有些人似乎更喜欢设计结构体以便需要像以下构造函数一样:
var temp = myThing.myProperty; // Assume `temp` is some kind of XNA Point structure
myThing.myProperty = new CoordinatePoint(temp.X+5, temp.Y);

我认为这种代码比之前的风格更难读、效率更低,错误率更高。如果CoordinatePoint暴露了一个带有X、Y、Z参数的构造函数以及一个仅带有X、Y参数并假设Z为零的构造函数,那么像第二种形式的代码将在没有任何指示的情况下将Z清零(无论是有意还是无意)。相比之下,如果X是一个公开字段,那么第一种形式只会修改X就更加清晰。
在某些情况下,一个类通过将内部字段或数组插槽作为ref参数传递给用户定义的例程的方法来公开它,例如List类可能会公开:
delegate void ActByRef<T1>(ref T1 p1);
delegate void ActByRef<T1,T2>(ref T1 p1, ref T2 p2);

void ActOnItem(int index, ActByRef<T> proc)
{
  proc(ref BackingArray[index]);
}
void ActOnItem<PT>(int index, ActByRef<T,PT> proc, ref PT extraParam)
{
  proc(ref BackingArray[index], ref extraParam);
}

如果有一段代码中有一个 FancyList<CoordinatePoint> 并且想要在第5个项目的字段X中添加一些本地变量 dx,则可以执行以下操作:

myList.ActOnItem(5, (ref Point pt, ref int ddx) => pt.X += ddx, ref dx);

请注意,这种方法允许对列表中的数据进行原地修改,甚至允许使用Interlocked.CompareExchange等方法。不幸的是,从List<T>派生的类型无法支持此类方法,也没有机制可以将支持此类方法的类型传递给期望List<T>的代码。

非常感谢,Supercat...我会接受你的答案,但是我只能接受一个,而Anders Abel比你快了一点,但是你的文本非常出色(非常详细)... ^_^ - NemoStein

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接