Статьи

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

Добавление функциональности в библиотеку, не затрагивая существующий код, должно быть безопасным для клиентов, не так ли? К сожалению, нет, добавление еще одной перегрузки в библиотеку может привести к серьезным изменениям. Он может работать при обновлении библиотеки, но внезапно прерываться при перекомпиляции клиента, даже если код не был изменен. Это противно, не так ли?

При работе с  Kentor.AuthServices  мне пришлось продумывать версии более подробно, чем раньше. Когда самое подходящее время перейти на 1.0? Какая разница между переходом от 0,8,2 до 0,8,3 или 0,9,0? При исследовании я обнаружил, что ответом на все вопросы о версиях является  семантическое управление версиями .

Версия находится в форме Major.Minor.Patch.

  • Major увеличивается, если есть  серьезные изменения . Другие номера сбрасываются.
  • Незначительное увеличивается, если есть добавленная функциональность, которая  не ломается . Номер патча сбрасывается.
  • Патч увеличен для  обратно совместимых исправлений ошибок .

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

Предыстория этого поста заключается в том, что я слушал выступление Скотта Мейерса в NDC « Эффективное современное С ++»,  и мне  напомнили о том, как программы на С ++ должны отслеживать все виды злобности в языке, которые могут превращаться в трудно выявляемые ошибки. C # намного проще во многих отношениях, с меньшим количеством ловушек, но иногда я думаю, что мы делаем это проще для нас самих. Разработчики C ++ часто довольно хорошо разбираются в деталях языка, потому что им приходится, Будучи разработчиком на C #, можно выжить гораздо дольше, не зная этих подробностей, но игнорирование их в конечном итоге приведет к неприятным ошибкам. Эта вероятность, вероятно, не произойдет в ленивое утро вторника, когда у нее будет много времени, чтобы все исправить. Это произойдет ближе к вечеру пятницы, прямо перед важным производственным развертыванием, запланированным на выходные …

Как разработчики C #, я думаю, что мы должны взглянуть на сообщество C ++ и посмотреть, что мы можем извлечь из них. Итак, давайте углубимся в некоторый код и посмотрим, как мы можем сломать вещи.

Базовый код

Позвольте мне представить версию 1 моей библиотеки образцов.

public static class Utility
{
  public static void Method(object obj)
  {
    Console.WriteLine("Utility.Method(object)");
  }  
 
  public static void DefaultMethod(int i = 7)
  {
    Console.WriteLine("Utility.DefaultMethod({0})", i);
  }
}

Существует также небольшая клиентская программа, которая использует библиотеку.

public static class Program
{
  public static void Main(string[] args)
  {
    Utility.Method(17);
    Utility.DefaultMethod();
  }
}

При сборке и запуске программа выводит ожидаемые значения.

Utility.Method(object)
Utility.DefaultMethod(7)

Обновление библиотеки

Позже библиотека обновляется.

public static class Utility
{
  public static void Method(object obj)
  {
    Console.WriteLine("Utility.Method(object)");
  }  
 
  public static void Method(int i)
  {
    Console.WriteLine("Utility.Method(int)");
  }
 
  public static void DefaultMethod(int i = 42)
  {
      Console.WriteLine("Utility.DefaultMethod({0})", i);
  }
}

Добавлена ​​новая перегрузка и DefaultMethod изменено значение по умолчанию для параметра  . Эта версия совместима со старой, поэтому библиотека dll может быть изменена  без перестройки клиентского приложения . При запуске клиентское приложение выдает следующий вывод.

Utility.Method(object)
Utility.DefaultMethod(7)

Это точно так же, как и раньше. Обновление библиотеки до новой версии не влияет на вывод программы. Но мы непреднамеренно поставили себя в очень нестабильную ситуацию.

Восстановление клиента

Представьте, что вы только что взяли на себя обслуживание этой программы. У вас есть производственная установка, которая работает. У вас есть источник, из которого была создана программа, и у вас есть ссылка на скачивание самой последней версии библиотеки. Все выглядит по порядку, поэтому вы начинаете настраивать среду разработки, загружаете библиотеку и собираете код. Но это не дает такие же результаты . Все, что вы пытаетесь, напрасно. Вы не можете построить код и получить те же результаты. Состояние системы, когда вы вступили во владение, было нестабильным и не может быть легко воспроизведено.

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

Utility.Method(int)
Utility.DefaultMethod(42)

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

Проблема в компиляции и компоновке с внешней библиотекой разных версий. Разрешение перегрузки и обработка значений по умолчанию управляются во  время компиляции  и используют информацию о версии внешней библиотеки, использованной  во время компиляцииКомпиляция  против другой версии дает разные результаты; хотя  запуск  с более поздней версией библиотеки — нет.

Связывание библиотеки времени компиляции

Именно во время компиляции имеет место разрешение перегрузки. Он проверяет доступные перегрузки в библиотеке и затем жестко связывает этот выбор скомпилированным выводом. Неважно, что используемая версия во время выполнения предлагает другие перегрузки, скомпилированная программа знает, какую перегрузку она должна вызвать. Он также знает, какие преобразования типов необходимо выполнить, чтобы вызвать эту перегрузку. В этом случае,  int он должен быть помещен в  object.

То же самое верно для значения по умолчанию. Несмотря на то, что значение по умолчанию указано вместе с методом утилиты, оно не обрабатывается таким образом компилятором. Так рассуждает компилятор.

Программа вызывает,  Utility.DefaultMethod() но ей не хватает аргумента. Это синтаксическая ошибка. Ха! Я могу взорвать компиляцию на части с синтаксической ошибкой! Нет, подождите, в объявлении метода что-то есть. Ааа значение по умолчанию. Хорошо, я возьму это. Я изменю вызов на Utility.DefaultMethod(7) и затем смогу его скомпилировать.

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

Однако важно то, что компилятор эффективно  меняет вызов, чтобы жестко связать значение по умолчанию с сайтом вызовов . Даже несмотря на то, что скомпилированная библиотека заменена на новую с новым значением по умолчанию, которое не имеет значения, поскольку значение по умолчанию извлекается во время компиляции.

Ломать перемены

Хорошо, теперь мы видели, что разрешение перегрузки и значения по умолчанию могут превращать кажущиеся невинными изменения в кошмары зависимостей. Чтобы избежать неприятностей, есть три простых метода, которые спасут день.

  1. Новые перегрузки и измененные значения по умолчанию  нарушают  совместимость библиотек и требуют новой основной версии.
  2. Следует избегать использования значений по умолчанию в общедоступных API.
  3. Всегда делайте полную перестройку и тестируйте ее перед развертыванием при обновлении зависимостей. Это позволяет избежать сценария, описанного выше, где пересборка без изменений кода изменяет поведение программы.

Полный код этого поста доступен на  github . Запустите  test.bat файл, чтобы скомпилировать и запустить разные версии кода в том же порядке, что и в этом посте.