好的,我会开始提问我说我理解可变结构背后的邪恶,但我正在使用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
}
}
}
注意:我将构建自己的类以涵盖相同的功能并保持可变性,但我看不出技术原因,为什么我可以做一个而不能做其他。
答案 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
作为ref
或out
参数传递给另一个方法。此外,如果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>
的代码