Question

I am using a struct in a project, like so:

struct Position
{
    public int X { get; private set; }
    public int Y { get; private set; }
    // etc
}

I would like to add a method that allows me to create a modified copy of the struct with arbitrarily changed properties. For example, it would be convenient to use this:

var position = new Position(5, 7);
var newPos = position.With(X: position.X + 1);

Is this idiom hacky? Are there better ways to support this?

public Position With(int? X = null, int? Y = null)
{
    return new Position(X ?? this.X, Y ?? this.Y);
}

Edit: in case it was unclear, the struct is immutable, I simply want to create a new value with some values modified. Incidentally, this is very similar to Haskell's syntactic sugar for records, where one would write newPos = oldPos { x = x oldPos + 1 }. This is just a bit experimental as to whether such an idiom is helpful in C#.

Was it helpful?

Solution

Personally, I consider the idiom of a plain-old-data-struct to be vastly underrated. Mutable structs which encapsulate state in anything other than public fields are problematic, but sometimes it's useful to bind together a fixed collection of variables stuck together with duct tape so they can be passed around as a unit. A plain-old-data-struct is a perfect fit for that usage; it behaves like a fixed collection of variables stuck together with duct tape, since that's what it is. One can with some work come up with an immutable class which requires slow and hard-to-read code to do anything with, or with some more work come up with something that's still slow but not quite so unaesthetic; one can also code structures in such fashion as to mimic such classes. In many cases, however, the only effect of going through all that effort is that one's code will be slower and less clear than it would have been if one had simply used a PODS.

The key thing that needs to be understood is that a PODS like struct PersonInfo { public string Name, SSN; public Date Birthdate; } does not represent a person. It represents a space that can hold two strings and a date. If one says var fredSmithInfo = myDatabase.GetPersonInfo("Fred Smith");, then FredSmithInfo.BirthDate doesn't represent Fred Smith's birthdate; it represents a variable of type Date which is initially loaded with the value returned by a call to GetPersonInfo--but like any other variable of type Date, could be changed to hold any other date.

OTHER TIPS

That's about as neat a way as you're going to get. Doesn't seem particularly hacky to me.

Although in cases where you're just doing position.X + 1 it'd be neater to have something that was like:

var position = new Position(5,7);
var newPos = position.Add(new Position(1,0));

Which would give you a modified X value but not a modified Y value.

One could consider this approach as a variant of the prototype pattern where the focus is on having a template struct rather than avoiding the cost of new instances. Whether the design is good or bad depends on your context. If you can make the message behind the syntax clear (I think the name With you're using is a bit unspecific; maybe something like CreateVariant or CreateMutant would make the intention clearer), I would consider it an appropriate approach.

I'm adding an expression based form as well. Do note the horrendous boxing/unboxing which needs to be done due to the fact that it is a struct.

But as one can see the format is quite nice:

var p2 = p.With(t => t.X, 4);
var p3 = p.With(t => t.Y, 7).With(t => t.X, 5); // Yeah, replace all the values :)

And the method is really applicable to all kinds of types.

public void Test()
{
  var p = new Position(8, 3);

  var p2 = p.With(t => t.X, 4);
  var p3 = p.With(t => t.Y, 7).With(t => t.X, 5);

  Console.WriteLine(p);
  Console.WriteLine(p2);
  Console.WriteLine(p3);
}

public struct Position
{
  public Position(int X, int Y)
  {
    this._X = X; this._Y = Y;
  }

  private int _X; private int _Y;
  public int X { get { return _X; } private set { _X = value; } }
  public int Y { get { return _Y; } private set { _Y = value; } }

  public Position With<T, P>(Expression<Func<Position, P>> propertyExpression, T value)
  {
    // Copy this
    var copy = (Position)this.MemberwiseClone();
    // Get the expression, might be both MemberExpression and UnaryExpression
    var memExpr = propertyExpression.Body as MemberExpression ?? ((UnaryExpression)propertyExpression.Body).Operand as MemberExpression;
    if (memExpr == null)
      throw new Exception("Empty expression!");

    // Get the propertyinfo, we need this one to set the value
    var propInfo = memExpr.Member as PropertyInfo;
    if (propInfo == null)
      throw new Exception("Not a valid expression!");

    // Set the value via boxing and unboxing (mutable structs are evil :) )
    object copyObj = copy;
    propInfo.SetValue(copyObj, value); // Since struct are passed by value we must box it
    copy = (Position)copyObj;
    // Return the copy
    return copy;
  }

  public override string ToString()
  {
    return string.Format("X:{0,4} Y:{1,4}", this.X, this.Y);
  }
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top