Questo articolo è specifico per .NET Framework. Non si applica alle implementazioni più recenti di .NET, incluse .NET 6 e versioni successive.
I contratti di codice consentono di specificare precondizioni, postcondizioni e invarianti dell'oggetto nel codice .NET Framework. Le precondizioni sono requisiti da soddisfare quando si accede a un metodo o a una proprietà. Le postcondizioni descrivono le aspettative al momento dell'uscita dal codice del metodo o della proprietà. Le invarianti dell'oggetto descrivono lo stato previsto per una classe in stato integro.
Nota
I contratti di codice non sono supportati in .NET 5+ (incluse le versioni di .NET Core). Valutare la possibilità di usare tipi riferimento nullable in alternativa.
I contratti di codice includono classi che consentono di contrassegnare il codice, un analizzatore statico per l'analisi in fase di compilazione e un analizzatore di runtime. Le classi per i contratti di codice sono disponibili nello spazio dei nomi System.Diagnostics.Contracts.
I vantaggi dei contratti di codice includono:
Test migliorati: i contratti di codice consentono la verifica statica, il controllo di runtime e la generazione di documentazione.
Strumenti di test automatici: è possibile usare i contratti di codice per generare unit test più significativi eliminando gli argomenti di test inutili che non soddisfano le precondizioni.
Verifica statica: lo strumento di controllo statico può rilevare eventuali violazioni dei contratti senza eseguire il programma. Verifica la presenza di contratti impliciti, ad esempio dereferenziazioni null e limiti di matrici, e di contratti espliciti.
Documentazione di riferimento: il generatore di documentazione integra i file di documentazione XML esistenti con le informazioni sul contratto. Sono anche presenti fogli di stile utilizzabili con Sandcastle in modo che nelle pagine della documentazione generate siano contenute sezioni relative ai contratti.
I contratti possono essere usati immediatamente da tutti i linguaggi di .NET Framework; non è necessario scrivere un parser o un compilatore speciale. Un componente aggiuntivo di Visual Studio consente di specificare il livello di analisi dei contratti di codice da eseguire. Gli analizzatori possono confermare che i contratti sono formalmente corretti (controllo del tipo e risoluzione dei nomi) e possono produrre un form compilato dei contratti nel formato Common Intermediate Language (CIL). La funzionalità di creazione dei contratti in Visual Studio consente di usare IntelliSense standard fornito dallo strumento.
La maggior parte dei metodi nella classe dei contratti sono compilati in modo condizionale; in altre parole, il compilatore genera chiamate a questi metodi solo quando si definisce un simbolo speciale, CONTRACTS_FULL, tramite la direttiva #define. CONTRACTS_FULL consente di scrivere contratti nel codice senza usare le direttive #ifdef; è possibile produrre build diverse, alcune con contratti e altre senza.
Per gli strumenti e le istruzioni dettagliate per l'uso dei contratti di codice, vedere Contratti di codice nel sito del marketplace di Visual Studio.
Condizioni preliminari
È possibile esprimere delle precondizioni usando il metodo Contract.Requires. Le precondizioni specificano lo stato nel momento in cui viene richiamato un metodo. In genere, vengono usate per specificare valori di parametro validi. Tutti i membri menzionati nelle precondizioni devono essere accessibili almeno quanto il metodo stesso; in caso contrario, la precondizione potrebbe non essere compresa da tutti i chiamanti di un metodo. La condizione non deve avere effetti collaterali. Il comportamento in fase di esecuzione delle precondizioni con errori è determinato dall'analizzatore di runtime.
Ad esempio, la precondizione seguente indica che il parametro x non deve essere null.
C#
Contract.Requires(x != null);
Se il codice deve generare una particolare eccezione in caso di errore di una precondizione, è possibile usare l'overload generico di Requires come descritto di seguito.
La maggior parte del codice contiene la convalida dei parametri sotto forma di codice if-then-throw. Gli strumenti dei contratti riconoscono queste istruzioni come precondizioni nei casi seguenti:
Le istruzioni vengono visualizzate prima di qualsiasi altra istruzione in un metodo.
Quando le istruzioni if-then-throw vengono visualizzate in questo formato, gli strumenti le riconoscono come istruzioni requires legacy. Se la sequenza if-then-throw non è seguita da altri contratti, terminare il codice con il metodo Contract.EndContractBlock.
C#
if (x == null) thrownew ...
Contract.EndContractBlock(); // All previous "if" checks are preconditions
La condizione nel test precedente è una precondizione negata. La precondizione effettiva sarebbe x != null. Una precondizione negata è altamente limitata: deve essere scritta come mostrato nell'esempio precedente, quindi non deve contenere clausole else e il corpo della clausola then deve essere un'unica istruzione throw. Il test if è soggetto a regole di purezza e visibilità (vedere Linee guida di utilizzo), ma l'espressione throw è soggetta solo a regole di purezza. Tuttavia, il tipo dell'eccezione generata deve essere visibile quanto il metodo in cui si verifica il contratto.
Postconditions
Le postcondizioni sono contratti per lo stato di un metodo nel momento in cui termina. La postcondizione viene controllata appena prima dell'uscita da un metodo. Il comportamento in fase di esecuzione delle postcondizioni con errori viene determinato dall'analizzatore di runtime.
Diversamente dalle precondizioni, le postcondizioni possono fare riferimento a membri con visibilità inferiore. Un client potrebbe non essere in grado di comprendere o usare alcune delle informazioni espresse da una postcondizione tramite uno stato privato, tuttavia ciò non influisce sulla capacità del client di usare il metodo correttamente.
Postcondizioni standard
È possibile esprimere postcondizioni standard con il metodo Ensures. Le postcondizioni esprimono una condizione che deve essere true quando il metodo termina regolarmente.
C#
Contract.Ensures(this.F > 0);
Postcondizioni eccezionali
Le postcondizioni eccezionali sono postcondizioni che devono essere true quando una particolare eccezione viene generata da un metodo. È possibile specificare queste postcondizioni tramite il metodo Contract.EnsuresOnThrow, come mostrato nell'esempio seguente.
C#
Contract.EnsuresOnThrow<T>(this.F > 0);
L'argomento è la condizione che deve essere true quando viene generata un'eccezione che corrisponde a un sottotipo di T.
Alcuni tipi di eccezione sono difficili da usare in una postcondizione eccezionale. Ad esempio, l'uso del tipo Exception per T richiede che il metodo garantisca la condizione indipendentemente dal tipo di eccezione generato, anche se si tratta di un overflow dello stack o di un'altra eccezione impossibile da controllare. Le postcondizioni eccezionali devono essere usate solo per eccezioni specifiche che potrebbero essere generate quando viene chiamato un membro, ad esempio quando viene generata un'eccezione InvalidTimeZoneException per una chiamata al metodo TimeZoneInfo.
Postcondizioni speciali
I seguenti metodi possono essere usati solo all'interno di postcondizioni:
È possibile fare riferimento ai valori restituiti dai metodi nelle postcondizioni usando l'espressione Contract.Result<T>(), dove T viene sostituito dal tipo restituito del metodo. Quando il compilatore non è in grado di dedurre il tipo, è necessario fornirlo in modo esplicito. Il compilatore C#, ad esempio, non è in grado di dedurre i tipi per i metodi che non accettano argomenti, pertanto richiede la seguente postcondizione: Contract.Ensures(0 <Contract.Result<int>()). I metodi con un tipo restituito void non possono fare riferimento a Contract.Result<T>() nelle relative postcondizioni.
Un valore di prestato in una postcondizione fa riferimento al valore di un'espressione all'inizio di un metodo o di una proprietà. Usa l'espressione Contract.OldValue<T>(e), dove T è il tipo di e. È possibile omettere l'argomento di tipo generico quando il compilatore è in grado di dedurre il tipo. Il compilatore C#, ad esempio, deduce sempre il tipo poiché accetta un argomento. Esistono diverse restrizioni relative a quanto può accadere in e e i contesti nei quali può essere visualizzata un'espressione Old. Un'espressione Old non può contenere un'altra espressione Old. In particolare, un'espressione Old deve fare riferimento a un valore esistente nello stato di precondizione del metodo. In altre parole, deve essere un'espressione valutabile finché la precondizione del metodo resta true. Di seguito sono riportate diverse istanze di questa regola.
Il valore deve esistere nello stato di precondizione del metodo. Per fare riferimento a un campo in un oggetto, le precondizioni devono garantire che l'oggetto sia sempre non null.
Non è possibile fare riferimento al valore restituito del metodo in un'espressione Old:
Non è possibile fare riferimento a parametri out in un'espressione Old.
Un'espressione Old non può dipendere dalla variabile associata di un quantificatore se l'intervallo del quantificatore dipende dal valore restituito del metodo:
C#
Contract.ForAll(0, Contract.Result<int>(), i => Contract.OldValue(xs[i]) > 3); // ERROR
Un'espressione Old non può fare riferimento al parametro del delegato anonimo in una chiamata a ForAll o Exists a meno che non venga usata come indicizzatore o argomento di una chiamata al metodo:
C#
Contract.ForAll(0, xs.Length, i => Contract.OldValue(xs[i]) > 3); // OK
Contract.ForAll(0, xs.Length, i => Contract.OldValue(i) > 3); // ERROR
Un'espressione Old non può verificarsi nel corpo di un delegato anonimo se il valore dell'espressione dipende da uno dei parametri del delegato, a meno che quest'ultimo non sia un argomento del metodo ForAll o Exists:
C#
Method(... (T t) => Contract.OldValue(... t ...) ...); // ERROR
I parametri Out presentano un problema in quanto i contratti vengono visualizzati prima del corpo del metodo e la maggior parte dei compilatori non consente riferimenti ai parametri out nelle postcondizioni. Per risolvere il problema, la classe Contract fornisce il metodo ValueAtReturn che consente una postcondizione basata su un parametro out.
Come per il metodo OldValue, è possibile omettere il parametro di tipo generico quando il compilatore è in grado di dedurre il tipo. Il rewriter del contratto sostituisce la chiamata al metodo con il valore del parametro out. Il metodo ValueAtReturn può essere visualizzato solo nelle postcondizioni. L'argomento del metodo deve essere un parametro out o un campo del parametro out di una struttura. Quest'ultimo è utile anche in caso di riferimento a campi nella postcondizione di un costruttore della struttura.
Nota
Attualmente, gli strumenti di analisi dei contratti di codice non verificano se i parametri out vengono inizializzati correttamente e ne ignorano la menzione nella postcondizione. Quindi, nell'esempio precedente, se la riga dopo il contratto avesse usato il valore x anziché assegnare un numero intero, un compilatore non avrebbe generato l'errore corretto. Tuttavia, in una build in cui il simbolo del preprocessore CONTRACTS_FULL non viene definito (ad esempio in una build di rilascio), il compilatore genererà un errore.
Invarianti
Le invarianti dell'oggetto sono condizioni che devono essere true per ogni istanza di una classe quando l'oggetto è visibile a un client. Esprimono le condizioni con le quali l'oggetto viene considerato corretto.
I metodi invarianti vengono contrassegnati ai fini dell'identificazione con l'attributo ContractInvariantMethodAttribute. I metodi invarianti non devono contenere codice, fatta eccezione per una sequenza di chiamate al metodo Invariant, ognuna delle quali specifica una singola invariante, come mostrato nell'esempio seguente.
Le invarianti vengono definite in modo condizionale dal simbolo del preprocessore CONTRACTS_FULL. Durante il controllo in fase di esecuzione, le invarianti vengono controllate alla fine di ogni metodo pubblico. Se un'invariante menziona un metodo pubblico nella stessa classe, il controllo dell'invariante che avverrebbe normalmente alla fine di tale metodo viene disabilitato. Al contrario, il controllo viene eseguito solo alla fine della chiamata al metodo più esterna in quella classe. Ciò avviene anche se la classe viene immessa di nuovo a causa di una chiamata a un metodo in un'altra classe. Le invarianti non vengono controllate per un finalizzatore di oggetto e un'implementazione di IDisposable.Dispose.
Linee guida per l'utilizzo
Ordine dei contratti
La tabella seguente mostra l'ordine degli elementi da usare per la scrittura di contratti del metodo.
If-then-throw statements
Precondizioni pubbliche compatibili con le versioni precedenti
Se si usano le precondizioni di stile if-then-throw senza altri contratti, inserire una chiamata a EndContractBlock per indicare che tutti i controlli if precedenti sono precondizioni.
Purezza
Tutti i metodi chiamati all'interno di un contratto devono essere puri; in altre parole, non devono aggiornare stati preesistenti. Un metodo puro può modificare gli oggetti creati in seguito all'accesso al metodo stesso.
Gli strumenti dei contratti di codice presuppongono che i seguenti elementi di codice siano puri:
Tutti i membri menzionati in un contratto devono essere visibili almeno quanto il metodo in cui vengono visualizzati. Un campo privato, ad esempio, non può essere menzionato in una precondizione per un metodo pubblico; i client non possono convalidare un simile contratto prima di chiamare il metodo. Tuttavia, se il campo è contrassegnato con ContractPublicPropertyNameAttribute, è esente da queste regole.
Esempio
L'esempio seguente mostra l'uso dei contratti di codice.
C#
#define CONTRACTS_FULLusing System;
using System.Diagnostics.Contracts;
// An IArray is an ordered collection of objects.
[ContractClass(typeof(IArrayContract))]
publicinterfaceIArray
{
// 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.intAdd(Object value);
// Removes all items from the list.voidClear();
// 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.voidInsert(int index, Object value);
// Removes the item at position index.voidRemoveAt(int index);
}
[ContractClassFor(typeof(IArray))]
internalabstractclassIArrayContract : 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);
returndefault(int);
}
Object IArray.this[int index]
{
get
{
Contract.Requires(index >= 0);
Contract.Requires(index < ((IArray)this).Count);
returndefault(int);
}
set
{
Contract.Requires(index >= 0);
Contract.Requires(index < ((IArray)this).Count);
}
}
publicint Count
{
get
{
Contract.Requires(Count >= 0);
Contract.Requires(Count <= ((IArray)this).Count);
returndefault(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
Collabora con noi su GitHub
L'origine di questo contenuto è disponibile in GitHub, in cui è anche possibile creare ed esaminare i problemi e le richieste pull. Per ulteriori informazioni, vedere la guida per i collaboratori.
Feedback su .NET
.NET è un progetto di open source. Selezionare un collegamento per fornire feedback:
Informazioni su come implementare classi usando tecniche avanzate come classi statiche, classi parziali e inizializzatori di oggetti che possono migliorare la leggibilità, la gestibilità e l'organizzazione del codice.
Questa specifica di funzionalità descrive gli aggiornamenti della sintassi necessari per supportare i metodi di interfaccia predefiniti. Questo include la dichiarazione dei corpi nelle dichiarazioni di interfaccia e il supporto dei modificatori nelle dichiarazioni.
Informazioni sul modificatore di record per i tipi di classe e struct in C#. I record forniscono supporto standard per l'uguaglianza dei valori in base alle istanze dei tipi di record.
Le espressioni di raccolta convertono in molti tipi di raccolta. È possibile scrivere valori letterali, espressioni o altre raccolte per creare una nuova raccolta.