变量与常量
本教程共 100 篇 · 第 8 篇 · 更新于 2026-07-31 · 约 9 分钟阅读
8. 变量与常量
本节目标:分清变量、隐式类型 var、编译期常量 const 和只读字段 readonly,知道什么时候用哪个。
程序说白了就是”数据 + 处理数据”。数据存在哪?存在变量(variable)里。这一章先把”装数据的盒子”讲透。
什么是变量
变量是一块带名字的内存,用来存一个值。你给它起名、规定类型、存入数据,之后随时取用或改动。
namespace CSharpDemo;
int age = 18;
age = 19; // 可以改
Console.WriteLine(age);
这里 age 是变量名,int 是类型,18 是初值。第二行把它改成 19,说明变量的值可变。
为什么需要”变量”这个概念?因为程序要处理的数据在运行前往往不知道。比如一个订单的金额、一个用户的年龄,只有在运行时才确定。如果所有值都写死,程序就只能算同一道题。变量让同一段代码能适配不同数据——这是程序”通用”的根基。你可以把变量想象成快递柜的格子:格子有编号(变量名),里面暂存一件包裹(值),随时取走换一件。
显式类型声明
上面就是显式声明:你明确写出类型 int。编译器据此分配合适的内存并检查类型是否匹配。常见基础类型有 int(整数)、double(小数)、string(文字)、bool(真假)。
显式声明的好处是清楚:读者一眼知道 age 装的是整数。在类型不那么一目了然、或者你想强调”这里就是 int”时,显式写最稳妥。
NoteC# 是**静态类型(static typing)**语言:每个变量一旦声明了类型,就只能装该类型的值。
age = "十八";这种把字符串塞给int的写法会直接编译报错。这层检查在写代码时就帮你挡掉大量低级错误,而不是等运行到才崩。
变量怎么起名
起名不是小事。好的名字让代码自己会说话,差的名字让人猜半天。C# 的约定(也是本教程统一口径):
- 局部变量、参数用 camelCase(首字母小写,如
age、peopleCount)。 - 类名用 PascalCase(首字母大写,如
Person、Calculator)。 - 用有意义的词,避免
a、tmp、x1这种天书。
int userAge = 18; // 好:一眼知道是用户年龄
int a = 18; // 差:a 是什么?
Tip命名的一条心法:名字要回答”它装的是什么”,而不是”它是什么类型”。
age比int1好,price比d好。类型编译器管,名字是写给人看的。
var 隐式类型
如果你嫌写类型麻烦,且编译器能从右边推出来,可用 var 让编译器自己判断:
var name = "小明"; // 编译器推断为 string
var price = 9.99m; // 推断为 decimal
var count = 10; // 推断为 int
var 不是”没有类型”,而是”类型交给编译器猜”。上面三行,编译器分别认定它们是 string、decimal、int,跟手写一模一样。
var 是 C# 3.0 引入的,本意是配合”类型名很长”或”右边一看就知类型”的场景,减少噪音、提升可读性。但它从没有改变类型系统的本质:推断出来的类型从那一刻起就焊死了,你不能把别的东西赋给它。
Tip用
var的两个场景:一是右边写法已明显暴露类型(如new List<string>());二是类型名太长,写了反而碍眼。但读起来类型不清时,宁可显式写,别为了省事牺牲可读性。
Note
var声明时必须立刻赋值,否则编译器无从推断。写var x;会直接报错。
Warning别在”右边也看不出类型”的复杂表达式上用
var,例如var result = Compute(x);——读者得跳进Compute才知道result是什么。这种情况显式写类型更友好。滥用var是新手常见的可读性陷阱。
const 编译期常量
有些值永远不变,且写代码时就确定了,比如圆周率、税率。用 const 定义为编译期常量(compile-time constant):
const double Pi = 3.14159;
const int MaxRetry = 3;
// Pi = 3.14; 错误:常量不能重新赋值
const 有两个硬约束:第一,声明时必须给初值;第二,之后任何地方都不能改。它的价值在于”语义”——看到 const,你就知道这是铁板钉钉的定值,也防止别人手滑改掉。
Note
const的值在编译时就要确定。所以const只能用于能直接写死的类型(数字、字符串、枚举等),不能把一个要运行时才算出的结果标成const。
为什么要有 const,而不直接用普通变量写死?因为普通变量随时可能被改,而 const 把”不可变”这件事刻进了类型系统。编译器还会把 const 直接内联到使用处,连读取字段的开销都没有。更关键的是:它向读代码的人(包括未来的你)宣告”这个值亘古不变”,省去一堆猜测。
readonly 只读字段
readonly(只读)和 const 都”不能改”,但区别关键:
const:编译期就定死,必须写死字面量。readonly:运行期才能定,但一旦构造完就再也改不了。
readonly 用在类的字段(field)上,不能在方法里的局部变量用:
namespace CSharpDemo;
class BankAccount
{
public readonly string AccountId; // 开户后不可改
public decimal Balance { get; set; }
public BankAccount(string id)
{
AccountId = id; // 构造函数里可赋值
}
}
var acc = new BankAccount("A1001");
// acc.AccountId = "B2002"; 错误:构造完就锁死
账号一旦开户就不该变,用 readonly 正合适。它能接收运行时传进来的值(如参数 id),这点 const 做不到。
readonly 解决的是这样一类需求:值不是写死的,要在创建对象时根据外界确定,但确定之后终身不变。const 做不到(它必须写死),而普通字段又太松(谁都能改)。readonly 正好卡在中间,是”构造后锁死”的语义担当。
三者怎么选
| 需求 | 写法 |
|---|---|
| 值会变,类型明显 | var x = ... 或显式 int x |
| 值永远不变,且写代码时已知 | const |
| 值构造后不变,但需运行时确定 | readonly 字段 |
Tip经验法则:默认用普通变量;确定是全局定值用
const;类里”创建后不可改”的成员用readonly。别一上来全写var然后把常量也写成变量,后期容易被人误改。
变量作用域:它在哪能用
变量不是”全局通行”的。它只在声明它的那对大括号(代码块)里有效,出了块就消失。这叫作用域(scope):
void Demo()
{
int outer = 1;
if (outer > 0)
{
int inner = 2; // inner 只在这对 if 括号里存在
Console.WriteLine(outer);
}
// Console.WriteLine(inner); 报错:inner 已超出作用域
}
理解作用域能帮你避免”变量哪来的”混乱,也让内存该释放时释放。局部变量在函数结束时随栈帧一起回收,这也是值类型常驻栈的原因之一(见第 9 章)。
一个综合示例
namespace CSharpDemo;
class Product
{
public readonly int Id;
public string Name { get; set; }
public decimal Price { get; set; }
public Product(int id, string name, decimal price)
{
Id = id;
Name = name;
Price = price;
}
}
const decimal TaxRate = 0.13m;
var p = new Product(1, "笔记本", 9.99m);
var finalPrice = p.Price * (1 + TaxRate);
Console.WriteLine($"{p.Name} 含税价:{finalPrice}");
TaxRate 是编译期常量;Id 是构造后只读;p、finalPrice 是可变变量。三种各司其职。
变量不赋值能用吗
不能。C# 要求” definite assignment(明确赋值)“:局部变量在使用前必须先赋值,否则编译报错。这是 C# 比某些语言安全的地方——不会读到内存里的随机垃圾值。
int x;
Console.WriteLine(x); // 报错:使用了未赋值的局部变量
字段(类里的变量)则不同:不赋值也有默认值(int 默认 0,bool 默认 false,引用默认 null)。但局部变量没有默认值,必须你给。
Tip这个规则帮你避开一大类 bug。写变量时顺手给个初值(哪怕
int count = 0;),既是好习惯,也让编译器闭嘴。
const 能修饰所有类型吗
不能。const 只能用于”编译期就能确定的常量”:基础数值类型、string、枚举、null。你不能写 const DateTime Now = DateTime.Now;,因为 Now 要运行时才算。这类”运行期定值、之后不变”的需求,请用 static readonly:
class Config
{
public static readonly DateTime StartTime = DateTime.Now;
}
static readonly 在第一次用时初始化,之后锁死,正好补上 const 的短板。
命名常量,干掉”魔法数字”
把 3.14、0.13 这类直接写在公式里的数叫魔法数字(magic number):读的人不知道它代表啥。用 const 给它起个名字,语义立刻清晰:
const decimal TaxRate = 0.13m;
decimal total = price * (1 + TaxRate); // 一眼看懂:含税
Tip凡是会在多处用到、或含义不直观的固定值,优先定义成命名常量。这是几乎零成本的代码可读性提升。
常见疑问解答
问:const 和 readonly 到底选哪个?
一句话:值写死在代码里、永远不变 → const(如圆周率);值要运行时才确定、但构造后不变 → readonly(如账号 ID)。拿不准就问”这个值编译时知道吗”,知道用 const,不知道用 readonly。
问:var 会不会让类型变模糊、难维护?
只有当你滥用在”右边也看不出类型”的复杂表达式上才会。规范用法是右边明显(如 new List<int>() 或字面量),此时 var 反而更干净。类型不清的地方,老老实实显式写。
问:局部变量能写成 readonly 吗?
不能。readonly 只修饰类的字段。局部变量要”不可改”,C# 没有 readonly 局部;若需常量语义且编译期已知,用 const(但 const 局部也只能用于编译期常量)。
要点速记
- 变量存可变数据;
var让编译器推断类型。 const编译期定值,声明即赋值且不可改。readonly是构造后锁死的字段,可运行期赋值。- 三者按”是否要变、何时确定”来选。
- 局部变量须先赋值,字段有默认值。
动手练一练
- 定义一个
Product类,用readonly int Id存商品编号、decimal Price存价格,并在构造函数里给Id赋值,验证构造后无法再改。 - 写一个
const decimal TaxRate = 0.13m;,然后用它计算9.99m含税价,体会const在公式里的用法。 - 故意把
var x;单独写一行,看编译器报什么错,理解var必须立刻赋值的约束。
小结与下一步
变量装可变数据,var 让编译器推断类型,const 是写死的定值,readonly 是构造后锁死的字段。命名常量化解魔法数字,作用域决定变量在哪能用。分清楚这些,代码才既灵活又安全。
下一章深入一点:为什么同样是”变量”,有的赋值像复印、有的像共享?这就要讲值类型与引用类型。