The Programming Language Tips
重載與重寫
問題
日常討論中,術語的不統一帶來些許混亂
慣用的表達
overload | 重載 | 函數名稱相同,參數不同(嚴格的定義還有其它一些限制) | 靜態決議 |
override | 重寫(覆寫,覆蓋,改寫) | 子類重新定義父類定義過的虛函數(個別語言允許返回值,訪問級別可以不同) | 動態決議 |
示例
class Base { } class Derived:Base { } class Client { void Test(Base obj){ Console.WriteLine("base"); } void Test(Derived obj){ Console.WriteLine("derived"); } static void Main(string[] args) { Base obj = new Derived(); new Client().Test(obj); //輸出“base” } } |
靜態構造函數
問題1,在工具類中,通常有一些初始化需要在任何靜態方法被調用前進行,如配置信息的讀取
2,普通類中的複雜的靜態信息,需要在任何實例方法被調用前初始化
我見過的解決方法
1,在每個靜態方法中都調用必需的初始化步驟
public class SomeUtilClass { private SomeUtilClass(){ } private static void Init(){ //.... } public static string GetUID(){ Init(); return uid; } public static string GetConnectionString(){ Init(); return connString; } } |
2,在普通構造函數中初始化
public class SomeMapperClass{ private static Hashtable types; public SomeMapperClass(){ if(types == null){ types = new Hashtable(); types.Add("RED", Color.Red); types.Add("GREEN", Color.Green); types.Add("BLUE", Color.Blue); } } public Color GetColor(string color){ return (Color)types[color]; } }
|
我推薦的解決方法
使用靜態構造函數(C#),或靜態初始化塊(Java)
[C#] public class SomeClass { static SomeClass(){ Init(); types = new Hashtable(); types.Add(...); types.Add(...); } } |
[Java] public class SomeClass { static{ Init(); types = new HashMap(); types.put("", ""); types.put("", ""); } } |
效果
1,Once,only once
2,定義中對異常處理等有要求,可參考規範
2,多線程時是否有問題,我不清楚,討論一下
只讀代理
問題對象內部有一個集合,由這個對象來控制其元素的增加刪除,但客戶需要訪問該集合取得自己想要的信息,而對象不可能爲所有的客戶都提供對應的方法,因此需要返回內部的這個集合,但不允許客戶增加或刪除其元素
我見過的解決方法
直接返回代表集合的成員引用,僅在文檔中要求客戶不能增刪集合中的元素
public class SomeClass { private List attrs; public List GetAttributes(){ return attrs; } } |
我推薦的解決方法
1,首選語言提供的功能
2,次選類庫提供的功能
3,自己包裝代理類,或返回深度拷貝,或使用AOP
[C++] class config { public: const list<string> & get_attributes(){ return attrs; } private: list<string> attrs; }; |
[C#] public class SomeClass { private IList attrs; public IList GetAttributes(){ return ArrayList.ReadOnly(attrs); } } |
[Java] public class SomeClass { private List attrs; public List getAttributes(){ return Collections.unmodifiableList(attrs); } } |
效果
1,語言提供的功能可幫助在編譯期進行檢查,確保程序中連試圖增刪元素的代碼都不存在;但對有意無意的const轉型無能爲力
2,類庫提供的功能可幫助在運行期進行檢查,確保程序中試圖增刪元素的操作都拋出異常
同步代理
問題爲了對象的線程安全引入了同步機制,卻使對象在單線程環境下付出了不必要的性能上的代價,曾經的例子如寫時拷貝COW
我見過的解決方法
就是視而不見,不做任何處理,使用同步原語
[C#] public class SomeClass { [MethodImplAttribute(MethodImplOptions.Synchronized)] public void Add(string name){ attrs.Add(name); } } |
[Java] public class SomeClass { public synchronized void Add(string name){ attrs.add(name); } }
|
我推薦的解決方法
參考類庫的實現,提供沒有同步的原始類,及有同步的代理類;早期的JDK中Vector及HashTable都是同步的類,新的ArrayList及HashMap都不是同步的,Collections提供了靜態方法返回同步代理;當在多線程環境中需要更改集合時,使用代理類
[C#,多線程環境中使用同步代理的客戶類代碼] public class SomeClass { public SomeClass(IList source){ attrs = ArrayList.Synchronized(source); } public void Add(string name){ attrs.Add(name); } public void Remove(string name){ attrs.Remove(name); } } [C#,單線程環境中使用同步代理的客戶類代碼] public class OtherClass{ public OtherClass(IList source){ attrs = source; } public void Add(string name){ attrs.Add(name); } public void Remove(string name){ attrs.Remove(name); } } |
[Java,多線程環境中使用同步代理的客戶類代碼] public class SomeClass { public SomeClass (List source){ attrs = Collections.synchronizedList(source); } public void add(string name){ attrs.add(name); } } [Java,單線程環境中使用同步代理的客戶類代碼] public class OtherClass{ public OtherClass(List source){ attrs = source; } public void add(string name){ attrs.add(name); } } |
效果
不必爲不需要的功能付出額外的代價
有時需要精確的控制資源分配和釋放的時機,保證資源的異常安全,避免資源泄漏,導致死鎖,文件丟失,數據庫連接過多等
在缺乏真正的局部對象和析構函數的語言中,try/catch/finally充斥在代碼中
在C++中,自動化的資源管理是與生俱來的,即B.S.提出的“資源管理即初始化”(RAII)
在C#中,可使用using+IDispose取得近似RAII的效果
[C++,RAII,僅僅示例,操作文件應首選std::fstream等] class File { public: explicit File(string path){ pf = fopen(path.c_str(), "rwb"); } ~File(){ fclose(pf); } operator FILE* (){ return pf; } private: FILE* pf; }; [C++,RAII的客戶代碼,僅僅示例,操作文件應首選std::fstream等] void test() { File file("auto.txt"); char buf[256]; fread(buf, 0, 256, file);//即使這個操作會拋出異常,文件依然會被關閉 } |
[C#,僅僅示例] public class File:IDisposable { private FileStream fs; public File(string path){ fs = new FileStream(path, FileMode.Create); } public static implicit operator FileStream(File file) { return file.fs; } public void Dispose() { fs.Close(); } } [C#,僅僅示例] public class Test{ void test(){ using(File file = new File("auto.txt")){ //some read, write, etc. } //文件已經被關閉,即使某步操作拋出異常 } } |
效果
1,資源管理自動化,不侷限於內存
2,C++中使用模板,可統一定義大部分資源的包裝類,目前的C#只能爲每種資源定義單獨的類,或者使用AOP
[C++] 虛函數與對象狀態有關,與訪問權限(public/protected/private)無關 只要子類對象構造出來了,就可以調用重寫的方法,不管訪問權限 |
[Java, C#] 虛函數與對象狀態無關,與訪問權限(public/protected/private/default/internal)有關 只要訪問權限允許,就可以調用重寫的方法,不管子類對象構造出來沒有 |
後果
[C++] 在基類構造函數/析構函數裏調用的方法永遠都是基類的實現,不會調到子類;在其它方法裏面虛函數永遠都是調到子類的覆寫實現,不管是不是private |
[Java, C#] 在基類構造函數裏調用方法,只要子類覆寫了該方法,就會調到子類的實現 |
解決方法
慎重的在構造函數中調用虛函數,尤其是在Java和C#中,至少應該在註釋中說明理由
儘管“針對接口編程”做爲一條原則已經廣爲流傳,但實際應用中仍然隨處可見HashMap,Vector等做爲接口參數、返回值傳來傳去
使用Factory Method返回接口,並最小化具體類構造函數的訪問權限,或類本身的訪問權限
Factory Method依然值得推薦,另外可以利用語言本身的特性來避免多寫一個Factory Method
在C++中,override一個虛函數時可以任意改變它的訪問權限,包括將它由public變爲private;有人說這樣會破壞封裝,但只要語義正確,有意爲之,也沒什麼問題
[C++] class ISomeInterface { public: virtual void SomeMethod() = 0; };
class SomeClass : public ISomeInterface { private: void SomeMethod(){ std::cout << "Subclass/n"; } };
int main(int argc, _TCHAR* argv[]) { SomeClass obj; obj.SomeMethod(); //Error ISomeInterface& iobj = obj; iobj.SomeMethod(); //Ok return 0; } |
[C#] public interface ISomeInterface { void SomeMethod(); } public class SomeClass:ISomeInterface { //1,不要寫訪問修飾符;2,使用方法全名 void ISomeInterface.SomeMethod(){ System.Console.WriteLine("Subclass"); } } public class Test{ void test(){ SomeClass obj = new SomeClass(); obj.SomeMethod(); //Error; ISomeInterface iobj = obj; iobj.SomeMethod(); //Ok } } |
效果
1,少寫一個Factory Method
2,不需要控制構造函數的訪問權限
抗變與協變
問題
在override虛函數時,子類有時想要返回或處理與父類函數參數和返回值略微不同的類型,比如假設“動物類”有一個“伴侶”的虛函數,其返回值類型爲“動物類”,但子類“兔子”override“伴侶”時,需要把返回值改爲“兔子”;假設“鳥類”有一個“進食”的虛函數,其參數類型爲“穀類”,但子類“食鐵鳥”override“進食”時,需要把參數改爲“鹼性食物”;這時,除了使用泛型可以解決外,就需要用到抗變與協變
定義
抗變:向父類的方向變化
協變:向子類的方向變化
語言支持
返回值抗變與參數協變會帶來明顯的類型安全問題,因此,常用的基本是返回值協變與參數抗變;對抗變與協變支持的最全面的是Eiffel,它同時提供了受束泛型來解決返回值抗變與參數協變帶來的類型安全問題
[C++] 只支持返回值協變 class Animal { public: virtual Animal const& Spouse() = 0; };
class Rabbit : public Animal { public: Rabbit const& Spouse(){ return Rabbit(); } }; |
[C#] 不支持 [Java] 1.5之前不支持,1.5與泛型結合,有限度的支持協變; 另外迫於checked exception的蹩腳實現,Java支持異常聲明的協變
|
【推薦參考資料】
1.C#標準:ECMA-334 : C# Language Specification
2.Java標準:The Java™ Language Specification Second Edition
3.C++標準:ISO/IEC 14882:2003 Programming Languages - C++
4.The C# Programming Language
5.The Java Programming Language
6.The C++ Programming Language