访问和更改结构作为属性与字段

时间:2013-08-17 18:33:34

标签: c# struct

好的,我会开始提问我说我理解可变结构背后的邪恶,但我正在使用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 个答案:

答案 0 :(得分:5)

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

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.X,p.Y,r.A和r.B的赋值,因为p和r是变量。但是,在示例中

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

分配都无效,因为r.A和r.B不是变量。

答案 1 :(得分:2)

尽管属性看起来像变量,但每个属性实际上都是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作为refout参数传递给另一个方法。此外,如果Field1的类型是具有公开字段的值类型,则尝试访问这些字段将访问存储在George中的结构字段。如果Field1已公开属性或方法,那么访问这些属性或方法会导致George.Field1传递给这些方法,就像它是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<T>的类可能会暴露:

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>且希望在iit中将项目5的字段X添加一些局部变量dx的代码可以执行:

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

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