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

时间:2022-11-25 08:14:50

I have created a function that takes a SQL command and produces output that can then be used to fill a List of class instances. The code works great. I've included a slightly simplified version without exception handling here just for reference - skip this code if you want to jump right the problem. If you have suggestions here, though, I'm all ears.

我创建了一个函数,它接受一个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;
    }

As I say, this works just fine. However, I'd like to be able to call a similar function with an anonymous class for obvious reasons.

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

Question #1: it appears that I must construct an anonymous class instance in my call to my anonymous version of this function - is this right? An example call is:

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

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

Question #2: the anonymous version of my ReturnList function is below. Can anyone tell me why the call to info.SetValue simply does nothing? It doesn't return an error or anything but neither does it change the value of the target field.

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

Any ideas?

Note: when I tried to use the FieldInfo array as I did in the function above, the typeFields array had zero elements (even though the objectType shows the field names - strange). Thus, I use TypeDescriptor.GetProperties instead.

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

Any other tips and guidance on the use of reflection or anonymous classes are appropriate here - I'm relatively new to this specific nook of the C# language.

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

UPDATE: I have to thank Jason for the key to solving this. Below is the revised code that will create a list of anonymous class instances, filling the fields of each instance from a query.

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

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

Note that the query has been constructed and the parameters initialized in previous calls to this object's methods. The original code had an inner/outer loop combination so that the user could have fields in their anonymous class that didn't match a field. However, in order to simplify the design, I've decided not to permit this and have instead adopted the db field access recommended by Jason. Also, thanks to Dave Markle as well for helping me understand more about the tradeoffs in using Activator.CreateObject() versus GenUninitializedObject.

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

3 个解决方案

#1


Anonymous types encapsulate a set of read-only properties. This explains

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

  1. Why Type.GetFields returns an empty array when called on your anonymous type: anonymous types do not have public fields.

    为什么Type.GetFields在您的匿名类型上调用时返回一个空数组:匿名类型没有公共字段。

  2. The public properties on an anonymous type are read-only and can not have their value set by a call to PropertyInfo.SetValue. If you call PropertyInfo.GetSetMethod on a property in an anonymous type, you will receive back null.

    匿名类型的公共属性是只读的,并且不能通过调用PropertyInfo.SetValue来设置其值。如果在匿名类型的属性上调用PropertyInfo.GetSetMethod,则会收到null。

In fact, if you change

事实上,如果你改变了

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

to

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

you will receive an exception informing you that the property set method can not be found.

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

Now, to solve your problem, what you can do is use Activator.CreateInstance. I'm sorry that I'm too lazy to type out the code for you, but the following will demonstrate how to use it.

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

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

So just run through a loop, as you've done, to fill up the object array that you need to pass to Activator.CreateInstance and then call Activator.CreateInstance when the loop is done. Property order is important here as two anonymous types are the same if and only if they have the same number of properties with the same type and same name in the same order.

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

For more, see the MSDN page on anonymous types.

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

Lastly, and this is really an aside and not germane to your question, but the following code

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

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

could be simplified by

可以简化

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

#2


I had the same problem, I resolved it by creating a new Linq.Expression that's going to do the real job and compiling it into a lambda: here's my code for example:

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

I want to transform that call:

我想转换那个电话:

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"),
            });

to that call:

那个电话:

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

and do the DataReader.Get things from the new method, the first method is:

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

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

I had to build an expression in the new method:

我必须在新方法中构建一个表达式:

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

Doing this that way, i can completely avoid the Activator.CreateInstance or the FormatterServices.GetUninitializedObject stuff, I bet it's a lot faster ;)

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

#3


Question #2:

I don't really know, but I would tend to use Activator.CreateObject() instead of FormatterServices.GetUninitializedObject(), because your object might not be created properly. GetUninitializedObject() won't run a default constructor like CreateObject() will, and you don't necessarily know what's in the black box of T...

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

#1


Anonymous types encapsulate a set of read-only properties. This explains

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

  1. Why Type.GetFields returns an empty array when called on your anonymous type: anonymous types do not have public fields.

    为什么Type.GetFields在您的匿名类型上调用时返回一个空数组:匿名类型没有公共字段。

  2. The public properties on an anonymous type are read-only and can not have their value set by a call to PropertyInfo.SetValue. If you call PropertyInfo.GetSetMethod on a property in an anonymous type, you will receive back null.

    匿名类型的公共属性是只读的,并且不能通过调用PropertyInfo.SetValue来设置其值。如果在匿名类型的属性上调用PropertyInfo.GetSetMethod,则会收到null。

In fact, if you change

事实上,如果你改变了

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

to

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

you will receive an exception informing you that the property set method can not be found.

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

Now, to solve your problem, what you can do is use Activator.CreateInstance. I'm sorry that I'm too lazy to type out the code for you, but the following will demonstrate how to use it.

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

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

So just run through a loop, as you've done, to fill up the object array that you need to pass to Activator.CreateInstance and then call Activator.CreateInstance when the loop is done. Property order is important here as two anonymous types are the same if and only if they have the same number of properties with the same type and same name in the same order.

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

For more, see the MSDN page on anonymous types.

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

Lastly, and this is really an aside and not germane to your question, but the following code

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

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

could be simplified by

可以简化

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

#2


I had the same problem, I resolved it by creating a new Linq.Expression that's going to do the real job and compiling it into a lambda: here's my code for example:

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

I want to transform that call:

我想转换那个电话:

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"),
            });

to that call:

那个电话:

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

and do the DataReader.Get things from the new method, the first method is:

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

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

I had to build an expression in the new method:

我必须在新方法中构建一个表达式:

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

Doing this that way, i can completely avoid the Activator.CreateInstance or the FormatterServices.GetUninitializedObject stuff, I bet it's a lot faster ;)

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

#3


Question #2:

I don't really know, but I would tend to use Activator.CreateObject() instead of FormatterServices.GetUninitializedObject(), because your object might not be created properly. GetUninitializedObject() won't run a default constructor like CreateObject() will, and you don't necessarily know what's in the black box of T...

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