Использование LINQ для создания списка <T>, где T: someClass <U>

Это связано с предыдущим вопросом моей Преобразование общего списка С# в список реализации класса <T>

У меня есть следующий код:

public abstract class DataField
{
    public string Name { get; set; }
}

public class DataField<T> : DataField
{
    public T Value { get; set; }
}

public static List<DataField> ConvertXML(XMLDocument data) {  
     result = (from d in XDocument.Parse(data.OuterXML).Root.Decendendants()
                      select new DataField<string>
                      {
                          Name = d.Name.ToString(),
                          Value = d.Value
                      }).Cast<DataField>().ToList();  
    return result;
}

Это работает, однако я хотел бы иметь возможность модифицировать часть выбора запроса LINQ, чтобы быть примерно такой:

select new DataField<[type defined in attribute of XML Element]>
{
  Name = d.Name.ToString(),
  Value = d.Value
}

Это просто плохой подход? Является ли это возможным? Любые предложения?

Ответы

Ответ 1

Вот рабочее решение: (Вы должны указать полностью квалифицированные имена типов для вашего атрибута Type, иначе вам нужно каким-то образом настроить сопоставление...)

Я использовал динамическое ключевое слово, вы можете использовать отражение, чтобы установить значение вместо этого, если у вас нет С# 4...

public static void Test()
{
    string xmlData = "<root><Name1 Type=\"System.String\">Value1</Name1><Name2 Type=\"System.Int32\">324</Name2></root>";

    List<DataField> dataFieldList = DataField.ConvertXML(xmlData);

    Debug.Assert(dataFieldList.Count == 2);
    Debug.Assert(dataFieldList[0].GetType() == typeof(DataField<string>));
    Debug.Assert(dataFieldList[1].GetType() == typeof(DataField<int>));
}

public abstract class DataField
{
    public string Name { get; set; }

    /// <summary>
    /// Instanciate a generic DataField<T> given an XElement
    /// </summary>
    public static DataField CreateDataField(XElement element)
    {
        //Determine the type of element we deal with
        string elementTypeName = element.Attribute("Type").Value;
        Type elementType = Type.GetType(elementTypeName);

        //Instanciate a new Generic element of type: DataField<T>
        dynamic dataField = Activator.CreateInstance(typeof(DataField<>).MakeGenericType(elementType));
        dataField.Name = element.Name.ToString();

        //Convert the inner value to the target element type
        dynamic value = Convert.ChangeType(element.Value, elementType);

        //Set the value into DataField
        dataField.Value = value;

        return dataField;
    }

    /// <summary>
    /// Take all the descendant of the root node and creates a DataField for each
    /// </summary>
    public static List<DataField> ConvertXML(string xmlData)
    {
        var result = (from d in XDocument.Parse(xmlData).Root.DescendantNodes().OfType<XElement>()
                      select CreateDataField(d)).ToList();

        return result;
    }
}

public class DataField<T> : DataField
{
    public T Value { get; set; }
}

Ответ 2

Вы не можете сделать это легко в С#. Аргумент generic type должен указываться во время компиляции. Вы можете использовать отражение в противном случае

             int X = 1;
            Type listype = typeof(List<>);
            Type constructed = listype.MakeGenericType(  X.GetType()  );
            object runtimeList = Activator.CreateInstance(constructed);

Здесь мы только что создали List <int> . Вы можете сделать это с помощью своего типа

Ответ 3

Различные экземпляры родового класса на самом деле являются разными классами.
То есть DataField<string> и DataField<int> не являются одним и тем же классом (!)

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

Ответ 4

Я бы сказал, что это плохой подход. На самом деле, даже после анализа вашего XML файла вы не узнаете, какие типы "DataFields" у вас есть. Вы могли бы просто проанализировать их как объекты.

Однако, если вы знаете, что у вас будет только x количество типов, вы можете сделать так:

var Dictionary<string, Func<string, string, DataField>> myFactoryMaps =
{
    {"Type1", (name, value) => { return new DataField<Type1>(name, Type1.Parse(value); } },
    {"Type2", (name, value) => { return new DataField<Type2>(name, Type2.Parse(value); }  },
};

Ответ 5

Термитный ответ, безусловно, отличный. Вот небольшой вариант.

     public abstract class DataField
        {
                public string Name { get; set; }
        }

        public class DataField<T> : DataField
        {
                public T Value { get; set; }
                public Type GenericType { get { return this.Value.GetType(); } }
        }

        static Func<XElement , DataField> dfSelector = new Func<XElement , DataField>( e =>
        {
                string strType = e.Attribute( "type" ).Value;
                //if you dont have an attribute type, you could call an extension method to figure out the type (with regex patterns)
                //that would only work for struct
                Type type = Type.GetType( strType );
                dynamic df = Activator.CreateInstance( typeof( DataField<>).MakeGenericType( type ) );

                df.Name = e.Attribute( "name" ).Value;
                dynamic value = Convert.ChangeType( e.Value , type );
                df.Value = value;
                return df;
        } );

        public static List<DataField> ConvertXML( string xmlstring )
        {
                var result = XDocument.Parse( xmlstring )
                                        .Root.Descendants("object")
                                        .Select( dfSelector )
                                        .ToList();
                return result;
        }


        static void Main( string[] args )
        {
                string xml = "<root><object name=\"im1\" type=\"System.String\">HelloWorld!</object><object name=\"im2\" type=\"System.Int32\">324</object></root>";

                List<DataField> dfs = ConvertXML( xml );
        }

Ответ 6

вы можете создать общий тип путем отражения

    var instance = Activator.CreateInstance( typeof(DataField)
                         .MakeGenericType(Type.GetType(typeNameFromAttribute) );
    // and here set properties also by reflection

Ответ 7

@Termit и @Burnzy предложили хорошие решения, включающие factory методы.

Проблема заключается в том, что вы загружаете свою процедуру разбора с помощью дополнительной логики (больше тестирования, больше ошибок) для сомнительных возвратов.

Другим способом сделать это будет использование упрощенного основанного на строках DataField с типизированными методами чтения - главный ответ для этого вопроса.

Реализация метода типизированного значения, который был бы хорош, но работает только для типов значений (который не включает строки, но включает DateTimes):

public T? TypedValue<T>()
    where T : struct
{
    try { return (T?) Convert.ChangeType(this.Value, typeof(T)); }
    catch { return null; }
}


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

Я делал много такого с подходами, которые кажутся вам похожими.

В конце дня вы должны отделить свои метаданные от своего кода - ответ @Burnzy выбирает код, основанный на метаданных (атрибут "type" элемента DataField) и является очень простым примером этого.

Если вы имеете дело с XML, XSD - очень полезная и расширяемая форма метаданных.

Что касается того, что вы храните в каждом поле данных, используйте строки, потому что:

  • они недействительны
  • они могут хранить частичные значения
  • они могут хранить недопустимые значения (заставляет пользователя сортировать их действие более прозрачным).
  • они могут хранить списки
  • специальные случаи не будут вторгаться в несвязанный код, потому что нет
  • изучать регулярные выражения, проверять, быть счастливыми
  • вы можете легко преобразовать их в более прочные типы.

Мне было очень полезно разрабатывать небольшие каркасы вроде этого - это опыт обучения, и вы поймете гораздо больше о UX и реальности моделирования из него.

Есть четыре группы тестовых примеров, которые я бы посоветовал вам сначала решить:

  • Даты, Times, Timestamps (то, что я называю DateTime), Periods (Timespan)
    • в частности, убедитесь, что вы проверяете наличие другого сервера на клиентском компьютере.
  • списки - многоэкранные внешние ключи и т.д.
  • нулевые значения
  • недопустимый ввод - это обычно включает сохранение исходного значения

Использование строк упрощает все это, потому что это позволяет четко разграничить обязанности в рамках вашей структуры. Подумайте о том, как создавать поля, содержащие списки в вашей общей модели, - они становятся волосатыми довольно быстро, и в конечном итоге легко получить специальный список для списков в почти каждом методе. Со строками, доллар останавливается там.

Наконец, если вам нужна прочная реализация такого рода материалов без необходимости делать что-либо, рассмотрите DataSets - старую школу, которую я знаю - они делают всевозможные чудесные вещи, которых не ожидали бы, но у вас есть RTFM.

Основным недостатком этой идеи было бы то, что она несовместима с привязкой данных WPF, хотя мой опыт в том, что реальность несовместима с привязкой данных WPF.

Надеюсь, я правильно истолковал ваши намерения - удачи в любом случае:)

Ответ 8

К сожалению, нет отношения наследования между C<T> и C<string>, например. Однако вы можете наследовать от общего не-общего класса и в дополнение к этому реализовать общий интерфейс. Здесь я использую явную реализацию интерфейса, чтобы иметь возможность объявлять свойство Value, набранное как объект, а также более специфично типизированное свойство Value. Значения доступны только для чтения и могут быть назначены только через типизированный параметр конструктора. Моя конструкция не идеальна, но безопасна и не использует отражение.

public interface IValue<T>
{
    T Value { get; }
}

public abstract class DataField
{
    public DataField(string name, object value)
    {
        Name = name;
        Value = value;
    }
    public string Name { get; private set; }
    public object Value { get; private set; }
}

public class StringDataField : DataField, IValue<string>
{
    public StringDataField(string name, string value)
        : base(name, value)
    {
    }

    string IValue<string>.Value
    {
        get { return (string)Value; }
    }
}

public class IntDataField : DataField, IValue<int>
{
    public IntDataField(string name, int value)
        : base(name, value)
    {
    }

    int IValue<int>.Value
    {
        get { return (int)Value; }
    }
}

Затем список может быть объявлен с абстрактным базовым классом DataField в качестве общего параметра:

var list = new List<DataField>();
switch (fieldType) {
    case "string":
        list.Add(new StringDataField("Item", "Apple"));
        break;
    case "int":
        list.Add(new IntDataField("Count", 12));
        break;
}

Доступ к строго типизированному полю через интерфейс:

public void ProcessDataField(DataField field)
{
    var stringField = field as IValue<string>;
    if (stringField != null) {
        string s = stringField.Value;
    }
}

Ответ 9

В то время как другие вопросы в основном предлагали элегантное решение для преобразования ваших XML-элементов в экземпляр универсального класса, я собираюсь иметь дело с последствиями подхода к моделированию класса DataField как общего типа, как DataField < [type defined в атрибуте XML Element] > .

После выбора экземпляра DataField в списке вы хотите использовать эти поля. Ее полиморфизм вступает в игру! Вы хотите, чтобы ваши DataFields обрабатывали их единообразно. Решения, использующие генерические средства, часто оказываются в странном переключателе/​​если оргия, поскольку нет простого способа связать поведение, основанное на родовом типе в С#.

Возможно, вы видели такой код (я пытаюсь вычислить сумму всех числовых экземпляров DataField)

var list = new List<DataField>()
{
    new DataField<int>() {Name = "int", Value = 2},
    new DataField<string>() {Name = "string", Value = "stringValue"},
    new DataField<float>() {Name = "string", Value = 2f},
};

var sum = 0.0;

foreach (var dataField in list)
{
    if (dataField.GetType().IsGenericType)
    {
        if (dataField.GetType().GetGenericArguments()[0] == typeof(int))
        {
            sum += ((DataField<int>) dataField).Value;
        }
        else if (dataField.GetType().GetGenericArguments()[0] == typeof(float))
        {
            sum += ((DataField<float>)dataField).Value;
        }
        // ..
    }
}

Этот код является полным беспорядком!

Давайте попробуем полиморфную реализацию с вашим универсальным типом DataField и добавим некоторый метод Sum, который принимает старую часть и возвращает (возможно, измененную) новую сумму:

public class DataField<T> : DataField 
{
    public T Value { get; set; }
    public override double Sum(double sum)
    {
       if (typeof(T) == typeof(int))
       {
           return sum + (int)Value; // Cannot really cast here!
       }
       else if (typeof(T) == typeof(float))
       {
           return sum + (float)Value; // Cannot really cast here!
       }
       // ...

       return sum;
    }
}

Вы можете представить, что ваш код итерации становится намного понятнее, но у вас все еще есть этот странный оператор switch/if в вашем коде. И здесь приходит пункт: Generics не поможет вам здесь, это неправильный инструмент в неправильном месте. Дженерики разработаны в С# для обеспечения безопасности во времени типа компиляции, чтобы избежать потенциальных небезопасных операций литья. Они дополнительно добавляют к читаемости кода, но это не так:)

Посмотрим на полиморфное решение:

public abstract class DataField
{
    public string Name { get; set; }
    public object Value { get; set; }
    public abstract double Sum(double sum);
}

public class IntDataField : DataField
{
    public override double Sum(double sum)
    {
        return (int)Value + sum;
    }
}

public class FloatDataField : DataField
{
    public override double Sum(double sum)
    {
        return (float)Value + sum;
    }
}

Думаю, вам не понадобится слишком много фантазий, чтобы представить, сколько добавляет ваша читаемость/качество кода.

Последний вопрос - как создать экземпляры этих классов. Просто используя условное обозначение TypeName + "DataField" и Activator:

Activator.CreateInstance("assemblyName", typeName);

Краткая версия:

Generics не является подходящим подходом для вашей проблемы, поскольку он не повышает ценность обработки экземпляров DataField. С помощью полиморфного подхода вы можете работать легко с экземплярами DataField!

Ответ 10

Это невозможно, так как вы можете сделать это с отражением. Но это не то, для чего были разработаны дженерики, а не как . Если вы собираетесь использовать отражение, чтобы создать общий тип, вы можете вообще не использовать общий тип и просто использовать следующий класс:

public class DataField
{
    public string Name { get; set; }
    public object Value { get; set; }
}

Ответ 11

Вам нужно будет вставить логику для определения типа данных из вашего XML и добавить все типы, которые вам нужно использовать, но это должно работать:

            result = (from d in XDocument.Parse(data.OuterXML).Root.Descendants()
                      let isString = true //Replace true with your logic to determine if it is a string.
                      let isInt = false   //Replace false with your logic to determine if it is an integer.
                      let stringValue = isString ? (DataField)new DataField<string>
                      {
                          Name = d.Name.ToString(),
                          Value = d.Value
                      } : null
                      let intValue = isInt ? (DataField)new DataField<int>
                      {
                          Name = d.Name.ToString(),
                          Value = Int32.Parse(d.Value)
                      } : null
                      select stringValue ?? intValue).ToList();