Как создать и получить доступ к новому экземпляру анонимного класса, переданному в качестве параметра в C#?

Я создал функцию, которая принимает команду SQL и выдает результат, который затем можно использовать для заполнения списка экземпляров класса. Код работает отлично. Я включил здесь немного упрощенную версию без обработки исключений только для справки — пропустите этот код, если хотите сразу решить проблему. Если у вас есть предложения здесь, я все уши.

    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.

Любые другие советы и рекомендации по использованию отражения или анонимных классов здесь уместны - я относительно новичок в этом конкретном уголке языка С#.

ОБНОВЛЕНИЕ: я должен поблагодарить Джейсона за ключ к решению этой проблемы. Ниже приведен измененный код, который создаст список экземпляров анонимного класса, заполняя поля каждого экземпляра из запроса.

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

Обратите внимание, что запрос был создан, а параметры были инициализированы в предыдущих вызовах методов этого объекта. Исходный код имел комбинацию внутреннего/внешнего цикла, так что пользователь мог иметь поля в своем анонимном классе, которые не соответствовали полю. Однако, чтобы упростить дизайн, я решил не допускать этого и вместо этого принял доступ к полю db, рекомендованный Джейсоном. Также спасибо Дейву Марклу за то, что он помог мне лучше понять компромиссы между использованием Activator.CreateObject() и GenUninitializedObject.


person Mark Brittingham    schedule 25.01.2009    source источник


Ответы (4)


Анонимные типы инкапсулируют набор свойств только для чтения. Это объясняет

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

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

вы получите исключение, информирующее вас о том, что метод набора свойств не может быть найден.

Теперь, чтобы решить вашу проблему, вы можете использовать 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, посвященной анонимным типам.

Наконец, и это действительно отступление и не относится к вашему вопросу, а следующий код

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]);
}
person jason    schedule 25.01.2009
comment
Джейсон - спасибо! Я действительно ценю все усилия. Я еще не воспользовался вашим советом, но это именно то, что я искал: особенно информация об ограничениях и Activator.CreateInstance. - person Mark Brittingham; 26.01.2009
comment
И последнее: я рассмотрел предложенное вами упрощение, но я еще не решил, буду ли я требовать, чтобы в запросе было соответствующее поле для каждого поля в объекте, поэтому я пока остаюсь гибким. Спасибо, но все равно хороший совет. - person Mark Brittingham; 26.01.2009
comment
Хммм... на самом деле, учитывая требование, чтобы в конструкторе было поле для каждого поля в исходном объекте, есть немного больше давления, чтобы сделать его требованием, чтобы все поля совпадали (поэтому мне не нужно устанавливать фиктивные значения для отсутствующих полей и т. д.). - person Mark Brittingham; 26.01.2009

У меня была та же проблема, я решил ее, создав новое выражение Linq.Expression, которое будет выполнять настоящую работу, и скомпилировав его в лямбду: например, вот мой код:

Я хочу преобразовать этот вызов:

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, держу пари, это намного быстрее;)

person Guillaume86    schedule 04.02.2011

Вопрос 2:

Я действительно не знаю, но я бы предпочел использовать Activator.CreateObject() вместо FormatterServices.GetUninitializedObject(), потому что ваш объект может быть создан неправильно. GetUninitializedObject() не будет запускать конструктор по умолчанию, как CreateObject(), и вы не обязательно знаете, что находится в черном ящике T...

person Dave Markle    schedule 25.01.2009

Этот метод сохраняет одну строку sql-запроса в переменной анонимного типа. Вы должны передать прототип методу. Если какое-либо свойство анонимного типа не может быть найдено в sql-запросе, оно заполняется значением-прототипом. 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);
    }
person Micha    schedule 19.02.2019