C# 数据类型与类型转换详解
为什么数据类型如此重要?
想象你正在整理一个仓库。如果所有货物都随意堆放,找东西时就得翻箱倒柜;但如果在入库时就按「易碎品」「液体」「大件家具」分类存放,并贴上标签,后续的存取、盘点、运输都会高效得多。
C# 的数据类型就是编程世界的「分类标签系统」。它告诉编译器:这块内存里装的是什么、占多大空间、能做什么操作。有了明确的类型约束,编译器能在你写代码时就发现「把牛奶装进文件柜」这类荒谬错误,而不是等到程序崩溃时才暴露问题。
C# 是强类型语言,这意味着一旦变量被声明为某种类型,它的「身份」就确定了。你不能把一个数字塞进字符串变量,也不能把一段文字当成数字计算——除非显式地告诉编译器「我知道自己在做什么」。这种严格性初看麻烦,实则是大型项目可靠性的基石。
值类型 vs 引用类型:两种存储哲学
C# 的所有类型分为两大阵营:值类型和引用类型。它们的差异不是语法细节,而是内存管理的根本逻辑。
底层存储机制
┌─────────────────────────────────────────┐
│ 内存布局示意图 │
├─────────────────────────────────────────┤
│ 栈 (Stack) │
│ ├─ int age = 25; → 直接存 25 │
│ ├─ Point p = new(1,2); → 直接存坐标 │
│ └─ 局部变量、值类型实例、方法参数 │
│ (自动分配/释放,速度快) │
├─────────────────────────────────────────┤
│ 堆 (Heap) │
│ ├─ "Hello" → 字符串对象 │
│ ├─ new int[] {1,2,3} → 数组对象 │
│ └─ new Person() → 类的实例 │
│ (GC 管理,生命周期灵活) │
├─────────────────────────────────────────┤
│ 引用类型变量在栈上存「地址」 │
│ 通过地址找到堆上的实际数据 │
│ string name ───────→ 堆上的 "Hello" │
└─────────────────────────────────────────┘生活化类比:
- 值类型像「纸质文件」——复印一份,原件和复印件各自独立,修改复印件不影响原件。
- 引用类型像「网页链接」——你把链接发给朋友,你们访问的是同一个页面;页面内容一改,所有人看到的变化。
赋值时的拷贝区别
// 值类型:复制数据本身
int a = 10;
int b = a; // b 得到 a 的副本
b = 20;
Console.WriteLine(a); // 输出 10,a 不受影响
// 引用类型:复制引用地址
int[] arr1 = { 1, 2, 3 };
int[] arr2 = arr1; // arr2 和 arr1 指向同一个数组
arr2[0] = 99;
Console.WriteLine(arr1[0]); // 输出 99,原数组被修改!应用场景提示:理解这个区别对调试至关重要。当你发现「明明没改原数据,数据却变了」时,多半是引用类型的共享引用导致的。
值类型家族
数值类型
C# 的数值类型按精度和范围细分,选择合适的类型既能节省内存,又能避免溢出。
| 类型 | 别名 | 字节 | 取值范围 | 默认值 | 适用场景 |
|---|---|---|---|---|---|
sbyte | System.SByte | 1 | -128 ~ 127 | 0 | 极小型整数 |
byte | System.Byte | 1 | 0 ~ 255 | 0 | 二进制数据、文件流 |
short | System.Int16 | 2 | -32,768 ~ 32,767 | 0 | 小型计数器 |
ushort | System.UInt16 | 2 | 0 ~ 65,535 | 0 | 端口号、协议字段 |
int | System.Int32 | 4 | ±21 亿 | 0 | 最常用整数 |
uint | System.UInt32 | 4 | 0 ~ 42 亿 | 0 | 位操作、ID 编码 |
long | System.Int64 | 8 | ±922 亿亿 | 0 | 大数值、时间戳 |
ulong | System.UInt64 | 8 | 0 ~ 1844 亿亿 | 0 | 无符号大数 |
float | System.Single | 4 | ±3.4×10³⁸,6-7 位精度 | 0f | 图形学、科学计算 |
double | System.Double | 8 | ±1.7×10³⁰⁸,15-16 位精度 | 0d | 默认浮点类型 |
decimal | System.Decimal | 16 | ±7.9×10²⁸,28-29 位精度 | 0m | 金融、货币计算 |
// 常用数值类型声明
int age = 25; // 最常用,32 位有符号整数
long timestamp = 1735689600000L; // 大数值加后缀 L
float temperature = 36.5f; // 单精度加后缀 f
double pi = 3.14159265359; // 双精度,默认浮点类型
decimal price = 19.99m; // 高精度小数加后缀 m,金融专用
// 科学计数法
double lightSpeed = 3.0e8; // 3.0 × 10⁸
// 二进制、十六进制字面量(C# 7.0+)
int binary = 0b_1010_1010; // 二进制,下划线提高可读性
int hex = 0x_FF_00_FF; // 十六进制应用场景提示:
- 循环计数、数组索引用
int - 金额计算必须用
decimal,double的浮点误差会导致「0.1 + 0.2 ≠ 0.3」 - 文件大小、时间戳用
long
bool 类型
bool isActive = true;
bool hasPermission = false;bool 仅接受 true 或 false,不能与其他类型隐式转换(这与 C/C++ 不同)。在 C# 中,if (x = 1) 这种「赋值当判断」的写法会直接编译报错,从根源上杜绝了一类经典 Bug。
可空类型
值类型默认不能为 null,但现实业务中「未知」「未填写」是常态。可空类型用 ? 后缀解决这个矛盾。
int? score = null; // 分数未知
DateTime? lastLogin = null; // 最后登录时间未知
// 判断是否有值
if (score.HasValue)
{
Console.WriteLine(score.Value); // 通过 .Value 获取实际值
}
// 空合并运算符提供默认值
int finalScore = score ?? 0; // score 为 null 时用 0
// 空条件运算符避免链式调用崩溃
string? name = GetUserName();
int? length = name?.Length; // name 为 null 则整个表达式为 null应用场景提示:数据库查询结果、可选表单字段、API 返回的可选参数,都适合用可空类型建模。
枚举类型
枚举将相关的常量组织为有意义的组,避免「魔法数字」污染代码。
// 基础枚举
enum Weekday { Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday }
// 指定底层类型和值
enum Status : byte { Inactive = 0, Active = 1, Pending = 2 }
// Flags 枚举(位运算组合)
[Flags]
enum Permission { None = 0, Read = 1, Write = 2, Execute = 4 }
Permission userPerm = Permission.Read | Permission.Write; // 组合权限
bool canRead = userPerm.HasFlag(Permission.Read); // 检查权限
// 枚举与字符串互转
Weekday day = (Weekday)Enum.Parse(typeof(Weekday), "Friday");
string dayName = day.ToString(); // "Friday"结构类型
结构是轻量级的值类型,适合表示坐标、颜色、复数等「值语义」的数据。
// 定义结构
struct Point
{
public int X;
public int Y;
public Point(int x, int y) => (X, Y) = (x, y);
public double DistanceTo(Point other) =>
Math.Sqrt(Math.Pow(X - other.X, 2) + Math.Pow(Y - other.Y, 2));
}
// 使用结构
Point p1 = new Point(0, 0);
Point p2 = new Point(3, 4);
double dist = p1.DistanceTo(p2); // 5.0
// 只读结构(C# 7.2+),防止意外修改
readonly struct ImmutablePoint
{
public readonly int X;
public readonly int Y;
public ImmutablePoint(int x, int y) => (X, Y) = (x, y);
}结构 vs 类的选择:数据量小(≤16 字节)、生命周期短、不需要继承、按值语义比较时,优先考虑结构。
引用类型家族
字符串 string
字符串是 C# 中最特殊的引用类型——它具有不可变性(immutable)。
string s1 = "Hello";
string s2 = s1;
s2 = "World";
Console.WriteLine(s1); // 输出 "Hello",s1 未被修改!
// 原因:字符串一旦创建就不能修改,s2 = "World" 是创建了新对象
// 高效字符串拼接
string bad = "";
for (int i = 0; i < 1000; i++)
bad += i.ToString(); // 每次循环都创建新字符串,性能极差!
var sb = new System.Text.StringBuilder();
for (int i = 0; i < 1000; i++)
sb.Append(i); // 原地修改,性能优异
string good = sb.ToString();
// 原始字符串字面量(C# 11)
string json = """
{
"name": "张三",
"age": 25
}
""";数组
数组是固定长度、相同类型元素的连续集合。
// 一维数组
int[] numbers = new int[5]; // 长度为 5,默认全 0
int[] init = { 1, 2, 3, 4, 5 }; // 声明并初始化
// 多维数组
int[,] matrix = new int[3, 4]; // 3 行 4 列
int[,,] cube = new int[2, 3, 4]; // 三维
// 交错数组(数组的数组)
int[][] jagged = new int[3][];
jagged[0] = new int[5];
jagged[1] = new int[3];
// 常用操作
Array.Sort(numbers); // 排序
int index = Array.IndexOf(numbers, 3); // 查找索引类、接口、委托、记录
// 类:面向对象的核心
public class Person
{
public string Name { get; set; } = "未知";
public virtual void Introduce() => Console.WriteLine($"我是 {Name}");
}
// 接口:定义契约
public interface IShape
{
double Area { get; }
void Draw();
}
// 委托:类型安全的函数指针
public delegate void EventHandler(string message);
public delegate TResult Func<in T, out TResult>(T arg);
// 记录:不可变数据模型(C# 9.0+)
public record Point(int X, int Y);
var p = new Point(1, 2);
// p.X = 3; // 编译错误!记录默认不可变类型转换:安全的「变形术」
现实世界中,数据很少以理想格式存在。用户输入的是字符串,数据库返回的是 object,计算需要统一的数值类型——类型转换是程序与现实世界交互的必经之路。
隐式转换:安全的自动升级
当转换不会丢失信息时,C# 自动完成,无需特殊语法。
int small = 100;
long big = small; // int → long,安全
double d = small; // int → double,安全
float f = small; // int → float,安全(精度可能微损,但值域覆盖)
// 小范围 → 大范围,总是安全的
byte b = 255;
int i = b; // byte → int,自动
long l = b; // byte → long,自动规则:只能从小范围类型转向大范围类型,或从精度低的转向精度高的(int → float 可能有精度损失,但编译器认为可接受)。
显式转换:风险自负的强制转换
当转换可能丢失数据或失败时,必须显式声明「我知道风险」。
double pi = 3.14159;
int roughPi = (int)pi; // 显式转换,结果为 3(小数部分截断)
long bigNumber = 100;
int smallNumber = (int)bigNumber; // 显式转换,编译通过
// 危险示例:溢出
int max = int.MaxValue;
long l = max + 1L; // long 类型,值为 2147483648
int overflowed = (int)l; // 运行时溢出!结果为 -2147483648溢出检查:
// checked:溢出时抛出 OverflowException
checked
{
int result = (int)l; // 抛出异常,暴露问题
}
// unchecked:默认行为,溢出静默发生
unchecked
{
int result = (int)l; // 结果错误,但不报错
}溢出处理的最佳实践
// 方式一:checked 强制检查(推荐用于关键计算)
try
{
int result = checked((int)someLongValue);
}
catch (OverflowException)
{
// 记录日志,使用降级策略
}
// 方式二:手动范围检查(性能最优)
long value = GetLargeNumber();
if (value >= int.MinValue && value <= int.MaxValue)
{
int safe = (int)value;
}
else
{
// 处理超范围情况
}常用转换方法深度对比
Convert 类:全能但「固执」
string input = "123";
int number = Convert.ToInt32(input); // 字符串 → int
double d = Convert.ToDouble("3.14"); // 字符串 → double
bool b = Convert.ToBoolean("true"); // 字符串 → bool
DateTime dt = Convert.ToDateTime("2025-01-15"); // 字符串 → 日期
// Convert 的「固执」之处
int? nullable = Convert.ToInt32(null); // 返回 0,不是 null!
string nullStr = null;
int fromNull = Convert.ToInt32(nullStr); // 也是 0!
// 处理空值
int safe = Convert.ToInt32(nullStr ?? "0"); // 显式处理 null特点:支持 null 输入(返回默认值)、支持多种重载、对格式要求较宽松;但 null 转值类型时返回 0/false/DateTime.MinValue,可能掩盖数据缺失问题。
int.Parse() / int.TryParse():精确控制
string input = "123";
// int.Parse:格式错误时抛出异常
try
{
int parsed = int.Parse(input); // 成功
int failed = int.Parse("abc"); // 抛出 FormatException
}
catch (FormatException ex)
{
Console.WriteLine($"格式错误:{ex.Message}");
}
// int.TryParse:推荐的安全方式
if (int.TryParse("abc", out int result))
{
Console.WriteLine($"转换成功:{result}");
}
else
{
Console.WriteLine("转换失败,result 为 0"); // result 被设为 0
}
// 带格式的 TryParse
if (int.TryParse("1,234", NumberStyles.AllowThousands,
CultureInfo.InvariantCulture, out int formatted))
{
Console.WriteLine(formatted); // 1234
}对比总结:
| 方法 | null 输入 | 格式错误 | 性能 | 推荐场景 |
|---|---|---|---|---|
Convert.ToInt32 | 返回 0 | 抛异常 | 中 | 数据已验证、允许 null 转默认值 |
int.Parse | 抛异常 | 抛异常 | 高 | 确定格式正确、需要严格校验 |
int.TryParse | 返回 false | 返回 false | 高 | 用户输入、外部数据、不确定格式 |
ToString():万能输出
int number = 12345;
string s1 = number.ToString(); // "12345"
string s2 = number.ToString("N2"); // "12,345.00"(千分位+两位小数)
string s3 = number.ToString("X"); // "3039"(十六进制)
string s4 = number.ToString("D8"); // "00012345"(8 位补零)
// 自定义格式
decimal price = 19.99m;
string p1 = price.ToString("C"); // "¥19.99"(货币)
string p2 = price.ToString("F1"); // "20.0"(一位小数)
string p3 = price.ToString("0.00"); // "19.99"(自定义)
// 日期格式化
DateTime now = DateTime.Now;
string d1 = now.ToString("yyyy-MM-dd"); // "2025-01-15"
string d2 = now.ToString("HH:mm:ss"); // "14:30:45"
string d3 = now.ToString("yyyy年MM月dd日 dddd"); // "2025年01月15日 星期三"装箱与拆箱:值类型的「跨界旅行」
底层原理
int value = 42; // 栈上的值类型
object boxed = value; // 装箱:值类型 → 引用类型
// 1. 在堆上分配内存
// 2. 将值复制到堆
// 3. 栈上存堆地址的引用
int unboxed = (int)boxed; // 拆箱:引用类型 → 值类型
// 1. 检查类型是否匹配
// 2. 将堆上的值复制回栈性能损耗
// 装箱的隐式发生点(性能陷阱)
ArrayList list = new ArrayList();
for (int i = 0; i < 10000; i++)
list.Add(i); // 每次 Add 都装箱!i 从 int 变为 object
// 优化:使用泛型集合
List<int> genericList = new List<int>();
for (int i = 0; i < 10000; i++)
genericList.Add(i); // 无装箱,直接存 int
// 字符串拼接时的装箱
int num = 42;
string s = "值是:" + num; // num 被装箱为 object,再调用 ToString()优化方案
// 1. 优先使用泛型集合
List<int> list = new List<int>(); // 推荐
Dictionary<string, int> dict = new(); // 推荐
// 2. 避免非泛型接口
IComparable obj = 42; // 装箱!
IComparable<int> obj = 42; // 无装箱
// 3. 字符串格式化用插值
string s = $"值是:{num}"; // 编译器优化,避免显式装箱高级转换技巧
as 与 is 运算符
object obj = "Hello";
// is:类型检查
if (obj is string)
{
string s = (string)obj; // 确定类型后转换
}
// C# 7.0+ 模式匹配:检查+转换一步完成
if (obj is string str)
{
Console.WriteLine(str.ToUpper()); // 直接使用 str
}
// as:安全转换,失败返回 null
string? text = obj as string;
if (text != null)
{
// 使用 text
}
// as vs 强制转换
string? safe = obj as string; // obj 不是 string 时返回 null
string crash = (string)obj; // obj 不是 string 时抛出 InvalidCastException
// 可空类型与 as
object numObj = 42;
int? number = numObj as int?; // 可空类型可用 as
// int bad = numObj as int; // 编译错误!as 仅支持引用类型和可空类型自定义类型转换
public struct Celsius
{
public double Degrees { get; }
public Celsius(double degrees) => Degrees = degrees;
// 隐式转换:Celsius → double(无损)
public static implicit operator double(Celsius c) => c.Degrees;
// 显式转换:double → Celsius(可能不合理,需显式确认)
public static explicit operator Celsius(double d) => new Celsius(d);
}
// 使用
Celsius temp = new Celsius(25);
double d = temp; // 隐式转换,自动调用
Celsius back = (Celsius)d; // 显式转换,需要 (Celsius)常见坑点与解决方案
坑点 1:字符串转数值的格式异常
// 用户输入 "1,234" 直接 Parse 会失败
string input = "1,234";
// int n = int.Parse(input); // 抛出 FormatException!
// 解决方案:指定 NumberStyles
int n = int.Parse(input, NumberStyles.AllowThousands);
// 或更稳妥的 TryParse
if (int.TryParse(input, NumberStyles.Any, CultureInfo.CurrentCulture, out int result))
{
// 成功
}坑点 2:可空类型转换的空引用
int? nullable = null;
// 错误方式
int dangerous = nullable.Value; // 抛出 InvalidOperationException!
int alsoBad = (int)nullable; // 同样抛出异常!
// 正确方式
int safe1 = nullable ?? 0; // 空合并
int safe2 = nullable.GetValueOrDefault(); // 默认 0
int safe3 = nullable.GetValueOrDefault(-1); // 指定默认值
if (nullable.HasValue)
{
int safe4 = nullable.Value;
}坑点 3:大精度数值转换溢出
// double 转 decimal 可能超出范围
double huge = 1e30;
// decimal d = (decimal)huge; // 抛出 OverflowException!
// 解决方案:先检查范围
if (huge >= (double)decimal.MinValue && huge <= (double)decimal.MaxValue)
{
decimal d = (decimal)huge;
}坑点 4:浮点数转整数的精度陷阱
double d = 2.999999999999;
int i = (int)d; // 结果为 2,不是 3!显式转换直接截断
// 解决方案:明确使用 Math.Round / Math.Floor / Math.Ceiling
int rounded = (int)Math.Round(d); // 3,四舍五入
int floor = (int)Math.Floor(d); // 2,向下取整
int ceiling = (int)Math.Ceiling(d); // 3,向上取整坑点 5:字符串与字符的混淆
char c = '5';
string s = "5";
// 错误
int wrong = (int)c; // 编译错误!char 不能直接转 int
int alsoWrong = int.Parse(c); // 编译错误!Parse 需要 string
// 正确
int fromChar = int.Parse(c.ToString()); // 5
int fromString = int.Parse(s); // 5
int ascii = (int)c; // 53(字符 '5' 的 ASCII 码,通常不是想要的结果)常见数据类型转换对照表
| 源类型 → 目标类型 | 转换方式 | 示例代码 | 注意事项 |
|---|---|---|---|
int → long | 隐式 | long l = i; | 自动,无损 |
long → int | 显式 | int i = (int)l; | 可能溢出,用 checked 检查 |
int → double | 隐式 | double d = i; | 自动,精度无损 |
double → int | 显式 | int i = (int)d; | 截断小数,非四舍五入 |
int → string | .ToString() | string s = i.ToString(); | 可带格式参数 |
string → int | int.Parse / TryParse | int i = int.Parse(s); | 推荐 TryParse 防异常 |
string → double | double.Parse / TryParse | double d = double.Parse(s); | 注意区域性格式(小数点/逗号) |
string → bool | bool.Parse / TryParse | bool b = bool.Parse(s); | 接受 "True"/"False"(不区分大小写) |
string → DateTime | DateTime.Parse / TryParse | DateTime dt = DateTime.Parse(s); | 格式多样,建议指定 CultureInfo |
object → 具体类型 | as / is + 强制转换 | string s = obj as string; | as 失败返回 null,强制转换失败抛异常 |
值类型 → object | 装箱 | object o = i; | 隐式发生,有性能开销 |
object → 值类型 | 拆箱 | int i = (int)o; | 必须类型匹配,否则抛异常 |
int → byte | 显式 | byte b = (byte)i; | 溢出截断,如 256 → 0 |
byte → int | 隐式 | int i = b; | 自动,无损 |
float → double | 隐式 | double d = f; | 自动,但可能引入浮点误差 |
double → float | 显式 | float f = (float)d; | 可能溢出或精度损失 |
decimal → double | 显式 | double d = (double)m; | 精度从 28 位降到 15 位 |
double → decimal | 显式 | decimal m = (decimal)d; | 可能溢出(double 范围更大) |
枚举 → int | 显式 | int i = (int)enumValue; | 获取底层值 |
int → 枚举 | 显式 | Enum e = (Enum)i; | 不检查值是否有效 |
| 自定义类型互转 | 自定义操作符 | public static implicit operator | 需自行实现转换逻辑 |
知识要点回顾
| 模块 | 核心要点 | 一句话记忆 |
|---|---|---|
| 值类型 vs 引用类型 | 栈存值、堆存引用;赋值时复制值 vs 复制地址 | 值类型是复印文件,引用类型是分享链接 |
| 数值类型 | 按范围选类型,decimal 用于金融 | 循环用 int,金额用 decimal,大数用 long |
| 可空类型 | ? 后缀,配合 ?? 和 ?. 安全操作 | 数据库查询、表单选填的标配 |
| 隐式转换 | 小范围 → 大范围,自动完成 | 安全无损,编译器自动放行 |
| 显式转换 | 大范围 → 小范围,可能丢数据 | 风险自负,必要时用 checked 兜底 |
| Convert vs Parse | Convert 容忍 null,Parse 严格快速 | 用户输入用 TryParse,内部数据用 Parse |
| 装箱拆箱 | 值类型与 object 互转,有性能成本 | 泛型集合是救星,ArrayList 是历史 |
| as/is 运算符 | 安全类型检查与转换 | as 失败给 null,is 模式匹配一步到位 |
| 自定义转换 | implicit/explicit operator | 让自定义类型无缝融入类型系统 |