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#のようなオブジェクト指向言語では,オブジェクトを作成しそのメソッドを呼び出すことを繰り返すことで計算を行う

さて,クラスProgramstatic void Main(string [] args)というメソッドを持っている.このMainメソッドは特殊なものであり,Cのmain関数のように,プログラムが実行されるときに実行されるメソッドを表している.ここでConsole.WriteLineは文字列が引数として与えられた場合にはそれを標準出力へ出力するメソッドであるので,このプログラムを実行するとHello World!という文字列が標準出力に出力されることになる.

クラスのとても基本的な構造

まずは,クラスはどういう構造をもっているかを見ていこう.基本的には以下の構造をしているが,これだとピンとこない人も多いと思うので,例を用いて説明することにする.

修飾子 class クラス名
{
   フィールドやコンストラクタ,メソッドの宣言の列
}

例:カウンター

今,カウンターを実装することを考える.カウンターの機能はシンプルに数をカウントするだけである.カウンターオブジェクトは内部でカウントを持っていて,作成時にカウントの初期値を指定することができる.またカウンターに対してできることは

のみであるようにしたい.

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) {…}の部分はこのクラスの コンストラクタを定義している.コンストラクタはオブジェクトを生成する際に呼ばれ,ここではフィールドcountc0で初期化している.コンストラクタはクラスと同じ名前である必要がある.public修飾子はこのコンストラクタが Counter クラスの外からアクセスできることを表している.

カウンターオブジェクトの作成,より正確な言い方をすればCounterクラスのインスタンス(オブジェクトのことをインスタンスと呼ぶことがある)は new式を用いることで行うことができる.

Counter c = new Counter(0); 

この文の実行後,cnew 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クラスに(インスタンス)メソッドを追加してもっと面白くしてみよう.(インスタンス)メソッド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; } 

struct counter* new_counter() {
    struct counter* p = new malloc( sizeof(struct counter) ); 
    construct_counter(p); 
    return p; 
}

void construct_counter(struct counter* p) {
    p->count = 0; 
}

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; 

修飾子

上で出てきたstaticpublic等は修飾子と呼ばれるものである.中でもprivatepublicアクセス修飾子と呼ばれ,メソッドやフィールドにアクセスする範囲を限定する.アクセス修飾子にはたとえば以下のようなものがある(全部ではない).

型にはCounter等のクラスや,int等の数値型,bool(真偽値型)やstring(文字列型)などがある.数値型にはたとえば,以下のようなものがある(全部ではない).

コンストラクタ宣言の基本的な形

修飾子 クラス名(「パラメータの型 パラメータ」のコンマ区切りの列)
{
    
    ...
    
}

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 () 式がtrueに評価されたときに実行される文
if () 式がtrueに評価されたときに実行される文 else 式がfalseに評価されたときに実行される文

// 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型でなければならない.これはforwhileでも同じ.

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()

インクリメント(++)とデクリメント(-

int a = 1; 
Console.WriteLine( "a = " + a++.ToString() ); // a = 1
Console.WriteLine( "a = " + a++.ToString() ); // a = 2
int 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 // true
1 != 1 // false
1 < 2 // true 
1.0 <= 2.0 // true

論理演算

true && false // false 
true || false // true
!true // false 

Note

&&および||は短絡する.すなわちfalse && eおよびtrue || eeは評価されない.

代入演算

int a; 
Console.WriteLine(a = 1);  // 1
Console.WriteLine(a);      // 1
Console.WriteLine(a += 1); // 2

Note

x[f()] += ex[f()] = x[f()] + eと異なり,f()を一回しか呼ばない

Note

第5回および第6回では,「イベント」を扱うための特殊な+=および-=が登場する.