IndexExpression 到 MemberBinding

IndexExpression to MemberBinding

我有一个问题需要帮助。有一个 object 说 TestObject

public class TestObject
{
      public int Value { get; set; }
      public string StringValue {get;set;}
} 

此 object 正在动态写入 csv 文件,其中 "Value" 和 "StringValue" header 下面初始化了这种类型的示例列表

    var testObjects = new List<TestObject> { new TestObject() { Value = 1, StringValue = "A" },
        new TestObject() { Value = 2, StringValue = "B" }, new TestObject() { Value = 3, StringValue = "C" } };

将表示为 table

╔═══════╦═════════════╗
║ Value ║ StringValue ║
╠═══════╬═════════════╣
║     1 ║ A           ║
║     2 ║ B           ║
║     3 ║ C           ║
╚═══════╩═════════════╝    

我正在为此 object 动态创建成员绑定列表,在本例中是 2 个绑定; 1 个用于 "Value" 属性,1 个用于 "StringValue" 属性。让我们调用这些绑定的列表,其中 testObjectBindings 是 MemberBindings 的列表。

我最终想重新创建已编写的测试列表object,因此伪代码将是

var recreatedList = new List<TestObject>();
for (int i=0; i <3; i++)
{
   //create a new test object and use
   //the binding information
   var newObject = new TestObject()
   {binding[0], binding[1]};
}

根据我对表达式的理解,它的工作方式如下

    var indexer = testObjectBindings.GetType()
        .GetDefaultMembers()
        .OfType<PropertyInfo>()
        .First();

//其中 0 是生成绑定的索引 0 处的成员绑定

IndexExpression propertyExpression = Expression.Property(Expression.Constant(testObjectBindings), indexer, 0);

我想使用 Expression.MemberInit 构建测试 object 并像下面那样分配绑定

 MemberInitExpression body =  Expression.MemberInit(Expression.New(typeof(TestObject)), propertyExpression[0] { });

这不起作用,因为 IndexExpression 不是 MemberBinding。如何将返回的 属性 转换或表示为 MemberBinding 以便我可以使用 MemberInit 调用?

感谢阅读

我也不确定你到底想干什么。下面是使用 MemberBindings

创建表达式列表的示例代码
        var testObjects = new List<TestObject> 
        { 
            new TestObject { Value = 1, StringValue = "A" },
            new TestObject { Value = 2, StringValue = "B" }, 
            new TestObject { Value = 3, StringValue = "C" } 
        };

        var valueMemberInfo = typeof (TestObject).GetMember("Value").First();
        var stringValueMemberInfo = typeof (TestObject).GetMember("StringValue").First();

        var expressions = new List<Expression>();
        foreach (var to in testObjects)
        {
            var expr = Expression.MemberInit(
                Expression.New(typeof(TestObject)),
                Expression.Bind(valueMemberInfo, Expression.Constant(to.Value)),
                Expression.Bind(stringValueMemberInfo, Expression.Constant(to.StringValue))
            );
            expressions.Add(expr);
        }

我假设如下,因为我不太清楚你的问题:

  • 您想将 CSV 文件转换为 objects 的列表。
  • CSV 文件的第一行包含 header,其中 header 中的名称映射到目标 objects 的 属性 个名称。
  • objects是使用默认构造函数创建的,之后你一个一个设置属性(MemberInit表达式类型代表C#语法糖,在动态构造时附加值很少代码)。

我在最后添加了更多详细信息来解释为什么 MemberBinding 在这种情况下没有帮助。

如果是这种情况,我们可以将主要问题归结为动态创建以下方法:

TestObject FromCsvLine(string[] csvFields) {
    var result = new TestObject();
    result.Value = (int)Convert.ChangeType(csvFields[0], typeof(int));
    result.StringValue = (string)Convert.ChangeType(csvFields[1], typeof(string));
    return result;
}

注意 Convert.ChangeType 调用,无论 属性 类型如何,它的结构都保持不变,我们只需要提供不同的参数,使其易于动态构造。另请注意,可以使用 Func<string[], TestObject>.

描述此函数的签名

我们创建这个方法需要的数据是:

  • 要反序列化的目标类型
  • 列名列表

因此,我们得到如下方法签名:

Func<string[], T> CreateCsvDeserializer<T>(string[] columnNames)
      where T : new()

new() 约束在编译时强制类型 T 将有一个带 0 个参数的构造函数。

实施:

private static Func<string[], T> CreateCsvDeserializer<T>(string[] columnNames)
    where T : new()
{
    var resultVariable = Expression.Variable(typeof (T), "result");
    var csvFieldsParameter = Expression.Parameter(typeof (string[]), "csvFields");
    var constructorCall = Expression.Assign(resultVariable, Expression.New(typeof (T)));

    //will contain all code lines that implement the method
    var codeLines = new List<Expression> {constructorCall};

    for (int i = 0; i < columnNames.Length; i++)
    {
        string columnName = columnNames[i];
        PropertyInfo property = typeof (T).GetProperty(columnName);
        if (property == null || !property.CanWrite || !property.GetSetMethod().IsPublic)
        {
            //cannot write to property
            throw new Exception();
        }

        //Convert.ChangeType(object, Type)
        var convertChangeTypeMethod = typeof (Convert).GetMethod("ChangeType",
            new[] {typeof (object), typeof (Type)});

        //csvFields[i]
        var getColumn = Expression.ArrayIndex(csvFieldsParameter, Expression.Constant(i));

        //Convert.ChangeType(csvFields[i], [propertyType])
        var conversion = Expression.Call(convertChangeTypeMethod, getColumn,
            Expression.Constant(property.PropertyType));

        //([propertyType])Convert.ChangeType(csvFields[i], [propertyType])
        var cast = Expression.Convert(conversion, property.PropertyType);

        //result.[property]
        var propertyExpression = Expression.Property(resultVariable, property);

        //result.[property] = ([propertyType])Convert.ChangeType(csvFields[i], [propertyType])
        codeLines.Add(Expression.Assign(propertyExpression, cast));
    }

    //create a line that returns the resultVariable
    codeLines.Add(resultVariable);

    //now, we have a list of code lines, it's time to build our function
    Type returnType = typeof (T);
    var variablesUsed = new[] {resultVariable};
    var codeBlock = Expression.Block(returnType, variablesUsed, codeLines);
    var parameterList = new[] {csvFieldsParameter};
    return Expression.Lambda<Func<string[], T>>(codeBlock, parameterList).Compile();
} 

正如在调试器中所见,使用 CreateCsvDeserializer<TestObject>(new [] { "Value", "StringValue" }); 调用时生成的代码行与我们在上面的示例中开始构建的代码匹配(除了一些语法差异)。

创建方法后,我们只需向其提供 CSV 行并构建列表,这相对容易:

private static List<T> BuildFromCsvFile<T>(string path, string separator = ",")
    where T : new()
{
    string[] separators = {separator};
    var lines = File.ReadAllLines(path);
    var deserializer = CreateCsvDeserializer<T>(lines[0].Split(separators, StringSplitOptions.RemoveEmptyEntries));
    return
        lines.Skip(1)
            .Select(s => s.Split(separators, StringSplitOptions.RemoveEmptyEntries))
            .Select(deserializer)
            .ToList();
} 

然后通过调用构造列表,例如BuildFromCsvFile<TestObject>("Data.csv");

这个解决方案没有解决的问题我能想到:

  • 数据本地化,不同的 date/number 格式。 Convert.ChangeType 接受允许指定文化的 IFormatProvider。 属性 属性然后可以指定如何将 CSV 数据转换为适当的类型。
  • 复杂类型/自定义类型构造。 (例如 Person.Address.Street
  • 适当的错误处理

为什么 MemberBinding 不是正确的选择

System.Linq.Expressions 命名空间使我们能够从本质上构建代码生成器。 MemberBinding class 是 MemberAssignmentMemberListBindingMemberMemberBinding class 的基础 class。这些表达式中的每一个都代表也可以使用 属性 或字段设置器表达的操作。使用 TestObject:

的原始示例
var obj = new TestObject { 
       Value = 1 //MemberBinding expression
};

这相当于:

var obj = new TestObject();
obj.Value = 1;

正如我们所见,编写代码生成器比编写我们生成的代码要复杂得多,我们希望它尽可能简单。使用不同种类的语法通常不会降低它的复杂性。