【问题标题】:Set a property given a lambda expression for that property为该属性设置一个给定 lambda 表达式的属性
【发布时间】:2021-12-02 19:25:45
【问题描述】:

我正在寻找创建一个漂亮的帮助函数来比较两个值,如果它们不同则返回 true,并且还将第一个值更新为等于第二个值。这是我想出的:

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

属性访问返回临时值。 'ref' 参数必须是可赋值的变量、字段或数组元素

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


我试着玩弄表达式,例如:

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

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

任何人想如何优雅地做到这一点?

【问题讨论】:

    标签: c# lambda reflection linq-expressions


    【解决方案1】:

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

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

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

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

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

    【讨论】:

    • 我很欣赏这个建议,我相信这对于这个职位的许多人来说是正确的解决方案。就我而言,性能并不重要,我更担心的是开发人员会犯复制粘贴错误(例如,具有与设置操作不匹配的 get 函数)。所以在我投票的同时,我也会坚持解决这个问题的解决方案:)
    • 正如我所说的,源生成器!
    • 实际上这是一个 XY 问题,您正在尝试自制 MVC/MVVM。 很多框架已经为你处理了这个问题,比如 MVVM 的 ReactUI
    • 不确定源生成器是否适用于我的情况。这是一个定制的 excel 加载项应用程序,我的“新属性”来自于从给定命名范围读取单元格值,而“设置属性”是一个对象属性,将被序列化并发送到 API。不是典型的视图+模型设置!通过将代码简化为更普遍可识别的情况,我可能对自己造成了一点伤害。
    • 从您的任何示例或问题本身中都看不到这一点。这似乎是在编译时已知的所有属性的静态列表。您确定您了解您要在这里实现的目标吗?
    【解决方案2】:

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

    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;
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2011-11-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2022-10-05
      相关资源
      最近更新 更多