Skip to content

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"     │
└─────────────────────────────────────────┘

生活化类比:

  • 值类型像「纸质文件」——复印一份,原件和复印件各自独立,修改复印件不影响原件。
  • 引用类型像「网页链接」——你把链接发给朋友,你们访问的是同一个页面;页面内容一改,所有人看到的变化。

赋值时的拷贝区别 ​

csharp
// 值类型:复制数据本身
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# 的数值类型按精度和范围细分,选择合适的类型既能节省内存,又能避免溢出。

类型别名字节取值范围默认值适用场景
sbyteSystem.SByte1-128 ~ 1270极小型整数
byteSystem.Byte10 ~ 2550二进制数据、文件流
shortSystem.Int162-32,768 ~ 32,7670小型计数器
ushortSystem.UInt1620 ~ 65,5350端口号、协议字段
intSystem.Int324±21 亿0最常用整数
uintSystem.UInt3240 ~ 42 亿0位操作、ID 编码
longSystem.Int648±922 亿亿0大数值、时间戳
ulongSystem.UInt6480 ~ 1844 亿亿0无符号大数
floatSystem.Single4±3.4×10³⁸,6-7 位精度0f图形学、科学计算
doubleSystem.Double8±1.7×10³⁰⁸,15-16 位精度0d默认浮点类型
decimalSystem.Decimal16±7.9×10²⁸,28-29 位精度0m金融、货币计算
csharp
// 常用数值类型声明
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 类型 ​

csharp
bool isActive = true;
bool hasPermission = false;

bool 仅接受 true 或 false,不能与其他类型隐式转换(这与 C/C++ 不同)。在 C# 中,if (x = 1) 这种「赋值当判断」的写法会直接编译报错,从根源上杜绝了一类经典 Bug。

可空类型 ​

值类型默认不能为 null,但现实业务中「未知」「未填写」是常态。可空类型用 ? 后缀解决这个矛盾。

csharp
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 返回的可选参数,都适合用可空类型建模。

枚举类型 ​

枚举将相关的常量组织为有意义的组,避免「魔法数字」污染代码。

csharp
// 基础枚举
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"

结构类型 ​

结构是轻量级的值类型,适合表示坐标、颜色、复数等「值语义」的数据。

csharp
// 定义结构
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)。

csharp
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
    }
    """;

数组 ​

数组是固定长度、相同类型元素的连续集合。

csharp
// 一维数组
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); // 查找索引

类、接口、委托、记录 ​

csharp
// 类:面向对象的核心
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# 自动完成,无需特殊语法。

csharp
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 可能有精度损失,但编译器认为可接受)。

显式转换:风险自负的强制转换 ​

当转换可能丢失数据或失败时,必须显式声明「我知道风险」。

csharp
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

溢出检查:

csharp
// checked:溢出时抛出 OverflowException
checked
{
    int result = (int)l; // 抛出异常,暴露问题
}

// unchecked:默认行为,溢出静默发生
unchecked
{
    int result = (int)l; // 结果错误,但不报错
}

溢出处理的最佳实践 ​

csharp
// 方式一: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 类:全能但「固执」 ​

csharp
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():精确控制 ​

csharp
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():万能输出 ​

csharp
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日 星期三"

装箱与拆箱:值类型的「跨界旅行」 ​

底层原理 ​

csharp
int value = 42;           // 栈上的值类型
object boxed = value;     // 装箱:值类型 → 引用类型
                          // 1. 在堆上分配内存
                          // 2. 将值复制到堆
                          // 3. 栈上存堆地址的引用

int unboxed = (int)boxed; // 拆箱:引用类型 → 值类型
                          // 1. 检查类型是否匹配
                          // 2. 将堆上的值复制回栈

性能损耗 ​

csharp
// 装箱的隐式发生点(性能陷阱)
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()

优化方案 ​

csharp
// 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 运算符 ​

csharp
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 仅支持引用类型和可空类型

自定义类型转换 ​

csharp
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:字符串转数值的格式异常 ​

csharp
// 用户输入 "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:可空类型转换的空引用 ​

csharp
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:大精度数值转换溢出 ​

csharp
// double 转 decimal 可能超出范围
double huge = 1e30;
// decimal d = (decimal)huge; // 抛出 OverflowException!

// 解决方案:先检查范围
if (huge >= (double)decimal.MinValue && huge <= (double)decimal.MaxValue)
{
    decimal d = (decimal)huge;
}

坑点 4:浮点数转整数的精度陷阱 ​

csharp
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:字符串与字符的混淆 ​

csharp
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 → intint.Parse / TryParseint i = int.Parse(s);推荐 TryParse 防异常
string → doubledouble.Parse / TryParsedouble d = double.Parse(s);注意区域性格式(小数点/逗号)
string → boolbool.Parse / TryParsebool b = bool.Parse(s);接受 "True"/"False"(不区分大小写)
string → DateTimeDateTime.Parse / TryParseDateTime 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 ParseConvert 容忍 null,Parse 严格快速用户输入用 TryParse,内部数据用 Parse
装箱拆箱值类型与 object 互转,有性能成本泛型集合是救星,ArrayList 是历史
as/is 运算符安全类型检查与转换as 失败给 null,is 模式匹配一步到位
自定义转换implicit/explicit operator让自定义类型无缝融入类型系统