結構介紹,結構與類別的區別

我被程式碼海扁 @codebeatme
閱讀 6:47·字數 2039·發佈 

先決條件
閱讀本節的先決條件是對實值型別,參考型別,堆疊,堆積等概念有所掌握,你可以檢視堆疊,堆積,指針介紹一節來了解他們。

需要說明,本節的“類別₁”一詞,表示與結構無關的類別,如果語言將所有資料型別均視為類別的話。

結構

結構(Structure)也被稱為結構體,他會通過一系列成員來儲存資料,這看上去和類別₁有點相似,但類別₁通常更註重功能,而結構則以資料為重點,即便在某些語言中,結構可以擁有自己的方法。

另外,結構是實值型別,而類別₁通常是參考型別,雖然結構本身可能也是類別的一種,但特定的約束將使其具有確定的儲存空間,以符合實值型別的要求。

為何使用結構?
想必結構的設計初衷與類別是相同的,沒有人願意面對重複而枯燥的程式碼,在不影響可讀性的情況下,少定義一些變數,何樂而不為哪?從淩亂走向簡潔,沒有人會拒絕。

結構的成員

如前所述,結構以資料為重點,因此,欄位是他的標準成員,如果你將實值型別和參考型別的不同,視為結構和類別₁的本質區別,那麽結構擁有類別₁的其他特點將是可以理解的。比如在 C# 中,從程式碼上看,結構和類別₁的界線已經越來越模糊了,結構的成員已經拓展到了屬性,方法,建構子。

下面的 C# 結構MyDate擁有三個欄位YearMonthDay,以及一個建構子。

structures.cs
// 結構 MyDate,表示日期
struct MyDate
{
	// 欄位 Year,表示年份
	public int Year;
	// 欄位 Month,表示月份
	public int Month;
	// 欄位 Day,表示一月中的第幾天
	public int Day;

// 建構子 public MyDate(int year, int month, int day) { // 初始化結構的欄位 Year = year; Month = month; Day = day; } }

結構的成員存取層級

對於結構的某些成員,比如欄位,其存取層級應該預設為公用,原因很簡單,既然側重於資料,那麽定義的欄位多半是提供給外部存取的。當然,是否預設為公用,語言會有自己的決定。

結構和執行效率

實值型別和預設的傳值,可能會使采用結構的邏輯執行效率降低,當結構包含大量資料並且程式碼中存在頻繁的指派行為時,因為這將導致需要進行反覆且代價不菲的結構建立。

如何解決結構可能帶來的效率問題?
這裏有兩種可能的方案,將傳值改為傳址,或者,將結構改為類別₁。傳址可以減少結構的建立次數,本應被建立的復本會被本體取代,這種方案需要語言給予支援。改為類別₁只能是嘗試性的,因為類別₁的建立同樣是耗時的,如果不能從邏輯上大量減少其建立次數,那麽你將不會從使用類別₁中受益。

無論采用哪種方案,請確保原本需要實作的業務目標不會受到影響。當然,檢查設計也是必要的,或許結構本身就不應該包含如此眾多的資料。

建立結構MyDate的執行個體後,嘗試通過先指派再設定的方式修改月份,發現結果顯示的月份依然為1,這是因為指派產生了新的MyDate執行個體。

*.cs
// 建立結構 MyDate 的執行個體
MyDate date = new(2024, 1, 16);

// 嘗試增加月份 for (int i = 0; i < 5; i++) { // 由於結構是實值型別,預設的傳值將產生一個新的結構執行個體 MyDate newDate = date; newDate.Month++; }
Console.WriteLine($"日期為:{date.Year}-{date.Month}-{date.Day}");
日期為:2024-1-16

將定義變數newDate的陳述式改為傳址的方式後,再次執行程式,月份顯示為6

*.cs
// 采用傳址不會產生新的執行個體,修改月份得到了滿意的效果
ref MyDate newDate = ref date;
日期為:2024-6-16

結構的參考型別成員

雖然結構是實值型別,但其成員卻可以是參考型別,當該成員儲存某個類別₁的執行個體時。事實上,該措施是必須的,因為你不可能將空間大小不確定的目標放入堆疊中,取而代之的只能是對該目標的參考。當然,這意味著結構執行個體和他的復本可能會用到某個類別₁的同一個執行個體。

下面,為結構MyDate增加一個參考型別的欄位Todo

structures.cs
// 結構 MyDate,表示日期
struct MyDate
{
	// …
	// 欄位 Todo,表示當天需要完成的事情
	public TODO Todo = new();
	// …
}

// 類別 TODO,表示需要完成的事情 class TODO { // 屬性 Content,表示事情的內容 public string Content { get; set; } = string.Empty; }

建立MyDate的執行個體,並通過指派的方式產生一個該執行個體的復本,通過dateTodoTodo欄位修改TODO執行個體的Content屬性,發現dateRest也受到了影響,因為他們使用了同一個TODO執行個體。

*.cs
// 建立 MyDate 的執行個體 dateRest
MyDate dateRest = new(2000, 1, 16);
// 修改 Todo 表示的 TODO 執行個體的 Content 屬性
dateRest.Todo.Content = "沒什麽事情";

// 建立 MyDate 的執行個體 dateTodo MyDate dateTodo = dateRest; // 修改年份 dateTodo.Year = 2024; // 修改 Todo 表示的 TODO 執行個體的 Content 屬性 dateTodo.Todo.Content = "有事了";
Console.WriteLine($"{dateRest.Year}-{dateRest.Month}-{dateRest.Day}{dateRest.Todo.Content}"); Console.WriteLine($"{dateTodo.Year}-{dateTodo.Month}-{dateTodo.Day}{dateTodo.Todo.Content}");
2000-1-16:有事了
2024-1-16:有事了

程式碼

structures.cs·codebeatme/programming·GitHub