Bagikan melalui


Catatan

Nota

Artikel ini adalah spesifikasi fitur. Spesifikasi berfungsi sebagai dokumen desain untuk fitur tersebut. Ini termasuk perubahan spesifikasi yang diusulkan, bersama dengan informasi yang diperlukan selama desain dan pengembangan fitur. Artikel ini diterbitkan sampai perubahan spesifikasi yang diusulkan diselesaikan dan dimasukkan dalam spesifikasi ECMA saat ini.

Mungkin ada beberapa perbedaan antara spesifikasi fitur dan implementasi yang selesai. Perbedaan tersebut tertuang dalam catatan rapat desain bahasa (LDM) terkait .

Anda dapat mempelajari lebih lanjut tentang proses untuk mengadopsi speklet fitur ke dalam standar bahasa C# dalam artikel tentang spesifikasi .

Masalah juara: https://github.com/dotnet/csharplang/issues/39

Proposal ini melacak spesifikasi untuk fitur rekaman C# 9, seperti yang disepakati oleh tim desain bahasa C#.

Sintaks untuk rekaman adalah sebagai berikut:

record_declaration
    : attributes? class_modifier* 'partial'? 'record' identifier type_parameter_list?
      parameter_list? record_base? type_parameter_constraints_clause* record_body
    ;

record_base
    : ':' class_type argument_list?
    | ':' interface_type_list
    | ':' class_type argument_list? ',' interface_type_list
    ;

record_body
    : '{' class_member_declaration* '}' ';'?
    | ';'
    ;

Jenis catatan adalah jenis referensi, mirip dengan deklarasi kelas. Ini adalah sebuah kesalahan jika catatan memberikan record_baseargument_list jika record_declaration tidak berisi parameter_list. Hanya boleh ada satu deklarasi jenis parsial untuk sebuah rekaman parsial yang dapat memberikan parameter_list.

Parameter rekaman tidak dapat menggunakan pengubah ref, out, atau this (tetapi in dan params diizinkan).

Warisan

Rekaman tidak dapat mewarisi dari kelas, kecuali kelas object, dan kelas tidak dapat mewarisi dari rekaman. Rekaman bisa mewarisi dari rekaman lain.

Anggota tipe catatan

Selain anggota yang dideklarasikan dalam isi rekaman, jenis rekaman memiliki anggota sintesis tambahan. Anggota akan disintesis kecuali jika ada anggota dengan "tanda tangan yang sesuai" yang dinyatakan dalam badan rekaman atau jika anggota non-virtual konkret yang dapat diakses dengan "tanda tangan yang sesuai" diwarisi. Anggota yang cocok mencegah pengkompilasi menghasilkan anggota tersebut, bukan anggota sintesis lainnya. Dua anggota dianggap cocok jika mereka memiliki tanda tangan yang sama atau akan dianggap "bersembunyi" dalam skenario warisan. Ini adalah kesalahan bagi anggota rekaman untuk dinamai "Clone". Ini adalah kesalahan untuk bidang instans rekaman untuk memiliki jenis penunjuk tingkat atas. Jenis pointer bersarang, seperti array pointer, diizinkan.

Anggota yang disintesis adalah sebagai berikut:

Anggota kesetaraan

Jika catatan berasal dari object, jenis catatan menyertakan properti baca-saja rekaan yang setara dengan properti yang dideklarasikan seperti berikut ini:

Type EqualityContract { get; }

Properti itu adalah private jika jenis catatan itu adalah sealed. Jika tidak, properti virtual dan protected. Properti dapat dinyatakan secara eksplisit. Ini adalah kesalahan jika deklarasi eksplisit tidak cocok dengan tanda tangan atau aksesibilitas yang diharapkan, atau jika deklarasi eksplisit tidak memungkinkan pengesampingannya dalam jenis turunan dan jenis catatan tidak sealed.

Jika jenis catatan berasal dari jenis catatan dasar Base, jenis catatan menyertakan properti baca-saja yang disintesis yang setara dengan properti yang dideklarasikan sebagai berikut:

protected override Type EqualityContract { get; }

Properti dapat dinyatakan secara eksplisit. Ini adalah kesalahan jika deklarasi eksplisit tidak cocok dengan tanda tangan atau aksesibilitas yang diharapkan, atau jika deklarasi eksplisit tidak memungkinkan pengesampingannya dalam jenis turunan dan jenis catatan tidak sealed. Ini adalah kesalahan jika properti yang disintesis atau dinyatakan secara eksplisit tidak menimpa properti dengan tanda tangan ini dalam tipe rekaman Base (misalnya jika properti hilang di Base, atau disegel, atau tidak virtual, dll.). Properti yang disintesis mengembalikan typeof(R) di mana R adalah jenis rekaman.

Jenis catatan mengimplementasikan System.IEquatable<R> dan menyertakan kelebihan Equals(R? other) yang diketik kuat yang disintesis di mana R adalah jenis catatan. Metodenya adalah public, dan metodenya adalah virtual kecuali jenis catatannya adalah sealed. Metode ini dapat dinyatakan secara eksplisit. Ini adalah kesalahan jika deklarasi eksplisit tidak cocok dengan tanda tangan atau aksesibilitas yang diharapkan, atau deklarasi eksplisit tidak dapat di-override pada jenis turunan dan tipe rekaman bukan sealed.

Jika Equals(R? other) ditentukan pengguna (tidak disintesis) tetapi GetHashCode tidak, peringatan akan dihasilkan.

public virtual bool Equals(R? other);

Equals(R?) yang disintesis mengembalikan true jika dan hanya jika semua yang berikut adalah true.

  • other tidak null, dan
  • Untuk setiap bidang instans fieldN dalam jenis catatan yang tidak diwarisi, nilai System.Collections.Generic.EqualityComparer<TN>.Default.Equals(fieldN, other.fieldN) di mana TN adalah jenis bidang, dan
  • Jika ada tipe catatan dasar, nilai base.Equals(other) (panggilan non-virtual ke public virtual bool Equals(Base? other)); jika tidak, nilai EqualityContract == other.EqualityContract.

Jenis rekaman mencakup operator == dan != yang disintesis yang setara dengan operator yang dinyatakan sebagai berikut:

public static bool operator==(R? left, R? right)
    => (object)left == right || (left?.Equals(right) ?? false);
public static bool operator!=(R? left, R? right)
    => !(left == right);

Metode Equals yang dipanggil oleh operator == adalah metode Equals(R? other) yang ditentukan di atas. Operator != mendelegasikan ke operator ==. Ini adalah kesalahan jika operator dinyatakan secara eksplisit.

Jika tipe rekaman diturunkan dari tipe rekaman dasar Base, tipe rekaman mencakup penimpaan sintetik yang setara dengan metode yang dideklarasikan sebagai berikut:

public sealed override bool Equals(Base? other);

Ini adalah kesalahan jika penggantian dinyatakan secara eksplisit. Ini adalah kesalahan jika metode tidak mengambil alih metode dengan tanda tangan yang sama dalam jenis rekaman Base (misalnya, jika metode hilang di Base, atau disegel, atau bukan virtual, dll.). Penggantian yang disintesis mengembalikan Equals((object?)other).

Jenis catatan menyertakan penimpaan yang disintesis yang setara dengan metode yang dinyatakan sebagai berikut:

public override bool Equals(object? obj);

Ini adalah kesalahan jika penggantian dinyatakan secara eksplisit. Ini adalah kesalahan jika metode tidak mengambil alih object.Equals(object? obj) (misalnya, karena bayangan dalam jenis dasar menengah, dll.). Penggantian yang disintesis mengembalikan Equals(other as R), tempat R adalah jenis rekaman.

Jenis catatan menyertakan penimpaan yang disintesis yang setara dengan metode yang dinyatakan sebagai berikut:

public override int GetHashCode();

Metode ini dapat dinyatakan secara eksplisit. Merupakan kesalahan jika deklarasi eksplisit tidak memungkinkan penimpaan pada tipe turunan dan tipe rekaman tersebut bukan sealed. Ini adalah kesalahan jika metode yang disintesis atau dinyatakan secara eksplisit tidak mengganti object.GetHashCode() (misalnya, karena terdapat penimpaan pada tipe dasar menengah, dll.).

Peringatan dilaporkan jika salah satu dari Equals(R?) dan GetHashCode() dinyatakan secara eksplisit sedangkan metode lainnya tidak eksplisit.

Penggantian GetHashCode() yang disintesis mengembalikan hasil int dengan menggabungkan nilai-nilai berikut:

  • Untuk setiap bidang instans fieldN dalam jenis catatan yang tidak diwarisi, nilai System.Collections.Generic.EqualityComparer<TN>.Default.GetHashCode(fieldN) di mana TN adalah jenis bidang, dan
  • Jika ada jenis catatan dasar, nilai base.GetHashCode(); jika tidak, nilai System.Collections.Generic.EqualityComparer<System.Type>.Default.GetHashCode(EqualityContract).

Misalnya, pertimbangkan jenis catatan berikut:

record R1(T1 P1);
record R2(T1 P1, T2 P2) : R1(P1);
record R3(T1 P1, T2 P2, T3 P3) : R2(P1, P2);

Untuk jenis catatan tersebut, anggota pembanding yang disintesis akan menjadi sesuatu seperti:

class R1 : IEquatable<R1>
{
    public T1 P1 { get; init; }
    protected virtual Type EqualityContract => typeof(R1);
    public override bool Equals(object? obj) => Equals(obj as R1);
    public virtual bool Equals(R1? other)
    {
        return !(other is null) &&
            EqualityContract == other.EqualityContract &&
            EqualityComparer<T1>.Default.Equals(P1, other.P1);
    }
    public static bool operator==(R1? left, R1? right)
        => (object)left == right || (left?.Equals(right) ?? false);
    public static bool operator!=(R1? left, R1? right)
        => !(left == right);
    public override int GetHashCode()
    {
        return HashCode.Combine(EqualityComparer<Type>.Default.GetHashCode(EqualityContract),
            EqualityComparer<T1>.Default.GetHashCode(P1));
    }
}

class R2 : R1, IEquatable<R2>
{
    public T2 P2 { get; init; }
    protected override Type EqualityContract => typeof(R2);
    public override bool Equals(object? obj) => Equals(obj as R2);
    public sealed override bool Equals(R1? other) => Equals((object?)other);
    public virtual bool Equals(R2? other)
    {
        return base.Equals((R1?)other) &&
            EqualityComparer<T2>.Default.Equals(P2, other.P2);
    }
    public static bool operator==(R2? left, R2? right)
        => (object)left == right || (left?.Equals(right) ?? false);
    public static bool operator!=(R2? left, R2? right)
        => !(left == right);
    public override int GetHashCode()
    {
        return HashCode.Combine(base.GetHashCode(),
            EqualityComparer<T2>.Default.GetHashCode(P2));
    }
}

class R3 : R2, IEquatable<R3>
{
    public T3 P3 { get; init; }
    protected override Type EqualityContract => typeof(R3);
    public override bool Equals(object? obj) => Equals(obj as R3);
    public sealed override bool Equals(R2? other) => Equals((object?)other);
    public virtual bool Equals(R3? other)
    {
        return base.Equals((R2?)other) &&
            EqualityComparer<T3>.Default.Equals(P3, other.P3);
    }
    public static bool operator==(R3? left, R3? right)
        => (object)left == right || (left?.Equals(right) ?? false);
    public static bool operator!=(R3? left, R3? right)
        => !(left == right);
    public override int GetHashCode()
    {
        return HashCode.Combine(base.GetHashCode(),
            EqualityComparer<T3>.Default.GetHashCode(P3));
    }
}

Menyalin dan Mengkloning anggota

Jenis catatan berisi dua elemen salinan.

  • Konstruktor mengambil argumen tunggal dari jenis rekaman. Ini disebut sebagai "konstruktor salin".
  • Metode "klon" instans tanpa parameter yang disintesis dengan nama yang dicadangkan oleh kompilator.

Tujuan dari konstruktor salin adalah untuk menyalin status dari parameter ke instans baru yang dibuat. Konstruktor ini tidak menjalankan penginisialisasi bidang/properti instans apa pun yang ada dalam deklarasi rekaman. Jika konstruktor tidak dideklarasikan secara eksplisit, konstruktor akan disintesis oleh kompilator. Jika catatan disegel, konstruktor akan bersifat pribadi, jika tidak, catatan akan dilindungi. Konstruktor salinan yang dinyatakan secara eksplisit harus publik atau dilindungi, kecuali rekaman disegel. Hal pertama yang harus dilakukan konstruktor, adalah memanggil konstruktor salinan dasar, atau konstruktor objek tanpa parameter jika rekaman mewarisi dari objek. Kesalahan dilaporkan jika konstruktor salinan yang ditentukan pengguna menggunakan inisialisasi konstruktor implisit atau eksplisit yang tidak memenuhi persyaratan ini. Setelah konstruktor salinan dasar dipanggil, konstruktor salinan yang disintesis menyalin nilai untuk semua bidang instans yang dideklarasikan baik secara implisit maupun eksplisit dalam jenis catatan. Satu-satunya kehadiran konstruktor salinan, baik eksplisit atau implisit, tidak mencegah penambahan otomatis konstruktor instans default.

Jika metode "klon" virtual ada dalam rekaman dasar, metode "klon" yang dihasilkan secara otomatis mengambil alihnya dan jenis pengembalian dari metode tersebut adalah jenis yang saat ini memuat. Kesalahan dihasilkan jika metode klon rekaman dasar disegel. Jika metode 'clone' virtual tidak ada dalam record dasar, maka jenis pengembalian metode clone adalah tipe yang mengandung dan metode tersebut adalah virtual, kecuali record tersebut disegel atau abstrak. Jika rekaman yang berisi abstrak, metode kloning yang disintesis juga abstrak. Jika metode "kloning" tidak abstrak, metode tersebut mengembalikan hasil panggilan ke konstruktor salinan.

Mencetak anggota: Metode PrintMembers dan ToString

Jika rekaman berasal dari object, rekaman menyertakan metode yang disintesis yang setara dengan metode yang dinyatakan sebagai berikut:

bool PrintMembers(System.Text.StringBuilder builder);

Jika jenis catatan adalah private, maka metodenya adalah sealed. Jika tidak, metode ini virtual dan protected.

Metode:

  1. memanggil metode System.Runtime.CompilerServices.RuntimeHelpers.EnsureSufficientExecutionStack() jika metodenya ada dan catatan memiliki anggota yang dapat dicetak.
  2. untuk setiap anggota rekaman yang dapat dicetak (kolom publik non-statis dan anggota properti yang dapat dibaca), menambahkan nama anggota tersebut diikuti oleh " = " diikuti oleh nilai anggota yang dipisahkan oleh ", ".
  3. mengembalikan benar jika rekaman memiliki anggota yang dapat dicetak.

Untuk anggota yang memiliki jenis nilai, kami akan mengonversi nilainya menjadi representasi string menggunakan metode paling efisien yang tersedia untuk platform target. Saat ini berarti memanggil ToString sebelum meneruskan ke StringBuilder.Append.

Jika jenis catatan berasal dari catatan dasar Base, catatan tersebut menyertakan penimpaan yang disintesis setara dengan metode yang diterangkan sebagai berikut:

protected override bool PrintMembers(StringBuilder builder);

Jika rekaman tidak memiliki anggota yang dapat dicetak, metode ini akan memanggil metode dasar PrintMembers dengan satu argumen (parameternya builder) dan mengembalikan hasilnya.

Jika tidak, metode :

  1. memanggil metode PrintMembers dasar dengan satu argumen (parameter builder),
  2. jika metode PrintMembers mengembalikan true, tambahkan ", " ke objek builder,
  3. untuk setiap anggota yang dapat dicetak dari rekaman, menyisipkan nama anggota tersebut diikuti dengan " = " kemudian diikuti oleh nilai anggota: this.member (atau this.member.ToString() untuk jenis nilai), dan dipisahkan dengan ", ",
  4. mengembalikan benar.

Metode PrintMembers dapat dinyatakan secara eksplisit. Ini adalah kesalahan jika deklarasi eksplisit tidak cocok dengan tanda tangan atau aksesibilitas yang diharapkan, atau jika deklarasi eksplisit tidak memungkinkan pengesampingannya dalam jenis turunan dan jenis catatan tidak sealed.

Catatan ini mencakup metode yang disintesis yang setara dengan metode yang dinyatakan sebagai berikut:

public override string ToString();

Metode ini dapat dinyatakan secara eksplisit. Ini adalah kesalahan jika deklarasi eksplisit tidak cocok dengan tanda tangan atau aksesibilitas yang diharapkan, atau jika deklarasi eksplisit tidak memungkinkan pengesampingannya dalam jenis turunan dan jenis catatan tidak sealed. Ini adalah kesalahan jika metode yang disintesis atau dinyatakan secara eksplisit tidak mengganti object.ToString() (misalnya, karena terdapat penimpaan pada tipe dasar menengah, dll.).

Metode yang disintesis:

  1. membuat instans StringBuilder,
  2. menambahkan nama rekaman ke penyusun, diikuti dengan " { ",
  3. memanggil metode PrintMembers catatan dengan memberikan builder, diikuti dengan " " jika mengembalikan true,
  4. menambahkan "}",
  5. mengembalikan konten pembangun berupa builder.ToString().

Misalnya, pertimbangkan jenis catatan berikut:

record R1(T1 P1);
record R2(T1 P1, T2 P2, T3 P3) : R1(P1);

Untuk jenis rekaman tersebut, anggota pencetakan yang tersintesis akan menjadi sesuatu seperti:

class R1 : IEquatable<R1>
{
    public T1 P1 { get; init; }
    
    protected virtual bool PrintMembers(StringBuilder builder)
    {
        builder.Append(nameof(P1));
        builder.Append(" = ");
        builder.Append(this.P1); // or builder.Append(this.P1.ToString()); if T1 is a value type
        
        return true;
    }
    
    public override string ToString()
    {
        var builder = new StringBuilder();
        builder.Append(nameof(R1));
        builder.Append(" { ");

        if (PrintMembers(builder))
            builder.Append(" ");

        builder.Append("}");
        return builder.ToString();
    }
}

class R2 : R1, IEquatable<R2>
{
    public T2 P2 { get; init; }
    public T3 P3 { get; init; }
    
    protected override bool PrintMembers(StringBuilder builder)
    {
        if (base.PrintMembers(builder))
            builder.Append(", ");
            
        builder.Append(nameof(P2));
        builder.Append(" = ");
        builder.Append(this.P2); // or builder.Append(this.P2); if T2 is a value type
        
        builder.Append(", ");
        
        builder.Append(nameof(P3));
        builder.Append(" = ");
        builder.Append(this.P3); // or builder.Append(this.P3); if T3 is a value type
        
        return true;
    }
    
    public override string ToString()
    {
        var builder = new StringBuilder();
        builder.Append(nameof(R2));
        builder.Append(" { ");

        if (PrintMembers(builder))
            builder.Append(" ");

        builder.Append("}");
        return builder.ToString();
    }
}

Anggota rekam posisional

Selain anggota yang telah disebutkan, rekaman dengan daftar parameter ("rekaman posisi") menyusun anggota tambahan dengan kondisi yang sama seperti yang disebutkan sebelumnya.

Konstruktor Utama

Jenis catatan memiliki konstruktor publik yang tanda tangannya sesuai dengan parameter nilai deklarasi jenis. Ini disebut konstruktor utama untuk tipe tersebut, dan menyebabkan konstruktor kelas bawaan yang secara implisit dinyatakan, jika ada, dihilangkan. Adalah sebuah kesalahan jika terdapat konstruktor utama dan konstruktor lain dengan signature yang sama yang sudah ada dalam kelas.

Pada saat runtime, konstruktor utama

  1. menjalankan penginisialisasi instans yang terdapat di dalam badan kelas

  2. memanggil konstruktor kelas dasar dengan argumen yang disediakan dalam klausa record_base, jika ada

Jika sebuah rekaman memiliki konstruktor utama, setiap konstruktor yang ditentukan pengguna kecuali "konstruktor penyalinan" harus memiliki penginisialisasi konstruktor this yang eksplisit.

Parameter konstruktor utama serta anggota catatan berada dalam lingkup dalam argument_list klausa record_base dan dalam penginisialisasi bidang atau properti instans. Anggota instans akan menjadi kesalahan pada lokasi ini (mirip dengan bagaimana anggota instans berada dalam cakupan dalam penginisialisasi konstruktor reguler hari ini, tetapi menjadi kesalahan jika digunakan), akan tetapi, parameter konstruktor utama akan berada dalam cakupan, dapat digunakan dan akan membayangi anggota. Anggota statis juga akan dapat digunakan, mirip dengan cara kerja panggilan dasar dan penginisialisasi di konstruktor biasa saat ini.

Peringatan dihasilkan jika parameter konstruktor utama tidak dibaca.

Variabel ekspresi yang dideklarasikan dalam argument_list berada dalam cakupan dalam argument_list. Aturan bayangan yang sama seperti dalam daftar argumen penginisialisasi konstruktor reguler berlaku.

Properti

Untuk setiap parameter rekaman dari deklarasi jenis rekaman, ada anggota properti publik terkait yang nama dan jenisnya diambil dari deklarasi parameter nilai.

Sebagai catatan:

  • Properti get publik dan init otomatis dibuat (lihat spesifikasi aksesor init terpisah). Properti abstract yang diwariskan dengan jenis yang cocok diganti. Ini adalah kesalahan jika properti yang diwariskan tidak memiliki public yang dapat diganti get dan aksesor init. Ini adalah kesalahan jika properti yang diwariskan disembunyikan.
    Atribut otomatis diinisialisasi ke nilai parameter konstruktor primer yang sesuai. Atribut dapat diterapkan ke properti otomatis yang disintesis dan bidang dukungannya dengan menggunakan target property: atau field: untuk atribut yang diterapkan secara sintetis ke parameter rekaman yang sesuai.

Mendekonstruksi

Rekam jejak posisional dengan setidaknya satu parameter menyusun metode instans publik yang mengembalikan void bernama Deconstruct, dengan deklarasi parameter keluar untuk setiap parameter dari deklarasi konstruktor utama. Setiap parameter metode Deconstruct memiliki jenis yang sama dengan parameter yang sesuai dari deklarasi konstruktor utama. Isi dari metode tersebut menetapkan setiap parameter dari metode Deconstruct dengan nilai properti instans yang memiliki nama sama. Metode ini dapat dinyatakan secara eksplisit. Ini adalah kesalahan jika deklarasi eksplisit tidak cocok dengan tanda tangan atau aksesibilitas yang diharapkan, atau statis.

Contoh berikut menunjukkan rekaman posisi R dengan metode Deconstruct yang disintesis kompilatornya, bersama dengan penggunaannya:

public record R(int P1, string P2 = "xyz")
{
    public void Deconstruct(out int P1, out string P2)
    {
        P1 = this.P1;
        P2 = this.P2;
    }
}

class Program
{
    static void Main()
    {
        R r = new R(12);
        (int p1, string p2) = r;
        Console.WriteLine($"p1: {p1}, p2: {p2}");
    }
}

ekspresi with

Ekspresi with adalah ekspresi baru menggunakan sintaks berikut.

with_expression
    : switch_expression
    | switch_expression 'with' '{' member_initializer_list? '}'
    ;

member_initializer_list
    : member_initializer (',' member_initializer)*
    ;

member_initializer
    : identifier '=' expression
    ;

Ekspresi with tidak diizinkan sebagai pernyataan.

Ekspresi with memungkinkan "mutasi yang tidak merusak", yang dirancang untuk menghasilkan salinan dari ekspresi penerima dengan modifikasi dalam penetapan di member_initializer_list.

Ekspresi with yang valid memiliki penerima dengan jenis non-void. Jenis penerima harus berupa rekaman.

Di sisi kanan dari ekspresi with terdapat member_initializer_list dengan serangkaian penugasan ke pengidentifikasi , yang harus menjadi instance field atau properti yang dapat diakses dari tipe penerima.

Pertama, metode "kloning" milik penerima (ditentukan di atas) dipanggil dan hasilnya dikonversi ke tipe penerima. Kemudian, setiap member_initializer diproses dengan cara yang sama dengan penugasan ke bidang atau akses properti dari hasil konversi. Penugasan diproses dalam urutan leksikal.