Changer les règles pour la compatibilité

Tout au long de son histoire, .NET a tenté de maintenir un niveau élevé de compatibilité de version en version et entre les différentes implémentations de .NET. Même si .NET 5 (et .NET Core) et versions ultérieures peuvent être considérés comme une nouvelle technologie par rapport au .NET Framework, deux facteurs majeurs limitent la capacité de cette implémentation de .NET à s’écarter du .NET Framework :

  • Un grand nombre de développeurs ont développé à la base ou continuent à développer des applications .NET Framework. Ils s’attendent à un comportement cohérent entre les implémentations de .NET.

  • Les projets de bibliothèque .NET Standard permettent aux développeurs de créer des bibliothèques qui ciblent les API communes partagées par le .NET Framework et .NET 5 (et .NET Core) et versions ultérieures. Les développeurs s’attendent à ce qu’une bibliothèque utilisée dans une application .NET 5 se comporte comme la bibliothèque utilisée dans une application .NET Framework.

En plus de la compatibilité entre les implémentations de .NET, les développeurs s’attendent à un haut niveau de compatibilité entre les versions d’une implémentation donnée de .NET. En particulier, le code écrit pour une version antérieure de .NET Core doit s’exécuter sans problème sur .NET 5 ou sur une version ultérieure. En fait, de nombreux développeurs s’attendent à ce que les nouvelles API dans les dernières versions de .NET soient également compatibles avec les versions préliminaires dans lesquelles ces API ont été introduites.

Cet article présente les changements qui affectent la compatibilité et la manière dont l'équipe .NET évalue chaque type de changement. Comprendre comment l’équipe .NET approche les changements cassants potentiels est particulièrement utile pour les développeurs qui ouvrent des demandes de tirage qui modifient le comportement des API .NET existantes.

Les sections suivantes décrivent les catégories de modifications apportées aux API .NET et leur impact sur la compatibilité des applications. Les changements sont autorisés (✔️), non autorisés (❌), ou nécessitent un jugement et une évaluation du caractère prévisible, évident et cohérent du comportement précédent (❓).

Notes

  • En plus de servir de guide pour l’évaluation des modifications apportées aux bibliothèques .NET, les développeurs de bibliothèques peuvent également utiliser ces critères pour évaluer les modifications apportées à leurs bibliothèques qui ciblent plusieurs implémentations et versions de .NET.
  • Pour plus d'informations sur les catégories de compatibilité, par exemple, la compatibilité ascendante et descendante, consultez Comment les changements de code peuvent affecter la compatibilité.

Modifications au contrat public

Les modifications de cette catégorie modifient la surface d’exposition publique d’un type. La plupart des modifications de cette catégorie ne sont pas autorisées dans la mesure où elles enfreignent une compatibilité descendante (la possibilité pour une application qui a été développée avec une version précédente d’une API de s’exécuter sans recompilation sur une version ultérieure).

Types

  • ✔️ AUTORISÉ : suppression d’une implémentation d’interface d’un type de l’interface est déjà implémentée par un type de base️

  • NÉCESSITE UN JUGEMENT : ajout d’une nouvelle implémentation d’interface à un type

    Il s’agit d’une modification acceptable, car elle n’affectera pas les clients existants. Toutes les modifications au type doivent fonctionner dans les limites des modifications acceptables définies ici pour la nouvelle implémentation afin de rester acceptables. Une prudence extrême est nécessaire lors de l’ajout d’interfaces qui affectent directement la capacité d’un concepteur ou sérialiseur à générer du code ou des données qui ne peuvent pas être consommées à un bas niveau. Par exemple, l’interface ISerializable.

  • NÉCESSITE UN JUGEMENT : présentation d’une nouvelle classe de base

    Un type peut être introduit dans une hiérarchie entre deux types existants si elle n’introduit pas de nouveaux membres abstraits et ne modifie pas la sémantique ou le comportement des types existants. Par exemple, dans .NET Framework 2.0, la classe DbConnection est devenue une classe de base pour SqlConnection, qui était précédemment dérivée directement de Component.

  • ✔️ AUTORISÉ : déplacement d’un type d’un assembly à un autre

    L’ancien assembly doit être marqué avec le TypeForwardedToAttribute qui pointe vers le nouvel assembly.

  • ✔️ AUTORISÉ : changement d’un type struct en type readonly struct

    Le changement d’un type readonly struct en type struct n’est pas autorisé.

  • ✔️ AUTORISÉ : ajout du mot clé sealed ou abstraite à un type lorsqu’il y a aucun constructeur accessible (public ou protégé)

  • ✔️ AUTORISÉ : extension de la visibilité d’un type

  • NON AUTORISÉ : changement de l’espace de noms ou du nom d’un type

  • NON AUTORISÉ : changement du nom ou suppression d’un type public

    Cela empêche tout code qui utilise le type renommé ou supprimé de fonctionner.

    Notes

    Dans de rares cas, .NET peut supprimer une API publique. Pour plus d’informations, consultez Suppression d’API dans .NET. Pour plus d’informations sur la stratégie de support de .NET, consultez Stratégie de support .NET.

  • NON AUTORISÉ : modification du type sous-jacent d’une énumération

    Il s’agit d’un changement cassant au niveau de la compilation et du comportement ainsi qu’un changement cassant binaire qui peut rendre les arguments d’attribut non analysables.

  • NON AUTORISÉ : scellement d’un type qui était précédemment non scellé

  • NON AUTORISÉ : ajout d’une interface à l’ensemble des types de base d’une interface

    Si une interface implémente une interface qu’elle n’implémentait pas précédemment, tous les types implémentant la version d’origine de l’interface cessent de fonctionner.

  • NÉCESSITE UN JUGEMENT : suppression d’une classe d’un ensemble des classes de base ou d’une interface à partir de l’ensemble des interfaces implémentées

    Il existe une exception à la règle pour la suppression de l’interface : vous pouvez ajouter l’implémentation d’une interface qui dérive de l’interface supprimée. Par exemple, vous pouvez supprimer IDisposable si le type ou l’interface implémente désormais IComponent, qui implémente IDisposable.

  • NON AUTORISÉ : changement d’un type readonly struct en type struct

    Notez cependant que le changement d’un type struct en type readonly struct est autorisé.

  • NON AUTORISÉ : changement d’un type struct en type ref struct et vice versa

  • NON AUTORISÉ : réduction de la visibilité d’un type

    Toutefois, l’augmentation de la visibilité d’un type est autorisée.

Membres

  • ✔️ AUTORISÉ : Développement de la visibilité d’un membre qui n’est pas virtual

  • ✔️ AUTORISÉ : ajout d’un membre abstrait à un type public qui n’a pas de constructeur️ accessible (public ou protégé) ou dont le type est sealed

    Toutefois, l’ajout à un membre abstrait à un type qui a des constructeurs accessibles (publics ou protégés) et n’est pas sealed n’est pas autorisé.

  • ✔️ AUTORISÉ : restriction de la visibilité d’un membre protected chaque fois que le type n’a pas de constructeur accessible (public ou protégé) ou que le type est sealed

  • ✔️ AUTORISÉ : déplacement d’un membre vers une classe supérieure au type à partir duquel il a été supprimé dans la hiérarchie

  • ✔️ AUTORISÉ : ajout ou suppression d’une substitution

    L’ajout d’une substitution, les consommateurs précédents pourraient ignorer la substitution lors de l’appel de base.

  • ✔️ AUTORISÉ : ajout d’un constructeur à une classe, ainsi que d’un constructeur par défaut (sans paramètre) si la classe n’avait auparavant aucun constructeur️

    Cependant, ajouter un constructeur à une classe qui n’avait auparavant aucun constructeur sans ajouter le constructeur sans paramètre n’est pas autorisé.

  • ✔️ AUTORISÉ : changement d’un membre de️ abstract à virtual

  • ✔️ AUTORISÉ : changement d’une valeur renvoyée ref readonly à ref (à l’exception des méthodes virtuelles et des interfaces)

  • ✔️ AUTORISÉ : suppression d’un readonly d’un champ, sauf si le type statique du champ est un type valeur mutable

  • ✔️ AUTORISÉ : appel d’un nouvel événement qui n’a pas été défini précédemment

  • NÉCESSITE UN JUGEMENT : ajout d’un nouveau champ d’instance à un type

    Cette modification a un impact sur la sérialisation.

  • NON AUTORISÉ : changement de nom ou suppression d’un paramètre ou membre public

    Cela empêche tout code qui utilise le membre ou paramètre renommé ou supprimé de fonctionner.

    Cela inclut la suppression et le changement de nom d’un accesseur Get ou Set à partir d’une propriété, ou de membres d’une énumération.

  • NON AUTORISÉ : ajout d’un membre à une interface

    Si vous fournissez une implémentation, l'ajout d'un nouveau membre à une interface existante n'entraînera pas nécessairement des échecs de compilation dans les assemblages en aval. Mais toutes les langues ne prennent pas en charge les membres d'interface par défaut (DIM). De plus, dans certains scénarios, le runtime ne peut pas décider quel membre d'interface par défaut invoquer. Pour ces raisons, l’ajout d’un membre à une interface existante est considéré comme un changement cassant.

  • NON AUTORISÉ : modification de la valeur d’un membre d’énumération ou d’une constante publique

  • NON AUTORISÉ : modification du type d’une propriété, un champ, d’un paramètre ou d’une valeur renvoyée

  • NON AUTORISÉ : ajout, suppression ou modification de l’ordre des paramètres

  • NON AUTORISÉ : ajout ou suppression des mots clés in, out ou ref dans un paramètre

  • NON AUTORISÉ : changement de nom d’un paramètre (y compris changement de casse)

    Cela est considéré comme un changement cassant pour deux raisons :

  • NON AUTORISÉ : changement d’une valeur renvoyée ref à ref readonly

  • NON AUTORISÉ : changement d’une valeur renvoyée ref readonly à ref sur une méthode virtuelle ou une interface

  • NON AUTORISÉ : ajout ou suppression de abstract sur un membre

  • NON AUTORISÉ : suppression du mot clé virtual d’un membre

  • NON AUTORISÉ : ajout du mot clé virtual à un membre

    Bien que cela n’est souvent pas un changement cassant, car le compilateur C# a tendance à émettre des instructions callvirt en langage intermédiaire pour appeler des méthodes non virtuelles (callvirt effectue une vérification de valeur null, alors qu’un appel normal ne le fait pas), ce comportement n’est pas invariable pour plusieurs raisons :

    • C# n’est pas le seul langage que .NET cible.

    • Le compilateur C# essaie progressivement d’optimiser callvirt en un appel normal chaque fois que la méthode cible est non virtuelle et n’a probablement pas la valeur null (par exemple une méthode accessible via l’opérateur de propagation de NULL ?).

    Rendre une méthode virtuelle signifie que le code consommateur finirait souvent par l’appeler de façon non virtuelle.

  • NON AUTORISÉ : changement d’un membre virtuel en abstrait

    Un membre virtuel fournit une implémentation de méthode qui peut être substituée par une classe dérivée. Un membre abstrait ne fournit aucune implémentation et doit être substitué.

  • NON AUTORISÉ : ajout du mot clé sealed à un membre d’interface

    L’ajout de sealed à un membre d’interface par défaut le rend non virtuel, ce qui empêche l’appel de l’implémentation d’un type dérivé de ce membre.

  • NON AUTORISÉ : ajout d’un membre abstrait à un type public qui a des constructeurs (publics ou protégés) accessibles et qui n’est pas sealed

  • NON AUTORISÉ : ajout ou suppression du mot clé static d’un membre

  • NON AUTORISÉ : ajout d’une surcharge qui exclut une surcharge existante et définit un comportement différent

    Cela empêche de fonctionner les clients existants qui étaient liés à la surcharge précédente. Par exemple, si une classe a une seule version d’une méthode qui accepte un UInt32, un consommateur existant est correctement lié à cette surcharge lors du passage d’une valeur Int32. Toutefois, si vous ajoutez une surcharge qui accepte un Int32 lors de la recompilation ou à l’aide d’une liaison tardive, le compilateur est maintenant lié à la nouvelle surcharge. Si un comportement différent se produit, il s’agit d’un changement cassant.

  • NON AUTORISÉ : ajout d’un constructeur à une classe qui n’avait auparavant pas de constructeur sans ajout du constructeur sans paramètre

  • ❌️ NON AUTORISÉ : ajout de readonly à un champ

  • NON AUTORISÉ : réduction de la visibilité d’un membre

    Cela inclut la réduction de la visibilité d’un membre protected lorsqu’il y a des constructeurs accessibles (public ou protected) et que le type n’est passealed. Si ce n’est pas le cas, la réduction de la visibilité d’un membre protégé est autorisée.

    L’augmentation de la visibilité d’un membre est autorisée.

  • NON AUTORISÉ : modification du type d’un membre

    La valeur renvoyée d’une méthode ou le type d’une propriété ou d’un champ ne peut pas être modifiée. Par exemple, la signature d’une méthode qui retourne un Object ne peut pas être modifiée afin de retourner un String, ou vice versa.

  • NON AUTORISÉ : ajout d’un champ d’instance à un struct qui n’a pas de champ non public

    Si un struct a uniquement des champs publics ou n’a pas de champ du tout, les appelants peuvent déclarer des variables locales de ce type de struct sans appeler le constructeur du struct ni initialiser d’abord la variable locale sur default(T), tant que tous les champs publics sont définis sur le struct avant la première utilisation. L’ajout de nouveaux champs (publics ou non publics) à ce type de struct est un changement cassant de la source pour ces appelants, car le compilateur a maintenant besoin que les champs supplémentaires soient initialisés.

    Par ailleurs, l’ajout de nouveaux champs (publics ou non publics) à un struct sans champ ou avec uniquement des champs publics est un changement cassant binaire pour les appelants qui ont appliqué [SkipLocalsInit] à leur code. Comme le compilateur ne connaît pas ces champs au moment de la compilation, il peut envoyer IL qui n’initialise pas entièrement le struct, ce qui entraîne la création du struct à partir de données de pile non initialisées.

    Si un struct a des champs non publics, le compilateur applique déjà l’initialisation via le constructeur ou default(T), et l’ajout de nouveaux champs d’instance n’est pas un changement cassant.

  • NON AUTORISÉ : déclenchement d’un événement existant alors qu’il n’a jamais été déclenché avant

Changements de comportement

Assemblys

  • ✔️ AUTORISÉ : rendre un assembly portable lorsque les mêmes plateformes sont toujours prises en charge

  • NON AUTORISÉ : modification du nom d’un assembly

  • NON AUTORISÉ : modification de la clé publique d’un assembly

Propriétés, champs, paramètres et valeurs renvoyées

  • ✔️ AUTORISÉ : modification de la valeur d’une propriété, d’un champ, d’une valeur renvoyée, ou d’un paramètre out en un type plus dérivé

    Par exemple, une méthode qui retourne un type Object peut retourner une instance de String. (Toutefois, la signature de méthode ne peut pas être modifiée).

  • ✔️ AUTORISÉ : augmentation de l’intervalle accepté de valeurs pour une propriété ou un paramètre si le membre n’est pas virtual

    La plage de valeurs qui peuvent être passées à la méthode ou sont retournées par le membre peut être étendue, mais le type de membre ou paramètre ne peut pas l’être. Par exemple, tandis que les valeurs passées à une méthode peuvent s’étendre de 0-124 à 0-255, le type de paramètre ne peut pas être changé de Byte à Int32.

  • NON AUTORISÉ : augmentation de l’intervalle accepté de valeurs pour une propriété ou un paramètre si le membre est virtual

    Ce changement empêche le fonctionnement des membres substitués existants, qui ne fonctionneront pas correctement pour la plage de valeurs étendue.

  • NON AUTORISÉ : diminution de la plage des valeurs acceptées pour une propriété ou un paramètre

  • NON AUTORISÉ : augmentation de l’intervalle de valeurs renvoyées pour une propriété, un champ, une valeur de retour ou un paramètre out

  • NON AUTORISÉ : modification des valeurs renvoyées pour une propriété, un champ, une valeur de retour de méthode ou un paramètre out

  • NON AUTORISÉ : modification de la valeur par défaut d’une propriété, d’un champ ou d’un paramètre

    La modification ou la suppression d’une valeur de paramètre par défaut ne constitue pas un saut binaire. La suppression d’une valeur par défaut de paramètre est un saut source et la modification d’une valeur par défaut de paramètre peut entraîner un arrêt comportemental après la recompilation.

    Pour cette raison, la suppression des valeurs de paramètres par défaut est acceptable dans le cas spécifique du « déplacement » de ces valeurs par défaut vers une nouvelle surcharge de méthode pour éliminer toute ambiguïté. Par exemple, prenons une méthode MyMethod(int a = 1)existante. Si vous introduisez une surcharge de MyMethod avec deux paramètres optionnels a et b, vous pouvez préserver la compatibilité en déplaçant la valeur par défaut de a vers la nouvelle surcharge. Désormais, les deux surcharges sont MyMethod(int a) et MyMethod(int a = 1, int b = 2). Ce modèle permet à MyMethod() de compiler.

  • NON AUTORISÉ : modification de la précision d’une valeur de retournée numérique

  • NÉCESSITE UN JUGEMENT : changement dans l’analyse de l’entrée et levée de nouvelles exceptions (même si le comportement d’analyse n’est pas spécifié dans la documentation)

Exceptions

  • ✔️ AUTORISÉ : levée d’une exception plus dérivée qu’une exception existante

    Étant donné que la nouvelle exception est une sous-classe d’une exception existante, le code de gestion des exceptions précédent continue à gérer l’exception. Par exemple, dans .NET Framework 4, les méthodes de création et de récupération de culture ont commencé à lever un CultureNotFoundException au lieu d’un ArgumentException si la culture est introuvable. Étant donné que CultureNotFoundException dérive de ArgumentException, il s’agit d’une modification acceptable.

  • ✔️ AUTORISÉ : levée d’une exception plus spécifique que NotSupportedException, NotImplementedException, NullReferenceException

  • ✔️ AUTORISÉ : levée d’une exception qui est considérée comme irrécupérable

    Les exceptions irrécupérables ne doivent pas être interceptées, mais plutôt être gérées par un gestionnaire fourre-tout de haut niveau. Par conséquent, les utilisateurs ne sont pas censés avoir du code qui intercepte les exceptions explicites. Les exceptions irrécupérables sont :

  • ✔️ AUTORISÉ : levée une exception dans un nouveau chemin de code

    L’exception doit s’appliquer uniquement à un nouveau chemin de code qui est exécuté avec les nouvelles valeurs de paramètre ou le nouvel état, et qui ne peut pas être exécuté par le code existant qui cible la version précédente.

  • ✔️ AUTORISÉ : suppression d’une exception pour permettre un comportement plus robuste ou de nouveaux scénarios️

    Par exemple, une méthode Divide qui pouvait auparavant uniquement gérer des valeurs positives et levait un ArgumentOutOfRangeException peut être modifiée pour prendre en charge les valeurs positives et négatives sans lever d’exception.

  • ✔️ AUTORISÉ : modification du texte d’un message d’erreur

    Les développeurs ne doivent pas compter sur le texte des messages d’erreur, qui changent également selon la culture de l’utilisateur.

  • NON AUTORISÉ : levée d’une exception dans tous les autres cas non répertoriés ci-dessus

  • NON AUTORISÉ : suppression d’une exception dans tous les autres cas non répertoriés ci-dessus

Attributs

  • ✔️ AUTORISÉ : modification de la valeur d’un attribut qui n’est pas observable

  • NON AUTORISÉ : modification de la valeur d’un attribut qui est observable

  • NÉCESSITE UN JUGEMENT : suppression d’un attribut

    Dans la plupart des cas, la suppression d’un attribut (comme NonSerializedAttribute) est un changement cassant.

Plateforme prise en charge

  • ✔️ AUTORISÉ : prise en charge d’une opération sur une plateforme qui n’était précédemment pas prise en charge

  • NON AUTORISÉ : cesser la prise en charge ou commencer à requérir un service pack spécifique pour une opération qui était précédemment prise en charge sur une plateforme

Modifications de l’implémentation interne

  • NÉCESSITE UN JUGEMENT : modification de la surface d’un type interne

    Ces modifications sont généralement autorisées, même si elles rompent la réflexion privée. Dans certains cas, où les bibliothèques tierces les plus courantes ou un grand nombre de développeurs dépendent d’API internes, ces modifications ne peuvent pas être autorisées.

  • NÉCESSITE UN JUGEMENT : modification de l’implémentation interne d’un membre

    Ces modifications sont généralement autorisées, même si elles rompent la réflexion privée. Dans certains cas où le code du client dépend souvent de la réflexion privée ou où la modification introduit des effets secondaires involontaires, ces modifications peuvent ne pas être autorisées.

  • ✔️ AUTORISÉ : amélioration des performances d’une opération

    La possibilité de modifier les performances d’une opération est essentielle, mais ces modifications peuvent endommager le code repose sur la vitesse actuelle d’une opération. Cela est particulièrement vrai pour du code qui dépend de la chronologie des opérations asynchrones. La modification de performances ne doit avoir aucun effet sur les autres comportements de l’API en question ; sinon, la modification sera un changement cassant.

  • ✔️ AUTORISÉ : changement indirect (et souvent avec un impact négatif) des performances d’une opération

    Si la modification en question n’est pas considérée comme un changement cassant pour une raison quelconque, cela est acceptable. Souvent, des mesures doivent être prises, ce qui peut inclure des opérations supplémentaires ou l’ajout de nouvelles fonctionnalités. Cela affectera presque toujours les performances, mais peut s’avérer essentiel pour que l’API en question fonctionne comme prévu.

  • NON AUTORISÉ : modification d’une API synchrone en asynchrone (et vice versa)

Modifications du code

  • ✔️ AUTORISÉ : ajout de params à un paramètre

  • NON AUTORISÉ : changement d’un struct en classe et vice versa

  • NON AUTORISÉ : ajout du mot clé checked à un bloc de code

    Cette modification peut pousser le code précédemment exécuté à lever OverflowException, ce qui n’est pas acceptable.

  • NON AUTORISÉ : suppression de params à partir d’un paramètre

  • NON AUTORISÉ : modification de l’ordre dans lequel les événements sont déclenchés

    Les développeurs peuvent raisonnablement s’attendre à ce que les événements se déclenchent dans le même ordre, et le code développeur dépend souvent de l’ordre dans lequel les événements sont déclenchés.

  • NON AUTORISÉ : suppression du déclenchement d’un événement sur une action donnée

  • NON AUTORISÉ : modification du nombre de fois que des événements donnés sont appelés

  • NON AUTORISÉ : ajout de FlagsAttribute à un type d’énumération

Voir aussi