メソッドの機能

メソッドのオーバーロード

メソッド(関数)の名前は自由に付けることができますが、同じ名前のメソッドを複数作ることもできます。
これをメソッドのオーバーロードといいます。
(overload=多重定義)


static void Main(string[] args)
{
    int num1 = 2;
    int num2 = 5;
    int num3 = 10;
    
    Console.WriteLine(Add(num1, num2));
    Console.WriteLine(Add(num1, num2, num3));

    Console.ReadLine();
}

static int Add(int x, int y)
{
    return x + y;
}

//メソッドのオーバーロード
static int Add(int x, int y, int z)
{
    return x + y + z;
}
7
17

メソッドをオーバーロードするには引数の数またはデータ型を変える必要があります。


//引数の数を変えてオーバーロード
static int Func()
{
    return 0;
}

static int Func(int x)
{
    return x;
}

static int Func(int x, int y)
{
    return x + y;
}

//引数のデータ型を変えてオーバーロード
static int Func(int x)
{
    return x;
}

static double Func(double x)
{
    return x;
}

メソッドの戻り値の型は同じでも異なっても構いません。
しかし戻り値の型が異なるだけ(引数が同じ)ではオーバーロードできません。


static int Func(int x)
{
    return x;
}

//戻り値の型が違うだけではオーバーロードできない
//エラー
static double Func(int x)
{
    return x;
}

コンパイラは、メソッドの名前と引数リスト(引数の数とデータ型と順序)でそれぞれのメソッドを見分けています。
メソッド名と引数リストをまとめてシグネチャといいます。

仮引数の名前はシグネチャではないので、仮引数名を変えただけではオーバーロードできません。


static int Func(int x)
{
    return x;
}

//仮引数の名前が違うだけではオーバーロードできない
//エラー
static int Func(int num)
{
    return num;
}

static int Func(int x, int y)
{
    return x + y;
}

//もちろんこれもダメ
static int Func(int y, int x)
{
    return x + y;
}

メソッドのオーバーロードは同じメソッド名で異なる処理をするメソッドを作ることができますが、同名メソッドは可能な限り同等の機能にすることを心掛けてください。
メソッド名が同じなのに全く別の動作をさせると混乱の元となります。

デフォルト引数(オプション引数)

メソッドの呼び出しは、メソッド側で定義されている通りに引数を渡す必要があります。
しかしC#では「省略可能な引数」を作ることができます。
これをデフォルト引数(オプション引数)といいます。
(デフォルト=既定値、オプション=選択可能な、という意味)


static void Main(string[] args)
{
    Console.WriteLine(Func());
    Console.WriteLine(Func(10));

    Console.ReadLine();
}

static int Func(int x = 0)
{
    return x;
}
0
10

Funcメソッドはint型の引数を一つだけ取りますが、引数名の後ろに=と値を記述すると、

  • 引数を省略してメソッドを呼び出すと、引数xの値は0
  • 引数を省略せずにメソッドを呼び出すと、引数xは実引数通りの値
    (サンプルコードでは「10」)

という引数を作ることができます。

デフォルト引数はいくつでも定義できますが、通常の引数と混在させる場合はデフォルト引数を最後に記述するというルールがあります。


static int Func(int a, int b, int c = 0, int d = 0)
{
    return a + b + c + d;
}

//デフォルト引数を通常の引数の前に記述はできない
//static int Func(int a = 0, int b = 0, int c, int d)
//{
//    return a + b + c + d;
//}

デフォルト引数に指定する値はコンパイル時定数(const定数、リテラル)である必要があります。
変数のように、プログラム実行中にその値が変化する可能性があるものは指定できません。


class Program
{
    const string str_const = "abc";
    static readonly string str_readonly = "abc";
    static string str = "abc";

    //OK
    static void Func1(string s = "abc") {}

    //OK
    static void Func2(string s = str_const) {}

    //NG
    static void Func3(string s = str_readonly) {}

    //NG
    static void Func4(string s = str) {}

    //NG
    //string.Emptyはコンパイル時定数ではない
    static void Func4(string s = string.Empty) { }

    //OK
    //空文字列はコンパイル時定数
    static void Func4(string s = "") { }
}

定数については定数を参照してください。

デフォルト引数とメソッドオーバーロード

メソッドのオーバーロードを使用すると、デフォルト引数とほぼ同じ機能を実現可能です。


static int Func(int a, int b = 0)
{
    return a + b;
}

//上記と同じ機能を
//メソッドのオーバーロードで実現する
static int Func(int a)
{
    //Func(int a, int b)を呼び出し、
    //戻り値をそのまま返す
    return Func(a, 0);
}
static int Func(int a, int b)
{
    return a + b;
}

メソッドのオーバーロードで、引数の異なるメソッドを二つ用意します。
引数がひとつのメソッド内では、引数がふたつのメソッドを呼び出します。
このとき、デフォルト値にしたい値を指定します。

これで呼び出し側からは引数を省略可能なメソッドが定義されているのと同じことになります。

このメソッドを普通に使用する分には違いはありませんが、メソッドをデリゲートで使用する場合には違いがあります。
また、Visual Studioなどの開発環境で呼び出し側から見られるメソッドの情報にも若干の違いがあります。
呼び出し側から見たデフォルト引数とメソッドオーバーロード違い

デフォルト引数にはコンパイル時定数を使用する必要がありますが、メソッドのオーバーロードは定数以外の値も指定できるという違いもあります。


static void Func()
{
    //実行する度にランダムな値を得る
    System.Random r = new System.Random();

    //0~99のランダムな値を得る
    Func(r.Next(100));
}

static void Func(int n)
{
    Console.WriteLine(n);
}

名前付き引数

これはメソッドの定義方法ではなく呼び出し方法になります。

メソッドを呼び出すにはメソッド側で定義された順序通りに引数を指定していきますが、名前付き引数を使用すると引数の順序ではなく名前で引数を指定できます。
引数を名前で指定するには「仮引数名:値」の形式で行います。


static void Main(string[] args)
{
    //名前付き引数
    Func(num: 2, str: "abc");
    Func(str: "abc", num: 2);

    Console.ReadLine();
}

static void Func(int num, string str)
{
    Console.WriteLine(num);
    Console.WriteLine(str);
}

このコードの4行目と5行目の実行結果は同じになります。
メソッドの定義側で指定されている順序を無視し、名前で引数を渡せるのでコードが分かりやすくなります。

ただし、名前付き引数と通常の呼び出しを混在させる場合は、名前付き引数を後に指定する必要があります。


static void Main(string[] args)
{
    //OK
    Func(3, c: 5, b: 7);

    //エラー
    //名前付き引数は通常の引数の後に指定しなければならない
    Func(b: 3, 5, 7);

    //エラー
    //「a」にはすでに「3」がセットされている
    Func(3, a: 5, b: 7);

    Console.ReadLine();
}

static void Func(int a, int b, int c)
{
}

C#7.2からは名前付き引数を通常の引数よりも前に指定できるようになっています。
ただし、名前付き引数を通常の引数よりも前に指定する場合、引数の指定順序はメソッド側で定義されている通りにしなければなりません。


static void Main(string[] args)
{
	Func(a: 1, 2, 3);
}

static void Func(int a, int b, int c)
{
}

これはメソッドの呼び出し側で値を指定する仮引数名が明確になるので、で間違いを減らすのに効果的です。

可変長引数

メソッドの実行には、メソッド側で定義されている通りの数(とデータ型)の引数が必要ですが、引数の数を自由に指定できるメソッドを作ることもできます。
引数の数が可変なので可変長引数といいます。


static void Main(string[] args)
{
    int num1 = Func(3, 5, 7);
	int num2 = Func(3, 5, 7, 11, 13);
    Console.ReadLine();
}

//可変長引数
static int Func(params int[] nums)
{
    if (nums == null)
        return 0;

    int ret = 0;
    for (int i = 0; i < nums.Length; i++)
        ret += nums[i];

    return ret;
}

自作メソッドFuncの引数はint型の配列になっていますが、その前にparamsというキーワードがつけられています。
params付きの引数は可変長引数であることを表します。

メソッドの呼び出し側では、配列ではなく引数を複数個指定しているだけです。
指定する引数の数は自由に増減できます。
これが可変長引数の特徴です。

可変長引数はメソッド内では通常の配列と同じように使用できます。

上記コードを、可変長引数を使わずに書き直すと以下のようになります。


static void Main(string[] args)
{
    //配列を作成して引数に指定する
    int[] nums = new int[] { 3, 5, 7 };
    int num1 = Func(nums);

    //実引数に配列の初期化式を直接指定しても良い
    int num2 = Func(new int[] { 3, 5, 7, 11, 13 });

    Console.ReadLine();
}

static int Func(int[] nums)
{
    if (nums == null)
        return 0;

    int ret = 0;
    for (int i = 0; i < nums.Length; i++)
        ret += nums[i];

    return ret;
}

可変長引数はメソッドの呼び出し時の手間を少し減らすことができます。
例えばConsole.WriteLineメソッドの複合書式も、可変長引数で任意の数の引数を渡せるように作られています。


//Console.WriteLineメソッドの第二引数以降は可変長引数
Console.WriteLine("{0}", 1);
Console.WriteLine("{0}-{1}", 1, 2);
Console.WriteLine("{0}-{1}-{2}", 1, 2, 3);

オーバーロードの優先順位

デフォルト引数と可変長引数で同じメソッドをオーバーロードした場合の優先順位は

  1. デフォルト引数なし
  2. デフォルト引数あり
  3. 可変長引数

となります。


static void Main(string[] args)
{
    Func(1);
    Func(1, 2);
    Func(1, 2, 3);

    Console.ReadLine();
}

static void Func(int x)
{
    Console.Write("引数1個   ");
    Console.WriteLine("{0}", x);
}

static void Func(int x, int y = 0)
{
    Console.Write("引数2個   ");
    Console.WriteLine("{0} {1}", x, y);
}

static void Func(params int[] nums)
{
    Console.Write("引数3個以上 ");
    foreach (var n in nums)
        Console.Write("{0} ", n);
    Console.WriteLine();
}
引数1個   1
引数2個   1 2
引数3個以上 1 2 3 

引数の参照渡し

通常の引数の場合

メソッドの仮引数には、呼び出し側で指定した実引数の値をコピーしたものが渡されます。
コピーなので、メソッド側で仮引数を書き換えても呼び出し側の実引数には影響しません。


static int Func(int val)
{
    val *= 2;
    return val;
}

static void Main(string[] args)
{
    int num = 3;
    int func = Func(num);

    Console.WriteLine(num);
    Console.WriteLine(func);
    Console.ReadLine();
}
3
6

引数の値を加工して結果を受け取るメソッドを作る場合、通常は戻り値として受け取ります。
コピーした値をやり取りする方法を値渡しといいます。

メソッドの戻り値は一つしか指定できないので、処理の結果を複数同時に受け取りたい場合は引数の参照渡しという方法を使用します。

ref修飾子

メソッドの引数定義にrefというキーワードを付けると、その変数は参照渡しとなります。
(reference=参照)


//引数の参照渡し
static bool Func(ref int value)
{
    if (value < 0)
        return false;

    value *= 2;
    return true;
}

static void Main(string[] args)
{
    int num1 = 3;
    int num2 = -3;
    
    //呼び出し側にも「ref」が必要
    bool result1 = Func(ref num1);
    bool result2 = Func(ref num2);
    
    Console.WriteLine(result1);
    Console.WriteLine(num1);
    
    Console.WriteLine(result2);
    Console.WriteLine(num2);

    Console.ReadLine();
}
True
6
False
-3

refキーワードはメソッド定義側と呼び出し側の両方につける必要があります。

上記のFuncメソッドは、引数に0未満の値が渡されるとメソッド実行失敗とみなし、falseを返します。
0以上の場合は引数の値を倍にし、trueを返します。

実行結果を見るとMainメソッドのローカル変数num1の値がメソッドの呼び出し後に変化しているのがわかります。
もう一つのnum2の方は実引数がマイナスなのでメソッドの処理は行われず、値が変化していません。
このようにメソッドの処理の結果を複数同時に受け取ることができます。

参照渡しは実引数をコピーするのではなく、実引数への参照がメソッド内に渡されます。
これは実引数そのものを渡すという意味で、Funcメソッド内ではMainメソッドのローカル変数(num1num2)を直接操作するのと同じことになります。
そのため、メソッド内で値を書き換えると呼び出し側の値も書き変わります。

なお、定数やリテラルなどの書き換えができない値は引数の参照渡しはできません。
(後述するin修飾子では可能です)


static void Func(ref int value) { }

static void Main(string[] args)
{
    //エラー
    bool result1 = Func(ref 3);
}

out修飾子

参照渡しにはoutというキーワードを付ける方法もあります。


//out修飾子による引数の参照渡し
static bool Func(int value, out int result)
{
    if (value < 0)
    {
        //メソッド終了前に何か値を代入する必要がある
        result = 0;
        return false;
    }

    result = value * 2;
    return true;
}

static void Main(string[] args)
{
    int num;

    //呼び出し側にも「out」が必要
    bool func = Func(3, out num);

    Console.WriteLine(num);
    Console.WriteLine(func);

    Console.ReadLine();
}
6
True

outで渡した値は、メソッド内で使用する前に、またはメソッドが終了する前に必ず値を代入する必要があります。

サンプルコードのFuncメソッドは、第一引数の値が0未満ならばメソッドの実行失敗と判断してメソッドを抜けます。
そのままreturnでメソッドを終了すると仮引数resultはメソッド内で何も値が割り当てられないまま終了してしまいます。
これはout修飾子による参照渡しではエラーになるので、何か適当な値を割り当ててからメソッドを終了しなければなりません。

同様に、メソッド内で値を代入する前にその値を使用しようとするとエラーになります。


static bool Func(out int result)
{
    //エラー
    //resultは代入前に使用できない
    int num = result * 2;
    return true;
}

static void Main(string[] args)
{
    int num = 3;
    bool func = Func(out num);
}

つまり、outにより参照渡しをした値はメソッド内で使うことはできないということです。
out修飾子は「呼び出し側から値を受け取る」のではなく「呼び出し側に値を渡す」、つまりアウトプットのための機能です。

引数指定と同時に変数を宣言

outで参照渡しする場合、そのための変数をあらかじめ宣言しておく必要があります。
中身はメソッド内で必ず書き換えられてしまうので、呼び出し側では実引数に指定する時点で変数にどのような値が入っていても、あるいは初期化されていなくても問題ありません。
むしろ値を受け取るためだけに宣言される変数は、宣言時に型推論(var)が使えないので少し面倒な場合もあります。
何かの値で初期化をすれば型推論は使えますが、使われない値で初期化をするのは無駄です。


//配列内の最小値と最大値を取得するメソッド
static bool MinMax(int[] array, out int min, out int max)
{
    if (array == null || array.Length == 0)
    {
        min = int.MinValue;
        max = int.MaxValue;
        return false;
    }

    min = int.MaxValue;
    max = int.MinValue;
    foreach (var n in array)
    {
        min = Math.Min(n, min);
        max = Math.Max(n, max);
    }
    return true;
}

static void Main(string[] args)
{
    int min, max; //事前に変数を用意する必要がある
    MinMax(new int[] { 1, 2, 3, 4, 5 }, out min, out max);
    Console.WriteLine("min: {0}, max: {1}", min, max);
}

C#7.0からは、outで参照渡しする変数を実引数の指定時に同時に宣言することが可能です。


static bool MinMax(int[] array, out int min, out int max)
{
    //省略
}

static void Main(string[] args)
{
    //int min, max; //事前の変数宣言は必要ない

    //実引数の指定時に同時に変数宣言
    //型推論も可能
    MinMax(new int[] { 1, 2, 3, 4, 5 }, out var min, out var max);
    Console.WriteLine("min: {0}, max: {1}", min, max);
}

目的のデータ型を記述しても良いですし、varによる型推論も可能なので簡潔な記述が可能です。

refとoutの違い

ref修飾子とout修飾子の違いを簡単にまとめると以下になります。

  ref out
呼び出し側 必ず初期化済み変数を渡す 初期化前の変数を渡せる
メソッド側 通常の引数と同じ
使わなくても問題ないし、書き換えなくても問題ない
使用前に初期化が必要
(つまり受け取った値は使えない)
メソッド終了までに値を代入する必要がある
要するに メソッド内で値を加工したい場合はref
実引数はメソッド内で書き換えられるかもしれないし、書き換えられないかもしれない
メソッド内で発生する値を受け取りたい場合はout
実引数はメソッド内で必ず書き換えられる

out修飾子はメソッドの処理結果が戻り値だけでは足りない場合に使用する、と考えると良いでしょう。

out引数の内部的な動作はref引数と同じで、参照によって値を直接やり取りするものです。
out引数はref引数の機能を制限したもので、これによりメソッドの機能や意図を明確にすることができます。

参照型のデータ型は、outrefなどを使用しない場合でも、参照先に関連付けられているデータをメソッド側で値を書き換えると呼び出し側に影響します。
今までの説明で登場している参照型はstring型、object型、配列型です。


static void Main(string[] args)
{
    int[] nums = new int[] { 1, 2, 3 };

    Func(nums);
    for (int i = 0; i < nums.Length; i++)
        Console.WriteLine(nums[i]);

    Console.ReadLine();
}

//配列の要素をメソッド内で書き換えると
//呼び出し側にも影響する
static void Func(int[] vals)
{
    for (int i = 0; i < vals.Length; i++)
        vals[i] *= 2;
}
2
4
6

string型は参照型ですが、文字列は書き換え不可なので上記のようなコードは書けません。
string型引数に別の文字列リテラルを代入することはできますが、この場合は呼び出し元には影響しません。
これはint型配列の引数に別の配列を代入した場合も同様です。


static void Main(string[] args)
{
    string str = "abc";
    int[] nums = new int[] { 1, 2, 3 };

    FuncString(str);
    FuncInt(nums);

    Console.WriteLine(str);
    for (int i = 0; i < nums.Length; i++)
        Console.Write("{0} ", nums[i]);

    Console.ReadLine();
}

static void FuncString(string s)
{
    s = "XYZ";
}
static void FuncInt(int[] vals)
{
    vals = new int[] { 7, 8, 9 };
}
abc
1 2 3 

詳しくは値型と参照型で改めて説明します。

in修飾子

C#7.2以降はinという修飾子でも参照渡しができます。
in修飾子は、メソッド内で書き換えることができない(つまり読み取り専用の)参照を作れます。


static void F(in int num)
{
    //コンパイルエラー
    num = 2;
}

引数の値渡しは実引数のコピーが渡されるため、コピーのためのコストが発生し、データ型のサイズが大きいほどコストが大きくなります。
引数の参照渡しは、実引数への参照情報がメソッド側に渡されます。
この参照情報のサイズはデータ型によらず一定で、サイズも大きくありません。

構造体などはそれなりに大きなサイズになることがあり、値渡しでコピーを発生させるよりも参照渡しのほうが効率が良い場合があります。
しかしref修飾子による参照渡しはメソッド側で値が書き換えられてしまう可能性があります。
もちろん書き換えなければ良いのですが、ミスしてしまう可能性はありますし、他人が作ったライブラリがref参照渡しを要求している場合、その値がメソッド内で書き換えられるかどうかは確認が必要です。

inによる参照は、そのような心配がなく実引数の書き換えが起こらないことが保証されます。

refoutはメソッド側で値が書き換えられるため、意図せず書き換えられることを防ぐため呼び出し側でもref/out修飾子が必要です。
inはそのおそれがないため、呼び出し側ではin修飾子は必要ありません。
(付けても良い)
また、通常の引数のようにリテラルや式、関数の戻り値などをそのまま実引数に指定できます。


static void Main(string[] args)
{
    int n = 1;

    //以下は全てOK
    F(n);
    F(in n);
    F(2 + 3);
}

static void F(in int num)
{
    Console.WriteLine(num);
}

なお、元が参照型の値をinで参照渡した場合、元の値が参照していた先の値は書き換え可能です。


static void Func(in int[] nums)
{
    //これはOK
    nums[0] = 99;
}

ドキュメントコメント

自作メソッドには、その定義の直前に特殊なコメントを書くことができます。


/// <summary>
/// float型配列の平均値を返す
/// </summary>
/// <param name="vals">平均を求めるfloat型配列</param>
/// <returns>平均値</returns>  
static float Average(params float[] vals)
{
    if (vals == null)
        return 0f;

    float ret = 0;
    for (int i = 0; i < vals.Length; i++)
        ret += vals[i];

    return ret / vals.Length;
}

これはメソッドの内容や引数、戻り値などを明示するためのコメントでドキュメントコメントと言います。
通常のコメントは「//」から始まりますが、ドキュメントコメントは「///」とスラッシュ3つから始まります。
Visual Studioならば、メソッドの定義後に上部にスラッシュを3つ打ち込むとメソッド定義に沿ったドキュメントコメントが自動的に生成されます。

ドキュメントコメントを書くと、コード記述中にコードエディタ上からそのメソッドの説明を表示することができます。
エディタ上でのドキュメントコメントの表示

外部に公開するライブラリを作る場合などはドキュメントコメントを記述しておいた方が使う人にとって便利になります。

ドキュメントコメントはタグを利用して記述します。
(HTMLのタグと書き方は同じ)
例えば「summary」タグを書く場合「<summary>概要</summary>」という風に「<tag>」と「</tag>」の間に目的のコメントを記述します。

メソッドのドキュメントコメントでよく使用されるタグには以下のようなものがあります。

summary
メソッドの概要
param
引数の説明
name="○○"という形で引数名を指定する
returns
戻り値の説明
remarks
メソッドの詳細な説明

ドキュメントコメントはコードエディット中に説明を表示するだけではなく、その説明文をXMLファイルとして出力することもできます。
(本来はこちらがメインの機能です)
内容が初心者向けではないのでここでは簡単な説明に留めておきます。

  1. Visual Studioの上部メニューの「プロジェクト」から「○○のプロパティ」を開く
    (「○○」はプロジェクト名)
  2. 左部メニューから「ビルド」を選択
  3. 「出力」から「XMLドキュメントファイル」のチェックを有効にする

これで指定の出力パス(デフォルトでは実行ファイルと同じ場所)にXMLドキュメントファイルが生成されます。

メソッドの再帰呼び出し

メソッドはその定義の中で自分自身を呼び出すこともできます。
このような処理を再帰呼び出しと言います。


/// <summary>
/// 整数値の階乗を返す
/// </summary>
/// <param name="number">階乗する整数値</param>
/// <returns>階乗された値</returns>
static int Factorial(int number)
{
    if (number > 0)
    {
        //自分自身のメソッドを呼び出す
        return number * Factorial(number - 1);
    }
    return 1;
}

static void Main(string[] args)
{
    int num = 6;
    Console.WriteLine(Factorial(num));

    Console.ReadLine();

}
720

上記のFactorialメソッドは、11行目で自分自身であるFactorialメソッドを呼び出しています。

再帰呼び出しの注意点

無限ループ

再帰呼び出しはループ処理と似た動作をします。
つまり、条件判定で適切に処理を終了させないと無限ループに陥ります。

メモリの枯渇

再帰呼び出しはメソッドを何度も呼び出す処理です。
これは単純なループとは異なり、「現在のメソッドの終了前に次のメソッドを呼び出す」ことを何度も繰り返します。
つまり、再帰呼び出しが100回行われるとすると、100個のメソッドが同時に呼び出された状態となります。

メソッドの実行時には、そのメソッドの引数やローカル変数などのために新たなメモリ領域が確保されます。
普通ならば問題にならないサイズですが、再帰呼び出しで大量に同時呼び出し状態にするとそこそこのサイズになります。

実はメソッドの引数やローカル変数に使用できるメモリ領域というのはそれほど大きくありません。
数十GBのメモリを搭載しているパソコンでもそれは変わらず、せいぜい数MB程度です。
(スタック領域という。もっと大きな領域を扱えるヒープ領域というものもあるが、引数やローカル変数では使えない)

そのため再帰呼び出しの回数が多いとメモリが足りなくなってしまい、プログラムが強制終了します。
これをスタックオーバーフローといいます。


//このコードはスタックオーバーフローが起きる可能性が高い

/// <summary>
/// 整数値の総和を返す
/// </summary>
/// <param name="number">総和する整数値</param>
/// <returns>総和された値</returns>
static int Summation(int number)
{
    if (number <= 0)
        return 0;
    return number + Summation(number - 1);
}

static void Main(string[] args)
{
    int num = 50000;
    Console.WriteLine(Summation(num));

    Console.ReadLine();
}

上記のコードでは再帰呼び出しが5万回行われることになります。
(つまり5万のメソッドが同時に実行状態になるということ)
これだけの呼び出しが行われると、一つ一つの呼び出しで使用されるメモリは小さくてもスタック領域を食いつぶす程度にはメモリ使用量が大きくなります。

これと同等の処理をループ文で書く場合、ひとつのメソッドを終了してから次の呼び出しを行うので、スタックオーバーフローは起こりません。
(メソッド終了時点で使用していたメモリは解放される)
そのためメモリ効率の点から再帰呼び出しは使用すべきではない、という人もいます。
処理によってはループ文よりも再帰呼び出しのほうが書きやすい場合もあるので、適宜使い分けると良いでしょう。
例えばPCのファイル構造の走査などはスタックオーバーフローが起こるほど深い呼び出しは起こらないので再帰呼び出しで書かれることが多いです。

ローカル関数

C#7.0以降、メソッドの中にさらにメソッドを定義することができます。
これをローカル関数といいます。


//通常の関数
static void Main(string[] args)
{
    //ローカル関数の実行
    //関数の定義より手前でも実行可能
    M(1);

    //ローカル関数
    void M(int n) 
    {
        Console.WriteLine(n);
    }
}

ローカル関数はローカル変数と同じように、その関数内からのみアクセス可能です。
それ以外は基本的に通常のメソッドと同じと考えて良いです。

ただし外部からはアクセスできないので、アクセス修飾子(publicprivateなど)は付けられません。
(→アクセス修飾子)
また、valueという名前の引数は使用できない制限があります。

メソッドのオーバーロードはC#12.0時点ではできないようです。

ローカル関数と同じスコープ内のローカル変数は関数内で使用できます。
関数の実行よりも後で宣言された変数は使用できません。


static void Main(string[] args)
{
    int num1 = 0;

    M();

    int num2 = 0;

    //ローカル関数
    void M()
    {
        Console.WriteLine(num1);

        //関数実行の位置より後で宣言された変数は使用できない
        //Console.WriteLine(num2);
    }
}

ローカル関数は、処理を関数に分けたいが他からはアクセスさせたくない場合に使用できます。
似たようなことはラムダ式でも可能ですが、ラムダ式ではできない(難しい)ことでもローカル関数なら可能なこともあります。

式形式のメソッド

メソッドは処理部分を波括弧{}で囲って定義するのが通常の方法ですが、これは文法上は「文」というものになります。
文の中には複数の式が記述できます。

文と式の違いは、大まかに言えば評価して値を得ることができるものが式、評価自体ができないものが文です。
たとえばif文は評価自体ができないため、if文の実行結果から値を得て変数に格納するというようなことはできません。
メソッドの定義自体は文で行いますが、定義したメソッドの名前は式になるので、実行して値(戻り値)を得ることができます。

メソッド本体の定義が一行の式で済む場合、そのメソッドは式形式で定義することができます。


static int Max(int a, int b)
    => a > b ? a : b;

//↑は↓と同じ意味
static int Max(int a, int b)
{
    return a > b ? a : b;
}

メソッドの処理はa > b ? a : b;の一行(式)です。
式形式のメソッドは波括弧の代わりに=>という記号(ラムダ式演算子)で行います。

戻り値がある場合はその式の評価値が戻り値となります。
return文は記述しません。