Kontrak kode (.NET Framework)
Catatan
Artikel ini khusus untuk .NET Framework. Ini tidak berlaku untuk implementasi .NET yang lebih baru, termasuk .NET 6 dan versi yang lebih baru.
Kontrak kode menyediakan cara untuk menentukan pra-kondisi, pasca-kondisi, dan invarian objek dalam kode .NET Framework. Pra-kondisi adalah persyaratan yang harus dipenuhi saat memasukkan metode atau properti. Pasca-kondisi menjelaskan ekspektasi pada saat metode atau kode properti keluar. Invarian objek menjelaskan status yang diharapkan untuk kelas yang berada dalam status baik.
Catatan
Kontrak kode tidak didukung di .NET 5+ (termasuk versi .NET Core). Sebagai gantinya, pertimbangkan untuk menggunakan jenis referensi Nullable.
Kontrak kode mencakup kelas untuk menandai kode Anda, penganalisis statik untuk analisis waktu kompilasi, dan penganalisis runtime. Kelas untuk kontrak kode dapat ditemukan di namespace layanan System.Diagnostics.Contracts.
Manfaat kontrak kode meliputi:
Pengujian yang ditingkatkan: Kontrak kode menyediakan verifikasi kontrak statik, pemeriksaan runtime, dan pembuatan dokumentasi.
Alat pengujian otomatis: Anda dapat menggunakan kontrak kode untuk menghasilkan pengujian unit yang lebih berarti dengan memfilter argumen pengujian yang tidak berarti yang tidak memenuhi prasyarat.
Verifikasi statik: Pemeriksa statik dapat memutuskan apakah ada pelanggaran kontrak tanpa menjalankan program. Ini memeriksa kontrak implisit, seperti dereferensi null dan ikatan array, dan kontrak eksplisit.
Dokumentasi referensi: Pembuat dokumentasi menambahkan file dokumentasi XML yang ada dengan informasi kontrak. Ada juga lembar gaya yang dapat digunakan dengan Sandcastle sehingga halaman dokumentasi yang dihasilkan memiliki bagian kontrak.
Semua bahasa .NET Framework dapat langsung memanfaatkan kontrak; Anda tidak perlu menulis pengurai atau pengompilasi khusus. Add-in Visual Studio memungkinkan Anda menentukan tingkat analisis kontrak kode yang akan dilakukan. Penganalisis dapat mengonfirmasi bahwa kontrak terbentuk dengan baik (pemeriksaan jenis dan resolusi nama) dan dapat menghasilkan bentuk kontrak yang dikompilasi dalam format bahasa perantara umum (CIL). Penulisan kontrak di Visual Studio memungkinkan Anda memanfaatkan IntelliSense standar yang disediakan oleh alat.
Sebagian besar metode di kelas kontrak dikompilasi secara bersyarat; artinya, pengompilasi mengeluarkan panggilan ke metode ini hanya saat Anda menentukan simbol khusus, CONTRACTS_FULL, dengan menggunakan arahan #define
. CONTRACTS_FULL memungkinkan Anda menulis kontrak dalam kode Anda tanpa menggunakan arahan #ifdef
; Anda dapat membuat build yang berbeda, beberapa dengan kontrak, dan beberapa tanpa kontrak.
Untuk alat dan petunjuk mendetail tentang penggunaan kontrak kode, lihat Kontrak Kode di situs marketplace Visual Studio.
Prasyarat
Anda dapat mengekspresikan pra-kondisi dengan menggunakan metode Contract.Requires. Pra-kondisi menentukan status saat metode dipanggil. Ini umumnya digunakan untuk menentukan nilai parameter yang valid. Semua anggota yang disebutkan dalam pra-kondisi harus setidaknya dapat diakses sebagai metode itu sendiri; jika tidak, pra-kondisi mungkin tidak dipahami oleh semua pemanggil metode. Kondisi ini tidak boleh memiliki efek samping. Perilaku run-time pra-kondisi yang gagal ditentukan oleh penganalisis runtime.
Misalnya, pra-kondisi berikut menyatakan bahwa parameter x
harus non-null.
Contract.Requires(x != null);
Jika kode Anda menampilkan pengecualian tertentu saat kegagalan pra-kondisi, Anda dapat menggunakan muatan berlebih generik Requires sebagai berikut.
Contract.Requires<ArgumentNullException>(x != null, "x");
Warisan Memerlukan Pernyataan
Sebagian besar kode berisi beberapa validasi parameter dalam bentuk kode if
-then
-throw
. Alat kontrak mengenali pernyataan ini sebagai pra-kondisi dalam kasus berikut:
Pernyataan ini muncul sebelum pernyataan lain dalam metode.
Seluruh kumpulan pernyataan tersebut diikuti dengan panggilan metode Contract eksplisit, seperti panggilan ke metode Requires, Ensures, EnsuresOnThrow, atau EndContractBlock.
Saat pernyataan if
-then
-throw
muncul dalam formulir ini, alat ini mengenalinya sebagai pernyataan requires
lama. Jika tidak ada kontrak lain yang mengikuti urutan if
-then
-throw
, akhiri kode dengan metode Contract.EndContractBlock.
if (x == null) throw new ...
Contract.EndContractBlock(); // All previous "if" checks are preconditions
Perhatikan bahwa kondisi dalam pengujian sebelumnya adalah pra-kondisi yang ditiadakan. (Pra-kondisi aktualnya adalah x != null
.) Pra-kondisi yang ditiadakan sangat dibatasi: Ini harus ditulis seperti yang ditunjukkan dalam contoh sebelumnya; artinya, pra-kondisi tidak boleh berisi klausul else
, dan isi klausul then
harus berupa satu pernyataan throw
. Pengujian if
tunduk pada aturan kemurnian dan visibilitas (lihat Panduan Penggunaan), tetapi ekspresi throw
hanya tunduk pada aturan kemurnian. Namun, jenis pengecualian yang ditampilkan harus terlihat seperti metode saat kontrak terjadi.
Pasca-kondisi
Pasca-kondisi adalah kontrak untuk status metode saat metode diakhiri. Pasca-kondisi diperiksa tepat sebelum keluar dari metode. Perilaku run-time pasca-kondisi yang gagal ditentukan oleh penganalisis runtime.
Tidak seperti pra-kondisi, pasca-kondisi dapat mereferensikan anggota dengan visibilitas yang lebih sedikit. Klien mungkin tidak dapat memahami atau menggunakan beberapa informasi yang dinyatakan oleh pasca-kondisi menggunakan status privat, tetapi ini tidak memengaruhi kemampuan klien untuk menggunakan metode dengan benar.
Pasca-kondisi Standar
Anda dapat mengekspresikan pasca-kondisi standar dengan menggunakan metode Ensures. Pasca-kondisi mengekspresikan kondisi yang harus berupa true
saat metode dihentikan secara normal.
Contract.Ensures(this.F > 0);
Pasca-kondisi Luar Biasa
Pasca-kondisi luar biasa adalah pasca-kondisi yang harus berupa true
saat pengecualian tertentu ditampilkan oleh metode. Anda dapat menentukan pasca-kondisi ini dengan menggunakan metode Contract.EnsuresOnThrow, seperti yang ditunjukkan dalam contoh berikut.
Contract.EnsuresOnThrow<T>(this.F > 0);
Argumen adalah kondisi yang harus berupa true
setiap kali pengecualian yang merupakan subjenis T
ditampilkan.
Ada beberapa jenis pengecualian yang sulit digunakan dalam pasca-kondisi luar biasa. Misalnya, penggunaan jenis Exception untuk T
memerlukan metode untuk menjamin kondisi terlepas dari jenis pengecualian yang ditampilkan, meskipun berupa luapan tumpukan atau pengecualian lain yang tidak dapat dikontrol. Anda harus menggunakan pasca-kondisi luar biasa hanya untuk pengecualian tertentu yang mungkin muncul saat anggota dipanggil, misalnya, saat InvalidTimeZoneException muncul untuk panggilan metode TimeZoneInfo.
Pasca-kondisi Khusus
Metode berikut hanya dapat digunakan dalam pasca-kondisi:
Anda dapat merujuk ke nilai pengembalian metode dalam pasca-kondisi dengan menggunakan ekspresi
Contract.Result<T>()
, dengan keteranganT
diganti dengan jenis pengembalian metode. Jika pengompilasi tidak dapat menyimpulkan jenisnya, Anda harus menyediakannya secara eksplisit. Misalnya, pengompilasi C# tidak dapat menyimpulkan jenis untuk metode yang tidak mengambil argumen apa pun, sehingga memerlukan secara eksplisit berikut:Contract.Ensures(0 <Contract.Result<int>())
Metode dengan jenis pengembalianvoid
tidak dapat merujuk keContract.Result<T>()
dalam pasca-kondisinya.Nilai pra-status dalam pasca-kondisi secara eksplisit mengacu pada nilai ekspresi di awal metode atau properti. Ini menggunakan ekspresi
Contract.OldValue<T>(e)
, di manaT
adalah jenise
. Anda dapat menghilangkan argumen jenis generik setiap kali pengompilasi dapat menyimpulkan jenisnya. (Misalnya, pengompilasi C# selalu menyimpulkan jenis karena mengambil argumen.) Ada beberapa batasan pada yang bisa terjadi dane
konteks saat ekspresi lama mungkin muncul. Ekspresi lama tidak boleh berisi ekspresi lama lainnya. Yang paling penting, ekspresi lama harus merujuk ke nilai yang ada dalam status pra-kondisi metode. Dengan kata lain, ini harus berupa ekspresi yang dapat dievaluasi selama pra-kondisi metode adalahtrue
. Berikut beberapa contoh aturan tersebut.Nilai harus ada dalam status pra-kondisi metode. Untuk mereferensikan bidang pada objek, pra-kondisi harus menjamin bahwa objek selalu non-null.
Anda tidak dapat merujuk ke nilai pengembalian metode dalam ekspresi lama:
Contract.OldValue(Contract.Result<int>() + x) // ERROR
Anda tidak dapat merujuk ke parameter
out
dalam ekspresi lama.Ekspresi lama tidak dapat bergantung pada variabel terikat pembilang jika rentang pembilang bergantung pada nilai pengembalian metode:
Contract.ForAll(0, Contract.Result<int>(), i => Contract.OldValue(xs[i]) > 3); // ERROR
Ekspresi lama tidak dapat merujuk ke parameter delegasi anonim dalam panggilan ForAll atau Exists kecuali digunakan sebagai pengindeks atau argumen ke panggilan metode:
Contract.ForAll(0, xs.Length, i => Contract.OldValue(xs[i]) > 3); // OK Contract.ForAll(0, xs.Length, i => Contract.OldValue(i) > 3); // ERROR
Ekspresi lama tidak dapat terjadi dalam isi delegasi anonim jika nilai ekspresi lama bergantung pada salah satu parameter delegasi anonim, kecuali delegasi anonim berupa argumen untuk metode ForAll atau Exists:
Method(... (T t) => Contract.OldValue(... t ...) ...); // ERROR
Out
parameter menimbulkan masalah karena kontrak muncul sebelum isi metode, dan sebagian besar pengompilasi tidak mengizinkan referensi ke parameterout
dalam pasca-kondisi. Untuk mengatasi masalah ini, kelas Contract menyediakan metode ValueAtReturn, yang memungkinkan pasca-kondisi berdasarkan parameterout
.public void OutParam(out int x) { Contract.Ensures(Contract.ValueAtReturn(out x) == 3); x = 3; }
Seperti halnya metode OldValue, Anda dapat menghilangkan parameter jenis generik setiap kali pengompilasi dapat menyimpulkan jenisnya. Penulis ulang kontrak menggantikan panggilan metode dengan nilai parameter
out
. Metode ValueAtReturn ini mungkin hanya muncul dalam pasca-kondisi. Argumen untuk metode harus berupa parameterout
atau bidang parameterout
struktur. Yang terakhir juga berguna saat merujuk ke bidang dalam pasca-kondisi konstruktor struktur.Catatan
Saat ini, alat analisis kontrak kode tidak memeriksa apakah parameter
out
diinisialisasi dengan benar dan mengabaikan sebutannya dalam pasca-kondisi. Oleh karena itu, dalam contoh sebelumnya, jika baris setelah kontrak telah menggunakan nilaix
alih-alih menetapkan bilangan bulat ke dalamnya, pengompilasi tidak akan mengeluarkan kesalahan yang benar. Namun, pada build tempat simbol pra-pemroses CONTRACTS_FULL tidak ditentukan (seperti build rilis), pengompilasi akan mengeluarkan kesalahan.
Invarian
Invarian objek adalah kondisi yang harus dipenuhi untuk setiap instans kelas setiap kali objek tersebut dilihat oleh klien. Invarian objek ini mengekspresikan kondisi di mana objek dianggap benar.
Metode invarian diidentifikasi dengan ditandai dengan atribut ContractInvariantMethodAttribute. Metode invarian tidak boleh berisi kode kecuali untuk urutan panggilan ke metode Invariant, yang masing-masing menentukan invarian individu, seperti yang ditunjukkan dalam contoh berikut.
[ContractInvariantMethod]
protected void ObjectInvariant ()
{
Contract.Invariant(this.y >= 0);
Contract.Invariant(this.x > this.y);
...
}
Invarian ditentukan secara kondisional oleh simbol pra-pemroses CONTRACTS_FULL. Selama pemeriksaan run-time, invarian diperiksa di akhir setiap metode publik. Jika invarian menyebutkan metode publik di kelas yang sama, pemeriksaan invarian yang biasanya terjadi di akhir metode publik tersebut akan dinonaktifkan. Sebaliknya, pemeriksaan hanya terjadi di akhir panggilan metode terluar ke kelas tersebut. Ini juga terjadi jika kelas dimasukkan kembali karena panggilan ke metode di kelas lain. Invarian tidak diperiksa untuk pengakhir objek dan implementasi IDisposable.Dispose.
Panduan Penggunaan
Pengurutan Kontrak
Tabel berikut menunjukkan urutan elemen yang harus Anda gunakan saat menulis kontrak metode.
If-then-throw statements |
Pra-kondisi publik dengan kompatibilitas mundur |
---|---|
Requires | Semua pra-kondisi publik. |
Ensures | Semua pasca-kondisi publik (normal). |
EnsuresOnThrow | Semua pasca-kondisi luar biasa publik. |
Ensures | Semua pasca-kondisi privat/internal (normal). |
EnsuresOnThrow | Semua pasca-kondisi luar biasa privat/internal. |
EndContractBlock | Jika menggunakan pra-kondisi gaya if -then -throw tanpa kontrak lain, lakukan panggilan ke EndContractBlock untuk menunjukkan bahwa semua yang sebelumnya jika pemeriksaan adalah pra-kondisi. |
Kemurnian
Semua metode yang dipanggil dalam kontrak harus murni; artinya, metode tidak boleh memperbarui status yang sudah ada sebelumnya. Metode murni diizinkan untuk memodifikasi objek yang telah dibuat setelah entri ke metode murni.
Alat kontrak kode saat ini mengasumsikan bahwa elemen kode berikut murni:
Metode yang ditandai dengan PureAttribute.
Jenis yang ditandai dengan PureAttribute (atribut berlaku untuk semua metode jenis).
Properti mendapatkan pengakses.
Operator (metode statik yang namanya dimulai dengan "op", dan yang memiliki satu atau dua parameter dan jenis pengembalian non-void).
Metode apa pun yang namanya sepenuhnya memenuhi syarat dimulai dengan "System.Diagnostics.Contracts.Contract", "System.String", "System.IO.Path", atau "System.Type".
Setiap delegasi yang dipanggil, asalkan jenis delegasi itu sendiri dikaitkan dengan PureAttribute. Jenis System.Predicate<T> delegasi dan System.Comparison<T> dianggap murni.
Visibilitas
Semua anggota yang disebutkan dalam kontrak harus setidaknya terlihat seperti metode di mana mereka muncul. Misalnya, bidang privat tidak dapat disebutkan dalam pra-kondisi untuk metode publik; klien tidak dapat memvalidasi kontrak tersebut sebelum metode dipanggil. Namun, jika bidang ditandai dengan ContractPublicPropertyNameAttribute, bidang tersebut dikecualikan dari aturan ini.
Contoh
Contoh berikut menunjukkan penggunaan kontrak kode.
#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