给定 属性 的 lambda 表达式设置 属性

Set a property given a lambda expression for that property

我想创建一个灵活的辅助函数来比较两个值,returns 如果它们不同则为真,并且还会将第一个值更新为等于第二个值。这是我想出的:

bool UpdateHelper<T>(ref T originalProperty, T newProperty) =>
    !Equals(originalProperty, newProperty) && (originalProperty = newProperty) is T _;

bool changes = false;
changes |= UpdateHelper(ref name, frmName.Text);
changes |= UpdateHelper(ref description, frmDescription.Text);
...

目标是在为数十个属性执行此操作时,避免为每个属性复制粘贴相同的 5 行代码 属性:

if (myModel.SomeProperty != someUserInput.Text)
{
   myModel.SomeProperty = someUserInput.Text
   changes = true
}

(尤其是在看到有人复制粘贴并更新 if 语句部分但忘记更新赋值之后!)

我上面的助手大部分都有效,但不幸的是,当我尝试在属性上使用它时,它就崩溃了:

changes |= UpdateHelper(ref myModel.Name, frmName.Text);
changes |= UpdateHelper(ref myModel.Description, frmDescription.Text);

Property access returns temporary value. 'ref' argument must be an assignable variable, field or an array element

谁能想出一个对两者都适用的解决方案?


我尝试使用表达式,例如:

bool UpdateHelper<T>(Expression<Func<T>> originalProperty, T newProperty) =>
    !Equals(originalProperty.Compile().Invoke(), newProperty) && (originalProperty.Body.??? = newProperty) is T _;

changes |= UpdateHelper(() => myModel.Name, frmName.Text);
changes |= UpdateHelper(() => myModel.Description, frmDescription.Text);
changes |= UpdateHelper(() => myModel.ChildObject.Name, frmChildName.Text);

但我觉得它会变成解析表达式树的噩梦,特别是如果它需要处理访问嵌套在几层下的属性的特殊情况(如上面的最后一个示例)。

有人知道如何优雅地做到这一点吗?

表达式很慢,正如您在代码中看到的那样,您需要为每个赋值编译它们。如果您想支持这两种情况,一种解决方案是为获取和设置提供单独的功能:

bool UpdateHelper<T>(Func<T> originalGet, Action<T> originalSet, T newProperty) => ...

你可以这样称呼它(对于字段和属性):

changes |= UpdateHelper(() => name, val => name = val, frmName.Text);

请注意,最优雅的解决方案是根本不编写它,而是使用模板编译器 (.tt) 或纯源代码生成器 (ISourceGenerator / IIncrementalGenerator).

我想我已经解决了。这种表达式解析在处理例如属性 更改了事件通知。

bool UpdateHelper<T>(Expression<Func<T>> originalProperty, T newProperty)
{
    if (Equals(originalProperty.Compile().DynamicInvoke(), newProperty))
        return false;
    MemberExpression mex = (originalProperty.Body is UnaryExpression ux ? ux.Operand : originalProperty.Body) as MemberExpression;
    if (mex?.Member is PropertyInfo pi)
        pi.SetValue(Expression.Lambda(mex.Expression).Compile().DynamicInvoke(), newProperty);
    else
        throw new ArgumentException("The originalProperty argument must be a property expression such as `() => someObject.SomeProperty`");
    return true;
}