NemoStein
NemoStein

Reputation: 2098

Accessing and changing structs as property vs as field

Ok, I'll start my question saying that I understand the evil behind mutable structs, but I'm working with SFML.net and using a lot of Vector2f and such structs.

What I don't get it is why I can have, and change the values of, a field in a class and can't do the same with a property, in the very same class.

Take a look at this code:

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

note: I'll build my own classes to cover the same functionality and keep with my mutability, but I can't see a technical reason why I can do one and can't do other.

Upvotes: 5

Views: 8974

Answers (2)

supercat
supercat

Reputation: 81277

Although properties look like variables, each property is really a combination of a get method and/or a set method. Typically a property get method will return a copy of what's in some variable or array slot, and a put method will copy its parameter into that variable or array slot. If one wants to do something like someVariable = someObject.someProeprty; or someobject.someProperty = someVariable;, it won't matter that those statements end up being executed as var temp=someObject.somePropertyBackingField; someVariable=temp; and var temp=someVariable; someObject.somePropertyBackingField=temp;, respectively. On the other hand, there are some operations which can be done with fields but cannot be done with properties.

If an object George exposes a field named Field1, then code may pass George.Field as a ref or out parameter to another method. Additionally, if the type of Field1 is a value type with exposed fields, then an attempt to access those fields will access the fields of the struct that is stored within George. If Field1 has exposed properties or methods, then accessing those will cause George.Field1 to be passed to those methods as though it were a ref parameter.

If George exposes a property named Property1, then an access of Property1 which is not the left side of an assignment operator will call the "get" method and store its result in a temporary variable. An attempt to read a field of Property1 will read that field from the temporary variable. An attempt to call a property getter or method on Property1 will pass that temporary variable as a ref parameter to that method and then discard it after the method returns. Within the method or property getter or method, this will refer to the temporary variable, and any changes the method makes to this will be discarded.

Because it would make no sense to write to fields of a temporary variable, attempts to write to fields of a property are forbidden. Additionally, present versions of the C# compiler will guess that property setters would be likely to modify this and will thus forbid any use of property setters even when they would in fact not modify the underlying structure [e.g. the reason ArraySegment includes an indexed get method and not an indexed set method is that if one were to try to say e.g. thing.theArraySegment[3] = 4; the compiler would think one was trying to trying to modify the structure returned by the theArraySegment property, rather than modify the array whose reference is encapsulated therein]. It would be extremely useful if one could specify that particular structure methods will modify this and should not be invokable on structure properties, but as yet no mechanism exists for that.

If one wants to write to a field contained within a property, the best pattern is usually:

var temp = myThing.myProperty; // Assume `temp` is a coordinate-point structure
temp.X += 5;
myThing.myProperty = temp;

If the type of myProperty is designed to encapsulate a fixed set of related but independent values (such as the coordinates of a point), it's best if it exposes those variables as fields. Although some people seem to prefer to design structs so as to require constructs like:

var temp = myThing.myProperty; // Assume `temp` is some kind of XNA Point structure
myThing.myProperty = new CoordinatePoint(temp.X+5, temp.Y);

I would regard such code as less readable, less efficient, and more error-prone than the previous style. Among other things, if CoordinatePoint happens to e.g. expose a constructor with parameters X,Y,Z as well as a constructor which takes parameters X,Y and assumes Z is zero, code like the second form would zero out Z without any indication that it was doing so (intentionally or unintentionally). By contrast, if X is an exposed field, it's much clearer that the first form would only modify X.

In some cases, it may be helpful for a class to expose an internal field or array slot via a method that passes it as a ref parameter to a user-defined routine, e.g. a List<T>-like class might expose:

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

Code which had a FancyList<CoordinatePoint> and wanted to add some local variable dx to field X of item 5 in iit could then do:

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

Note that this approach would allow in-place modification of data in the list, and even allow the use of such methods as Interlocked.CompareExchange). Unfortunately, there's no possible mechanism by which a type which derives from List<T> can support such a method, and no mechanism by which a type which does support such a method can be passed to code which expects a List<T>.

Upvotes: 7

Anders Abel
Anders Abel

Reputation: 69280

When you access a field, you are accessing the actual struct. When you access it through property, you call a method that returns whatever is stored in the property. In the case of a struct, which is a value type, you will get back a copy of the struct. Apparently that copy is not a variable and cannot be changed.

Section "1.7 Structs" of the C# language specification 5.0 says:

With classes, it is possible for two variables to reference the same object and thus possible for operations on one variable to affect the object referenced by the other variable. With structs, the variables each have their own copy of the data, and it is not possible for operations on one to affect the other.

That explains that you will receive a copy of the struct and not be able to modify the original struct. However, it doesn't describe why it isn't allowed.

Section "11.3.3" of the specifcation:

When a property or indexer of a struct is the target of an assignment, the instance expression associated with the property or indexer access must be classified as a variable. If the instance expression is classified as a value, a compile-time error occurs. This is described in further detail in §7.17.1.

So the returned "thing" from the get accessor is a value and not a variable. That explains the wording in the error message.

The specification also contains an example in section 7.17.1 that is nearly identical to your code:

Given the declarations:

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

in the example

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

the assignments to p.X, p.Y, r.A, and r.B are permitted because p and r are variables. However, in the example

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

the assignments are all invalid, since r.A and r.B are not variables.

Upvotes: 13

Related Questions