Я хотел бы собрать как можно больше информации о версиях API в .NET / CLR и, в частности, о том, как изменения API нарушают или не нарушают клиентские приложения. Сначала давайте определим некоторые термины:
Изменение API - изменение в общедоступном определении типа, включая любого из его открытых членов. Это включает в себя изменение типа и имен элементов, изменение базового типа типа, добавление / удаление интерфейсов из списка реализованных интерфейсов типа, добавление / удаление элементов (включая перегрузки), изменение видимости элемента, переименование метода и параметров типа, добавление значений по умолчанию для параметров метода, добавление / удаление атрибутов для типов и членов, а также добавление / удаление параметров общих типов для типов и членов (я что-то пропустил?). Это не включает какие-либо изменения в членских органах или какие-либо изменения в частных членах (т.е. мы не принимаем во внимание Рефлексию).
Разрыв двоичного уровня - изменение API, в результате которого клиентские сборки, скомпилированные для более старой версии API, потенциально не загружаются с новой версией. Пример: изменение сигнатуры метода, даже если он позволяет вызываться так же, как и раньше (то есть: void для возврата значений по умолчанию для типов / параметров).
Разрыв на уровне исходного кода - изменение API, в результате которого существующий код, написанный для компиляции со старой версией API, потенциально не компилируется с новой версией. Однако уже скомпилированные клиентские сборки работают как и прежде. Пример: добавление новой перегрузки, которая может привести к неоднозначности в вызовах методов, которые были однозначными в предыдущем.
Изменение тихой семантики на уровне исходного кода - изменение API, в результате которого существующий код, написанный для компиляции со старой версией API, незаметно меняет свою семантику, например, путем вызова другого метода. Однако код должен продолжать компилироваться без предупреждений / ошибок, а ранее скомпилированные сборки должны работать как прежде. Пример: реализация нового интерфейса в существующем классе, который приводит к другой перегрузке, выбранной во время разрешения перегрузки.
Конечная цель состоит в том, чтобы каталогизировать как можно больше ломающих и тихих изменений API семантики и описать точный эффект поломки, а также то, какие языки влияют на него и не затрагиваются им. Более подробно о последнем: хотя некоторые изменения повлияют на все языки повсеместно (например, добавление нового члена в интерфейс нарушит реализацию этого интерфейса на любом языке), некоторые требуют очень специфической семантики языка для вступления в игру, чтобы получить разрыв. Обычно это связано с перегрузкой методов и вообще с тем, что связано с неявными преобразованиями типов. Кажется, нет никакого способа определить «наименее общий знаменатель» здесь даже для CLS-совместимых языков (то есть тех, которые соответствуют, по крайней мере, правилам «потребителя CLS», как определено в спецификации CLI) - хотя я Буду признателен, если кто-то исправит меня как неправильную здесь - так что это будет идти от языка к языку. Естественно, наиболее интересны те, которые поставляются с .NET из коробки: C #, VB и F #; но другие, такие как IronPython, IronRuby, Delphi Prism и т. д. также актуальны. Чем больше это угловой ситуации, тем интереснее это будет - такие вещи, как удаление элементов, довольно очевидны, но тонкое взаимодействие между, например, перегрузкой метода, необязательными параметрами / параметрами по умолчанию, выводом типа лямбда-выражения и операторами преобразования может быть очень удивительным во время.
Вот несколько примеров, чтобы начать это:
Добавление нового метода перегрузки
Вид: разрыв на уровне источника
Затрагиваемые языки: C #, VB, F #
API до изменения:
public class Foo
{
public void Bar(IEnumerable x);
}
API после изменения:
public class Foo
{
public void Bar(IEnumerable x);
public void Bar(ICloneable x);
}
Пример клиентского кода, работающего до изменения и сломанного после него:
new Foo().Bar(new int[0]);
Добавление новых неявных перегрузок операторов преобразования
Вид: разрыв на уровне источника.
Затрагиваемые языки: C #, VB
Языки, не затронутые: F #
API до изменения:
public class Foo
{
public static implicit operator int ();
}
API после изменения:
public class Foo
{
public static implicit operator int ();
public static implicit operator float ();
}
Пример клиентского кода, работающего до изменения и сломанного после него:
void Bar(int x);
void Bar(float x);
Bar(new Foo());
Примечания: F # не сломан, потому что он не поддерживает языковые уровни для перегруженных операторов, ни явных, ни неявных - оба должны вызываться напрямую как op_Explicit
и op_Implicit
методы.
Добавление новых методов экземпляра
Вид: изменение тихой семантики на уровне источника.
Затрагиваемые языки: C #, VB
Языки, не затронутые: F #
API до изменения:
public class Foo
{
}
API после изменения:
public class Foo
{
public void Bar();
}
Пример клиентского кода, который подвергается тихому изменению семантики:
public static class FooExtensions
{
public void Bar(this Foo foo);
}
new Foo().Bar();
Примечания: F # не сломан, потому что он не поддерживает языковой уровень ExtensionMethodAttribute
и требует, чтобы методы расширения CLS вызывались как статические методы.
источник
Ответы:
Изменение подписи метода
Вид: разрыв двоичного уровня
Затрагиваемые языки: C # (VB и F #, скорее всего, но не проверено)
API до изменения
API после изменения
Пример кода клиента, работающего до изменения
источник
bar
.Добавление параметра со значением по умолчанию.
Вид разрыва: разрыв двоичного уровня
Даже если исходный код вызова не нужно менять, его все равно нужно перекомпилировать (как при добавлении обычного параметра).
Это потому, что C # компилирует значения параметров по умолчанию непосредственно в вызывающую сборку. Это означает, что если вы не перекомпилируете, вы получите MissingMethodException, потому что старая сборка пытается вызвать метод с меньшим количеством аргументов.
API до изменения
API после изменения
Образец клиентского кода, который впоследствии нарушается
Код клиента должен быть перекомпилирован
Foo(5, null)
на уровне байт-кода. Вызываемая сборка будет содержать толькоFoo(int, string)
, неFoo(int)
. Это связано с тем, что значения параметров по умолчанию являются чисто языковой функцией, а среда выполнения .Net о них ничего не знает. (Это также объясняет, почему значения по умолчанию должны быть константами времени компиляции в C #).источник
Func<int> f = Foo;
// это не удастся с измененной подписьюКогда я его обнаружил, он был совершенно неочевиден, особенно в свете различий в той же ситуации для интерфейсов. Это вовсе не перерыв, но достаточно удивительно, что я решил включить его:
Рефакторинг членов класса в базовый класс
Вид: не перерыв!
Затрагиваемые языки: нет (т.е. ни один не сломан)
API до изменения:
API после изменения:
Пример кода, который продолжает работать на протяжении всего изменения (хотя я ожидал, что он сломается):
Ноты:
C ++ / CLI - единственный язык .NET, имеющий конструкцию, аналогичную явной реализации интерфейса для членов виртуального базового класса - «явное переопределение». Я полностью ожидал, что это приведет к тому же виду поломки, что и при перемещении элементов интерфейса на базовый интерфейс (поскольку IL, сгенерированный для явного переопределения, такой же, как и для явной реализации). К моему удивлению, это не тот случай - хотя сгенерированный IL по-прежнему указывает, что
BarOverride
переопределения,Foo::Bar
а неFooBase::Bar
загрузчик сборок, достаточно умны, чтобы корректно заменять одно на другое без каких-либо претензий - очевидно, тот факт, чтоFoo
это класс, является тем, что имеет значение. Пойди разберись ...источник
Это, возможно, не столь очевидный особый случай «добавления / удаления членов интерфейса», и я подумал, что он заслуживает отдельной записи в свете другого случая, который я собираюсь опубликовать в следующем. Так:
Рефакторинг членов интерфейса в базовый интерфейс
Вид: разрывы на исходном и двоичном уровнях
Затрагиваемые языки: C #, VB, C ++ / CLI, F # (для разрыва источника; двоичный естественно влияет на любой язык)
API до изменения:
API после изменения:
Пример кода клиента, который нарушается при изменении на уровне источника:
Пример кода клиента, который нарушается при изменении на двоичном уровне;
Ноты:
Для разрыва исходного уровня проблема в том, что C #, VB и C ++ / CLI требуют точного имени интерфейса в объявлении реализации члена интерфейса; таким образом, если член перемещается в базовый интерфейс, код больше не будет компилироваться.
Двоичный разрыв связан с тем, что методы интерфейса полностью определены в сгенерированном IL для явных реализаций, и имя интерфейса там также должно быть точным.
Неявная реализация, где она доступна (например, C # и C ++ / CLI, но не VB), будет хорошо работать как на исходном, так и на двоичном уровне. Вызовы методов также не прерываются.
источник
Implements IFoo.Bar
будет прозрачно ссылатьсяIFooBase.Bar
?Изменение порядка перечисляемых значений
Вид разрыва: Изменение семантики тихого семантики на уровне источника / двоичного уровня
Затрагиваемые языки: все
Изменение порядка перечисляемых значений сохранит совместимость на уровне источника, поскольку литералы имеют одинаковые имена, но их порядковые индексы будут обновлены, что может вызвать некоторые виды молчаливых разрывов на уровне источника.
Еще хуже то, что тихие разрывы двоичного уровня могут быть введены, если клиентский код не перекомпилируется с новой версией API. Enum-значения являются константами времени компиляции, и любое их использование запекается в IL клиентской сборки. Этот случай может быть особенно трудно заметить время от времени.
API до изменения
API после изменения
Пример клиентского кода, который работает, но впоследствии не работает:
источник
Это действительно очень редкая вещь на практике, но, тем не менее, удивительно, когда это происходит.
Добавление новых не перегруженных участников
Вид: разрыв исходного уровня или тихая семантика.
Затрагиваемые языки: C #, VB
Языки, не затронутые: F #, C ++ / CLI
API до изменения:
API после изменения:
Пример кода клиента, который нарушается при изменении:
Ноты:
Проблема здесь вызвана выводом лямбда-типа в C # и VB при наличии разрешения перегрузки. Ограниченная форма утиной типизации используется здесь, чтобы разорвать связи, где более чем один тип совпадает, проверяя, имеет ли смысл лямбда-выражение для данного типа - если только один тип приводит к компилируемому телу, этот выбирается.
Опасность здесь в том, что в клиентском коде может быть перегруженная группа методов, где некоторые методы принимают аргументы своих собственных типов, а другие принимают аргументы типов, предоставляемых вашей библиотекой. Если какой-либо из его кодов затем использует алгоритм вывода типов для определения правильного метода, основанного исключительно на наличии или отсутствии членов, то добавление нового члена к одному из ваших типов с тем же именем, что и в одном из типов клиента, может потенциально вызвать вывод выключено, что приводит к неоднозначности при разрешении перегрузки.
Обратите внимание, что типы
Foo
иBar
в этом примере не связаны никоим образом, ни по наследству, ни по-другому. Простого использования их в одной группе методов достаточно, чтобы вызвать это, и если это происходит в клиентском коде, вы не можете его контролировать.Приведенный выше пример кода демонстрирует более простую ситуацию, когда это разрыв на уровне исходного кода (то есть результаты ошибки компилятора). Тем не менее, это также может быть автоматическим изменением семантики, если перегрузка, которая была выбрана с помощью логического вывода, имела другие аргументы, которые иначе привели бы к ее ранжированию ниже (например, необязательные аргументы со значениями по умолчанию или несоответствие типов между объявленным и фактическим аргументом, требующим неявного преобразование). В таком случае разрешение перегрузки больше не будет терпеть неудачу, но компилятор будет спокойно выбирать другую перегрузку. На практике, однако, очень трудно столкнуться с этим случаем без тщательного построения сигнатур методов, чтобы преднамеренно вызывать его.
источник
Преобразуйте неявную реализацию интерфейса в явную.
Вид разрыва: источник и бинарный
Затрагиваемые языки: все
На самом деле это всего лишь вариант изменения доступности метода - он немного более тонкий, поскольку легко упустить из виду тот факт, что не весь доступ к методам интерфейса обязательно осуществляется через ссылку на тип интерфейса.
API до изменения:
API после изменения:
Пример кода клиента, который работает до изменения и впоследствии не работает:
источник
Преобразуйте явную реализацию интерфейса в неявную.
Вид разрыва: Источник
Затрагиваемые языки: все
Рефакторинг явной реализации интерфейса в неявную является более тонким в том, как она может сломать API. На первый взгляд может показаться, что это должно быть относительно безопасно, однако в сочетании с наследованием это может вызвать проблемы.
API до изменения:
API после изменения:
Пример кода клиента, который работает до изменения и впоследствии не работает:
источник
Foo
не было имени публичного методаGetEnumerator
, и вы вызываете метод через ссылку типаFoo
.. .yield return "Bar"
:), но да, я вижу, куда это идет сейчас -foreach
всегда вызывает открытый метод namedGetEnumerator
, даже если это не настоящая реализация дляIEnumerable.GetEnumerator
. Кажется, у этого есть еще один угол: даже если у вас есть только один класс, и он реализуетсяIEnumerable
явно, это означает, что это изменение источника, добавляющее к нему открытый методGetEnumerator
, потому что теперь онforeach
будет использовать этот метод поверх реализации интерфейса. Также эта же проблема применима и кIEnumerator
реализации ...Изменение поля на свойство
Вид перерыва: API
Затрагиваемые языки: Visual Basic и C # *
Информация: Когда вы заменяете обычное поле или переменную на свойство в Visual Basic, любой внешний код, ссылающийся на этот элемент каким-либо образом, необходимо будет перекомпилировать.
API до изменения:
API после изменения:
Пример клиентского кода, который работает, но впоследствии не работает:
источник
out
иref
аргументы методов, в отличие от полей, и не могут быть целью унарного&
оператора.Дополнение пространства имен
Разрыв на уровне источника / Изменение тихой семантики на уровне источника
Из-за способа разрешения пространства имен в vb.Net добавление пространства имен в библиотеку может привести к тому, что код Visual Basic, скомпилированный с предыдущей версией API, не скомпилируется с новой версией.
Пример кода клиента:
Если новая версия API добавляет пространство имен
Api.SomeNamespace.Data
, приведенный выше код не будет компилироваться.Это становится более сложным с импортом пространства имен на уровне проекта. Если
Imports System
этот код не указан, ноSystem
пространство имен импортировано на уровне проекта, код все равно может привести к ошибке.Тем не менее, если API включает в себя класс
DataRow
в своеApi.SomeNamespace.Data
пространство имен, тогда код будет компилироваться, ноdr
будет экземпляромSystem.Data.DataRow
при компиляции со старой версией API иApi.SomeNamespace.Data.DataRow
при компиляции с новой версией API.Аргумент Переименование
Разрыв уровня источника
Изменение имен аргументов - это серьезное изменение в vb.net с версии 7 (?) (.Net версия 1?) И c # .net с версии 4 (.Net версия 4).
API до изменения:
API после изменения:
Пример кода клиента:
Параметры Ref
Разрыв уровня источника
Добавление переопределения метода с той же сигнатурой, за исключением того, что один параметр передается по ссылке, а не по значению, приведет к тому, что vb source, который ссылается на API, не сможет разрешить функцию. Visual Basic не имеет возможности (?) Дифференцировать эти методы в точке вызова, если они не имеют разных имен аргументов, поэтому такое изменение может привести к невозможности использования обоих членов из кода VB.
API до изменения:
API после изменения:
Пример кода клиента:
Поле для изменения свойства
Разрыв двоичного уровня / Разрыв исходного уровня
Помимо очевидного разрыва двоичного уровня, это может вызвать разрыв уровня источника, если элемент передается методу по ссылке.
API до изменения:
API после изменения:
Пример кода клиента:
источник
Изменение API:
Разрыв двоичного уровня:
Добавление нового члена (защищенного от событий), который использует тип из другой сборки (Class2) в качестве ограничения аргумента шаблона.
Изменение дочернего класса (Class3) для наследования от типа в другой сборке, когда класс используется в качестве аргумента шаблона для этого класса.
Изменение тихой семантики на уровне источника:
(не уверен, где они подходят)
Изменения развертывания:
Начальная загрузка / Изменения конфигурации:
Обновить:
Извините, я не осознавал, что единственная причина, по которой это сломалось, заключалась в том, что я использовал их в шаблонных ограничениях.
источник
TypeForwardedToAttribute
используется.-Werror
свою систему сборки, которую вы поставляете с релизными тарболами. Этот флаг наиболее полезен для разработчика кода и чаще всего бесполезен для потребителя.Добавление методов перегрузки для прекращения использования параметров по умолчанию
Вид разрыва: изменение семантики тихого уровня источника
Поскольку компилятор преобразует вызовы метода с отсутствующими значениями параметров по умолчанию в явный вызов со значением по умолчанию на вызывающей стороне, обеспечивается совместимость для существующего скомпилированного кода; метод с правильной сигнатурой будет найден для всего ранее скомпилированного кода.
С другой стороны, вызовы без использования необязательных параметров теперь компилируются как вызов нового метода, в котором отсутствует необязательный параметр. Все по-прежнему работает нормально, но если вызываемый код находится в другой сборке, то вновь скомпилированный код, вызывающий его, теперь зависит от новой версии этой сборки. Развертывание сборок, вызывающих реорганизованный код, без развертывания сборки, в которой находится реорганизованный код, приводит к исключениям «метод не найден».
API до изменения
API после изменения
Пример кода, который все еще будет работать
Пример кода, который теперь зависит от новой версии при компиляции
источник
Переименование интерфейса
Вид разрыва: источник и бинарный
Затрагиваемые языки: Скорее всего все, протестировано на C #.
API до изменения:
API после изменения:
Пример клиентского кода, который работает, но впоследствии не работает:
источник
Метод перегрузки с параметром обнуляемого типа
Вид: Разрыв уровня источника
Затрагиваемые языки: C #, VB
API до изменения:
API после изменения:
Пример клиентского кода, работающего до изменения и сломанного после него:
Исключение: вызов неоднозначен между следующими методами или свойствами.
источник
Продвижение в метод продления
Вид: разрыв на уровне источника
Затрагиваемые языки: C # v6 и выше (может быть, другие?)
API до изменения:
API после изменения:
Пример клиентского кода, работающего до изменения и сломанного после него:
Дополнительная информация: https://github.com/dotnet/csharplang/issues/665
источник