Kodkontrakt (.NET Framework)
Kommentar
Den här artikeln är specifik för .NET Framework. Det gäller inte för nyare implementeringar av .NET, inklusive .NET 6 och senare versioner.
Kodkontrakt är ett sätt att ange förhandsvillkor, postkonditioner och objektvarianter i .NET Framework-kod. Förhandsvillkor är krav som måste uppfyllas när du anger en metod eller egenskap. Postconditions beskriver förväntningar när metoden eller egenskapskoden avslutas. Objektvarianter beskriver det förväntade tillståndet för en klass som är i ett bra tillstånd.
Kommentar
Kodkontrakt stöds inte i .NET 5+ (inklusive .NET Core-versioner). Överväg att använda null-referenstyper i stället.
Kodkontrakt innehåller klasser för att markera din kod, en statisk analys för kompileringstidsanalys och en körningsanalys. Klasserna för kodkontrakt finns i System.Diagnostics.Contracts namnområdet.
Fördelarna med kodkontrakt är följande:
Förbättrad testning: Kodkontrakt ger statisk kontraktsverifiering, körningskontroll och generering av dokumentation.
Automatiska testverktyg: Du kan använda kodkontrakt för att generera mer meningsfulla enhetstester genom att filtrera bort meningslösa testargument som inte uppfyller förhandsvillkoren.
Statisk verifiering: Den statiska kontrollen kan avgöra om det finns några kontraktsöverträdelser utan att köra programmet. Den söker efter implicita kontrakt, till exempel null-avreferenser och matrisgränsvärden och explicita kontrakt.
Referensdokumentation: Dokumentationsgeneratorn utökar befintliga XML-dokumentationsfiler med kontraktsinformation. Det finns även formatmallar som kan användas med Sandcastle så att de genererade dokumentationssidorna har kontraktavsnitt.
Alla .NET Framework-språk kan omedelbart dra nytta av kontrakt. du behöver inte skriva en särskild parser eller kompilator. Med ett Visual Studio-tillägg kan du ange vilken nivå av kodkontraktsanalys som ska utföras. Analysverktygen kan bekräfta att kontrakten är välformulerade (typkontroll och namnmatchning) och kan skapa en kompilerad form av kontrakten i CIL-format (common intermediate language). Med redigeringskontrakt i Visual Studio kan du dra nytta av den Standard IntelliSense som tillhandahålls av verktyget.
De flesta metoder i kontraktsklassen kompileras villkorligt. Kompilatorn sänder bara ut anrop till dessa metoder när du definierar en särskild symbol, CONTRACTS_FULL, med hjälp #define
av direktivet. CONTRACTS_FULL kan du skriva kontrakt i koden utan att använda #ifdef
direktiv. Du kan skapa olika versioner, vissa med kontrakt och andra utan.
Verktyg och detaljerade instruktioner för att använda kodkontrakt finns i Kodkontrakt på Webbplatsen för Visual Studio Marketplace.
Förvillkor
Du kan uttrycka förhandsvillkor med hjälp Contract.Requires av metoden . Förhandsvillkor anger tillstånd när en metod anropas. De används vanligtvis för att ange giltiga parametervärden. Alla medlemmar som nämns i förhandsvillkor måste vara minst lika tillgängliga som själva metoden. annars kanske inte alla som anropar en metod förstår förhandsvillkoret. Villkoret får inte ha några biverkningar. Körningsbeteendet för misslyckade förhandsvillkor bestäms av körningsanalysatorn.
Följande villkor uttrycker till exempel att parametern x
måste vara icke-null.
Contract.Requires(x != null);
Om koden måste utlösa ett visst undantag vid fel i en förhandsvillkor kan du använda den allmänna överbelastningen på Requires följande sätt.
Contract.Requires<ArgumentNullException>(x != null, "x");
Äldre kräver instruktioner
De flesta kod innehåller viss parameterverifiering i form av if
--then
throw
kod. Kontraktsverktygen identifierar dessa instruktioner som förhandsvillkor i följande fall:
Uttrycken visas före andra instruktioner i en metod.
Hela uppsättningen av sådana instruktioner följs av ett explicit Contract metodanrop, till exempel ett anrop till Requiresmetoden , Ensures, EnsuresOnThroweller EndContractBlock .
När if
--then
throw
instruktioner visas i det här formuläret identifierar verktygen dem som äldre requires
instruktioner. Om inga andra kontrakt följer sekvensen if
--then
throw
avslutar du koden med Contract.EndContractBlock -metoden.
if (x == null) throw new ...
Contract.EndContractBlock(); // All previous "if" checks are preconditions
Observera att villkoret i föregående test är en negerad förutsättning. (Den faktiska förutsättningen skulle vara x != null
.) En negerad förutsättning är mycket begränsad: Den måste skrivas enligt föregående exempel. det vill: den ska inte innehålla några else
satser, och brödtexten i then
satsen måste vara en enda throw
instruktion. Testet if
omfattas av både renhets- och synlighetsregler (se användningsriktlinjer), men throw
uttrycket omfattas endast av renhetsregler. Typen av undantag måste dock vara lika synlig som den metod där kontraktet sker.
Postconditions
Postconditions är kontrakt för tillståndet för en metod när den avslutas. Postkonditionen kontrolleras precis innan en metod avslutas. Körningsbeteendet för misslyckade postkonditioner bestäms av körningsanalysatorn.
Till skillnad från förhandsvillkor kan postkonditioner referera till medlemmar med mindre synlighet. En klient kanske inte kan förstå eller använda viss information som uttrycks i en postkondition med privat tillstånd, men detta påverkar inte klientens möjlighet att använda metoden korrekt.
Standard Postconditions
Du kan uttrycka standard postconditions med hjälp Ensures av metoden . Postconditions uttrycker ett villkor som måste vara true
vid normal avslutning av metoden.
Contract.Ensures(this.F > 0);
Exceptionella efterkonditioner
Exceptionella postkonditioner är postkonditioner som bör vara true
när ett visst undantag genereras av en metod. Du kan ange dessa postconditions med hjälp Contract.EnsuresOnThrow av metoden, som följande exempel visar.
Contract.EnsuresOnThrow<T>(this.F > 0);
Argumentet är det villkor som måste vara true
när ett undantag som är en undertyp av T
genereras.
Det finns vissa undantagstyper som är svåra att använda i en exceptionell efterkondition. Om du till exempel använder typen Exception för krävs metoden för T
att garantera villkoret oavsett vilken typ av undantag som utlöses, även om det är ett stackspill eller något annat undantag som är omöjligt att kontrollera. Du bör endast använda exceptionella postkonditioner för specifika undantag som kan genereras när en medlem anropas, till exempel när en InvalidTimeZoneException genereras för ett TimeZoneInfo metodanrop.
Särskilda postkonditioner
Följande metoder får endast användas inom efterkonditioner:
Du kan referera till metodreturvärden i postconditions med uttrycket
Contract.Result<T>()
, därT
ersätts av metodens returtyp. När kompilatorn inte kan härleda typen måste du uttryckligen ange den. C#-kompilatorn kan till exempel inte härleda typer för metoder som inte tar några argument, så det kräver följande postcondition:Contract.Ensures(0 <Contract.Result<int>())
Metoder med returtypenvoid
kan inte referera tillContract.Result<T>()
i sina postkonditioner.Ett prestate-värde i en postkondition refererar till värdet för ett uttryck i början av en metod eller egenskap. Det använder uttrycket
Contract.OldValue<T>(e)
, därT
är typen ave
. Du kan utelämna argumentet för allmän typ när kompilatorn kan härleda sin typ. (C#-kompilatorn härleder till exempel alltid typen eftersom den tar ett argument.) Det finns flera begränsningar för vad som kan inträffa ie
och de sammanhang där ett gammalt uttryck kan visas. Ett gammalt uttryck får inte innehålla ett annat gammalt uttryck. Viktigast av allt är att ett gammalt uttryck måste referera till ett värde som fanns i metodens villkorstillstånd. Med andra ord måste det vara ett uttryck som kan utvärderas så länge metodens förhandsvillkor ärtrue
. Här är flera instanser av regeln.Värdet måste finnas i metodens villkorstillstånd. För att kunna referera till ett fält i ett objekt måste förhandsvillkoren garantera att objektet alltid inte är null.
Du kan inte referera till metodens returvärde i ett gammalt uttryck:
Contract.OldValue(Contract.Result<int>() + x) // ERROR
Du kan inte referera till
out
parametrar i ett gammalt uttryck.Ett gammalt uttryck kan inte vara beroende av den bundna variabeln för en kvantifierare om kvantifierarens intervall är beroende av metodens returvärde:
Contract.ForAll(0, Contract.Result<int>(), i => Contract.OldValue(xs[i]) > 3); // ERROR
Ett gammalt uttryck kan inte referera till parametern för det anonyma ombudet i ett ForAll eller Exists -anrop om det inte används som indexerare eller argument till ett metodanrop:
Contract.ForAll(0, xs.Length, i => Contract.OldValue(xs[i]) > 3); // OK Contract.ForAll(0, xs.Length, i => Contract.OldValue(i) > 3); // ERROR
Ett gammalt uttryck kan inte förekomma i brödtexten för ett anonymt ombud om värdet för det gamla uttrycket är beroende av någon av parametrarna för det anonyma ombudet, såvida inte den anonyma delegaten ForAll är ett argument till metoden eller Exists :
Method(... (T t) => Contract.OldValue(... t ...) ...); // ERROR
Out
parametrar utgör ett problem eftersom kontrakt visas före metodens brödtext och de flesta kompilatorer tillåter inte referenser tillout
parametrar i postkonditioner. För att lösa det här problemet Contract tillhandahåller ValueAtReturn klassen metoden, som tillåter en postcondition baserat på enout
parameter.public void OutParam(out int x) { Contract.Ensures(Contract.ValueAtReturn(out x) == 3); x = 3; }
Precis som OldValue med metoden kan du utelämna den generiska typparametern när kompilatorn kan härleda dess typ. Kontraktrewriter ersätter metodanropet med värdet för parametern
out
. Metoden ValueAtReturn kan bara visas i postkonditioner. Argumentet till metoden måste vara enout
parameter eller ett fält i en strukturparameterout
. Det senare är också användbart när du refererar till fält i postkonditionen för en strukturkonstruktor.Kommentar
För närvarande kontrollerar inte verktygen för kodkontraktsanalys om
out
parametrar initieras korrekt och bortser från deras omnämnande i efterkonditionen. Om raden efter kontraktet hade använt värdetx
för i stället för att tilldela ett heltal till det i föregående exempel, skulle en kompilator därför inte utfärda rätt fel. I en version där symbolen för CONTRACTS_FULL förprocessor inte har definierats (till exempel en versionsversion) kommer kompilatorn dock att utfärda ett fel.
Invariants
Objektvarianter är villkor som ska vara sanna för varje instans av en klass när objektet är synligt för en klient. De uttrycker de villkor under vilka objektet anses vara korrekt.
De invarianta metoderna identifieras genom att markeras med attributet ContractInvariantMethodAttribute . De invarianta metoderna får inte innehålla någon kod förutom en sekvens med anrop till Invariant metoden, som var och en anger en enskild invariant, som visas i följande exempel.
[ContractInvariantMethod]
protected void ObjectInvariant ()
{
Contract.Invariant(this.y >= 0);
Contract.Invariant(this.x > this.y);
...
}
Invarianter definieras villkorligt av symbolen CONTRACTS_FULL förprocessor. Under körningskontroll kontrolleras invarianter i slutet av varje offentlig metod. Om en invariant nämner en offentlig metod i samma klass inaktiveras den invarianta kontroll som normalt skulle inträffa i slutet av den offentliga metoden. I stället sker kontrollen endast i slutet av det yttersta metodanropet till den klassen. Detta inträffar också om klassen anges på nytt på grund av ett anrop till en metod i en annan klass. Invarianter kontrolleras inte för en objektfinaliserare och en IDisposable.Dispose implementering.
Användningsriktlinjer
Beställning av kontrakt
I följande tabell visas ordningen på de element som du bör använda när du skriver metodkontrakt.
If-then-throw statements |
Bakåtkompatibla offentliga förutsättningar |
---|---|
Requires | Alla offentliga förutsättningar. |
Ensures | Alla offentliga (normala) postconditions. |
EnsuresOnThrow | Alla offentliga exceptionella postconditions. |
Ensures | Alla privata/interna (normala) postconditions. |
EnsuresOnThrow | Alla privata/interna exceptionella postconditions. |
EndContractBlock | Om du använder if --then throw stilförutsättningar utan andra kontrakt kan du anropa för EndContractBlock att ange att alla tidigare om kontroller är förhandsvillkor. |
Renhet
Alla metoder som anropas inom ett kontrakt måste vara rena. Det vill säga att de inte får uppdatera något befintligt tillstånd. En ren metod tillåts att ändra objekt som har skapats efter posten i den rena metoden.
Verktyg för kodkontrakt förutsätter för närvarande att följande kodelement är rena:
Metoder som är markerade med PureAttribute.
Typer som är markerade med PureAttribute (attributet gäller för alla typer av metoder).
Egenskapen hämtar accessorer.
Operatorer (statiska metoder vars namn börjar med "op" och som har en eller två parametrar och en icke-void returtyp).
Alla metoder vars fullständigt kvalificerade namn börjar med "System.Diagnostics.Contracts.Contract", "System.String", "System.IO.Path" eller "System.Type".
Alla anropade ombud, förutsatt att själva ombudstypen tillskrivs PureAttribute. De delegerade typerna System.Predicate<T> och System.Comparison<T> betraktas som rena.
Synlighet
Alla medlemmar som nämns i ett kontrakt måste vara minst lika synliga som den metod där de visas. Ett privat fält kan till exempel inte nämnas i en förutsättning för en offentlig metod. klienter kan inte verifiera ett sådant kontrakt innan de anropar metoden. Men om fältet är markerat med ContractPublicPropertyNameAttributeär det undantaget från dessa regler.
Exempel
I följande exempel visas användningen av kodkontrakt.
#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