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 结构不能是数组的元素类型。
ref 结构不能是类或非 ref 结构的字段的声明类型。
ref 结构不能实现接口。
ref 结构不能被装箱为 System.ValueType 或 System.Object。
ref 结构不能是类型参数(泛型)。
ref 结构变量不能由 lambda 表达式或本地函数捕获。
ref 结构变量不能在 async 方法中使用。 但是,可以在同步方法中使用 ref 结构变量,例如,在返回 Task
或 Task<TResult> 的方法中。
ref 结构变量不能在迭代器中使用。
通常,如果需要一种同时包含 ref 结构类型的数据成员的类型,可以定义 ref 结构类型:
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>。

结构类型的实例化

在 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 约束。

结构体进阶---内存布局

我们来看IL代码,其中sequential 是什么意思?这个是结构内在内存中的布局方式,接下来我详细讲解

 
.net托管环境中,CLR提供了更自由的方式来控制struct中Layout:我们可以在定义struct时,在struct上运用StructLayoutAttribute特性来控制成员的内存布局。默认情况下,struct实例中的字段在栈上的布局(Layout)顺序与声明中的顺序相同,即在struct上运用[StructLayoutAttribute(LayoutKind.Sequential)]特性,这样做的原因是结构常用于和非托管代码交互的情形。如果我们正在创建一个与非托管代码没有任何互操作的struct类型,我们很可能希望改变C#编译器的这种默认规则,因此LayoutKind除了Sequential成员之外,还有两个成员Auto和Explicit,给StructLayoutAttribute传入LayoutKind.Auto可以让CLR按照自己选择的最优方式来排列实例中的字段;传入LayoutKind.Explicit可以使字段按照我们的在字段上设定的FieldOffset来更灵活的设置字段排序方式,但这种方式也挺危险的,如果设置错误后果将会比较严重。下面就看几个示例,算下四个struct各占多少Byte?
1.[StructLayout(LayoutKind.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。 

结论:
        默认(LayoutKind.Sequential)情况下,CLR对struct的Layout的处理方法与C/C++中默认的处理方式相同,即按照结构中占用空间最大的成员进行对齐(Align);
        使用LayoutKind.Explicit的情况下,CLR不对结构体进行任何内存对齐(Align),而且我们要小心就是FieldOffset;
        使用LayoutKind.Auto的情况下,CLR会对结构体中的字段顺序进行调整,使实例占有尽可能少的内存,并进行4byte的内存对齐(Align)。

 

C# Struct结构的介绍的更多相关文章

  1. 1.0 基础、标示符、常量、数据类型(enum 枚举,struct 结构体)、操作符、循环、数组

    一.程序 现实生活中,程序是指完成某些事务的一种既定方法和过程,可以把程序看成是一系列动作执行过程的描述. 在计算机世界,程序是指令,即为了让计算机执行某些操作或解决某个问题而编写的一系列有序指令的集 ...

  2. golang(07)结构体介绍

    golang支持面向对象的设计,一般支持面向对象的语言都会有class的设计,但是golang没有class关键字,只有struct结构体.通过结构体达到类的效果,这叫做大成若缺,其用不弊. stru ...

  3. C# Struct结构体里数组长度的指定

    typedef struct Point{ unsigned short x; unsigned short y; }mPoint;//点坐标 typedef struct Line{ mPoint ...

  4. C#基础--struct(结构体)

    结构体和类有点类似    我们定义一个类的时候    是class   类名   定义结构体的时候是 struct   结构体名 结构体的写法 struct Point { // public int ...

  5. HTML5的新的结构元素介绍

    HTML5的新的结构元素介绍 一.HTML5与HTML4的区别 1. 取消了一些过时的HTML4的标签 其中包括纯粹显示效果的标记,如<font>和<center>,它们已经被 ...

  6. Golang struct结构

    结构struct Go中的struct与C中的struct非常相似,并且Go没有class,代替了class的位置,但并没有代替class的功能 使用type struct{} 定义结构,名称遵循可见 ...

  7. C#语言struct结构体适用场景和注意事项

    在C#语言中struct结构体和class之间的区别主要是值类型和引用类型的区别,但实际上如果使用不当是非常要命的.从Win32时代过来的人对于struct一点不感觉陌生,但是却反而忽略了一些基本问题 ...

  8. C语言 Struct 结构体在 Java 中的体现

    大一整个学期完成了 C 语言的学习,大二就进入了Java 的学习. 和C语言一样,我们都会尝试写一个小小的学生管理系统什么的,学习过 C 语言同学知道,在管理系统中 Struct 结构体是个很好用的东 ...

  9. P Invoke struct结构

    一.获取Struct CHCNetSDK.NET_DVR_PTZPOS pos = new CameraTest.CHCNetSDK.NET_DVR_PTZPOS(); int size = Mars ...

随机推荐

  1. bit操作常见trick

    x&(x-1)可以消去最右边的1, 如果判断一个数是否是2的指数的快捷方法,比如8,二进制位1000, 那么8&(8-1)为0,只要为0就是2的指数

  2. 学习AJAX必知必会(4)~同源策略、解决跨域问题(JSONP、CORS)

    一.同源策略(Same-Origin Policy),是浏览器的一种安全策略. 1.同源(即url相同):协议.域名.端口号 必须完全相同.(请求是来自同一个服务) 2.跨域:违背了同源策略,即跨域. ...

  3. 关于C++11共享数据带来的死锁问题的提出与解决

    举个例子,如果有一份资源,假如为list<int>资源,假设有两个线程要对该资源进行压入弹出操作,如果不进行锁的话,那么如果两个线程同时操作,那么必然乱套,得到的结果肯定不是我们想要的结果 ...

  4. 普罗米修斯+grafana监控k8s

    其实现原理有点类似ELK.node-exporter组件负责收集节点上的metrics监控数据,并将数据推送给prometheus, prometheus负责存储这些数据,grafana将这些数据通过 ...

  5. linux用户密码过期导致命令执行失败

    背景介绍: 使用zabbix调用系统命令,检查时间同步,发现一直在报错,root 用户执行无异常,问题还是出现zabbix用户上面. [zabbix@test-10-12 ~]$ sudo ntpda ...

  6. JavaFx 软件重启功能实现

    原文地址: JavaFx 软件重启功能实现 | Stars-One的杂货小窝 本篇使用Kotlin在TornadoFx中实践,没有Java代码的示例,各位自行参考,思路已在本文中提及 实现思路 主要思 ...

  7. hive DML 操作

    数据导入 向表中装载数据(Load) 1.语法 load data [local] inpath '数据的 path' [overwrite] into table student [partitio ...

  8. Atcoder ARC-063

    ARC063(2020.7.16) A \(A\) 题如果洛谷评分很低就不看了. B 可以发现一定是选择在一个地方全部买完然后在之后的一个地方全部卖完,那么我们就只需要即一个后缀最大值就可以计算答案了 ...

  9. Java--这张线程瓜图保熟,不进来看看?这瓜要是不熟它就Dead掉了

    先来看一下线程这张图线程的几种运行状态之间运行流程: 看不懂没关系,慢慢来学习,往下学习来继续了解一下~ 什么是线程? 线程是进程的一部分,是程序执行中的一条执行路线: 进程就是指程序在其自身地址空间 ...

  10. Java线程--CopyOnWrite容器使用

    原创:转载需注明原创地址 https://www.cnblogs.com/fanerwei222/p/11871602.html Java线程--CopyOnWrite容器使用 CopyOnWrit容 ...