分享方式:


結構類型 (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 structreadonly ref struct 的相關資訊,請參閱 ref 結構類型一文。

結構類型具有「值語意」。 也就是說,結構類型的變數,包含 該類型的執行個體。 根據預設,指派時會複製變數值、將引數傳遞至方法,並傳回方法結果。 如果是結構類型變數,則會複製該類型的執行個體。 如需詳細資訊,請參閱 實值型別

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

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

readonly 結構

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

如此可確保 readonly 結構中沒有任何成員,修改了該結構的狀態。 這表示除了建構函式以外的其他執行個體成員,都應該會是 readonly

注意

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

下列程式碼會使用僅限 Init 屬性 setter 來定義 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 修飾詞,進行效能最佳化。 如需詳細資訊,請參閱避免配置

非破壞性變異

從 C# 10 開始,可以使用 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 結構

從 C# 10 開始,可以定義記錄結構類型。 記錄類型提供內建功能,來封裝資料。 您可以定義 record structreadonly 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# 資料結構使用內嵌陣列。

內嵌陣列是進階的語言功能。 它們適用於高效能案例,其中內嵌、連續的元素區塊比其他替代資料結構更快。 您可以從功能規格深入了解內嵌陣列

結構初始化和預設值

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。 叫用建構函式時,必須明確指派所有欄位。 您可以使用下列機制,初始化欄位:

  • 您以將「欄位初始設定式」,新增至任何欄位或自動實作屬性。
  • 您可以在建構函式主體中,初始化任何欄位或自動屬性。

從 C# 11 開始,若未初始化結構中的所有欄位,編譯器就會將程式碼新增至會將那些欄位初始化為預設值的建構函式。 編譯器會執行其一般性的明確指派分析。 當建構函式完成執行時,在指派之前或未明確指派之前,所存取的任何欄位,都會在執行建構函式主體之前,指派有預設值。 如果在指派所有欄位之前,存取了 this,則在執行建構函式主體之前,會先將結構初始化為預設值。

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# 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)
    }
}

如果是內建實值型別,請使用對應的常值,指定該類型的值。

結構類型的設計限制

結構具有類別類型的大部分功能。 雖然也有一些例外,但有些例外在較新的版本中也已除去:

  • 結構類型無法繼承自其他類別或結構類型,因而不能是類別的基底。 但結構類型可以實作介面
  • 您無法在結構類型內宣告完成項
  • 在 C# 11 之前,結構類型的建構函式,必須初始化該類型的所有執行個體欄位。

藉傳址方式傳遞結構類型變數

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

結構條件約束

您也可以在 struct 條件約束中使用 struct 關鍵字,來指定型別參數是不可為 null 的實值型別。 結構和列舉類型都能滿足 struct 條件約束。

轉換

對於任何結構類型 (ref struct 類型除外) 來說,有對 System.ValueTypeSystem.Object 類型的 boxing 和 unboxing 轉換。 在結構類型與其實作的任何介面之間,也有 boxing 和 unboxing 的轉換。

C# 語言規格

如需詳細資訊,請參閱 C# 語言規格中的結構一節。

如需 struct 功能的詳細資訊,請參閱下列功能提案筆記:

另請參閱