Использование 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();