Skip to content

C# 变量与常量:从储物柜到共享地址牌 ​

开篇:为什么需要「容器」? ​

想象你经营一家快递驿站。每天有大量包裹进出,如果没有编号、没有分类、没有固定位置,找一件快递无异于大海捞针。变量和常量就是程序世界的「包裹管理系统」——变量是「可替换内容的储物格」,常量是「贴了封条的专用柜」。

在 C# 中,每一个数据都有明确的「存放位置」和「使用规则」。理解这些规则,能帮你避免「快递放错柜子」「包裹被误拆」这类低级错误,让代码像高效运转的驿站一样井然有序。


一、变量基础:程序中的「储物格」 ​

1.1 变量的核心定义 ​

变量是内存中一块有名字的存储区域,用于存放程序运行过程中可能变化的数据。在 C# 中,变量必须「先声明,后使用」,且声明时必须确定类型(或让编译器推断)。

csharp
// 标准声明语法:类型 变量名 = 初始值;
int studentCount = 25;        // 整数变量,存储学生人数
string className = "三年二班"; // 字符串变量,存储班级名称
double averageScore = 87.5;    // 双精度浮点变量,存储平均分
bool isActive = true;          // 布尔变量,标记是否激活

生活化类比:变量就像超市的储物柜。int 是小柜(放手机),string 是大柜(放背包),double 是加厚柜(放精密仪器)。柜子一旦确定大小,就不能硬塞不匹配的物品——这就是类型安全。

1.2 三种存储位置的底层差异 ​

C# 变量按存储位置分为三类,理解它们的差异是写出高效代码的基础。

┌─────────────────────────────────────────┐
│           C# 变量存储位置示意图           │
├─────────────────────────────────────────┤
│  栈 (Stack) —— 储物柜                    │
│  ├─ int age = 25;         直接存值       │
│  ├─ Point p = new(1,2);   直接存坐标     │
│  └─ 特点:自动分配/释放,速度快,容量小     │
│     (方法调用结束,柜子立即清空)          │
├─────────────────────────────────────────┤
│  堆 (Heap) —— 共享仓库                   │
│  ├─ string name = "张三";  存对象内容     │
│  ├─ int[] arr = {1,2,3};  存数组数据     │
│  └─ 特点:GC 管理,容量大,生命周期灵活     │
│     (多个变量可指向同一仓库位置)          │
├─────────────────────────────────────────┤
│  指针类型 —— 仓库钥匙                    │
│  ├─ int* ptr = &age;      存内存地址     │
│  └─ 特点: unsafe 代码中使用,直接操作内存 │
│     (像直接拿钥匙开仓库,跳过管理员)      │
└─────────────────────────────────────────┘

新手易错案例 1:

csharp
// 错误:混淆值类型与引用类型的赋值行为
int[] arr1 = { 1, 2, 3 };
int[] arr2 = arr1;      // arr2 不是副本,而是指向同一数组!
arr2[0] = 99;
Console.WriteLine(arr1[0]); // 输出 99,原数组被修改!

错误原因:数组是引用类型,arr2 = arr1 复制的是「仓库钥匙」而非「仓库里的货物」。

1.3 变量声明的语法演进 ​

C# 的类型声明经历了从「显式声明」到「智能推断」的演进,最新版本(C# 10)的语法简洁且安全。

csharp
// 1. 显式声明(最传统)
int number = 10;
string text = "Hello";

// 2. var 隐式类型(C# 3.0+)—— 编译器推断类型,编译后仍是强类型
var age = 25;              // 推断为 int
var name = "张三";          // 推断为 string
var list = new List<int>(); // 推断为 List<int>

// var 的限制:必须初始化,且不能为 null
// var x;           // 编译错误!
// var y = null;    // 编译错误!

// 3. dynamic 动态类型(C# 4.0+)—— 运行时解析,绕过编译时检查
dynamic dyn = 10;
dyn = "字符串";      // 允许!类型在运行时改变
dyn.NonExistent();   // 编译通过,运行时抛 RuntimeBinderException!

// 4. 目标类型 new(C# 9.0+)—— 已知类型时省略重复
List<string> names = new();        // 等价于 new List<string>()
Dictionary<string, int> dict = new(); // 简洁清晰

应用场景:

  • var:局部变量,类型名冗长或显而易见时(如 var result = GetData();)
  • dynamic:与 COM 组件、动态语言(Python/JavaScript)互操作,或反射场景
  • 目标类型 new():字段初始化、数组元素创建,减少代码重复

新手易错案例 2:

csharp
// 错误:误以为 var 是弱类型
var num = 10;
num = "字符串"; // 编译错误!var 推断后类型固定为 int

错误原因:var 是「编译时类型推断」,不是「运行时动态类型」。推断结果一旦确定,就不能更改。


二、变量分类:作用域与生命周期 ​

变量的「可见范围」和「存活时间」由声明位置决定。理解作用域规则,能避免「变量未定义」「值意外残留」等常见 Bug。

2.1 块级作用域 ​

由 {} 界定的代码块内,变量仅在块内有效。

csharp
if (true)
{
    int blockVar = 10;  // 块级变量
    Console.WriteLine(blockVar); // 有效
}
// Console.WriteLine(blockVar); // 编译错误!超出作用域

2.2 方法级作用域 ​

方法体内声明的变量,在整个方法内有效,方法调用结束即销毁。

csharp
void Calculate()
{
    int localVar = 100; // 方法级变量
    for (int i = 0; i < 10; i++)
    {
        Console.WriteLine(localVar + i); // 有效
    }
} // localVar 和 i 都在这里销毁

2.3 类级作用域(字段) ​

类的成员变量(字段)与类的实例同生命周期,或静态字段与应用程序域同生命周期。

csharp
public class Student
{
    // 实例字段:每个 Student 对象独立拥有
    public string Name = "未命名";
    private int _score = 0;

    // 静态字段:所有 Student 对象共享,类加载时创建
    public static int TotalCount = 0;
    private static readonly DateTime _startTime = DateTime.Now;
}

// 使用
Student s1 = new Student();
Student s2 = new Student();
s1.Name = "张三";
s2.Name = "李四";
Student.TotalCount = 2; // 静态字段通过类名访问

2.4 静态全局作用域 ​

静态字段在首次访问时初始化,直到应用程序域卸载。常用于全局配置、缓存计数器。

csharp
public static class AppConfig
{
    public static string Version = "1.0.0";
    public static int MaxRetry = 3;
    private static Dictionary<string, string> _cache = new();
}

新手易错案例 3:

csharp
// 错误:在循环外意外累积
int total = 0;
for (int i = 0; i < 3; i++)
{
    int sum = 0;        // 每次循环重新声明,正确
    sum += i;
    total += sum;
}
Console.WriteLine(total); // 输出 0+1+2 = 3

// 对比错误写法
int sumWrong = 0;       // 声明在循环外
for (int i = 0; i < 3; i++)
{
    sumWrong += i;      // 累积:0+1+2 = 3,但若逻辑要求每次重置则出错
}

错误原因:变量声明位置决定其生命周期。需要「每次重置」的变量必须声明在循环体内。


三、常量核心:程序的「封条专用柜」 ​

3.1 const:编译期确定的「死封条」 ​

const 声明的常量在编译时就确定值,直接嵌入到使用它的代码中,像快递单上打印死的条形码。

csharp
public class MathConstants
{
    public const double PI = 3.14159265359;        // 数学常数
    public const int MAX_RETRY = 3;                // 重试次数上限
    public const string APP_NAME = "MyApp";        // 应用名称

    // const 限制:必须在声明时初始化
    // public const int WRONG; // 编译错误!

    // const 限制:仅支持基元类型、枚举、字符串
    // public const DateTime NOW = DateTime.Now; // 编译错误!DateTime 不是编译时常量
}

底层行为:编译器将 const 值直接替换到使用处。假设 PI 被 100 个文件引用,编译后这 100 处都嵌入了 3.14159265359 这个数值,而非引用关系。

3.2 readonly:运行期确定的「活封条」 ​

readonly 字段在运行时初始化,初始化后不可修改。像快递柜的密码——设置一次,之后只能取件不能改密码。

csharp
public class Config
{
    // 声明时初始化(推荐)
    public readonly string Environment = "Production";

    // 构造函数中初始化(常见场景:依赖注入的配置)
    public readonly string ConnectionString;
    public readonly DateTime StartTime;

    public Config(string env, string connStr)
    {
        Environment = env;           // 允许:构造函数中赋值
        ConnectionString = connStr;  // 允许:构造函数中赋值
        StartTime = DateTime.Now;    // 允许:运行时计算
    }

    public void Update()
    {
        // Environment = "Test";     // 编译错误!readonly 不可在方法中修改
    }
}

3.3 static readonly:全局只读配置 ​

static readonly 组合用于「全局共享且运行期确定」的常量,是实际项目中最常用的形式。

csharp
public static class GlobalSettings
{
    // 静态只读:程序启动时计算一次,之后不可变
    public static readonly string AppPath = AppDomain.CurrentDomain.BaseDirectory;
    public static readonly DateTime LaunchTime = DateTime.Now;
    public static readonly int ProcessorCount = Environment.ProcessorCount;

    // 静态只读集合:引用不可变,但内容可变(注意!)
    public static readonly List<string> SupportedLanguages = new() { "zh-CN", "en-US" };
}

// 使用
Console.WriteLine(GlobalSettings.AppPath);
GlobalSettings.SupportedLanguages.Add("ja-JP"); // 允许!集合内容可修改
// GlobalSettings.SupportedLanguages = new();     // 编译错误!引用不可变

3.4 进阶特性 ​

C# 8.0:readonly struct

确保结构体所有成员不可变,避免意外修改。

csharp
public readonly struct ImmutablePoint
{
    public readonly int X;
    public readonly int Y;

    public ImmutablePoint(int x, int y) => (X, Y) = (x, y);

    // 编译器强制:readonly struct 中不能声明可变字段
    // public int Z; // 编译错误!
}

C# 9.0:init 访问器

允许对象初始化后「冻结」,实现「构造后不可变」。

csharp
public class Product
{
    public string Name { get; init; } = "未命名";
    public decimal Price { get; init; }

    // init 仅允许在对象初始化器中赋值
}

var product = new Product { Name = "手机", Price = 3999m };
// product.Price = 2999m; // 编译错误!init 属性初始化后不可修改

3.5 const vs readonly vs static readonly 对比 ​

特性constreadonly(实例)static readonly
确定时机编译时运行时(构造函数)运行时(静态构造函数/声明时)
内存位置嵌入到使用处堆(对象实例中)静态存储区
修改限制绝对不可变构造函数中可赋值一次静态构造函数中可赋值一次
支持类型基元类型、枚举、字符串任意类型任意类型
跨程序集值被嵌入,更新需重新编译所有引用方通过引用访问,更新只需重新编译定义方同 readonly
典型场景数学常数、固定配置依赖注入的配置、对象标识全局路径、环境信息、启动时间

新手易错案例 4:

csharp
// 错误:误以为 const 可以延迟赋值
public class BadExample
{
    public const string Config;

    static BadExample()
    {
        Config = LoadFromFile(); // 编译错误!const 不能在静态构造函数中赋值
    }
}

// 正确:使用 static readonly
public class GoodExample
{
    public static readonly string Config;

    static GoodExample()
    {
        Config = LoadFromFile(); // 正确:运行时加载配置
    }
}

四、实战避坑指南 ​

4.1 循环中的变量作用域陷阱 ​

csharp
// 经典陷阱:闭包捕获循环变量
var actions = new List<Action>();
for (int i = 0; i < 3; i++)
{
    actions.Add(() => Console.WriteLine(i)); // C# 5.0+ 修复:每次迭代捕获新副本
}
actions.ForEach(a => a()); // 输出 0, 1, 2

// 但在旧版 C# 或某些场景下仍需注意
var tasks = new List<Task>();
for (int i = 0; i < 3; i++)
{
    int temp = i; // 显式创建副本,确保捕获正确值
    tasks.Add(Task.Run(() => Console.WriteLine(temp)));
}

4.2 多线程下的只读变量线程安全 ​

csharp
public class ThreadSafeCounter
{
    // readonly 引用本身线程安全,但指向的对象未必
    private readonly Dictionary<string, int> _cache = new();
    private readonly object _lock = new();

    public void Increment(string key)
    {
        lock (_lock)
        {
            _cache[key] = _cache.GetValueOrDefault(key) + 1;
        }
    }
}

// 注意:readonly 字段本身不可变引用是线程安全的
// 但 readonly 集合的内容修改仍需同步

4.3 ASP.NET Core 实战示例 ​

csharp
// Program.cs - .NET 6+ 最小 API 风格
var builder = WebApplication.CreateBuilder(args);

// 常量定义:应用级配置
const string API_VERSION = "v1";
const int DEFAULT_PAGE_SIZE = 20;

// 只读配置:从 appsettings.json 读取,运行期确定
var appSettings = builder.Configuration.GetSection("AppSettings").Get<AppSettings>();

var app = builder.Build();

// 使用常量
app.MapGet($"/api/{API_VERSION}/products", () =>
{
    // 使用只读配置
    var pageSize = appSettings?.PageSize ?? DEFAULT_PAGE_SIZE;
    return Results.Ok(new { Version = API_VERSION, PageSize = pageSize });
});

app.Run();

// 配置类:init 属性确保绑定后不可变
public class AppSettings
{
    public int PageSize { get; init; } = 10;
    public string Theme { get; init; } = "light";
    public IReadOnlyList<string> AllowedOrigins { get; init; } = new List<string>();
}

五、基础自测题 ​

题目 1:以下代码输出什么?

csharp
int a = 5;
object obj = a;
a = 10;
Console.WriteLine(obj);
查看答案

答案:5

解析:a = 10 修改的是栈上的值类型变量;obj 是装箱后的堆上对象,与 a 已无关。


题目 2:const 和 static readonly 在跨程序集引用时的关键差异是什么?

查看答案

答案:const 值在编译时嵌入到引用程序集中,修改 const 定义后必须重新编译所有引用程序集;static readonly 通过引用访问,修改定义后只需重新编译定义程序集。

解析:这是 const 最隐蔽的陷阱。公共 API 中的常量若可能变化,应使用 static readonly 而非 const。


题目 3:以下代码能否编译通过?

csharp
readonly int x = 10;
void Test() { x = 20; }
查看答案

答案:不能。

解析:readonly 实例字段只能在声明时或构造函数中赋值。普通方法中赋值会触发编译错误 CS0191。


题目 4:var 和 dynamic 的核心区别是什么?

查看答案

答案:var 是编译时类型推断,编译后类型固定,享有完整的编译时检查;dynamic 是运行时类型解析,编译时绕过检查,所有操作在运行时绑定,可能抛出 RuntimeBinderException。

解析:var 是「让编译器帮你写类型」,dynamic 是「告诉编译器别管类型」。前者安全,后者灵活但危险。


题目 5:以下代码输出什么?

csharp
var list = new List<int> { 1, 2, 3 };
var copy = list;
copy.Add(4);
Console.WriteLine(list.Count);
查看答案

答案:4

解析:List<T> 是引用类型,copy = list 复制的是引用地址。两个变量指向同一个列表对象,修改相互可见。若需真正副本,应使用 new List<int>(list) 或 list.ToList()。


知识点总结思维导图 ​

C# 变量与常量
│
├─ 变量基础
│  ├─ 定义:命名的内存存储区域
│  ├─ 声明语法:类型 变量名 = 值;
│  ├─ 存储位置
│  │  ├─ 栈:值类型、局部变量 → 自动管理,快速
│  │  ├─ 堆:引用类型对象 → GC 管理,灵活
│  │  └─ 指针:unsafe 代码 → 直接内存操作
│  └─ 声明方式演进
│     ├─ 显式声明(传统)
│     ├─ var 隐式类型(C# 3.0)→ 编译时推断
│     ├─ dynamic 动态类型(C# 4.0)→ 运行时解析
│     └─ 目标类型 new()(C# 9.0)→ 简洁初始化
│
├─ 变量分类(作用域)
│  ├─ 块级:{} 内有效,如 if/for 内部
│  ├─ 方法级:方法体内有效,方法结束销毁
│  ├─ 类级(字段):与对象同生命周期
│  │  ├─ 实例字段:每对象独立
│  │  └─ 静态字段:全局共享,类加载时创建
│  └─ 静态全局:应用程序域生命周期
│
├─ 常量核心
│  ├─ const
│  │  ├─ 编译时确定,嵌入使用处
│  │  ├─ 仅支持基元类型、枚举、字符串
│  │  └─ 跨程序集更新需重新编译引用方
│  ├─ readonly(实例)
│  │  ├─ 运行时确定,构造函数中赋值
│  │  ├─ 任意类型,比 const 灵活
│  │  └─ 适合依赖注入的配置
│  ├─ static readonly(推荐)
│  │  ├─ 全局只读,运行时确定
│  │  ├─ 静态构造函数或声明时初始化
│  │  └─ 适合环境信息、启动时间等
│  └─ 进阶特性
│     ├─ readonly struct(C# 8.0)→ 不可变值类型
│     └─ init 访问器(C# 9.0)→ 对象初始化后冻结
│
└─ 实战要点
   ├─ 循环变量作用域:需要重置的变量声明在循环内
   ├─ 闭包捕获:C# 5.0+ 修复,但仍建议显式副本
   ├─ 多线程安全:readonly 引用安全,内容修改需加锁
   └─ ASP.NET Core:const 版本号 + static readonly 配置 + init 属性