Question

I have a small class called Tank has one public member called Location which is a Rectangle(a struct). When I write:

Tank t = new Tank();
t.Location.X+=10;

everything works fine, and the tank moves.

But after I changed the member to be a property, I can no longer use this syntax. It doesn't compile, because the t.Location is now a property(which is function) and returns a temporary copy of the location(because it's a value type).

The only way I can use Location now is to do something like this :

k = t.Location 
k.X +=10;
t.Location = k;

Is there any workaround that can help me not to write this ugly code, and use the intuitive a+=10; syntax?

Was it helpful?

Solution

From @ Servy
"structs are immutable" well, no, they're not. They should be, in most cases, but they are not inherently immutable. The inherent problem here is that the property returns a copy of the struct, not a reference to the struct. If there was a syntax in C# for ref returns, then this would be possible.

Fundamentally why this will not work is that structs are immutable. Once they're made, that's it. For this reason it is not possible to partially reassign a struct. It would be like trying to swap your leg out. You can't. It's part of you, and you came with it!

I think the only thing you're going to be able to do is implement your own X and Y attributes, such that:

public double LocationX
{
   get
   {
       return Location.X;
   }
   set
   {
       Location = new Rectangle(value,Location.Y);
   }
}

You obviously need to mirror this to Y as well, but this should allow what you want (but don't expect it to be quick or efficient!)

Whilst this answers your immediate question, I would raise a few points about your model. I would consider not attempting to update the movement like this. From an OO point of view, your tank is its own object, and should be managing its own position. Give it a movement instruction, and then have it update it's own position.

e.g:

Tank.MoveRelative(10,0);   
Tank.MoveAbsolute(100,100);

this allows you a little more freedom and allows the tank to validate any requests made on it based on logic you've given it.

OTHER TIPS

This problem occurs pretty often when you start programming in 2D and 3D space using properties. Generally, the best workaround is to implement addition between two vector structures or two differing structures that would add together in a logical manner (in your case, you would implement addition between a 2D vector and your rectangle to offset its position - you wouldn't add two rectangles together).

Doing so, allows you to write:

myTank.Location += new Vector2(10, 0);

Which, while still slightly clunky, allows you to make changes for both components in a single operation. Ideally, the added vector would be a velocity vector which you would use to update your tank's location.

I would suggest making a method to move your tank.

public class Tank
{
    private Rectangle _location;

    public int X { get { return _location.X; } }
    public int Y { get { return _location.Y; } }

    public Tank(int width, int height /* other params */)
    {
        _location = new Rectangle(0, 0, width, height);
    }

    public Tank Move(Point offset)
    {
        _location.X += offset.X;
        _location.Y += offset.Y;

        return this;
    }
}

Usage would be

var tank = new Tank(1, 1);
tank.Move(new Point(1, 1)).Move(new Point(1, 1)); //Tank would have X: 2, Y: 2

This can be changed to use Vector2 or whatever.

The core difference is that a property is classified as a function while a field is classified as a variable. Function member invocation kicks in.

A work around is to use a field or a backing store rather than a property just as you have done. One should avoid creating mutable value types because the behavior is often surprising, difficult to predict and/or sometimes downright inconsistent.

Here's some nitty gritty details, relevant sections from the spec that help describe the behavior you are experiencing.

C# 4.0 section 1.6.7.2

A set accessor corresponds to a method with a single parameter named value and no return type.

A get accessor corresponds to a parameterless method with a return value of the property type.

Now switch over to 7.5.5 Function Member Invocation, the relevant section:

If [the function member] is an instance function member declared in a value type:

If [the instance expression] is not classified as a variable, then a temporary local variable of [the instance expression]'s type is created and the vlue of [the instance expression] is assigned to that variable. [the instance expression] is then reclassified as a reference to that temporary variable. The temporary variable is accessible as this within [the function member], but not in any other way. Thus, only when [the instance expression] is a true variable is it possible for the caller to observe the changes that [the function member] makes to this.

If a class or struct-type variable exposes a value-type field, and that value type exposes its contents as fields, operations on those fields can be performed as efficiently as operands on the surrounding variable type. IF the value type is exposed as a property, then the best one can do is generally something like:

var temp = t.Location;
temp.X += 4;
t.Location = temp;

Not terribly elegant, but relatively clear and not too horribly inefficient. An alternative would be to have the tank expose a method AdjustLocation, something like:

delegate void ActByRef<T1>(ref T1 p1);
void ActOnLocation(ActByRef<Point> proc)
  { proc(ref _Location); }

and probably also

delegate void ActByRef<T1,T2>(ref T1 p1, ref T2 p2);
void ActOnLocation<PT1>(ActByRef<Point, PT1>, ref PT1 param1)
  { proc(ref _Location, ref param1); }

These methods assume that the Location property uses a backing field named _Location. Code could then do something like:

// Add 5 to X
myTank.ActOnLocation( (ref Point loc) => loc.X += 5 ); 

or

// Add YSpeed to Y
myTank.ActOnLocation( (ref Point loc, ref int param) => loc.Y += param, ref YSpeed);

Note that in the latter case, neither YSpeed, nor this, nor any other local variable, is not used within the lambda; instead, YSpeed is passed as a ref parameter. Because of that, even if the above code is run a million times, the system will only have to generate one delegate which can then be re-used every time.

If the structure were large, the above approach could be faster than the approach using a temporary variable. While overhead is probably greater than the cost of copying a small structure, the overhead is independent of structure size. One could efficiently use structures that were several kilobytes in size if one used constructs like the above so as to avoid having to make temporary copies.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top