如何创建和访问在C#中作为参数传递的匿名类的新实例?

问题描述 投票:10回答:4

我创建了一个函数,它接受一个SQL命令并生成输出,然后可以用它来填充类实例的List。代码效果很好。我在这里提供了一个稍微简化的版本,无需异常处理,仅供参考 - 如果您想直接解决问题,请跳过此代码。如果你在这里有建议,我会全力以赴。

    public List<T> ReturnList<T>() where T : new()
    {
        List<T> fdList = new List<T>();
        myCommand.CommandText = QueryString;
        SqlDataReader nwReader = myCommand.ExecuteReader();
        Type objectType = typeof (T);
        FieldInfo[] typeFields = objectType.GetFields();
        while (nwReader.Read())
        {
            T obj = new T();
            foreach (FieldInfo info in typeFields)
            {
                for (int i = 0; i < nwReader.FieldCount; i++)
                {
                    if (info.Name == nwReader.GetName(i))
                    {
                        info.SetValue(obj, nwReader[i]);
                        break;
                    }
                }
            }
            fdList.Add(obj);
        }
        nwReader.Close();
        return fdList;
    }

正如我所说,这很好用。但是,出于显而易见的原因,我希望能够使用匿名类调用类似的函数。

问题1:似乎我必须在调用此函数的匿名版本时构造一个匿名类实例 - 这是对的吗?一个示例电话是:

.ReturnList(new { ClientID = 1, FirstName = "", LastName = "", Birthdate = DateTime.Today });

问题2:我的ReturnList函数的匿名版本如下。任何人都可以告诉我为什么对info.SetValue的调用什么都不做?它不会返回错误或任何内容,但也不会更改目标字段的值。

    public List<T> ReturnList<T>(T sample) 
    {
        List<T> fdList = new List<T>();
        myCommand.CommandText = QueryString;
        SqlDataReader nwReader = myCommand.ExecuteReader();
        // Cannot use FieldInfo[] on the type - it finds no fields.
        var properties = TypeDescriptor.GetProperties(sample); 
        while (nwReader.Read())
        {
            // No way to create a constructor so this call creates the object without calling a ctor. Could this be a source of the problem?
            T obj = (T)FormatterServices.GetUninitializedObject(typeof(T)); 
            foreach (PropertyDescriptor info in properties)  
            {
                for (int i = 0; i < nwReader.FieldCount; i++)
                {
                    if (info.Name == nwReader.GetName(i))
                    {
                        // This loop runs fine but there is no change to obj!!
                        info.SetValue(obj, nwReader[i]);
                        break;
                    }
                }
            }
            fdList.Add(obj);
        }
        nwReader.Close();
        return fdList;
    }

有任何想法吗?

注意:当我尝试像上面函数中那样使用FieldInfo数组时,typeFields数组的元素为零(即使objectType显示字段名称 - 奇怪)。因此,我使用TypeDescriptor.GetProperties代替。

关于使用反射或匿名类的任何其他提示和指导都适用于此 - 我对C#语言的这个特定角落相对较新。

更新:我要感谢杰森解决这个问题的关键。下面是修改后的代码,它将创建一个匿名类实例列表,从查询中填充每个实例的字段。

   public List<T> ReturnList<T>(T sample)
   {
       List<T> fdList = new List<T>();
       myCommand.CommandText = QueryString;
       SqlDataReader nwReader = myCommand.ExecuteReader();
       var properties = TypeDescriptor.GetProperties(sample);
       while (nwReader.Read())
       {
           int objIdx = 0;
           object[] objArray = new object[properties.Count];
           foreach (PropertyDescriptor info in properties) 
               objArray[objIdx++] = nwReader[info.Name];
           fdList.Add((T)Activator.CreateInstance(sample.GetType(), objArray));
       }
       nwReader.Close();
       return fdList;
   }

请注意,已构造查询,并且在先前对此对象的方法的调用中初始化了参数。原始代码具有内部/外部循环组合,因此用户可以在其匿名类中具有与字段不匹配的字段。但是,为了简化设计,我决定不允许这样做,而是采用了Jason推荐的db字段访问。另外,感谢Dave Markle帮助我更多地了解使用Activator.CreateObject()与GenUninitializedObject的权衡。

c# reflection anonymous-types
4个回答
25
投票

匿名类型封装了一组只读属性。这解释了

  1. 为什么Type.GetFields在您的匿名类型上调用时返回一个空数组:匿名类型没有公共字段。
  2. 匿名类型的公共属性是只读的,不能通过调用PropertyInfo.SetValue来设置它们的值。如果您在匿名类型的房产上致电PropertyInfo.GetSetMethod,您将收到null

事实上,如果你改变了

var properties = TypeDescriptor.GetProperties(sample);
while (nwReader.Read()) {
    // No way to create a constructor so this call creates the object without calling a ctor. Could this be a source of the problem?
    T obj = (T)FormatterServices.GetUninitializedObject(typeof(T)); 
    foreach (PropertyDescriptor info in properties) {
        for (int i = 0; i < nwReader.FieldCount; i++) {
            if (info.Name == nwReader.GetName(i)) {
                // This loop runs fine but there is no change to obj!!
                info.SetValue(obj, nwReader[i]);
                break;
            }
        }
    }
    fdList.Add(obj);
}

PropertyInfo[] properties = sample.GetType().GetProperties();
while (nwReader.Read()) {
    // No way to create a constructor so this call creates the object without calling a ctor. Could this be a source of the problem?
    T obj = (T)FormatterServices.GetUninitializedObject(typeof(T));
    foreach (PropertyInfo info in properties) {
        for (int i = 0; i < nwReader.FieldCount; i++) {
            if (info.Name == nwReader.GetName(i)) {
                // This loop will throw an exception as PropertyInfo.GetSetMethod fails
                info.SetValue(obj, nwReader[i], null);
                break;
            }
        }
    }
    fdList.Add(obj);
}

您将收到一个异常,通知您无法找到属性集方法。

现在,要解决您的问题,您可以做的是使用Activator.CreateInstance。对不起,我懒得为你输入代码,但下面将演示如何使用它。

var car = new { Make = "Honda", Model = "Civic", Year = 2008 };
var anothercar = Activator.CreateInstance(car.GetType(), new object[] { "Ford", "Focus", 2005 });

因此,只需运行一个循环就可以填充需要传递给Activator.CreateInstance的对象数组,然后在循环完成时调用Activator.CreateInstance。属性顺序在这里很重要,因为两个匿名类型是相同的,当且仅当它们具有相同数量的具有相同类型和相同名称的相同顺序的属性时。

有关更多信息,请参阅匿名类型的MSDN page

最后,这真的是一个旁边,与你的问题没有密切关系,但以下代码

foreach (PropertyDescriptor info in properties) {
    for (int i = 0; i < nwReader.FieldCount; i++) {
        if (info.Name == nwReader.GetName(i)) {
            // This loop runs fine but there is no change to obj!!
            info.SetValue(obj, nwReader[i]);
            break;
        }
    }
}

可以简化

foreach (PropertyDescriptor info in properties) {
            info.SetValue(obj, nwReader[info.Name]);
}

2
投票

我有同样的问题,我通过创建一个新的Linq.Expression来解决它,它将完成真正的工作并将其编译成lambda:这是我的代码,例如:

我想转换那个电话:

var customers = query.ToList(r => new
            {
                Id = r.Get<int>("Id"),
                Name = r.Get<string>("Name"),
                Age = r.Get<int>("Age"),
                BirthDate = r.Get<DateTime?>("BirthDate"),
                Bio = r.Get<string>("Bio"),
                AccountBalance = r.Get<decimal?>("AccountBalance"),
            });

那个电话:

var customers = query.ToList(() => new 
        { 
            Id = default(int),
            Name = default(string),
            Age = default(int), 
            BirthDate = default(DateTime?),
            Bio = default(string), 
            AccountBalance = default(decimal?)
        });

并从新方法获取DataReader.Get的东西,第一种方法是:

public List<T> ToList<T>(FluentSelectQuery query, Func<IDataReader, T> mapper)
    {
        return ToList<T>(mapper, query.ToString(), query.Parameters);
    }

我不得不在新方法中构建一个表达式:

public List<T> ToList<T>(Expression<Func<T>> type, string sql, params object[] parameters)
        {
            var expression = (NewExpression)type.Body;
            var constructor = expression.Constructor;
            var members = expression.Members.ToList();

            var dataReaderParam = Expression.Parameter(typeof(IDataReader));
            var arguments = members.Select(member => 
                {
                    var memberName = Expression.Constant(member.Name);
                    return Expression.Call(typeof(Utilities), 
                                           "Get", 
                                           new Type[] { ((PropertyInfo)member).PropertyType },  
                                           dataReaderParam, memberName);
                }
            ).ToArray();

            var body = Expression.New(constructor, arguments);

            var mapper = Expression.Lambda<Func<IDataReader, T>>(body, dataReaderParam);

            return ToList<T>(mapper.Compile(), sql, parameters);
        }

这样做,我可以完全避免Activator.CreateInstance或FormatterServices.GetUninitializedObject的东西,我敢打赌它快了很多;)


1
投票

问题2:

我真的不知道,但我倾向于使用Activator.CreateObject()而不是FormatterServices.GetUninitializedObject(),因为您的对象可能无法正确创建。 GetUninitializedObject()不会运行像CreateObject()那样的默认构造函数,而且你不一定知道T的黑盒子里有什么...


1
投票

此方法将一行sql查询存储在匿名类型的变量中。您必须将原型传递给方法。如果在sql查询中找不到匿名类型的任何属性,则会使用prototype-value填充它。 C#为其匿名类创建构造函数,这些参数与(只读)属性具有相同的名称。

    public static T GetValuesAs<T>(this SqlDataReader Reader, T prototype)
    {
        System.Reflection.ConstructorInfo constructor = prototype.GetType().GetConstructors()[0];
        object[] paramValues = constructor.GetParameters().Select(
            p => { try               { return Reader[p.Name]; }
                   catch (Exception) { return prototype.GetType().GetProperty(p.Name).GetValue(prototype); } }
            ).ToArray();
        return (T)prototype.GetType().GetConstructors()[0].Invoke(paramValues);
    }
© www.soinside.com 2019 - 2024. All rights reserved.