结构介绍,结构与类的区别
关注 1421
前提
阅读本节的前提是对值类型,引用类型,栈,堆等概念有所掌握,你可以查看栈,堆,指针介绍一节来了解他们。
需要说明,本节的“类₁”一词,表示与结构无关的类,如果语言将所有数据类型均视为类的话。
结构
结构(Structure)也被称为结构体,他会通过一系列成员来存储数据,这看上去和类₁有点相似,但类₁通常更注重功能,而结构则以数据为重点,即便在某些语言中,结构可以拥有自己的方法。
另外,结构是值类型,而类₁通常是引用类型,虽然结构本身可能也是类的一种,但特定的约束将使其具有确定的存储空间,以符合值类型的要求。
为何使用结构?
想必结构的设计初衷与类是相同的,没有人愿意面对重复而枯燥的代码,在不影响可读性的情况下,少定义一些变量,何乐而不为哪?从凌乱走向简洁,没有人会拒绝。
结构的成员
如前所述,结构以数据为重点,因此,字段是他的标准成员,如果你将值类型和引用类型的不同,视为结构和类₁的本质区别,那么结构拥有类₁的其他特点将是可以理解的。比如在 C# 中,从代码上看,结构和类₁的界线已经越来越模糊了,结构的成员已经拓展到了属性,方法,构造器。
下面的 C# 结构MyDate
拥有三个字段Year
,Month
,Day
,以及一个构造器。
// 结构 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
实例。
// 创建结构 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
。
// 采用引用传递不会产生新的实例,修改月份得到了满意的效果
ref MyDate newDate = ref date;
日期为:2024-6-16
结构的引用类型成员
虽然结构是值类型,但其成员却可以是引用类型,当该成员存储某个类₁的实例时。事实上,该措施是必须的,因为你不可能将空间大小不确定的目标放入栈中,取而代之的只能是对该目标的引用。当然,这意味着结构实例和他的副本可能会用到某个类₁的同一个实例。
下面,为结构MyDate
增加一个引用类型的字段Todo
。
// 结构 MyDate,表示日期
struct MyDate
{
// …
// 字段 Todo,表示当天需要完成的事情
public TODO Todo = new();
// …
}
// 类 TODO,表示需要完成的事情
class TODO
{
// 属性 Content,表示事情的内容
public string Content { get; set; } = string.Empty;
}
创建MyDate
的实例,并通过赋值的方式产生一个该实例的副本,通过dateTodo
的Todo
字段修改TODO
实例的Content
属性,发现dateRest
也受到了影响,因为他们使用了同一个TODO
实例。
// 创建 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:有事了