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 系列之六--深入淺出.NetJust-In-Time 編譯機制(下),這也不符合程式局部性原理。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關鍵字來實現。
關於二者對效能的提升作用,我認為有待考證,從理論層面來講,為難以證明的效能因素而特意使用此關鍵字有些得不償失。