1.4 变量、常量与类型推断


1.4 变量、常量与类型推断

本节摘要:本节讲清三组容易混淆的机制——constreadonly 的编译期嵌入差异、var 推断与显式声明的取舍、dynamic 把类型检查推迟到运行时的代价,并顺带把运算符中与类型相关的部分(is、as、空运算符)串成一线。

const 的一桩历史事故

先看一个能说明 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 是最被误解的关键字。它声明的变量类型在编译期完全确定,只是让编译器从右侧表达式推断:

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 提升可读性:var orders = new List<Order>(); 重复写 List 纯属噪音。其二,右侧类型不明显时刻意写出类型:decimal ratio = ComputeRatio(); 让读者不必跳转到方法定义。这条参考线的本质是:var 省的是书写,不该省的是信息。

类型相关的运算符三件套

isastypeof 是与类型系统打交道的三个入口,行为值得逐一确认:

object o = "hello"; if (o is string) { } // 类型测试,返回 bool var s = o as string; // 转型失败得 null,不抛异常 Type t = typeof(string); // 编译期取类型对象(反射入口) Console.WriteLine(o.GetType().Name); // 运行期取实际类型:String

注意 typeofGetType 的分工:前者在编译期解析、参数必须是类型名;后者在运行期读取对象头里的方法表得到真实类型。变量声明为 object 但装着 string 时,typeof 拿的是你问的类型,GetType 拿的是它实际是谁——多态场景下两者可能不同,第二章讲虚方法时分派依据的正是后者。

现代 C# 的 is 已经进化成模式匹配主力:

if (o is string text && text.Length > 3) Console.WriteLine(text.ToUpper()); // text 已是强类型变量

一次完成"测试 + 声明 + 使用",替代了旧的"先 is 后强转"两步。新代码一律用这个形态。

空值运算符:编译器防 NULL 的补丁

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.IsNullOrEmptyis [not] null 模式的组合,别把空值合并当成万金油。

本节要点回顾

  • const 是编译期嵌入:跨程序集引用时不更新编译就会读到旧值,可变配置用 readonly static;
  • var 编译期定型:不是弱类型;dynamic 是运行期绑定,代价与风险都更高;
  • is 模式匹配优于 as 加强转:一次完成测试与声明;
  • typeof 与 GetType 分工:一个问编译期类型,一个问运行期身份;
  • 空运算符三件套加 NRT:把 null 伤害拦在编译期是现代 C# 的主线。

下一节聚焦引用类型里最特殊的一员:字符串为什么设计成不可变,驻留池如何工作,拼接性能的账怎么算。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U