.NET開発者のためのブログメディア

C#のローカル関数とは?使い方やメリットをご紹介!
目次
C#のローカル関数とは?
今回は、C#のローカル関数について説明します。ローカル関数とは、関数の中に記述する関数のことで、C# 7.0から追加された機能です。
ローカル関数の使い方やメリットについて説明します。C#のローカル関数に興味のある方はぜひご覧ください。
ローカル関数とは
C# 7.0までは、通常の関数は以下のように記述していました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
static void Main(string[] args) { System.Console.WriteLine(Func1(2)); } static int Func1(int a) { // 引数チェック if (a < 0) return 0; return Func2(a, 1); } static int Func2(int a, int b) { // 引数チェック if (a < 0) return 0; if (b < 0) return 0; return a + b; } |
ローカル関数を使うと、以下のように記述できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
static void Main(string[] args) { System.Console.WriteLine(Func1(2)); } static int Func1(int a) { // 引数チェック if (a < 0) return 0; return Func2(1); int Func2(int b) { // aは引数チェックしているので、チェック不要 // 親関数の変数を使用できる return a + b; } } |
当然ながら、どちらも同じ結果になります。
また、通常の関数定義と同様に再帰やイテレータが記述できます。
ローカル関数のメリット
C#のローカル関数のメリットとしては、以下の内容があげられます。
・内部的にしか使用しない変数の値を隠蔽可能
・変数のスコープが関数内に限定されるので、プログラマが読みやすい
・親関数で引数チェックされていれば、改めての引数チェックは不要(コードがシンプルになる)
・親関数の変数を使用できるので、ローカル関数の引数が少なくなる(コードがシンプルになる)
・メソッド反復子や非同期メソッドで例外を直ちに検出できる(後述します)
ローカル関数の書き方
C#のローカル関数の書き方は、通常の関数とほとんど同じです。
次のような違いがあります。
・staticキーワードは付与できない
・アクセス修飾子を付与できない
・属性を付与できない
また、C#のローカル関数は、ローカル変数とは違い、定義前でも参照できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
static int Func1(int a) { // 引数チェック if (a < 0) return 0; int Func2(int b) { return a + b; } // Func2の定義は上に記述している System.Console.WriteLine(Func2(1)); // Func3の定義は下に記述している System.Console.WriteLine(Func3(1)); int Func3(int b) { return a - b; } return 1; } |
ローカル関数と例外(メソッド反復子)
C#のローカル関数は、例外を直ちに検出できるメリットがあります。メソッド反復子の場合、例外は、反復子を取得した時点では検出されず、シーケンスを列挙する時点で検出されます。
実際のソースコードを見てみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
using System; using System.Collections.Generic; public class Program { static void Main(string[] args) { IEnumerable<int> val1 = GetIntListYeild(3); IEnumerable<int> val2 = GetIntListYeild(-1); // ここでは例外が検出されない try { foreach (var i in val1) { Console.WriteLine(i.ToString()); } foreach (var i in val2) // ここで例外が検出される { Console.WriteLine(i.ToString()); } } catch (Exception e) { Console.WriteLine(e.GetType()); } } static IEnumerable<int> GetIntListYeild(int max) { // 引数チェック if (max < 1) { throw new ArgumentOutOfRangeException(); } for (int i = 0; i < max; i++) { yield return i; } } } |
実行結果は以下のようになります。
1 2 3 4 |
0 1 2 System.ArgumentOutOfRangeException |
例外は、反復子を取得した時点では検出されず、シーケンスを列挙する時点で検出されることが分かります。
これに対して、ローカル関数にすると、反復子を取得した時点で例外を検出できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 |
using System; using System.Collections.Generic; public class Program { static void Main(string[] args) { IEnumerable<int> val1 = null; IEnumerable<int> val2 = null; try { val1 = GetIntListYeild(3); val2 = GetIntListYeild(-1); // ここで例外が検出される } catch (Exception e) { Console.WriteLine(e.GetType()); } if (val1 != null) { foreach (var i in val1) { Console.WriteLine(i.ToString()); } } if (val2 != null) { foreach (var i in val2) { Console.WriteLine(i.ToString()); } } } static IEnumerable<int> GetIntListYeild(int max) { // 引数チェック if (max < 1) { throw new ArgumentOutOfRangeException(); } return LocalFunc(); IEnumerable<int> LocalFunc() { for (int i = 0; i < max; i++) { yield return i; } } } } |
実行結果は以下のようになります。
1 2 3 4 |
System.ArgumentOutOfRangeException 0 1 2 |
ArgumentOutOfRangeExceptionが先に発生しています。
反復子を取得した時点で例外が検出されることが分かります。
ローカル関数と例外(非同期メソッド)
非同期メソッドの場合、例外はAggregateExceptionにラップされ、タスク待機中に検出されます。
実際のソースコードを見てみましょう。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
using System; using System.Threading.Tasks; public class Program { static void Main(string[] args) { Task<int> task1 = AsyncFunc(3); Task<int> task2 = AsyncFunc(-1); // ここでは例外が検出されない try { task1.Wait(); int result1 = task1.Result; Console.WriteLine(result1); task2.Wait(); // ここで例外が検出される int result2 = task2.Result; Console.WriteLine(result2); } catch (Exception e) { Console.WriteLine(e.GetType()); } } static async Task<int> AsyncFunc(int secondsDelay) { // 引数チェック if (secondsDelay < 0 || secondsDelay > 5) { throw new ArgumentOutOfRangeException(); } // 待ち合わせ await Task.Delay(secondsDelay * 1000); return 0; } } |
実行結果は以下のようになります。
1 2 |
0 System.AggregateException |
このように、例外はAggregateExceptionにラップされ、タスク待機中に検出されます。
これに対して、ローカル関数にすると非同期メソッドを呼び出す前に例外を検出できます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
using System; using System.Threading.Tasks; public class Program { static void Main(string[] args) { Task<int> task1 = null; Task<int> task2 = null; try { task1 = AsyncFunc(3); task2 = AsyncFunc(-1); // ここで例外が検出される } catch (Exception e) { Console.WriteLine(e.GetType()); } if (task1 != null) { task1.Wait(); int result1 = task1.Result; Console.WriteLine(result1); } if (task2 != null) { task2.Wait(); int result2 = task2.Result; Console.WriteLine(result2); } } static Task<int> AsyncFunc(int secondsDelay) { // 引数チェック if (secondsDelay < 0 || secondsDelay > 5) { throw new ArgumentOutOfRangeException(); } return LocalFunc(); async Task<int> LocalFunc() { // 待ち合わせ await Task.Delay(secondsDelay * 1000); return 0; } } } |
実行結果は以下のようになります。
1 2 |
System.ArgumentOutOfRangeException 0 |
このように、ローカル関数にすると非同期メソッドを呼び出す前に例外を検出できます。
また、例外はAggregateExceptionにラップされることもありません。
C#のソースコードを書いてみよう
C#のローカル関数についてご紹介しましたが、いかがでしたでしょうか。C# 7.0から追加されたローカル関数には様々なメリットがあります。
通常の関数との違いを理解し、ローカル関数で作成するべきか否か検討してみてください。ぜひご自身でC#のソースコードを書いて、理解を深めてください。