Полное руководство по изменениям API в .NET - PullRequest
214 голосов
/ 22 сентября 2009

Я хотел бы собрать как можно больше информации о версиях 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 вызывались как статические методы.

Ответы [ 14 ]

4 голосов
/ 08 октября 2009

Изменение API:

  1. Добавление атрибута [Устаревший] (вы как бы упомянули об этом, упомянув атрибуты; однако это может привести к серьезным изменениям при использовании предупреждения об ошибке.)

Разрыв двоичного уровня:

  1. Перемещение типа из одной сборки в другую
  2. Изменение пространства имен типа
  3. Добавление типа базового класса из другой сборки.
  4. Добавление нового члена (защищенного от событий), который использует тип из другой сборки (Class2) в качестве ограничения аргумента шаблона.

    protected void Something<T>() where T : Class2 { }
    
  5. Изменение дочернего класса (Class3) для наследования от типа в другой сборке, когда класс используется в качестве аргумента шаблона для этого класса.

    protected class Class3 : Class2 { }
    protected void Something<T>() where T : Class3 { }
    

Изменение тихой семантики на уровне источника:

  1. Добавление / удаление / изменение переопределений Equals (), GetHashCode () или ToString ()

(не уверен, где они подходят)

Изменения в развертывании:

  1. Добавление / удаление зависимостей / ссылок
  2. Обновление зависимостей до более новых версий
  3. Изменение «целевой платформы» между x86, Itanium, x64 или anycpu
  4. Сборка / тестирование на другой установке фреймворка (то есть установка 3.5 на коробке .Net 2.0 разрешает вызовы API, которые затем требуют .Net 2.0 SP2)

Начальная загрузка / Изменения конфигурации:

  1. Добавление / удаление / изменение пользовательских параметров конфигурации (т.е. настроек App.config)
  2. При интенсивном использовании IoC / DI в современных приложениях необходимо переконфигурировать и / или изменить код начальной загрузки для кода, зависимого от DI.

Обновление:

Извините, я не осознавал, что единственная причина, по которой это меня сломало, заключалась в том, что я использовал их в шаблонных ограничениях.

3 голосов
/ 16 ноября 2016

Добавление методов перегрузки для прекращения использования параметров по умолчанию

Вид разрыва: Изменение семантики тихого уровня источника

Поскольку компилятор преобразует вызовы метода с отсутствующими значениями параметров по умолчанию в явный вызов со значением по умолчанию на вызывающей стороне, обеспечивается совместимость для существующего скомпилированного кода; метод с правильной сигнатурой будет найден для всего ранее скомпилированного кода.

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

API до изменения

  public int MyMethod(int mandatoryParameter, int optionalParameter = 0)
  {
     return mandatoryParameter + optionalParameter;
  }    

API после изменения

  public int MyMethod(int mandatoryParameter, int optionalParameter)
  {
     return mandatoryParameter + optionalParameter;
  }

  public int MyMethod(int mandatoryParameter)
  {
     return MyMethod(mandatoryParameter, 0);
  }

Пример кода, который все еще будет работать

  public int CodeNotDependentToNewVersion()
  {
     return MyMethod(5, 6); 
  }

Пример кода, который теперь зависит от новой версии при компиляции

  public int CodeDependentToNewVersion()
  {
     return MyMethod(5); 
  }
1 голос
/ 19 ноября 2012

Переименование интерфейса

Вид разрыва: Источник и Двоичный код

Затронутые языки: Скорее всего все, протестировано на C #.

API до изменения:

public interface IFoo
{
    void Test();
}

public class Bar
{
    IFoo GetFoo() { return new Foo(); }
}

API после изменения:

public interface IFooNew // Of the exact same definition as the (old) IFoo
{
    void Test();
}

public class Bar
{
    IFooNew GetFoo() { return new Foo(); }
}

Пример клиентского кода, который работает, но впоследствии не работает:

new Bar().GetFoo().Test(); // Binary only break
IFoo foo = new Bar().GetFoo(); // Source and binary break
0 голосов
/ 23 мая 2019

Повышение в методе продления

Вид: разрыв уровня источника

Затронутые языки: C # v6 и выше (может быть, другие?)

API до изменения:

public static class Foo
{
    public static void Bar(string x);
}

API после изменения:

public static class Foo
{
    public void Bar(this string x);
}

Пример кода клиента, работающего до изменения и прерываемого после него:

using static Foo;

class Program
{
    static void Main() => Bar("hello");
}

Подробнее: https://github.com/dotnet/csharplang/issues/665

...