C#基础(一):数据类型

C# 是强类型语言,所有类型均派生自 System.Object,整体分为值类型、引用类型两大分支,底层内存分配、赋值行为、生命周期完全不同。下面从底层原理、细分类型、代码实践、常见误区四个维度展开讲解。

一、值类型(Value Types)

1.1 核心本质

值类型的变量直接存储数据本身,通常分配在线程栈(Stack)上,随方法执行结束自动释放,无需 GC 回收。

  • 赋值时会创建完整的数据副本,两个变量互不影响
  • 隐式继承自 System.ValueType(该类又继承自 object
  • 不能被继承,默认是隐式密封的
  • 默认值为其 “零值”(如 int 默认 0,bool 默认 false)

1.2 简单值类型

1.2.1 整型

类型占用字节取值范围说明
sbyte 1 -128 ~ 127 有符号字节
byte 1 0 ~ 255 无符号字节,常用于二进制数据
short 2 -32768 ~ 32767 短整型
ushort 2 0 ~ 65535 无符号短整型
int 4 -2^31 ~ 2^31-1 最常用整型,默认整数字面量为 int
uint 4 0 ~ 2^32-1 无符号整型
long 8 -2^63 ~ 2^63-1 长整型,大数值场景
ulong 8 0 ~ 2^64-1 无符号长整型
// 整型字面量后缀
int a = 100;          // 默认int
long b = 100L;        // L后缀指定long
uint c = 100U;        // U后缀指定uint
ulong d = 100UL;      // UL后缀指定ulong

// 二进制与十六进制字面量(C# 7.0+)
int binary = 0b1010;  // 二进制,值为10
int hex = 0xFF;       // 十六进制,值为255

1.2.2 浮点型

类型占用字节精度取值范围适用场景
float 4 约 7 位十进制 ±1.5e-45 ~ ±3.4e38 精度要求不高、数据量大的场景
double 8 约 15-16 位十进制 ±5e-324 ~ ±1.7e308 科学计算、默认浮点类型
decimal 16 28-29 位十进制 ±1e-28 ~ ±7.9e28 金融、货币计算(精度优先)

关键区别:float/double 是二进制浮点,存在精度丢失问题;decimal 是十进制浮点,专门为财务场景设计,运算速度略慢。

float f = 3.14f;      // 必须加f后缀
double d = 3.14;      // 默认浮点字面量为double
decimal money = 99.99m; // 必须加m后缀,金融计算首选

// 经典精度丢失问题
double x = 0.1 + 0.2;
Console.WriteLine(x); // 输出 0.30000000000000004,二进制浮点固有缺陷
decimal y = 0.1m + 0.2m;
Console.WriteLine(y); // 输出 0.3,精度准确

1.2.3 字符与布尔

  • char:2 字节,Unicode UTF-16 字符,支持转义字符、Unicode 转义
  • bool:1 字节,只有 true/false 两个值,不能与整型互转
char c1 = 'A';
char c2 = '\u0041';   // Unicode转义,等同于'A'
char tab = '\t';      // 转义字符:制表符

bool isSuccess = true;
// 错误写法:if (isSuccess == 1) { } C#不支持bool与数字隐式转换

1.3 结构体(struct)

结构体是用户自定义值类型,用于封装小型相关变量组,适合轻量数据模型。

核心特点

  • 栈上分配(除非被装箱),性能优于类
  • 不支持继承,不能显式指定无参构造函数(C# 10+ 允许自定义无参构造)
  • 必须在构造函数中为所有字段赋值
// 基础结构体定义
public struct Point
{
    // 字段
    public int X;
    public int Y;

    // 有参构造函数
    public Point(int x, int y)
    {
        X = x;
        Y = y;
    }

    // 方法
    public double DistanceToOrigin()
    {
        return Math.Sqrt(X * X + Y * Y);
    }
}

// 使用
Point p1 = new Point(3, 4);
Point p2 = p1; // 值拷贝,p1和p2是两个独立对象
p2.X = 10;
Console.WriteLine(p1.X); // 输出3,不受p2修改影响

高级结构体特性

只读结构体(C# 7.2+):所有成员不可变,编译器优化避免不必要

public readonly struct ReadonlyPoint
{
    public int X { get; }
    public int Y { get; }

    public ReadonlyPoint(int x, int y)
    {
        X = x;
        Y = y;
    }
}

ref 结构体(C# 7.2+):只能分配在栈上,不能被装箱,用于高性能内存操作(如 Span<T> 就是 ref struct)

public ref struct BufferWrapper
{
    public Span<byte> Buffer;
    // 不能作为泛型参数、不能被装箱、不能在类中声明
}

1.4 枚举(enum)

枚举是命名的整型常量集合,增强代码可读性,底层默认是 int 类型。

基础用法

// 默认底层类型为int,从0开始递增
public enum OrderStatus
{
    Pending,    // 0
    Processing, // 1
    Shipped,    // 2
    Completed,  // 3
    Cancelled   // 4
}

// 指定底层类型与初始值
public enum WeekDay : byte
{
    Monday = 1,
    Tuesday = 2,
    Wednesday = 3
}

// 使用
OrderStatus status = OrderStatus.Processing;
Console.WriteLine(status);         // 输出 Processing(字符串形式)
Console.WriteLine((int)status);    // 输出 1(强转获取数值)

Flags 位枚举(高频实用)

标注 [Flags] 特性后,枚举可通过按位运算组合多个值,适合表示多选状态。

[Flags]
public enum Permission
{
    None = 0,          // 0000
    Read = 1 << 0,     // 0001 = 1
    Write = 1 << 1,    // 0010 = 2
    Delete = 1 << 2,   // 0100 = 4
    Admin = Read | Write | Delete // 0111 = 7
}

// 位运算操作
Permission userPerm = Permission.Read | Permission.Write; // 组合权限

// 判断是否包含某权限
bool canWrite = userPerm.HasFlag(Permission.Write); // true

// 移除权限
userPerm = userPerm & ~Permission.Write;

// 输出结果
Console.WriteLine(userPerm); // 输出 Read

二、引用类型(Reference Types)

2.1 核心本质

引用类型的变量存储的是数据的内存地址(引用),实际数据存储在托管堆(Heap)上,由 GC(垃圾回收器)负责生命周期管理。

  • 赋值时只复制引用地址,多个变量可指向同一个堆对象
  • 默认值为 null(不指向任何对象)
  • 支持继承、多态

2.2 核心引用类型分类

类型说明
class 自定义引用类型,面向对象核心载体
interface 行为契约,支持多继承
delegate 类型安全的函数指针,用于回调与事件
record / record class C# 9.0+,不可变引用类型,默认按值比较
record struct C# 10+,不可变值类型 record

2.3 特殊引用类型详解

2.3.1 string(字符串)

stringSystem.String 的别名,是最特殊的引用类型:

  • 不可变性:字符串一旦创建就无法修改,任何修改操作都会生成新字符串
  • 字符串驻留池:相同字面量字符串在内存中只存一份,复用引用
  • 支持 == 按值比较(编译器重写了运算符)
// 不可变性演示
string s1 = "hello";
string s2 = s1;
s1 = "world"; // 修改s1,实际是创建了新字符串,s2不受影响
Console.WriteLine(s2); // 输出 hello

// 字符串驻留池
string a = "abc";
string b = "abc";
// 虽然是引用类型,但指向堆中同一个对象
Console.WriteLine(object.ReferenceEquals(a, b)); // 输出 true

// 动态拼接不进入驻留池
string c = new string(new char[] { 'a', 'b', 'c' });
Console.WriteLine(object.ReferenceEquals(a, c)); // 输出 false

// 大量拼接用StringBuilder,避免生成大量临时字符串
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 1000; i++)
{
    sb.Append(i);
}
string result = sb.ToString();

2.3.2 object(万物基类)

object 是所有类型的基类,任何类型都可以隐式转换为 object

  • 装箱的核心载体
  • 提供四个基础方法:Equals()GetHashCode()GetType()ToString()

2.3.3 dynamic(动态类型)

C# 4.0 引入,绕过编译时类型检查,在运行时解析类型。

  • 适合与 COM、动态语言交互,或反射简化
  • 失去智能提示,性能略差,异常延迟到运行时
dynamic dyn = 100;
Console.WriteLine(dyn.GetType()); // System.Int32

dyn = "hello"; // 运行时改变类型
Console.WriteLine(dyn.Length); // 编译通过,运行时检查是否有Length属性

// 对比var:var是编译时类型推断,本质还是强类型
var v = 100; // v编译时就是int
// v = "hello"; // 编译报错

三、值类型 vs 引用类型 核心差异

3.1 维度对比表

对比维度值类型引用类型
内存分配 主要在栈 托管堆
变量存储 数据本身 数据的内存地址
赋值行为 创建完整副本 只复制引用
默认值 零值(0/false 等) null
继承性 不能被继承,隐式密封 支持继承
GC 影响 不触发 GC,随栈自动释放 由 GC 回收
基类 System.ValueType System.Object

3.2 赋值行为差异演示

// 值类型:复制数据,互不影响
int a = 10;
int b = a;
b = 20;
Console.WriteLine($"a={a}, b={b}"); // a=10, b=20

// 引用类型:复制引用,指向同一对象
public class Person { public int Age; }
Person p1 = new Person { Age = 10 };
Person p2 = p1;
p2.Age = 20;
Console.WriteLine($"p1.Age={p1.Age}, p2.Age={p2.Age}"); // 均为20

3.3 参数传递的补充说明

默认情况下,无论值类型还是引用类型,参数都是值传递:

  • 值类型:传递数据副本
  • 引用类型:传递引用地址的副本(指向同一个堆对象)

ref/out 后变为引用传递:

  • 值类型:传递变量地址,方法内修改会影响原变量
  • 引用类型:传递引用的引用,可修改原变量指向的对象
// 值类型按引用传递
void ChangeValue(ref int num)
{
    num = 999;
}
int x = 10;
ChangeValue(ref x);
Console.WriteLine(x); // 999,原变量被修改

四、可空类型

4.1 可空值类型(Nullable<T>)

值类型默认不能为 null,Nullable<T> 结构体为值类型增加了 “空值” 能力,语法糖为 T?

// 两种写法等价
int? num1 = null;
Nullable<int> num2 = null;

// 核心属性与方法
int? value = 10;
Console.WriteLine(value.HasValue);   // true,是否有值
Console.WriteLine(value.Value);      // 10,获取值(无值时抛异常)
Console.WriteLine(value.GetValueOrDefault()); // 安全获取,无值返回默认值

// 配合空合并运算符 ??
int result = num1 ?? 0; // num1为null则取0

// 空条件运算符
int? length = str?.Length; // str为null时直接返回null,不抛空引用异常

4.2 可空引用类型(C# 8.0+)

为解决引用类型空引用异常问题,C# 8.0 引入可空引用类型,属于编译时检查。

  • string:不可空引用,编译器保证不会为 null
  • string?:可空引用,明确表示可能为 null,使用前需判空
#nullable enable // 开启可空上下文

string notNullStr = "hello"; // 不可空
// notNullStr = null; // 编译器警告

string? maybeNullStr = null; // 可空

// 使用前需判空,否则编译器警告
if (maybeNullStr != null)
{
    Console.WriteLine(maybeNullStr.Length);
}

注意:可空引用类型只是编译时检查,运行时和普通引用类型无区别,不能完全杜绝空引用异常。

五、装箱与拆箱(Boxing & Unboxing)

5.1 核心原理

  • 装箱:将值类型转换为 object 或接口类型,会在堆上分配对象并复制值
  • 拆箱:将装箱后的引用类型转换回值类型,从堆中复制值到栈

这是 C# 类型系统的核心特性,但频繁装箱拆箱会带来性能损耗和 GC 压力。

5.2 代码演示

int num = 123;

// 装箱:int -> object
object obj = num; // 堆上创建副本,产生内存分配

// 拆箱:object -> int
int num2 = (int)obj;

// 错误拆箱:类型不匹配会抛InvalidCastException
// long l = (long)obj; // 运行时异常,装箱时是int,只能拆回int

5.3 常见发生场景

  • 值类型赋值给 object/ 接口变量
  • 调用非泛型集合(如 ArrayList)添加值类型元素
  • 值类型调用 ToString()Equals() 等基类方法(部分情况会优化)
// 非泛型ArrayList:每个元素都会装箱,性能差
ArrayList list = new ArrayList();
list.Add(1); // 装箱
list.Add(2); // 装箱

// 泛型List<int>:无装箱,性能更好(推荐)
List<int> genericList = new List<int>();
genericList.Add(1); // 无装箱

5.4 最佳实践

  • 优先使用泛型集合,避免非泛型集合
  • 避免频繁在值类型和 object 之间转换
  • 高性能场景使用 Span<T>Memory<T> 减少装箱

六、类型转换全解

6.1 隐式转换

编译器自动执行,无数据丢失,比如小范围整型转大范围整型、子类转父类。

int i = 100;
long l = i; // int隐式转long,无精度丢失

string s = "abc";
object obj = s; // 子类隐式转父类

6.2 显式转换(强制转换)

需要手动写 (T)value,可能丢失精度或抛异常。

long l = 100;
int i = (int)l; // 强制转换

// 数值溢出检查
checked
{
    int max = int.MaxValue;
    // int overflow = max + 1; // 抛出OverflowException
}

6.3 as 与 is 运算符

  • as:转换失败返回 null,不抛异常,只能用于引用类型和可空值类型
  • is:判断类型是否匹配,返回 bool,C# 7.0 + 支持模式匹配
object obj = "hello";

// as转换
string? str = obj as string;
if (str != null) { /* 使用str */ }

// is模式匹配(推荐写法)
if (obj is string s)
{
    Console.WriteLine(s.Length); // 直接使用s,无需二次转换
}

// 逻辑取反模式(C# 9.0+)
if (obj is not null) { }

// 属性模式
if (obj is Person { Age: > 18 } p) { }

6.4 Convert 类

通用类型转换,支持字符串转值类型,内部做了容错处理。

string numStr = "123";
int num = Convert.ToInt32(numStr);
double d = Convert.ToDouble("3.14");

6.5 自定义类型转换

在类型中定义 implicit(隐式)或 explicit(显式)转换运算符。

public struct Celsius
{
    public double Value;
    public Celsius(double value) => Value = value;

    // 隐式转换:Celsius -> double
    public static implicit operator double(Celsius c) => c.Value;

    // 显式转换:double -> Celsius
    public static explicit operator Celsius(double d) => new Celsius(d);
}

// 使用
Celsius c = new Celsius(25.5);
double temp = c; // 隐式转换
Celsius c2 = (Celsius)30.0; // 显式转换

七、常见坑点与最佳实践

  • 结构体不要设计过大:超过 16 字节建议用类,否则赋值拷贝开销大
  • 不要用 == 比较 object 装箱的值类型:会比较引用而非值
  • 字符串拼接大量数据必须用 StringBuilder:避免生成大量临时字符串增加 GC 压力
  • 可空引用类型只是编译检查:不要依赖它保证运行时安全,关键逻辑仍需判空
  • 枚举默认从 0 开始:始终为 0 值设置合理的默认成员,避免未初始化枚举出现意外值
  • decimal 不支持无符号整型直接计算:跨类型运算时注意类型兼容

 

posted @ 2026-07-07 16:04  挺秃然的i  阅读(13)  评论(0)    收藏  举报