C# 本地函數與 Lambda 表達式
C# 局部函數通常被視爲 lambda 表達式的進一步增強。雖然功能是相關的,但也存在重大差異。
Local Functions 是嵌套函數[1]功能的 C# 實現。一種語言在支持 lambdas 之後獲得對嵌套函數的支持幾個版本是有點不尋常的。通常情況相反。
Lambda 或一般的一流函數需要實現未在堆棧上分配且生命週期與需要它們的功能對象相關聯的局部變量。如果不依賴垃圾收集或通過捕獲列表等解決方案將變量所有權的負擔減輕給用戶,則幾乎不可能正確有效地實現它們。對於某些早期語言來說,這是一個嚴重的阻塞問題。嵌套函數的簡單實現不會遇到這種複雜情況,因此一種語言更常見的是僅支持嵌套函數而不支持 lambda。
無論如何,由於 C# 長期以來一直使用 lambda,因此從差異和相似之處來看本地函數確實是有意義的。
Lambda 表達式
Lambda 表達式x => x + x
是抽象地表示一段代碼以及它如何綁定到其詞法環境中的參數和變量的表達式。作爲代碼的抽象表示,lambda 表達式不能單獨使用。爲了使用由 lambda 表達式生成的值,需要將其轉換爲更多內容,例如委託或表達式樹。
using System;
using System.Linq.Expressions;
class Program
{
static void Main(string[] args)
{
// can't do much with the lambda expression directly
// (x => x + x).ToString(); // error
// can assign to a variable of delegate type and invoke
Func<int, int> f = (x => x + x);
System.Console.WriteLine(f(21)); // prints "42"
// can assign to a variable of expression type and introspect
Expression<Func<int, int>> e = (x => x + x);
System.Console.WriteLine(e); // prints "x => (x + x)"
}
}
有幾點值得注意:
•lambdas 是產生函數值的表達式。•lambda 值的生命週期是無限的——從 lambda 表達式的執行開始,只要存在對該值的任何引用。這意味着 lambda 從封閉方法中使用或“捕獲”的任何局部變量都必須在堆上分配。由於 lambda 值的生命週期不受產生它的堆棧幀的生命週期的限制,因此不能在該堆棧幀上分配變量。•lambda 表達式要求在執行 lambda 表達式時明確分配主體中使用的所有外部變量。lambda 的第一次和最後一次使用的時刻很少是確定性的,因此該語言假設 lambda 值可以在創建後立即使用,只要它們是可訪問的。因此,一個 lambda 值在創建時必須是完全可用的,並且它使用的所有外部變量都必須明確分配。
int x;
// ERROR: 'x' is not definitely assigned
Func<int> f = () => x;
•lambdas 沒有名字,也不能被象徵性地引用。特別是 lambda 表達式不能遞歸聲明。
注意:可以通過調用分配給 lambda 的變量或傳遞給自應用其參數的高階方法來創建遞歸 lambda(請參閱:C# 中的匿名遞歸[2]),但這不會表達真正的自我參照。
本地函數
局部函數基本上只是在另一個方法中聲明的方法,作爲一種降低方法對其聲明範圍內的可見性的方法。
自然地,局部函數中的代碼可以訪問其包含範圍內可訪問的所有內容——局部變量、封閉方法的參數、類型參數、局部函數。一個值得注意的例外是外部方法標籤的可見性。封閉方法的標籤在局部函數中不可見。這只是普通的詞法範圍,它的工作原理與 lambdas 相同。
public class C
{
object o;
public void M1(int p)
{
int l = 123;
// lambda has access to o, p, l,
Action a = ()=> o = (p + l);
}
public void M2(int p)
{
int l = 123;
// Local Function has access to o, p, l,
void a()
{
o = (p + l);
}
}
}
與 lambda 的明顯區別在於局部函數具有名稱並且可以在沒有任何間接方式的情況下使用。局部函數可以是遞歸的。
static int Fac(int arg)
{
int FacRecursive(int a)
{
return a <= 1 ?
1 :
a * FacRecursive(a - 1);
}
return FacRecursive(arg);
}
與 lambda 表達式的主要語義區別在於局部函數不是表達式,它們是聲明語句。在代碼執行方面,聲明是非常被動的實體。事實上,聲明並沒有真正被“執行”。與標籤等其他聲明類似,局部函數聲明只是將函數引入包含範圍,而無需運行任何代碼。
更重要的是,無論是聲明本身還是嵌套函數的常規調用都不會導致對環境的不確定捕獲。在簡單和常見的情況下,如普通的調用/返回場景,捕獲的局部變量不需要進行堆分配。
例子:
public class C
{
public void M()
{
int num = 123;
// has access to num
void Nested()
{
num++;
}
Nested();
System.Console.WriteLine(num);
}
}
上面的代碼大致相當於(反編譯):
public class C
{
// A struct to hold "num" variable.
// We are not storing it on the heap,
// so it does not need to be a class
private struct <>c__DisplayClass0_0
{
public int num;
}
public void M()
{
// reserve storage for "num" in a display struct on the _stack_
C.<>c__DisplayClass0_0 env = default(C.<>c__DisplayClass0_0);
// num = 123
env.num = 123;
// Nested()
// note - passes env as an extra parameter
C.<M>g__a0_0(ref env);
// System.Console.WriteLine(num)
Console.WriteLine(env.num);
}
// implementation of the the "Nested()".
// note - takes env as an extra parameter
// env is passed by reference so it's instance is shared
// with the caller "M()"
internal static void <M>g__a0_0(ref C.<>c__DisplayClass0_0 env)
{
env.num += 1;
}
}
請注意,上面的代碼直接調用了“Nested()”的實現(不是通過委託間接),並且沒有在堆上引入顯示存儲的分配(就像 lambda 會那樣)。局部變量存儲在結構中而不是類中。的生命週期num
並沒有因爲它在 中的使用而改變Nested()
,所以它仍然可以在棧上分配。M()
可以只通過num
引用傳遞,但編譯器使用結構體進行打包,因此它可以傳遞所有本地變量,就像num
只使用一個 env 參數一樣。
另一個有趣的一點是,只要本地函數在給定範圍內可見,就可以使用它們。這是一個重要的事實,使遞歸和相互遞歸的場景成爲可能。這也使得本地函數聲明在源代碼中的確切位置在很大程度上變得不重要。
例如,封閉方法的所有變量必須在調用讀取它們的本地函數時明確分配,而不是在其聲明時。實際上,如果調用可以更早發生,那麼在聲明時提出該要求將沒有任何好處。
public void M()
{
// error here -
// Use of unassigned local variable 'num'
Nested();
int num;
// whether 'num' is assigned here or not is irrelevant
void Nested()
{
num++;
}
num = 123;
// no error here - 'num' is assigned
Nested();
System.Console.WriteLine(num);
}
此外 - 如果從未使用過局部函數,它也不會比一段無法訪問的代碼和任何變量更好,否則它會使用,不需要分配。
public void M()
{
int num;
// warning - Nested() is never used.
void Nested()
{
// no errors on unassigned 'num'.
// this code never runs.
num++;
}
}
那麼,局部函數的目的是什麼?
與 lambdas 相比,局部函數的主要價值主張是局部函數在概念上和運行時開銷方面都更簡單。
Lambda 可以很好地充當一類函數[3]的角色,但有時您只需要一個簡單的助手。分配給局部變量的 Lambda 可以完成這項工作,但存在間接開銷、委託分配和可能的閉包開銷。私有方法也有效,調用成本更低,但存在封裝問題,或缺乏封裝。這樣的助手對包含類型中的每個人都是可見的。太多這樣的幫手會導致嚴重的混亂。
局部函數非常適合這種情況。調用本地函數的開銷與調用私有方法的開銷相當,但使用其他不應調用的方法污染包含類型沒有問題。
http://mustoverride.com/local_functions/
References
[1]
嵌套函數: https://en.wikipedia.org/wiki/Nested_function[2]
C# 中的匿名遞歸: https://blogs.msdn.microsoft.com/wesdyer/2007/02/02/anonymous-recursion-in-c/[3]
一類函數: https://en.wikipedia.org/wiki/First-class_function
本文分享自微信公衆號 - 一線碼農聊技術(dotnetfly)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。