4

好的,我将开始我的问题,说我了解可变结构背后的邪恶,但我正在使用 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
        }
    }
}

注意:我将构建自己的类来涵盖相同的功能并保持我的可变性,但我看不出我可以做一个而不能做其他的技术原因。

4

2 回答 2

12

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

C# 语言规范 5.0 的“1.7 结构”部分说:

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

这说明您将收到该结构的副本,并且无法修改原始结构。但是,它没有描述为什么不允许这样做。

规范的“11.3.3”部分:

当结构的属性或索引器是赋值的目标时,与属性或索引器访问关联的实例表达式必须分类为变量。如果实例表达式被归类为值,则会发生编译时错误。这在第 7.17.1 节中有更详细的描述。

所以从 get 访问器返回的“东西”是一个值而不是一个变量。这解释了错误消息中的措辞。

该规范还包含第 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;

对 pX、pY、rA 和 rB 的赋值是允许的,因为 p 和 r 是变量。但是,在示例中

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

赋值都是无效的,因为 rA 和 rB 不是变量。

于 2013-08-17T18:39:19.283 回答
5

尽管属性看起来像变量,但每个属性实际上是 get 方法和/或 set 方法的组合。通常,属性 get 方法将返回某个变量或数组槽中内容的副本,而 put 方法会将其参数复制到该变量或数组槽中。如果一个人想做类似someVariable = someObject.someProeprty;orsomeobject.someProperty = someVariable;的事情,那么这些语句最终分别被执行为var temp=someObject.somePropertyBackingField; someVariable=temp;and并不重要var temp=someVariable; someObject.somePropertyBackingField=temp;。另一方面,有些操作可以用字段完成,但不能用属性完成。

如果一个对象George公开了一个名为 的字段Field1,那么代码可以George.Field作为一个refout参数传递给另一个方法。此外,如果 的类型Field1是具有暴露字段的值类型,则访问这些字段的尝试将访问存储在George. 如果Field1已经暴露了属性或方法,那么访问它们将导致George.Field1传递给这些方法,就好像它是一个ref参数一样。

如果George暴露了一个名为 的属性Property1,那么一个Property1不在赋值运算符左侧的访问将调用“get”方法并将其结果存储在一个临时变量中。尝试读取 的Property1字段将从临时变量中读取该字段。尝试调用属性 getter 或方法Property1会将该临时变量作为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>-like 类可能会公开:

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);
}

具有 aFancyList<CoordinatePoint>并希望将一些局部变量添加dx到 iit 中项目 5 的字段 X 的代码可以执行以下操作:

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

请注意,这种方法将允许就地修改列表中的数据,甚至允许使用诸如Interlocked.CompareExchange) 之类的方法。不幸的是,没有可能的机制可以让派生自的类型List<T>支持这种方法,也没有任何机制可以将支持这种方法的类型传递给需要List<T>.

于 2013-08-17T19:21:33.287 回答