微软.NET6开发的C#特性——类、结构体和联合体
我是荔园微风,作为一名在IT界整整25年的老兵,看到不少初学者在学习编程语言的过程中如此的痛苦,我决定做点什么,下面我就重点讲讲微软.NET6开发人员需要知道的C#特性,然后比较其他各种语言进行认识。
C#经历了多年发展, 进行了多次重大创新, 大幅优化了开发者的编码体验。在.NET 平台移交给.NET基金会运营后, C#更新的越来越不像原来的C#了,但总体上来说,所有改进依然以优化开发者的编码体验为最终目的。
首先,要记住一张表,如下:
C#版本 发布时间 .NET版本 VS版本 CLR版本
C#1.0 2002-2 .NET Framework 1.0 VS.NET 2002 .NET Framework CLR 1.0
C#2.0 2005-11 .NET Framework 2.0 VS2005 .NET Framework CLR 2.0
C#3.0 2006-11 .NET Framework 3.0 VS2008 .NET Framework CLR 2.0
C#3.0 2007-11 .NET Framework 3.5 VS2008 .NET Framework CLR 2.0
C#4.0 2010-4 .NET Framework 4.0 VS2010 .NET Framework CLR 4.0
C#5.0 2012-2 .NET Framework 4.5 VS2012 .NET Framework CLR 4.0
C#6.0 2015-7 .NET Framework 4.6 VS2015 .NET Framework CLR 4.0
C#7.0 2016-8 .NET Framework 4.6.2 VS2017(v15) .NET Framework CLR 4.0
C#7.1 2017-4 .NET Framework 4.7 VS2017(v15.3) .NET Framework CLR 4.0
C#7.2 2017-10 .NET Framework 4.7.1 VS2017(v15.5) .NET Framework CLR 4.0
C#7.3 2018-4 .NET Framework 4.7.2 VS2017(v15.8) .NET Framework CLR 4.0
C#8.0 2019-4 .NET Framework 4.8 VS2019(v16.3) .NET Framework CLR 4.0
C#8.0 2019-9 .NETCore 3.0 VS2019(v16.4) .NETCore CLR 3.0
C#9.0 2020-11 .NET 5.0 VS2019(v16.8) .NET CLR 5.0
C#10.0 2021-11 .NET 6.0 VS2022(v17) .NET CLR 6.0
看完这张表,我真的是很感慨,从测试版开始,我居然陪伴着.NET和C#走过了二十多年,我不知道有没有微软公司的人在看这篇文章,如果有的话,不知道我这样的二十多年的.NET和C#程序员有没有机会去微软中国和微软亚洲研究院的总部去参观一下,去坐一坐,并作一下技术交流。二十多年了,人生又有几个二十多年啊。
.NET平台是基于IL中间语言的应用运行环境,面向对象语言C#是平台的主要开发语言。除此之外还有同样面向对象的C++/CLI。C++/CLI主要用于和原生C++交互,在.NET平台中仅支持Windows系统。
C#和.NET平台本来是微软为了与Java平台竞争而打造的,C#在设计时充分总结了Java的经验教训,解决了大量Java的基本设计缺陷。本着为一线开发者谋实惠的宗旨,C#设计了大量能减轻开发者的编写负担、容易理解且安全高效的实用功能。为了尽可能降低因安全措施导致性能大幅下降的影响,C#还在有限的情况下保留了C/C++语言的部分语法和功能。到了.NET时代,微软依然在运行时(Runtime)和语言两边同时进行着优化。
随着上世纪九十年代Java的发布,软件公司和开发者开始感受到基于虚拟机的托管语言所带来的好处,微软也不甘示弱,在2001年发布了.NET Framework平台和C#。提供了完整的基础面向对象支持。
类、结构体和联合体
类和结构体是从C/C++继承的功能,结构体从C语言开始就作为供开发者自定义数据结构的基本功能出现,在C++中升级为了面向对象的类。C++的类和结构体并没有明确的概念和功能上的差别,Java删除了结构体这个概念,只保留了类。微软发现了能够有效利用这两个概念的方法,因此C#保留了类和结构体。
Java和C#都有一套完善的类型系统,所有的类型都是直接或间接地由 Object派生而来。但不知为何Java有基元类型。基元类型基本代表了在C/C++中由编译器和CPU直接支持的原始类型,而这些类型却不属于类型系统。为了解决这个问题,Java又设计了一套包装类型。这带来一个问题,类型系统是Java的根基,但作为其中的基石的基元类型居然和类型系统不兼容,这也在后来为Java带来了更多的麻烦。
C#却巧妙地利用了类和结构体完成了没有内生矛盾的类型系统,一切类型都是Object的后代,包括基本数据类型。在C#的设计中,基本类型的继承路径是 System.Object→System.ValueType→各种基本类型。
ValueType禁止使用常规语法继承,并且其子类是强制封闭的,禁止继续继承。这时结构体就派上用场了,结构体就是隐式继承了ValueType的封闭类型,基本数据类型就是由.NET预定义的结构体。结构体是直接在线程栈上分配的免回收类型,拥有极高的性能。也因为在栈上的分配必须静态确定其占用的内存空间并直接分配,因此结构体禁止赋值为null。
由于结构体的复制策略是深拷贝,因此在方法之间作为参数传递时传递的是完整的独立副本,互不影响,和普通类的引用拷贝形成了鲜明的对比。为了保持和Object的完整兼容性,.NET还特地为结构体准备了自动装箱和拆箱。装箱即是指在显式或隐式转换为Object类型的时候运行时自动在托管堆上分配对象内存并把值复制到对象中;拆箱即是指在显式或隐式转换回原类型时自动在线程栈上分配内存并把值从托管堆复制到栈上。托管堆中的对象占用的内存一般比线程栈上的大,因为堆对象占用的内存除了基本值所需内存之外还包含类型对象指针和同步块索引(C#的lock同步锁语句块就是依靠同步块索引实现的)等额外信息,而栈上的结构体实例只占用基本值所需的内存。经过这些周密的设计,C#拥有了完美自治的类型系统,类和结构体也拥有了明显的功能区分。
在C++中有一个被称为友元类的功能,友元类之间允许相互访问对方的私有成员,这在一定程度上破坏了类的封装性,因此Java和C#都删除了这个功能。不过C#却有一个被称为友元程序集的功能,友元程序集的类之间允许互相访问对方的内部(internal)成员,这在编写单元测试时经常用到。由于C#和Java都是托管语言,因此都可以通过反射彻底绕过成员访问保护机制,C/C++也可以通过万能的指针绕过编译器保护。
C#、C、C++、Java的类和结构体的代码如下。
(1)C#
//结构体
public struct Point2D
{
public double x;
public double y;
}
//类
public class Point3D
{
public double x;
public double y;
public double z;
}
//抽象类
public abstract class MyClass
{
public abstract void Method1();
public virtual void Method2() {}
}
(2)C
typedef struct {
double x;
double y;
} Point 2D;
(3)C++
① Point3D.h
#pragma once
class Point3D
{
public:
double x;
double y;
double z;
};
class MyClass
{
public:
virtual void Method1() =0;
virtual void Method2();
};
② Point3D.cpp
#include"Point3D.h"
void MyClass::Method2() {}
(4) Java
public class Point3D {
public double x;
public double y;
public double z;
}
public abstract class MyClass{
public abstract void method1();
public void method2() {}
}
在C语言中有一种独特的数据结构叫作联合体,它的特点是所有数据成员共享内存空间,并且同一时刻最多只有一个成员处于可用状态。这种数据类型的诞生主要是因为C语言刚面世的时候计算机的内存还比较小, 需要尽量节约使用内存。C#和Java诞生的年代内存不再紧缺, 因此并不支持这种数据类型。但是C#为了兼容和C语言的互操作, 从.NET Framework 1.1开始支持通过特殊方式模拟联合体。
C#在模拟联合体时,如果其中有类类型的成员,可能在运行时引发异常,因此在模拟联合体时一般只用结构体类型的成员,当然,虽然读取非激活状态的成员不会引发异常,但是仍然可能读取到错误的值。在C语言的联合体中通常也只使用基本数据类型。
C的联合体和C#的模拟联合体示例代码分别如下所示。
(1) C
typedef union {
int x;
float y;
double z;
} MyUnion;
(2)C#
using System. Runtime.InteropServices;
namespace Example
{
//手动定义结构体布局,占用8字节空间
[StructLayout (LayoutKind. Explicit, Size =8)]
public struct MyUnion
{
//字段偏移量为0,实际占用4字节,剩下4字节不使用
[Fieldoffset(0)]
public int x;
//字段偏移量为0,实际占用4字节,剩下4字节不使用
[Fieldoffset(0)]
public float y;
//字段偏移量为0,刚好用完8个字节
[Fieldoffset (0)]
public double z;
}
}
C#使用结构体模拟联合体时,StructLayout特性可以告知运行时(Runtime)开发者要手动定义结构体的内存布局,其中字段的FieldOffset特性告知运行时这个字段在对象中的内存偏移量,示例中全部指定为0就表示所有成员共享相同的内存空间。上面代码中的各个字段所需的内存空间不尽相同,因此当需求内存较小的成员处于激活状态时,多余的内存会处于空闲状态。
作者简介:荔园微风,1981年生,高级工程师,浙大工学硕士,软件工程项目主管,做过程序员、软件设计师、系统架构师,早期的Windows程序员,Visual Studio忠实用户,C/C++使用者,是一位在计算机界学习、拼搏、奋斗了25年的老将,经历了UNIX时代、桌面WIN32时代、Web应用时代、云计算时代、手机安卓时代、大数据时代、ICT时代、AI深度学习时代、智能机器时代,我不知道未来还会有什么时代,只记得这一路走来,充满着艰辛与收获,愿同大家一起走下去,充满希望的走下去。