值类型和引用类型
值类型
C# 中的值类型(Value Types)是一种数据类型,它们的值被直接存储在变量或对象中,而不是存储在堆上并通过引用进行访问。与之相对的是引用类型(Reference Types),它们的值存储在堆上,并且变量或对象只存储一个指向堆上实际数据的引用。
C# 中的值类型包括以下几种:
- 整数类型(Integer Types):包括
sbyte、byte、short、ushort、int、uint、long和ulong等。它们用于存储整数值。 - 浮点数类型(Floating-point Types):包括
float和double等。它们用于存储浮点数值。 - 十进制类型(Decimal Type):用于存储高精度的小数值。
- 布尔类型(Boolean Type):用于存储布尔值(
true或false)。 - 字符类型(Character Type):包括
char类型。用于存储单个字符。 - 枚举类型(Enumeration Types):用于存储一组常量值。
- 结构体类型(Struct Types):用于创建自定义的值类型,可以包含多个字段和方法。
- 可空类型(Nullable Types): 允许为值类型赋值为
null。它提供了一种表示值类型缺失或未定义状态的方式。
值类型有以下特点:
- 存储在堆栈中,访问速度较快。
- 由于值类型的变量直接包含数据,因此它们在传递给方法时,是按值传递的(pass by value),即会在方法内复制一份变量的值。
- 值类型的变量的生命周期与其所属的代码块相同,当代码块执行结束后,变量将被销毁。
- 值类型的变量可以通过在其前面添加
ref关键字或者out关键字来传递给方法,以便在方法内修改变量的值。 - 值类型的变量默认情况下是不能为
null的,但可以使用可空类型来使其可以为null。
整数类型 Integer Types
sbyte:有符号的 8 位整数类型,取值范围为 -128 到 127。默认值为 0。byte:无符号的 8 位整数类型,取值范围为 0 到 255。默认值为 0。short:有符号的 16 位整数类型,取值范围为 -32,768 到 32,767。默认值为 0。ushort:无符号的 16 位整数类型,取值范围为 0 到 65,535。默认值为 0。int:有符号的 32 位整数类型,取值范围为 -2,147,483,648 到 2,147,483,647。默认值为 0。uint:无符号的 32 位整数类型,取值范围为 0 到 4,294,967,295。默认值为 0。long:有符号的 64 位整数类型,取值范围为 -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807。默认值为 0。默认值为 0。ulong:无符号的 64 位整数类型,取值范围为 0 到 18,446,744,073,709,551,615。默认值为 0。
在 C# 中,整数类型的默认类型是 int。如果需要声明其他类型的整数变量,需要使用特定的后缀来指定变量的类型。
使用 L 后缀表示
long类型使用 U 后缀表示
uint类型使用 UL 后缀表示
ulong类型csharpint x = 100; long y = 123456789L; uint z = 4294967295U;在进行整数类型的运算时,如果操作数类型不同,则会发生类型转换。
C# 中的整数类型转换遵循一定的规则,其中较小的类型会被自动转换为较大的类型,而较大的类型需要显式转换为较小的类型。
如果进行类型转换时发生数据溢出,则会发生异常。
csharpint x = 100; long y = x; // 隐式转换 int z = (int)y; // 显式转换 byte b = (byte)x; // 强制转换,可能发生数据溢出
注意
C# 中的整数类型提供了多种精度和范围不同的类型,可以满足不同的需求。在使用时需要注意类型转换的规则和可能发生的数据溢出等问题。
浮点数类型 Floating-point Types
在 C#中,浮点数类型是值类型(Value Type)之一,表示带小数点的数值,用于处理实数运算。C#中的浮点数类型分为两种:单精度浮点数类型(float)和双精度浮点数类型(double)。
- 单精度浮点数类型(
float)用于存储小数点后面最多包含 7 位数字的实数,占用 4 个字节(32 位),表示范围约为 -3.4E38~3.4E38,精度为大约 7 位有效数字。默认值为 0.0f。 - 双精度浮点数类型(
double)用于存储小数点后面最多包含 15 位数字的实数,占用 8 个字节(64 位),表示范围约为 -1.7E308~1.7E308,精度为大约 15 位有效数字。默认值为 0.0d。
浮点数类型的特点是可以存储非常大或非常小的数值,同时也可以存储非常小的小数位。但是由于浮点数的存储方式是采用科学计数法,所以在一些计算中可能会出现精度误差,需要特别注意。
注意
在使用浮点数类型时需要注意其特点和注意事项,以避免出现精度误差和异常情况。
- 由于浮点数类型是值类型,因此在传递给函数或方法时,会进行值复制,可能会影响性能。如果需要传递浮点数类型的大量数据,建议使用引用类型。
- 浮点数类型的值可以与整数类型的值进行计算,但在进行计算时需要注意类型转换,否则可能会出现类型错误或精度误差。
- 浮点数类型的值可以用于比较运算,但由于精度误差的存在,不能使用等于(==)运算符直接比较两个浮点数类型的值。可以使用类似“Math.Abs(a-b) < 0.0001”这样的方式进行比较,其中 0.0001 为允许的误差范围。
- 在进行浮点数类型的计算时,需要特别注意处理溢出、除以 0 等异常情况。
十进制类型 Decimal Type
Decimal 类型是 C# 中用于表示精确的十进制数值的值类型,它具有较高的精度和范围,采用十进制表示法,用于存储高精度的十进制数字。可以进行基本的算术运算和比较操作,并具有特殊值以表示特殊情况,适用于需要精确计算的场合,例如货币计算和科学计算等。
decimal: 十进制类型,占用 16 个字节。精度和范围:具有较高的精度和较大的范围,能够表示 28 位十进制数字,范围为 ±7.9x10^28。默认值为 0.0m。
表示方式:Decimal 类型采用十进制表示法,以十进制数字的形式存储数值,而不是使用二进制表示法,这使得 Decimal 类型可以在计算机中以更为精确的方式表示和处理十进制数。
声明方式:可以使用 "decimal" 关键字来声明 Decimal 类型的变量,例如:
decimal myDecimal = 123.45m;注意:需要在数字后面添加 'm' 或 'M',以明确指示该数字是一个 Decimal 类型的值。
运算和比较:Decimal 类型可以执行基本的算术运算和比较操作,如加、减、乘、除和取模等运算,以及大于、小于、等于、不等于等比较操作。
转换:Decimal 类型可以与其他数值类型进行相互转换,如将
Decimal类型的值转换为Int32、Double、Single等类型,也可以将这些类型的值转换为Decimal类型。特殊值:Decimal 类型还具有特殊值,如正无穷大、负无穷大和
NaN(非数字),用于表示某些特殊情况,例如计算结果为无穷大或未定义值时。适用场景:由于 Decimal 类型具有高精度和精确表示十进制数值的能力,因此常用于金融、货币、税收和精确计算等方面,以确保计算结果的准确性。
注意
在使用十进制类型时,由于其占用的存储空间较大,可能会对内存和性能产生一定的影响。另外,在进行赋值或传递时,也会进行值的复制,因此在处理大量数据时,需要注意性能问题。
布尔类型 Boolean Type
在 C#中,布尔类型(bool)是一种值类型,用于表示逻辑值,即真(true)或假(false)。布尔类型的内置值是 true 和 false,其中 true 表示逻辑真,false 表示逻辑假。
注意
布尔类型在 C#中非常重要,因为它们是控制程序流程和判断条件的基本构建块。
布尔类型在编程中非常常用,它通常用于控制程序的流程,例如判断某个条件是否成立。
在 C#中,可以使用布尔运算符(
&&、||、!等)来操作布尔类型的值。这些运算符可以用于将多个布尔值组合成复合条件。布尔类型在 C#中也可以用于条件语句,例如 if 语句。if 语句根据条件的布尔值来决定是否执行某个代码块。
csharpbool condition = true; if (condition) { Console.WriteLine("条件成立"); // 如果 condition 变量的值为 true,则输出"条件成立" } else { Console.WriteLine("条件不成立"); // 如果 condition 变量的值为 false,则输出"条件不成立" }布尔类型还可以用于循环语句,例如 while 和 do-while 循环。这些循环语句根据条件的布尔值来判断是否继续执行循环体。
csharpbool condition = true; while (condition) { Console.WriteLine("循环中"); condition = false; // 循环体中的代码将执行一次,因为 condition 的初始值为 true // 在循环体中,condition 被赋值为 false,因此下一次循环将不再执行 }上述代码中,循环体中的代码将执行一次,因为 condition 的初始值为 true。在循环体中,condition 被赋值为 false,因此下一次循环将不再执行。
字符类型 Character Type
在 C#中,字符类型(char)是一种值类型,用于处理文本数据、构建字符串,以及表示各种符号和表情符号等。每个 char 变量都占据 16 位(即 2 个字节)的内存空间,可以表示从 U+0000 到 U+FFFF 范围内的所有 Unicode 字符。
字符常量用单引号(
')括起来。csharpchar c1 = 'A'; char c2 = '中';字符类型可以用于处理文本数据,例如从文件或网络读取文本数据,或者在控制台或窗口中显示文本。
在 C#中,字符串(string)类型实际上是由一系列
char字符组成的,因此字符类型也可以用于构建字符串。csharpstring str = "Hello, world!"; // str 是一个字符串 char firstChar = str[0]; // firstChar 变量保存了 str 字符串的第一个字符字符类型还可以表示更复杂的字符。如果需要存储非 ASCII 字符,也可以直接使用 Unicode 转义序列来表示。例如 Unicode 编码中的各种符号、表情符号等。
csharpchar smileyFace = '\u263A'; // '\u263A'是 Unicode 编码中表示微笑表情符号的值 char c = '\u4e2d'; // 存储 Unicode 编码为 0x4e2d 的字符(中文“中”)字符类型还可以用于执行一些常见的字符处理操作,例如大小写转换、字符比较、字符串截取等等。
csharpchar c = 'a'; char upperC = char.ToUpper(c); // 转换为大写字母'A'
枚举类型 Enumeration Types
枚举类型(Enumeration)是一种值类型(Value Type),用于定义了一个固定的集合常量。枚举类型通常用于代表一组相关的常量(例如一周中的天数,颜色、方向、状态等)。
枚举类型可以帮助我们更好地组织和管理代码中的常量值,并且能够增加代码的可读性和可维护性。
枚举类型通过使用
enum关键字进行声明。枚举类型在定义时,可以指定每个枚举成员的名称和相应的值。csharpenum DaysOfWeek { Monday = 1, Tuesday = 2, Wednesday = 3, Thursday = 4, Friday = 5, Saturday = 6, Sunday = 7 }枚举类型的值在使用时可以直接使用枚举成员的名称来表示。
csharpDaysOfWeek today = DaysOfWeek.Tuesday; //定义了一个名为 today 的枚举类型变量,它的值为 DaysOfWeek.Tuesday,即星期二使用枚举类型的
ToString()方法来将其转换为字符串形式。csharpConsole.WriteLine(today.ToString()); // 输出:Monday枚举类型还可以使用一些特殊的属性和方法,例如
GetValues和GetNames方法可以分别返回枚举类型的所有值和名称。csharpDaysOfWeek[] days = (DaysOfWeek[])Enum.GetValues(typeof(DaysOfWeek)); string[] names = Enum.GetNames(typeof(DaysOfWeek)); // 分别使用 GetValues 和 GetNames 方法获取了 DaysOfWeek 枚举类型的所有值和名称 // 并将它们分别保存在了 days 和 names 数组中除了整数值之外,枚举类型中的常量还可以包括其他值类型,例如浮点数、双精度浮点数、十六进制值等。
csharp// 定义了一个 `ErrorCode` 枚举类型 // 并将其中的一些常量设置为了浮点数和十六进制值 enum ErrorCode { None = 0, NotFound = 404, InternalServerError = 500, Pi = 3.14, HexValue = 0xFF }
结构体类型 Struct Types
结构体(Struct)是一种值类型(Value Type),它可以包含不同类型的成员,例如字段、属性、方法和有参构造函数等成员,类似于类(Class),这些成员可以像类一样进行访问和使用。
与类不同的是,结构体是一种轻量级的类型,它被设计为适合用于存储小型数据结构的情况,例如坐标、颜色、日期等。
结构体类型通过使用
struct关键字进行声明。csharp// 定义了一个名为 `Point` 的结构体 // 它有两个整型的成员 `X` 和 `Y` // 并定义了一个构造函数,用于初始化结构体的成员 struct Point { public int X; public int Y; public Point(int x, int y) { X = x; Y = y; } }结构体的实例化和访问成员与类类似。
csharpPoint p = new Point(10, 20); // 创建了一个 `Point` 类型的变量,并传递了初始化参数 `10` 和 `20` // Point point = new Point(); // point.X = 10; // point.Y = 20; Console.WriteLine($"X: {p.X}, Y: {p.Y}"); // 访问了它的成员 `X` 和 `Y`结构体和类有很多相似之处,但也有一些重要的区别。
其中一个主要的区别是,结构体是值类型,而类是引用类型。这意味着当我们将一个结构体变量赋值给另一个变量时,会创建原始结构体的一个副本。
csharpPoint point1 = new Point(); // 创建了一个名为 `point1` 的 `Point` 结构体变量 point1.X = 10; point1.Y = 20; // 将其 `X` 和 `Y` 字段分别设置为 10 和 20 Point point2 = point1; // 将 `point1` 赋值给了另一个名为 `point2` 的变量 point2.X = 30; point2.Y = 40; // 修改了 `point2` 中的 `X` 和 `Y` 字段 // 输出两个结构体变量的值。结果表明, `point1` 中的值没有被改变,而 `point2` 中的值被修改了。 Console.WriteLine(point1.X + "," + point1.Y); // 输出:10,20 Console.WriteLine(point2.X + "," + point2.Y); // 输出:30,40另一个重要的区别是,结构体不能继承其他结构体或类,也不能作为基类使用。但是,我们可以使用接口来实现结构体之间的多态性。
结构体的特性还包括:
- 结构体可以包含构造函数、方法、属性、字段、索引器、操作符重载等成员,使得结构体可以拥有更加丰富的功能。
- 结构体是一种值类型,它的实例在传递时会进行值拷贝,而不是引用拷贝,这意味着结构体的实例是不可变的,这也是结构体被设计为轻量级类型的原因之一。
- 结构体可以被用于泛型类型参数、数组元素、局部变量、函数返回值等场合,使得结构体可以更加灵活地应用于不同的场景。
- 结构体支持接口实现,可以实现多态性。
注意
结构体是一种非常实用的数据类型,它可以在很多场景下替代类,提供更加高效的存储和传递方式。但需要注意的是,在一些情况下,使用类比使用结构体更加合适,例如需要使用继承、多态性、引用类型等特性的情况下。
可空类型 Nullable Types
可空类型是通过在值类型后面添加一个问号(?)来声明的。例如,int? 表示一个可空的 int 类型,可以存储一个 int 值或者 null 值。声明可空类型后,可以使用 HasValue 属性检查变量是否包含值,使用 Value 属性来获取其实际的值。如果变量包含 null 值,则 HasValue 属性返回 false,调用 Value 属性会抛出 NullReferenceException 异常。
int? x = null;
if (x.HasValue)
{
int y = x.Value;
Console.WriteLine("x 的值是 " + y);
}
else
{
Console.WriteLine("x 不包含值");
}在上面的示例中,变量 x 是一个可空的 int 类型,其初始值为 null。在 if 语句中,使用 HasValue 属性检查变量是否包含值,如果包含值,则使用 Value 属性获取其实际的值并输出。否则,输出提示信息。
使用可空类型可以使代码更加简洁和可读,同时还可以避免 null 值带来的不必要的异常。但是,需要注意在使用可空类型时要注意对 null 值进行判断和处理,以避免出现空引用异常。
在 C# 中,值类型的变量存储在堆栈(Stack)中,而引用类型的变量存储在堆(Heap)中。因为值类型存储的是实际的数据值,所以它们在内存中的占用空间大小较小,并且它们的生命周期通常比引用类型更短,因此访问速度较快。而引用类型的变量存储的是指向实际数据的引用,因此占用的空间大小不确定,访问速度相对较慢。此外,值类型的生命周期与其所在的方法或作用域有关,而引用类型则需要进行垃圾回收。
在编写 C# 程序时,应根据具体情况选择使用值类型还是引用类型,以提高程序的性能和可读性。对于较小的数据,可以使用值类型,对于较大的数据或需要动态创建的数据,则应使用引用类型。
引用类型
- 引用类型指的是那些存储在托管堆中的对象,它们的值是对象在内存中的地址,而不是对象本身的值。
- 这些对象是由 .NET 框架的垃圾回收器负责释放的,因为它们无法在栈上分配空间,只能在托管堆中分配。
- 引用类型的实例化是通过使用 new 运算符来完成的。
- 引用类型是指变量存储的是内存地址,而不是直接存储数据本身。
- 通过存储内存地址,可以允许多个变量引用相同的对象,从而实现对象共享和传递引用的效果。
C# 中的引用类型包括:
- 类(class):类是 C# 中最基本的引用类型,它们是定义对象的蓝图。类定义了一组属性、方法和事件,用于描述对象的行为和状态。类可以包含属性、方法、字段、构造函数等。在 C# 中创建类时,可以使用
class关键字。 - 接口(interface):接口定义了一组方法,没有实现,只有方法签名。实现接口的类必须实现接口中定义的所有方法。接口是一种定义类的行为的协议。接口定义了类需要实现的方法和属性,但不提供任何实现。在 C# 中创建接口时,可以使用
interface关键字。 - 委托(delegate):委托是一个类型,用于引用方法。它可以让您将方法作为参数传递给其他方法或将方法存储在变量中。委托在事件处理程序和多线程编程中非常有用。在 C# 中创建委托时,可以使用
delegate关键字。 - 数组(array):数组是一个存储相同类型数据的集合。数组可以是一维或多维的,可以是值类型或引用类型。数组是一种按顺序存储相同类型数据的集合。数组是引用类型,因此数组变量存储的是数组对象的地址。数组的大小是固定的,一旦创建,就不能改变。在 C# 中创建数组时,可以使用
[]运算符。 - 字符串(string):字符串是一组字符的序列。字符串是一个引用类型,但由于其常用性,C# 中的字符串具有特殊的支持。字符串用于表示文本数据。在 C# 中,字符串是不可变的,这意味着一旦字符串被创建,就不能修改它的值。由于字符串是引用类型,因此多个变量可以引用同一个字符串对象,从而实现字符串的共享和传递引用。
引用类型的特点包括:
- 对象存储在堆上:引用类型的对象都存储在堆(Heap)上,而不是栈(Stack)上。这意味着当对象被创建时,系统会分配一块内存来存储对象的数据,并返回这块内存的地址。该地址被存储在变量中,作为变量的值。
- 传递引用:当将引用类型的变量传递给方法时,传递的是该对象在堆上的内存地址,而不是对象本身。这就允许在方法中修改对象的值,并且这些修改会在方法调用结束后保留。
- 对象共享:由于多个变量可以引用同一个对象,因此当一个变量修改该对象时,其它引用该对象的变量也能看到这些修改。这使得对象共享变得容易,也可以减少内存使用,特别是对于大型对象或者在多个对象需要共享相同数据时。
- 与值类型的区别:值类型的变量存储的是值本身,而引用类型的变量存储的是地址,因此在赋值和比较时有很大的区别。例如,值类型的变量在进行比较时会比较它们的值,而引用类型的变量比较的是它们在内存中的地址。
引用类型的赋值是通过将一个变量的引用分配给另一个变量来完成的。
- 在这种情况下,两个变量都将引用同一对象。这种行为被称为引用类型的“浅复制”,因为它们复制的只是对象的引用,而不是对象本身。
- 如果您想复制整个对象,而不是只复制其引用,则需要使用 C# 中的“深复制”技术。
注意
在使用引用类型时,需要注意以下几点:
- 引用类型需要通过 new 运算符来创建。new 运算符会在堆上分配内存,用于存储对象的数据,并返回对象在堆上的地址。
- 引用类型需要使用赋值运算符(=)来进行赋值。赋值运算符将变量设置为对象在堆上的地址。
- 对象创建后,需要使用引用变量来访问对象的属性和方法。引用变量包含对象在堆上的地址,使用该变量可以访问对象在堆上存储的数据。
- 在进行比较时,需要注意引用类型比较的是对象在堆上的地址,而不是对象本身的值。因此,如果要比较对象的值,需要使用对象的方法或属性来进行比较。
- 在使用引用类型时,需要注意对象的生命周期。如果没有引用指向对象,那么对象就会成为垃圾,由垃圾回收器回收。因此,如果需要对象保持有效,需要确保有至少一个引用指向该对象。
- 引用类型可以作为参数传递给函数,这样函数可以修改对象的属性或者状态。但是需要注意,如果函数修改了对象的状态,这个修改会影响到其他引用该对象的变量。
- 可以使用 null 值来表示引用类型的变量没有指向任何对象。当一个变量被设置为 null 值时,它不再引用任何对象,因此对象的内存会被释放。在访问一个 null 引用时会抛出 NullReferenceException 异常。
总之,C# 中的引用类型是一种重要的数据类型,它们在对象的共享和传递引用、动态分配内存等方面具有很大的优势。在使用引用类型时需要注意对象的生命周期、赋值、比较等问题。