Bagikan melalui


Kontrak kode (.NET Framework)

Nota

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 prasyarat, pascakondisi, dan invarian objek dalam kode .NET Framework. Prasyarat adalah persyaratan yang harus dipenuhi saat memasukkan metode atau properti. Pasca-kondisi menjelaskan harapan saat eksekusi metode atau kode properti selesai. Invarian objek menjelaskan status yang diharapkan untuk kelas yang dalam keadaan baik.

Nota

Kontrak kode tidak didukung di .NET 5+ (termasuk versi .NET Core). Pertimbangkan untuk menggunakan jenis referensi yang bisa kosong sebagai gantinya.

Kontrak kode mencakup kelas untuk menandai kode Anda, penganalisis statis untuk analisis waktu kompilasi, dan penganalisis runtime. Kelas untuk kontrak kode dapat ditemukan di System.Diagnostics.Contracts namespace.

Manfaat kontrak kode mencakup hal-hal berikut:

  • Pengujian yang ditingkatkan: Kontrak kode menyediakan verifikasi kontrak statis, pemeriksaan runtime, dan pembuatan dokumentasi.

  • Alat pengujian otomatis: Anda dapat menggunakan kontrak kode untuk menghasilkan pengujian unit yang lebih bermakna dengan memfilter argumen pengujian yang tidak berarti yang tidak memenuhi prasyarat.

  • Verifikasi statis: Pemeriksa statis dapat memutuskan apakah ada pelanggaran kontrak tanpa menjalankan program. Ini memeriksa kontrak implisit, seperti dereferensi null dan batas array, dan kontrak eksplisit.

  • Dokumentasi referensi: Generator dokumentasi menambah 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 segera memanfaatkan kontrak; Anda tidak perlu menulis pengurai atau pengkompilasi 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). Kontrak penulisan di Visual Studio memungkinkan Anda memanfaatkan IntelliSense standar yang disediakan oleh alat ini.

Sebagian besar metode di kelas kontrak dikompilasi secara kondisional; artinya, pengkompilasi memancarkan panggilan ke metode ini hanya ketika Anda menentukan simbol khusus, CONTRACTS_FULL, dengan menggunakan direktif #define . CONTRACTS_FULL memungkinkan Anda menulis kontrak dalam kode Anda tanpa menggunakan #ifdef arahan; Anda dapat menghasilkan build yang berbeda, beberapa dengan kontrak, dan beberapa tanpa.

Untuk alat dan instruksi terperinci untuk menggunakan kontrak kode, lihat Kontrak Kode di situs marketplace Visual Studio.

Prasyarat

Anda dapat mengekspresikan prasyarat dengan menggunakan metode .Contract.Requires Prasyarat menentukan status ketika metode dipanggil. Mereka umumnya digunakan untuk menentukan nilai parameter yang valid. Semua anggota yang disebutkan dalam prasyarat harus setidaknya sama dapat diaksesnya dengan metode itu sendiri; jika tidak, prasyarat tersebut mungkin tidak akan dipahami oleh semua pemanggil metode. Kondisi harus tidak memiliki efek samping. Perilaku pada waktu proses dari prasyarat yang gagal ditentukan oleh analisis waktu proses.

Misalnya, prasyarat berikut mengekspresikan bahwa parameter x harus non-null.

Contract.Requires(x != null);

Jika kode Anda harus melempar pengecualian tertentu ketika kegagalan prasyarat terjadi, Anda dapat menggunakan overload generik Requires sebagai berikut.

Contract.Requires<ArgumentNullException>(x != null, "x");

Warisan Memerlukan Pernyataan

Sebagian besar kode berisi beberapa validasi parameter dalam bentuk if-then-throw kode. Alat kontrak mengenali pernyataan ini sebagai prasyarat dalam kasus berikut:

Ketika if-then-throw pernyataan muncul dalam formulir ini, alat mengenalinya sebagai pernyataan warisan.requires 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 prasyarat yang dinegasikan. (Prasyarat aktual adalah x != null.) Prasyarat yang dinegasikan sangat dibatasi: Prasyarat harus ditulis seperti yang ditunjukkan dalam contoh sebelumnya; artinya, tidak boleh berisi else klausul, dan isi then klausul harus berupa satu throw pernyataan. Pengujian if tunduk pada aturan kemurnian dan visibilitas (lihat Panduan Penggunaan), tetapi throw ekspresi hanya tunduk pada aturan kemurnian. Namun, jenis pengecualian yang dilemparkan harus sama terlihatnya dengan metode di mana kontrak terjadi.

Pascakondisi

Pascakondisi adalah kontrak untuk keadaan suatu metode saat berakhir. Pascakondisi diperiksa tepat sebelum keluar dari metode. Perilaku runtime pascakondisi yang gagal ditentukan oleh penganalisis runtime.

Tidak seperti prasyarat, pascakondisi dapat mengacu pada anggota dengan visibilitas yang lebih rendah. Klien mungkin tidak dapat memahami atau menggunakan beberapa informasi yang dinyatakan oleh pascakondisi menggunakan status privat, tetapi ini tidak memengaruhi kemampuan klien untuk menggunakan metode dengan benar.

Kondisi Pasca Standar

Anda dapat mengekspresikan pascakondisi standar dengan menggunakan metode Ensures. Pascakondisi menyatakan kondisi yang harus dipenuhi true setelah penghentian metode secara normal.

Contract.Ensures(this.F > 0);

Pascakondisi Luar Biasa

Pascakondisi luar biasa adalah pascakondisi yang seharusnya true ketika pengecualian tertentu dilemparkan oleh metode . Anda dapat menentukan pascakondisi ini dengan menggunakan Contract.EnsuresOnThrow metode , seperti yang ditunjukkan contoh berikut.

Contract.EnsuresOnThrow<T>(this.F > 0);

Argumen adalah kondisi yang harus true setiap kali pengecualian yang merupakan subjenis T dilemparkan.

Ada beberapa jenis pengecualian yang sulit digunakan dalam pascakondisi yang luar biasa. Misalnya, menggunakan jenis Exception untuk T memerlukan metode untuk menjamin kondisi terlepas dari jenis pengecualian yang dilemparkan, bahkan jika itu adalah stack overflow atau pengecualian lain yang tidak dapat dikontrol. Anda harus menggunakan pascakondisi luar biasa hanya untuk pengecualian tertentu yang mungkin dilemparkan ketika anggota dipanggil, misalnya, ketika InvalidTimeZoneException dilemparkan untuk TimeZoneInfo panggilan metode.

Pascakondisi Khusus

Metode berikut hanya dapat digunakan dalam pascakondisi:

  • Anda dapat merujuk ke nilai pengembalian metode dalam pascakondisi dengan menggunakan ekspresi Contract.Result<T>(), di mana T digantikan oleh jenis pengembalian metode. Ketika pengkompilasi tidak dapat menyimpulkan jenisnya, Anda harus secara eksplisit menyediakannya. Misalnya, pengkompilasi C# tidak dapat menyimpulkan jenis untuk metode yang tidak mengambil argumen apa pun, sehingga memerlukan pascakondisi berikut: Contract.Ensures(0 <Contract.Result<int>()) Metode dengan jenis void pengembalian tidak dapat merujuk Contract.Result<T>() dalam pascakondisi mereka.

  • Nilai prestat dalam pascakondisi mengacu pada nilai ekspresi di awal metode atau properti. Ini menggunakan ekspresi Contract.OldValue<T>(e), di mana T adalah jenis e. Anda dapat menghilangkan argumen jenis generik setiap kali pengkompilasi dapat menyimpulkan jenisnya. (Misalnya, pengkompilasi C# selalu menyimpulkan jenis karena membutuhkan argumen.) Ada beberapa batasan tentang apa yang dapat terjadi di e dan konteks di mana ekspresi lama mungkin muncul. Ekspresi lama tidak boleh berisi ekspresi lama lainnya. Yang terpenting, ekspresi lama harus merujuk ke nilai yang ada dalam status prasyarat metode. Dengan kata lain, itu harus menjadi ekspresi yang dapat dievaluasi selama prasyarat metode adalah true. Berikut adalah beberapa contoh aturan tersebut.

    • Nilai harus ada dalam status prasyarat metode. Untuk mereferensikan bidang pada objek, prasyarat 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 out parameter dalam ekspresi lama.

    • Ekspresi lama tidak dapat bergantung pada variabel terikat kuantifier jika rentang kuantifier bergantung pada nilai pengembalian metode:

      Contract.ForAll(0, Contract.Result<int>(), i => Contract.OldValue(xs[i]) > 3); // ERROR
      
    • Ekspresi yang sudah usang tidak dapat mengacu pada parameter delegasi anonim dalam panggilan ForAll atau Exists kecuali jika digunakan sebagai pengindeks atau argumen untuk 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 muncul dalam tubuh delegasi anonim jika nilai ekspresi lama bergantung pada salah satu parameter delegasi anonim, kecuali delegasi anonim adalah argumen untuk metode ForAll atau Exists.

      Method(... (T t) => Contract.OldValue(... t ...) ...); // ERROR
      
    • Out parameter menyajikan masalah karena kontrak muncul sebelum isi metode, dan sebagian besar kompilator tidak mengizinkan referensi ke out parameter dalam pascakondisi. Untuk mengatasi masalah ini, Contract kelas menyediakan ValueAtReturn metode , yang memungkinkan pascakondisi berdasarkan out parameter.

      public void OutParam(out int x)
      {
          Contract.Ensures(Contract.ValueAtReturn(out x) == 3);
          x = 3;
      }
      

      Seperti halnya OldValue metode , Anda dapat menghilangkan parameter jenis generik setiap kali pengkompilasi dapat menyimpulkan jenisnya. Penulis ulang kontrak menggantikan panggilan metode dengan nilai parameter out. Metode ValueAtReturn ini mungkin hanya muncul dalam pascakondisi. Argumen ke metode harus berupa out parameter atau bidang parameter struktur out . Yang terakhir juga berguna ketika mengacu pada elemen dalam kondisi akhir dari konstruktor struktur.

      Nota

      Saat ini, perangkat analisis kontrak kode tidak memeriksa apakah parameter out diinisialisasi dengan benar dan mengabaikan penyebutannya dalam pascakondisi. Oleh karena itu, dalam contoh sebelumnya, jika baris setelah kontrak telah menggunakan nilai x daripada menetapkan bilangan bulat padanya, pengkompilasi tidak akan mengeluarkan kesalahan yang benar. Namun, pada build di mana simbol pra-pemroses CONTRACTS_FULL tidak didefinisikan (seperti build rilis), kompiler akan mengeluarkan error.

Ketetapan

Invarian objek adalah kondisi yang harus benar untuk setiap instans kelas saat objek tersebut diakses oleh klien. Mereka mengekspresikan kondisi di mana objek dianggap benar.

Metode invariant diidentifikasi dengan ditandai dengan ContractInvariantMethodAttribute atribut . Metode invarian tidak boleh berisi kode kecuali untuk urutan panggilan ke Invariant metode , 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 didefinisikan secara kondisional oleh simbol preprosesor CONTRACTS_FULL. Selama pemeriksaan runtime, invarian diperiksa di akhir setiap metode publik. Jika invarian menyebutkan metode publik di kelas yang sama, pemeriksaan invarian yang biasanya akan terjadi di akhir metode publik tersebut dinonaktifkan. Sebaliknya, pemeriksaan hanya terjadi di akhir panggilan metode terluar ke kelas tersebut. Ini juga terjadi jika kelas diakses ulang karena adanya pemanggilan metode di kelas lain. Invarian tidak diperiksa untuk finalizer objek dan implementasi IDisposable.Dispose.

Panduan Penggunaan

Pemesanan Berdasarkan Kontrak

Tabel berikut menunjukkan urutan elemen yang harus Anda gunakan saat menulis kontrak metode.

If-then-throw statements Prasyarat publik yang kompatibel secara mundur
Requires Semua prasyarat publik.
Ensures Semua pascakondisi publik (normal).
EnsuresOnThrow Semua kondisi pasca khusus publik.
Ensures Semua pascakondisi (normal) yang bersifat privat/internal.
EnsuresOnThrow Semua pascakondisi privat/internal yang luar biasa.
EndContractBlock Jika menggunakan if-then-throw prasyarat gaya tanpa kontrak lain, lakukan pemanggilan ke EndContractBlock untuk menunjukkan bahwa semua pengecekan 'jika' sebelumnya adalah prasyarat.

Kemurnian

Semua metode yang dipanggil dalam kontrak harus murni; artinya, mereka tidak boleh memperbarui status yang sudah ada sebelumnya. Metode murni diizinkan untuk memodifikasi objek yang telah dibuat setelah entri ke dalam 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 aksesor.

  • Operator (metode statis yang namanya dimulai dengan "op", dan yang memiliki satu atau dua parameter dan jenis pengembalian yang bukan 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 tipe delegasi itu sendiri memiliki atribut PureAttribute. Jenis-jenis delegasi System.Predicate<T> dan System.Comparison<T> dianggap murni.

Keterlihatan

Semua anggota yang disebutkan dalam kontrak harus memiliki tingkat visibilitas yang minimal setara dengan metode tempat mereka disebutkan. Misalnya, bidang privat tidak dapat disebutkan dalam prasyarat untuk metode publik; klien tidak dapat memvalidasi kontrak tersebut sebelum mereka memanggil metode . 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