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

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

Это жалко. :(


person Garret Wilson    schedule 24.09.2010    source источник
comment
Возможно, вы захотите проверить реальную стоимость упаковки/распаковки — она не равна нулю, но очень низка. Среда выполнения .NET делает выделение нового объекта чрезвычайно дешевым (стоимость аналогична добавлению указателя), и вы платите только за сборку мусора для объектов, которые существуют во время сборки; мгновенный бокс почти бесплатен. Даже если бы накладные расходы составляли 1 микросекунду (2000-3000 тактовых циклов, в зависимости от скорости процессора) на коробку, миллион добавит накладных расходов всего на 1 секунду. Я считаю, что реальные накладные расходы намного ниже. Беспокоиться о боксе, ИМХО, преждевременная оптимизация.   -  person Bevan    schedule 02.10.2010


Ответы (5)


Я думаю, что самый близкий способ сделать эту работу - перегрузить 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;
    }
person Steve Ellinger    schedule 25.09.2010
comment
struct — это C# способ объявления типа значения. Библиотека .Net определяет тип ValueType, C# не позволит вам использовать его напрямую, однако вы должны использовать зарезервированное слово struct для создания типа значения. Упаковка/распаковка происходит, когда вы приводите тип значения к Object (который является ссылочным типом) и обратно, поскольку мы говорим ему привести к ValueType (не ссылочному типу), упаковка/распаковка не произойдет. Структура where T : сообщает компилятору, что тип должен быть типом значения, поэтому он разрешает первое приведение (ValueType) при возврате. - person Steve Ellinger; 25.09.2010
comment
При дальнейшем исследовании может показаться, что я ошибаюсь, глядя на сгенерированный компилятором msil, код упаковывает и распаковывает глупые вещи. Единственный способ, которым я вижу, как это работает без глупой упаковки/распаковки, - это перегрузить readColumnValue, как я сделал с типами String для всех необходимых типов значений. И да, я убедился, посмотрев в отражатель, что в этом сценарии упаковка/распаковка не происходит. Я сейчас немного зол на себя. - person Steve Ellinger; 25.09.2010
comment
Стив, спасибо за расследование, но я бы хотел, чтобы вы полностью не редактировали свой ответ, заменяя свой первый ответ, потому что теперь мой первый комментарий выше не имеет смысла. :( Просто для протокола: первое предложение Стива заключалось в том, что ограничение метода типами структур (где T : struct) предотвратило бы упаковку путем приведения к (T)(ValueType)readInt(). Я также повторю, что заменяет все универсальные методы с методами отдельных типов, как указывает Стив в своем новом ответе, также потребуют, чтобы все универсальные методы, вызывающие эти методы, были реплицированы и т. д. :( - person Garret Wilson; 25.09.2010
comment
Гаррет, я не был полностью уверен, как справиться с ситуацией, поскольку я мог отредактировать эту запись или добавить новую запись / проголосовать за удаление этой записи и добавить новую запись. Я не уверен, что новый подход поможет вам, потому что я не уверен в контексте вызова метода readColumnValue. Понятно, что у вас есть экземпляр Column‹›, компилятор знает тип и может вызвать правильную перегрузку, тем самым устраняя переключатель, который должен был бы существовать в общей версии readColumnValue, мне кажется, это хорошо - person Steve Ellinger; 25.09.2010

Почему бы вам не реализовать свой собственный оператор приведения от 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;
person Bevan    schedule 30.09.2010
comment
Если уже есть private T value, почему бы вам просто не сделать его общедоступным свойством типа Column<T>? Разве int value = column.Value не является даже более простым, чем int value = (int)column;? - person Dan Tao; 01.10.2010
comment
Если есть уже private T value, то да, вы можете это сделать. Мы не знаем реализацию Column<T> (ОП нам не показывал), поэтому мы не знаем, есть она там или нет. Я включил его в свой пример исключительно для того, чтобы его было легче читать. - person Bevan; 01.10.2010
comment
Столбец‹T› не содержит значения — он просто инкапсулирует концепцию столбца и его тип. Если бы столбец содержал значение, мне не понадобился бы оператор --- я бы просто использовал T getValue(). Скорее, как показано в примере, я просто даю Column‹T› синтаксическому анализатору, чтобы сообщить ему, какой тип анализировать и возвращать мне. - person Garret Wilson; 01.10.2010
comment
Ах. Возможно, вы могли бы перевернуть проблему (или вывернуть наизнанку). Column<T> имеет контекст выполнения, где T привязан к типу, а ваш парсер - нет. Код, который у вас есть внутри синтаксического анализатора, не является универсальным и вынужден иметь дело с вариациями типов с помощью операторов отражения и переключения. Попробуйте переместить ключевые части внутрь Columns<T>, где T — просто другой тип. Также обратите внимание, что .NET Jitter достаточно умен, чтобы избежать избыточной упаковки/распаковки при работе с универсальным методом. - person Bevan; 02.10.2010

Краткий ответ на все это (см. подробности вопроса) заключается в том, что С# не позволяет явное приведение к общему типу 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, я буду плакать.

person Garret Wilson    schedule 30.09.2010
comment
Здесь два важных замечания. Прежде всего, очевидная причина, почему вы не можете просто написать там (T)x, состоит в том, что нет гарантии, что приведение вообще применимо ко всем возможным T (примечание: я говорю здесь не о невозможности приведения вниз, а о случаях, которые будут ошибки времени компиляции в отсутствие дженериков, таких как (long)"foo"). Когда вы сначала выполняете преобразование к (object), на первый взгляд это обходной путь, но на практике это означает нечто иное семантически — точно так же, как (long)(object)123 сильно отличается от (long)123. - person Pavel Minaev; 01.10.2010
comment
Что касается производительности, на самом деле нет необходимости в том, чтобы реализация CLR физически упаковывала или распаковывала что-либо для IL, сгенерированного из (T)(object)x. Ясно, что согласно правилу «как если бы» он мог просто выполнить преобразование напрямую (хотя он должен быть осторожен, чтобы уловить семантику упаковки/распаковки, когда речь идет о совместимости типов, а не о прямом преобразовании!). Так что это строго проблема качества реализации, а не проблема дизайна языка. К сожалению, существующий JIT в .NET не оптимизирует этот случай, поэтому происходит физическая упаковка/распаковка. - person Pavel Minaev; 01.10.2010
comment
Спасибо за ваши общие комментарии, Павел --- они повторяют то, что другие отметили выше в отношении оптимизации компилятора. Но если говорить непосредственно о решении, изложенном в этом ответе, мне любопытно узнать, считаете ли вы, что А) оно решает проблему, Б) будет ли оно вводить бокс и В) будет ли .NET требовать (или запрещать) бокс в этом кейс. P.S. JIT здесь неуместен, поскольку бокс является частью CIL --- я уверен, что вы просто имели в виду компилятор. - person Garret Wilson; 01.10.2010
comment
Конечно, JIT имеет значение. CIL никогда не выполняется, он преобразуется в машинный код с помощью JIT, и это происходит только один раз для каждого типа (ваши опасения по поводу количества итераций цикла не относятся к оптимизации, выполненной во время JIT-компиляции). - person Ben Voigt; 01.10.2010
comment
Бен, вы говорите (а я еще не эксперт в этой области), что если CIL содержит инструкцию блока, то JIT-компилятор может игнорировать ее, если считает, что бокс не нужен? И я предполагаю, что другая большая проблема, которую я пытался решить (как упоминали другие здесь), заключается в том, что если бы компилятор C # оптимизировал упаковку, нам даже не пришлось бы беспокоиться о JIT-компиляторе. Спасибо. - person Garret Wilson; 01.10.2010

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

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

person Ben Voigt    schedule 01.10.2010

person    schedule
comment
Не приведет ли это к ненужной упаковке/распаковке, которой не будет при обычном приведении? - person Garret Wilson; 25.09.2010
comment
Да, он будет упакован, а затем распакует int. Теоретически реализация может оптимизировать это (одна общая реализация выпускается JIT для каждого типа значения, и упаковка может быть удалена, когда это возможно), но я не думаю, что текущие реализации делают это. Если вам нужна скорость, единственный способ создать код. - person Julien Roncaglia; 25.09.2010
comment
На самом деле я сделал это, используя код xsl . google.com/p/labsharp/source/browse/srcs/LabSharp/ и IL.emit code.google.com/p/foodordersuite/source/browse/trunk/, но это также должно быть возможно с использованием деревьев выражений на С#4. оба решения были болью в заднице для реализации. - person Julien Roncaglia; 25.09.2010
comment
Я хочу, чтобы приведение работало не иначе, чем (int)readInt(), и я понимаю, как работает Java type.cast(readInt()) (при обеспечении безопасности типов во время компиляции). Я не вижу смысла вводить ненужную упаковку/распаковку, когда все, что я пытаюсь сделать, это обойти ограничение компилятора, и я на 100% знаю, каким будет тип (для чего и предназначено приведение). - person Garret Wilson; 25.09.2010
comment
Генераторы С# не могут выполнять эту проверку во время компиляции, они являются истинными, существующими во время выполнения дженериками, а не синтаксическим сахаром компилятора, как в Java или C++. У него есть некоторые преимущества, но это один из недостатков (есть и другие, особенно в отношении общего параметра ?, который позволяет java) - person Julien Roncaglia; 25.09.2010
comment
Кстати, мне кажется, что метод приведения в java принимает параметр объекта и возвращает экземпляр класса, поэтому в этом случае Integer... чистый результат заключается в том, что у вас есть один бокс и один распаковщик точно так же, как в С#, если вы используете Этот метод. В этом конкретном случае самым чистым и эффективным решением являются шаблоны C++, поскольку они могут быть специализированы по типу параметра. - person Julien Roncaglia; 25.09.2010
comment
Но я не прошу проверки во время компиляции — я прошу проверку во время выполнения. Если я скажу (MyType)createSomething(), C# скажет: «ОК, я не могу проверить это во время компиляции, поэтому я оставлю это до времени выполнения и пожалуюсь на это тогда». Я просто хочу что-то аналогичное приведению (MyType) с использованием (T). Стирание Java делает это менее возможным, а не более возможным. Вот почему в Java приведение к (T) выдает предупреждение, тогда как в C# можно было бы проверить, добавили бы они эту функцию. Но пусть будет так — думаю, мне придется признать, что это невозможно. - person Garret Wilson; 25.09.2010
comment
@VirtualBlackFox: что касается вашего нового комментария о целых числах Java, да, в Java бокс будет происходить в любом случае для примитивов. Я предполагаю, что мои надежды не оправдались вдвойне, потому что кажется, что в C# бокс не должен был бы происходить, если бы язык допускал приведение универсальных типов, и в сочетании с отсутствием стирания C# мог бы сделать это в два раза лучше, чем дженерики Java. :( - person Garret Wilson; 25.09.2010
comment
Проблема с тем, чтобы оставить его до выполнения, заключается в том, что инструкция IL для приведения - это OpCodes.Castclass и ожидается ссылка на экземпляр класса, поэтому это то, что выдается, поэтому требуется некоторый бокс (один и тот же IL для всех типов T). Теперь, как было сказано ранее, для виртуальной машины теоретически возможно обнаружить Int, Box, Cast Int, Unbox и удалить это, как если бы я хорошо помню, что виртуальная машина Microsoft уже генерирует код x86 для каждого параметра типа значения. Я не проверял, но я предполагаю, что этой оптимизации нет в текущей виртуальной машине. - person Julien Roncaglia; 25.09.2010
comment
Другой случай, который можно оптимизировать таким же образом, — это случай, когда T равно int, и вы выполняете (T)(object)long_value, поскольку теоретически он мог бы вызывать OpCodes.conv вместо того, чтобы проходить через box,castclass,unbox, но для этого я также есть некоторые сомнения, что это реализовано. - person Julien Roncaglia; 25.09.2010
comment
Он не может просто выполнить conv, потому что это будет иметь другую семантику. Например. вы можете conv из long в int, но вы получите исключение приведения, если вы упаковываете long, а затем пытаетесь распаковать его в int. Конечно, он все еще может оптимизировать это, ему просто нужно будет вставить проверки, чтобы точно соответствовать поведению бокса/распаковки относительно совместимости типов. И да - последний раз, когда я проверял вывод JIT для такого рода вещей, он на самом деле не пытался его оптимизировать, поэтому на практике происходит настоящий бокс. - person Pavel Minaev; 01.10.2010
comment
Точно, я написал быстро, не задумываясь, если бы это сработало, conv было бы несовместимо с тем, что происходит с определенными пользователем неявными приведениями в этом случае. Печально, что этот случай не оптимизирован, но с ручной генерацией кода, Reflection.Emit, Expression‹T› и DLR не похоже, что нет решения для ручной оптимизации при необходимости. - person Julien Roncaglia; 04.10.2010