Страницы

среда, 23 декабря 2009 г.

Сравнивая дженерики в C#, C++ и Delphi(Win32) (перевод)

Перевод статьи: Comparing C#, C++, and Delphi (Win32) Generics. Автор: Craig Stuntz



В языках C #, C++ и Delphi есть возможность использовать обобщённые типы и функции. Хотя все три языка используют статическую типизацию, дженерики в них реализованы очень разными способами. Я собираюсь дать краткий обзор различий, как в плане возможностей языка, так и в плане особенностей реализации. Я предполагаю, что в Delphi Prism дженерики работают точно так же, как и в C#, которые, как вы увидите, отличаются от дженериков в Delphi/Win32.

Позвольте мне прежде всего сказать, что, хотя все три системы работают по-своему, я не вижу явного преимущества в каком-либо из этих дизайнов. В большинстве случаев, вы можете делать то, что вам нужно, во всех трех средах. Я пишу эту статью не для того, чтобы сказать, что какая-то система лучше, чем другие, а для того, чтобы указать на некоторые тонкости в реализациях.

Прежде чем я начну, я хотел бы поблагодарить Барри Келли за его полезные замечания по первому черновому варианту этой статьи.

Компилируя инстанцирования

Процесс использования любого обобщённого типов проходит через два этапа. Сначала, вы определяете обобщённый тип или метод с помощью "меток" для типа, который будет заменен в дальнейшем. Позже (когда именно, зависит от языка), этот тип инстанцируется. Следует отметить, что инстанцирование обобщённого типа сильно отличается от инстанцирования объекта. Первое происходит при компиляции, тогда как второе происходит уже во время выполнения.

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

В C# и Delphi, существует возможности языка, которые посвящены исключительно реализации обобщённых типов и методов. В C++, с другой стороны, такой механизм как "шаблоны" может быть использован для реализации обобщённых типов и методов, а также многих, многих других вещей. С их помощью можно даже заниматься программированием общего назначения, используя шаблоны, которые C++ программисты называют "метапрограммированием".

Шаблоны C++ требуют чтобы шаблон исходного кода был доступен при компиляции кода использующего этот шаблон.Это происходит потому, что компилятор реально компилирует шаблон не в качестве самостоятельной сущности, а сначала инстанцирует шаблон "на месте" и тогда компилирует полученный экземпляр (инстанцию). В C++ компилятор эффективно генерирует код, заменяя параметр типа (или значение) в метках-заполнителях для данного типа, и генерируя новый код для экземпляра (инстанцирования). Дополнено: Moritz Beutel раскрывает эту тему в своём прекрасном комментарии к посту (англ.).

В Delphi и C#, с другой стороны, обобщённый тип или метод и код, который использует обобщённый тип или метод, могут быть скомпилированы отдельно. Таким образом, вы можете скомпилировать библиотеку, которая содержит обобщённый тип, а затем скомпилировать исполняемый файл, который будет использовать экземпляр с этим типом и ссылаться на бинарную библиотеку, вместо исходного кода библиотеки.

Другой способ представить это различие, это считать, что в C++, шаблон не будет скомпилирован до тех пор, пока он будет использован. А в Delphi и C#, обобщённый тип или метод обязан быть скомпилирован прежде чем он может быть использован.

В Delphi, компилятор использует решения, которые тесно связаны с встраиванием методов (method inlining). Для этого компилятор сохраняет соответствующие фрагменты абстрактного синтаксического дерева относящегося к обобщённому параметру в скомпилированном DCU-файле. А когда компилируется код, использующий обобщённый тип, этот фрагмент абстрактного синтаксического дерева включается в синтаксическое дерево кода, использующего обобщённый тип. Так что, в тот момент, когда генерируется машинный код, он уже основан на новом, "объединённом" абстрактном синтаксическом дереве. Для эмиттера кода (code emitter) это выглядит как тип с жёстко закодированным типом параметра. Вместо линковки к уже скомпилированного в DCU коду, код, использующий обобщённый тип, генерирует (emits) отдельный новый код для этого инстанцирования в своей собственной DCU.

Из-за того, что инстанцирование дженериков обрабатывается той же частью компилятора Delphi, которая отвечает за встраивание методов, существуют некоторые ограничения на использование обобщённых методов и типов. Как встроенные методы, обобщённые методы не могут содержать ASM. Кроме того, вызовы таких методов не могут быть встроены. Эти ограничения являются ограничениями реализации, а не дизайна языка, и, теоретически, могут быть убраны в будущей версии компилятора.

Дженерики в C# используют .NET Framework 2.0+, который имеет встроенную поддержку обобщённых типов. C# компилятор генерирует IL в котором указывается, что общий тип должен быть использован, с определенными параметрами типа. .NET Framework реализует эти типы, используя одно инстанцирование для любого ссылочного типа, а также пользовательское инстанцирование для типов значений. (не путайте “инстанцирование” и "инстанция, экземпляр" в предыдущем предложении; они означают совершенно разные вещи в этом контексте. Обычно существует много инстанций (экземпляров) одного инстанцирования.) Это происходит потому, что ссылка на ссылочный тип всегда одного и того же размера, в то время как типы значений могут быть разных размеров. Позднее IL будет скомпилирован “на лету” в машинный код, и, как и в скомпилированном коде C++ или Delphi, на уровне машинного кода типов на самом деле не существует. В .NET, инстанцирование обобщённого типа и компиляция “на лету” – это две отдельные операции.

Таким образом, одно важное отличие в реализации генериков проявляется в момент появления инстанцирования. Это происходит очень рано при компиляции С++, несколько позже для компиляции Delphi, и как можно позже, при компиляции .NET.

 

Пользовательская специализация

Другим очень важным отличием является то, что в C++ разрешено пользовательское инстанцирование, называемые специализаций, в том числе специализации по значению. С# и Delphi, с другой стороны, единственный способ инстанцировать обобщённый тип заключается в том, чтобы использовать такой типа с явным параметром типа. Реализация всегда будет одинаковой, за исключением типа параметра типа. Поскольку в C++ разрешено пользовательское инстанцирование, программистам становится легче писать различные реализации метода, например, для различных целочисленных значений. Как и перегрузки операторов, это мощное средство, которое требует значительной сдержанности в использовании, чтобы избежать злоупотреблений.

 

Ограничения дженериков

И Delphi и C# имеют возможность ограничений для дженериков, которая позволяют разработчику обобщённого типа или метода ограничивать используемые параметры типа. Например, обобщённый тип, который должен использоваться при переборе некоторого списка данных, может потребовать, чтобы параметр типа поддерживал интерфейс IEnumerable. Это справедливо для любого языка. Это позволяет разработчику обобщенного типа, чётко продемонстрировать свои намерения в отношении использования этого типа. Это также позволяет IDE обеспечить завершение кода/IntelliSense для типа параметра, подпадающее под определение обобщённого типа. Кроме того, ограничения позволяет пользователю обобщённого типа, быть уверенными, что они удовлетворяют условиям для значения параметра типа без необходимости компилировать код для того, чтобы проверить это.

В C++, с другой стороны, на данный момент такой возможности не существует. Для этого предназначалась более мощная/сложная фича названием "концепт", но в конце концов она была убрана из C++0x.

Последствием отсутствия ограничений является то, что C++ шаблоны C++ типизируются по-утиному. Если обобщённый метод вызывает какой-то другой метод Foo, по типу переданному в качестве параметра обобщённого типа, то шаблон будет скомпилирован только в том случае, если тип переданного параметра будет содержать какой-то метод с названием Foo и с соответствующей сигнатурой, независимо от того, где и как она определена.

 

Ковариантность и контрвариантность

Предположим, у меня есть функция, которая принимает аргумент типа IEnumerable<TParent>. Могу ли я передать аргумент типа IEnumerable<TChild> этой функции? Что, если тип аргумента были List<TParent> вместо IEnumerable<TParent> Или что если обобщённый тип был результатом функции, а не аргументом? Формально, эти проблемы называют ковариантность и контрвариантность. Точные детали слишком сложны, чтобы объяснять их в этой статье, но приведенные выше примеры дают общее представление о наиболее распространенных случаях связанных с этой проблемой.

Генерики Delphi и шаблоны С++ не поддерживают ковариантность и контрвариантность. Таким образом, ответы на вышеперечисленные вопросы будут такими: нет, нет и нет; хотя есть, конечно, обходные пути, такие как копирование данных в новый список. В C# 4.0, аргументы функции и результаты могут быть объявлены ковариантно или контравариантно, поэтому, приведенные выше примеры там можно заставить работать, при необходимости. "При необходимости" включает в себя нетривиальные тонкости упомянутые выше, и иллюстрируется тем фактом, что массивы в. NET обладают (намеренно) сломанной ковариантностью. Тем не менее, BCL процедуры в .NET Framework 4.0, были аннотированы с поддержкой ковариантности и контрвариантности когда это уместно, так что разработчики выиграют от этого без необходимости в полной мере понимать это.

 



Словарик

примечание к словарику: в нём упомянуты термины, которые я использовал при переводе, но это необязательно делает их корректными. ;)


Ссылки по теме

6 комментариев:

  1. "Code emitter" - это, наверное, "генератор кода".

    ОтветитьУдалить
  2. Полезная статья, но прочитать с первого раза не получилось, пришлось обратиться к первоисточнику, что не очень помогло в некоторых моментах (хорошо, что я раньше это знал :). Всё-таки запись в блоге - это не более и не менее, чем запись в блоге.

    Навскидку:
    1. "программированием общего назначения" я сначала для себя воспринял как "обобщённым программированием", но затем в первоисточнике увидел "general-purpose programming", после чего оканчательно выпал в осадок. Интересно, что имел в виду автор?

    2. value type - это тип-значение, то есть тип данных, переменная которого содержит непосредственное значение, а не ссылку

    3. Инстанция, инстанцирование, инстация и др. - это очень тяжко :)Есть хорошее русское слово "конкретизация" (в смысле разницы между обобщённым типом и конкретным типом) или уж "инстанцирование" в одной грамматической форме. Причём, если следовать русской традиции "инстанцирование" - это процесс, результат которого - экземпляр. Но в английском всё не так (виноват Страус труп и др.), поскольку смыслов понавешали очень много (class - object, template - template instantiation - instance).

    Если ,напрмиер, понятие "дженерик" ещё можно принять для повышения краткости и дистинктивности, то "инстанцирование" этим критериям не удовлетворяет.

    4. "линковка" - жаргонизм, лучше "компоновка"

    В любом случае спасибо!

    ОтветитьУдалить
  3. Алексей, можно ли передать(из/в DLL) Объект типа интерфейс IEnumerable из Delphi в С# или С++, будет ли он совместим?

    ОтветитьУдалить