Использование LINQ для создания списка <T>, где T: someClass <U> - PullRequest
14 голосов
/ 17 ноября 2011

Это связано с моим предыдущим вопросом Преобразование общего списка в C # в список, реализующий класс

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

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
}

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

Ответы [ 11 ]

8 голосов
/ 23 ноября 2011

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

Я использовал ключевое слово dynamic, вы можете использовать отражение длявместо этого установите значение, если у вас нет C # 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; }
}
5 голосов
/ 17 ноября 2011

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

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

Здесь мы только что создали список . Вы можете сделать это с вашим типом

4 голосов
/ 23 ноября 2011

Ответ Термита, безусловно, превосходен. Вот маленький вариант.

     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 );
        }
4 голосов
/ 17 ноября 2011

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

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

4 голосов
/ 17 ноября 2011

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

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

3 голосов
/ 17 ноября 2011

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

    var instance = Activator.CreateInstance( typeof(DataField)
                         .MakeGenericType(Type.GetType(typeNameFromAttribute) );
    // and here set properties also by reflection
2 голосов
/ 27 ноября 2011

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

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

Возможно, вы видели такой код (я пытаюсь вычислить сумму всех числовых экземпляров 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 , который принимает старые some и возвращает (возможно измененную) новую сумму:

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 в вашем коде. И вот тут-то и дело: дженерики здесь не помогут, это неправильный инструмент в неправильном месте. Обобщения разработаны в C # для обеспечения безопасности типов времени компиляции во избежание возможных небезопасных операций приведения. Они дополнительно повышают читабельность кода, но это не так:)

Давайте посмотрим на полиморфное решение:

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

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

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

2 голосов
/ 25 ноября 2011

К сожалению, между 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;
    }
}
2 голосов
/ 24 ноября 2011

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1 голос
/ 22 ноября 2011

Вам нужно будет вставить логику для определения типа данных из вашего 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();
...