記錄 (C# 參考)

record修飾符內建了封裝資料的功能。 和record classrecord語法定義了參考類型。 語法定義 record struct值型態

C# 語言參考資料記錄了 C# 語言最新版本。 同時也包含即將推出語言版本公開預覽功能的初步文件。

文件中標示了語言最近三個版本或目前公開預覽版中首次引入的任何功能。

小提示

欲查詢某功能何時首次在 C# 中引入,請參閱 C# 語言版本歷史的條目。

當您在記錄上宣告主要建構函式時,編譯器會產生主要建構函式參數的公用屬性。 記錄的主要建構參數是 位置參數。 編譯器會建立「位置屬性」,以鏡像主要建構函式或位置參數。 編譯器不會針對沒有 record 修飾元的型別來合成主要建構函式參數的屬性。

下列兩個範例示範 record (或 record class) 參考類型:

public record Person(string FirstName, string LastName);
public record Person
{
    public required string FirstName { get; init; }
    public required string LastName { get; init; }
};

下列兩個範例示範 record struct 值類型:

public readonly record struct Point(double X, double Y, double Z);
public record struct Point
{
    public double X { get; init; }
    public double Y { get; init; }
    public double Z { get; init; }
}

您也可以建立具有可變屬性和欄位的記錄:

public record Person
{
    public required string FirstName { get; set; }
    public required string LastName { get; set; }
};

record struct 也可以是可變的 (位置記錄結構與沒有位置參數的記錄結構):

public record struct DataMeasurement(DateTime TakenAt, double Measurement);
public record struct Point
{
    public double X { get; set; }
    public double Y { get; set; }
    public double Z { get; set; }
}

雖然記錄可以是可變的,但它們主要是用來支援不可變的資料模式。 記錄類型提供下列功能:

上面的範例顯示了參考類型的記錄以及為值類型的記錄之間的一些區別:

  • recordrecord class 宣告了參考類型。 class 關鍵字是選用性的,但它可為讀者增加清楚性。 record struct 宣告了值類型。
  • 位置屬性在 record class 中是readonly record struct。 它們在 中是record struct

本文的其餘部分會討論 record classrecord struct 類型。 每個小節都會詳細說明這些差異。 在 a record class 和 之間 record struct 做決定,就像在 a class 和 a struct之間做決定一樣。 「記錄」一詞描述適用於所有紀錄類型的行為。 record structrecord class 是分別用來描述僅適用於結構 (struct) 或類別 (class) 類型的行為。

屬性和欄位定義的位置語法

使用位置參數來宣告記錄的屬性,或初始化屬性或欄位值。 下列範例會建立具有兩個位置屬性的記錄:

public record Person(string FirstName, string LastName);

public static void Main()
{
    Person person = new("Nancy", "Davolio");
    Console.WriteLine(person);
    // output: Person { FirstName = Nancy, LastName = Davolio }
}

當您使用位置語法來進行屬性定義時,編譯器會建立:

  • 記錄宣告中提供之每個位置參數的公用自動實作屬性。
    • 對於 record 類型和 readonly record struct 類型:init-only 屬性。
    • 對於 record struct 類型:read-write 屬性。
  • 一個其參數符合記錄宣告上的位置參數的主要建構函式。
  • 一個將每個欄位設為其預設值的無參數建構函式 (為 record struct 類型)。
  • 一個含 Deconstruct 參數的 out 方法 (為記錄宣告中所提供的每一個位置參數)。 方法會解構使用位置語法所定義的屬性;它會忽略使用標準屬性語法所定義的屬性。

您可能會想要將屬性新增至由編譯器從記錄定義中建立的任何這些元素。 您可以將目標 新增至您套用到位置記錄屬性 (property) 的任何屬性 (attribute)。 下列範例會將 System.Text.Json.Serialization.JsonPropertyNameAttribute 套用至 Person 記錄的每一個屬性 (property)。 property: 目標指出屬性 (attribute) 會套用至編譯器產生的屬性 (property)。 其他值是 field: (將 attribute 套用至欄位),以及 param: (將 attribute 套用至參數)。

/// <summary>
/// Person record type
/// </summary>
/// <param name="FirstName">First Name</param>
/// <param name="LastName">Last Name</param>
/// <remarks>
/// The person type is a positional record containing the
/// properties for the first and last name. Those properties
/// map to the JSON elements "firstName" and "lastName" when
/// serialized or deserialized.
/// </remarks>
public record Person([property: JsonPropertyName("firstName")] string FirstName,
    [property: JsonPropertyName("lastName")] string LastName);

上面的範例也示範如何為記錄建立 XML 文件註解。 您可以加入 <param> 標記,以加入主要建構函式參數的文件註解。

如果產生的自動實現屬性定義不是你想要的,請自行定義同名屬性或欄位。 例如,您可能想要變更可存取性或可變性,或提供 getset 存取子的其中一個實作。 如果您在原始碼中宣告成員,則必須從紀錄的定位參數初始化它。 如果您的屬性是自動實作的屬性,您必須初始化 屬性。 如果您在來源中新增一個支援欄位,則必須將該支援欄位初始化。 產生的解構函式會使用您的屬性或欄位定義。 例如,下列範例會將位置記錄的 FirstNameLastName 屬性宣告為 public,但會將 Id 位置參數限制為 internal。 您可以針對記錄和記錄結構類型使用此語法。

public record Person(string FirstName, string LastName, string Id)
{
    internal string Id { get; init; } = Id;
}

public static void Main()
{
    Person person = new("Nancy", "Davolio", "12345");
    Console.WriteLine(person.FirstName); //output: Nancy

}

如果您想要建立欄位而非屬性,請將位置參數指派給欄位,如下列範例所示:

public record Person(string FirstName, string LastName, string Id)
{
    internal readonly string Id = Id; // this.Id set to parameter Id
}

public static void Main()
{
    Person person = new("Nancy", "Davolio", "12345");
    Console.WriteLine(person.FirstName); //output: Nancy

}

記錄類型不需要宣告任何的位置屬性。 您可以宣告不含任何位置屬性的記錄,也可以宣告其他欄位和屬性,如下列範例所示:

public record Person(string FirstName, string LastName)
{
    public string[] PhoneNumbers { get; init; } = [];
};

編譯器從位置參數產生的屬性是 public。 您可以在任何明確宣告的屬性上指定存取修飾詞。

不變性

位置記錄類別位置只讀記錄結構 宣告僅限 init 屬性。 位置記錄結構會宣告 read-write 屬性。 您可以覆寫這些預設值中的任何一個,如上節所示。

不可變性在你需要資料中心型別以執行緒安全,或依賴雜湊碼在雜湊表中保持不變時可能很有用。 然而,不可變性並非適用於所有資料情境。 例如,Entity Framework Core 不支援以不可變實體類型更新。

僅初始化的性質,無論是由位置參數(record classreadonly record struct)或透過指定 init 存取者所創造,都具有 淺層不變性。 在初始化之後,您無法變更 value-type 屬性的值或 reference-type 屬性的參考。 不過,reference-type 屬性所參考的資料可以變更。 下列範例顯示 reference-type 不可變屬性的內容 (在本例中為陣列) 是可變的:

public record Person(string FirstName, string LastName, string[] PhoneNumbers);

public static void Main()
{
    Person person = new("Nancy", "Davolio", new string[1] { "555-1234" });
    Console.WriteLine(person.PhoneNumbers[0]); // output: 555-1234

    person.PhoneNumbers[0] = "555-6789";
    Console.WriteLine(person.PhoneNumbers[0]); // output: 555-6789
}

記錄類型特有的功能是由編譯器合成方法所實作,而且這些方法都不會藉由修改物件狀態來危害不變性。 除非指定,否則會針對 recordrecord structreadonly record struct 宣告來產生合成方法。

實值相等

如果您未覆寫或取代相等方法,則您所宣告的類型決定了相等的定義方式:

  • 對於 class 類型,如果兩個物件參考記憶體中的相同物件,則這兩個物件相等。
  • 對於 struct 類型,如果兩個物件屬於相同類型並儲存相同的值,則這兩個物件相等。
  • 對於具有 record 修飾元的型別 (record classrecord structreadonly record struct),如果兩個物件屬於相同型別並儲存相同的值,則這兩個物件相等。

record struct 的相等定義與 struct 的相等定義相同。 不同之處在於,對於 struct,實作是在 ValueType.Equals(Object) 中並依賴於反映。 對於記錄,實作是編譯器合成,並使用宣告的資料成員。

某些資料模式需要參考相等。 例如,Entity Framework Core 依賴參考等值,確保它只使用一個實體類型,該實體概念上屬於一個實體。 因此,記錄和記錄結構不適合作為 Entity Framework Core 中的實體類型使用。

下列範例說明記錄類型的值相等:

public record Person(string FirstName, string LastName, string[] PhoneNumbers);

public static void Main()
{
    var phoneNumbers = new string[2];
    Person person1 = new("Nancy", "Davolio", phoneNumbers);
    Person person2 = new("Nancy", "Davolio", phoneNumbers);
    Console.WriteLine(person1 == person2); // output: True

    person1.PhoneNumbers[0] = "555-1234";
    Console.WriteLine(person1 == person2); // output: True

    Console.WriteLine(ReferenceEquals(person1, person2)); // output: False
}

為了實作值相等,編譯器會合成幾種方法,包括:

  • Object.Equals(Object) 的覆寫。 如果你明確宣告覆蓋,那就是錯誤。

    當兩個參數都是非 Null 時,此方法會用作 Object.Equals(Object, Object) 靜態方法的基礎。

  • virtual 是記錄類型的 sealed (或 Equals(R? other)) R。 這個方法會實作 IEquatable<T>。 你可以明確宣告這個方法。

  • 如果記錄類型衍生自基底記錄類型 Base,則 Equals(Base? other)。 如果你明確宣告覆蓋,那就是錯誤。 如果您提供自己的 Equals(R? other) 實作,請同時提供 GetHashCode 的實作。

  • Object.GetHashCode() 的覆寫。 你可以明確宣告這個方法。

  • 運算子 ==運算子 !=的覆寫。 如果你明確宣告運算元,那就是錯誤。

  • 如果記錄類型衍生自基底記錄類型,則 protected override Type EqualityContract { get; };。 你可以明確申報這個屬性。 如需詳細資訊,請參閱繼承階層中的相等

當某個記錄型別的方法與該合成方法的簽名相符,且該方法被明確宣告時,編譯器就不會自動合成該方法。

非破壞性變異

如果你需要複製一個經過修改的實例,可以使用 with 表達式來達成 非破壞性變異。 表達 with 式會建立一個新的記錄實例,該實例是現有實例的複製品,但修改了指定的屬性和欄位。 使用 物件初始化器 語法來指定要更改的值,如下範例所示:

public record Person(string FirstName, string LastName)
{
    public string[] PhoneNumbers { get; init; }
}

public static void Main()
{
    Person person1 = new("Nancy", "Davolio") { PhoneNumbers = new string[1] };
    Console.WriteLine(person1);
    // output: Person { FirstName = Nancy, LastName = Davolio, PhoneNumbers = System.String[] }

    Person person2 = person1 with { FirstName = "John" };
    Console.WriteLine(person2);
    // output: Person { FirstName = John, LastName = Davolio, PhoneNumbers = System.String[] }
    Console.WriteLine(person1 == person2); // output: False

    person2 = person1 with { PhoneNumbers = new string[1] };
    Console.WriteLine(person2);
    // output: Person { FirstName = Nancy, LastName = Davolio, PhoneNumbers = System.String[] }
    Console.WriteLine(person1 == person2); // output: False

    person2 = person1 with { };
    Console.WriteLine(person1 == person2); // output: True
}

with 運算式可以使用標準屬性語法來設定位置屬性或所建立的屬性。 明確宣告的屬性必須具有要在 init 運算式中變更的 setwith 存取子。

表達式的 with 結果是 淺層複製。 對於參考屬性,該表達式僅複製對某個實例的引用。 原始記錄和複本最後都會具有同一個執行個體的參考。

為了實作 record class 類型的這項功能,編譯器會合成一個 clone 方法和一個 copy 建構函式。 虛擬複製方法會傳回 copy 建構函式初始化的新記錄。 當你使用 with 表達式時,編譯器會建立程式碼呼叫複製方法,然後設定該 with 表達式包含的屬性。

這很重要

當記錄缺乏主要建構子或任何使用者定義建構子時,編譯器也會合成一個公開無參數建構子。 此無參數建構器會將所有欄位初始化為預設值。 若無此綜合建構子,則無法使用公開建構器。

如果你需要不同的複製行為,可以在 . 中寫自己的複製建構器。record class 如果你有,編譯器並不會合成一個。 如果記錄是 private,則建立你的構造器sealed。 否則,就去做 protected。 編譯器不會為 record struct 類型合成 copy 建構函式。 您可以撰寫一個,但編譯器不會為 with 運算式產生對它的呼叫。 record struct 的值會在指派時複製。

您無法覆寫 clone 方法,也不能在任何記錄類型中建立一個名為 Clone 的成員。 clone 方法的實際名稱是編譯器產生的。

這很重要

在上述範例中,所有屬性都是獨立的。 不會從其他屬性值計算任何屬性。 表達 with 式會先複製現有的記錄實例,然後修改該 with 表達式中包含的屬性或欄位。 類型中的 record 計算屬性應該在存取時計算,而不是在建立執行個體時初始化。 否則,屬性可能會根據原始實例傳回計算值,而不是修改後的複本。

您可以計算存取值,以確保計算屬性的正確性,如下列宣告所示:

public record Point(int X, int Y)
{
    public double Distance => Math.Sqrt(X * X + Y * Y);
}

上述記錄類型會 Distance 計算存取時間,如下列範例所示:

Point p1 = new Point(3, 4);
Console.WriteLine($"Original point: {p1}");
p1 = p1 with { Y = 8 };
Console.WriteLine($"Modified point: {p1}");
// Output:
// Original point: Point { X = 3, Y = 4, Distance = 5 }
// Modified point: Point { X = 3, Y = 8, Distance = 8.54400374531753 }

與此方法形成對比,以下 Distance 宣告中該屬性是在新實例初始化時計算並快取的:

public record PointInit(int X, int Y)
{
    public double Distance { get; } = Math.Sqrt(X * X + Y * Y);
}

因為 是在初始化過程中計算的,所以Distance在運算式變更副本中的值with之前Y,會計算並快取值。 結果是距離不正確:

PointInit pt1 = new PointInit(3, 4);
Console.WriteLine($"Original point: {pt1}");
pt1 = pt1 with { Y = 8 };
Console.WriteLine($"Incorrect Modified point: {pt1}");
// Output:
// Original point: PointInit { X = 3, Y = 4, Distance = 5 }
// Modified point: PointInit { X = 3, Y = 8, Distance = 5 }

Distance每次存取的計算成本並不高。 不過,某些計算屬性可能需要存取更多資料或更廣泛的計算。 在這些情況下,請使用 class 類型,並在其中一個元件變更值時計算快取值,而不是記錄。

用於顯示的內建格式設定

記錄類型有一個編譯器產生的 ToString 方法,可顯示公開屬性和欄位的名稱和值。 此 ToString 方法回傳格式為以下字串:

<記錄類型名稱> { <屬性名稱> = <value>, <property name> = <value>, ...}

<value> 列印的字串是 ToString() 為屬性類型所傳回的字串。 在下列範例中,ChildNamesSystem.Array,其中 ToString 會傳回 System.String[]

Person { FirstName = Nancy, LastName = Davolio, ChildNames = System.String[] }

為了實作此功能,在 record class 類型中,編譯器會合成了一個虛擬 PrintMembers 方法和一個 ToString 覆寫。 在 record struct 類型中,此成員為 privateToString 覆寫會建立一個 StringBuilder 物件,其類型名稱後面接著一個左括弧。 它會呼叫 PrintMembers 以新增屬性名稱和值,然後加入右括弧。 以下範例顯示的程式碼類似於合成覆寫所包含的內容:

public override string ToString()
{
    StringBuilder stringBuilder = new StringBuilder();
    stringBuilder.Append("Teacher"); // type name
    stringBuilder.Append(" { ");
    if (PrintMembers(stringBuilder))
    {
        stringBuilder.Append(" ");
    }
    stringBuilder.Append("}");
    return stringBuilder.ToString();
}

您可以提供自己的 PrintMembersToString 覆寫的實作。 本文後面的衍生記錄中的 PrintMembers 格式設定一節提供了範例。 您的 ToString 實作可能包含 sealed 修飾詞,以防止編譯程式針對任何衍生記錄合成 ToString 實作。 您可以在整個 record 型別階層中建立一致的字串表示。 (衍生的記錄仍具有針對所有衍生屬性產生的 PrintMembers 方法。)

繼承

本節僅適用於使用 record class 類型。

記錄可以繼承自另一個記錄。 不過,記錄無法繼承自類別,而類別無法繼承自記錄。

衍生記錄類型中的位置參數

衍生的記錄會為基底記錄主要建構函式中所有參數宣告位置參數。 基底記錄會宣告並初始化這些屬性。 衍生的記錄不會隱藏它們,但只會為其基底記錄中未宣告的參數建立並初始化屬性。

下列範例說明使用位置屬性語法的繼承:

public abstract record Person(string FirstName, string LastName);
public record Teacher(string FirstName, string LastName, int Grade)
    : Person(FirstName, LastName);
public static void Main()
{
    Person teacher = new Teacher("Nancy", "Davolio", 3);
    Console.WriteLine(teacher);
    // output: Teacher { FirstName = Nancy, LastName = Davolio, Grade = 3 }
}

繼承階層中的相等

本節適用於 record class 類型,但不適用於 record struct 類型。 若要讓兩個記錄變數相等,執行階段類型必須相等。 包含變數的類型可能不同。 下列程式碼範例說明了繼承的相等比較:

public abstract record Person(string FirstName, string LastName);
public record Teacher(string FirstName, string LastName, int Grade)
    : Person(FirstName, LastName);
public record Student(string FirstName, string LastName, int Grade)
    : Person(FirstName, LastName);
public static void Main()
{
    Person teacher = new Teacher("Nancy", "Davolio", 3);
    Person student = new Student("Nancy", "Davolio", 3);
    Console.WriteLine(teacher == student); // output: False

    Student student2 = new Student("Nancy", "Davolio", 3);
    Console.WriteLine(student2 == student); // output: True
}

在此範例中,所有變數都會宣告為 Person,即使在執行個體是 StudentTeacher 的衍生類型時也一樣。 執行個體具有相同的屬性和相同的屬性值。 但是 student == teacher 會傳回 False (雖然兩個都是 Person 類型的變數),而 student == student2 會傳回 True (雖然一個是 Person 變數,一個是 Student 變數)。 相等測試取決於實際物件的執行階段類型,而不是變數的宣告類型。

為了實作值相等,編譯器會合成一個 EqualityContract 屬性,該屬性會傳回一個符合記錄類型的 Type 物件。 EqualityContract 會啟用相等方法,以在它們檢查是否相等時比較物件的執行階段類型。 如果記錄的基底類型為 object,則此屬性為 virtual。 如果基底類型是另一個記錄類型,則此屬性為覆寫。 如果記錄類型為 sealed,則此屬性實際上是 sealed,因為類型為 sealed

當程式碼比較衍生型別的兩個執行個體時,合成的相等方法會檢查基底和衍生型別的所有資料成員是否相等。 合成的 GetHashCode 方法會使用基底型別和衍生記錄型別中宣告之所有資料成員中的 GetHashCode 方法。 的數據成員包含所有宣告的 record 欄位,以及任何自動實作屬性的編譯程式合成支援字段。

衍生記錄中的 with 運算式

with 運算式的結果與運算式的運算元具有相同的執行階段類型。 會複製執行階段類型的所有屬性,但您只能設定編譯時間類型的屬性,如下列範例所示:

public record Point(int X, int Y)
{
    public int Zbase { get; set; }
};
public record NamedPoint(string Name, int X, int Y) : Point(X, Y)
{
    public int Zderived { get; set; }
};

public static void Main()
{
    Point p1 = new NamedPoint("A", 1, 2) { Zbase = 3, Zderived = 4 };

    Point p2 = p1 with { X = 5, Y = 6, Zbase = 7 }; // Can't set Name or Zderived
    Console.WriteLine(p2 is NamedPoint);  // output: True
    Console.WriteLine(p2);
    // output: NamedPoint { X = 5, Y = 6, Zbase = 7, Name = A, Zderived = 4 }

    Point p3 = (NamedPoint)p1 with { Name = "B", X = 5, Y = 6, Zbase = 7, Zderived = 8 };
    Console.WriteLine(p3);
    // output: NamedPoint { X = 5, Y = 6, Zbase = 7, Name = B, Zderived = 8 }
}

衍生記錄中的 PrintMembers 格式設定

衍生記錄類型的合成 PrintMembers 方法會呼叫基底實作。 結果是衍生和基底類型的所有公開屬性和欄位都包含在 ToString 輸出中,如下列範例所示:

public abstract record Person(string FirstName, string LastName);
public record Teacher(string FirstName, string LastName, int Grade)
    : Person(FirstName, LastName);
public record Student(string FirstName, string LastName, int Grade)
    : Person(FirstName, LastName);

public static void Main()
{
    Person teacher = new Teacher("Nancy", "Davolio", 3);
    Console.WriteLine(teacher);
    // output: Teacher { FirstName = Nancy, LastName = Davolio, Grade = 3 }
}

您可以提供自己的 PrintMembers 方法的實作。 如果您這樣做,請使用下列簽章:

  • 對於衍生自 sealedobject 記錄 (未宣告基底記錄):private bool PrintMembers(StringBuilder builder)
  • 對於衍生自另一個記錄的 sealed 記錄 (請注意,封入類型是 sealed,因方法實際上為 sealed):protected override bool PrintMembers(StringBuilder builder)
  • 對於不是 sealed 且衍生自物件的記錄:protected virtual bool PrintMembers(StringBuilder builder);
  • 對於不是 sealed 且衍生自另一個記錄的記錄:protected override bool PrintMembers(StringBuilder builder);

以下是取代合成 PrintMembers 方法的程式碼範例 (一個用於衍生自物件的記錄類型,而另一個用於衍生自另一個記錄的記錄類型):

public abstract record Person(string FirstName, string LastName, string[] PhoneNumbers)
{
    protected virtual bool PrintMembers(StringBuilder stringBuilder)
    {
        stringBuilder.Append($"FirstName = {FirstName}, LastName = {LastName}, ");
        stringBuilder.Append($"PhoneNumber1 = {PhoneNumbers[0]}, PhoneNumber2 = {PhoneNumbers[1]}");
        return true;
    }
}

public record Teacher(string FirstName, string LastName, string[] PhoneNumbers, int Grade)
    : Person(FirstName, LastName, PhoneNumbers)
{
    protected override bool PrintMembers(StringBuilder stringBuilder)
    {
        if (base.PrintMembers(stringBuilder))
        {
            stringBuilder.Append(", ");
        }
        ;
        stringBuilder.Append($"Grade = {Grade}");
        return true;
    }
};

public static void Main()
{
    Person teacher = new Teacher("Nancy", "Davolio", new string[2] { "555-1234", "555-6789" }, 3);
    Console.WriteLine(teacher);
    // output: Teacher { FirstName = Nancy, LastName = Davolio, PhoneNumber1 = 555-1234, PhoneNumber2 = 555-6789, Grade = 3 }
}

注意

即使基底記錄密封了 PrintMembers 方法,編譯程式也會在衍生記錄中合成 ToString。 您也可以建立自己的 PrintMembers 實作。

衍生記錄中的解構函式行為

衍生記錄的 Deconstruct 方法會傳回編譯時間類型的所有位置屬性值。 若變數類型為基底記錄,解構操作只會回傳基底記錄屬性,除非物件被鑄造為衍生型別。 下列範例示範如何在衍生記錄上呼叫解構函式。

public abstract record Person(string FirstName, string LastName);
public record Teacher(string FirstName, string LastName, int Grade)
    : Person(FirstName, LastName);
public record Student(string FirstName, string LastName, int Grade)
    : Person(FirstName, LastName);

public static void Main()
{
    Person teacher = new Teacher("Nancy", "Davolio", 3);
    var (firstName, lastName) = teacher; // Doesn't deconstruct Grade
    Console.WriteLine($"{firstName}, {lastName}");// output: Nancy, Davolio

    var (fName, lName, grade) = (Teacher)teacher;
    Console.WriteLine($"{fName}, {lName}, {grade}");// output: Nancy, Davolio, 3
}

泛型條件約束

record 關鍵字是 classstruct 型別的修飾元。 新增 record 修飾元包含此文章先前描述的行為。 沒有需要類型為記錄的泛型條件約束。 record class 滿足 class 限制式。 record struct 滿足 struct 限制式。 如需詳細資訊,請參閱類型參數上的條件約束

C# 語言規格

如需詳細資訊,請參閱 C# 語言規格類別一節。

如需這些功能的詳細資訊,請參閱下列功能提議說明:

另請參閱