Bezdepbonusu7Программирование: статьи и ответы

Вопрос

Приведение во время выполнения в С#?

Автор вопроса: Garret WilsonОтветов: 4

Я читаю данные из пользовательского формата данных, который концептуально хранит данные в таблице. Каждый столбец может иметь отдельный тип. Типы зависят от формата файла и сопоставляются с типами 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

Это жалко. :(

  • Возможно, вы захотите проверить реальную стоимость упаковки/распаковки - она не равна нулю, но очень низка. Среда выполнения .NET делает выделение нового объекта чрезвычайно дешевым (стоимость аналогична добавлению указателя), и вы платите только за сборку мусора для объектов, которые существуют во время сборки; мгновенный бокс почти бесплатен. Даже если бы накладные расходы составляли 1 микросекунду (2000-3000 тактовых циклов, в зависимости от скорости процессора) на коробку, миллион добавит накладных расходов всего на 1 секунду. Я считаю, что реальные накладные расходы намного ниже. Bevan 02.10.2010

genericscastingruntime

Ответы

Ответ 1 · рейтинг 1 · Steve Ellinger

Я думаю, что самый близкий способ сделать эту работу - перегрузить readColumnInfo, а не делать ее универсальной, например:

    protected Int32 readColumnValue(Column<Int32> column) {
        return readInt();
    }
    protected Int64 readColumnValue(Column<Int64> column) {
        return readLong();
    }
    protected String readColumnValue(Column<String> column){
        return String.Empty;
    }

Ответ 2 · рейтинг 1 · Bevan

Почему бы вам не реализовать свой собственный оператор приведения от Column<T> к T?

public class Column<T>
{
    public static explicit operator T(Column<T> value)
    {
        return value;
    }

    private T value;
}

Затем вы можете легко конвертировать, когда вам нужно:

Column<int> column = new Column<int>(...)
int value = (int)column;

Ответ 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[]> или что-то подобное.

Перевод вопроса и ответов сообщества Stack Overflow, лицензия CC BY-SA. Оригинал на Stack Overflow.

Похожие материалы