Что такое овеществление?

163

Я знаю, что Java реализует параметрический полиморфизм (Generics) с стиранием. Я понимаю, что такое стирание.

Я знаю, что C # реализует параметрический полиморфизм с овеществлением. Я знаю, что может заставить тебя написать

public void dosomething(List<String> input) {}
public void dosomething(List<Int> input) {}

или что вы можете знать во время выполнения, что является параметром типа некоторого параметризованного типа, но я не понимаю, что это такое .

  • Что такое ограниченный тип?
  • Что такое ценность?
  • Что происходит, когда тип / значение определяется?
Мартейн
источник
Это не ответ, но может как-то помочь: beust.com/weblog/2011/07/29/erasure-vs-reification
heringer
@heringer, который, кажется, довольно хорошо отвечает на вопрос «что такое стирание», и, по-видимому, в основном отвечает «что такое овеществление» на «не стирание» - обычная тема, которую я нашел, когда первоначально искал ответ перед публикацией здесь.
Мартин
5
... и я думал, что ifповторение - это процесс преобразования switchконструкции обратно в if/ else, когда она ранее была преобразована из if/ elseв switch...
Digital Trauma
8
Res , Reis на латыни вещь , так что материализация буквально thingification . У меня нет ничего полезного в том, чтобы использовать этот термин в C #, но сам факт того, что они использовали его, вызывает у меня улыбку.
KRyan

Ответы:

209

Реификация - это процесс взятия абстрактной вещи и создания конкретной вещи.

Термин reification в C # generics относится к процессу, с помощью которого определение универсального типа и один или несколько аргументов универсального типа (абстрактная вещь) объединяются, чтобы создать новый универсальный тип (конкретную вещь).

Выражаясь иначе, это процесс принятия определения List<T>и intи производства конкретного List<int>типа.

Чтобы понять это далее, сравните следующие подходы:

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

    1. Как побочный эффект этой техники реализации, единственными допустимыми аргументами универсального типа являются те типы, которые могут совместно использовать двоичный код своего конкретного типа; что означает те типы, чьи места хранения имеют взаимозаменяемые представления; что означает ссылочные типы. Использование типов значений в качестве аргументов универсального типа требует их упаковки (помещая их в простую обертку ссылочного типа).
    2. Никакой код не дублируется для реализации обобщений таким способом.
    3. Информация о типе, которая могла быть доступна во время выполнения (с использованием отражения), теряется. Это, в свою очередь, означает, что специализация универсального типа (возможность использовать специализированный исходный код для любой конкретной комбинации универсальных аргументов) очень ограничена.
    4. Этот механизм не требует поддержки со стороны среды выполнения.
    5. Существует несколько обходных путей для сохранения информации о типах, которую может использовать Java-программа или язык на основе JVM.
  • В обобщениях C # определение универсального типа сохраняется в памяти во время выполнения. Всякий раз, когда требуется новый конкретный тип, среда выполнения объединяет определение универсального типа и аргументы типа и создает новый тип (reification). Таким образом, мы получаем новый тип для каждой комбинации аргументов типа во время выполнения .

    1. Этот метод реализации позволяет создавать любые комбинации аргументов типа. Использование типов значений в качестве аргументов универсального типа не вызывает бокс, так как эти типы получают свою собственную реализацию. ( Бокс все еще существует в C # , конечно, но это происходит в других сценариях, не в этом.)
    2. Дублирование кода может быть проблемой, но на практике это не так, потому что достаточно умные реализации ( включая Microsoft .NET и Mono ) могут совместно использовать код для некоторых экземпляров.
    3. Информация о типах поддерживается, что позволяет в определенной степени специализироваться путем изучения аргументов типа с использованием отражения. Однако степень специализации ограничена из-за того, что определение универсального типа компилируется до того, как произойдет какое-либо преобразование (это делается путем компиляции определения с ограничениями на параметры типа - таким образом, компилятор должен иметь возможность «понимать» определение даже при отсутствии аргументов конкретного типа ).
    4. Этот метод реализации сильно зависит от поддержки времени выполнения и JIT-компиляции (именно поэтому вы часто слышите, что дженерики C # имеют некоторые ограничения на платформах, таких как iOS , где динамическая генерация кода ограничена).
    5. В контексте обобщений C # реификация выполняется для вас средой выполнения. Однако, если вы хотите более интуитивно понять разницу между определением универсального типа и конкретным типовым универсальным типом, вы всегда можете выполнить самостоятельное преобразование с помощью System.Typeкласса (даже если конкретная комбинация аргументов универсального типа, которую вы создаете, не выполнялась) не появляются в вашем исходном коде напрямую).
  • В шаблонах C ++ определение шаблона сохраняется в памяти во время компиляции. Всякий раз, когда в исходном коде требуется новое создание экземпляра типа шаблона, компилятор объединяет определение шаблона и аргументы шаблона и создает новый тип. Таким образом, мы получаем уникальный тип для каждой комбинации аргументов шаблона во время компиляции .

    1. Этот метод реализации позволяет создавать любые комбинации аргументов типа.
    2. Известно, что это дублирует двоичный код, но достаточно умная цепочка инструментов может обнаружить это и поделиться кодом для некоторых экземпляров.
    3. Само определение шаблона не является «скомпилированным» - фактически скомпилированы только его конкретные экземпляры . Это накладывает меньше ограничений на компилятор и обеспечивает большую степень специализации шаблонов .
    4. Поскольку создание шаблона выполняется во время компиляции, здесь также не требуется поддержка во время выполнения.
    5. Этот процесс в последнее время называют мономорфизацией , особенно в сообществе Rust. Слово используется в отличие от параметрического полиморфизма , который является названием концепции, из которой происходят дженерики.
Теодорос Чатзигианнакис
источник
7
Отличное сравнение с шаблонами C ++ ... кажется, что они находятся где-то между C # и дженериками Java. У вас есть другой код и структура для обработки разных специфических универсальных типов, как в C #, но все это делается во время компиляции, как в Java.
Luaan
3
Кроме того, в C ++ это позволяет ввести специализацию шаблонов, где каждый (или только некоторые) конкретные типы могут иметь разные реализации. Очевидно, что это невозможно в Java, но ни в C #.
Кецалькоатль
@quetzalcoatl, хотя одной из причин его использования является уменьшение объема производимого кода с типами указателей, а C # делает что-то сопоставимое со ссылочными типами за кулисами. Тем не менее, это только одна из причин использования этого, и бывают случаи, когда специализация шаблона будет хорошей.
Джон Ханна
Для Java вы можете добавить, что, пока информация о типе стирается, приведения добавляются компилятором, что делает байт-код неотличимым от байт-кода предварительного кода.
Rusty Core
27

Реификация обычно означает (вне компьютерной науки) «сделать что-то реальное».

В программировании что-то улучшается, если мы можем получить доступ к информации о нем на самом языке.

Для двух совершенно не связанных с дженериками примеров того, что C # делает, а что нет, давайте рассмотрим методы и доступ к памяти.

ОО-языки обычно имеют методы (и многие из них не имеют функций , которые похожи, но не связаны с классом). Таким образом, вы можете определить метод на таком языке, вызвать его, возможно переопределить и так далее. Не все такие языки позволяют вам иметь дело с самим методом как данными для программы. C # (и на самом деле .NET, а не C #) позволяет вам использовать MethodInfoобъекты, представляющие методы, поэтому в C # методы реализованы. Методы в C # являются «объектами первого класса».

Все практические языки имеют некоторые средства для доступа к памяти компьютера. На низкоуровневом языке, таком как C, мы можем напрямую иметь дело с отображением между числовыми адресами, используемыми компьютером, поэтому подобное int* ptr = (int*) 0xA000000; *ptr = 42;разумно (если у нас есть веские основания подозревать, что доступ к адресу памяти 0xA000000таким образом выиграл) взорвать что-нибудь). В C # это не разумно (мы можем просто заставить это сделать в .NET, но с управлением движением памяти .NET это вряд ли будет полезно). C # не имеет адресной памяти.

Таким образом, поскольку refied означает «сделано реальным», «reified type» - это тип, о котором мы можем «говорить» на данном языке.

В общем, это означает две вещи.

Во- первых, List<string>это такой же тип, как stringи intесть. Мы можем сравнить этот тип, получить его имя и узнать о нем:

Console.WriteLine(typeof(List<string>).FullName); // System.Collections.Generic.List`1[[System.String, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089]]
Console.WriteLine(typeof(List<string>) == (42).GetType()); // False
Console.WriteLine(typeof(List<string>) == Enumerable.Range(0, 1).Select(i => i.ToString()).ToList().GetType()); // True
Console.WriteLine(typeof(List<string>).GenericTypeArguments[0] == typeof(string)); // True

Следствием этого является то, что мы можем «говорить» о типах параметров универсального метода (или метода универсального класса) внутри самого метода:

public static void DescribeType<T>(T element)
{
  Console.WriteLine(typeof(T).FullName);
}
public static void Main()
{
  DescribeType(42);               // System.Int32
  DescribeType(42L);              // System.Int64
  DescribeType(DateTime.UtcNow);  // System.DateTime
}

Как правило, делать это слишком "вонючим", но у него много полезных случаев. Например, посмотрите на:

public static TSource Min<TSource>(this IEnumerable<TSource> source)
{
  if (source == null) throw Error.ArgumentNull("source");
  Comparer<TSource> comparer = Comparer<TSource>.Default;
  TSource value = default(TSource);
  if (value == null)
  {
    using (IEnumerator<TSource> e = source.GetEnumerator())
    {
      do
      {
        if (!e.MoveNext()) return value;
        value = e.Current;
      } while (value == null);
      while (e.MoveNext())
      {
        TSource x = e.Current;
        if (x != null && comparer.Compare(x, value) < 0) value = x;
      }
    }
  }
  else
  {
    using (IEnumerator<TSource> e = source.GetEnumerator())
    {
      if (!e.MoveNext()) throw Error.NoElements();
      value = e.Current;
      while (e.MoveNext())
      {
        TSource x = e.Current;
        if (comparer.Compare(x, value) < 0) value = x;
      }
    }
  }
  return value;
}

Это не делает много сравнений между типом TSourceи различными типами для различных поведений (как правило, признак того, что вы вообще не должны были использовать дженерики), но это разделяет путь кода для типов, которые могут быть null(должны возвращаться, nullесли элемент не найден, и он не должен сравнивать, чтобы найти минимум, если один из сравниваемых элементов есть null), и путь к коду для типов, которые не могут быть null(должен выдавать, если элемент не найден, и не должен беспокоиться о возможности nullэлементов ).

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

(Не принято говорить о повторении обобщенных типов в C #, если только вы не имеете дело с Java, потому что в C # мы просто принимаем это преобразование как должное; все типы повторяются. В Java неуниверсальные типы называются reified, потому что это это различие между ними и родовыми типами).

Джон Ханна
источник
Вы не думаете, что сможете сделать то, Minчто выше, полезно? В противном случае очень трудно выполнить задокументированное поведение.
Джон Ханна
Я считаю, что ошибка - это (не) документированное поведение, и подразумевается, что это поведение полезно (кроме того, поведение Enumerable.Min<TSource>отличается тем, что оно не генерирует не-ссылочные типы в пустой коллекции, но возвращает значение по умолчанию). (TSource), и задокументировано только как «Возвращает минимальное значение в общей последовательности». Я бы сказал, что оба должны выдавать пустую коллекцию или что нулевой элемент должен быть передан в качестве базовой линии, а компаратор / функция сравнения всегда должна быть передана)
Martijn
1
Это было бы намного менее полезно, чем текущий Min, который соответствует обычному поведению БД в обнуляемых типах, не пытаясь сделать невозможное для ненулевых типов. (Базовая идея не является невозможной, но не очень полезной, если нет значения, которое, как вы знаете, никогда не будет в источнике).
Джон Ханна
1
Thingification было бы лучшим названием для этого. :)
tchrist
@ вещь может быть нереальной.
Джон Ханна
15

Как уже заметил Даффимо , «овеществление» не является ключевым отличием.

В Java дженерики в основном существуют для улучшения поддержки во время компиляции - это позволяет вам использовать строго типизированные, например, коллекции, в вашем коде и обеспечивать безопасность типов для вас. Однако это существует только во время компиляции - скомпилированный байт-код больше не имеет понятия обобщений; все универсальные типы преобразуются в «конкретные» типы (используя, objectесли универсальный тип не ограничен), добавляя преобразования типов и проверки типов по мере необходимости.

В .NET дженерики являются неотъемлемой особенностью CLR. Когда вы компилируете универсальный тип, он остается универсальным в сгенерированном IL. Он не просто превращается в неуниверсальный код, как в Java.

Это несколько влияет на то, как генерики работают на практике. Например:

  • Java SomeType<?>должна позволять вам передавать любую конкретную реализацию заданного универсального типа. C # не может этого сделать - каждый конкретный ( усовершенствованный ) универсальный тип имеет свой собственный тип.
  • Неограниченные универсальные типы в Java означают, что их значение хранится как object. Это может оказать влияние на производительность при использовании типов значений в таких обобщенных типах. В C #, когда вы используете тип значения в универсальном типе, он остается типом значения.

Для примера давайте предположим, что у вас есть Listуниверсальный тип с одним универсальным аргументом. В Java List<String>и в List<Int>конечном итоге будет точно такого же типа во время выполнения - универсальные типы действительно существуют только для кода времени компиляции. Все вызовы, например, GetValueбудут преобразованы в (String)GetValueи (Int)GetValueсоответственно.

В C # List<string>и List<int>есть два разных типа. Они не являются взаимозаменяемыми, и их безопасность типов также обеспечивается во время выполнения. Независимо от того, что вы делаете, никогда неnew List<int>().Add("SomeString") будет работать - лежащим в основе хранилищем на самом деле является некоторый целочисленный массив, в то время как в Java это обязательно массив. В C # не задействованы броски, бокс и т. Д.List<int>object

Это также должно сделать очевидным, почему C # не может делать то же самое, что и Java SomeType<?>. В Java все универсальные типы, «производные от», в SomeType<?>конечном итоге являются одинаковыми. В C # все различные специфические SomeType<T>типы имеют свой отдельный тип. Удаляя проверки во время компиляции, можно пропустить SomeType<Int>вместо SomeType<String>(и на самом деле все, что SomeType<?>означает, это «игнорировать проверки во время компиляции для данного универсального типа»). В C # это невозможно, даже для производных типов (то есть, вы не можете сделать, List<object> list = (List<object>)new List<string>();даже если stringэто производное от object).

Обе реализации имеют свои плюсы и минусы. Было несколько раз, когда я хотел бы иметь возможность просто разрешить SomeType<?>в качестве аргумента в C # - но это просто не имеет смысла, как работают дженерики C #.

Luaan
источник
2
Ну, вы можете использовать типы List<>, Dictionary<,>и так далее в C #, но разрыв между тем , что и в данном конкретном списке или словарем занимает совсем немного отражения на мост. Дисперсия в интерфейсах помогает в некоторых случаях, когда мы, возможно, когда-то хотели легко преодолеть этот разрыв, но не во всех.
Джон Ханна
2
@JonHanna Вы можете использовать List<>для создания нового определенного универсального типа - но это все равно означает создание определенного типа, который вы хотите. Но вы не можете использовать List<>в качестве аргумента, например. Но да, по крайней мере, это позволяет вам преодолеть разрыв с помощью отражения.
Luaan
.NET Framework имеет три жестко закодированных общих ограничения, которые не являются типами места хранения; все остальные ограничения должны быть типами места хранения. Кроме того, единственные случаи, когда универсальный тип Tможет удовлетворять ограничению типа места хранения, - Uэто когда Tи Uимеют одинаковый тип, или Uэто тип, который может содержать ссылку на экземпляр T. Было бы невозможно иметь осмысленное место хранения типа, SomeType<?>но теоретически было бы возможно иметь общее ограничение этого типа.
суперкат
1
Это неправда, что скомпилированный байт-код Java не имеет понятия обобщений. Просто у экземпляров классов нет понятия обобщений. Это важное различие; Ранее я писал об этом по адресу programmers.stackexchange.com/questions/280169/… , если вам интересно.
Руах
2

Реификация - это объектно-ориентированная концепция моделирования.

Reify - это глагол, означающий «сделать что-то абстрактное реальным» .

Когда вы занимаетесь объектно-ориентированным программированием, обычно моделируют объекты реального мира как программные компоненты (например, «Окно», «Кнопка», «Человек», «Банк», «Автомобиль» и т. Д.).

Также принято преобразовывать абстрактные концепции в компоненты (например, WindowListener, Broker и т. Д.)

duffymo
источник
2
Реификация - это общая концепция «сделать что-то реальное», которая, как вы говорите, применима к объектно-ориентированному моделированию, но также имеет значение в контексте реализации обобщений.
Джон Ханна
2
Так что я получил образование, прочитав эти ответы. Я исправлю свой ответ.
duffymo
2
Этот ответ не имеет ничего общего с интересом OP к дженерикам и параметрическому полиморфизму.
Эрик Дж. Хагстром,
Этот комментарий не делает ничего, чтобы удовлетворить чей-либо интерес или повысить репутацию. Я вижу, вы ничего не предложили. Мой был первым ответом, и он определил реификацию как нечто более широкое.
duffymo
1
Ваш ответ, возможно, был первым, но вы ответили на другой вопрос, а не на тот, который задал ОП, что было бы ясно из содержания вопроса и его тегов. Возможно, вы не прочитали вопрос до того, как написали свой ответ, или, возможно, вы не знали, что термин «овеществление» имеет установленное значение в контексте обобщения. В любом случае, ваш ответ бесполезен. Downvote.
jcsahnwaldt Восстановить Монику