日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 >

C# 中的本地函数

發布時間:2023/12/4 62 豆豆
生活随笔 收集整理的這篇文章主要介紹了 C# 中的本地函数 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

今天我們來聊一聊 C# 中的本地函數。本地函數是從 C# 7.0 開始引入,并在 C# 8.0 和 C# 9.0 中加以完善的。

引入本地函數的原因

我們來看一下微軟 C# 語言首席設計師 Mads Torgersen 的一段話:

Mads Torgersen:
我們認為這個場景是有用的 —— 您需要一個輔助函數。您僅能在單個函數中使用它,并且它可能使用包含在該函數作用域內的變量和類型參數。另一方面,與 lambda 不同,您不需要將其作為第一類對象,因此您不必關心為它提供一個委托類型并分配一個實際的委托對象。另外,您可能希望它是遞歸的或泛型的,或者將其作為迭代器實現。[1]

正是 Mads Torgersen 所說的這個原因,讓 C# 語言團隊添加了對本地函數的支持。
本人在近期的項目中多次用到本地函數,發現它比使用委托加 Lambda 表達式的寫法更加方便和清晰。

本地函數是什么

用最簡單的大白話來說,本地函數就是方法中的方法,是不是一下子就理解了?不過,這樣理解本地函數難免有點片面和膚淺。

我們來看一下官方對本地函數的定義:

本地函數是一種嵌套在另一個成員中的私有方法,僅能從包含它的成員中調用它。?[2]

定義中點出了三個重點:

  • 本地函數是私有方法。

  • 本地函數是嵌套在另一成員中的方法。

  • 只能從定義該本地函數的成員中調用它,其它位置都不可以。

  • 其中,可以聲明和調用本地函數的成員有以下幾種:

    • 方法,尤其是迭代器方法和異步方法

    • 構造函數

    • 屬性訪問器

    • 事件訪問器

    • 匿名方法

    • Lambda 表達式

    • 析構函數

    • 其它本地函數

    舉個簡單的示例,在方法?M?中定義一個本地函數?add:

    public class C {public void M(){int result = add(100, 200);// 本地函數 addint add(int a, int b) { return a + b; }} }

    本地函數都是私有的,目前可用的修飾符只有?async、unsafe、static(靜態本地函數無法訪問局部變量和實例成員) 和?extern?四種。在包含成員中定義的所有本地變量和其方法參數都可在非靜態的本地函數中訪問。本地函數可以聲明在其包含成員中的任意位置,但通常的習慣是聲明在其包含成員的最后位置(即結束?}?之前)。

    本地函數與Lambda表達式的比較

    本地函數和我們熟知的?Lambda 表達式?[3]非常相似,比如上面示例中的本地函數,我們可以使用 Lambda 表達式實現如下:

    public void M() {// Lambda 表達式Func<int, int, int> add = (int a, int b) => a + b;int result = add(100, 200); }

    如此看來,似乎選擇使用 Lambda 表達式還是本地函數只是編碼風格和個人偏好問題。但是,應該注意到,使用它們的時機和條件其實是存在很大差異的。

    我們來看一下獲取斐波那契數列第 n 項的例子,其實現包含遞歸調用。

    // 使用本地函數的版本 public static uint LocFunFibonacci(uint n) {return Fibonacci(n);uint Fibonacci(uint num){if (num == 0) return 0;if (num == 1) return 1;return checked(Fibonacci(num - 2) + Fibonacci(num - 1));} } // 使用 Lambda 表達式的版本 public static uint LambdaFibonacci(uint n) {Func<uint, uint> Fibonacci = null; //這里必須明確賦值Fibonacci = num => {if (num == 0) return 0;if (num == 1) return 1;return checked(Fibonacci(num - 2) + Fibonacci(num - 1));};return Fibonacci(n); }

    命名

    本地函數的命名方式和類中的方法類似,聲明本地函數的過程就像是編寫普通方法。Lambda 表達式是一種匿名方法,需要分配給委托類型的變量,通常是?Action?或?Func?類型的變量。

    參數和返回值類型

    本地函數因為語法類似于普通方法,所以參數類型和返回值類型已經是函數聲明的一部分。Lambda 表達式依賴于為其分配的?Action?或?Func?變量的類型來確定參數和返回值的類型。

    明確賦值

    本地函數是在編譯時定義的方法。由于未將本地函數分配給變量,因此可以從包含它的成員的任意代碼位置調用它們。在本例中,我們將本地函數?Fibonacci?定義在其包含方法?LocFunFibonacci?的?return?語句之后,方法體的結束?}?之前,而不會有任何編譯錯誤。

    而?Lambda 表達式是在運行時聲明和分配的對象。使用 Lambda 表達式時,必須先對其進行明確賦值:聲明要分配給它的?Action?或?Func?變量,并為其分配 Lambda 表達式,然后才能在后面的代碼中調用它們。在本例中,我們首先聲明并初始化了一個委托變量?Fibonacci, 然后將 Lambda 表達式賦值給了該委托變量。

    這些區別意味著使用本地函數創建遞歸算法會更輕松。因為在創建遞歸算法時,使用本地函數和使用普通方法是一樣的; 而使用 Lambda 表達式,則必須先聲明并初始化一個委托變量,然后才能將其重新分配給引用相同 Lambda 表達式的主體。

    變量捕獲

    我們使用 VS 編寫或者編譯代碼時,編譯器可以對代碼執行靜態分析,提前告知我們代碼中存在的問題。

    看下面一個例子:

    static int M1() {int num; //這里不用賦值默認值LocalFunction();return num; //OKvoid LocalFunction() => num = 8; // 本地函數 }static int M2() {int num; //這里必須賦值默認值(比如改為:int num = 0;),下面使用 num 的行才不會報錯Action lambdaExp = () => num = 8; // Lambda 表達式lambdaExp();return num; //錯誤 CS0165 使用了未賦值的局部變量“num” }

    在使用本地函數時,因為本地函數是在編譯時定義的,編譯器可以確定在調用本地函數?LocalFunction?時明確分配?num。因為在 return 語句之前調用了?LocalFunction,也就在 return 語句前明確分配了?num,所以不會引發編譯異常。
    而在使用 Lambda 表達式時,因為?Lambda 表達式是在運行時聲明和分配的,所以在 return 語句前,編譯器不能確定是否分配了?num,所以會引發編譯異常。

    內存分配

    為了更好地理解本地函數和 Lambda 表達式在分配上的區別,我們先來看下面兩個例子,并看一下它們編譯后的代碼。

    Lambda 表達式:

    public class C {public void M(){int c = 300;int d = 400;int num = c + d;//Lambda 表達式Func<int, int, int> add = (int a, int b) => a + b + c + d;var num2 = add(100, 200);} }

    使用 Lambda 表達式,編譯后的代碼如下:

    public class C {[CompilerGenerated]private sealed class <>c__DisplayClass0_0{public int c;public int d;internal int <M>b__0(int a, int b){return a + b + c + d;}}public void M(){<>c__DisplayClass0_0 <>c__DisplayClass0_ = new <>c__DisplayClass0_0();<>c__DisplayClass0_.c = 300;<>c__DisplayClass0_.d = 400;int num = <>c__DisplayClass0_.c + <>c__DisplayClass0_.d;Func<int, int, int> func = new Func<int, int, int>(<>c__DisplayClass0_.<M>b__0);int num2 = func(100, 200);} }

    可以看出,使用 Lambda 表達式時,編譯后實際上是生成了包含實現方法的一個類,然后創建該類的一個對象并將其分配給了委托。因為要創建類的對象,所以需要額外的堆(heap)分配。

    我們再來看一下具有同樣功能的本地函數實現:

    public class C {public void M(){int c = 300;int d = 400;int num = c + d;var num2 = add(100, 200);//本地函數int add(int a, int b) { return a + b + c + d; }} }

    使用本地函數,編譯后的代碼如下:

    public class C {[StructLayout(LayoutKind.Auto)][CompilerGenerated]private struct <>c__DisplayClass0_0{public int c;public int d;}public void M(){<>c__DisplayClass0_0 <>c__DisplayClass0_ = default(<>c__DisplayClass0_0);<>c__DisplayClass0_.c = 300;<>c__DisplayClass0_.d = 400;int num = <>c__DisplayClass0_.c + <>c__DisplayClass0_.d;int num2 = <M>g__add|0_0(100, 200, ref <>c__DisplayClass0_);}[CompilerGenerated]private static int <M>g__add|0_0(int a, int b, ref <>c__DisplayClass0_0 P_2){return a + b + P_2.c + P_2.d;} }

    可以看出,使用本地函數時,編譯后只是在包含類中生成了一個私有方法,因此調用時不需要實例化對象,不需要額外的堆(heap)分配。
    當本地函數中使用到其包含成員中的變量時,編譯器生成了一個結構體,并將此結構體的實例以引用(ref)方式傳遞到了本地函數,這也有助于節省內存分配。

    綜上所述,使用本地函數相比使用 Lambda 表達式更能節省時間和空間上的開銷。

    本地函數與異常

    本地函數還有一個比較實用的功能是,可以在迭代器方法和異步方法中立即顯示異常。

    我們知道,迭代器方法的主體是延遲執行的,所以僅在枚舉其返回的序列時才顯示異常,而并非在調用迭代器方法時。
    我們來看一個經典的迭代器方法的例子:

    static void Main(string[] args) {int[] list = new[] { 1, 2, 3, 4, 5, 6 };var result = Filter(list, null);Console.WriteLine(string.Join(',', result)); }public static IEnumerable<T> Filter<T>(IEnumerable<T> source, Func<T, bool> predicate) {if (source == null) throw new ArgumentNullException(nameof(source));if (predicate == null) throw new ArgumentNullException(nameof(predicate));foreach (var element in source)if (predicate(element))yield return element; }

    運行上面的代碼,由于迭代器方法的主體是延遲執行的,所以拋出異常的位置將發生在?string.Join(',', result)?所在的行,也就是在枚舉返回的序列結果?result?時顯示,如圖:

    如果我們把上面的迭代器方法?Filter?中的迭代器部分放入本地函數:

    static void Main(string[] args) {int[] list = new[] { 1, 2, 3, 4, 5, 6 };var result = Filter(list, null);Console.WriteLine(string.Join(',', result)); }public static IEnumerable<T> Filter<T>(IEnumerable<T> source, Func<T, bool> predicate) {if (source == null) throw new ArgumentNullException(nameof(source));if (predicate == null) throw new ArgumentNullException(nameof(predicate));//本地函數IEnumerable<T> Iterator(){foreach (var element in source)if (predicate(element))yield return element;}return Iterator(); }

    那么這時拋出異常的位置將發生在?Filter(list, null)?所在的行,也就是在調用?Filter?方法時顯示,如圖:

    可以看出,使用了本地函數包裝迭代器邏輯的寫法,相當于把顯示異常的位置提前了,這有助于我們更快的觀察到異常并進行處理。

    同理,在使用了?async?的異步方法中,如果把異步執行部分放入?async?的本地函數中,也有助于立即顯示異常。由于篇幅問題這里不再舉例,可以查看官方文檔。

    總結

    綜上所述,本地函數是方法中的方法,但它又不僅僅是方法中的方法,它還可以出現在構造函數、屬性訪問器、事件訪問器等等成員中;本地函數在功能上類似于 Lambda 表達式,但它比 Lambda 表達式更加方便和清晰,在分配和性能上也比 Lambda 表達式略占優勢;本地函數支持范型和作為迭代器實現;本地函數還有助于在迭代器方法和異步方法中立即顯示異常。


    相關鏈接:

  • https://github.com/dotnet/roslyn/issues/3911?C# Design Meeting Notes???

  • https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/classes-and-structs/local-functions?本地函數???

  • https://docs.microsoft.com/zh-cn/dotnet/csharp/language-reference/operators/lambda-expressions?Lambda 表達式???

  • 作者 :技術譯民?
    出品 :技術譯站(https://ITTranslator.cn/)

    END

    總結

    以上是生活随笔為你收集整理的C# 中的本地函数的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。