Bagikan melalui


Tutorial: Menjelajahi konstruktor utama

C# 12 memperkenalkan konstruktor utama, sintaks ringkas untuk mendeklarasikan konstruktor yang parameternya tersedia di mana saja dalam isi jenis.

Dalam tutorial ini, Anda akan mempelajari:

  • Kapan harus mendeklarasikan konstruktor utama pada jenis Anda
  • Cara memanggil konstruktor utama dari konstruktor lain
  • Cara menggunakan parameter konstruktor utama dalam anggota jenis
  • Tempat parameter konstruktor utama disimpan

Prasyarat

Anda perlu menyiapkan komputer Anda untuk menjalankan .NET 8 atau yang lebih baru, termasuk pengkompilasi C# 12 atau yang lebih baru. Pengkompilasi C# 12 tersedia dimulai dengan Visual Studio 2022 versi 17.7 atau .NET 8 SDK.

Konstruktor utama

Anda dapat menambahkan parameter ke struct deklarasi atau class untuk membuat konstruktor utama. Parameter konstruktor utama berada dalam cakupan di seluruh definisi kelas. Penting untuk melihat parameter konstruktor utama sebagai parameter meskipun berada dalam cakupan di seluruh definisi kelas. Beberapa aturan mengklarifikasi bahwa aturan tersebut adalah parameter:

  1. Parameter konstruktor utama mungkin tidak disimpan jika tidak diperlukan.
  2. Parameter konstruktor utama bukan anggota kelas. Misalnya, parameter konstruktor utama bernama param tidak dapat diakses sebagai this.param.
  3. Parameter konstruktor utama dapat ditetapkan.
  4. Parameter konstruktor utama tidak menjadi properti, kecuali dalam record jenis.

Aturan ini sama dengan parameter untuk metode apa pun, termasuk deklarasi konstruktor lainnya.

Penggunaan yang paling umum untuk parameter konstruktor utama adalah:

  1. Sebagai argumen untuk base() pemanggilan konstruktor.
  2. Untuk menginisialisasi bidang atau properti anggota.
  3. Mereferensikan parameter konstruktor dalam anggota instans.

Setiap konstruktor lain untuk kelas harus memanggil konstruktor utama, secara langsung atau tidak langsung, melalui this() pemanggilan konstruktor. Aturan tersebut memastikan bahwa parameter konstruktor utama ditetapkan di mana saja dalam isi jenis.

Menginisialisasi properti

Kode berikut menginisialisasi dua properti readonly yang dihitung dari parameter konstruktor utama:

public readonly struct Distance(double dx, double dy)
{
    public readonly double Magnitude { get; } = Math.Sqrt(dx * dx + dy * dy);
    public readonly double Direction { get; } = Math.Atan2(dy, dx);
}

Kode sebelumnya menunjukkan konstruktor utama yang digunakan untuk menginisialisasi properti readonly terhitung. Penginisialisasi bidang untuk Magnitude dan Direction menggunakan parameter konstruktor utama. Parameter konstruktor utama tidak digunakan di tempat lain dalam struktur. Struktur sebelumnya adalah seolah-olah Anda akan menulis kode berikut:

public readonly struct Distance
{
    public readonly double Magnitude { get; }

    public readonly double Direction { get; }

    public Distance(double dx, double dy)
    {
        Magnitude = Math.Sqrt(dx * dx + dy * dy);
        Direction = Math.Atan2(dy, dx);
    }
}

Fitur baru memudahkan penggunaan penginisialisasi bidang saat Anda memerlukan argumen untuk menginisialisasi bidang atau properti.

Membuat status dapat diubah

Contoh sebelumnya menggunakan parameter konstruktor utama untuk menginisialisasi properti readonly. Anda juga dapat menggunakan konstruktor utama saat properti tidak readonly. Pertimbangkan gambar berikut:

public struct Distance(double dx, double dy)
{
    public readonly double Magnitude => Math.Sqrt(dx * dx + dy * dy);
    public readonly double Direction => Math.Atan2(dy, dx);

    public void Translate(double deltaX, double deltaY)
    {
        dx += deltaX;
        dy += deltaY;
    }

    public Distance() : this(0,0) { }
}

Dalam contoh sebelumnya, metode mengubah Translatedx komponen dan dy . Itu mengharuskan Magnitude properti dan Direction dihitung saat diakses. Operator => menunjuk aksesor bertubuh get ekspresi, sedangkan = operator menunjuk penginisialisasi. Versi ini menambahkan konstruktor tanpa parameter ke struct. Konstruktor tanpa parameter harus memanggil konstruktor utama, sehingga semua parameter konstruktor utama diinisialisasi.

Dalam contoh sebelumnya, properti konstruktor utama diakses dalam metode . Oleh karena itu, pengkompilasi membuat bidang tersembunyi untuk mewakili setiap parameter. Kode berikut menunjukkan kira-kira apa yang dihasilkan kompilator. Nama bidang aktual adalah pengidentifikasi CIL yang valid, tetapi pengidentifikasi C# tidak valid.

public struct Distance
{
    private double __unspeakable_dx;
    private double __unspeakable_dy;

    public readonly double Magnitude => Math.Sqrt(__unspeakable_dx * __unspeakable_dx + __unspeakable_dy * __unspeakable_dy);
    public readonly double Direction => Math.Atan2(__unspeakable_dy, __unspeakable_dx);

    public void Translate(double deltaX, double deltaY)
    {
        __unspeakable_dx += deltaX;
        __unspeakable_dy += deltaY;
    }

    public Distance(double dx, double dy)
    {
        __unspeakable_dx = dx;
        __unspeakable_dy = dy;
    }
    public Distance() : this(0, 0) { }
}

Penting untuk dipahami bahwa contoh pertama tidak mengharuskan pengkompilasi membuat bidang untuk menyimpan nilai parameter konstruktor utama. Contoh kedua menggunakan parameter konstruktor utama di dalam metode, dan oleh karena itu memerlukan pengkompilasi untuk membuat penyimpanan untuk mereka. Pengkompilasi membuat penyimpanan untuk konstruktor utama apa pun hanya ketika parameter tersebut diakses dalam isi anggota jenis Anda. Jika tidak, parameter konstruktor utama tidak disimpan dalam objek.

Injeksi dependensi

Penggunaan umum lainnya untuk konstruktor utama adalah menentukan parameter untuk injeksi dependensi. Kode berikut membuat pengontrol sederhana yang memerlukan antarmuka layanan untuk penggunaannya:

public interface IService
{
    Distance GetDistance();
}

public class ExampleController(IService service) : ControllerBase
{
    [HttpGet]
    public ActionResult<Distance> Get()
    {
        return service.GetDistance();
    }
}

Konstruktor utama dengan jelas menunjukkan parameter yang diperlukan di kelas . Anda menggunakan parameter konstruktor utama seperti yang Anda lakukan pada variabel lain di kelas .

Menginisialisasi kelas dasar

Anda dapat memanggil konstruktor utama kelas dasar dari konstruktor utama kelas turunan. Ini adalah cara term mudah bagi Anda untuk menulis kelas turunan yang harus memanggil konstruktor utama di kelas dasar. Misalnya, pertimbangkan hierarki kelas yang mewakili jenis akun yang berbeda sebagai bank. Kelas dasar akan terlihat seperti kode berikut:

public class BankAccount(string accountID, string owner)
{
    public string AccountID { get; } = accountID;
    public string Owner { get; } = owner;

    public override string ToString() => $"Account ID: {AccountID}, Owner: {Owner}";
}

Semua rekening bank, terlepas dari jenisnya, memiliki properti untuk nomor rekening dan pemilik. Dalam aplikasi yang telah selesai, fungsionalitas umum lainnya akan ditambahkan ke kelas dasar.

Banyak jenis memerlukan validasi yang lebih spesifik pada parameter konstruktor. Misalnya, BankAccount memiliki persyaratan khusus untuk owner parameter dan accountID : owner tidak boleh null atau spasi kosong, dan accountID harus berupa string yang berisi 10 digit. Anda dapat menambahkan validasi ini saat menetapkan properti yang sesuai:

public class BankAccount(string accountID, string owner)
{
    public string AccountID { get; } = ValidAccountNumber(accountID) 
        ? accountID 
        : throw new ArgumentException("Invalid account number", nameof(accountID));

    public string Owner { get; } = string.IsNullOrWhiteSpace(owner) 
        ? throw new ArgumentException("Owner name cannot be empty", nameof(owner)) 
        : owner;

    public override string ToString() => $"Account ID: {AccountID}, Owner: {Owner}";

    public static bool ValidAccountNumber(string accountID) => 
    accountID?.Length == 10 && accountID.All(c => char.IsDigit(c));
}

Contoh sebelumnya menunjukkan bagaimana Anda dapat memvalidasi parameter konstruktor sebelum menetapkannya ke properti. Anda dapat menggunakan metode bawaan, seperti String.IsNullOrWhiteSpace(String), atau metode validasi Anda sendiri, seperti ValidAccountNumber. Dalam contoh sebelumnya, pengecualian apa pun dilemparkan dari konstruktor, ketika memanggil penginisialisasi. Jika parameter konstruktor tidak digunakan untuk menetapkan bidang, pengecualian apa pun akan dilemparkan saat parameter konstruktor pertama kali diakses.

Satu kelas turunan akan menunjukkan akun pemeriksaan:

public class CheckingAccount(string accountID, string owner, decimal overdraftLimit = 0) : BankAccount(accountID, owner)
{
    public decimal CurrentBalance { get; private set; } = 0;

    public void Deposit(decimal amount)
    {
        if (amount < 0)
        {
            throw new ArgumentOutOfRangeException(nameof(amount), "Deposit amount must be positive");
        }
        CurrentBalance += amount;
    }

    public void Withdrawal(decimal amount)
    {
        if (amount < 0)
        {
            throw new ArgumentOutOfRangeException(nameof(amount), "Withdrawal amount must be positive");
        }
        if (CurrentBalance - amount < -overdraftLimit)
        {
            throw new InvalidOperationException("Insufficient funds for withdrawal");
        }
        CurrentBalance -= amount;
    }
    
    public override string ToString() => $"Account ID: {AccountID}, Owner: {Owner}, Balance: {CurrentBalance}";
}

Kelas turunan CheckingAccount memiliki konstruktor utama yang mengambil semua parameter yang diperlukan di kelas dasar, dan parameter lain dengan nilai default. Konstruktor utama memanggil konstruktor dasar menggunakan sintaks.: BankAccount(accountID, owner) Ekspresi ini menentukan jenis untuk kelas dasar, dan argumen untuk konstruktor utama.

Kelas turunan Anda tidak diperlukan untuk menggunakan konstruktor utama. Anda dapat membuat konstruktor di kelas turunan yang memanggil konstruktor utama kelas dasar, seperti yang ditunjukkan dalam contoh berikut:

public class LineOfCreditAccount : BankAccount
{
    private readonly decimal _creditLimit;
    public LineOfCreditAccount(string accountID, string owner, decimal creditLimit) : base(accountID, owner)
    {
        _creditLimit = creditLimit;
    }
    public decimal CurrentBalance { get; private set; } = 0;

    public void Deposit(decimal amount)
    {
        if (amount < 0)
        {
            throw new ArgumentOutOfRangeException(nameof(amount), "Deposit amount must be positive");
        }
        CurrentBalance += amount;
    }

    public void Withdrawal(decimal amount)
    {
        if (amount < 0)
        {
            throw new ArgumentOutOfRangeException(nameof(amount), "Withdrawal amount must be positive");
        }
        if (CurrentBalance - amount < -_creditLimit)
        {
            throw new InvalidOperationException("Insufficient funds for withdrawal");
        }
        CurrentBalance -= amount;
    }

    public override string ToString() => $"{base.ToString()}, Balance: {CurrentBalance}";
}

Ada satu masalah potensial dengan hierarki kelas dan konstruktor utama: dimungkinkan untuk membuat beberapa salinan parameter konstruktor utama karena digunakan dalam kelas turunan dan dasar. Contoh kode berikut membuat dua salinan masing-masing bidang owner dan accountID :

public class SavingsAccount(string accountID, string owner, decimal interestRate) : BankAccount(accountID, owner)
{
    public SavingsAccount() : this("default", "default", 0.01m) { }
    public decimal CurrentBalance { get; private set; } = 0;

    public void Deposit(decimal amount)
    {
        if (amount < 0)
        {
            throw new ArgumentOutOfRangeException(nameof(amount), "Deposit amount must be positive");
        }
        CurrentBalance += amount;
    }

    public void Withdrawal(decimal amount)
    {
        if (amount < 0)
        {
            throw new ArgumentOutOfRangeException(nameof(amount), "Withdrawal amount must be positive");
        }
        if (CurrentBalance - amount < 0)
        {
            throw new InvalidOperationException("Insufficient funds for withdrawal");
        }
        CurrentBalance -= amount;
    }

    public void ApplyInterest()
    {
        CurrentBalance *= 1 + interestRate;
    }

    public override string ToString() => $"Account ID: {accountID}, Owner: {owner}, Balance: {CurrentBalance}";
}

Baris yang disorot menunjukkan bahwa ToString metode menggunakan parameter konstruktor utama (owner dan accountID) daripada properti kelas dasar (Owner dan AccountID). Hasilnya adalah bahwa kelas turunan, SavingsAccount membuat penyimpanan untuk salinan tersebut. Salinan di kelas turunan berbeda dari properti di kelas dasar. Jika properti kelas dasar dapat dimodifikasi, instans kelas turunan tidak akan melihat modifikasi tersebut. Kompilator mengeluarkan peringatan untuk parameter konstruktor utama yang digunakan dalam kelas turunan dan diteruskan ke konstruktor kelas dasar. Dalam hal ini, perbaikannya adalah menggunakan properti kelas dasar.

Ringkasan

Anda dapat menggunakan konstruktor utama yang paling sesuai dengan desain Anda. Untuk kelas dan struktur, parameter konstruktor utama adalah parameter untuk konstruktor yang harus dipanggil. Anda dapat menggunakannya untuk menginisialisasi properti. Anda dapat menginisialisasi bidang. Properti atau bidang tersebut dapat tidak dapat diubah, atau dapat diubah. Anda dapat menggunakannya dalam metode. Parameternya adalah parameter, dan Anda menggunakannya dengan cara apa yang paling sesuai dengan desain Anda. Anda dapat mempelajari selengkapnya tentang konstruktor utama di artikel panduan pemrograman C# tentang konstruktor instans dan spesifikasi konstruktor utama yang diusulkan.