4

我在项目中使用结构,如下所示:

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

我想添加一个方法,允许我创建具有任意更改属性的结构的修改副本。例如,使用它会很方便:

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

这个成语很hacky吗?有更好的方法来支持这一点吗?

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

编辑:如果不清楚,结构是不可变的,我只想创建一个修改了一些值的新值。顺便说一句,这与 Haskell 的记录语法糖非常相似,人们会在其中写newPos = oldPos { x = x oldPos + 1 }. 关于这样的习语在 C# 中是否有用,这只是一个实验性的例子。

4

4 回答 4

2

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.

于 2013-04-15T20:48:57.623 回答
1

这与您将获得的一样简洁。对我来说似乎并不特别hacky。

尽管在您只是这样做的情况下,position.X + 1拥有类似的东西会更整洁:

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

这会给你一个修改后的 X 值,而不是修改后的 Y 值。

于 2013-04-15T09:43:27.723 回答
0

可以将这种方法视为原型模式的一种变体,其重点是拥有一个模板结构,而不是避免新实例的成本。设计的好坏取决于您的上下文。如果您可以清楚地说明语法背后的信息(我认为With您使用的名称有点不具体;可能类似于CreateVariantCreateMutant会使意图更清晰),我会认为这是一种合适的方法。

于 2013-04-15T09:46:42.113 回答
0

我也在添加一个基于表达式的表单。请注意由于它是一个结构而需要完成的可怕的装箱/拆箱。

但正如人们所看到的那样,格式非常好:

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 :)

而且该方法确实适用于各种类型。

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);
  }
}
于 2013-04-15T11:56:51.943 回答