在學習方法/函數時,咱們總會接觸到 按值傳值 和 引用傳值 兩個概念。像C#是按值傳值,但參數列表添加了ref/out後則是引用傳值,但奇怪的事出現了html
namespace Foo{ class Bar{ public String Msg{get;set;} } class Program{ public static void main(String[] args){ Bar bar1 = new Bar(); bar1.Msg = "Hey, man!"; UpdateProp(bar1); Console.WriteLine(bar1.Msg); // Bye! } static void UpdateProp(Bar bar){ bar.Msg = "Bye!"; } } }
Q:UpdateProp明明是按值傳值,對bar的修改怎麼會影響到main中的bar1呢?
延伸Q:到底什麼是按值傳值、引用傳值?
爲了解答上述疑問,咱們就須要理解求值策略了!算法
Evaluation Strategy其實就是定義函數/方法的實參應該在什麼時候被計算,而且以什麼類型的值做爲實參傳入到函數/方法內部。編程
A programming language uses an evaluation strategy to determine when to evaluate the argument(s) of a function call (for function, also read: operation, method, or relation) and what kind of value to pass to the function.promise
以時間爲維度,那麼就有如下三種類別的求值策略:
1. Strict/Eager Evaluation,在執行函數前對實參求值(實質上是在構建函數執行上下文前)。
2. Non-strict Evaluation(Lazy Evaluation),在執行函數時纔對實參求值。
3. Non-deterministic,實參求值時機飄忽。
另外注意的是,大部分編程語言採用不止一種求值策略。併發
如今絕大部分語言均支持這類求值策略,而咱們也習覺得常,所以當如Linq、Lambda Expression等延遲計算出現時咱們才如此興奮不已。
但Strict/Eager Evaluation下還包括不少具體的定義,下面咱們來逐個瞭解。ecmascript
Applicative Order又名leftmost innermost,中文翻譯爲「應用序列」,實際運算過程和Post-order樹遍歷算法相似,必須先計算完葉子節點再計算根節點,所以下面示例將致使在計算實參時就發生內存溢出的bug。異步
// function definitions function foo(){ return false || foo() } function test(a, f){ console.log(a + f) } // main thread,陷入foo函數無盡的遞歸調用中 test(1, foo())
按值傳值也就是咱們接觸最多的一種求值策略,實際運算過程是對實參進行克隆,而後將副本賦值到參數變量中。async
function foo(val){ val = 3 } var bar = 1 foo(bar) console.log(bar) // 顯示1 // 函數做用域中對實參進行賦值操做,並不會影響全局做用域的變量bar的值。
那如Brief中C#那種狀況究竟是啥回事呢?其實問題在於 到底要克隆哪裏的「值」了,對於Bar bar = new Bar()而言,bar對應的內存空間存放的是指向 new Bar()內存空間的指針,而所以克隆的就是指針而不是 new Bar()這個對象,也就是說克隆的是實參對應的內存空間存放的「值」。假如咱們將Bar定義爲Struct而不是Class,則明白C#確實遵循Call-by-value策略。編程語言
namespace Foo{ struct Bar{ public String Msg{get;set;} } class Program{ public static void main(String[] args){ Bar bar1 = new Bar(); bar1.Msg = "Hey, man!"; UpdateProp(bar1); Console.WriteLine(bar1.Msg); // Hey,man! } static void UpdateProp(Bar bar){ bar.Msg = "Bye!"; } } }
稍微總結一下,Call-by-value有以下特色:
1. 若克隆的「值」爲值類型則爲值自己,而且在函數內的任何修改將不影響外部對應變量的值;
2. 若克隆的「值」爲引用類型則爲內存地址,而且在函數內的修改將影響外部對應變量的值,但賦值操做則不影響外部對應變量的值。
注意:因爲第2個特色與Call-by-sharing的特色是同樣的,所以雖然Java應該屬於採用Call-by-sharing策略,但社區仍是聲稱Java採用的是Call-by-value策略。ide
其實Call-by-reference和Call-by-value同樣那麼容易被人誤會,覺得把內存地址做爲實參傳遞就是Call-by-reference,其實否則。關鍵是這個「內存地址」是實參對應的內存地址,而不是實參對應的內存中所存放的地址。C語言木有自然地支持Call-by-reference策略,但能夠經過指針來模擬,反而能讓咱們更好地理解整個求值過程。
int i = 1; int *pI = &i; // &i會獲取i對應內存空間的地址,並存放到pI對應的內存空間中 void foo(int *); void foo(int *pI){ pI = 2; // 直接操做i對應的內存空間,等同於i = 2 } int main(){ foo(pI); printf("%s", i); // 返回2 return 0; }
內存結構:
而C#可經過在形參上添加ref或out來設置採用Call-by-reference策略,Java和JavaScript就天生不支持也沒有提供模擬的方式。
採用該策略的語言暗示該語言主要基於引用類型而不是值類型。
Call by sharing implies that values in the language are based on objects rather than primitive types, i.e. that all values are "boxed".
明顯Java和受Java影響甚深的JavaScript就是採用這種策略的。
該策略特色和Call-by-value的個特色一致。
暫時我還沒接觸到哪一種語言採用了Call-by-copy-restore這種求值策略,它的運算過程主要分爲兩步:
1. 如Call-by-value的特色1那樣,對實參進行拷貝操做,而後將副本傳遞到函數體內。重點是,即便實參爲引用類型,也對引用所指向的對象進行拷貝,而不是僅拷貝指針而已。
效果:在函數體內對實參的任何操做(PutValue和Assignment)均不影響外部對應的變量。
2. 當退出函數執行上下文後,將實參值賦值到外部對應的變量。
/*** pseudo code ***/ var a = {} function foo(a){ a.name = 'fsjohnhuang' console.log('within foo:' + a.name) // 線程掛起1000ms var sd = +new Date() while(+new Date - sd < 1000); } // 異步執行foo var promise = foo.async(a) while(+new Date - sd < 100); // 未退出foo的執行上下文時,訪問a.name,返回undefined console.log(a.name) if (promise.done){ // 退出foo的執行上下文時,返回a.name,返回'fsjohnhuang' console.log(a.name) }
便是部分實參在進入函數執行上下文前將不參與求值操做。示例以下:
var freeVar = {type: 'freeVar'} function getName(){ return freeVar } function print(msg, fn){ console.log(msg + fn()) } // 調用print時getName將不會被立刻求值 print('Hi,', getName)
能夠看到上述print函數調用時不會立刻對getName實參求值,但會立刻對'Hi,'進行求值操做。而須要注意的地方是,因爲getName是延遲計算,若函數體內存在自由變量(如freeVar),那麼後續的每次計算結果均有可能不一樣(也就是side effect)。
Non-strict Evaluation是指在執行函數體的過程當中,須要用到該實參才進行運算的策略。還記得邏輯運算符(||,&&)的短路運算(short-circuit evaluation)嗎?這個就是延遲計算其中一個實例。
下面咱們一塊兒來了解4種延遲計算策略吧!
Normal Order又名leftmost outermost,中文翻譯爲「正常序列」,通常經過與Applicative Order做對比來理解效果較好。還記得Applicative Order可能會引發內存溢出的問題嗎?那是由於Applicative Order會不斷地對AST中層數最深的可規約表達式節點優先求值的緣故,而Normal Order則採用計算完AST中層數最淺的可規約表達式節點便可。
/ function definitions function foo(){ return false || foo() } function test(a, f){ console.log(a + f) } // main thread, 顯示 "1false" test(1, foo())
這種延遲計算策略十分容易明白,計算過程就是在執行函數體時,遇到需計算實參表達式時才執行運算。注意點:
1. 每次在執行實參表達式時均會執行運算;
2. 若實參的運算過程爲計算密集型或阻塞性操做時,則會阻塞函數體後續命令的執行。(這時會可經過Thunk對Call-by-name進行優化)
其實就是Call-by-name + Memoized,就是第一計算實參表達式時,在返回計算結果的同時內部自動保存該結果,當下次執行實參表達式計算時直接返回首次計算的結果。注意點:
1. 該策略僅適用於pure function的實參,存在free variable則會致使沒法確保每次的求值結果都同樣。
在Clojure中使用macro時則就是採用Call-by-macro-expansion策略,會執行expansion階段對函數體內的實參表達式替換爲macro所定義的表達式,而後在進行運算。
另外因爲實參的運算時機具備不肯定性,所以下面的策略不能納入Strict和Non-strict求值策略中。
這是一個併發求值策略,就是將求值操做委託給future,並由後續的promise去完成求值操做,而後調用者則經過future獲取求值結果。注意點:
1. 求值操做可能發生在future剛建立時,也有可能調用future獲取結果時才求值。
上述是查閱各資料後,對幾類求值策略的理解,如有紕漏請你們指正,謝謝!
https://en.wikipedia.org/wiki/Evaluation_strategy
http://stackoverflow.com/questions/8848402/whats-the-difference-betwee...
https://en.wikipedia.org/wiki/Thunk#Call_by_name
http://blog.csdn.net/institute/article/details/23750307
http://www.cnblogs.com/leowanta/articles/2958581.html
http://blog.csdn.net/sk__________________/article/details/12848597
http://dmitrysoshnikov.com/ecmascript/chapter-8-evaluation-strategy/
若是您以爲本文的內容有趣就掃一下吧!捐贈互勉!