Що таке реіфікація?


163

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

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

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

або що ви можете знати під час виконання, що таке параметр типу певного параметризованого типу, але я не розумію, що це таке .

  • Що таке рефікований тип?
  • Що таке змінена цінність?
  • Що відбувається, коли тип / значення повторно змінюється?

Це не відповідь, але може допомогти якось: beust.com/weblog/2011/07/29/erasure-vs-refication
heringer

@heringer, схоже, досить добре відповідає на питання "що таке стирання", і, здається, в основному відповідає "що таке переробка" з "не стирання" - поширена тема, яку я знайшов під час пошуку відповіді перед публікацією тут.
Martijn

5
... і я думав, що реалізація if- це процес перетворення switchконструкції назад в if/ else, коли вона раніше була перетворена з if/ elseв switch...
Digital Trauma

8
Res , reis - це латинська річ , тож реіфікація - це буквально рефетифікація . Я не маю нічого корисного для того, щоб сприяти використанню терміна C #, але сам по собі факт його використання він змушує посміхнутися.
KRyan

Відповіді:


209

Реіфікація - це процес взяття абстрактної речі і створення конкретної речі.

Термін реіфікація в C # generics позначає процес, за допомогою якого загальне визначення типу та один чи більше аргументів загального типу (абстрактна річ) поєднуються для створення нового родового типу (конкретна річ).

Висловлюючись інакше, це процес прийняття ухвали List<T>і intта виробництва конкретного List<int>типу.

Щоб зрозуміти це далі, порівняйте наступні підходи:

  • У Java generics визначення загального типу перетворюється по суті на один конкретний загальний тип, спільний для всіх дозволених комбінацій аргументів типу. Таким чином, кілька типів (рівень вихідного коду) відображаються в один тип (двійковий рівень), але в результаті інформація про аргументи типу примірника відкидається в цьому екземплярі (видалення типу) .

    1. В якості побічного ефекту цієї методики здійснення, єдиними загальними аргументами типу, які в основному є дозволеними, є ті типи, які можуть поділяти двійковий код свого конкретного типу; що означає типи, місця зберігання яких мають взаємозамінні представлення; що означає еталонні типи. Використання типів значень як аргументів загального типу вимагає встановлення боксу (розміщення їх у простому обгортці опорного типу).
    2. Жоден код не дублюється для того, щоб реалізувати дженерики таким чином.
    3. Інформація про тип, яка могла бути доступна під час виконання (за допомогою відображення), втрачається. Це, в свою чергу, означає, що спеціалізація загального типу (можливість використовувати спеціалізований вихідний код для будь-якої конкретної комбінації загальних аргументів) дуже обмежена.
    4. Цей механізм не вимагає підтримки з боку середовища виконання.
    5. Існує кілька обхідних завдань, щоб зберегти інформацію про тип, яку може використовувати програма Java або мова, заснована на JVM.
  • У C # generics визначення загального типу зберігається в пам'яті під час виконання. Кожного разу, коли потрібен новий тип конкретного типу, середовище виконання комбінує загальне визначення типу та аргументи типу та створює новий тип (перероблення). Таким чином, ми отримуємо новий тип для кожної комбінації аргументів типу під час виконання .

    1. Цей метод реалізації дозволяє створити будь-яку комбінацію аргументів типу. Використання типів значень як аргументів загального типу не спричиняє боксу, оскільки ці типи отримують власну реалізацію. ( Бокс все ще існує в C # , звичайно - але це трапляється і в інших сценаріях, не в цьому.)
    2. Дублювання коду може бути проблемою, але на практиці це не так, оскільки досить розумні реалізації ( сюди входять Microsoft .NET і Mono ) можуть ділитися кодом для деяких екземплярів.
    3. Інформація про тип зберігається, що дозволяє спеціалізуватися в певній мірі шляхом вивчення аргументів типу за допомогою відображення. Однак ступінь спеціалізації обмежена, як результат того, що загальне визначення типу складається до того, як відбудеться будь-яка модифікація (це робиться шляхом складання визначення проти обмежень для параметрів типу - таким чином, компілятор повинен бути в змозі "зрозуміти" визначення навіть за відсутності аргументів конкретного типу ).
    4. Ця техніка реалізації значною мірою залежить від підтримки виконання та JIT-компіляції (саме тому ви часто чуєте, що C # generics має деякі обмеження на таких платформах, як iOS , де динамічне генерування коду обмежено).
    5. У контексті C # generics виправлення для вас проводиться середовищем виконання. Однак, якщо ви хочете більш інтуїтивно зрозуміти різницю між визначенням загального типу та конкретним загальним типом, ви завжди можете виконати переробку самостійно, використовуючи System.Typeклас (навіть якщо конкретна комбінація аргументів загального типу, яку ви інстанціюєте, не робила ' t відображатись у вихідному коді безпосередньо).
  • У шаблонах C ++ визначення шаблону зберігається в пам'яті під час компіляції. Щоразу, коли потрібна нова інстанція типу шаблону у вихідному коді, компілятор поєднує визначення шаблону та аргументи шаблону та створює новий тип. Таким чином, ми отримуємо унікальний тип для кожної комбінації аргументів шаблону під час компіляції .

    1. Цей метод реалізації дозволяє створити будь-яку комбінацію аргументів типу.
    2. Це, як відомо, дублює двійковий код, але достатньо розумна ланцюжок інструментів все ж може виявити це та поділити код за деякими моментами.
    3. Саме визначення шаблону не "складено" - фактично складені лише його конкретні моменти . Це ставить менше обмежень на компілятор і дозволяє більш високу ступінь спеціалізації шаблонів .
    4. Оскільки екземпляри шаблонів виконуються під час компіляції, тут також не потрібна підтримка часу виконання.
    5. Останнім часом цей процес називають мономорфізацією , особливо в громаді Раст. Слово вживається на відміну від параметричного поліморфізму , що є назвою концепції, з якої походить генерік.

7
Чудове порівняння з шаблонами C ++ ... вони, схоже, потрапляють десь між C # і дженериками Java. У вас різний код і структура для обробки різних специфічних типів, як-от у C #, але все це робиться за час компіляції, як у Java.
Луань

3
Крім того, в C ++ це дозволяє запровадити спеціалізацію шаблонів, де кожен (або лише деякі) конкретні типи можуть мати різну реалізацію. Очевидно, що це не можливо на Java, але ні на C #.
quetzalcoatl

@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 # не має змінених адрес пам'яті.

Отож, як рефіковані засоби "зробили реальним" "рефіфікований тип" - це тип, про який ми можемо "поговорити" у відповідній мові.

У генериці це означає дві речі.

По- перше, 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, не загальні типи називається. Упредметнені бо - це відмінність між ними та родовими типами).


Ви не думаєте, що вміти робити те, Minщо вище є корисним? Дуже важко виконати його документально підтверджене поведінку інакше.
Джон Ханна

Я вважаю, що помилка - це (не) задокументована поведінка, і це означає, що така поведінка є корисною (як сторону, поведінка Enumerable.Min<TSource>відрізняється тим, що вона не кидає для нереференційних типів у порожній колекції, але повертає за замовчуванням (TSource), і це задокументовано лише як "Повертає мінімальне значення в загальній послідовності". Я заперечую, що обидва повинні кидати на порожню колекцію, або що "нульовий" елемент повинен бути переданий як базовий рівень, а компаратор / Функція порівняння завжди повинна бути передана)
Martijn

1
Це було б набагато менш корисно, ніж поточний Min, який відповідає звичайній поведінці db на зведених типах без спроб неможливого для ненульованих типів. (Ідея базової лінії не є неможливою, але не дуже корисною, якщо в джерелі не існує значення, яке ви можете знати, ніколи не було).
Джон Ханна

1
Очеревиця була б кращою назвою для цього. :)
tchrist

@tchrist річ може бути нереальною.
Джон Ханна

15

Як вже зазначалося в дуффімо , "переробка" не є ключовою різницею.

У Java в цілому дженеріки є, щоб покращити підтримку часу компіляції - вона дозволяє використовувати сильно набрані, наприклад, колекції у вашому коді та захищати безпеку типу. Однак це існує лише під час компіляції - у складеному байтовому коді більше немає поняття генерики; всі родові типи перетворюються на "конкретні" типи (використовуючи, objectякщо загальний тип не обмежений), додаючи перетворення типів та перевіряючи тип, якщо це необхідно.

У .NET, дженерики є невід'ємною особливістю CLR. Коли ви компілюєте загальний тип, він залишається загальним у створеному ІР. Він не просто перетворюється на негенеричний код, як у 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")вони ніколи не працюватимуть - базове сховище в насправдіList<int> є деяким цілим масивом, тоді як у Java це обов'язково масив. У C # немає жодних закидів, боксу тощо.object

Це також повинно зробити зрозумілим, чому C # не може робити те саме, що і Java SomeType<?>. У Java всі родові типи "похідні від" в SomeType<?>кінцевому підсумку є точно таким же типом. У C # всі різні специфічні SomeType<T>s мають свій окремий тип. Видаляючи перевірки часу компіляції, можна пройти SomeType<Int>замість SomeType<String>(і насправді, все це SomeType<?>означає "ігнорувати перевірки часу компіляції для даного загального типу"). У C # це неможливо, навіть не для похідних типів (тобто ви не можете зробити це, List<object> list = (List<object>)new List<string>();навіть якщо stringпохідне від object).

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


2
Ну, ви можете використовувати типи List<>, Dictionary<,>і так далі в C #, але розрив між тим , що і в даному конкретному списку або словником займає зовсім небагато відображення на міст. Різниця в інтерфейсах допомагає в деяких випадках, коли ми, можливо, хотіли колись легко усунути цей пробіл, але не у всіх.
Джон Ханна

2
@JonHanna Ви можете використовувати List<>для створення нового конкретного загального типу - але це все ще означає створення конкретного типу, який ви хочете. Але ви не можете використовувати List<>як аргумент, наприклад. Але так, принаймні це дозволяє подолати розрив за допомогою рефлексії.
Луань

.NET Framework має три жорстко кодованих загальних обмеження, які не є типом місця зберігання; всі інші обмеження повинні бути типами місця зберігання. Крім того, єдиний раз, коли загальний тип Tможе задовольнити обмеження типу зберігання-розташування U, коли це Tі Uє той самий тип, або Uце тип, який може містити посилання на екземпляр T. Неможливо мати значиме місце зберігання типу, SomeType<?>але теоретично було б можливо загальне обмеження цього типу.
supercat

1
Це неправда, що компільований байт-код Java не має поняття генерики. Просто екземпляри класу не мають поняття про дженерики. Це важлива різниця; Раніше я писав про це на programmers.stackexchange.com/questions/280169/… , якщо вас цікавить.
ruakh

2

Реіфікація - це об'єктно-орієнтована концепція моделювання.

Reify - це дієслово, яке означає «зробити щось абстрактне реальним» .

Коли ви робите об'єктно-орієнтоване програмування, зазвичай моделюють об'єкти реального світу як компоненти програмного забезпечення (наприклад, вікно, кнопка, людина, банк, транспортний засіб тощо)

Також звичайно перетворювати абстрактні поняття на компоненти (наприклад, WindowListener, Broker тощо).


2
Реіфікація - це загальне поняття "зробити щось реальне", яке, як ви говорите, стосується об'єктно-орієнтованого моделювання, але має значення і в контексті впровадження генерики.
Джон Ханна

2
Тож я отримав освіту, читаючи ці відповіді. Я допомню свою відповідь.
duffymo

2
Ця відповідь нічого не допомагає вирішити інтерес ОП до генерики та параметричного поліморфізму.
Ерік Г. Хагстром

Цей коментар нічого не стосується інтересів когось або посилення вашого представника. Я бачу, ти нічого не пропонував. Моя перша відповідь, і вона визначила переосмислення як щось більш широке.
duffymo

1
Ваша відповідь, можливо, була першою, але ви відповіли на інше питання, а не на запитання ОП, що було б зрозуміло зі змісту питання та його тегів. Можливо, ви не прочитали ретельно питання, перш ніж писали свою відповідь, або, можливо, ви не знали, що термін "реіфікація" має усталене значення в контексті генерики. Так чи інакше, ваша відповідь не корисна. Downvote.
jcsahnwaldt Reinstate Monica
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.