C#教程

什么是数据类型

一、基本概念

首先,数据类型应该拆开来理解,分别是数据和类型。我们先讲什么是数据,本质上是计算机内存中的0和1,由于计算机存储的基本单位是字节(byte),最小单位是位(bit),一个节字等于8位,这些都是计算机客观存在的概念。所以,byte在计算机占8个位,bit在计算机中占1个位,由于这两个家伙所占的存储空间长度不一样,所以我们才分别命名为byte和bit,其实,这里的byte和bit就是计算机中两种不同的数据类型。

哦,原来是这样,把占据不同存储空间的数据进行分类,并分别取名,于是就出现了各种各样的数据类型。它其实指的是变量或数据在内存中所占用的空间大小和布局方式,以及该数据可以进行的操作。在计算机编程中,数据类型不仅能够决定程序的正确性和效率,还能够影响程序的可读性和可维护性。

这有点像长度单位,打个不太严谨的比方,把圆珠笔芯的直径叫1毫米,把人的指甲盖的距离叫1厘米,把一个鸡蛋的长度叫1分米,把一根适合的拐杖的长度叫1米。在C#中,占一个bit的数据长度称为bit,占8个bit的数据长度称为byte,占16个bit的数据长度称为短整型short,占32个bit的数据称为整型int,占64个bit的数据长度称为长整型long。

于是,不同的内存存储长度,就可以表示不同的数值范围。理化上讲,存储长度越长,即所占位数越多,所能表示的数值越大。

这就好比作文本,一个格子只能写一个字,一页纸就可以写一篇短文,一本厚厚的作文书就可以写一本小说。

二、数据类型的作用

为什么我们要定义不同的数据类型,从本质上,也就是说,为什么我们在使用内存时, 要将内存分成长短不一的存储空间,然后再给程序员使用?

这和酒店的房间有相似之处。本着节约思想,如果是一个人入住,开个单间就够了,如果是两个人入住,就需要开个标间,如果是三个人入住,有的酒店还会提供三人间,如果是四个人入住,那可以开两个标间,这里单间、标间和三人间就是房间的(数据)类型。如果一个人入住也开个三人间,那就有点浪费床位了。

将内存分成不同长度的存储形式,以便程序员申请内存时,选择适合的存储空间,也是为了节约计算机的资源。比如我们要存储一个人的年龄,人的年龄不可能是一个很大的数值,比如9000岁,那是传说中太监,实际开发中是不存在这种情形的。人的年龄区间一般来说是0-120,也不可能是负数。所以,用一个bit来存储,显示存不下,因为一个bit只能存0或1两个数据,而且计算机也没有将bit做为一种数据类型开放给程序员。用8个bit来存储年龄是否可以呢?我们就要看看这8个bit所能表示的数值范围了,首先是8个0,00000000=0,这是最小的情况(不考虑负数存储),然后是8个1,11111111=255,这是最大的情况,而8个bit在C#中用byte(字节)来表示,说明我们可以申请一个byte类型的内存空间来存储年龄数据,虽然120以上的数值我们也用不上,但这是C#给程序员开放的最小的数据存储类型了,它就好比酒店的单间。

我们是否可以用16个bit空间来存放年龄呢?还是先看看16个bit的数值范围,0000000000000000=0,1111111111111111=65535,在C#中short表示16位的整数,范围是0-65535。由此可见,申请一个short类型的内存空间来存储年龄,属实有点小资。

综上所述,它的作用是:

1.决定变量在内存中所占用的空间大小和布局方式。不同的数据类型在内存中所占用的空间大小是不同的,因此数据类型的选择会直接影响到程序的内存使用效率。

2.决定变量可以进行的操作。不同的数据类型可以进行的操作也是不同的,例如整型可以进行加减乘除等算数运算,而字符型则可以进行字符串拼接等操作。

3.决定程序的正确性和效率。数据类型的正确选择可以保证程序的正确性,而数据类型的选择不当会导致程序出错或者效率低下。

结论

了解计算机的数据类型,正确选择适合的数据类型,是开发高效稳健程序的关键。作为程序员,在选择数据类型时,应从下面几个因素考虑。

首先是数据的范围和精度,按需分配,根据实际需求申请不同存储长度的内存空间,避免不必要的浪费。其次,不同的数据类型在内存使作效率方面也是不同的,要了解每种数据类型的效率,最后,尽量选择简单明了的数据类型,增加程序的可读性和维护性。

我们探讨了数据类型与所占内存的长度关系之后,下一步就要了解.NET框架定义了哪些基础数据类型,这些类型被称为基元类型,它们被定义在FCL框架类库(Framework Class Library)中。由于.NET同时支持C#、F#、VB语言,所以每一种语言也有自己的数据类型,不过,通常它们与.NET框架的基元类型都有一一对应的关系。

——重庆教主 2023年12月13日

一、.NET源代码编译过程

由于Microsoft 在 .NET 平台上提供 3 种语言 – C#、F# 和 Visual Basic,所以, .NET被设计成本质上并不知道所运行的程序代码到底是哪种语言,因为.NET只认识IL语言。IL语言即 Intermediate Language (微软中间语言)。为了说清楚整个事件的来龙去脉,我们以C#源代码为例。

首先,程序员利用Visual Studio开发C#的源代码,然后语言编译器(Language Compiler)将源代码编译为IL中间语言,实际上是一些元数据和中间语言指令,接着在运行这个IL程序时,JIT编译器将根据系统环境将IL中间语言指令转换为机器码。

这里面有一个疑问,为什么不一开始就直接编译成机器码,而是编译成IL中间码?

JIT的全名叫即时编译器,它是在运行时环境中发生的编译行为。不同的CPU或操作系统的环境是不同的,为了实际跨平台运行C#程序,所以只能一开始将源代码编译成IL中间码,等到实际运行时,再根据当前的CPU或操作系统环境将IL中间码由JIT编译成机器代码。

所以,对于.NET的CLR运行时而言,它只认识IL中间码,不在乎这个中间码是C#编译而来,还是由VB编译而来。正是因为 C#、F# 和 Visual Basic都可以开发.NET应用程序,.NET 为了能与多种编程语言代码进行交互,于是它制定了一个通用类型系统 (common type system,CTS),要求所有语言都必须遵守这个约束。

二、通用类型系统 (CTS)

请注意,CTS是一个非常重要的知识点,程序员一定要理解它存在的必要,以及它的职责范围。因为程序员在本质上是申请内存,操作内存数据,归还内存的过程。要申请多大的内存,这个就要看CTS的脸色。CTS将内存分成多种类型,为了节约内存资源,有短一点的只占一个字节长度的内存,也有稍长一点的占4个字节的内存,或者8个字节的内存等等。还是那句话,按需申请。

CTS要面临可能来自C#、F#或VB语言的内存申请要求,所以为了方便统一管理,于是提供了一系列标准的基元类型(Primitive Type),不管是C#程序员,还是VB程序员,在申请内存时,都得按这个基元类型表进行申请。我的地盘我说了算!

除此之外,CTS还拥有其它的功能:

  • 建立用于跨语言执行的框架。
  • 提供面向对象的模型,支持在 .NET 实现上实现各种语言。
  • 定义处理类型时所有语言都必须遵守的一组规则。
  • 提供包含应用程序开发中使用的基本基元数据类型(如 BooleanByteChar 等)的库。

在本节课程中,我们先了解一下CTS提供了哪些基本基元类型(Primitive Type)。因为除了BooleanByteChar属于基元类型,像类、结构、枚举这些也是基元类型,但它们不算基本基元类型。

重庆教主友情提示

其实,我们学习C#语言这门课程时,大多数时候都是在学习CTS的基元类型

三、基本基元类型(Primitive Type)

编译器直接支持的数据类型称为基元类型(primitive type)。基本的基元类型如下表所示:

基元类型名称说明范围
System.SByte有符号8位值
System.Byte无符号8位值
System.Int16有符号16位值
System.UInt16无符号16位值
System.Int32有符号32位值
System.UInt32无符号32位值
System.Int64有符号64位值
System.UInt64无符号64位值
System.Char16位Unicode字符
System.SingleIEEE32位浮点值
System.DoubleIEEE64位浮点值
System.Booleantrue/false值
System.Decimal128位高精度浮点值,常用于不容许舍入误差的金融计算
System.String字符数组
System.Object所有类型的基类型

这些基元类型都定义在System命名空间当中,如果要使用其中的基元类型,则必须指明它的命名空间或者通过using将System命名空间引入到源文件的开头。

虽然.NET提供了这些基元类型,但是,C#、F#、VB也拥有属于自己的基础数据类型,以及声明这些基础数据类型的关键字。比如,在C#中我们用string定义一个字符串,在.NET中用String定义一个字符串,所以C#的基础数据类型基本上与.NET的基础基元类型有着一一对应的关系。

我们在下一节来介绍两者的对比关系。

——重庆教主 2023年12月19日

C#语言在.NET基元类型的基础上,也编制了一份数据类型。所以,将来我们在开发C#程序时,声明基础数据类型,其实就有两种写法。

它们的对应如下表所示。

.NET数据类型C#数据类型说明范围
System.SBytesbyte8 位有符号整数类型-128 到 127
System.Bytebyte8 位无符号整数0 到 255
System.Int16short16 位有符号整数类型-32,768 到 32,767
System.UInt16ushort16 位无符号整数类型0 到 65,535
System.Int32int32 位有符号整数类型-2,147,483,648 到 2,147,483,647
System.UInt32uint32 位无符号整数类型0 到 4,294,967,295
System.Int64long64 位有符号整数类型-9,223,372,036,854,775,808 到 9,223,372,036,854,775,807
System.UInt64ulong64 位无符号整数类型0 到 18,446,744,073,709,551,615
System.Charchar16 位 Unicode 字符U +0000 到 U +ffff
System.Singlefloat32 位单精度浮点型-3.4 x 1038 到 + 3.4 x 1038
System.Doubledouble64 位双精度浮点型(+/-)5.0 x 10-324 到 (+/-)1.7 x 10308
System.Booleanbool布尔值True 或 False
System.Decimaldecimal128 位精确的十进制值,28-29 有效位数
(-7.9 x 1028 到 7.9 x 1028) / 100 到 28
System.Stringstring字符串按MSND文档,String类的Length属性的类型为int。而int的最大值为2147483647。所以string的最大长度为2147483647(2,147,483,647)。
System.Objectobject引用类型

.NET下的基元类型(Primitive Type)一共有14个。从占用字节数的长度来区分:长度(字节数)分别为1、2、4、8的有/无符号的整数;外加两个基于指针宽度(x86=4; x64=8)的整数,计10个。长度(字节数)分别为4和8的单精度和双精度浮点数,计2个。外加布尔类型和字符类型, 计2个。System.String(string)、System.Decimal(decimal)、System.Object(object)并不是基元类型。

  • 整数(10):
  • System.Byte(byte)和System.SByte(sbyte),
  • System.Int16(short)和System.UInt16(ushort),
  • System.Int32(int)和System.UInt32(uint),
  • System.Int64(long)和System.UInt64(ulong),
  • System.IntPtr(nint)/System.UIntPtr(nuint)
  • 浮点(2):System.Single(float),和System.Double(double)
  • 布尔(1):System.Boolean(bool)
  • 字符(1):System.Char(char)

这些种类繁多的数据类型,在CTS(公共类型系统)中又分为引用类型和值类型。下一节,我们将介绍引用类型与值类型的概念与区别。

重庆教主 2023年12月20日

一、内存分配

了解内存分配是计算机编程的基础。内存提供了存储数据和程序高效运行所需的所有命令的空间,程序本质上就是数据+指令,两者都需要分配内存空间。就好比菜板和菜刀是指令,蔬菜是数据,厨房就是计算机的内存,我们要进行“切菜”这个程序,厨具和蔬菜肯定都在厨房里面占用一定的空间才行。要是厨房里面连厨具都没有,那还切什么菜呢,只能是歇菜了。

计算机的内存可以分为以下几个部分:全局段(Global segment)、代码段(Code segment)、堆栈(Stack)、堆(Heap)。

  • 全局段:负责存储全局变量和静态变量,这些变量的生命周期等于程序执行的整个持续时间。
  • 代码段:也称为文本段,包含组成我们程序的实际机器代码或指令,包括函数和方法。
  • 堆栈段:用于管理局部变量、函数参数和控制信息(例如返回地址)。
  • 堆段:提供了一个灵活的区域来存储大型数据结构和具有动态生命周期的对象。堆内存可以在程序执行期间分配或释放。

二、栈内存(Stack)

每个程序都有自己的虚拟内存布局,由操作系统映射到物理内存。栈内存是为线程留出的临时空间,每个线程都有一个固定大小的栈空间,而且栈空间存储的数据只能由当前线程访问,所以它是线程安全的。栈空间的分配和回收是由系统来做的,我们不需要手动控制。

栈内存用于存放函数的参数值、局部变量等,其操作方式类似于数据结构中的栈。当一个函数调用时,系统就会为该函数的调用分配栈空间,当函数返回后,系统就会自动回收这块空间,同理,下次其它函数调用和返回,系统还是会自动分配和回收空间。

栈内存通常存储程序中的值类型数据,因为它是按顺序排序的,所以访问速度非常快,比堆内存的访问速度快。栈内存中的数据随着方法的结束而自动释放,程序员不必担心内存泄露的问题。

什么是内存泄露

内存泄漏(Memory Leak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

三、堆内存(Heap)

堆内存,也称为动态内存。堆内存允许我们在程序执行期间随时分配和释放内存。它非常适合存储大型数据结构或大小事先未知的对象。比如在C#中使用new关键字实例化的对象就会存储在堆内存上。堆内存让程序员又爱又恨,爱的是它非常灵活,可以动态申请任意长度的内容空间。这与栈内存不同,比如我们在栈内存上申请了一个byte数据,它的长度就只有8位,固定得死死的,而我们new一个对象,这时会在堆上开辟内存空间,空间长度随着这个对象的长度而定,富得流油的感觉。

当然,它也有缺点啦,第一是容易产生内存泄露,第二是容易产生内存碎片,第三是线程不安全。

可见,堆内存和栈内存都有自己的优势与不足,作为程序员,要非常了解它们的特点,尽量发挥它们的长处,避免它们的短处。

比较点
速度
空间管理高效,不会产生碎片会产生内存碎片
访问权限只能局部变量可以访问全局变量
空间大小限制操作系统限制没有特定的限制
内存分配连续随机分配
分配和释放编译器指令自动管理程序员手动管理
开销
主要问题空间小内存碎片
灵活性固定大小可改变

四、引用类型与值类型

在前面我们学习了.NET的公共类型系统(Common Type System,CTS),它提供了一系列的数据类型,在这些类型中,有些是基础的数据类型,例如BooleanByteChar等,有些则是比较复杂的数据类型,例如类、结构、枚举、界面、委托。不同的数据类型,本质上是对计算机内存的长度的一种别称。比如程序员要临时处理一个人的年龄数据,这个我们在前面也讨论过,人的年龄大概范围就是0-120之间,很少使用小数点的情况,所以,申请一个byte(字节)大小的内存空间就足够了,因为byte表示0-255的整数。事实上我们通常使用int(整型)去定义一个变量来保存年龄数据,int长度为32位,可表示的数值范围是-2,147,483,648 到 2,147,483,647。我们要保存一个人的名字时,由于名字是一个字符串,所以就不能用byte、int之类的数据类型,而应该采用string去声明一个变量,这个string也是向计算机内存开辟一个空间,最后将名字写到这个内存空间上。

由于计算机拥有四种内存:全局段(Global segment)、代码段(Code segment)、堆栈(Stack)、堆(Heap),那么,哪些数据类型会在堆(Heap)上开辟,哪些数据类型又在栈(栈)上开辟呢?.NET将数据类型分成两种,分别是值类型(Value Type)和引用类型(Reference Type)。值类型的数据都会在栈内存上开辟,引用类型的数据会在堆内存上开辟。但是,引用类型变量在堆内存中的首地址会保存在栈内存中,也就是说,引用类型变量的实际数据存储于托管堆,变量本身仅仅是一个指向堆中实际数据的地址。那么从这里开始,一个神奇的效果就产生了,那就是,如果我们将一个值类型的变量赋值给另一个值类型的变量,会在内存中产生两份相同的数据,两份数据各自拥有自己的内存地址;如果我们将一个引用类型变量a赋值给另一个引用类型变量b,由于a只是代表这个引用类型数据在堆内存上的地址,所以b也只是保存了这个地址,换句话说,a和b共同指向这个堆内存上的地址,此时依旧只有一份引用类型数据(实例)。

这就是值类型的值传递和引用类型的引用传递的区别!

最后,我们来了解C#的数据类型分类,哪些是引用类型,哪些是值类型。

——重庆教主 2023年12月21日

想必大家在使用计算机时都知道可以同时打开多个软件,比如Word、Visual Studio、QQ音乐。通常在办公的时候或者程序员在编程的时候,一边开发软件,一边听着歌曲。其实,这是操作系统为这三款不同的程序开辟了彼此独立的内存,以保证它们的良好运行。每一个程序都代表一个进程(Process)。进程中的一个执行任务(控制单元),负责当前进程中程序的执行。一个进程至少有一个线程(Thread),一个进程可以运行多个线程,多个线程可共享数据。

笔者工作之地是深圳某某科技园区,里面入驻了上百家企业。如果把这个科技园区看成是一台计算机的话,那么科技园区的所属管理单位就是操作系统,里面的每家企业就是一个程序,也就是一个进程,而每个企业都可能有研发部、销售部、财务部、生产部等等,那么这些部门就是线程。每个部门都有自己的小金库——团队建设费。我们可以把每个线程的小金库理解成线程堆栈(Stack Memory),这部分内存是每个部门的自留地,私有财产神圣不可侵犯,所以线程堆栈只为线程服务,用于保存自身的一些数据,如函数中定义的局部变量、函数调用时传送的参数值等。所以值类型的数据都会保存在线程堆栈中,换句话说,程序员申请的所有值类型都会在线程堆栈中得到分配。

有时候,部门和部门之间也是需要共享数据的,比如我们部门经常要借用生产部的设备材料。那这些设备材料就不能放在小金库里面了,于是,它们必须要放在大家都看得见够得着的地方——堆内存(Heap Memory)。在.NET这种托管环境下,堆由CLR(Common Language Runtime)管理,所以又称托管堆Managed Heap。例如使用new关键字创建类的对象实例时,分配给对象的内存单元就位于托管堆中。

我们在这里讲解进程和线程的概念,是因为将来开发程序时,往往要在一个程序中创建多个线程。C#程序拥有一个主线程,通常指UI线程,但是我们不能把所有代码都写在主线程中。如果某个执行单元需要耗费大量的时间,这时我们可以把这个执行单元看成是一个任务,然后创建一个子线程,在子线程中去执行这个任务。这样做的好处是,UI线程不会出现卡顿的情况,它会继续响应用户的键盘或鼠标操作,而背后的子线程同时也执行着我们需要处理的业务需求。

我们把开发拥有多个线程的程序称为多线程开发。在多线程开发中,往往需要访问同一片内存区域,比如对某个对象进行读写操作,由于堆内存上的数据可以共享,所以往往有多个线程在同时写堆内存上的数据时,会出现资源抢夺。意思是说,当两个线程在同一时钟周期尝试写入同一个内存地址时,可能会发生竞态条件(Race Condition)。

竞态条件是指多个线程或进程同时访问共享资源,并且最终的结果依赖于它们执行的相对速度或顺序。假如线程1和线程2同时写入某个内存地址,结果是不确定的:这可能导致内存中保存的是线程1和线程2的写入值的组合,或者是其中一个线程的写入值。这种竞态条件可能导致程序的行为不一致、数据损坏或其他错误。为了避免竞态条件,可以使用同步机制,如互斥锁(Mutex)或信号量(Semaphore),来确保在同一时间只有一个线程可以访问共享资源。这样可以保证线程按照正确的顺序进行写入操作,避免竞态条件的发生。

在前面的章节中,我们学习了数据类型、进程、线程等概念,也熟悉了不同数据类型,本质上是指不同长度的内存空间。由于我们向计算机申请一段内存,它只会返回这个内存的地址,而这个地址是二进制形式(实际是以16进制显示),只有计算机能秒懂,人类是看不懂的。针对32位CPU,用16进制表示线性地址,其取值范围为0x00000000~0xffffffff。64位CPU,其取值范围为0x0000000000000000~0xffffffffffffffff。假设我们在64位操作系统中申请一个byte类型的内存空间,这时计算机在0x00004578ABCD0001处开辟好了,我们要用这个地址去做数据处理是极其不便的,于是,我们设计了一个“变量”的概念,利用一个标识符去代指这段内存地址。

如下所示

byte v = 0;

这里的v表示变量名,v指向的内存地址就是0x00004578ABCD0001,当我们给v赋值为1时,实际上就是地址为0x00004578ABCD0001的内存数据被改为1。

v=1;

所以,我们创造性地用字母v代表了内存地址0x00004578ABCD0001。将来,对变量v进行各种操作运算,实际上就是对内存地址0x00004578ABCD0001进行了各种操作运算。

这就是变量的由来。

变量的命名规则

  • 只能以字母、数字、下划线和@符号组成
  • 只能以字母、下划线或 @ 符号开头
  • 数字可以放在中间或结尾
  • @符号不能放在中间或结尾

注意事项

  • 变量名不能与C#关键字重复
  • 大小写敏感,比如变量名age和Age是两个不同的变量
  • 变量名不能重复定义

变量的声明和初始化

声明和初始化一个变量,就是向计算机申请开辟内存的过程。声明表示我们要开辟多长的内存空间,初始化表示真正执行开辟动作。这就好比向房管局递交设计图纸,这只能表明要建多大的房子,房管局最终指定可以在哪儿建,这时才可以打地基,真正开始建房。

变量的声明和变量的初始化实际上是两个步骤,不过在编程时写成一句代码。下面是它的格式

关键字 变量名 = 初始化值

例如上面的声明代码

byte v = 0;

在这个示例中,byte表示关键字,v表示变量名,并且在声明这个变量时,给它初始化了一个0。下一节,我们介绍C#的byte类型。

——重庆教主 2023年12月21日

byte是我们学习的第一个数据类型,表示一个8位无符号整数,也表示一个字节。在计算机中,一个字节的长度是8位(bit)。byte类型的范围是0~255,转换为二进制是00000000~11111111。

C#中byte对应的.NET数据类型是System.Byte。

声明和初始化

我们可以使用.NET提供的关键字声明一个Byte变量,也可以使用C#关键字声明一个byte变量,两者在内存上的长度和特性是一致的。例如:

byte age = 18;
Byte money = 18;

byte的范围被定义在0-255当中,我们可以从它的源代码中找到这一点。或者直接输出范围的最小值和最大值。

public const byte MaxValue = 255;
public const byte MinValue = 0;
internal class Program
{
    static void Main(string[] args)
    {
        byte age = 18;
        Byte money = 99;

        Console.WriteLine($"age={age}");
        Console.WriteLine($"money={money}");
        Console.WriteLine($"MinValue ={byte.MinValue}");
        Console.WriteLine($"MaxValue ={byte.MaxValue}");

        var v = Console.ReadKey();
    }
}

输出如下:

age=18
money=99
MinValue =0
MaxValue =255

我们已知byte能存储的数值范围是0-255,那么,假如我们给它赋值-1,0,255,256这4个数值,然后将它们打印出来,结果又如何呢?

当我们给变量age赋值为-1时,提示常量值-1无法转换为byte。同理,给age赋值256时,由于超过了255的限制,提示常量值256无法转换为byte。

说明编译器至少在程序员编码时,做了一定的错误限制。别急,我们还有别的办法可以实现这一测试。

short a = -1;
age = (byte)a;
Console.WriteLine($"将-1转为byte={age}");

short b = 256;
age = (byte)b;
Console.WriteLine($"将256转为byte={age}");

short c = 257;
age = (byte)c;
Console.WriteLine($"将257转为byte={age}");

因为short型的数据可以表示的范围是-32768到32767,所以我们可以先定义一个short变量,然后通过类型转换,将其转换成byte型,最后结果如下

将-1转为byte=255
将256转为byte=0
将257转为byte=1

我们先打个比方,byte就好比星巴克的小杯,比如可以容纳100毫升的咖啡,short就好比大杯,可以容纳500毫升的咖啡,现在大杯装有150毫升的咖啡,我们强制全部倒进小杯中,当这个过程结束后,小杯中的咖啡有多少毫升?

在现实中,此时小杯里面有100毫升的咖啡,还溢出了50毫升,散落在桌上。而在计算机当中,答案却相反,将一个256的数值存到byte里面,得到的答案是0,将257的数值装到byte里面,得到的答案是1。这是为什么呢?

short在内存中占2个字节,byte在内存中只占1个字节,将大杯里面的咖啡倒进小杯里面,这会造成精度损失,或者数据丢失。计算机在处理这种情况时,会通过一个计算公式,如上面这个例子:256%256=0,所以将256赋值给byte时,得到结果为0,257%256=1,所以得到结果为1。

也就是说short和byte求模运算(白话:short除以byte得到余数),将这个求模运算的余数结果保存到byte当中。由此我们可以得如下推论:在下列等式中,左边是short型,右边是byte型。

  • ...
  • -3 = 253
  • -2 = 254
  • -1 = 255
  • 0 = 0
  • 1 = 1
  • 2 = 2
  • 3 = 3
  • 253 = 253
  • 254 = 254
  • 255 = 255
  • 256 = 0
  • 257 = 1
  • 258 = 2
  • 259 = 3
  • ...

怎么去快速理解它们之间的转换关系呢?

等式换算关系换算关系
-3 = 253-3 % 256 =253-3=(-1)*256+253
-2 = 254-2 % 256 =254-2=(-1)*256+254
-1 = 255-1 % 256 =255-1=(-1)*256+255
0 = 00 % 256 =00=(+0)*256+0
1 = 11 % 256 =11=(+0)*256+1
2 = 22 % 256 =22=(+0)*256+2
3 = 33 % 256 =33=(+0)*256+3
253 = 253253 % 256 =253253=(+0)*256+253
254 = 254254 % 256 =254254=(+0)*256+254
255 = 255255 % 256 =255255=(+0)*256+255
256 = 0256 % 256 =0256=(+1)*256+0
257 = 1257 % 256 =1257=(+1)*256+1
258 = 2258 % 256 =2258=(+1)*256+2
259 = 3259 % 256 =3259=(+1)*256+3

当前课程源码下载:(注明:本站所有源代码请按标题搜索)

文件名:002-《byte数据类型》源代码
链接:https://pan.baidu.com/s/1Bq2lX7cruUbklLwgjGggSw
提取码:byte

——重庆教主 2023年12月25日

sbyte表示一种整型数据,它的范围是-128到+127,它是一个有符号的8位整数,在.NET中与之对应的是System.Sbyte。

C#中的byte类型的范围是0-255。它们两者都只能表示256个数值,只不过,一个有符号,一个没有符号。为什么有符号的sbyte的范围是-128到+127呢?

首先8位的二进制数据的范围本来应该是00000000-11111111,然后我们分两种情况来讨论。

第一种情况,在无符号的byte类型二进制数据中,直接将00000000视为0(十进制),将11111111视为255(十进制)。

第二种情况,在有符号的sbyte类型中,左边第一位表示符号位,0表示正数,1表示负数,所以01111111表示一个sbyte最大的正数,也就是127。而负数的二进制在计算机中表示为正数的补码,补码等于反码加1,所以128的反码用二进制表示是01111111,再加1得到补码为10000000。注意,10000000在无符号二进制状态下,它本来是128,而在有符号数中,通常将最高有效位为0的认为是正数,最高有效位为1的认为是负数,因此1000 0000就表示为-128了。所以8位有符号整数的范围是-128~127

观察下面这个例子

internal class Program
{
    static void Main(string[] args)
    {
        sbyte min_sbyte = -128;
        SByte max_sbyte = 127;

        Console.WriteLine($"min_sbyte={min_sbyte}");
        Console.WriteLine($"max_sbyte={max_sbyte}");
        Console.WriteLine($"MinValue ={sbyte.MinValue}");
        Console.WriteLine($"MaxValue ={sbyte.MaxValue}");

        short a = -129;
        sbyte age = (sbyte)a;
        Console.WriteLine($"将-129转为sbyte={age}");

        short b = +128;
        age = (sbyte)b;
        Console.WriteLine($"将+128转为sbyte={age}");            

        var v = Console.ReadKey();
    }
}

输出结果:

min_sbyte=-128
max_sbyte=127
MinValue =-128
MaxValue =127
将-129转为sbyte=127
将+128转为sbyte=-128

为什么将-129转为sbyte=127?将+128转为sbyte=-128?这里和上一节关于byte的超出范围转换是一样的道理。

比如+128这个数,二进制表示为10000000,由于第一位表示为符号位,所以10000000=-0,-0就看成是sbyte的最小值,可不就是-128嘛。

当前课程源码下载:(注明:本站所有源代码请按标题搜索)

文件名:003-《sbyte数据类型》源代码
链接:https://pan.baidu.com/s/1Bq2lX7cruUbklLwgjGggSw
提取码:byte

——重庆教主 2023年12月25日

bool类型的值只有两种,分别是true和false。它对应.NET中的System.Boolean。在这个值类型的内部,其实定义了两个int类型的值分别表示true和false,另外,还定义了它的字符串形式。

internal const int True = 1;

internal const int False = 0;

[__DynamicallyInvokable]
public static readonly string TrueString = "True";

[__DynamicallyInvokable]
public static readonly string FalseString = "False";

声明和初始化

bool state = true;

将state变量声明为bool型,同时初始化一个true。

虽然0表示false,1表示true,但是我们不能直接将0或1赋值给一个bool类型的变量,它会报下面的错误,例如,我们定义如下:bool b = 1;

错误提示

错误 CS0029 无法将类型“int”隐式转换为“bool”

也并非不能实现int类型与bool类型的转换。参考如下的例子:

internal class Program
{
    static void Main(string[] args)
    {
        bool state = true;

        Console.WriteLine($"state={state}");
        Console.WriteLine($"FalseString={bool.FalseString}");
        Console.WriteLine($"TrueString ={bool.TrueString}");

        int a = 0;
        bool b = Convert.ToBoolean(a);
        Console.WriteLine($"将int类型的{a}转化为bool类型的{b}");

        a = 1;
        b = Convert.ToBoolean(a);
        Console.WriteLine($"将int类型的{a}转化为bool类型的{b}");

        a = 2;
        b = Convert.ToBoolean(a);
        Console.WriteLine($"将int类型的{a}转化为bool类型的{b}");

        a = -1;
        b = Convert.ToBoolean(a);
        Console.WriteLine($"将int类型的{a}转化为bool类型的{b}");

        var v = Console.ReadKey();
    }
}

输出如下:

state=True
FalseString=False
TrueString =True
将int类型的0转化为bool类型的False
将int类型的1转化为bool类型的True
将int类型的2转化为bool类型的True
将int类型的-1转化为bool类型的True

我们可以利用Convert.ToBoolean()函数实现int向bool的转换,并且得出结论,0表示false,非0表示true。bool类型的变量通常用在判断语句中,根据不同的状态执行不同的业务流程。

当前课程源码下载:(注明:本站所有源代码请按标题搜索)

文件名:004-《bool数据类型》源代码
链接:https://pan.baidu.com/s/1Bq2lX7cruUbklLwgjGggSw
提取码:byte

——重庆教主 2023年12月25日

C#中的char类型对应.NET中的System.Char,它表示 Unicode UTF-16 字符。要了解char,我们必须先了解什么是Unicode UTF-16 。

先说说ASCII码,自第一台计算机诞生于美国后,人们将英语大小写字母、数字、常用符号与二进制位之间的关系做了统一规定,并制定了一套字符编码规则,这套编码规则被称为ASCII编码。ASCII 编码一共定义了128个字符的编码规则,用七位二进制表示 ( 0x00 - 0x7F ),这些字符组成的集合就叫做 ASCII 字符集。

后来,各个国家都有一些类似的符号编码集,为了统一,就制定了Unicode编码。

Unicode叫统一码,统一码是国际组织制定的可以容纳世界上所有文字和符号的字符编码方案。UTF是Unicode Transfer Format的缩写,即把Unicode转做某种格式的意思。UTF-16是Unicode的其中一个使用方式。它表示的范围U+0000 到 U+FFFF,也就是说,可以容纳65535个字符,这几乎将地球上的各种字符一网打尽了,而这也是C#中的char表示的范围。char 类型的默认值为 \0,即 U+0000。

字符和字符串的区别

char只能表示一个字符,而多个char就可以组成一个字符串,在C#中,使用string关键字声明一个字符串,它们两者的关系是:字符串类型将文本表示为 char 值的序列。

声明和初始化

char code1 = 'a';//字符文本

声明并初始化一个char有4种方法,常用的是直接给出字符文本或使用转义字符。观察下面的例子

internal class Program
{
    static void Main(string[] args)
    {
        int number = 97;
        char code1 = 'a';//字符文本
        char code2 = '\x0061';//16进制
        char code3 = '\u0061';//16进制
        char code4 = (char)number;//10进制
        char min = char.MinValue;
        char max = char.MaxValue;
        Console.WriteLine($"code1={code1}");
        Console.WriteLine($"code2={code2}");
        Console.WriteLine($"code3={code3}");
        Console.WriteLine($"code4={code4}");
        Console.WriteLine($"MinValue=[{min}]");
        Console.WriteLine($"MaxValue=[{max}]");
        Console.WriteLine($"将int类型的{number}转化为char类型的{code4}");  

        var v = Console.ReadKey();
    }

输出结果

code1=a
code2=a
code3=a
code4=a
MinValue=[ ]
MaxValue=[?]
将int类型的97转化为char类型的a

结论

char由于可以表示的范围是0x0000-0xffff这么多的字节,即0-65535的范围,说明它本质上也是一个ushort型,占2个字节。它的第一字符为'\0',表示一个空格,最后一个字符为'\uffff',表示?(问号)。设计这个编码的人挺可爱,以空格开头,就像有生于无,而以一个大大的问号结尾,代表对计算机的无尽探索。

题外话:在C或C++语言中,char只占1个字节,所以它只能表达256个字符。

当前课程源码下载:(注明:本站所有源代码请按标题搜索)
文件名:005-《char数据类型》源代码
链接:https://pan.baidu.com/s/1Bq2lX7cruUbklLwgjGggSw
提取码:byte

——重庆教主 2023年12月25日

copyright @重庆教主 C#中文网 联系站长:(QQ)23611316 (微信)movieclip (QQ群).NET小白课堂:864486030 | 本文由C#中文网原创发布,谢绝转载 渝ICP备2023009518号-1