Reputation: 2098
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
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
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