漫谈.Net关键字系列之一Sealed与Final
Sealed与Final修饰符其实并不是一个语言平台的产物,他们有着各自所属的语言环境,但这两个关键字都是.Net平台中不可或缺的,那么二者用法几何,随本文一探究竟。
一.Sealed
sealed 修饰符可以应用于类、实例方法和属性。用于类时,该类被称为密封类,密封类不能被继承;用于方法时,该方法被称为密封方法,密封方法会重写基类中的方法;sealed修饰符应用于方法或属性时,必须始终与override一起使用;结构是隐式密封的,因此它们不能被继承。
● 描述方法:
//Error: cannot be sealed because it is not an override
public sealed string func()
{
return "";
}
//OK
public sealed override string func()
{
return "";
}
● 继承中的方法:(TestChild2中无法重写任何方法)
●描述属性:
public sealed override double Hours
{
get { return 0.1; }
set { }
}
●描述变量:
//Error The modifier 'sealed' is not valid for this item
sealed override string a;
●描述接口:
interface Itesta
{
//Error cannot be sealed because it is not an override
sealed string Geta();
}
● sealed能提高性能优化?
有一些朋友认为当元素被标记为sealed时,有助于系统运行性能的提升,其理由有2:
1.有助于JIT内联。
2.消除了协变与逆变和后期绑定,使CLR直接执行这个实例。
这看似是有些道理的,可这样做又会提升多少性能,提升性能的同时又损失了什么呢?
先说说“第1点”,促使JIT内联代码的因素有很多,JIT不会因为一个类是sealed,就去装入其中的内容(详见.Net Discovery 系列之六--深入浅出.Net实时编译机制(下),这也不符合程序局部性原理。http://www.cnblogs.com/isline/archive/2009/12/27/1633453.html),而对于sealed类内部方法的内联,很大原因也是由于方法槽映射关系决定的,sealed作用有待考证。
第2点,由于sealed类不可派生或被继承,所以的确在运行时省去可CLR一些额外的工作,但是这些工作只是一些类似于“寻址”的工作,因为虚拟方法表已经完成了运行时与编译时的对应关系,纯粹的运行时只是在寻找这些关系而已,所以sealed省去的只是一部分较为复杂的寻址关系,因为即使没有继承,也不可避免一个方法表的应用。
而这样做又损失了什么呢?大家想想,面向对象的原因是什么?是提升性能吗?显然不是,面向对象的只是高级语言层面的,最终运行的代码都是以顺序流程的方式出现,面向对象的本质是“抽象”,它解决的是软件产品的“控制”问题,变不可控为可控,变不可预测的风险为可预测的风险,所以如果因为要提升性能,而把大部分类都sealed化,岂不是大大削弱了面向对象的抽象能力呢?
● Sealed不能同时abstract?
也许在高级语言中抽象须实现与密封不可继承是一对矛盾者,但IL暴露了一些不一样的细节,让我们来分析一下这段IL代码:
这段代码简单得很,就是声明了一个类,然而这个类却是abstract和Sealed的,猜猜这个类用了什么修饰符修饰它?
好吧,其实高级语言中对应的修饰符就是static。
static类不能被实例化(abstract)亦不可派生(Sealed),我想abstract同时Sealed也未尝不可,但这样做会使语义出现二义性,为避免这种效果才规定在编辑器中不可abstract+Sealed,static修饰类的初衷我想也是如此,实际上static在修饰类时,就是一个包含了实现的abstract+Sealed的类,这个类不能被实例化也不能派生出新的类。
二.Final
final修饰符来限定变量、字段、方法和类。用于变量时,该变量只能赋值一次,不可修改;用于方法时,该方法不能被重写或隐藏;用于类时,该类不能被继承。
接口的成员是不能使用该关键字的,道理和不能在abstract类使用final一样。
值得一提的是,如果使用final修饰类中的字段,那么该字段必须在构造函数中赋值,否则使用类实例调用的方式是无法对该字段进行赋值的,道理很简单,类在实例化时,会为每一个成员字段赋初值,之后你如果再通过实例方式调用该final字段,就属于二次赋值的情况了,这种情况是不允许的。在构造函数中为final变量赋值的方法叫做“延时赋值”(Java),相应的final变量叫做“空白final”(Java)。
Final并不是一个C#中的关键字,但经常在C#面试题中出现,例如说说“Final、Finally、finalize的区别”,其实这已经超出C#的范畴,这三个关键字分别考核了J#、.Net 容错方法、.Net垃圾收集机制,奇怪的是,每次我面试C#程序人员时,大部分人员对Final这个关键字并无陌生之感,相反却答得头头是道,看来来面试之前,早在网上有所预习,呵呵。
例子(摘自MSDN,已做翻译):
public class Value
{
public int i = 1;
}
public class FinalData
{
//可认为等同于编译时常量
final int i1 = 9;
static final int i2 = 99;
//public 常量:
public static final int i3 = 999;
//不可作为编译时常量:
final int i4 = (int)(Math.random() * 11);
static final int i5 = (int)(Math.random() * 11);
Value v1 = new Value();
final Value v2 = new Value();
static final Value v3 = new Value();
// 数组:
final int[] a = { 1, 2, 3, 4, 5, 6 };
public void print(String id)
{
System.out.println(id + ": " + "i4 = " + i4 + ", i5 = " + i5);
}
public static void main(String[] args)
{
FinalData fd1 = new FinalData();
// Error: Can't change value! (i1被描述为fianl的)
// fd1.i1++;
// OK. Object isn't constant(虽然v2是fianl的,但其中的变量并不受此约束)
fd1.v2.i++;
// OK. Not final.
fd1.v1 = new Value();
for (int i = 0; i < fd1.a.length; i++)
{
fd1.a[i]++; // OK. Object isn't constant.(与上面那个v2一样,数组是final的,但数组元素不受约束)
}
// Error: Can't change handle! (v2是final的)
// fd1.v2 = new Value();
// Error: Can't change handle! (v3是static final的,等同于常量)
// fd1.v3 = new Value();
// Error: Can't change handle!(数组本身是final,不可new)
// fd1.a = new int[3];
fd1.print("fd1");
System.out.println("Creating new FinalData");
FinalData fd2 = new FinalData();
fd1.print("fd1");
fd2.print("fd2");
}
}
答案:
fd1: i4 = 0, i5 = 7
Creating new FinalData
fd1: i4 = 0, i5 = 7
fd2: i4 = 8, i5 = 7
总结:final是J#中的一种修饰符,在VS2008及以后版本中就放弃J#了,它与sealed不同的是fianl可以修饰变量,而sealed则不能,不过你可以通过readonly关键字来实现。
关于二者对性能的提升作用,我认为有待考证,从理论层面来讲,为难以证明的性能因素而特意使用此关键字有些得不偿失。