1. 你以为的 this 与它真正的五种身份
先说个我自己面试时经常遇到的场景。我问候选人“this 关键字是干什么的”,十个人里有八个回答“指当前对象”,然后我就接着问“那你给我说说,扩展方法第一个参数前面为什么要加 this”,能答上来的人立刻少了一半。这其实不能怪大家基础不牢,C# 的 this 在教材里通常只作为“指代当前实例”的一种语法糖出现,但往深处走几步就会发现,它在构造函数链、扩展方法、索引器、ref 返回这些场景里各顶各的角色,真实面目远比课本写的复杂。
我刚工作那会儿写 Winform,满脑子都是 this.Text、this.button1.Enabled 这种用法,总觉得 this 就是“为了防止参数名和字段名撞车而存在的加前缀的工具”。后来开始做上位机、写一些偏底层的通讯封装,才慢慢意识到 this 本质上是一个“隐藏参数”,这个认知的转变让我对整个实例方法调用模型都通透了。如果你看这篇文章之前也是那种“大概会用,但说不清原理”的状态,那恭喜你,看完之后你会对 C# 这套类型系统的设计思路有一个显著的提升。
这篇文章适合谁?不止是刚入门的 C# 学习者。如果你已经在用 C# 做上位机、业务系统、Unity 脚本,其实也会从里面找到值得留意的点,尤其是值类型中修改 this 的机制、反射调用时 this 作为参数的传递逻辑,以及事件委托里 this 作为 sender 的本质——这些内容平时文档里写得分散,面试时却是高频考点,工作中理解透了能省不少排查问题的时间。
1.1 从“指当前对象”到“五种身份”
把 C# 的 this 简单理解为“当前对象”,错不错?不算错,但只覆盖了它的一部分使用场景。我习惯把它拆成五类来看,这个分类能帮助记忆,也能帮助你在读到别人代码时更快抓准作者意图:
- 实例方法内引用当前实例成员:obj.Method() 里,方法体内用 this 能找到那个“被调用者的引用”。
- 构造器初始化器:this(...) 用于让一个构造函数在开始前先调用同类的另一个构造函数,形成构造链。
- 扩展方法的第一个参数标记:表示这个静态方法会被“附加”到某个类型上,以实例方法的写法调用。
- 索引器声明:this[int index] 这种属性语法里,this 代表“当前对象的索引器”,不是指成员访问。
- 值类型中作为 ref 参数的载体:在 struct 的方法内部,this 会被编译成对当前变量的引用,允许你对它本身做修改。
这五类看着各不相干,其实逻辑全部统一在“this 不是语法糖,而是真实传递的参数”这条线下面。我们直接看第二层本质。
2. 从调用约定看 this 的本质:编译器如何“塞参数”
很多 C# 开发者没有接触过 C++,对“类成员函数本质是一个带隐藏参数的函数”这种说法毫无概念。这很正常,因为 C# 语法层面把 this 藏得太好了,你写出来的每个实例方法都像天生就该知道自己是“谁的代码”,但 CLR 并不会魔法般地把这些信息塞进方法里,它靠的正是每次调用时悄悄多传的那个参数,也就是 this。
2.1 实例方法调用时发生了什么
假设有一段最简单的代码:
class Device { public string Name { get; set; } public void ShowName() { Console.WriteLine(this.Name); } } var d = new Device { Name = "采集卡" }; d.ShowName();你看到的是一次属性访问和一次方法调用。但如果你尝试从 IL 层面理解,实际上编译器生成的调用序列接近这样:
ldloc.0 // 加载 d 的引用 callvirt instance void Device::ShowName()这个过程中,d 的引用被作为“第 0 个参数”压到了求值栈上,然后进入 ShowName 方法内部。你在方法体里写的 this.Name,本质上是读取第 0 个参数所指向对象的 Name 属性。换句话说,每次调用 d.ShowName(),CLR 做的事和调用一个全局方法 ShowName(d) 在参数传递上是没有本质区别的。
这个认知有什么用?最直接的作用是理解“为什么实例方法里可以放心使用 this,而静态方法里不能”。静态方法没有这个隐藏参数,所以你在静态方法里写 this 编译器直接报错。更深一层,当你做反射时,这种“this 是参数”的理解能帮你完整地解释 Invoke 方法的签名——你调用一个实例方法时为什么第一个参数要传对象实例:
MethodInfo showName = typeof(Device).GetMethod("ShowName"); showName.Invoke(d, null); // 第一个参数 d 就是 this如果被调方法是有参数的,比如 Something(int x),则 Invoke 的第二个参数数组里要按顺序放参数,这也反过来印证了“实例参数和方法参数一起构成完整调用参数列表”这一结论。
2.2 this 在值类型与引用类型中的差异
引用类型里,this 当然是一个引用,指向堆上对象。值类型中最典型的是 struct,你也许已经见过类似写法:
struct Point { public int X; public int Y; public void Move(int dx, int dy) { X += dx; Y += dy; } }Point 是值类型,当它作为局部变量存在时,它的字段存在栈或寄存器上。Move 方法内写 X += dx 时,这个 X 到底改的是谁?如果把 this 仅仅理解成“当前对象”,这里很容易产生迷惑。实际上,struct 中 this 的本体是“当前变量本身”,编译器以传引用方式传入,所以方法内对字段的修改会真实作用于调用方那个变量上,而非一份副本:
var p = new Point(1, 2); p.Move(3, 4); Console.WriteLine($"{p.X}, {p.Y}"); // 输出 4, 6这一点初学者通常会踩坑。假如你把上例中 Point 的字段改成属性,然后在 Move 方法里写 X += dx,仍然能编译;但如果你在方法里写 this = new Point(...),则普通方法里是会报错的,只有构造函数内部才允许对 this 做这种赋值式的初始化。实际工作中做工业控制时我常用这种方式封装小结构体,用来表示机械轴的坐标并暴露移动方法,C# 对 struct 方法内 this 的按引用传递保证了方法调用后局部变量真的被修改,这在性能与可读性上很有价值。
2.3 this 为 ref 变量带来的读改写能力
上面说编译器会把 this 以按引用方式传入 struct 方法,这句话完整的翻译是:对于值类型实例方法的调用,this 会被编译成托管引用,等价于 ref 参数。
如果你还不信,来看一个更“野”的写法。值类型方法内部可以给 this 赋值吗?C# 从 7.2 开始,在结构体方法内可以直接对 this 赋值(要求该结构体没有只读限制),前提是不在 readonly 修饰的方法里:
struct Counter { public int Value; public void Reset() { this = new Counter(); // 合法,相当于把整个结构体变量重置 // 实际等价于 Value = default; } }这个写法看着奇怪,却能帮我们验证一件事:this 不是“对象的别名”,而是“当前变量的别名”。一个结构体变量被 Reset 后,不仅 Value 归零,整个变量的所有状态都回到默认值,行为与直接对局部变量赋值 new Counter() 完全一致。这种机制对编写无 GC 压力的紧凑数据模型有大用,也是 Unity DOTS 领域比较常见的 C# 技巧。
理解“this 是隐藏参数”之后,很多高级用法就不再是死记硬背的东西了。接下来我们把五种典型场景逐个过一遍,每一处都标注了背后的“为什么”,方便各位以后写码时不光会用,还能给同事讲明白原理。
3. this 在工程中的高频用法逐个拆解
3.1 构造器链:this(...) 如何复用初始化逻辑
在 C# 里,构造函数与构造函数之间可以通过 this 互相调用。先看例子:
class SerialPortClient { private readonly string portName; private readonly int baudRate; public SerialPortClient(string portName) : this(portName, 9600) { } public SerialPortClient(string portName, int baudRate) { this.portName = portName; this.baudRate = baudRate; } }第一个构造函数签名后跟了 : this(portName, 9600),它的意思是“在执行本构造函数体之前,先执行同类的另一个构造函数,传入 portName 和 9600”。这样的好处清楚明了:默认波特率、校验位、停止位的组合只需要在一处写初始化逻辑,其他形参较少的构造函数都往那头“带参跳转”,避免复制粘贴造成的后续维护噩梦。
需要特别强调的是,this(...) 在语法上必须写在这个构造函数签名后面、方法体大括号之前,而且它前面不能加其他语句。这一点是 C# 编译器定的规则,不是你的选择性问题。如果你想让一个构造函数先做点自己的初始化、再调另一个构造函数,那是做不到的——必须先调 this(...),再进入方法体。如果确实需要灵活初始化,更合理的方式是走工厂方法或静态方法:
class TcpClientWrapper { public static TcpClientWrapper Connect(string host, int port, int timeout = 3000) { var wrapper = new TcpClientWrapper(host, port); wrapper.ConnectWithTimeout(timeout); return wrapper; } }工程上我见过很多人滥用构造器链,把六七个构造函数垒在一起,每个参数都不一样,读起来比不看还累。我的个人标准是:构造参数如果超过三组或者某个参数之间没有自然的“默认 - 升级”关系,就直接用命名参数 + 对象初始化器,或者提供带语义的静态工厂方法,别硬把 this(...) 用成炫耀技巧的手段。
3.2 扩展方法:第一个参数前那个 this 才是灵魂
扩展方法应该是最容易被忽略 this 的场景之一。语法上说,扩展方法是静态类里的静态方法,只是第一个参数前加了 this:
public static class StringExtensions { public static bool IsNullOrWhiteSpace(this string input) { return string.IsNullOrWhiteSpace(input); } }调用时,你可以直接写:
string s = " "; bool empty = s.IsNullOrWhiteSpace();编译器其实将其编译成 StringExtensions.IsNullOrWhiteSpace(s) 的静态调用,而那个隐藏的调用目标 s 就是方法体内可访问的 input 参数。读者看到这应该有所触动:方法里的 this 关键字和扩展方法参数前的 this 关键字本质上同源同根,都是告诉编译器“这个参数在语法层会被当作当前实例使用”。
从 8.0 开始,扩展方法可以作用于任何类型,包括接口、结构体、枚举。这件事在写通用逻辑时极度好用。我在上位机开发里常用的一个扩展方法是把 byte[] 转成十六进制字符串:
public static string ToHexString(this byte[] data) { if (data == null || data.Length == 0) return string.Empty; var sb = new StringBuilder(data.Length * 2); foreach (byte b in data) sb.Append(b.ToString("X2")); return sb.ToString(); }但警告一句:扩展方法虽然“看起来像实例方法”,它并不能访问目标类型的私有成员,本质还是静态方法。你在阅读或 debug 时如果忘了这层包装,很容易产生“它是不是自带访问类内部状态的能力”的误解,进而写出依赖扩展方法“包装类特殊状态”的诡异代码,最后坑到自己。
3.3 索引器 this[int index]:给对象一个“下标”
this 也可以用来定义索引器。这里 this 不再是指当前对象,而是一种声明语法,代表“当前类的带参属性访问入口”。无论类、结构体还是接口,都能声明索引器:
class DataBuffer { private byte[] _buffer = new byte[256]; public byte this[int index] { get => _buffer[index]; set => _buffer[index] = value; } }这样外部调用 dataBuffer[3] 就像访问数组一样自然。如果你想处理二维映射或者按名称索引,也能通过索引器重载实现更多参数形式。索引器参数甚至可以不是 int:
class SensorValues { private readonly Dictionary<string, double> _values = new(); public double this[string sensorName] { get => _values.TryGetValue(sensorName, out var v) ? v : double.NaN; set => _values[sensorName] = value; } }从设计上看,索引器非常适合暴露一种“集合访问语义”但本身不是集合的类。比如工业视觉项目里对多个相机的参数做按名称访问时,索引器能显著减少使用方代码量,让主流程更清晰。不过也别贪杯——一个类里如果又搞了三四个不同签名的索引器,可读性反而下降。我的原则是当你能用属性或者方法表达清楚得更好时,就不要强上索引器。
3.4 ref 返回与 ref 局部变量:让 this 更“锋利”
从 C# 7.0 开始,this 在索引器、属性和方法中还能配合 ref 返回使用。这带来了一个很“性能向”的用法:把对象内部某个字段的引用直接暴露给调用方,让调用方不经过属性拷贝直接修改原值。例如有一个数组作为底层存储的类:
class SampleStorage { private float[] _samples = new float[1000]; public ref float GetSampleRef(int index) => ref _samples[index]; } var storage = new SampleStorage(); ref float s = ref storage.GetSampleRef(10); s = 42f; // 直接修改 storage 内部 _samples[10]这里表面上没写 this,但方法解析时其实还是通过 this 确定了对象实例,并返回了 _samples[10] 的托管引用。ref 局部变量可以一直持有着这个引用,之后的操作就少了冗余的数组索引检查,在热路径性能比较高时有用。但不要为了炫技硬上——ref 返回让调用方可以直接操作类内部状态,破坏了封装,一旦出 bug 排查起来很痛苦。我建议只在私有辅助方法或经过严谨设计的内部结构中使用,公开 API 慎用。
3.5 在委托、事件、LINQ 里 this 作为参数传递
事件委托中,你写事件处理方法时用的 sender 参数实际上就是 this 的一种“化名”。比如:
button.Click += OnButtonClick; private void OnButtonClick(object sender, EventArgs e) { var btn = sender as Button; if (btn != null) btn.Text = "已点击"; }当按钮触发点击事件时,sender 就是触发事件的对象——从窗体类内部看,这个对象和 this.button1 是同一个实例。理解 “sender 就是 this,只是通过参数传出来” 能帮助你写出更通用的事件处理方法。比如多个按钮共用同一个事件处理函数时,你可以依赖 sender 区分到底点了哪个按钮。这是 Winform / WPF / ASP.NET 里最常见的实践之一。
LINQ 表达式中的 lambda,与 this 的关系大多数情况下是“捕获”关系:lambda 内访问当前实例的字段或方法时,编译器会生成一个闭包类,把 this 保存进去。很多不好排查的内存问题、事件重复注册导致的多次执行问题,根源都出在这个隐藏的 this 捕获上。尤其是事件订阅时:
class MainViewModel { public void Subscribe() { Service.Instance.DataReceived += (s, e) => Handle(e); } }如果 Subscribe 被调用多次,每次都会创建一个新的闭包对象并捕获 this,然后重复订阅。解决办法之一就是缓存 lambda 字段:
private readonly EventHandler<DataEventArgs> _handler; public MainViewModel() { _handler = (s, e) => Handle(e); } public void SubscribeOnce() { Service.Instance.DataReceived += _handler; }这是一个非常典型的 this 捕获相关坑。知道了 this 在闭包生命周期里的角色,排查这类事件的多次触发问题就快得多。
4. 那些容易踩坑的 this 陷阱与边界条件
4.1 值类型里修改 this 字段:是特性还是坑
前面提过在 struct 内部,this 可以被理解成“当前变量的引用”,因此值类型方法内修改 this 字段本质是修改原变量。如果你对一个定义在数组里的结构体元素调用方法,会怎样?
var points = new Point[10]; points[0].Move(1, 2);这在 C# 里是允许的,因为索引器返回数组元素地址,方法调用会直接引用该元素进行修改。如果换成 List<Point> 呢?
var points = new List<Point>(); points.Add(new Point()); points[0].Move(1, 2); // 可能你预期修改了元素,实际没有修改成功原因是 List 的索引器返回的是元素的副本,Move 方法修改的是那个临时副本,原列表里的点仍然纹丝不动。这个坑我见过不止一次,类似场景在各个 C# 论坛的提问中反复出现。解决方案通常是让 Move 返回新值并重新赋值,或者把 Point 改成 class,又或者使用数组而不是 List。实际工程项目里,我的建议是:凡是在容器中存放并需要原地修改的数据,直接用 class,别在 struct 上硬扛。性能收益往往远小于维护成本和认知成本。
同样,readonly struct 和 readonly 成员修饰符对 this 也有影响。在 readonly 修饰的方法内,this 会被视为只读变量,任何通过 this 修改字段的行为都会触发编译错误,这限制了一些比较“暴力”的写法。对普通开发者来讲,知道这一点有助于理解为什么一个结构体方法明明看起来逻辑没问题,编译却提示无法修改。
4.2 命名冲突:this 是解决歧义的钥匙,但不是银弹
this 最传统的用途是区分参数与字段。类似这样:
public void SetName(string name) { this.name = name; }这个场景最简单,日常最多。但如果你以为加上 this 就万事大吉,那就错了。假设基类和派生类都有同名字段或属性:
class Animal { public string Category { get; set; } } class Dog : Animal { public string Category { get; set; } public void Print() { Console.WriteLine(Category); // 输出 Dog.Category Console.WriteLine(this.Category); // 仍然输出 Dog.Category Console.WriteLine(base.Category); // 输出 Animal.Category } }在派生类里 this.Category 只会绑定到当前类的成员,即使当前类没有定义 Category,这个绑定也会去基类找。如果你以为 this 能强制访问基类同名成员,那一定困惑于每次都拿到派生类的值。此时要用 base 关键字而不是 this。写出正确的多态字段访问,需要先弄清类型成员的隐藏规则。
另一个有趣场景是在构造函数中使用 this 时结合静态成员或实例初始化顺序。实例字段初始化器会在构造函数体内语句执行之前运行,而 this(...) 是发生在字段初始化器之后的。这意味着 base(...) 和 this(...) 的调用时机在不同层级之间有一定顺序要求。多数情况下不用太细究,但如果你读过一些由代码分析工具带来的“构造函数中包含虚成员调用”警告,应该能明白顺序绕行的意义。
4.3 为什么静态方法、静态类、匿名类型里没有 this
静态方法和静态类中写 this 会编译报错 CS0027,原因很简单:静态调用无需传递实例,自然没有“当前对象”。匿名类型其实也有类似情况。匿名类型是 sealed 类,但它的成员都是只读属性,这里不会出现 this 关键字,因为匿名类型没有显式定义行为方法,不存在 this 方法内用法的诉诸场景。
扩展方法还有一个特殊情境:静态类中的扩展方法照样不可以在扩展方法体里访问目标实例的其他非公开成员。表面上你能使用 this 传入实例,但它只是静态方法的一个普通参数而已。不认清这一点,在写封装时容易错手写出“我明明已经拿到了 this,为什么还访问不了它的 private 字段”的疑问。
再看一个比较反直觉的点:this 关键字可用于扩展方法的第一个参数但不能再在同一个扩展方法里用它引用别的内容,因为此 this 实际是参数名修饰。例如:
public static string Double(this string s) => s + s;s 就是“被扩展的实例”,方法体里要用参数名 s,而不能用 this。有些新手以为在扩展方法内部还能照旧把 this 当“当前对象”,一写就报错——这正好加深了“this 就是第一个参数”这一层理解。
4.4 和 Java 的 this 相比,C# 多了哪些“私货”
Java 和 C# 语法很像,this 关键词基本作用也类似,都表示当前实例。但 C# 的 this 能用在构造器链、索引器声明、扩展方法这几个场景,Java 则用 this(...) 只能实现构造链,没有索引器语法、也没有真正的扩展方法(只能用静态工具类做替代)。这说明 C# 把 this 的使用范围设计得更广,也更强调类型自身的“可调用”和“可扩展”能力。
如果是从 Java 转 C# 的读者,我建议你尤其注意扩展方法和索引器这两个差异点,因为它们最影响代码风格。Java 里你习惯写 Objects.requireNonNull(x),C# 里更自然的做法是扩展方法 x.RequireNonNull()。代码表达更内敛,但第一次在“非本类定义的方法”内部理解 this 时会有错位感,认清楚本质后就迎刃而解了。
5. 从 this 延伸出去的编码习惯与代码嗅觉
5.1 用“谁在调用”的角度看待 this 提升代码嗅觉
当你习惯了从“隐藏参数”的角度理解 this 后,解读代码的方式会发生改变。以前看一个成员方法可能只关心它的返回值和工作流,现在你会更关心一个问题:这个方法有没有副作用地依赖或修改了对象自身状态?更进一步,看到一段在方法内频繁使用 this 来访问成员变量的代码,你会自然产生“这段逻辑是否过度耦合实例状态”的警觉,并从方法设计角度思考是否应该拆成更纯粹的静态工具方法或独立对象。
举个例子,我之前维护过一个上位机通讯类,它有 Send(byte[] data)、ReceiveAsync()、ParseFrame(byte[] bytes) 三个方法。ParseFrame 里调用了 this.currentChannel、this.frameQueue.Push() 等一堆实例成员,导致单元测试时无法单独测试协议解析逻辑。后来我把 ParseFrame 改成静态方法或者带有纯输入输出的方法,完全不需要 this,测试难度立刻断崖式下降。不要小看 this 出现频率对代码可测试性的提示价值——这是很多团队做代码重构时一个轻松可行的入口指标。
5.2 什么时候该写 this,什么时候不写
团队规范里常有“是否强制写 this.前缀”之争。坚持写的人认为提高可读性,反对的人觉得冗余。我在中大型项目里的长期感受是:宁可统一风格也不要混用。只要编译器为你做好了变量名遮蔽的检查,字段与参数同名却没有写 this 时其实是非常容易出错的写法——因为你以为在给字段赋值,实际却把局部变量赋给了自己。这种 bug 在复杂一点的方法逻辑里只靠人肉看很容易漏掉。
所以在能显式写 this 区分参数和字段的情况下,我的个人习惯是倾向显式写出 this。尤其当构造函数、属性和方法中参数名与字段名高度相似时,this 前缀就像给读者一个信号:这里改的是实例状态,不是局部临时物。而当你调用某个实例方法或访问某个实例属性时,如果不涉及歧义,不写 this 当然可以,代码更简洁。最终选择看团队规范,关键是保证同一代码库内风格一致。
5.3 进阶联想:this 与闭包捕获、异步状态的纠缠
最后再聊一个和 this 没那么直接、但又脱不开关系的点:异步方法中的 this。一旦你在 async 方法里通过 this 访问了字段,这段逻辑就可能在某个未来的时间点被续延执行。这里的“this 引用”会被异步状态机捕获,时刻对象生命周期不结束,引用就不会释放。
这一点尤其容易形成事件循环里的长期引用链。例如某个服务类把自己订阅到一个全局静态事件里,它的实例方法里用了 this 访问实例状态,那 GC 根通过静态事件 -> 委托 -> 实例 this 这一整条链就把对象拖住了。很多人排查“为什么对象回收不了”“为什么内存不断上涨”时,最终会定位到相似的模式上。处理方法是:在不需要时解除事件订阅,或用 WeakReference 包装 this 再订阅。这里的 this 已经不是语法问题了,而是对象生命周期管理的一根引线。
同样,在 async void 事件处理器中,this 捕获后如果抛异常,异常会被推向同步上下文,也可能让程序状态难以预判。每当写出这样的代码时,把 this 换成一个显式变量名并想清楚:这个对象存活多久?事件还链在谁身上?往往能提前规避几个疑难 bug。
6. 把 this 放进调试、反射与性能里看
6.1 调试时如何观察 this
Visual Studio 调试器里的“局部变量”窗口通常能看到名为 this 的项,其值就是当前实例。在非实例方法的上下文中调试器则不会给你显示 this。这个小细节可以帮助我们迅速判断当前断点是不是停在了真实例方法里。曾经有一次我怀疑一个静态方法被调用后改了某个实例值,后来打断点发现根本不在实例方法里,于是引发了一个思考——方法设计是不是有问题。
如果做更底层的调试,你可以通过 SOS 扩展在 dump 分析时查看线程栈上的 this 参数,也可以通过 WinDbg 的 !clrstack -a 看到方法的参数列表里包含 this。这种方式能帮你定位一些非常隐蔽的内存泄漏或悬空引用问题。把这些工具和 this 的调用约定结合来看,你会发现调试体验上了一个层次。
6.2 反射调用与 this 参数的类型匹配
上面提过 MethodInfo.Invoke 的第一个参数如果针对实例方法就要传对象实例。如果传错类型的对象呢?运行时会抛 TargetException。这类报错表面上和 this 无关,但排查时如果能意识到“Invoke 的第一参数就是 this”,就能立刻明白为什么传入一个基类实例有时可以、有时不行——这完全取决于你要调用方法的声明类型和实际实例是否兼容。
反射中也存在获取扩展方法的情况。扩展方法这个 this 参数在反射里就是一个普通参数,没有任何特殊标记。但你若用编译后的静态调用方式去 Invoke 它,仍然需要把扩展目标作为普通参数传入。所以很多封装库在处理“给类型附加方法”时,本质上都是先判断第一个参数是否有 ExtensionAttribute,再用静态方法调用方式去执行,而不是实例调用。理解这一点后,你自己在做代码生成或 AOP 相关工作时会顺手很多。
6.3 性能:this 传递会有成本吗
这里可以放松一下神经:在 JIT 优化之后,实例方法调用上的 this 传递几乎不会产生额外的运行成本。值类型方法中 this 按引用传递也不会导致装箱,前提是被调用方法确实被 JIT 识别为非虚方法且调用目标类型是确定的。C# 里对 readonly struct 参数可以避免防御性拷贝,这在 .NET Core 3.0 以后的意义更为明显。常规业务代码不需要为了“优化 this 的传递性能”而搞特殊设计,先把可读性和正确性抓好。
但有一个点值得注意:当你把一个值类型实例通过接口引用来调用方法时,通常会触发一次装箱,装箱后的 this 就是堆上副本的引用。这个特性与值类型的修改语义混合在一起,会产生前面“List 元素调用 Move 没有生效”类似的隐蔽问题,只不过这次的根因是接口分派。尽量避免让 struct 实现接口后又期待它的方法能修改原始变量,这一条对工业设备和游戏领域做高性能数值运算时尤为重要。
从编码、调试到性能分析,this 这个关键字几乎贯通了 C# 程序运行的主链路。它不是英语里 that 的对应物那样只承担指示功能,而是一个承载了实例方法调用机制、构造链设计、类型扩展能力与值类型可变性的枢纽。日常写码时养成留意 this 的习惯,往往能顺藤摸瓜地提前识别出很多设计层面的隐患,这大概是它带给我最大的收益。以后你再看到代码里这个小小的关键字,不妨多想一步:这个 this 出现在这里,究竟是因为语言机制的要求,还是代码设计本该如此?