講座全体の概要
プログラミング基礎講座の第8回の後半、メソッド(関数)について学習します。講座全体の概要は以下の記事ゼロから始めるプログラミング基礎講座 第1回【講座の概要・目的】をご覧ください。
前半の内容
前半ではメソッドの使用方法について解説しました。後半となる今回は独自メソッドの作成方法、および独自メソッドの使いどころについて学習します。
準備として、Visual Studio で新しいプロジェクトを作成しましょう。名前は「BasicCourse8_1」とします。
独自のメソッドを定義する
さっそく独自メソッドを作成してみましょう。メソッドなどを作成することを定義すると言います。正確には宣言と定義を併せて行います。宣言はメソッド名、戻り値の型、引数等を決めることを言います。定義は処理の内容を決めることを言います。
以下のコードを実装してデバッグ実行で動作を確認してください。
ちなみにC#では、下の例にあるように、$"文字列"として文字列の中に{変数}を指定することで、文字列に変数の値を埋め込むことができます。+で結合するよりもわかりやすく、簡単に記述できます。
namespace BasicCourse8_1.Pages
{
public class IndexModel : PageModel
{
private readonly ILogger<IndexModel> _logger;
public IndexModel(ILogger<IndexModel> logger)
{
_logger = logger;
}
public string CourseName { get; set; } = string.Empty;
public void OnGet()
{
CourseName = GetCourseName(8, "関数について");
}
private string GetCourseName(int courseNumber, string courseTitle)
{
return $"第{courseNumber}回講座【{courseTitle}】";
}
}
}
以下の部分が独自メソッドです。
private string GetCourseName(int courseNumber, string courseTitle)
{
return $"第{courseNumber}回講座【{courseTitle}】";
}
独自メソッド例の1行目がメソッドの宣言です。構文は下の通りです。[ ]は省略可能、||はその左右どちらかを選択可能、…は直前の記述が複数指定可能であることを表しています。
[アクセス修飾子] [オプションの修飾子]... 戻り値の型||void メソッド名 ([引数の型 引数名][, 引数の型 引数名]...)
なお、引数名は呼び出し元の変数名とは無関係で、メソッド内でのみ使用される名前になります。引数の意味がわかるような名前を付けるようにしてください。
独自メソッド例の2行目以降がメソッドの定義です。{ }内が処理ブロックとなり、任意の処理を記述できます。ここでは引数を変数と同じ様に使用することができます。
宣言で戻り値の型を指定した場合は、return命令を使用して戻り値を返す必要があります。
return文の構文
return 戻り値;
戻り値には変数、プロパティ、式、リテラルを指定できますが、型が宣言と一致する必要があります。
プログラミングにおいて式とは、メソッドの呼び出し文や、演算子を用いた演算式など、最終的に何らかの値を返す文のことを指します。
プログラミングにおいてリテラルとは、数値や文字列の値を直接記述したものを指します。例えば10や、"Hello World."がリテラルです。
また、return文が実行されると、メソッド内のそれ以降の処理は行われずに中断され、メソッドの呼び出し元に処理が移ります。通常は処理ブロックの最後にreturn文を記述しますが、条件分岐で処理を中断することもできます。なお、条件分岐で処理を中断する場合、どの分岐を通っても、必ず最終的にreturn文が実行される必要があります。return文が実行されない可能性がある場合にはコンパイルエラーが表示されます。
void
voidは戻り値がないことを表すキーワードです。メソッドの宣言で戻り値の型のかわりに指定することができます。この場合、メソッドの定義ではreturn文は不要ですが、条件分岐で中断したい場合は単にreturn;と記述することが可能です。
メソッド定義の場所について
メソッド定義は、他のメンバーの中でなければ、クラス内のどこに記述しても構いません。メソッドを呼び出している箇所より後に記述しても、前に記述しても問題ありません。
これに対して変数は、使用される前に宣言する必要があります。
値型と参照型
一般的にオブジェクト指向言語の変数等の型は、値型と参照型という2種類に大別されます。
これまでに紹介した型を値型と参照型に分類すると以下の様になります。(C#での分類であり、これは言語により異なりますので別の言語を使用する場合は確認が必要です。)
- 値型
- int等の整数型
- decimal等の浮動小数点型
- 論理型のbool
- 参照型
- 文字列型のstring
- 配列
- List
続いて値型と参照型の特徴について説明します。難しく感じるかもしれませんが、必要最低限の説明にとどめますので頑張りましょう。ここを理解していないと、思わぬバグの原因になってしまいます。
以下のサンプルコードをコピペして、デバッグ実行で確認しながら説明を読んで理解してください。
public class IndexModel : PageModel
{
private readonly ILogger<IndexModel> _logger;
public IndexModel(ILogger<IndexModel> logger)
{
_logger = logger;
}
public void OnGet()
{
bool 比較結果;
// 値型のテスト
int intA = 1;
int intB = intA;
intB += 10; // 代入先の値を変更
比較結果 = (intA == 1); // 代入元に影響しないのでintAは1のままのはず。
int intC = 1;
値型引数のテストメソッド(intC);
比較結果 = (intC == 1); // 呼び出し元に影響しないのでintCは1のままのはず。
// 参照型のテスト
List<int> listA = new List<int>();
listA.Add(1);
List<int> listB = listA;
listB.Add(2); // 代入先に要素を追加
listB[0] += 10; // 代入先の0番目の要素の値を変更
// 変数の実体が同じなので以下は全部trueになるはず。
比較結果 = (listA == listB);
比較結果 = (listA.Count == listB.Count); // 要素の数
比較結果 = (listA[0] == listB[0]); // 0番目の要素の値
List<int> listC = new List<int>();
listC.Add(1);
参照型引数のテストメソッド(listC);
比較結果 = (listC.Count == 2); // 要素の数が増えているはず
比較結果 = (listC[0] == 11); // 0番目の要素の値は10足されているはず
// listBに新しい別のListを代入してテスト
listB = new List<int>();
listB.Add(100);
listB[0] += 10;
// listAとlistBは無関係なので以下は全部falseになるはず。
比較結果 = (listA == listB);
比較結果 = (listA.Count == listB.Count); // 要素の数
比較結果 = (listA[0] == listB[0]); // 0番目の要素の値
// listAとlistBは無関係なのでメソッドを呼んでも、当然、listAとlistBは異なるはず。
参照型引数のテストメソッド(listB);
比較結果 = (listA == listB);
}
private void 値型引数のテストメソッド(int arg)
{
arg += 10;
}
private void 参照型引数のテストメソッド(List<int> arg)
{
arg.Add(3);
arg[0] += 10;
}
}
- 値型の特徴
- 他の変数やプロパティに代入した場合、代入元の値のコピーが代入される。このため、代入先の変数の値を変更しても、代入元の変数の値は変更されない。
- メソッドの引数に渡した場合、基本的には値のコピーが代入される。このため、メソッド内で引数の値を変更しても、呼び出し元の変数の値は変更されない。特別な指定をすることで呼び出し元を変更することができるが、おすすめしないので説明も省略する。
- 値の代入にはリテラルが指定できる。
- 明示的に初期値を代入しなくても、型に応じた初期値が設定される。
- C#では値型でもクラスの様にメソッドを持っているが、他の言語ではメソッドを持っていないことが多い。
- 参照型の特徴
- 基本的にクラスであり、クラスのメンバーとして値が保持されている。
- 他の変数やプロパティに代入した場合、代入先の変数の値はコピーではなく、代入元とまったく同じものになる。このため、代入先の変数のメンバーを変更すると、代入元の変数のメンバーも変更される。Listを例にすると、Listが格納された変数Aを変数Bに代入し、変数Bの要素を変更すると、変数Aも実体は同じListなので変数Bの変更が反映される。
- メソッドの引数に渡した場合も同様に、引数のメンバーを変更すると、呼び出し元の変数のメンバーも変更される。
- Listの例で、変数Bのメンバー(Listの要素)の変更ではなく、変数Bそのものに別のListを代入すると、変数Aと変数Bは関係がなくなり、変数Bのメンバーの変更は変数Aに影響しなくなる。
- 初期値は明示的に代入する必要があり、初期値指定しないとnullという何もない状態になり、そのまま変数のメンバーにアクセスしようとすると実行時エラーとなる。
- string型は参照型ではあるが、値型の様に取り扱える。ただし、初期化を行わないとnullとなる点は参照型の特徴そのままなので注意が必要。値の代入が行われない可能性がある場合は、初期値としてstring.Emptyを代入しておく。
変数のスコープ
第4回講座でスコープについて解説しました。今回はメソッドの中に注目して、更に詳しく見ていきたいと思います。
Visual Studio で新しいプロジェクトを作成しましょう。名前は「BasicCourse8_2」とします。以下のコードを見ながら説明していきますので、自身のプロジェクトにコピペしてください。コピペするとコンパイルエラーが出ますが、意図したものなのでそのままで問題ありません。
namespace BasicCourse8_2.Pages
{
public class IndexModel : PageModel
{
private readonly ILogger<IndexModel> _logger;
public IndexModel(ILogger<IndexModel> logger)
{
_logger = logger;
}
private int フィールド1 = 1;
public void OnGet()
{
ローカル変数1++; // 変数宣言より前なので使用できない。
int ローカル変数1 = フィールド1;
スコープ確認用メソッド(ローカル変数1);
if (true)
{
int ローカル変数2 = フィールド1 + ローカル変数1;
スコープ確認用メソッド(ローカル変数2);
if (true)
{
ローカル関数1(); //ローカル関数1はスコープ内なので使用できる。
ローカル関数3(); // ローカル関数3で使用しているローカル変数3の宣言前なので使用できない。
int ローカル変数3 = フィールド1 + ローカル変数1 + ローカル変数2;
ローカル関数3(); // ローカル関数3の宣言前でもスコープ内なので使用できる。
スコープ確認用メソッド(ローカル変数3);
void ローカル関数3()
{
int ローカル変数4 = フィールド1 + ローカル変数1 + ローカル変数2 + ローカル変数3;
スコープ確認用メソッド(ローカル変数4);
}
ローカル関数3(); // ローカル関数3の宣言後でもスコープ内なので当然使用できる。
}
ローカル変数3++; // 変数宣言されているスコープの外なので使用できない。
}
ローカル変数2++; // 変数宣言されているスコープの外なので使用できない。
void ローカル関数1()
{
フィールド1++;
ローカル変数1++;
ローカル変数2++; // 変数宣言されているスコープの外なので使用できない。
}
}
private void スコープ確認用メソッド(int 引数1)
{
ローカル変数1++; // 変数宣言されているスコープの外なので使用できない。
引数1++;
}
}
}
フィールドやプロパティ等のクラススコープに宣言されたもの
以下の部分の1行目がクラスの宣言で、{ }内がクラスのスコープです。
public class IndexModel : PageModel
{
フィールド、プロパティ、メソッド等の宣言
}
このクラスのネストされていないスコープに直接記述されたメンバーは、クラス内のネストされたものを含む全てのスコープで使用が可能です。スコープ確認用コードでは「フィールド1」と「スコープ確認用メソッド」がどこでも使用できていることで確認できます。(コンパイルエラーにならない=使用できる)
親子関係にあるスコープで宣言された変数等の取り扱い
先ほどのクラス(親)とメンバー(子)の関係の様にネストされて親子関係にあるスコープでは、親で宣言されたものは子でも使用が可能で、子の子の子・・・と階層がどれだけ深くなっても使用が可能です。
スコープ確認用コードでは「フィールド1」、「スコープ確認用メソッド()」がどこでも使用できていること、および「OnGet()」メソッドで宣言された「ローカル変数1」がネストされたif文の中で使用できていることで確認できます。if文やfor文なども、{ }で括られている部分が1つのスコープとなります。また、この後に説明するローカル関数のスコープでも同様に使用できています。
逆に、子で宣言された変数等は自分と自分の子孫以外からは使用できません。Visual Studio では入力補完の候補にも出てきませんし、記述してもそんな名前のものはないよと言われてコンパイルエラーとなります。スコープ確認用コードでは「ローカル変数4~1」、「ローカル関数3」の使用でコンパイルエラーになっている箇所で確認できます。
メソッドの呼び出しではスコープを無視して引数を渡せる
「スコープ確認用メソッド()」に注目してもらうと、呼び出し側で引数として指定した変数は、スコープとは関係なくメソッド内で「引数1」として使用できています。メソッド自体が使用できる状況ならば、必要な変数はスコープを無視して引数として渡すことができます。(ただし、値型の場合は値のコピーになります。)
引数はメソッド定義内ではローカル変数と同様の扱いとなります。
メソッド自体はクラスのメンバーなので、クラス内のどのスコープからも使用できますし、アクセス修飾子にpublicを指定すれば、クラスの外からでも使用でき、変数の受け渡しが可能です。
C#の便利な機能 ローカル関数
上記の例では、C#特有のローカル関数という関数を使用しています。ローカル関数はメソッド内で定義できる関数で、定義したメソッド内でのみ使用できます。
他の言語ではラムダ式や匿名関数と言われる機能があるものが多いですが、詳細は省きますが、これらはメソッド内で関数を定義できるという点では同じ機能です。ただし、ローカル関数は関数の名前を付けて再利用が可能ですが、ラムダ式や匿名関数では名前を付けられません。
名前を付けられることが便利なのは、関数名で処理の概要を表せるということです。1行や2行の処理で、再利用もしないのであれば名前を付けるほどではありませんが、少しでも長くなる場合や、繰り返し使用する場合は意味が表せた方が断然読みやすいです。(可読性が上がると言います)
また、C#でもラムダ式や匿名関数は使えます。それとは別に独自のローカル関数も使えるということです。状況に応じて使いわければいいのですが、現段階で色々使う必要はありません。ローカル関数が一番わかりやすいと思いますので、メソッド内の処理をまとめたい時にはこちらを使用してください。
プログラムコードの可読性は超重要
プログラミングにおいて可読性は非常に重要です。可読性が低いと、たとえ自分一人で開発している場合でも、後から見返したら何をしているかわかりづらく、機能の変更やバグ修正が大変になります。チームで開発したり、担当の引き継ぎが発生したりする状況ではなおさらです。
Web等でプログラムのコードを紹介や解説しているものでも、ラムダ式等を乱用して可読性が低くなっているものがよく見受けられます。これは、「記述量が少なくなり、簡単に実装を済ませられる」という理由だけで使用していると思われます。可読性が上がるか、下がらないケースであれば積極的に利用するべきだと思いますが、可読性が下がるのであれば、少し面倒でも可読性を上げるように工夫すべきです。
独自メソッドの使いどころ
プログラミングを始めたばかりの頃は、どんな時にメソッド(関数)を定義すればいいのかわからないということがあると思います。慣れるまでは以下の事を意識してみてください。
・処理が長くなったら、ある程度の意味のかたまりをプライベートメソッド(アクセス修飾子でprivateを指定したメソッドのこと)に分けます。長さの明確な指標はありませんが、私は20行を超えるぐらいだと長いと感じます。このケースは、C#であれば先ほど説明したローカル関数でもいいです。
・同じような処理をクラス内の複数箇所で行う必要がある場合もプライベートメソッドにしましょう。若干の処理の違い(メソッド内で可変の要素)は、引数にして処理を可変にしましょう。
・第6回講座【分岐を理解する】延長戦で画面からの入力を受け付けて処理を行ったように、クラスの外から処理を呼び出す必要がある場合は、パブリックメソッド(アクセス修飾子でpublicを指定したメソッドのこと)にしましょう。この場合、クラスのフィールドやプロパティだけで処理ができるなら引数は不要です。その他に変数が必要ならば引数を設けましょう。処理中にフィールドやプロパティの値を、呼び出し側の指定によって可変となる値に変更する必要がある場合も引数を設けましょう。
・基本的に、スコープは狭くしましょう。具体的にはローカル関数→プライベートメソッド→パブリックメソッドの優先度で実装しましょう(左側の方が優先度が高い)。必要になった時にプライベートメソッドまたはパブリックメソッドに変更すればいいので、悩んだら優先度の高いものを選択しましょう。
コンストラクターという特別なメソッド
クラスには、クラス名と同じ名前で戻り値の型指定がない、コンストラクターと呼ばれる特別なメソッドがあります。これはクラスのメンバーを初期化する際に呼び出されます。いつも使用しているIndex.cshtml.csでは、以下の部分がコンストラクターです。
public IndexModel(ILogger<IndexModel> logger)
{
_logger = logger;
}
参照型の変数は、new クラス名(引数)の様にして初期化するが、この時に呼び出されるのがコンストラクターとなる。
List<int> listA = new List<int>();
List<int>クラスの初期化は上記の様になり、newキーワードでList<int>クラスのコンストラクターを呼び出している。上記では引数を指定していないが、引数を指定できるオーバーロードもある。
エラー処理
エラー処理についても今回解説するつもりでしたが、ボリュームが大きくなってきたので、番外編として別個に解説します。下の記事をご覧ください
以上で今回の講座を終了します。どうしてもオブジェクト指向プログラミングについてある程度理解する必要があり、難しいと感じているかもしれません。現段階で完璧に理解する必要はありませんので、学習を進める中で徐々に理解を深めましょう。
オブジェクト指向プログラミングについてもっと知りたい方は下の記事をご覧ください。
この記事へのコメントはありません。