[原譯]接口VS 委托


背景

對於指定的任務有不同的方案可供選擇,通常是很好的。因為可能某一種方案會更加適合該任務,但是有時候做決定會很難。因為這些不同的方案有其各自的優缺點。

我經常會停下來好好想想,是不是接口比委托更適合或者是更不適合某個任務。有時候我甚至會回去看我寫的代碼,這些代碼剛開始使用委托來實現,我后來用接口替換掉。因此,是時候寫篇文章來闡述一下這兩種技術的優缺點了。

 

性能

我經常看到有人問接口是不是比委托更快啊。或者是不是相反。通常。別人給的答案會是:

  1.   接口更快。委托相當慢
  2.   委托更快,因為他們是指向方法的指針,接口則需要一個v-table(虛函數解析表),然后找到委托
  3.  他們一樣快,但委托更容易使用

 

好吧。那些都是錯的。也許在.Net 1中。委托真的很慢。但是事實是:

  1.  委托執行(execute)的時候更快
  2. 接口獲得(get)的時候更快

在下面這段代碼中:

Action action = SomeMethod;

我們將得到一個Action(委托類型)來調用SomeMethod。問題是:委托是包含被調用方法的實例和指針的引用類型。而不僅僅只是一個指向方法的指針,通過引用類型,委托需要分配內存,因此,每一次你把一個方法變換成一個委托的時候。都會分配一個新的對象。

如果委托是一個值類型。會有些不一樣。但是他們不是。。

另一方面,如果我們這樣寫代碼:

IRunnable runnable = this

如果實現了IRunnable接口的對象。我們簡單通過一個轉換得到同樣的引用。沒有涉及內存分配。我們將可以通過下面的代碼來進行速度比較:

對於委托:

Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
for(int i=0; i<COUNT; i++)
{
  Action action = SomeMethod;
  action();
}
stopwatch.Stop();
Console.WriteLine(stopwatch.Elapsed);

 

對於接口

Stopwatch stopwatch = new Stopwatch();
stopwatch.Start();
for(int i=0; i<COUNT; i++)
{
  IRunnable runnable = this;
  runnable.Run();
}
stopwatch.Stop();
Console.WriteLine(stopwatch.Elapsed);

 

我知道接口會更快。不是因為他們執行更快。而是因為每一次迭代,一個新的Action委托都會被分配。 但是。如果把委托和接口的獲得語句放在循環之外。委托會更快一些。

當創建事件的時候。舉個例子。我們在給事件添加委托的時候,就只添加一次。這樣。即使事件被觸發再多次。也只進行了一次內存分配。

那么?誰贏了?

好。對於事件,委托將會更快些。

但是。在說委托更好或是更快之前,我們再看另一種情況。

 

匿名方法

在我看來,匿名方法是委托最糟糕的使用。但是同時。也正在變成最普遍的用法。

當你像這段代碼這樣調用的時候

for(int i=0; i<count; i++)
  MethodThatReceivesADelegate(() => SomeCall(i));

 

事實上,編譯器將會創建一個接受參數i的方法實例,然后創建另一個實例(即委托)來引用這個實例。

如果用接口來替換的話。編譯器將指揮分配單一的對象,該對象實現了接口

 

可能的抱怨

一些人也許對那個接受參數i的方法實例的問題有所疑惑。他們可能認為,在每一次迭代中。實例里面的之被改變了。也許編譯器可以優化這個委托的內存分配。實際只分配了一次。

好把。對於委托的內存分配我不知道。但是。對於要分配一個接受參數i的單一實例,確實真的。也是一個bug。如果MethodThatReceivesADelegate 把委托傳遞給另一個線程。其他的線程也許會接收到錯誤的i值,在.net 4.5中。這塊不會出錯。因為。每一次迭代。一個新的對象被創建。這就能保證當委托被傳遞到另一個線程里的時候。結果總是正確的。但這也就意味着每次都會有一個新的委托會創建。

如果MethodThatReceivesADelegate 僅僅使用委托一次。使用接口也許更好。不幸的是。我們沒有辦法實現匿名接口。

好。如果是為了方便。委托更好。但是如果要求性能。在這種情況下。接口會更好。因為避免了一次不必要的內存分配。

事實上,我創建了IRunnable接口是為了強制使用者實現了一個新的類型,而不是使用匿名委托。這樣就可以解決在for循環(或是任何在foreach里使用的任何值)i值可變的問題,同時也有一定的性能提升。。

 

調用和動態調用

 

現在我們知道有匿名委托,但是沒有匿名接口,只使用一次的情況下,接口將會比委托有更好的性能。因為只請求一個對象而不是兩個。

這讓我開始考慮,當一個方法接受一個只會執行一次的方法作為參數的時候,我應該使用委托還是是用接口。

但是。更多的性能相關的情況下我們可以這樣用。。

你是否曾經有過要用動態調用代替直接委托調用的情況?一般是在編譯時並不知道委托的參數類型的情況下。

好。當有一個接口。在接口的方法里有一個方法調用的參數類型未定。我不知道為什么。但是反射調用和委托的動態調用都極慢。比直接對參數做轉換都慢。而數組長度驗證。然后放到一個try/catch塊里會拋出TargetInvocationException 異常。

因此。如果你有類似下面的代碼:

public interface IDynamicInvokable

{

  object DynamicInvoke(params object[] parameters);

}

 

那么你可以創建你的委托接口,是IDynamicInvokable 接口的繼承接口,像這樣:

public interface IAction<T>:

  IDynamicInvokable

{

  void Invoke(T parameter);

}

 

這樣你的用戶就可以通過Invoke方法調用你的接口,如果他們在編譯時不知道接口的類型。他們可以使用更泛化一些的IDynamicInvoke。

注意:我討厭泛型這個名字。對於我來說。IDynamicInvoke 是調用方法最泛型的的途徑,而IAction<T> 是類型接口,因此,我我說泛型的時候。我其實是在說更加普遍無類型的調用泛型。而不是類型指定的泛型。

那么,如果對委托做上千次調用。但是使用DynamicInvoke 代替Invoke,接口會做的更好

我又一次的問我自己。匿名委托的簡單性值得嗎?僅僅為了更好的性能我就把讓我的用戶對我方法的調用變得困難?並且這真的會影響到程序的整體性能嗎?

 

泛型,差異,無類型的使用

 

我剛剛說我討厭泛型的名字。因為使用泛型的代碼通常是有類型的代碼,而我們也許需要的無具體類型的代碼,我認為這樣更加泛一些。

但是。讓我好好討論一下.net的泛型。假設你知道一個委托的參數數目,但是你不知道具體的類型,這和DynamicInvoke 是不一樣的。因為這個方法。簡單的把所有的參數當成一個數組。

泛型具化或者是相反可以有一些幫助。但是很小。

比如。我們可以把 Func<string> 當成 Func<object> ,或是把Action<object> 看成 Action<string>

理由很簡單,當返回一個值的時候(Func的情況),string是一個object。將不會做任何轉換。將會簡單地返回一個string,調用這會看成一個無類型的object。但是可以。而在Action這個情況下。它需要一個object,而string.是可用的object,所以也可以。

但是。如果你想要把Func<int> 當作Func<object>。更廣泛一點。想把所有的參數轉換成object,可以嗎?

答案是否定的。即使int在.net中也是object。但是。所有的值類型需要裝箱。這個一個額外的動作。想要簡單的把int作為object,而不進行裝箱操作。將會引發很多問題。這也就是他不被支持的原因。

但是如果設計的好,接口會有個好處。我個人的原則是:沒當有一個泛型類型的時候(可能是類或是接口),我會創建更泛型的一個接口,好吧。一個無類型的接口。該接口有所有的方法和性,但是使用object代替泛型參數類型。他會作為泛型類型的基類接口。

也就是說。如果我有一個IAction<T>,我會創建一個IAction接口。如果有IAction<T1, T2>,我會創建IAction2接口。

事實是:我更願意Action<int> 能夠被看作Action<>,然后讓。Net可以知道我想要使用一個無類型的泛型委托。但是.net 不支持對泛型類型的無類型的使用。所以。我添加一些額外,有着無類型方法和屬性的接口,到我的泛型類和接口里。然后就可以了。但是。對於委托來說這是不可能的。因此,此處接口更好。

 

不同的用法

我們已經討論過Invoke(調用)和DynamicInvoke(動態調用)了,那么TryInvoke呢?

我的上兩篇文章討論了這種轉換。我們下面會回到那種情況下。

如果我們使用Converter<TInput, TOutput>  委托。轉換可能正常也可能拋出一個異常。如果代碼可以處理不正確的值,那么拋出異常對於表示轉換失敗就不合適了

我考慮創建另一個委托(TryConverter),這個委托返回一個bool值表示轉換是否正常。並且使用一個out參數來表示轉換結果

對於像Int.TryParse這樣的異常無關的轉換。將會很好。但是。如果沒有(比如當TryConverter轉換完成的時候)。我們需要捕獲返回false的異常。

這個問題並沒有真正產生。這個問題我正在構造來拋出該異常。在這種情況下。異常將會被捕獲。然后返回false,然后又引發另一個異常。。太恐怖了。

但是接口可以解決這個問題。通過接口我們可以定義兩個方法。Convert 和TryConvert.

這樣Convert可以用來進行拋出異常的轉換,而TryConvert則用於不拋出異常的轉換。

如果僅有一個引起一個異常的轉換,那么TryConvert將會被強制捕獲異常,如果有一個不引起異常,並且可能失敗的轉換,那么Convert將需要檢查然后引起異常。但是我們要避免那種捕獲一個返回false的異常。然后又拋出另一個異常的情況。

這種復雜性使得接口成為最佳解決方案。沒有一個同等的委托解決方案,能夠確定的比TryConvert給出更好的性能

對於那些讀過我其他文章的人。你可以等Converters的更新。更新會使用接口來解決。Convert和TryConvert都會實現,作為一個無類型的接口的功能。並且CastedGet將會被淘汰。

結論

 

我依舊問我自己。當一個方法接受一些參數。但只是使用一次。接口更快。委托則支持匿名。我到底該用哪一個。

對於多數情況下的事件。委托更好這毫無疑問。但是更多的情況下(通常意義上,當我注冊委托並且允許用戶能夠找到他們的時候)我會用接口替換掉委托,因為后者支持更好的類型或無類型的支持。更易使用並且性能更好。

以一些關鍵點結束吧。

 

委托:

  1. 引用類型,因此分配一個完整的對象引用到一個方法。
  2. 當你編譯時就知道參數類型的使用調用最快。
  3.  允許使用匿名委托,簡化了單行或是小的委托的創建。
  4.  可以不用創建新類型引用到一個私有的方法上。

 

接口:

  1.   不分配新對象。因為獲得很快。
  2.   對於單一用例的情況。更快。金輝創建一個對象而不是兩個。
  3.   如果設計的好,允許無類型的泛型使用,並且比委托的DynamicInvoke更快。
  4.   如果設計的好。泛型接口可以被無類型的接口(該接口有着相同的方法簽名和參數)訪問,通過object改變泛型參數。
  5.   允許不同的調用(比如Convert和TryConvert)
  6.  對於已知的參數類型調用會慢一些。
  7.   不支持編譯時的匿名使用
  8.   即使只需要其中的一個方法。也需要創建完整的類型。

 

例子

 

示例軟件將會比較不同情況下的速度。

本來打算都執行1億次迭代。但是DynamicInvoke 實在太慢了。所以就降到1000萬了。。

 

在我的電腦上。輸出結果如下;

This application tests the speed of interfaces and delegates in

different situations. Compile it in release and execute it outside

Visual Studio to get the right results.

The following tests do 100 millions of iterations:

Testing delegate speed, the wrong way: 00:00:01.6483403

Testing interface speed, the wrong way: 00:00:00.5369746

Testing delegate speed, the right way: 00:00:00.3757670

Testing interface speed, the right way: 00:00:00.4831114

Testing anonymous delegate speed: 00:00:01.7475340

Testing an interface that does the same: 00:00:01.1950063

The following tests do only 10 millions of iterations:

Testing delegate's DynamicInvoke speed: 00:00:37.0368337

Testing interface's DynamicInvoke speed: 00:00:00.3218726

All the tests are finished. Press ENTER to exit.

許可

本文包括源代碼和文件在CPOL下授權。

 Demo下載:

 接口VS 委托

原文地址: Interfaces-vs-Delegates

著作權聲明:本文由http://leaver.me 翻譯,歡迎轉載分享。請尊重作者勞動,轉載時保留該聲明和作者博客鏈接,謝謝!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM