結構類型 (C# 參考)

「結構類型」(或稱「結構型別」) 是一種實值型別,可以封裝資料和相關功能。

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

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

小提示

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

使用 struct 關鍵字來定義結構類型:

public struct Coords
{
    public Coords(double x, double y)
    {
        X = x;
        Y = y;
    }

    public double X { get; }
    public double Y { get; }

    public override string ToString() => $"({X}, {Y})";
}

如需類型 ref struct 和 readonly ref struct 的相關資訊,請參閱 ref 結構類型一文。

結構類型具有「值語意」。 也就是說,結構類型的變數擁有該類型的實例。 預設情況下,系統會在賦值時、將參數傳給方法時,以及回傳方法結果時,複製變數值。 對於結構型變數,系統會複製該型別的實例。 如需詳細資訊,請參閱 實值型別。

一般而言,結構類型通常用於設計小型的資料為中心的類型,這些類型提供極少或沒有任何行為。 例如,.NET 使用結構型別來表示數字 (整數 與 實數)、 布林值、 Unicode 字元及 時間實例。 如果您專注於類型的行為,請考慮定義一個類別。 類別類型有「參考語意」。 也就是說,類別類型的變數,包含對類型執行個體的參考,而非執行個體本身。

因為結構類型有值語意,所以建議您定義「不可變」的結構類型。

readonly 結構

使用 readonly 修飾符宣告結構型態是不可變的。 所有 readonly 結構的資料成員,都必須為唯讀,如下所示:

  • 任何欄位宣告都必須具有 readonly 修飾詞。
  • 任何屬性,包含自動實作的屬性,必須是唯讀或僅限的。 僅限 Init 的 setter 僅適用於 C# 第 9 版,。

此規則保證結構體中沒有任何成員 readonly 會修改結構體的狀態。 除建構子外,所有其他實例成員皆隱含為 readonly。

注意

在 readonly 結構中,可變參考型別的資料成員,仍然可以變動自己的狀態。 例如,您無法取代 List<T> 執行個體,但可以為其加入新的元素。

以下程式碼定義了一個包含僅限於初始化的屬性設定子的readonly結構:

public readonly struct Coords
{
    public Coords(double x, double y)
    {
        X = x;
        Y = y;
    }

    public double X { get; init; }
    public double Y { get; init; }

    public override string ToString() => $"({X}, {Y})";
}

readonly 執行個體成員

使用 readonly 修飾符宣告實例成員不會修改結構體的狀態。 如果無法將整個結構類型宣告為 readonly,請使用 readonly 修飾詞來標記未修改結構狀態的執行個體成員。

在實 readonly 例成員中,你無法指派到結構的實例欄位。 但 readonly 成員可以呼叫非 readonly 成員。 在此情況下,編譯器會建立結構執行個體的複本,並會呼叫該複本上的非 readonly 成員。 因此,不會修改原始結構執行個體。

一般而言,您會將 readonly 修飾詞套用至下列種類的執行個體成員:

  • 方法:

    public readonly double Sum()
    {
        return X + Y;
    }
    

    您也可以將 readonly 修飾詞套用至覆寫 System.Object 所宣告方法的方法中。

    public readonly override string ToString() => $"({X}, {Y})";
    
  • 屬性與索引器:

    private int counter;
    public int Counter
    {
        readonly get => counter;
        set => counter = value;
    }
    

    如果需要將 readonly 修飾詞套用至屬性或索引子的兩個存取子,請在屬性或索引子的宣告中進行套用。

    注意

    編譯器會宣 get 告自動 實作屬性 的存取器為 readonly,不論屬性宣告中是否存在修 readonly 飾符。

    您可以使用 readonly 存取子,將 init 修飾詞套用至屬性或索引器:

    public readonly double X { get; init; }
    

你可以將修 readonly 飾符套用在結構類型的靜態欄位上,但不能套用到其他靜態成員,例如屬性或方法。

編譯程式可以使用 readonly 修飾詞來進行效能優化。 如需詳細資訊,請參閱避免配置。

非破壞性變異

使用該with表達式建立一個結構型實例的複製品,並更改指定的屬性與欄位。 使用 物件初始化器 語法來指定要修改的成員及其新值,如下範例所示:

public readonly struct Coords
{
    public Coords(double x, double y)
    {
        X = x;
        Y = y;
    }

    public double X { get; init; }
    public double Y { get; init; }

    public override string ToString() => $"({X}, {Y})";
}

public static void Main()
{
    var p1 = new Coords(0, 0);
    Console.WriteLine(p1);  // output: (0, 0)

    var p2 = p1 with { X = 3 };
    Console.WriteLine(p2);  // output: (3, 0)

    var p3 = p1 with { X = 1, Y = 4 };
    Console.WriteLine(p3);  // output: (1, 4)
}

record 結構

您可以定義記錄結構類型。 記錄類型提供內建功能,來封裝資料。 您可以定義 record struct 和 readonly record struct 類型。 記錄結構不可為 ref struct。 如需詳細資訊和範例,請參閱 Records。

內嵌陣列

從 C# 12 開始,你可以宣告 內嵌陣列 作為 struct 一種型別:

[System.Runtime.CompilerServices.InlineArray(10)]
public struct CharBuffer
{
    private char _firstElement;
}

內嵌陣列是一個結構,其中包含相同型別的 N 元素連續區塊。 它是與 固定緩衝區 宣告相等的安全程式碼,僅能在不安全的程式碼中使用。 內嵌陣列是具有下列特性的 struct:

  • 它包含單一欄位。
  • 結構未指定明確的配置。

此外,編譯程式會驗證 System.Runtime.CompilerServices.InlineArrayAttribute 屬性:

  • 長度必須大於零 (> 0)。
  • 目標類型必須是結構。

在大多數情況下,你可以存取像陣列一樣的內嵌陣列,同時讀取和寫入數值。 你也可以使用 範圍 和 指數 運算子。

內嵌陣列的單一欄位類型幾乎沒有任何限制。 它不能是指針類型:

[System.Runtime.CompilerServices.InlineArray(10)]
public struct CharBufferWithPointer
{
    private unsafe char* _pointerElement;    // CS9184
}

但它可以是任何參考型別,或任何實值型別:

[System.Runtime.CompilerServices.InlineArray(10)]
public struct CharBufferWithReferenceType
{
    private string _referenceElement;
}

您可以搭配幾乎任何 C# 資料結構使用內嵌陣列。

內嵌陣列是進階的語言功能。 它們適用於高效能案例,其中內嵌、連續的元素區塊比其他替代資料結構更快。 你可以參考 C# 標準中的 第 16.6 條 Inline 陣列 ,了解更多關於內嵌陣列的資訊。

結構初始化和預設值

struct 類型的變數,直接包含該 struct 的資料。 這種直接的資料儲存區分了未初始化 struct的 ,後者有其預設值,而 struct初始化的 則是透過構造該值所設定的值。 例如,請考慮下列程式碼:

public readonly struct Measurement
{
    public Measurement()
    {
        Value = double.NaN;
        Description = "Undefined";
    }

    public Measurement(double value, string description)
    {
        Value = value;
        Description = description;
    }

    public double Value { get; init; }
    public string Description { get; init; }

    public override string ToString() => $"{Value} ({Description})";
}

public static void Main()
{
    var m1 = new Measurement();
    Console.WriteLine(m1);  // output: NaN (Undefined)

    var m2 = default(Measurement);
    Console.WriteLine(m2);  // output: 0 ()

    var ms = new Measurement[2];
    Console.WriteLine(string.Join(", ", ms));  // output: 0 (), 0 ()
}

如上述範例所示,預設值運算式會忽略無參數建構函式,並會產生結構類型的預設值。 具現化結構類型陣列,也會忽略無參數建構函式,並會產生填入結構類型預設值的陣列。

最常見的情況是會在陣列或其他集合中看到預設值,而內部儲存體則包含變數區塊。 下列範例會建立有 30 個 TemperatureRange 結構的陣列,每個結構都有預設值:

// All elements have default values of 0:
TemperatureRange[] lastMonth = new TemperatureRange[30];

因為 類型直接儲存其數據,因此在建立時,結構體的所有成員欄位都必須 struct。 default結構體的值確實會將所有欄位分配為 0。 叫用建構函式時,必須明確指派所有欄位。 你可以透過以下機制初始化欄位:

  • 在任何欄位或自動實作屬性中加入 欄位初始化器 。
  • 初始化建構子主體中的任何欄位或自動屬性。

如果你沒有初始化結構中的所有欄位,編譯器會在建構器中加入程式碼,將這些欄位初始化為預設值。 指派給其 default 值的結構會初始化為 0 位模式。 使用 new 初始化的結構會初始化為 0 位模式,然後執行任何欄位初始化運算式和建構函式。

public readonly struct Measurement
{
    public Measurement(double value)
    {
        Value = value;
    }

    public Measurement(double value, string description)
    {
        Value = value;
        Description = description;
    }

    public Measurement(string description)
    {
        Description = description;
    }

    public double Value { get; init; }
    public string Description { get; init; } = "Ordinary measurement";

    public override string ToString() => $"{Value} ({Description})";
}

public static void Main()
{
    var m1 = new Measurement(5);
    Console.WriteLine(m1);  // output: 5 (Ordinary measurement)

    var m2 = new Measurement();
    Console.WriteLine(m2);  // output: 0 ()

    var m3 = default(Measurement);
    Console.WriteLine(m3);  // output: 0 ()
}

每個 struct 都會有一個 public 無參數建構函式。 如果要撰寫無參數建構函式,必須是公開的 (public)。 如果結構宣告了任何欄位初始設定式,其必須明確宣告建構函式。 該建構函式不一定要是無參數。 如果結構宣告了欄位初始設定式,但沒有建構函式,則編譯器會回報錯誤。 任何明確宣告的建構函式 (有參數或無參數),都會執行該結構的所有欄位初始設定式。 所有在建構函式中沒有欄位初始設定式或指派的欄位,都會設定為預設值。 欲了解更多資訊,請參閱 C# 語言規範—建構子。

從 C# 12 開始,struct 類型可以在其宣告中,將主要建構函定義為其中的一部分。 主要建構函式會針對建構函式參數提供簡潔的語法,可在該結構的任何成員宣告中,在整個 struct 主體中使用。

如果可以存取結構類型的所有執行個體欄位,則也可以在沒有 new 運算子的情況下,將其具現化。 在這種情況下,你必須在首次使用實例前初始化所有實例欄位。 下列範例顯示如何執行該項工作:

public static class StructWithoutNew
{
    public struct Coords
    {
        public double x;
        public double y;
    }

    public static void Main()
    {
        Coords p;
        p.x = 3;
        p.y = 4;
        Console.WriteLine($"({p.x}, {p.y})");  // output: (3, 4)
    }
}

針對 內建實值型別,請使用對應的文字來設定該型別的值。

結構類型的設計限制

結構具有類別類型的大部分功能。 有一些例外狀況:

  • 結構類型無法繼承自其他類別或結構類型,因而不能是類別的基底。 但結構類型可以實作介面。
  • 您無法在結構類型內宣告完成項。
  • 結構型態的建構器必須初始化該型態的所有實例欄位。

以參照傳遞結構類型變數

當您將結構類型變數傳遞至方法而作為引數,或是從方法傳回結構類型值時,會複製結構型別的整個執行個體。 以值傳遞可能會在涉及大型結構類型的高效能場景中影響程式碼效能。 藉傳址方式傳遞結構類型變數,即可避免複製值。 使用 ref、out、in 或 ref readonly 方法參數修飾詞,可指出引數必須藉傳址方式傳遞。 使用 ref returns 可藉傳址方式傳回方法結果。 如需詳細資訊,請參閱避免分配。

struct 條件約束

使用struct限制中的struct關鍵字指定型別參數為不可空值型別。 結構和列舉類型都能滿足 struct 條件約束。

轉換

對於任何結構類型(類型除外ref struct),存在與 and 類型之間的System.ValueTypeSystem.Object轉換。 結構類型與其實作的任何介面之間也存在盒裝與開箱轉換。

C# 語言規格

欲了解更多資訊,請參閱 C# 語言規範中的結構體部分,特別是以下條款:

你也可以閱讀 C# 語言規範中關於 自動預設結構的 相關資訊。

另請參閱