Изменения, влияющие на совместимость

На протяжении всей истории своего развития в .NET по возможности поддерживался высокий уровень совместимости между версиями и вариантами этой платформы. Хотя .NET 5 (и .NET Core) и более поздних версий можно считать новой технологией в сравнении с платформой .NET Framework, возможность независимого от .NET Framework развития данной реализации .NET ограничивается следующими двумя факторами.

  • Большое количество разработчиков разрабатывали ранее и продолжают разрабатывать приложения для .NET Framework. Все они рассчитывают на согласованное поведение разных реализаций .NET.

  • Проекты библиотек .NET Standard позволяют разработчикам создавать библиотеки для распространенных API, которые совместно используются в .NET Framework и .NET 5 (и .NET Core) и более поздних версий. Разработчики ожидают, что библиотеки из приложения .NET 5 будут вести себя точно так же, как аналогичные библиотеки в приложении .NET Framework.

Кроме совместимости разных реализаций .NET, разработчики также ожидают высокий уровень совместимости между версиями данной реализации .NET. В частности, код, написанный для более ранней версии .NET Core, должен без проблем работать в .NET 5 и любой последующей версии. Более того, многие разработчики ожидают, что новые API в свежих версиях .NET будут также совместимы с предварительными версиями, в которых впервые появились эти API.

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

В следующих разделах описываются категории изменений, внесенных в API .NET, и их влияние на совместимость приложений. Изменения могут быть разрешены (✔️), запрещены (❌) или требовать оценки предсказуемости и очевидности, а также согласованности таких изменений с прежним поведением.

Примечание

  • Эти критерии не только разъясняют оценку изменений в библиотеках .NET, но и могут помочь разработчикам оценивать изменения в собственных библиотеках, предназначенных для различных реализаций и версий .NET.
  • Категории совместимости, например обратная и с будущими версиями, описаны в статье Совместимость.

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

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

Типы

  • ✔️ РАЗРЕШЕНО. Удаление реализации интерфейса из типа, если этот интерфейс уже реализован в базовом типе

  • ТРЕБУЕТСЯ ОЦЕНКА. Добавление новой реализации интерфейса в тип

    Это допустимое изменение, так как оно не сказывается отрицательно на существующих клиентах. Чтобы новая реализация оставалась допустимой, любые изменения типа должны выполняться в пределах допустимых изменений, которые определены здесь. Следует соблюдать предельную осторожность при добавлении интерфейсов, которые напрямую влияют на способность конструктора или сериализатора создавать код или данные, которые нельзя использовать на нижнем уровне. Пример — интерфейс ISerializable.

  • ТРЕБУЕТСЯ ОЦЕНКА. Добавление нового базового класса

    Тип можно включать в иерархию между двумя существующими типами, если он не включает новые абстрактные элементы и не изменяет семантику или поведение существующих типов. Например, в .NET Framework 2.0 класс DbConnection стал новым базовым классом для SqlConnection с наследованием напрямую от Component.

  • ✔️ РАЗРЕШЕНО. Перемещение типа из одной сборки в другую

    Старой сборке следует присвоить метку TypeForwardedToAttribute, указывающую на новую сборку.

  • ✔️ РАЗРЕШЕНО. Изменение типа struct на тип readonly struct

    Изменение типа readonly struct на тип struct запрещено.

  • ✔️ РАЗРЕШЕНО. Добавление ключевых слов sealed или abstract к типу, в котором нет доступных конструкторов (открытых или защищенных)

  • ✔️ РАЗРЕШЕНО. Расширение видимости типа

  • ЗАПРЕЩЕНО. Изменение пространства имен или имени типа

  • ЗАПРЕЩЕНО. Переименование или удаление открытого типа

    Это изменение нарушает весь код, в котором использовался переименованный или удаленный тип.

  • ЗАПРЕЩЕНО. Изменение базового типа для перечисления

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

  • ЗАПРЕЩЕНО. Запечатывание типа, который ранее был незапечатанным

  • ЗАПРЕЩЕНО. Добавление интерфейса в набор базовых типов интерфейса

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

  • ТРЕБУЕТСЯ ОЦЕНКА. Удаление класса из набора базовых классов или интерфейса из набора реализованных интерфейсов

    Есть одно исключение из правила удаления интерфейса: вы можете добавить реализацию интерфейса, наследуемую от удаленного интерфейса. Например, можно удалить IDisposable, если тип или интерфейс теперь реализуют IComponent с реализацией IDisposable.

  • ЗАПРЕЩЕНО. Изменение типа readonly struct на тип struct

    Обратите внимание, что изменение типа struct на тип readonly struct разрешено.

  • ЗАПРЕЩЕНО. Изменение типа struct на тип ref struct и наоборот

  • ЗАПРЕЩЕНО. Снижение видимости типа

    При этом увеличение видимости типа разрешено.

Участники

  • ✔️ РАЗРЕШЕНО. Расширение видимости для элемента, который не является виртуальным

  • ✔️ РАЗРЕШЕНО. Добавление абстрактного элемента в открытый тип без доступных конструкторов (открытых или защищенных) или в запечатанный тип

    При этом добавление абстрактного элемента в тип с доступными конструкторами (открытыми или защищенными) и в незапечатанный тип (sealed) разрешено.

  • ✔️ РАЗРЕШЕНО. Ограничение видимости защищенного элемента, если у типа нет доступных (открытых или защищенных) конструкторов или этот тип запечатан

  • ✔️ РАЗРЕШЕНО. Перемещение элемента в класс, расположенный в иерархии выше типа, из которого он был удален

  • ✔️ РАЗРЕШЕНО. Добавление или удаление переопределения

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

  • ✔️ РАЗРЕШЕНО. Добавление в класс конструктора одновременно с конструктором без параметров, если ранее у этого класса не было конструкторов

    При этом добавление конструктора в класс, ранее не имевший конструкторов, без добавления конструктора без параметров, не разрешено.

  • ✔️ РАЗРЕШЕНО. Изменение абстрактного элемента на виртуальный

  • ✔️ РАЗРЕШЕНО. Изменение возвращаемого значения с ref readonly на ref (за исключением виртуальных методов или интерфейсов)

  • ✔️ РАЗРЕШЕНО. Удаление из поля метки readonly, за исключением полей со статическим типом изменяемого значения

  • ✔️ РАЗРЕШЕНО. Вызов нового события, которое не было определено ранее

  • ТРЕБУЕТСЯ ОЦЕНКА. Добавление нового поля экземпляра к типу

    Это изменение влияет на сериализацию.

  • ЗАПРЕЩЕНО. Переименование или удаление открытого типа или параметра

    Это изменение нарушает весь код, в котором использовался переименованный или удаленный элемент либо параметр.

    Сюда относятся удаление и переименование методов получения и определения свойств, а также элементов перечисления.

  • ЗАПРЕЩЕНО. Добавление элемента к интерфейсу

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

  • ЗАПРЕЩЕНО. Изменение значения общедоступной константы или элемента перечисления

  • ЗАПРЕЩЕНО. Изменение типа для свойства, поля, параметра или возвращаемого значения

  • ЗАПРЕЩЕНО. Добавление, удаление параметров или изменение их порядка

  • ЗАПРЕЩЕНО. Добавление или удаление ключевого слова in, out или ref для параметра

  • ЗАПРЕЩЕНО. Переименование параметра (в том числе изменение регистра символов)

    Такое изменение считается критическим по двум причинам:

  • ЗАПРЕЩЕНО. Изменение возвращаемого значения с ref на ref readonly

  • ❌️ ЗАПРЕЩЕНО. Изменение возвращаемого значения с ref readonly на ref для виртуальных методов или интерфейсов

  • ЗАПРЕЩЕНО. Добавление или удаление ключевого слова abstract для элемента

  • ЗАПРЕЩЕНО. Удаление ключевого слова virtual для элемента

  • ЗАПРЕЩЕНО. Добавление ключевого слова virtual к элементу

    Такое изменение часто не является критическим, так как компилятор C# обычно выдает инструкции callvirt на промежуточном языке (IL) для вызова невиртуальных методов (callvirt, в отличие об обычного кода, выполняет проверку значений null). При этом такое поведение не может считаться стабильным по следующим причинам:

    • .NET используется не только с C#, но и с другими языками.

    • Компилятор C# продолжает попытки оптимизировать callvirt в обычный вызов, если целевой метод не является виртуальным и с высокой вероятностью не имеет значения null (например, метод с доступом с использованием оператора распространения значений null ?.).

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

  • ЗАПРЕЩЕНО. Преобразование виртуального элемента в абстрактный

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

  • ЗАПРЕЩЕНО. Добавление абстрактного элемента в общедоступный тип с доступными конструкторами (открытыми или защищенными) и незапечатанный тип

  • ЗАПРЕЩЕНО. Добавление в элемент ключевого слова static или его удаление

  • ЗАПРЕЩЕНО. Добавление перегрузки, которая исключает существующую перегрузку и определяет другое поведение

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

  • ЗАПРЕЩЕНО. Добавление конструктора в класс, ранее не имевший конструкторов, без добавления конструктора без параметров

  • ❌️ ЗАПРЕЩЕНО. Добавление ключевого слова readonly в поле

  • ЗАПРЕЩЕНО. Снижение видимости элемента

    Сюда входит ограничение видимости защищенного элемента, если имеются доступные (public или protected) конструкторы, но тип не является запечатанным. Если это не так, снижение видимости защищенного элемента разрешено.

    Увеличение видимости типа разрешено.

  • ЗАПРЕЩЕНО. Изменение типа элемента

    Возвращаемое значение метода или типа свойства или поля изменить нельзя. Например, сигнатуру метода, который возвращает Object, нельзя изменить так, чтобы он возвращал String, или наоборот.

  • ЗАПРЕЩЕНО. Добавление поля к структуре, у которой ранее не было состояний

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

  • ЗАПРЕЩЕНО. Вызов существующего события, которое ранее никогда не срабатывало

Изменения поведения

Сборки

  • ✔️ РАЗРЕШЕНО. Преобразование сборки в переносимую при сохранении поддержки тех же платформ

  • ЗАПРЕЩЕНО. Изменение имени сборки

  • ЗАПРЕЩЕНО. Изменение открытого ключа сборки

Свойства, поля, параметры и возвращаемые значения

  • ✔️ РАЗРЕШЕНО. Изменение значения для свойства, поля, возвращаемого значения или параметра out на более производный тип

    Например, метод, возвращающий тип Object, может возвращать экземпляр String. (Но при этом сигнатура метода не должна изменяться.)

  • ✔️ РАЗРЕШЕНО. Увеличение диапазона допустимых значений для свойства или параметра, если элемент не является виртуальным

    Можно расширять диапазон значений, которые передаются методу или возвращаются из него, но нельзя расширять тип параметра или элемента. Например, диапазон передаваемых методу значений можно расширить с 0–124 до 0–255, но нельзя изменить тип параметра с Byte на Int32.

  • ЗАПРЕЩЕНО. Увеличение диапазона допустимых значений для свойства или параметра, если элемент является виртуальным

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

  • ЗАПРЕЩЕНО. Уменьшение диапазона допустимых значений для свойства или параметра

  • ЗАПРЕЩЕНО. Увеличение диапазона допустимых значений для свойства, поля, возвращаемого значения или параметра out

  • ЗАПРЕЩЕНО. Изменение возвращаемых значений для свойства, поля, возвращаемого значения метода или параметра out

  • ЗАПРЕЩЕНО. Изменение значения по умолчанию для свойства, поля или параметра

  • ЗАПРЕЩЕНО. Изменение точности числового возвращаемого значения

  • ТРЕБУЕТСЯ ОЦЕНКА. Изменение логики синтаксического анализа входных данных и создание новых исключений (даже если поведение синтаксического анализа не указано в документации

Исключения

  • ✔️ РАЗРЕШЕНО. Вызов более производного исключения, чем существующие исключения

    Так как новое исключение является подклассом существующего исключения, существующий код обработки исключений будет обрабатывать это исключение. Например, в .NET Framework 4 методы создания и получения языка и региональных параметров теперь вызывают исключение CultureNotFoundException вместо ArgumentException, если не могут найти язык и региональные параметры. Так как CultureNotFoundException является производным от ArgumentException, это изменение считается допустимым.

  • ✔️ РАЗРЕШЕНО. Вызов более конкретного исключения, чем NotSupportedException, NotImplementedException, NullReferenceException

  • ✔️ РАЗРЕШЕНО. Вызов исключения, которое считается неустранимым

    Неустранимые исключения не нужно перехватывать, они попадают в обработчик catch-all на верхнем уровне. Это означает, что у пользователей нет кода, который перехватывает эти явные исключения. Неустранимыми считаются следующие исключения:

  • ✔️ РАЗРЕШЕНО. Вызов нового исключения в новом пути кода

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

  • ✔️ РАЗРЕШЕНО. Удаление исключения для более надежной работы или для нового сценария

    Например, метод Divide, который ранее обрабатывал только положительные значения и вызвал исключение ArgumentOutOfRangeException, можно изменить так, чтобы он поддерживал положительные и отрицательные значения без вызова исключения.

  • ✔️ РАЗРЕШЕНО. Изменение текста сообщения об ошибке

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

  • ЗАПРЕЩЕНО. Вызов исключения в любом случае, кроме перечисленных выше

  • ЗАПРЕЩЕНО. Удаление исключения в любом случае, кроме перечисленных выше

Атрибуты

  • ✔️ РАЗРЕШЕНО. Изменение значения атрибута, который не является наблюдаемым

  • ЗАПРЕЩЕНО. Изменение значения атрибута, который является наблюдаемым

  • ТРЕБУЕТСЯ ОЦЕНКА. Удаление атрибута

    В большинстве случаев удаление атрибута (например, NonSerializedAttribute) является критическим изменением.

Поддержка платформ

  • ✔️ РАЗРЕШЕНО. Поддержка операции для платформы, которая ранее не поддерживалась

  • ЗАПРЕЩЕНО. Прекращение поддержки или требование конкретного пакета обновления для операции, которая ранее поддерживалась для платформы

Изменения внутренней реализации

  • ТРЕБУЕТСЯ ОЦЕНКА. Изменение контактной зоны внутреннего типа

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

  • ТРЕБУЕТСЯ ОЦЕНКА. Изменение внутренней реализации элемента

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

  • ✔️ РАЗРЕШЕНО. Повышение производительности операции

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

  • ✔️ РАЗРЕШЕНО. Косвенное (и обычно отрицательное) изменение производительности операции

    Если изменение не относится к категории критических по другим причинам, оно считается допустимым. Зачастую требуются дополнительные действия, включая дополнительные операции или операции, которые добавляют новые функции. Это почти всегда влияет на производительность, но может требоваться для правильной работы API.

  • ЗАПРЕЩЕНО. Преобразование синхронного API в асинхронный (и наоборот)

Изменения кода

  • ✔️ РАЗРЕШЕНО. Добавление params в параметр

  • ЗАПРЕЩЕНО. Замена структуры на класс и наоборот

  • ЗАПРЕЩЕНО. Добавление ключевого слова checked в блок кода

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

  • ЗАПРЕЩЕНО. Удаление params из параметра

  • ЗАПРЕЩЕНО. Изменение порядка возникновения событий

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

  • ЗАПРЕЩЕНО. Удаление вызова события для конкретного действия

  • ЗАПРЕЩЕНО. Изменение количества вызовов определенных событий

  • ЗАПРЕЩЕНО. Добавление FlagsAttribute к типу перечисления