結構類型(或 struct type)是一種可封裝數據和相關功能的值類型。使用 struct 關鍵字定義結構類型:
public struct JG座標
{
public JG座標 ( double x , double y )
{
X = x;
Y = y;
}
public double X { get; }
public double Y { get; }
public override string ToString ( ) => $"({X},{Y})";
}
結構類型具有值語義。也就是説,結構類型的變量包含類型的實例。默認情況下,在分配中,通過將參數傳遞給方法並返回方法結果來複制變量值。對於結構類型變量,將複製該類型的實例。
通常,可以使用結構類型來設計以數據為中心的較小類型,這些類型只有很少的行為或沒有行為。例如,.NET 使用結構類型來表示數字(整數和實數)、布爾值、Unicode 字符以及時間實例。如果側重於類型的行為,請考慮定義一個類。類類型具有引用語義。也就是説,類類型的變量包含的是對類型的實例的引用,而不是實例本身。
由於結構類型具有值語義,因此建議定義不可變的結構類型。
readonly 結構
可以使用 readonly 修飾符來聲明結構類型為不可變。readonly 結構的所有數據成員都必須是隻讀的,如下所示:
- 任何字段聲明都必須具有 readonly 修飾符。
- 任何屬性(包括自動實現的屬性)都必須是隻讀的,或者是 init 只讀的。init 資源庫僅限從 C# 版本 9 開始可用。
這樣可以保證 readonly 結構的成員不會修改該結構的狀態。這意味着除構造函數外的其他實例成員是隱式 readonly。
備註:在 readonly 結構中,可變引用類型的數據成員仍可改變其自身的狀態。例如,不能替換 List < T > 實例,但可以向其中添加新元素。
下面的代碼使用 init-only 屬性資源庫定義 readonly 結構:
public readonly struct JG座標
{
public JG座標 ( 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 JiShu; public int 計數器 { readonly get => JiShu; set => JiShu = value; }如果需要將 readonly 修飾符應用於屬性或索引器的兩個訪問器,請在屬性或索引器的聲明中應用它。
備註:編譯器將 get 自動實現的屬性的訪問器聲明為 readonly,無論屬性聲明中是否存在 readonly 修飾符。
可以將 readonly 修飾符應用於具有 init 訪問器的屬性或索引器:
public readonly double X { get; init; }
可以將 readonly 修飾符應用於結構類型的靜態字段,但不能應用於任何其他靜態成員,例如屬性或方法。
編譯器可以使用 readonly 修飾符進行性能優化。
非破壞性變化
你可以使用 with 表達式來生成修改了指定屬性和字段的結構類型實例的副本。使用對象初始值設定項語法來指定要修改的成員及其新值,如以下示例所示:
public readonly struct JG座標
{
public JG座標 ( 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 z1 = new JG座標 ( 0 , 0 );
Console . WriteLine ( z1 ); // (0,0)
var z2 = z1 with { X = 3 };
Console . WriteLine ( z2 ); // (3,0)
var z3 = z1 with { X = 1, Y = 4 };
Console . WriteLine ( z3 ); // (1,4)
}
record 結構
你可以定義 record 結構類型。record 類型提供用於封裝數據的內置功能。可同時定義 record struct 和 readonly record struct 類型。記錄結構不能是 ref struct。
內聯數組
從 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 JG測量
{
public JG測量 ( )
{
This . 值 = double . NaN;
This . 説明 = "未指定";
}
public JG測量 ( double 值 , string 説明 )
{
This . = 值;
This . 説明 = 説明;
}
public double 值 { get; init; }
public string 説明 { get; init; }
public override string ToString ( ) => $"{Value},({Description})";
}
public static void Main ( )
{
var CL1 = new JG測量 ( );
Console . WriteLine ( CL1 ); // NaN(未指定)
var CL2 = default ( JG測量 );
Console . WriteLine ( CL2 ); // 0()
var CLs = new JG測量 [ 2 ];
Console . WriteLine ( string . Join ( "," , CLs ) ); // 0(),0()
}
如前面的示例所示,默認值表達式忽略了無參數構造函數,並生成了結構類型的默認值。結構類型數組實例化還忽略無參數構造函數並生成使用結構類型的默認值填充的數組。
你看到默認值的最常見情況是在數組中或內部存儲包含變量塊的其他集合中。以下示例創建了一個由 30 個 TemperatureRange 結構組成的數組,每個結構都具有默認值:
// 所有的元素都具有默認值 0:
TemperatureRange [ ] lastMonth = new TemperatureRange [ 30 ];
結構的所有成員字段在創建時必須進行明確指定,因為 struct 類型直接存儲其數據。結構的 default 值已將所有字段明確指定為 0。調用構造函數時,必須明確指定所有字段。可以使用以下機制初始化字段:
- 可以將字段初始化表達式添加到任何字段或自動實現的屬性。
- 可以在構造函數主體中初始化任何字段或自動屬性。
從 C# 11 開始,如果你沒有初始化結構中的所有字段,編譯器會將代碼添加到將這些字段初始化為默認值的構造函數中。分配給其 default 值的結構將初始化為 0 位模式。使用 new 初始化的結構體初始化為 0 位模式,然後執行所有字段初始值設定項和構造函數。
public readonly struct JG測量
{
public JG測量 ( double 值 )
{
This . 值 = 值;
}
public JG測量 ( double 值 , string 説明 )
{
This . 值 = 值;
This . 説明 = 説明;
}
public JG測量 ( string 説明 )
{
This . 説明 = 説明;
}
public double 值 { get; init; }
public string 説明 { get; init; } = "常規測量";
public override string ToString ( ) => $"{Value},({Description})";
}
public static void Main ( )
{
var CL1 = new JG測量 ( 5 );
Console . WriteLine ( CL1 ); // 5(常規測量)
var CL2 = new JG測量 ( );
Console . WriteLine ( CL2 ); // 0()
var CL3 = default ( JG測量 );
Console . WriteLine ( CL3 ); // 0()
}
每個 struct 都具有一個 public 無參數構造函數。如果要編寫無參數構造函數,它必須是公共構造函數。如果結構聲明瞭任何字段初始值設定項,就必須顯式聲明一個構造函數。該構造函數不必是無參數的。如果結構聲明瞭字段初始值設定項,但沒有構造函數,編譯器將報告錯誤。任何顯式聲明的構造函數(有參數或無參數)都會執行該結構的所有字段初始值設定項。沒有字段初始值設定項或構造函數的賦值的所有字段均設置為默認值。
從 C# 12 開始,struct 類型可以將主構造函數定義為其聲明的一部分。主要構造函數為構造函數參數提供了簡潔的語法,可在該結構的任何成員聲明中的整個 struct 正文中使用。
如果結構類型的所有實例字段都是可訪問的,則還可以在不使用 new 運算符的情況下對其進行實例化。在這種情況下,在首次使用實例之前必須初始化所有實例字段。下面的示例演示如何執行此操作:
public static class JG沒有New
{
public struct JG座標
{
public double x;
public double y;
}
public static void Main ( )
{
JG座標 D;
D . x = 3;
D . y = 4;
Console . WriteLine ( $"({p . x},{p . y})" ); // (3, 4)
}
}
對於內置值類型,請使用相應的文本來指定類型的值。
結構類型的設計限制
結構具有類類型的大部分功能。也有一些例外情況:
- 結構類型不能從其他類或結構類型繼承,也不能作為類的基礎類型。但是,結構類型可以實現接口。
- 不能在結構類型中聲明終結器。
- 在 C# 11 之前,結構類型的構造函數必須初始化該類型的所有實例字段。
按引用傳遞結構類型變量
將結構類型變量作為參數傳遞給方法或從方法返回結構類型值時,將複製結構類型的整個實例。通過值傳遞可能會影響高性能方案中涉及大型結構類型的代碼的性能。通過按引用傳遞結構類型變量,可以避免值複製操作。使用 ref、out、in 或 ref readonly 方法參數修飾符,指示必須按引用傳遞某個參數。使用 ref 返回值按引用返回方法結果。
struct 約束
你還可在 struct中使用 struct 關鍵字,來指定類型參數為不可為 null 的值類型。結構類型和枚舉類型都滿足 struct 約束。
轉換
對於任何結構類型(ref struct 類型除外),都存在與 System . ValueType 和 System . Object 類型之間的相互轉換。還存在結構類型和它所實現的任何接口之間的 boxed 和 unboxed 轉換。