C# Struct结构的介绍
C# (Struct)结构的介绍
在 C# 中,所有简单值类型都是结构类型。结构类型是一种可封装数据和相关功能的值类型 ,是隐式密封的值类型,不可继承。 使用 struct 关键字定义结构类型。struct 语句为程序定义了一个带有多个成员的新的数据类型。例如,.NET 使用结构类型来表示数字(整数和实数)、布尔值、Unicode 字符以及时间实例。 如果侧重于类型的行为,请考虑定义一个类。
继承
Object-> ValueType ->Enum Object-> ValueType ->struct 包括int float等简单值类型 Object-> ValueType ->ValueTuple Object-> ValueType ->Nullable
由于结构类型具有值语义,因此建议定义不可变的 结构类型。
一、声明结构的语法 - struct关键字
public readonly struct AddressBook
{
//字段、属性、方法、事件
}
对于类而言,两个变量指向同一个对象的情况是存在的,因此对这样两个变量中的任意一个进行操作,起结果必然会影响另外一个,因为它们指向的是同一个对象。
结构是值类型,,直接包含它自己的数据,每个结构都保存自己的一份数据,修改每一个结构的数据都不会对其他结构的数据造成影响。
struct的IL代码
结构的初始化方式
static void Main(string[] args) { //第一种赋值方式 Mystruct se; se.i = 1; //第二种赋值方式 se = new(); Console.WriteLine(se.i); }
结构变量为什么不能进行初始值设定?
个人的理解
C# 实例有默认值的规范,而这个默认值从编译角度都是为0,引用类型默认都是null,值类型应该默认都是0。
默认值的初始化通常是通过使内存管理器或垃圾回收器将全部内存初始化为零来完成的,然后再将其分配给使用---msdn,然后在调用构造函数完成默认值初始化 。
内存清空了,对于引用类型存在堆栈中地址都没了,自然就为null,类可以实例字段设置初始值,因为那些都是保持托管堆中的,不影响初始化。
内存清空了,对于引用值类型相应的内容就没了,值类型是存在托堆栈中的。
而struct型是实实在在内容保存在堆栈中,要保持默认值,就必须保证堆栈的内容都为空。
为了实现 值类型堆栈内容都为0,C# 就将struct 的无参构造函数要隐藏起来并且不允许在struct定义无参构造函数,保证了值类型初始化为系统默认值。
当我在程序中调用deaflut(),默认值的初始化通常是通过使内存管理器或垃圾回收器将全部内存初始化为零来完成的,然后再将其分配给使用,然后在调用构造函数完成默认值初始化。
msdn 官方的解释
默认值的初始化通常是通过使内存管理器或垃圾回收器将全部内存初始化为零来完成的,然后再将其分配给使用。 出于此原因,可以方便地使用所有位数表示空引用。
C# 结构的特点
1、结构可带有方法、字段、索引、属性、运算符方法和事件。
2、 结构可定义构造函数,但不能定义析构函数。但是,您不能为结构定义无参构造函数。无参构造函数(默认)是自动定义的,且不能被改变。
3、与类不同,结构不能继承其他的结构或类。
4、结构不能作为其他结构或类的基础结构。
5、 结构可实现一个或多个接口。
6、 结构成员不能指定为 abstract、virtual 或 protected。因为不能继承声明这些也没意义
7、当您使用 New 操作符创建一个结构对象时,会调用适当的构造函数来创建结构。与类不同,结构可以不使用 New 操作符即可被实例化。
8、 如果不使用 New 操作符,只有在所有的字段都被初始化之后,字段才被赋值,对象才被使用。
9、结构体中声明的变量无法赋予初值,类可以。
10、类的对象是存储在堆空间中,结构存储在栈中。堆空间大,但访问速度较慢,栈空间小,访问速度相对更快。故而,当我们描述一个轻量级对象的时候,结构可提高效率,成本更低。当然,这也得从需求出发,假如我们在传值的时候希望传递的是对象的引用地址而不是对象的拷贝,就应该使用类了。
11、结构式sealed是密封类型,所以不能继承
12、如果不使用 New 操作符,只有在所有的字段都被初始化之后,字段才被赋值,对象才被使用
结构类型的设计限制
设计结构类型时,具有与类类型相同的功能,但有以下例外:
1、 不能声明无参数构造函数(C#10开始可以声明无参数构造函数)。 每个结构类型都已经提供了一个隐式无参数构造函数,该构造函数生成类型的默认值。
2、 不能在声明实例字段或属性时对它们进行初始化(C# 后开始可以)。 但是,可以在其声明中初始化静态或常量字段或静态属性。
3、 结构类型的构造函数必须初始化该类型的所有实例字段。
4、结构类型不能从其他类或结构类型继承,也不能作为类的基础类型。 但是,结构类型可以实现接口(ref struct除外)。
5、不能在结构类型中声明终结器。值类型由GC直接回收
readonly 结构
从 C# 7.2 开始,可以使用 readonly 修饰符来声明结构类型为不可变。 readonly 结构的所有数据成员都必须是
只读的,如下所示:
任何字段声明都必须具有 readonly 修饰符
任何属性(包括自动实现的属性)都必须是只读的。 在 C# 9.0 和更高版本中,属性可以具有 init 访问器。
这样可以保证 readonly 结构的成员不会修改该结构的状态。 在 C# 8.0 及更高版本中,这意味着除构造函数外
的其他实例成员是隐式 readonly 。
另一种定义只读属性的方式是:init访问器
public readonly struct Coords { public Coords(double x, double y) { X = x; Y = y; } public double X { get; init; } public double Y { get; init; } } pu
readonly 实例成员
从 C#8.0 开始,还可以使用 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})";
ref 结构
按引用传递结构类型变量
将结构类型变量作为参数传递给方法或从方法返回结构类型值时,将复制结构类型的整个实例。 这可能会影响
高性能方案中涉及大型结构类型的代码的性能。 通过按引用传递结构类型变量,可以避免值复制操作。 使用
ref 、 out 或 in 方法参数修饰符,指示必须按引用传递参数。 使用 ref 返回值按引用返回方法结果。 有关详细
通常,如果需要一种同时包含 ref 结构类型的数据成员的类型,可以定义 ref 结构类型:
从 C# 7.2 开始,可以在结构类型的声明中使用 ref 修饰符。 ref 结构类型的实例在堆栈上分配,并且不能转义
到托管堆。 为了确保这一点,编译器将 ref 结构类型的使用限制如下:
ref struct 是仅在堆栈上的值类型使用限制如下:
- 表现一个顺序结构的布局;(译注:可以理解为连续内存)
- 只能在堆栈上使用。即用作方法参数和局部变量;
- 不能用来声明类或非 ref结构的静态或实例成员;
- 不能被本地函数捕获或lambda表达式的方法参数;
- 不能动态绑定、装箱\拆箱。
- 不能在异步方法、迭代器、数组、类型参数(泛型)中使用
ref struct也被称为嵌入式引用。
public ref struct CustomRef { public bool IsValid; public Span<int> Inputs; public Span<int> Outputs; }
若要将 ref 结构声明为 readonly ,请在类型声明中组合使用 readonly 修饰符和 ref 修饰符( readonly 修饰
符必须位于 ref 修饰符之前):
public readonly ref struct ConversionRequest { public ConversionRequest(double rate, ReadOnlySpan<double> values) { Rate = rate; Values = values; } } public double Rate { get; } public ReadOnlyS
在 .NET 中, ref 结构的示例分别是 System.Span<T> 和 System.ReadOnlySpan<T>。
一次性引用结构
ref struct Book : IDisposable { public void Dispose() {
//但从现在开始,如果我们在 ref 结构中添加 public Dispose 方法,它将被 using 语句自动使用,整个事情就会编译: } } class Program { static void Main(string[] args) { using var book = new Book(); // ... } }
结构类型的实例化
在 C# 中,必须先初始化已声明的变量,然后才能使用该变量。 由于结构类型变量不能为 null (除非它是可为空
的值类型的变量),因此,必须实例化相应类型的实例。 有多种方法可实现此目的。
通常,可使用 new 运算符调用适当的构造函数来实例化结构类型。 每个结构类型都至少有一个构造函数。 这是
一个隐式无参数构造函数,用于生成类型的默认值。 还可以使用默认值表达式来生成类型的默认值。
如果结构类型的所有实例字段都是可访问的,则还可以在不使用 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#结构的的作用
结构体是用来代表一个记录。假设您想跟踪图书馆中书的动态。您可能想跟踪每本书的以下属性:
Title
Author
Subject
Book ID
C#类 、结构、枚举直接的比较
struct 约束
你还可在 struct 约束中使用 struct 关键字,来指定类型参数为不可为 null 的值类型。 结构类型和枚举类型
都满足 struct 约束。
C#结构体的内存对齐(边界对齐)
结构体的边界对齐 规则如下:
规则1、每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。程序员可以通过[StructLayout(LayoutKind.Sequential, Pack =n)],n=1,2,4,8,16来改变这一系数,其中的n就是你要指定的“对齐系数”。
C#对齐系数 n=0,1,2,4,8,16, 0表示默认的平台对齐系数,就是操作系统的字长字长/8。
规则2、数据成员对齐 :结构体的第一个字段的偏移量(offset)为0以后每一个成员相对于结构体首字段的offset都是该成员大小与有效对其值中较小的那个的整数倍,不满足条件,自动填补字节。
找最大数据长度时,如果结构体T有复杂类型成员A的,该A成员的长度为该复杂类型成员A的最大成员长度,例如 demail 由4个int 组成,它的最大成员是int,decimal a;a应是int 而不是decimal
备注:对齐单位(有效对齐值): 系统给定的对齐系数和结构体内最长数据长度,之中较小的那一个。有效对其值也叫做对齐单位。
规则3、结构的整体对齐:结构体总体size mod 对齐单位=0 ,结构体的总的大小时对齐单位的整数倍,不满足将自动填充。
案例1:
[StructLayout(LayoutKind.Sequential, Pack =4)] unsafe struct ExampleStruct1 { //对齐系数 pack=4 //最大对齐成员 是int =4 //对齐单位=4 pack<最大对齐成员?pcke:最大对齐成员 byte b;//第一个默认是对齐地址 为0=成员地址-对象地址 int a;// 4 a=对齐单位=4 ,因此a 从4开始对齐,前面补null byte s;//8 b=1<对齐单位 ,因此 对齐地址= 对齐地址 mode b =0,不足在前面补3个空白字节 short d;//10 d=2<对齐单位,因此 对齐地址= 对齐地址 mode d =0,不足在前面补空白字节
byte c;//12 } //最后一步是对象对齐= 结构体总体size mod 对齐单位=0 ,13/4!=0 所以 要补3个空白字节 得到16/4=0
案例2:
[StructLayout(LayoutKind.Sequential, Pack =8)] unsafe struct ExampleStruct1 { byte b; int a;//int长为4 比pack=8 小,int 又是ExampleStruct1 结构体中最大的,因此选4作为实例对齐 符合第2条规则 byte c; } public class Example { public unsafe static void Main() { Console.WriteLine( sizeof(ExampleStruct1) ); } }
案例2:
struct MyStruct { int a; decimal b;//法则二的应用每个字段必须与自己大小的字段对齐,以较小者为准。 decimal由4个组成,最大是是int 4位 decimal 与字节内部的字段对齐,内部最大字段是 double,因此decimal不是最长为,double才是最长的 。 double c; } //计算结果是32个字节=8+16+8
结构体内存布局
我们来看IL代码,其中sequential 是什么意思?这个是结构内在内存中的布局方式,接下来我详细讲解
struct StructDeft//C#编译器会自动在上面运用[StructLayout(LayoutKind.Sequential)] { bool i; //1Byte(字节) double c;//8byte bool b; //1byte }
sizeof(StructDeft)得到的结果是24byte!啊哈,本身只有10byte的数据却占有了24byte的内存,这是因为默认(LayoutKind.Sequential)情况下,CLR对struct的Layout的处理方法与C/C++中默认的处理方式相同(8+8+8=24),即按照结构中占用空间最大的成员进行对齐(Align)。10byte的数据却占有了24byte,严重地浪费了内存,所以如果我们正在创建一个与非托管代码没有任何互操作的struct类型,最好还是不要使用默认的StructLayoutAttribute(LayoutKind.Sequential)特性。
2.[StructLayout(LayoutKind.Explicit)]
FieldOffset定义每个数据元素在结构内的位置,单位是字节。以下是错误代码示范:
[StructLayout(LayoutKind.Explicit)] struct BadStruct { [FieldOffset(0)] public bool i; //1字节(Byte) [FieldOffset(0)] public double c;//8字节 [FieldOffset(0)] public bool b; //1字节 }
sizeof(BadStruct)得到的结果是9byte,显然得出的基数9显示CLR并没对结构体进行任何内存对齐(Align);本身要占有10byte的数据却只占了9byte,显然有些数据被丢失了,这也正是我给struct取BadStruct作为名字的原因。如果在struct上运用了[StructLayout(LayoutKind.Explicit)],计算FieldOffset一定要小心,例如我们使用上面BadStruct来进行下面的测试:
StructExpt e = new StructExpt(); e.c = 0 ; e.i = true ; Console.WriteLine(e.c);
输出的结果不再是0了,而是4.94065645841247E-324,这是因为e.c和e.i共享同一个byte,执行“e.i = true;时”也改变了e.c,CPU在按照浮点数的格式解析e.c时就得到了这个结果.所以在运用LayoutKind.Explicit时千万别吧FieldOffset算错了:)
正确示范:
[StructLayout(LayoutKind.Explicit)] struct Person { [FieldOffset(0)] public int age; [FieldOffset(4)]//因为int存储数据要4个字节,所有string 应该4个字节后开始计算 public string name; }
3.[StructLayout(LayoutKind.Auto)]
sizeof(StructAuto)得到的结果是12byte。下面来测试下这StructAuto的三个字段是如何摆放的:
[StructLayout(LayoutKind.Auto)] struct StructAuto { public bool i; //1字节(Byte) public double c;//8字节 public bool b; //1字节 }
unsafe { StructAuto s = new StructAuto(); Console.WriteLine(string.Format("i:{0}", (int)&(s.i))); Console.WriteLine(string.Format("c:{0}", (int)&(s.c))); Console.WriteLine(string.Format("b:{0}", (int)&(s.b))); } // 测试结果: i:1242180 c: 1242172 b: 1242181
即CLR会对结构体中的字段顺序进行调整,将i调到c之后,使得StructAuto的实例s占有尽可能少的内存,并进行4byte的内存对齐(Align),字段顺序调整结果如下图所示:
4.空struct实例的Size
struct EmptyStruct{}
无论运用上面LayoutKind的Explicit、Auto还是Sequential,得到的sizeof(EmptyStct)都是1byte。
5、“诡异”的decimal类型
struct MyStruct { int a; decimal b;//decimal由4int组成,因此它不是最长的,double才是最长的。 double c; }
//计算结果是32个字节=8+16+8
Why?Decimal不是16字节的吗?
我刨析了Decimal结构发现了真相
原理Decimal是由int、uint、ulong组成的,它的内部最长也是long。所以上述MyStruct结构中占内存最大的成员是double,对齐长度是8。Decimal结构如下:
struct Decimal{ private readonly int _flags; private readonly uint _hi32; private readonly ulong _lo64; //静态字段和属性、const 不算入 sizeof() 。。。。 }
7、
[StructLayout(LayoutKind.Sequential, Pack = 16)] unsafe struct ExampleStruct2 { int a; decimal b; double c; } unsafe struct ExampleStruct1 { int a; static ExampleStruct2 b;//sizeof()只算实例对象的内存空间,因此const 、static字段都不包含 double c; } public class Example { public unsafe static void Main() { Console.WriteLine( sizeof(ExampleStruct1) ); } } //输出结果是16 sizeof()只算实例对象的内存空间,因此const 、static字段都不包含
结论:
默认(LayoutKind.Sequential)情况下,CLR对struct的Layout的处理方法与C/C++中默认的处理方式相同,即按照结构中占用空间最大的成员进行对齐(Align);
使用LayoutKind.Explicit的情况下,CLR不对结构体进行任何内存对齐(Align),而且我们要小心就是FieldOffset;
使用LayoutKind.Auto的情况下,CLR会对结构体中的字段顺序进行调整,使实例占有尽可能少的内存,并进行4byte的内存对齐(Align)。
StructLayoutAttribute.Pack Field (System.Runtime.InteropServices) |微软文档 (microsoft.com)