记录 'Lenses' - with 表达式的表达式树

Record 'Lenses' - expression tree for with expression

有没有办法为新的 with 运算符构建表达式树?

我正在尝试为记录实现 'Lens' 功能,它只需要一个选择器并自动生成修改器

我的目标是从 'selector':

Expression<Func<T, TMember>> expression(即employee => employee.Name

致 'mutator':

(employee, newName) => employee with { Name = newName }

我确实设法为上面的简单案例做到了这一点,请参阅下面的答案,但是这不适用于嵌套案例,即:

record Employee(string Name, int Age);
record Manager(String Name, Employee Employee);

这里我要改成ie从

manager => manager.Employee.Name

(manager, newEmployeeName) => manager with { Employee = manager.Employee with { Name = newEmployeeName}}

有什么帮助吗?

在@JL0PD 的带领下,我最终转换了:

t => t.Member(即employee => employee.Name

进入:

(t, v) => { 
  var c = t.<Clone>$(); 
  c.Member = v; 
  return c; 
}

即:

(employee, newName) => { 
  var c = employee.<Clone>$(); 
  c.Name=newName; 
  return c; 
}

下面是记录镜头的完整实现,包括代理缓存

请注意,这不包括嵌套的修改器,所以我上面的问题仍然存在

static class RecLens<T, TMember> {
  public static (Func<T, TMember> Selector, Func<T, TMember, T> Mutator) Get(Expression<Func<T, TMember>> expression) {
    if (!IsExpressionValid(expression.Body)) throw new Exception($"Lens Invalid expression ({expression})");

    // create unique cache key, calc same key for x=>x.p and y=>y.p
    var exprStr = expression.Body.ToString();
    var dotPos = exprStr.IndexOf(Type.Delimiter);
    var cacheKey = typeof(T).FullName + '|' + (dotPos > 0 ? exprStr.Remove(0, exprStr.IndexOf(Type.Delimiter) + 1) : "root");

    if (!Cache.TryGetValue(cacheKey, out var res)) {
      res = (expression.Compile(), CalcMutator(expression));
      Cache = Cache.Add(cacheKey, res);
    }

    return res;
  }

  // key: "{srcType.FullName}|{member}" , ie: "Test.Organization|DevelopmentDepartment.Manager"
  static ImmutableDictionary<string, (Func<T, TMember>, Func<T, TMember, T>)> Cache = ImmutableDictionary<string, (Func<T, TMember>, Func<T, TMember, T>)>.Empty;

  // create delegate: (t, v) => { var c=t.<Clone>$(); c.Member = v; return c; }
  static Func<T, TMember, T> CalcMutator(Expression<Func<T, TMember>> expression) {
    var result = Expression.Variable(typeof(T), "c");
    var typeParam = Expression.Parameter(typeof(T), "t");
    var valueParam = Expression.Parameter(typeof(TMember), "v");
    var cloneMethod = typeof(T).GetMethod("<Clone>$");
    if (cloneMethod is null) throw new Exception($"CalcMutatorNo Clone method on {typeof(T)}");
    var cloneCall = Expression.Call(typeParam, cloneMethod);
    var assignResult = Expression.Assign(result, cloneCall);
    var memberInfo = (expression.Body as MemberExpression)!.Member;
    var resultMemberAccess = Expression.MakeMemberAccess(result, memberInfo);
    var assign = Expression.Assign(resultMemberAccess, valueParam);
    var block = Expression.Block(new[] { result }, assignResult, assign, result);
    var assignLambda = (Expression<Func<T, TMember, T>>)Expression.Lambda(block, typeParam, valueParam);
    return assignLambda.Compile();
  }

  // verify that expr is a member expression of its parameter
  static bool IsExpressionValid(Expression expr, bool first = true) {
    if (expr is ParameterExpression) return !first;
    if (expr is MemberExpression memberExpr && memberExpr.Expression is object) return IsExpressionValid(memberExpr.Expression, false);
    return false;
  }
}

使用:

record Employee(string Name, int Age);
var (Selector, Mutator) = RecLens<Employee, string>.Get(e => e.Name);

var dave = new Employee("Dave", 30);
var name = Selector(dave); // "Dave"
var john = Mutator(dave, "John"); // Employee("John", 30)

CalcMutator 可以处理嵌套属性的方法看起来像这样

static Func<T, TMember, T> CalcMutator(Expression<Func<T, TMember>> expression)
{
    var typeParam = expression.Parameters.First();
    var valueParam = Expression.Parameter(typeof(TMember), "v");

    var variables = new List<ParameterExpression>();
    var blockExpressions = new List<Expression>();

    var property = (MemberExpression)expression.Body;
    Expression currentValue = valueParam;
    var index = 0;

    while (property != null)
    {
        var variable = Expression.Variable(property.Expression.Type, $"v_{index}");
        variables.Add(variable);

        var cloneMethod = property.Expression.Type.GetMethod("<Clone>$");
        if (cloneMethod is null) throw new Exception($"CalcMutatorNo Clone method on {typeof(T)}");
        var cloneCall = Expression.Call(property.Expression, cloneMethod);

        var assignClonedToVariable = Expression.Assign(variable, cloneCall);
        
        var accessVariableProperty = Expression.MakeMemberAccess(variable, property.Member);
        var assignVariablePropertyValue = Expression.Assign(accessVariableProperty, currentValue);

        blockExpressions.Add(assignClonedToVariable);
        blockExpressions.Add(assignVariablePropertyValue);

        property = property.Expression as MemberExpression;
        currentValue = variable;
        index++;
    }

    // Return root object
    blockExpressions.Add(currentValue);

    var block = Expression.Block(variables, blockExpressions);
    var assignLambda = (Expression<Func<T, TMember, T>>)Expression.Lambda(block, typeParam, valueParam);
    return assignLambda.Compile();
}

请记住,使用 ImmutableDictionary 实现的 Cache 不是线程安全的。如果要确保缓存的表达式可以在多线程环境中安全使用,最好使用 ConcurrentDictionary 作为缓存,或者在 ImmutableDictionary.

周围应用一些同步原语。