本节摘要:本节讲清三组容易混淆的机制——
const与readonly的编译期嵌入差异、var推断与显式声明的取舍、dynamic把类型检查推迟到运行时的代价,并顺带把运算符中与类型相关的部分(is、as、空运算符)串成一线。
先看一个能说明 const 本质的真实 bug 模式。某个库发布时定义了:
public const int MaxConnections = 100;
引用方工程编译时,编译器把 100 直接嵌进了引用方的 CIL——const 不是对变量的引用,是字面量的替身。后来库作者把值改成 200 重新发布,引用方只更新了库文件却没重新编译,运行时读到的仍是 100。readonly 没有这个问题:
public static readonly int MaxConnections = 100;
readonly 在运行时从字段读取,库更新即生效。结论:对可能随版本变化的值(配置上限、开关阈值)用 readonly static,只对真正的数学常数(每打十二个、每小时六十分钟)用 const。这个选择的本质是:你想让值在编译时固化,还是运行时读取。
const 还有一条限制:右侧只能是编译期可算出的表达式(字面量、其他 const、字符串拼接)。const DateTime LaunchDay = ... 直接编译报错,因为 DateTime 的构造发生在运行时——编译器做不了这道题。
var 是最被误解的关键字。它声明的变量类型在编译期完全确定,只是让编译器从右侧表达式推断:
var count = 42; // int,确定且不可变 var name = "hello"; // string // count = "text"; // CS0029:无法把 string 隐式转为 int
第三行报错证明了 var 变量不是"随便装什么"的盒子,它的类型在声明那一刻已经钉死。var 与动态类型的区别是本节的核心对比:
| 维度 | var | dynamic |
|---|---|---|
| 确定时机 | 编译期 | 运行时 |
| 错误暴露 | 编译报错 | 运行时抛 RuntimeBinderException |
| 底层机制 | 纯语法糖,产物与显式声明一致 | 编译器生成动态调用站点,运行时绑定 |
| 性能 | 零开销 | 每次调用经历绑定器缓存查找 |
dynamic 的典型场景是与动态语言或 COM 组件互操作(调用 Office 自动化接口时免写大量强制转换)。业务代码里用 dynamic 传递数据,等于放弃了编译器最强的安全网,出了问题要到运行时才炸,排查成本高得多。我的建议很直接:能用 var 不用 dynamic,能定类型早定。
什么时候用 var 是团队风格问题,但有两条经验值得参考。其一,右侧类型一目了然时用 var 提升可读性:var orders = new List<Order>(); 重复写 List 纯属噪音。其二,右侧类型不明显时刻意写出类型:decimal ratio = ComputeRatio(); 让读者不必跳转到方法定义。这条参考线的本质是:var 省的是书写,不该省的是信息。
is、as、typeof 是与类型系统打交道的三个入口,行为值得逐一确认:
object o = "hello"; if (o is string) { } // 类型测试,返回 bool var s = o as string; // 转型失败得 null,不抛异常 Type t = typeof(string); // 编译期取类型对象(反射入口) Console.WriteLine(o.GetType().Name); // 运行期取实际类型:String
注意 typeof 与 GetType 的分工:前者在编译期解析、参数必须是类型名;后者在运行期读取对象头里的方法表得到真实类型。变量声明为 object 但装着 string 时,typeof 拿的是你问的类型,GetType 拿的是它实际是谁——多态场景下两者可能不同,第二章讲虚方法时分派依据的正是后者。
现代 C# 的 is 已经进化成模式匹配主力:
if (o is string text && text.Length > 3) Console.WriteLine(text.ToUpper()); // text 已是强类型变量
一次完成"测试 + 声明 + 使用",替代了旧的"先 is 后强转"两步。新代码一律用这个形态。
null 被称为"十亿美元的错误",C# 近年的演进大半在补这个坑。三件武器:
string name = maybeName ?? "匿名"; // 空合并:左侧为 null 取右侧 int? len = order?.Customer?.Name?.Length; // 空条件链:任一环节 null 整体为 null if (name is null) { } // 空检查的推荐写法
更根本的是可空引用类型(NRT):工程文件开启后,string 默认不可空,string? 才允许 null。这是编译期静态分析——你在不可空变量上和 null 比较,编译器会发警告甚至提示分支不可达。它不改运行时一行代码,却把绝大多数空引用异常拦在了编译阶段。新工程应默认开启,老工程可以逐文件启用。
⚠️ 常见坑:
??只认 null,不认"空字符串"或"零"。"" ?? "匿名"返回空串。判断业务上的"空"要用string.IsNullOrEmpty或is [not] null模式的组合,别把空值合并当成万金油。
下一节聚焦引用类型里最特殊的一员:字符串为什么设计成不可变,驻留池如何工作,拼接性能的账怎么算。