0

Is there a way to write a C# extension method that interacts with the getter/setter of properties with a given type?

Here's a simplified example to show what I mean. The end objective is to make code that gets and sets properties in more complex ways cleaner to read.

// Assume this is provided by your API:
struct Vector {
  public float x;
  public float y;
}
class Line {
  // The structs make these pass-by-value, so Fn(this ref Vector v) won't help
  public Vector start { get; set; }
  public Vector end { get; set; }
}

Given the above, can you somehow write an extension on the property type? Something like this:

// Obviously this syntax doesn't work, but hopefully this gets the idea across
public static void MoveX(this IProperty<Vector> prop, float val) {
  Vector v = prop.GetValue();
  v.x += val;
  prop.SetValue(v);
}

So that in your code you can write this:

public static void UsePropExensions(Line myLine) {
  myLine.start.MoveX(1f);
  myLine.end.MoveX(2f);
}

Instead of this:

public static void UsePropsDirectly(Line myLine) {
  myLine.start = new Vector(myLine.start.x + 1f, myline.start.y);
  myLine.end = new Vector(myLine.start.x + 2f, myline.start.y);
}

Why:

The best it seems you can do is this:

public static void AddX(this Vector v, float x) {
  return new Vector(v.x + x, v.y);
}
void DoStuff(Line myLine) {
  myLine.start = myLine.start.AddX(1f);
}

But that leads to copy-paste errors when the path to the property is more complex and repetitive. Humans may be pretty good at spotting differences, but they're pretty bad at spotting inconsistencies between differences. So the more you repeat yourself with subtle variations, the easier it is to introduce bugs.

void MoveStuffAround() {   
  obj.left.top.scale = obj.left.top.scale.AddX(-1f);
  obj.left.bottom.pos = obj.left.bottom.pos.AddY(-0.5f);
  obj.right.top.invScale = obj.right.top.invScale.AddX(1f);
  obj.right.bottom.pos = obj.left.bottom.pos.AddY(0.5f); // oops!
}

// But if you don't repeat yourself, you make fewer errors
void MoveWithProperties() {
  obj.left.top.scale.IncrX(-1f);
  obj.left.bottom.pos.IncrY(-0.5f);
  obj.right.top.invScale.IncrX(1f);
  obj.right.bottom.pos.IncrY(0.5f);
}
tylerl
  • 30,197
  • 13
  • 80
  • 113
  • 1
    I think you're looking for a class instead of a struct. That class has 2 readonly properties `X` and `Y` and 2 methods `MoveX` and `MoveY`. And to assign it later on you got a `GetVector` method or sth to assign it to your api. The simple answer to your question is: No – Chrᴉz remembers Monica Nov 10 '21 at 14:16
  • 1
    Since the start/end properties are not returning references to the structure, then you can't and shouldn't modify the members of the returned value, as you're dealing with a copy of the underlying struct value. And no, there is no easy direct way of doing this. – Lasse V. Karlsen Nov 10 '21 at 14:26
  • 1
    Extension methods don't deal with properties, they deal with instances. If those instances are copies of underlying values, like in the context of obtaining a value type through a property, then there is nothing the extension method can do about that. – Lasse V. Karlsen Nov 10 '21 at 14:28
  • The only solution I can think of would involve expression trees, a lot of reflection, and you'd need to call it like that: `MoveX(() => myLine.start, 1f);`. Not pretty. – Heinzi Nov 10 '21 at 14:35

1 Answers1

2

Are you trying to avoid needing to call the get and the set separately? It would work to use just a normal extension method like

public Vector MoveX(this Vector prop, float val){...}

and then you would use it like

myLine.start = myLine.start.MoveX(2f);

Is that still more complicated than what you would want to do? If so, you could make a public method that calls the get and set for you and you just supply the float that you want to move by.

public void MoveStartX(float x){...}
public void MoveEndX(float x){...}

Solution using reflection... is it worth it?

If you are wanting a very generic extension that works using reflection and doesn't have to specifically use a Line object, you could do the following

public static void MoveX<T>(this T obj, Expression<Func<T, Vector>> lambda, float x)
{
    if (lambda.Body is MemberExpression memberExpression)
    {
        var propertyInfo = (PropertyInfo)memberExpression.Member;
        var getMethod = propertyInfo.GetGetMethod();
        var setMethod = propertyInfo.GetSetMethod();
        if (setMethod == null || getMethod == null)
            throw new ArgumentException("lambda expression must reference a settable and gettable property.");
        var vector = (Vector)getMethod.Invoke(obj, null);
        vector.x += x;
        setMethod.Invoke(obj, new object[]{vector});
    }
    else
    {
        throw new ArgumentException("lambda expression must reference an accessible property.");
    }
}

and then you can use the extension method like this

var line = new Line();
line.MoveX(l => l.start, 2);
TJ Rockefeller
  • 3,178
  • 17
  • 43
  • What you're proposing (`myLine.start = myLine.start.MoveX(..)`) is what I already use. I was hoping to somehow be able to get and set within the function itself, as repeating the path to these properties leads to copy-paste bugs. (eg: `myLine.end = myLine.start.MoveX(..)`) – tylerl Nov 10 '21 at 14:30
  • I see, but as other commenters have said, the getter here is returning a Vector, not a property, and unless you change the vector to a class you won't be able to modify it in place. There may be a way with specifying a lambda, but then it may be more complicated than the original. – TJ Rockefeller Nov 10 '21 at 14:36