自作メソッド(関数)の定義その2

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

メソッド(関数)の名前は自由に付けることができますが、同じ名前のメソッドを複数作ることもできます。
これをメソッドのオーバーロードといいます。
(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) {}
}

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

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

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


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);
}

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

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


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メソッドの複合書式も、可変長引数により引数の数を可変にしています。

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

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

  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 value)
{
    value *= 2;

    return value;
}

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

    int func = Func(num);

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

    Console.ReadLine();

}
3
6

引数の値を加工して結果を受け取るメソッドを作る場合、通常は戻り値として受け取ります。
しかしメソッドの戻り値は一つしか指定できないので、複数の値を同時に受け取りたい場合は引数の参照渡しという方法を使用します。
参照渡しには二種類あります。

参照渡しは、データをコピーして渡すのではなくデータそのものを渡すことをイメージすると良いです。

ref修飾子

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


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

    value *= 2;
    return true;
}

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

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

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

    Console.ReadLine();
}
6
True

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

メソッドFuncは、引数に0未満の値が渡されるとメソッドの実行失敗と判断するように動作を変更しています。
そして、メソッドの実行の成否を戻り値として返します。

実行結果を見ると変数numの値が変化しているのがわかります。
このように引数に渡した値をメソッド側で直接書き換えたい場合に参照渡しを使用します。

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 void Main(string[] args)
{
    int num = 3;
    bool func = Func(out num);
}

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

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

refとoutの違い

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

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

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

参照型のデータ型は、outやrefなどを使用しない場合でも、参照先に関連付けられているデータをメソッド側で値を書き換えると呼び出し側に影響します。
今までの説明で登場している参照型は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 FuncA(int[] nums)
{
    nums = new int[0];
}

//in修飾子
//参照型の引数でも
//書き換えられないことが保障される
static void FuncB(in int[] nums)
{
    //コンパイルエラー
    nums = new int[0];
}

ただし、参照型変数そのものが書き換えられないだけで、参照型に関連付けられているデータの書き換えはできます。


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

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万のメソッドが同時に実行状態になるということ)
これだけの呼び出しが行われると、一つ一つの呼び出しで使用されるメモリは小さくてもスタック領域を食いつぶす程度にはメモリ使用量が大きくなります。

メモリ効率の点から再帰呼び出しは使用すべきではない、という人もいます。
再帰呼び出しはfor文などのループに置き換えることもできますが、再帰呼び出しのほうがコードが書きやすい場合もあります。
(ループ文内でのメソッド呼び出しは、現在のメソッドが終了してから次のメソッドを呼び出すのでスタックオーバーフローは起こらない)
ファイルやフォルダの列挙などは再帰呼び出しのほうが書きやすいので、絶対に使わないのではなく適宜使い分けると良いでしょう。