C Sharp/クラスとメソッド

メソッドとクラス編集

オブジェクト指向の用語については「オブジェクト指向プログラミング」を参照のこと。

C# の「メソッド」と「クラス」の概念はお互いに関連しあっており、やや難しい。メソッドを読んでて分からなければ、とりあえずクラスも読んでみるのが良い。

メソッド編集

C言語でいう「関数」に似た機能として、c#では「メソッド」がある。

関数との違いとして、メソッドは必ずどこかのクラスまたは構造体あるいは同等のものに所属していなければならない。開発元のマイクロソフトの公式ドキュメントでも、そう明言されていますMicrosoft Docs 日本語訳『C# のクラス、構造体、レコードの概要』2022/06/10 (2022年6月18日に確認)。

つまり、必ずどこかのクラスまたは構造体のブロック中にてメソッドは宣言されることになる。どこのクラスにも構造体にも属してない むきだしのメソッドを宣言しても、それがコード中に存在しているだけでコンパイルできずにエラーになる。このような仕組みのため、説明の便宜上、広い意味では、「構造体」をクラスの一種の、特別なクラスとして解釈する場合もある。

うごくコード例
using System;

// ユーザー定義側のメソッドには public はなくてもいい
class testclass{
  static public void testfunc (){
    var a = 55;
    Console.WriteLine(a);
  }
}

public class sample {
  public static void Main(string[] args) {

    testclass.testfunc();
    
  }
}
実行結果
55


同じクラスの中にある関数を呼び出す場合なら、呼出時のクラス指定を省略できる。

using System;

public class sample {
  public static void Main(string[] args) {

    testfunc();
    
  }
  
  static public void testfunc (){
    var a = 14;
    Console.WriteLine(a);
  }
  
}
実行結果
14


なお、変数宣言で用いる var は、メソッド内でのローカル変数の宣言です。

ただし、Mainメソッドのブロック内のvarだけ、Mainメソッドよりも上位のメソッドは無いので結果的にMainメソッド内のvar宣言された変数があたかもグローバル変数のような働きをしますが、あくまで結果論です。


さて、C#のカプセル化などの理念により、すべての変数は、なんらかのクラスに所属していなければなりません。クラスにも構造体にもどこにも所属してない、むきだしの変数は、エラーです。

定数の宣言も同様です。どこにも属してない、むきだしの定数は、エラーになります。いっぽう、クラスや構造体でなくても、列挙体(enum型)でもrecord型でも何でも良いですが、なにかに所属していれば、定数の宣言はエラーになりません。このようなC#の仕組みのため、enum型やrecord型すらも、広い意味でのクラスの一種として解釈する場合もあります。読者は文脈から広い意味か狭い意味かを判断してください。


さて、変数宣言で static 修飾子をつけると、どこからでもアクセスできるし、どこでアクセスして読み書きしても結果は共有されます。このため、static修飾をつけることで、標準C や C++ でいうグローバル変数のように使えます。


下記コードでは、Mainメソッド以外のユーザ定義メソッドから書き換えをできるかのテストをしています。


うごくコード例
using System;

// 変数管理用
public class testval{
    public static int a = 3;   
    public static int b =15; // 使わないけど比較用
    
}

// ユーザ定義メソッド
class testclass{
  static public void testfunc (){
    
    // 書き換えテスト
    testval.a = 403;
    testval.b = 415;
    
    Console.WriteLine("ユーザー定義メソッドにて書き換え実行");
  }
}

// Mainメソッド
public class sample {
  public static void Main(string[] args) {

    Console.WriteLine("aは"); 
    Console.WriteLine(testval.a); 
    //test inst = new test();
    
    Console.WriteLine("Main側での書き換えテスト");
    testval.a = 203;
    //testval.b = 215;
    
    Console.WriteLine(testval.a); 

    Console.WriteLine("これからユーザー定義メソッドでさらに書き換えを試みる");
    testclass.testfunc();
    Console.WriteLine("今、Mainに戻ったあとです");

    Console.WriteLine("aは"); 
    Console.WriteLine(testval.a); 
  }
}
実行結果
aは
3
Main関数側での書き換えテスト
203
これからユーザー定義関数でさらに書き換えを試みる
ユーザー定義関数にて書き換え実行
今、Main関数に戻ったあとです
aは
403

このように、staticで宣言された変数は、書き換えしたメソッドとは他のメソッドに移っても、書き換えの結果が保存されています。

「でもMainメソッドは、ユーザ定義メソッドの呼出元じゃないか? 呼出元以外ではどうなるんだ?」と疑問に思うなら、どうぞ試してください。結果は、Main以外の場所で結果確認をしても、上記コードと同様に書き換え結果は保存されます。


うごくコード例
using System;

// 変数管理用
public class testval{
    public static int a = 3;   
    public static int b =15; // 使わないけど比較用
    
}

// ユーザ定義メソッド
class testclass{
  static public void testfunc (){
    
    // 書き換えテスト
    testval.a = 403;
    testval.b = 415;
    
    Console.WriteLine("ユーザー定義メソッドにて書き換え実行");
  }
}

// 結果確認用メソッド
class testcheck{
  static public void testfunc2 (){
        
    Console.WriteLine("aは"); 
    Console.WriteLine(testval.a); 
  }
}

// Mainメソッド
public class sample {
  public static void Main(string[] args) {

    Console.WriteLine("aは"); 
    Console.WriteLine(testval.a); 
    //test inst = new test();
    
    Console.WriteLine("Main側での書き換えテスト");
    testval.a = 203;
    //testval.b = 215;
    
    Console.WriteLine(testval.a); 

    Console.WriteLine("これからユーザー定義メソッドでさらに書き換えを試みる");
    testclass.testfunc();
    Console.WriteLine("今、Mainに戻ったあとです");

    Console.WriteLine("結果チェック用メソッドに移動します");
    testcheck.testfunc2();
    
  }
}
実行結果
aは
3
Main側での書き換えテスト
203
これからユーザー定義メソッドでさらに書き換えを試みる
ユーザー定義メソッドにて書き換え実行
今、Mainに戻ったあとです
結果チェック用メソッドに移動します
aは
403

このように、とにかくstatic宣言された変数は、標準CやC++でいうグローバル変数的に振る舞います。

static 変数にアクセスする場合、インスタンスの作成は不要です。


また、相手先のメソッドの位置さえ分かるように呼び出せば、そのメソッドがpublicならアクセスできます。呼び出し元がどこのクラスにいるかは、呼び出し方法の指定には基本、関係ありません。

後述のクラスでも同様、相手先の変数がどのクラスにいるかさえ分かるように指定すれば、事前に適切な処理がされていれば、その変数にアクセスできます。


上記の説明では不要だってので説明省略しましたが、C#のメソッドは戻り値(返却値、返り値)をひとつ持てます。C++などと同様です。

戻り値の型 メソッド名 (引数1の型 引数名1, 引数2の型 引数名2 ) {

のような書式です。void は、「戻り値は無し」を意味します。

引数が特に無い場合、下記のように、引数名とその型を省略できます。

void testfunc (){

クラス編集

C++でいうクラスは、データの集合をあらわすものの事である標準C言語とC++でいう「構造体」も、若干の違いはあるがクラスと似たようなものである。

だがC#は理念として、あらゆるものをクラスまたは構造体で管理するという方針があるので、C++的なクラスの性質だけでなく、C#特有の独特の性質がある。

基本編集

クラスの基本的な使いかたは下記のとおり。

コード例

using System;

class testclass {
  public string name;
  public int price;
}

public class sample {
  public static void Main(string[] args) {

    testclass a = new testclass() ;

    a.name = "牛乳";
    a.price = 140;
    Console.WriteLine(a.name);
    Console.WriteLine(a.price);
    
  }
}
実行結果
牛乳
140

上記コードでいう string name;int price; などをメンバという。C言語の構造体やC++の構造体/クラスでも同様の働きをするものを「メンバ」と言う。C#の用語はC++を踏襲したものである。

メンバは public にアクセス修飾子を指定しないかぎり、基本的には直接はアクセスできない。


クラス側の変数宣言において、static 宣言されていない変数は、インスタンスを作成しないかぎり、使うことは出来ません。

インスタンスの生成には、new キーワードを使います。上記コードでいう、

testclass a = new testclass() ;

がインスタンスの生成です。書式は、

クラス名 変数名 = new クラス名() ;

です。

左辺の「変数名」をクラス変数とかインスタンス変数とか呼びたくなりますが、しかし既に「クラス変数」「インスタンス変数」はプログラミング界隈では別の意味で使われている用語ですので、ここでは単に「変数」「変数名」と呼ぶことにしましょう。

また、別々のインスタンスに属する変数は、それぞれ別の変数です。単に、「クラス」とは型の情報や初期値などを提供するだけです。つまりクラスは、変数の集まりをつくるときのメモ帳です。

個々の変数の実体は、原則的に、(クラス側ではなく)インスタンス側でされています。

試しに、複数のインスタンスを作って確認してみましょう。

コード例

using System;

class testclass {
  public string name;
  public int price;
}

public class sample {
  public static void Main(string[] args) {

    testclass a = new testclass() ;

    a.name = "牛乳";
    a.price = 140;
    Console.WriteLine(a.name);
    Console.WriteLine(a.price);
    Console.WriteLine();    
    
    // 別インスタンスを作成
    testclass b = new testclass() ;

    b.name = "みかんジュース";
    b.price = 120;
    Console.WriteLine(b.name);
    Console.WriteLine(b.price);
    Console.WriteLine();      
    
    // 牛乳の情報が残ってるかの確認
    Console.WriteLine(a.name);
    Console.WriteLine(a.price);
    Console.WriteLine();  
            
  }
}
実行結果
牛乳
140

みかんジュース
120

牛乳
140

このように、最低でも宣言したインスタンスのぶんだけ、変数は新規に確保されます。また、別々のインスタンスにもとづく変数は、それぞれ別個の変数です。なので、上記コードでは「みかんジュース」を宣言しようが「牛乳」はそのまま問題なく残りつづけます。

コンストラクタ編集

下記コードのように、構造体またはクラス中に、構造体名あるいはクラス名と同じメソッド名をもつメソッドを書くと、その構造体/クラスを呼び出したときの処理を指定できる。

コード例

using System;

struct teststr {
  
  public string name;
  public int price;
  
  public teststr(string a, int b){
   name = a;
   price = b;
  }
}

public class sample {
  public static void Main(string[] args) {

    teststr milk = new teststr("牛乳", 150);
    Console.WriteLine(milk.name);
    Console.WriteLine(milk.price);
    Console.WriteLine();
    
    teststr cmilk = new teststr("コーヒー牛乳", 180);
    Console.WriteLine(cmilk.name);
    Console.WriteLine(cmilk.price);
    Console.WriteLine();
    
    Console.WriteLine(milk.name);
    Console.WriteLine(milk.price);
    Console.WriteLine();
    
  }
}
実行結果
牛乳
150

コーヒー牛乳
180

牛乳
150

インスタンスの配列化編集

インスタンスを配列にする場合、下記のように作成したい配列の要素数だけ new キーワードを宣言しなければならない。

コード例

using System;

class testclass {
  public string name;
  public int price;
}

public class sample {
  public static void Main(string[] args) {

    //testclass[] a = { new[2] testclass() }; // これはエラー 
    testclass[] a = { new testclass(), new testclass() }; // かっこ悪いがこれが通る

    //testclass[] a = new testclass[2] ; //ビルドは通るが実行時にエラー

    a[0].name = "牛乳";
    a[0].price = 140;
    Console.WriteLine(a[0].name);
    Console.WriteLine(a[0].price);
    Console.WriteLine();    
    
    a[1].name = "みかんジュース";
    a[1].price = 120;
    Console.WriteLine(a[1].name);
    Console.WriteLine(a[1].price);
    Console.WriteLine();      
    
  }
}
実行結果
牛乳
140

みかんジュース
120

Linuxの場合、.net core でも .net framework(mono) でも、上記のように new を配列の要素数ぶんだけ実行しなければならない。

for 文でnew の記述を1行だけにする方法

C#およびJavaでは、どうあがいても要素数のぶんだけ new することでメモリを確保する仕組みである。

なので、どうしても人力でnewを複数回も入力したくない場合は(たとえば配列の要素数が100個とかある場合には人力は非現実的)、 for 文を使うしかない。よって、下記のようなコードになる。(なお、Javaもほぼ同様のテクニックでインスタンスを配列化できる。)

この方法を書いている書籍は少ないだろうが(ないかもしれない)、しかしこれを知らないと、実用性が無い。

コード例

using System;

class testclass {
  public string name;
  public int price;
}

public class sample {
  public static void Main(string[] args) {

    testclass[] a =  new testclass[2];
    
    for(int i = 0; i < a.Length; i++){
        a[i] = new testclass();
    }
    

    a[0].name = "牛乳";
    a[0].price = 140;
    Console.WriteLine(a[0].name);
    Console.WriteLine(a[0].price);
    Console.WriteLine();    
    
    a[1].name = "みかんジュース";
    a[1].price = 120;
    Console.WriteLine(a[1].name);
    Console.WriteLine(a[1].price);
    Console.WriteLine();      
    
  }
}
実行結果
牛乳
140

みかんジュース
120

.Length プロパティの冒頭「L」は大文字である。間違えて小文字にするとエラーになる。 なおJavaでは同様のプロパティが小文字。

オブジェクト指向の理論において、クラスには「継承」だの様々な概念があるが、しかし上記のインスタンスの配列化の例から分かるように、単に同じクラスから作成するインスタンスを量産するだけなら、「継承」は必要ない。

本来なら、インスタンスの配列化の話題もオブジェクト指向の理論の教育などで紹介すべきであろう。


new宣言時に代入して行数削減

下記のように、new宣言時にまとめて初期値を代入することも出来る。これによって行数は減らせる。しかしどちらにせよ、配列の要素数のぶんだけ new が必要である。

コード例

using System;

class testclass {
  public string name;
  public int price;
}

public class sample {
  public static void Main(string[] args) {

    testclass[] a = { 
    	new testclass{ name = "牛乳", price = 140 },
    	new testclass{ name = "みかんジュース", price = 120 },
    };

    Console.WriteLine(a[0].name);
    Console.WriteLine(a[0].price);
    Console.WriteLine();    
    
    Console.WriteLine(a[1].name);
    Console.WriteLine(a[1].price);
    Console.WriteLine();      
    
  }
}
実行結果
牛乳
140

みかんジュース
120

クラスと構造体の違い編集

値型と参照型編集

クラスは「参照型」、構造体は「値型」です。とはいっても、初学者に「参照型」と言っても何のことだが分からないだろうから、下記の例で説明します。

特に下記コードの teststr cmilk = milk; と実行結果に注目してください。

まず、クラスの場合、

コード例

using System;

class testcl {
  
  public string name;
  public int price;
}

public class sample {
  public static void Main(string[] args) {

    testcl milk = new testcl();
    milk.name = "牛乳";
    milk.price = 150;

    testcl cmilk = milk; // "new testcl();" ではない
    cmilk.name = "コーヒー牛乳";
    cmilk.price = 180;

    Console.WriteLine(milk.name);
    Console.WriteLine(milk.price);
    
    Console.WriteLine(cmilk.name);
    Console.WriteLine(cmilk.price);
    
  }
}
実行結果
コーヒー牛乳
180
コーヒー牛乳
180


いっぽう、構造体の場合、

コード例

using System;

struct teststr {
  
  public string name;
  public int price;
}

public class sample {
  public static void Main(string[] args) {

    teststr milk = new teststr();
    milk.name = "牛乳";
    milk.price = 150;

    teststr cmilk = milk; // "new teststr();" ではない
    cmilk.name = "コーヒー牛乳";
    cmilk.price = 180;

    Console.WriteLine(milk.name);
    Console.WriteLine(milk.price);
    
    Console.WriteLine(cmilk.name);
    Console.WriteLine(cmilk.price);
    
  }
}
実行結果
牛乳
150
コーヒー牛乳
180

このように、構造体の場合は、「牛乳」の情報が残っています。

こうなる理由は、クラス自体では、例外として明示的に new でインスタンスを作成しないかぎり、変数の代入などでコピーを作成した場合には値を共有するからです。裏を返した言い方をすれば、new命令の際には、新しい参照先が用意される事になります。


だからクラスで宣言していても、変数のコピーの際に new によって参照先を新規作成しておけば、下記コードのように「牛乳」の情報も残ります。

コード例

using System;

class testcl {
  
  public string name;
  public int price;
}

public class sample {
  public static void Main(string[] args) {

    testcl milk = new testcl();
    milk.name = "牛乳";
    milk.price = 150;

    testcl cmilk = new testcl(); // new で宣言している
    cmilk.name = "コーヒー牛乳";
    cmilk.price = 180;

    Console.WriteLine(milk.name);
    Console.WriteLine(milk.price);
    
    Console.WriteLine(cmilk.name);
    Console.WriteLine(cmilk.price);
    
  }
}
実行結果
牛乳
150
コーヒー牛乳
180

その他の違い編集

構造体は「継承」が出来ません。

インターフェースは構造体でもクラスでも可能です。