C#未来系列文章中的下一条提议希望能够使用扩展属性。这一功能是大家期盼已久的特性,但通常来说被认为是不可行的,因为它会带来内存泄露。
实际上,实现扩展属性所需的基础功能在.NET 4.0 中就已经具备了。这里的秘密在于,你可以使用某个 ConditionalWeakTable 类以保存对象与扩展字段之间的映射。其实创建 ConditionalWeakTable 的目的正是为了提供这种动态类型语言的功能。
Sam Harwell 为我们演示了如何正确地使用这个类:
private static readonly ConditionalWeakTable<t strongbox=""><fieldtype>> _extensionField_f; public static FieldType get_f(T obj) { StrongBox<fieldtype> box; if (!_extensionField_f.TryGetValue(obj, out box)) return default(FieldType); return box.Value; } public static void set_f(T obj, FieldType value) { StrongBox<fieldtype> box = _extensionField_f.GetOrCreateValue(obj); box.Value = value; }</fieldtype></fieldtype></fieldtype></t>
扩展类
以上这种方式虽然能够实现最终的目的,但它的语法并不简洁。因此一种被称为“扩展类”的概念就能够派上用场了。可以通过以下语法定义一个扩展类:
public extension class MyClassExtensions : MyClass
在 MyClassExtensions 中定义的每个方法或属性都会自动成为 MyClass 中的扩展方法或属性。在 MyClassExtensions 中定义成员看起来与在 MyClass 中直接定义成员没有任何差别。这种方式让你能够避免使用旧的语法,即将某个方法定义为静态方法,并在参数中包含一个“this”关键字。
在扩展类中定义的字段和事件将通过上述的 ConditionalWeakTable 所实现。属性可以明确指定对应的字段或是自动实现,在后一种情况下,其内部实现依然依赖于 ConditionalWeakTable 的使用。
静态扩展
由于扩展类概念的存在,使得一种被称为“静态扩展方法”的使用方式也成为可能。它允许你为某个类添加一个静态方法,与你为对象添加成员方法的方式相同。
为了澄清这种用法,Erik Schierboom 这样写道:
比方说,它能够允许我扩展 xUnit 中的 Assert 类。
思考
虽然这种做法在动态类型语言中很常见,但这种能够为某个类任意添加状态的能力还是让某些开发者感到有些不安。HellBrick 总结了人们对于扩展字段的顾虑:
看上去对这种功能的尝试是很自然的事,但我担心这个特性可能会很容易遭到误用。如果你想要为某个类的实例附加一个额外的状态,我认为在大部分情况下,更好的方式是继承这个类,或是为它创建一个包装器。无论是哪种做法,你的意图都很明确。使用某种像魔法一样自动附加的弱字段引用,就不清楚它的生命周期,将这种方式作为类的设计方式或许是一种非常廉价的方案,并且具有一定吸引力。但同时,它可能会导致出现一些复杂的代码,增加日后维护的困难。
对上述说法的一种反对意见认为,如果没有扩展字段,那么也不可能通过扩展提供自动属性。Chrisaut 继续这个讨论:
即使扩展字段这条提议通不过,只要属性的 setter 仍然是允许使用的,就会有开发者尝试着提出自己的解决方案,而其中有半数都存在着一些微秒的错误。比方说,他们可能会创建一个 Dictonary<MyClass, string> 并保存在属性中。额……这样一来这个实例就永远不会被垃圾回收了,这将带来内存泄露。大多数开发者甚至不知道弱引用和 ConditionalWeakTable 的存在。
因此如果我们真的要阻止附加额外状态的行为(也就是不希望引入扩展字段),那么我感觉唯一的方法就是明确地表示只允许属性的 getter,不支持属性的 setter,或者在自动属性中只支持 getter。
这样一来大概就可以了。但我猜使用者会试图打破这一限制,我不确定只支持 getter 是否能够涵盖用户所需的所有场景。或者我们也可以说它已经涵盖了足够多的场景,而剩余的部分不值得实现。
扩展字段的另一个问题是对可回收对象的存储。如果你在 ConditionalWeakTable 中保存了一个可回收(disposable)对象,你需要找到一种方式,在父对象被垃圾回收时显式地回收该对象。ConditionalWeakTable 不会自动为你实现这一功能,而依赖于终结器又存在着风险。
评论