C#プログラムの基本的な構造
まずは,dotnet new console -o HelloWorldCS --langVersion 8.0で作成されるテンプレートに含まれている,Program.csの中身について見てみよう.
// Program.cs
using System;
namespace HelloWorldCS
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
}このプログラムはC#のプログラムの基本的な構造についての示唆に富んでいる.
まず,最初に気付くのはusing System;の後にnamespace HelloWorldCS {…}が続いていることである.これらに関する説明は後の回で行うこととする.特に namespaceはプログラムの構成要素として必須ではないので,本演習では後で説明するのにとどめる.上のプログラムは,namespaceの部分を除くと
using System;
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}となる.using System;の部分はしばらくはプログラムはそういうものを含むのだと思ってほしい.
次に興味深いのはclass Program {…}の部分である.この部分はProgramというクラスを宣言している.C#はオブジェクト指向言語であり,
オブジェクトという概念が重要となる.ものすごく大雑把には,オブジェクトは 「データ(状態)と,それに関連する関数」をまとめたものであり,
クラスはオブジェクトの定義の記述(方法の一つ)で大雑把にはどういったデータ(状態)や関数がまとめられているのかを記述する.そうして「まとめられる」関数はメソッドと呼ばれる.ものすごく大雑把にはC#のようなオブジェクト指向言語では,オブジェクトを作成しそのメソッドを呼び出すことを繰り返すことで計算を行う.
さて,クラスProgramはstatic void Main(string [] args)というメソッドを持っている.このMainメソッドは特殊なものであり,Cのmain関数のように,プログラムが実行されるときに実行されるメソッドを表している.ここでConsole.WriteLineは文字列が引数として与えられた場合にはそれを標準出力へ出力するメソッドであるので,このプログラムを実行するとHello World!という文字列が標準出力に出力されることになる.
クラスのとても基本的な構造
まずは,クラスはどういう構造をもっているかを見ていこう.基本的には以下の構造をしているが,これだとピンとこない人も多いと思うので,例を用いて説明することにする.
class { }
例:カウンター
今,カウンターを実装することを考える.カウンターの機能はシンプルに数をカウントするだけである.カウンターオブジェクトは内部でカウントを持っていて,作成時にカウントの初期値を指定することができる.またカウンターに対してできることは
- カウントを1増やす
- カウントを0にもどす
- 現在のカウントを調べる
のみであるようにしたい.
Counterクラスのフィールドとコンストラクタ
ではこの要求を少しずつ形にしていってみよう.まずは,カウンターオブジェクトは内部でカウントを持っているので,以下を実装する.
// クラスCounterの宣言
class Counter
{
// int型のprivateフィールド count の宣言
private int count;
// Counterのコンストラクタ
public Counter(int c0)
{
// フィールド count に c0 を代入
count = c0;
}
}ここで,private int count;の部分がフィールド宣言である.これはカウンターオブジェクトがcountというint型のフィールドを持っていることを表している.また,private修飾子はこのフィールドがこのクラスの外からはアクセスできないことを表している.public Counter(int c0) {…}の部分はこのクラスの
コンストラクタを定義している.コンストラクタはオブジェクトを生成する際に呼ばれ,ここではフィールドcountをc0で初期化している.コンストラクタはクラスと同じ名前である必要がある.public修飾子はこのコンストラクタが Counter クラスの外からアクセスできることを表している.
カウンターオブジェクトの作成,より正確な言い方をすればCounterクラスのインスタンス(オブジェクトのことをインスタンスと呼ぶことがある)は new式を用いることで行うことができる.
Counter c = new Counter(0); この文の実行後,cはnew Counter (0)によって作成されたCounterクラスのインスタンスを指すこととなる.すなわち大雑把には以下のような状況になる.
+-----------+
c---------------------->| Counter |
| --------- |
| count = 0 |
+-----------+
その後
Counter d = new Counter(7);とすれば,下記のようにカウンターオブジェクトが二つ作成される.
+-----------+
c---------------------->| Counter |
| --------- |
| count = 0 |
+-----------+
+-----------+
d---------------------->| Counter |
| --------- |
| count = 7 |
+-----------+
しかしながら,現在のままではこれらのカウンターcおよびdについてできることがない.これらのオブジェクトの唯一のフィールドであるcountはprivateなので,Counterクラスの外からは見えないためである.たとえば以下のプログラムはコンパイル時エラーとなる.
using System;
class Counter
{
private int count;
public Counter(int c0)
{
count = c0;
}
}
class Program
{
static void Main(string[] args)
{
Counter c = new Counter(0);
// c.count.ToString()はオブジェクトcのフィールドcountに対し,ToString()メソッドを呼ぶの意
// Console.WriteLine(s)は文字列sを標準出力に改行付きで出力する
Console.WriteLine(c.count.ToString()); // エラー
}
}Note
単に実行するだけならば,MainメソッドをCounterの中に書けばよい.
using System;
class Counter
{
private int count;
public Counter(int c0)
{
count = c0;
}
static void Main(string[] args)
{
Counter c = new Counter(0);
Console.WriteLine(c.count.ToString());
}
}が,これはあまりよくないコードである.なぜならば,Counterはカウンターを実装するということが関心事であるのにそれ以外の処理を行っているためである.さらには,Counterは直接カウンターへのアクセスを防ぐためにcountをprivateフィールドにしたので,そのアクセス制限を台無しにしている.また,エントリポイントがCounterの中にあるということは実質的にCounterはクラスの外から使えないということでもある.
メソッドの追加
では,Counterクラスに(インスタンス)メソッドを追加してもっと面白くしてみよう.(インスタンス)メソッドMethodはオブジェクトoに対し,o.Method(arg1, arg2)のよう形で呼出すことができる関数のようなものである.具体的にはカウンタをインクリメントするメソッドInc(),リセットするメソッドReset(),取得するメソッドGetCount()を定義してみよう.
class Counter
{
private int count;
// コンストラクタ
public Counter(int c0)
{
count = c0;
}
// メソッド
public void Inc()
{
count++;
}
public void Reset()
{
count = 0;
}
public int GetCount()
{
return count;
}
}上の実装が示すように,Inc()はカウントを1増加させ,Reset()はカウントを0にリセットし,GetCount()は現在のカウントを返す.
public修飾子が示すように,これらはpublicメソッドとして実装されてるため,Counterクラスの外側でも用いることができる.
using System;
class Counter
{
private int count;
public Counter(int c0)
{
count = c0;
}
public void Inc()
{
count++;
}
public void Reset()
{
count = 0;
}
public int GetCount()
{
return count;
}
}
class Program
{
static void Main(string[] args)
{
Counter c = new Counter(0);
Counter d = new Counter(7);
// ここで,"c.GetCount() = " + c.GetCount().ToString() は
// 文字列"c.GetCount() = "と文字列c.GetCount().ToString()を連接した文字列
// を表す.
// C#では+演算子は文字列の連接にも用いられる.
// たとえば,"Hello" + "World"の結果は"HelloWorld"である.
Console.WriteLine("c.GetCount() = " + c.GetCount().ToString());
Console.WriteLine("d.GetCount() = " + d.GetCount().ToString());
c.Inc(); // cの指すカウンタをインクリメント
c.Inc(); // cの指すカウンタをインクリメント
Console.WriteLine("c.GetCount() = " + c.GetCount().ToString());
c.Reset(); // cの指すカウンタをリセット
Console.WriteLine("c.GetCount() = " + c.GetCount().ToString());
c.Inc(); // cの指すカウンタをインクリメント
Console.WriteLine("c.GetCount() = " + c.GetCount().ToString());
// cとdは異なるカウンタオブジェクトを指しているため,
// cの指すカウンタオブジェクトの操作は,
// dの指すカウンタオブジェクトに影響しない.
Console.WriteLine("d.GetCount() = " + d.GetCount().ToString());
}
}このプログラムをビルド・実行すると以下の出力が得られる.
c.GetCount() = 0
d.GetCount() = 7
c.GetCount() = 2
c.GetCount() = 0
c.GetCount() = 1
d.GetCount() = 7
メソッドInc()等が,obj.Inc()の形で呼び出されていることを確認しよう.また,(インスタンス)メソッドInc()の定義において(インスタンス)フィールドcountが参照されていることにも注意する.大雑把には,このcountは,メソッドInc()が属するインスタンスの同名のフィールドを差している.
Note
C#の慣習として public なメソッドやフィールド(およびプロパティ)の名前は大文字で始める.
Note
大雑把には,Counterクラスはアクセス制御子等を無視すればCにおける以下の構造体定義と関数のあつまりと似たようなものであるように理解できる.
struct counter { int count; };
void construct_counter(struct counter* p, int c) {
p->count = c;
}
struct counter* new_counter(int c) {
struct counter* p = (struct counter*) malloc( sizeof(struct counter) );
construct_counter(p, c);
return p;
}
void increment(struct counter* p) {
p->count++;
}
void reset(struct counter* p) {
p->count = 0;
}
int get_count(struct counter* p) {
return p->count;
}クラスと,こうした構造体定義と関数のあつまりの違いには次回触れる.
Note
上で「cは…指すことになる」と述べたが,cがインスタンスを「指す」ということは以下の挙動を理解する上で重要である.
Counter c = new Counter(0);
Counter d = c; // dはcと同じインスタンスを指す
Console.WriteLine(d.GetCount().ToString()); // 0
// なので,c.Inc()をすれば…
c.Inc();
// …d.GetCount()の値も変化する
Console.WriteLine(d.GetCount().ToString()); // 1この点については第3回のNoteでも少し触れる.
staticメソッドの追加
さらにCounterを便利なものにするために,"12"や"-34"といった文字列からカウンターを作成することを考えてみよう.
このような処理はコンストラクタとしても実装可能である.が,カウンターオブジェクトの生成という関心事からいささか複雑すぎるので,今回はそのようなアプローチは取らないことにする.一方で,このような処理をCounterクラスの(インスタンス)メソッドとして実装することはできない.これは,インタンスメソッドはインタンスに属するものであるが,行いたい処理の時点ではインスタンスはまだ生成されていないためである.実際に,構文上においても,インスタンスoに対するインスタンスメソッドMethodの呼出しはo.Method(arg1, arg2)となっていて,インスタンスが与えられなければインスタンスメソッドを呼び出すことができない.
staticメソッドを用いればこの問題を解決することができる.staticメソッドはざっくりと説明するならば, インスタンスではなくクラスに属するメソッド(この言い方は少し正確でない.C#だと構造体型がありそちらもstaticメソッドを持てるので)である.そのため,インスタンスが与えられなくても呼び出すことが可能である.具体的には以下の形のメソッドを追加する.
public static Counter Parse(string s)
{
...
}static修飾子はこのメソッドがstaticメソッドであることを表している.
さて,この関数の実装であるが,
Int32.Parseメソッド(参考:.NETのAPIリファレンス)を用いることで実装できる.
public static Counter Parse(string s)
{
int i = Int32.Parse(s);
return new Counter(i);
}Int32.Parseもstaticメソッドの一つである.また,Console.WriteLineも実はstaticメソッドである.このようにstaticメソッドMethodはクラスCに対し,C.Method(arg1, arg2)のようにすることで呼び出すことができる.実際に以下のプログラムをビルド・実行してみよう.
using System;
class Counter
{
private int count;
public static Counter Parse(string s)
{
int i = Int32.Parse(s);
return new Counter(i);
}
public Counter(int c0)
{
count = c0;
}
public void Inc()
{
count++;
}
public void Reset()
{
count = 0;
}
public int GetCount()
{
return count;
}
}
class Program
{
static void Main(string[] args)
{
Counter c = Counter.Parse("3");
Console.WriteLine("c.GetCount() = " + c.GetCount().ToString());
c.Inc();
c.Inc();
Console.WriteLine("c.GetCount() = " + c.GetCount().ToString());
}
}すると以下の出力が得られる.
c.GetCount() = 3
c.GetCount() = 5
ところで,読者の中には以下では何故問題なのかと疑問に思った人もいるかもしれない.
public static Counter Parse(string s)
{
int i = Int32.Parse(s);
count = i; // エラー
}直観的にはParseはクラスに属するメソッドであるので,インスタンスに属するフィールドcountをオブジェクト経由なしにアクセスすることができないためである.
フィールド宣言の基本的な形
;
あるいは
= ;
例
private int count; 修飾子
上で出てきたstatic,public等は修飾子と呼ばれるものである.中でもprivateやpublicはアクセス修飾子と呼ばれ,メソッドやフィールドにアクセスする範囲を限定する.アクセス修飾子にはたとえば以下のようなものがある(全部ではない).
private: それを含むクラスの中からしかアクセスできない.メソッドやフィールドなどの修飾子を省略した場合はprivateになる.public: どこからでもアクセスできる.protected: 自身と派生クラス(次回)のみからアクセスできる.internal: 同一コンパイル単位内からのみアクセスできる.(ネストされていない)クラスの修飾子を省略した場合はinternalになる.
型
型にはCounter等のクラスや,int等の数値型,bool(真偽値型)やstring(文字列型)などがある.数値型にはたとえば,以下のようなものがある(全部ではない).
整数型
sbyte: 符号付き8ビット(1バイト)整数型.-128〜127byte: 符号なし8ビット(1バイト)整数型.0〜255int: 符号付き32ビット整数型.-2,147,483,648〜2,147,483,647uint: 符号なし32ビット整数型.0〜4,294,967,295long: 符号付き64ビット整数型.-9,223,372,036,854,775,808〜9,223,372,036,854,775,807ulong: 符号なし64ビット整数型.0〜18,446,744,073,709,551,615
浮動小数点数型
float: 単精度浮動小数点型double: 倍精度浮動小数点型
コンストラクタ宣言の基本的な形
() { ... }
例
public Counter(int c0)
{
count = c0;
}メソッド宣言の基本的な形
() { ... }
返り値がない場合は,voidを「返り値の型」として用いる.
例
public int GetCount()
{
return count;
}public void Reset()
{
count = 0;
}static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}基本的な文(statement)
宣言文
(主に)変数を宣言する文.
;
= ;
例
Counter c = new Counter(0); 変数の型が右辺から明らかな場合には「型」の部分に型を書く代わりにvarと書くことができる.たとえば上記は
var c = new Counter(0);とも書ける.また,複数の宣言をまとめて行うことができる.
int x, y = 2, z; 式文(いわゆる代入文など)
「式」のみからなる文.全ての式が式文として使用できるわけではない.代表的なものは,インクリメントやデクリメント(前置後置),メソッド呼出,代入など.
例
count++;Console.WriteLine("Hello World");count = 0;return文
メソッドから値を返す.
return
例
public int GetCount()
{
return count;
}ブロック
複数の文をまとめた文.
{
...
}
if文
真偽値によって分岐を行う文.
if ()
if () else
例
// a と bの小さいほうを返す.
if (a < b) { return a; } else { return b; } // isSwapがtrueならaとbの中身を入れ替える.
if (isSwap) { var tmp = a; a = b; b = tmp; } Tip
「式がtrueに評価されたときに実行される文」や「式がfalseに評価されたときに実行される文」のところにはブロックを書くようにするとよい.以下のようなミスを防ぐことができる.
if (x > 0)
c++;
d++; // x > 0 でないときも実行される.if (x)
if (y) a++;
else b++; // 内側のifに対応するelse(b++はxがtrueでyがfalseのときに実行される)Note
C言語と異なり条件に来る式はbool型でなければならない.これはforやwhileでも同じ.
Tip
if (a < b) {
return true;
}
else {
return false;
}上のようなコードは冗長で,もっと簡潔に
return (a < b)のように書ける.
Note
細かい話ではあるが,宣言文単独は「式がtrueに評価されたときに実行される文」や「式がfalseに評価されたときに実行される文」に来ることができない.
if (x) int b = 0; // エラー一方で以下は文法上は正しい.
if (x) {int b = 0;} for文,while文
for ( ; ; )
while ()
例
int s = 0;
// s は 0 から n - 1までの和.
for (int i = 0; i < n; i++ )
{
s += i;
} // 標準出力に"yes"を出力しつづける.
while (true) // 無限ループ
{
Console.WriteLine("yes");
}上のコードはforを用いても書ける.
for (;;)
{
Console.WriteLine("yes");
}for (;true;)
{
Console.WriteLine("yes");
}continue文,break文
continueは次のループに移る.breakはループから抜ける.
基本的な式(expression)
式:評価されて値となるもの.
メソッド呼出
例
Console.WriteLine("Hello World!")c.Inc() メソッド呼び出し式において.の左側には識別子のみならず式も来ることができる.またメソッド呼び出し式も式なので別の式の一部としても使うことができる.
c.GetCount().ToString() Console.WriteLine( (1 + 2).ToString() );オブジェクト生成式(new)
オブジェクトを生成する.
例
new Counter(0)式なので,他の式の中でも(型が合えば)使うことができる.
new Counter(0).GetCount().ToString()インクリメント(++)とデクリメント(-)
++xはxを1増やす.増やした後の値がこの式の評価結果となる.--xはxを1減らす.減らした後の値がこの式の評価結果となる.x++はxを1増やす.増やす前の値がこの式の評価結果となる.x--はxを1減らす.減らす前の値がこの式の評価結果となる.
例
int a = 1;
Console.WriteLine( "a = " + a++.ToString() ); // a = 1
Console.WriteLine( "a = " + a++.ToString() ); // a = 2int a = 1;
Console.WriteLine( "a = " + (++a).ToString() ); // a = 2
Console.WriteLine( "a = " + (++a).ToString() ); // a = 3なお ++a.ToString()は++(a.ToString())と解釈される(結果,エラーになる).これはメソッド呼出構文のほうが結合が強いため.
四則演算
+加算-減算*乗算/除算%剰余(余り)
例
3 + 4 2 + 3 * 4 // 14に評価される(2 + 3) * 4 // 20に評価される.文字列の連接
また,+演算子は文字列の連接にも使用する.
例
"Hello " + "World" // "Hello World"に評価される1.ToString() + (2 + 3).ToString() // "15"に評価されるなお,一方が文字列であればもう一方はToString()は不要(自動的に呼ばれる).
1.ToString() + (2 + 3) // "15"に評価される1 + (2 + 3).ToString() // "15"に評価される比較演算
==等しい!=等しくない<小なり<=小なりイコール>大なり>=大なりイコール
例
1 == 1 // true1 != 1 // false1 < 2 // true 1.0 <= 2.0 // true論理演算
&&論理積||論理和!論理否定
例
true && false // false true || false // true!true // false Note
&&および||は短絡する.すなわちfalse && eおよびtrue || eのeは評価されない.
代入演算
x = eはxにeの評価結果を代入する.また,それが,x = eの評価結果となる.x += eは基本的にはx = x + eに同じx -= eは基本的にはx = x - eに同じx *= eは基本的にはx = x * eに同じx /= eは基本的にはx = x / eに同じx %= eは基本的にはx = x % eに同じ- 他にも
<<=や|=などがあるが,本講義では扱わない.
int a;
Console.WriteLine(a = 1); // 1
Console.WriteLine(a); // 1
Console.WriteLine(a += 1); // 2Note
x[f()] += eはx[f()] = x[f()] + eと異なり,f()を一回しか呼ばない
Note
第5回および第6回では,「イベント」を扱うための特殊な+=および-=が登場する.