Contrats de code (.NET Framework)
Remarque
Cet article est spécifique au .NET Framework. Ceci ne s’applique pas aux implémentations plus récentes de .NET, y compris .NET 6 et ultérieur.
Les contrats de code offrent un moyen de spécifier des préconditions, des postconditions et des invariants d’objet dans du code .NET Framework. Les conditions préalables sont des exigences qui doivent être satisfaites à l'entrée d'une méthode ou d'une propriété. Les post-conditions décrivent les attentes à la sortie de la méthode ou de la propriété. Les invariants d'objet décrivent l'état attendu pour une classe présentant un état correct.
Notes
Les contrats de code ne sont pas pris en charge dans .NET 5+ (y compris les versions de .NET Core). Envisagez d’utiliser à la place des types de référence Nullables.
Les contrats de code incluent des classes pour le marquage de votre code, un analyseur statique pour l'analyse au moment de la compilation, ainsi qu'un analyseur au moment de l'exécution. Les classes des contrats de code se trouvent dans l'espace de noms System.Diagnostics.Contracts.
Les contrats de code offrent les avantages suivants :
Tests améliorés : les contrats de code permettent la vérification de contrat statique, la vérification au moment de l'exécution et la génération de documentation.
Outils de test automatique : vous pouvez utiliser les contrats de code pour générer des tests unitaires plus explicites en filtrant les arguments de test sans signification qui ne remplissent pas les conditions préalables.
Vérification statique : le vérificateur statique peut déterminer s'il existe des violations de contrat sans exécuter le programme. Il recherche des contrats implicites, tels que des déréférencements et des limites de tableau null, ainsi que des contrats explicites.
Documentation de référence : le générateur de documentation complète les fichiers de documentation XML existants avec des informations de contrat. Des feuilles de style peuvent également être utilisées avec Sandcastle pour ajouter des sections de contrat dans les pages de documentation générées.
Tous les langages .NET Framework peuvent directement utiliser les contrats, sans que vous ayez besoin de développer un analyseur ou compilateur spécial. Un complément Visual Studio vous permet de spécifier le niveau d'analyse de contrats de code à effectuer. Les analyseurs peuvent vérifier que les contrats sont bien formés (contrôle du type et résolution de noms), et produire une forme compilée des contrats au format CIL (Common Intermediate Language). La création de contrats dans Visual Studio vous permet de tirer parti de la fonctionnalité IntelliSense standard fournie par l'outil.
La plupart des méthodes dans la classe de contrat sont compilées de façon conditionnelle : le compilateur émet des appels à ces méthodes seulement si vous définissez le symbole spécial CONTRACTS_FULL en utilisant la directive #define
. Avec CONTRACTS_FULL, vous pouvez écrire des contrats dans votre code sans utiliser de directives #ifdef
, et générer ainsi différentes builds, avec et sans contrats.
Pour obtenir des outils et des instructions détaillées sur l’utilisation des contrats de code, consultez Contrats de code sur le site Visual Studio Marketplace.
Preconditions
Vous pouvez spécifier des conditions préalables à l'aide de la méthode Contract.Requires. Les conditions préalables définissent l'état à l'appel d'une méthode. Elles sont généralement utilisées pour indiquer des valeurs de paramètre valides. Tous les membres spécifiés dans les conditions préalables doivent être au moins aussi accessibles que la méthode elle-même. Autrement, la condition préalable risque de ne pas être comprise par tous les appelants de la méthode. La condition ne doit pas avoir d'effets secondaires. Le comportement au moment de l'exécution des conditions préalables non réussies est déterminé par l'analyseur au moment de l'exécution.
Par exemple, la condition préalable suivante spécifie que le paramètre x
ne doit pas être null.
Contract.Requires(x != null);
Si votre code doit lever une exception particulière en cas d'échec d'une condition préalable, utilisez la surcharge générique de Requires comme suit.
Contract.Requires<ArgumentNullException>(x != null, "x");
Instructions Requires héritées
Dans la plupart des cas, le code contient du code de validation de paramètres sous la forme d’instructions if
-then
-throw
. Les outils de contrat reconnaissent ces instructions comme étant des conditions préalables dans les cas suivants :
Ces instructions sont placées avant toutes les autres instructions dans une méthode.
L'ensemble de ces instructions est suivi d'un appel de méthode Contract explicite, tel qu'un appel à la méthode Requires, Ensures, EnsuresOnThrow ou EndContractBlock.
Quand les instructions if
-then
-throw
apparaissent sous cette forme, les outils les reconnaissent en tant qu’instructions requires
héritées. Si aucun autre contrat ne suit la séquence if
-then
-throw
, terminez le code par la méthode Contract.EndContractBlock.
if (x == null) throw new ...
Contract.EndContractBlock(); // All previous "if" checks are preconditions
Notez que la condition du test précédent est une condition préalable négative (La précondition réelle serait x != null
.) Une précondition négative est très restreinte. Elle doit être écrite comme indiqué dans l’exemple précédent : elle ne doit pas contenir de clauses else
et le corps de la clause then
doit être une seule instruction throw
. Le test if
est soumis aux règles de pureté et à celles de visibilité (voir Indications relatives à l’utilisation), mais l’expression throw
est soumise uniquement aux règles de pureté. Toutefois, le type de l'exception levée doit être aussi visible que la méthode dans laquelle le contrat se produit.
Postconditions
Les post-conditions sont des contrats relatifs à l'état d'une méthode qui se termine. La post-condition est vérifiée juste avant la sortie de la méthode. Le comportement au moment de l'exécution des post-conditions non réussies est déterminé par l'analyseur au moment de l'exécution.
Contrairement aux conditions préalables, les post-conditions peuvent référencer des membres ayant moins de visibilité. Il est possible qu'un client ne puisse pas comprendre ou utiliser certaines des informations spécifiées par une post-condition avec un état privé, mais cela n'empêche pas le client d'utiliser la méthode correctement.
Post-conditions standard
Vous pouvez spécifier les post-conditions standard à l'aide de la méthode Ensures. Les post-conditions expriment une condition qui doit être true
à l'arrêt normal de la méthode.
Contract.Ensures(this.F > 0);
Post-conditions exceptionnelles
Les post-conditions exceptionnelles sont des post-conditions qui doivent être true
quand une exception particulière est levée par une méthode. Vous pouvez les spécifier à l'aide de la méthode Contract.EnsuresOnThrow, comme le montre l'exemple suivant.
Contract.EnsuresOnThrow<T>(this.F > 0);
L’argument est la condition qui doit être true
chaque fois qu’une exception qui est un sous-type de T
est levée.
Certains types d'exception sont difficiles à utiliser dans une post-condition exceptionnelle. Par exemple, l'utilisation du type Exception pour T
nécessite que la méthode garantisse la condition indépendamment du type d'exception levée, même s'il s'agit d'un dépassement de capacité de la pile ou d'une autre exception impossible à contrôler. Utilisez les post-conditions exceptionnelles uniquement pour des exceptions spécifiques qui peuvent être levées après l'appel d'un membre, par exemple, quand une exception InvalidTimeZoneException est levée pour un appel de méthode TimeZoneInfo.
Post-conditions spéciales
Les méthodes suivantes peuvent être utilisées uniquement au sein de post-conditions :
Vous pouvez faire référence aux valeurs de retour de la méthode dans les post-conditions à l'aide de l'expression
Contract.Result<T>()
, oùT
est remplacé par le type de retour de la méthode. Si le compilateur ne peut pas déduire le type, vous devez le fournir explicitement. Par exemple, le compilateur C# ne peut pas déduire les types des méthodes qui ne prennent pas d'arguments. Il nécessite donc la post-condition suivante :Contract.Ensures(0 <Contract.Result<int>())
. Les méthodes possédant le type de retourvoid
ne peuvent pas faire référence àContract.Result<T>()
dans leurs post-conditions.Une valeur de « pré-état » dans une post-condition fait référence à la valeur d'une expression au début d'une méthode ou d'une propriété. Elle utilise l'expression
Contract.OldValue<T>(e)
, oùT
est le type dee
. Vous pouvez omettre l'argument de type générique chaque fois que le compilateur est en mesure de déduire son type (Par exemple, le compilateur C# infère toujours le type, car il prend un argument.) Il existe plusieurs restrictions sur ce qui peut se produire danse
et les contextes dans lesquels une ancienne expression peut apparaître. Une expression ancienne ne peut pas contenir une autre expression ancienne. Encore plus important, une expression ancienne doit faire référence à une valeur qui a existé dans l'état de condition préalable de la méthode. En d'autres termes, il doit s'agir d'une expression qui peut être évaluée tant que la condition préalable de la méthode esttrue
. Voici plusieurs instances de cette règle :La valeur doit exister dans l'état de condition préalable de la méthode. Pour référencer un champ sur un objet, les préconditions doivent garantir que l’objet a toujours une valeur non Null.
Vous ne pouvez pas faire référence à la valeur de retour de la méthode dans une expression ancienne :
Contract.OldValue(Contract.Result<int>() + x) // ERROR
Vous ne pouvez pas faire référence aux paramètres
out
dans une expression ancienne.Une expression ancienne ne peut pas dépendre de la variable liée d'un quantificateur si la plage du quantificateur dépend de la valeur de retour de la méthode :
Contract.ForAll(0, Contract.Result<int>(), i => Contract.OldValue(xs[i]) > 3); // ERROR
Une expression ancienne ne peut pas faire référence au paramètre du délégué anonyme dans un appel ForAll ou Exists, sauf si elle est utilisée en tant qu’indexeur ou argument pour un appel de méthode :
Contract.ForAll(0, xs.Length, i => Contract.OldValue(xs[i]) > 3); // OK Contract.ForAll(0, xs.Length, i => Contract.OldValue(i) > 3); // ERROR
Une expression ancienne ne peut pas s'utiliser dans le corps d'un délégué anonyme si la valeur de l'expression ancienne dépend de l'un des paramètres du délégué anonyme, à moins que le délégué anonyme ne soit un argument de la méthode ForAll ou Exists :
Method(... (T t) => Contract.OldValue(... t ...) ...); // ERROR
Les paramètres
Out
posent un problème, car les contrats sont placés avant le corps de la méthode, et la plupart des compilateurs n'autorisent pas les références aux paramètresout
dans les post-conditions. Pour résoudre ce problème, la classe Contract fournit la méthode ValueAtReturn, qui permet d'utiliser une post-condition basée sur un paramètreout
.public void OutParam(out int x) { Contract.Ensures(Contract.ValueAtReturn(out x) == 3); x = 3; }
Comme avec la méthode OldValue, vous pouvez omettre le paramètre de type générique chaque fois que le compilateur est en mesure de déduire son type. Le module de réécriture de contrat remplace l'appel de méthode par la valeur du paramètre
out
. La méthode ValueAtReturn peut apparaître uniquement dans des post-conditions. L’argument de la méthode doit être un paramètreout
ou un champ d’un paramètreout
de structure. Ce dernier est également utile pour faire référence aux champs dans la post-condition d'un constructeur de structure.Notes
Actuellement, les outils d'analyse de contrat de code ne vérifient pas si les paramètres
out
sont correctement initialisés et ignorent leur mention dans la post-condition. Si, dans l'exemple précédent, la ligne après le contrat avait utilisé la valeur dex
au lieu de lui assigner un nombre entier, un compilateur n'aurait donc pas généré l'erreur correspondante. Toutefois, dans une build où le symbole de préprocesseur CONTRACTS_FULL n’est pas défini (telle qu’une build de mise en production), le compilateur génère une erreur.
Invariants
Les invariants d'objet sont des conditions qui doivent être remplies (true) pour chaque instance d'une classe dès que cet objet est visible par un client. Ils spécifient les conditions sous lesquelles l'objet est considéré comme étant correct.
Les méthodes invariantes sont identifiées par l'attribut ContractInvariantMethodAttribute. Elles ne doivent contenir aucun code à l'exception d'une séquence d'appels à la méthode Invariant, chacun spécifiant un invariant individuel, comme indiqué dans l'exemple suivant.
[ContractInvariantMethod]
protected void ObjectInvariant ()
{
Contract.Invariant(this.y >= 0);
Contract.Invariant(this.x > this.y);
...
}
Les invariants sont définis de façon conditionnelle par le symbole de préprocesseur CONTRACTS_FULL. Ils sont vérifiés au moment de l'exécution à la fin de chaque méthode publique. Si un invariant spécifie une méthode publique dans la même classe, la vérification d'invariant prévue normalement à la fin de cette méthode publique est désactivée. À la place, la vérification se produit uniquement à la fin de l'appel de méthode le plus à l'extérieur de cette classe. Cela se produit également si la classe est à nouveau entrée à cause d'un appel à une méthode sur une autre classe. Les invariants ne sont pas vérifiés pour un finaliseur d’objet et une implémentation de IDisposable.Dispose.
Instructions d’utilisation
Classement de contrat
Le tableau suivant indique l'ordre des éléments à utiliser quand vous écrivez des contrats de méthode.
If-then-throw statements |
Conditions préalables publiques à compatibilité descendante |
---|---|
Requires | Toutes les conditions préalables publiques |
Ensures | Toutes les post-conditions standard publiques |
EnsuresOnThrow | Toutes les post-conditions exceptionnelles publiques |
Ensures | Toutes les post-conditions standard privées/internes |
EnsuresOnThrow | Toutes les post-conditions exceptionnelles privées/internes |
EndContractBlock | Si vous utilisez des conditions préalables de style if -then -throw sans autres contrats, ajoutez un appel à EndContractBlock pour indiquer que toutes les vérifications if précédentes sont des conditions préalables. |
Pureté
Toutes les méthodes appelées dans un contrat doivent être pures, c'est-à-dire qu'elles ne doivent pas mettre à jour un état préexistant. Une méthode pure peut modifier des objets qui ont été créés après l'entrée dans la méthode pure.
Les outils de contrat de code considèrent actuellement que les éléments de code suivants sont purs :
Méthodes marquées avec l'attribut PureAttribute.
Types marqués avec l'attribut PureAttribute (cet attribut s'applique à toutes les méthodes du type).
Accesseurs get de propriété.
Opérateurs (méthodes statiques dont le nom commence par « op », qui comportent un ou deux paramètres et dont le type de retour n’est pas void).
Méthodes dont le nom qualifié complet commence par « System.Diagnostics.Contracts.Contract », « System.String », « System.IO.Path » ou « System.Type ».
Les délégués appelés, à condition que le type délégué lui-même soit attribué avec l'attribut PureAttribute. Les types délégués System.Predicate<T> et System.Comparison<T> sont considérés comme purs.
Visibilité
Tous les membres indiqués dans un contrat doivent être au moins aussi visibles que la méthode dans laquelle ils apparaissent. Par exemple, un champ privé ne peut pas être spécifié dans une condition préalable pour une méthode publique, car les clients ne peuvent pas valider un tel contrat avant d'appeler la méthode. Cependant, si le champ est marqué avec l'attribut ContractPublicPropertyNameAttribute, il est exempté de ces règles.
Exemple
L'exemple suivant illustre l'utilisation des contrats de code.
#define CONTRACTS_FULL
using System;
using System.Diagnostics.Contracts;
// An IArray is an ordered collection of objects.
[ContractClass(typeof(IArrayContract))]
public interface IArray
{
// The Item property provides methods to read and edit entries in the array.
Object this[int index]
{
get;
set;
}
int Count
{
get;
}
// Adds an item to the list.
// The return value is the position the new element was inserted in.
int Add(Object value);
// Removes all items from the list.
void Clear();
// Inserts value into the array at position index.
// index must be non-negative and less than or equal to the
// number of elements in the array. If index equals the number
// of items in the array, then value is appended to the end.
void Insert(int index, Object value);
// Removes the item at position index.
void RemoveAt(int index);
}
[ContractClassFor(typeof(IArray))]
internal abstract class IArrayContract : IArray
{
int IArray.Add(Object value)
{
// Returns the index in which an item was inserted.
Contract.Ensures(Contract.Result<int>() >= -1);
Contract.Ensures(Contract.Result<int>() < ((IArray)this).Count);
return default(int);
}
Object IArray.this[int index]
{
get
{
Contract.Requires(index >= 0);
Contract.Requires(index < ((IArray)this).Count);
return default(int);
}
set
{
Contract.Requires(index >= 0);
Contract.Requires(index < ((IArray)this).Count);
}
}
public int Count
{
get
{
Contract.Requires(Count >= 0);
Contract.Requires(Count <= ((IArray)this).Count);
return default(int);
}
}
void IArray.Clear()
{
Contract.Ensures(((IArray)this).Count == 0);
}
void IArray.Insert(int index, Object value)
{
Contract.Requires(index >= 0);
Contract.Requires(index <= ((IArray)this).Count); // For inserting immediately after the end.
Contract.Ensures(((IArray)this).Count == Contract.OldValue(((IArray)this).Count) + 1);
}
void IArray.RemoveAt(int index)
{
Contract.Requires(index >= 0);
Contract.Requires(index < ((IArray)this).Count);
Contract.Ensures(((IArray)this).Count == Contract.OldValue(((IArray)this).Count) - 1);
}
}
#Const CONTRACTS_FULL = True
Imports System.Diagnostics.Contracts
' An IArray is an ordered collection of objects.
<ContractClass(GetType(IArrayContract))> _
Public Interface IArray
' The Item property provides methods to read and edit entries in the array.
Default Property Item(ByVal index As Integer) As [Object]
ReadOnly Property Count() As Integer
' Adds an item to the list.
' The return value is the position the new element was inserted in.
Function Add(ByVal value As Object) As Integer
' Removes all items from the list.
Sub Clear()
' Inserts value into the array at position index.
' index must be non-negative and less than or equal to the
' number of elements in the array. If index equals the number
' of items in the array, then value is appended to the end.
Sub Insert(ByVal index As Integer, ByVal value As [Object])
' Removes the item at position index.
Sub RemoveAt(ByVal index As Integer)
End Interface 'IArray
<ContractClassFor(GetType(IArray))> _
Friend MustInherit Class IArrayContract
Implements IArray
Function Add(ByVal value As Object) As Integer Implements IArray.Add
' Returns the index in which an item was inserted.
Contract.Ensures(Contract.Result(Of Integer)() >= -1) '
Contract.Ensures(Contract.Result(Of Integer)() < CType(Me, IArray).Count) '
Return 0
End Function 'IArray.Add
Default Property Item(ByVal index As Integer) As Object Implements IArray.Item
Get
Contract.Requires(index >= 0)
Contract.Requires(index < CType(Me, IArray).Count)
Return 0 '
End Get
Set(ByVal value As [Object])
Contract.Requires(index >= 0)
Contract.Requires(index < CType(Me, IArray).Count)
End Set
End Property
Public ReadOnly Property Count() As Integer Implements IArray.Count
Get
Contract.Requires(Count >= 0)
Contract.Requires(Count <= CType(Me, IArray).Count)
Return 0 '
End Get
End Property
Sub Clear() Implements IArray.Clear
Contract.Ensures(CType(Me, IArray).Count = 0)
End Sub
Sub Insert(ByVal index As Integer, ByVal value As [Object]) Implements IArray.Insert
Contract.Requires(index >= 0)
Contract.Requires(index <= CType(Me, IArray).Count) ' For inserting immediately after the end.
Contract.Ensures(CType(Me, IArray).Count = Contract.OldValue(CType(Me, IArray).Count) + 1)
End Sub
Sub RemoveAt(ByVal index As Integer) Implements IArray.RemoveAt
Contract.Requires(index >= 0)
Contract.Requires(index < CType(Me, IArray).Count)
Contract.Ensures(CType(Me, IArray).Count = Contract.OldValue(CType(Me, IArray).Count) - 1)
End Sub
End Class