Я читаю данные из пользовательского формата данных, который концептуально хранит данные в таблице. Каждый столбец может иметь отдельный тип. Типы зависят от формата файла и сопоставляются с типами C#.
У меня есть тип столбца, который инкапсулирует идею столбца, с универсальным параметром T, указывающим тип С#, который находится в столбце. Column.FormatType указывает тип с точки зрения типов формата. Итак, чтобы прочитать значение столбца, у меня есть простой метод:
protected T readColumnValue<T>(Column<T> column)
{
switch (column.FormatType)
{
case FormatType.Int:
return (T)readInt();
}
}
Как просто и изящно! Теперь все, что мне нужно сделать, это:
Column<int> column=new Column<int>(...)
int value=readColumnValue(column);
Приведенное выше приведение к типу T будет работать в Java (хотя и с предупреждением), и из-за стирания приведение не будет оцениваться до тех пор, пока значение не будет фактически использовано вызывающей стороной - в этот момент будет выброшено исключение ClassCastException, если приведение было неправильно.
Это не работает в С#. Однако, поскольку C# не отбрасывает универсальные типы, можно сделать его еще лучше! Кажется, я могу запросить тип T во время выполнения:
Type valueType=typeof(T);
Отлично, так что у меня есть тип значения, которое я буду возвращать. Что я могу с этим сделать? Если бы это была Java, потому что существует метод Class.Cast, который выполняет приведение во время выполнения, я был бы свободен дома! (Поскольку каждый класс класса Java имеет параметр универсального типа, указывающий, что класс также обеспечивает безопасность типов во время компиляции.) Нижеследующее из мира моей мечты, где класс C# Type работает как класс Java Class:
protected T readColumnValue<T>(Column<T> column)
{
Type<T> valueType=typeof(T);
switch (column.FormatType)
{
case FormatType.Int:
return valueType.Cast(readInt());
}
}
Очевидно, что Type.Cast() отсутствует --- так что мне делать?
(Да, я знаю, что есть метод Convert.ChangeType(), но он, похоже, выполняет преобразования, а не простое приведение типов.)
Обновление: кажется, что это просто невозможно без упаковки/распаковки с использованием (T)(object)readInt(). Но это неприемлемо. Эти файлы действительно большие, например, 80 МБ. Допустим, я хочу прочитать весь столбец значений. У меня был бы элегантный маленький метод, который использует дженерики и вызывает метод, описанный выше, следующим образом:
public T[] readColumn<T>(Column<T> column, int rowStart, int rowEnd, T[] values)
{
... //seek to column start
for (int row = rowStart; row < rowEnd; ++row)
{
values[row - rowStart] = readColumnValue(column);
... //seek to next row
Упаковка/распаковка для миллионов значений? Это не звучит хорошо. Я нахожу абсурдным, что мне придется отказаться от дженериков и прибегнуть к readColumnInt(), readColumnFloat() и т. д. и воспроизвести весь этот код только для предотвращения упаковки/распаковки!
public int[] readColumnInt(Column<int> column, int rowStart, int rowEnd, int[] values)
{
... //seek to column start
for (int row = rowStart; row < rowEnd; ++row)
{
values[row - rowStart] = readInt();
... //seek to next row
public float[] readColumnFloat(Column<float> column, int rowStart, int rowEnd, float[] values)
{
... //seek to column start
for (int row = rowStart; row < rowEnd; ++row)
{
values[row - rowStart] = readFloat();
... //seek to next row
Это жалко. :(
Ответ 3 · Garret Wilson
Краткий ответ на все это (см. подробности вопроса) заключается в том, что С# не позволяет явное приведение к общему типу T, даже если вы знаете тип T и знаете, что у вас есть значение T --- если вы не хотите live с боксом/распаковкой:
return (T)(object)myvalue;
Это лично кажется серьезным недостатком языка --- в ситуации нет ничего, что говорило бы о необходимости упаковки/распаковки.
Однако есть обходной путь, если вы заранее знаете все возможные типы T. Продолжая пример в вопросе, у нас есть столбец универсального типа T, представляющий табличные данные в файле, и синтаксический анализатор, который считывает значения из столбца на основе типа столбца. Я хотел следующее в парсере:
protected T readColumnValue<T>(Column<T> column)
{
switch (column.FormatType)
{
case FormatType.Int:
return (T)readInt();
}
}
Как обсуждалось, это не работает. Но (предполагая для этого примера, что синтаксический анализатор имеет тип MyParser), вы можете фактически создать другой подкласс Column для каждого T, например:
public abstract class Column<T>
{
public abstract T readValue(MyParser myParser);
}
public class IntColumn : Column<int>
{
public override int readValue(MyParser myParser)
{
return myParser.readInt();
}
}
Теперь я могу обновить свой метод синтаксического анализа, чтобы делегировать его столбцу:
protected T readColumnValue<T>(Column<T> column)
{
return column.readValue(this);
}
Обратите внимание, что в программе используется та же логика - просто создав подкласс универсального типа столбца, мы разрешили специализацию метода для выполнения приведения к T для нас. Другими словами, у нас все еще есть (T)readInt(), просто приведение (T) происходит не в одной строке, а в переопределении метода, который меняется с:
public abstract T readValue(MyParser myParser);
to
public override int readValue(MyParser myParser)
Таким образом, если компилятор может выяснить, как выполнить приведение к T в специализации метода, он должен быть в состоянии понять это при приведении одной строки. Иными словами, ничто не мешает C# иметь метод typeof(T).cast(), который будет делать точно то же самое, что и вышеприведенная специализация методов.
(Что еще больше расстраивает во всем этом упражнении, так это то, что это решение вынудило меня смешивать код синтаксического анализа с моделью объекта данных после того, как я так старался отделить его.)
Теперь, если кто-то скомпилирует это, посмотрит на сгенерированный CIL и обнаружит, что .NET упаковывает/распаковывает возвращаемое значение только для того, чтобы специализированный метод readValue() мог удовлетворить общий тип возвращаемого значения T, я буду плакать.
Ответ 4 · Ben Voigt
Хранятся ли данные в порядке строк или столбцов? Если он находится в порядке строк, то необходимость сканирования всего набора данных (миллионы значений, которые вы сказали) несколько раз, чтобы выбрать каждый столбец, затмит стоимость упаковки.
Я действительно предлагаю делать все за один проход через данные, возможно, путем создания вектора Action<string> (или Predicate<string> для сообщения об ошибках) делегатов, которые обрабатывают одну ячейку, каждая в List<T>, связанную со столбцом. Закрытые делегаты могут очень помочь. Что-то вроде:
public class TableParser
{
private static bool Store(List<string> lst, string cell) { lst.Append(cell); return true; }
private static bool Store(List<int> lst, string cell) { int val; if (!int.TryParse(cell, out val)) return false; lst.Append(val); return true; }
private static bool Store(List<double> lst, string cell) { double val; if (!double.TryParse(cell, out val)) return false; lst.Append(val); return true; }
private static readonly Dictionary<Type, System.Reflection.MethodInfo> storeMap = new Dictionary<Type, System.Reflection.MethodInfo>();
static TableParser()
{
System.Reflection.MethodInfo[] storeMethods = typeof(TableParser).GetMethods("Store", BindingFlags.Private | BindingFlags.Static);
foreach (System.Reflection.MethodInfo mi in storeMethods)
storeMap[mi.GetParameters()[0].GetGenericParameters()[0]] = mi;
}
private readonly List< Predicate<string> > columnHandlers = new List< Predicate<string> >;
public bool TryBindColumn<T>(List<T> lst)
{
System.Reflection.MethodInfo storeImpl;
if (!storeMap.TryGetValue(typeof(T), out storeImpl)) return false;
columnHandlers.Add(Delegate.Create(typeof(Predicate<string>), storeImpl, lst));
return true;
}
// adapt your existing logic to grab a row, pull it apart with string.Split or whatever, and walk through columnHandlers passing in each of the pieces
}
Конечно, вы можете отделить логику синтаксического анализа элементов от логики обхода набора данных, выбрав между storeMap альтернативными словарями для каждого формата. И если вы не храните вещи в виде строк, вы можете использовать Predicate<byte[]> или что-то подобное.