C#プログラムの基本的な構造
まずは,dotnet new console -o HelloWorldCS --langVersion 8.0
で作成されるテンプレートに含まれている,Program.cs
の中身について見てみよう.
// Program.cs
using System;
namespace HelloWorldCS
{
class Program
{
static void Main(string[] args)
{
.WriteLine("Hello World!");
Console}
}
}
このプログラムはC#のプログラムの基本的な構造についての示唆に富んでいる.
まず,最初に気付くのはusing System;
の後にnamespace HelloWorldCS {…}
が続いていることである.これらに関する説明は後の回で行うこととする.特に namespace
はプログラムの構成要素として必須ではないので,本演習では後で説明するのにとどめる.上のプログラムは,namespace
の部分を除くと
using System;
class Program
{
static void Main(string[] args)
{
.WriteLine("Hello World!");
Console}
}
となる.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 を代入
= c0;
count }
}
ここで,private int count;
の部分がフィールド宣言である.これはカウンターオブジェクトがcount
というint
型のフィールドを持っていることを表している.また,private修飾子はこのフィールドがこのクラスの外からはアクセスできないことを表している.public Counter(int c0) {…}
の部分はこのクラスの
コンストラクタを定義している.コンストラクタはオブジェクトを生成する際に呼ばれ,ここではフィールドcount
をc0
で初期化している.コンストラクタはクラスと同じ名前である必要がある.public修飾子はこのコンストラクタが Counter
クラスの外からアクセスできることを表している.
カウンターオブジェクトの作成,より正確な言い方をすればCounter
クラスのインスタンス(オブジェクトのことをインスタンスと呼ぶことがある)は new
式を用いることで行うことができる.
= new Counter(0); Counter c
この文の実行後,c
はnew Counter (0)
によって作成されたCounter
クラスのインスタンスを指すこととなる.すなわち大雑把には以下のような状況になる.
+-----------+
c---------------------->| Counter |
| --------- |
| count = 0 |
+-----------+
その後
= new Counter(7); Counter d
とすれば,下記のようにカウンターオブジェクトが二つ作成される.
+-----------+
c---------------------->| Counter |
| --------- |
| count = 0 |
+-----------+
+-----------+
d---------------------->| Counter |
| --------- |
| count = 7 |
+-----------+
しかしながら,現在のままではこれらのカウンターc
およびd
についてできることがない.これらのオブジェクトの唯一のフィールドであるcount
はprivateなので,Counter
クラスの外からは見えないためである.たとえば以下のプログラムはコンパイル時エラーとなる.
using System;
class Counter
{
private int count;
public Counter(int c0)
{
= c0;
count }
}
class Program
{
static void Main(string[] args)
{
= new Counter(0);
Counter c // c.count.ToString()はオブジェクトcのフィールドcountに対し,ToString()メソッドを呼ぶの意
// Console.WriteLine(s)は文字列sを標準出力に改行付きで出力する
.WriteLine(c.count.ToString()); // エラー
Console}
}
Note
単に実行するだけならば,Main
メソッドをCounter
の中に書けばよい.
using System;
class Counter
{
private int count;
public Counter(int c0)
{
= c0;
count }
static void Main(string[] args)
{
= new Counter(0);
Counter c .WriteLine(c.count.ToString());
Console}
}
が,これはあまりよくないコードである.なぜならば,Counter
はカウンターを実装するということが関心事であるのにそれ以外の処理を行っているためである.さらには,Counter
は直接カウンターへのアクセスを防ぐためにcount
をprivateフィールドにしたので,そのアクセス制限を台無しにしている.
メソッドの追加
では,Counter
クラスに(インスタンス)メソッドを追加してもっと面白くしてみよう.(インスタンス)メソッドMethod
はオブジェクトo
に対し,o.Method(arg1, arg2)
のよう形で呼出すことができる関数のようなものである.具体的にはカウンタをインクリメントするメソッドInc()
,リセットするメソッドReset()
,取得するメソッドGetCount()
を定義してみよう.
class Counter
{
private int count;
// コンストラクタ
public Counter(int c0)
{
= c0;
count }
// メソッド
public void Inc()
{
++;
count}
public void Reset()
{
= 0;
count }
public int GetCount()
{
return count;
}
}
上の実装が示すように,Inc()
はカウントを1増加させ,Reset()
はカウントを0にリセットし,GetCount()
は現在のカウントを返す.
public修飾子が示すように,これらはpublicメソッドとして実装されてるため,Counter
クラスの外側でも用いることができる.
using System;
class Counter
{
private int count;
public Counter(int c0)
{
= c0;
count }
public void Inc()
{
++;
count}
public void Reset()
{
= 0;
count }
public int GetCount()
{
return count;
}
}
class Program
{
static void Main(string[] args)
{
= new Counter(0);
Counter c = new Counter(7);
Counter d // ここで,"c.GetCount() = " + c.GetCount().ToString() は
// 文字列"c.GetCount() = "と文字列c.GetCount().ToString()を連接した文字列
// を表す.
// C#では+演算子は文字列の連接にも用いられる.
// たとえば,"Hello" + "World"の結果は"HelloWorld"である.
.WriteLine("c.GetCount() = " + c.GetCount().ToString());
Console.WriteLine("d.GetCount() = " + d.GetCount().ToString());
Console
.Inc(); // cの指すカウンタをインクリメント
c.Inc(); // cの指すカウンタをインクリメント
c.WriteLine("c.GetCount() = " + c.GetCount().ToString());
Console
.Reset(); // cの指すカウンタをリセット
c.WriteLine("c.GetCount() = " + c.GetCount().ToString());
Console
.Inc(); // cの指すカウンタをインクリメント
c.WriteLine("c.GetCount() = " + c.GetCount().ToString());
Console
// cとdは異なるカウンタオブジェクトを指しているため,
// cの指すカウンタオブジェクトの操作は,
// dの指すカウンタオブジェクトに影響しない.
.WriteLine("d.GetCount() = " + d.GetCount().ToString());
Console}
}
このプログラムをビルド・実行すると以下の出力が得られる.
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; }
struct counter* new_counter() {
struct counter* p = new malloc( sizeof(struct counter) );
(p);
construct_counterreturn p;
}
void construct_counter(struct counter* p) {
->count = 0;
p}
void increment(struct counter* p) {
->count++;
p}
void reset(struct counter* p) {
->count = 0;
p}
int get_count(struct counter* p) {
return p->count;
}
クラスと,こうした構造体定義と関数のあつまりの違いには次回触れる.
Note
上で「c
は…指すことになる」と述べたが,c
がインスタンスを「指す」ということは以下の挙動を理解する上で重要である.
= new Counter(0);
Counter c = c; // dはcと同じインスタンスを指す
Counter d .WriteLine(d.GetCount().ToString()); // 0
Console// なので,c.Inc()をすれば…
.Inc();
c// …d.GetCount()の値も変化する
.WriteLine(d.GetCount().ToString()); // 1 Console
この点については第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)
{
= c0;
count }
public void Inc()
{
++;
count}
public void Reset()
{
= 0;
count }
public int GetCount()
{
return count;
}
}
class Program
{
static void Main(string[] args)
{
= Counter.Parse("3");
Counter c .WriteLine("c.GetCount() = " + c.GetCount().ToString());
Console.Inc();
c.Inc();
c.WriteLine("c.GetCount() = " + c.GetCount().ToString());
Console}
}
すると以下の出力が得られる.
c.GetCount() = 3
c.GetCount() = 5
ところで,読者の中には以下では何故問題なのかと疑問に思った人もいるかもしれない.
public static Counter Parse(string s)
{
int i = Int32.Parse(s);
= i; // エラー
count }
直観的には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)
{
= c0;
count }
メソッド宣言の基本的な形
( ) { ... }
返り値がない場合は,void
を「返り値の型」として用いる.
例
public int GetCount()
{
return count;
}
public void Reset()
{
= 0;
count }
static void Main(string[] args)
{
.WriteLine("Hello World!");
Console}
基本的な文(statement)
宣言文
(主に)変数を宣言する文.
;
= ;
例
= new Counter(0); Counter c
変数の型が右辺から明らかな場合には「型」の部分に型を書く代わりにvar
と書くことができる.たとえば上記は
var c = new Counter(0);
とも書ける.また,複数の宣言をまとめて行うことができる.
int x, y = 2, z;
式文(いわゆる代入文など)
「式」のみからなる文.全ての式が式文として使用できるわけではない.代表的なものは,インクリメントやデクリメント(前置後置),メソッド呼出,代入など.
例
++; count
.WriteLine("Hello World"); Console
= 0; count
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++; // x > 0 でないときも実行される. d
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++ )
{
+= i;
s }
// 標準出力に"yes"を出力しつづける.
while (true) // 無限ループ
{
.WriteLine("yes");
Console}
上のコードはfor
を用いても書ける.
for (;;)
{
.WriteLine("yes");
Console}
for (;true;)
{
.WriteLine("yes");
Console}
continue文,break文
continue
は次のループに移る.break
はループから抜ける.
基本的な式(expression)
式:評価されて値となるもの.
メソッド呼出
例
.WriteLine("Hello World!") Console
.Inc() c
メソッド呼び出し式において.
の左側には識別子のみならず式も来ることができる.またメソッド呼び出し式も式なので別の式の一部としても使うことができる.
.GetCount().ToString() c
.WriteLine( (1 + 2).ToString() ); Console
オブジェクト生成式(new
)
オブジェクトを生成する.
例
new Counter(0)
式なので,他の式の中でも(型が合えば)使うことができる.
new Counter(0).GetCount().ToString()
インクリメント(++
)とデクリメント(-
)
++x
はx
を1増やす.増やした後の値がこの式の評価結果となる.--x
はx
を1減らす.減らした後の値がこの式の評価結果となる.x++
はx
を1増やす.増やす前の値がこの式の評価結果となる.x--
はx
を1減らす.減らす前の値がこの式の評価結果となる.
例
int a = 1;
.WriteLine( "a = " + a++.ToString() ); // a = 1
Console.WriteLine( "a = " + a++.ToString() ); // a = 2 Console
int a = 1;
.WriteLine( "a = " + (++a).ToString() ); // a = 2
Console.WriteLine( "a = " + (++a).ToString() ); // a = 3 Console
なお ++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 // true
1 != 1 // false
1 < 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;
.WriteLine(a = 1); // 1
Console.WriteLine(a); // 1
Console.WriteLine(a += 1); // 2 Console
Note
x[f()] += e
はx[f()] = x[f()] + e
と異なり,f()
を一回しか呼ばない
Note
第5回および第6回では,「イベント」を扱うための特殊な+=
および-=
が登場する.