C Sharp/クラスとメソッド

出典: フリー教科書『ウィキブックス(Wikibooks)』

メソッドとクラス[編集]

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

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

メソッド[編集]

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

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

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

using System;

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

public class Sample {
  public static void Main(string[] args) {
    TestClass.myFunc();
  }
}
実行結果
55
同じクラスの中にある関数を呼び出す場合なら、呼出時のクラス指定は省略できます。
using System;

public class Sample {
  public static void Main(string[] args) {
    myFunc();
  }

  static public void myFunc() {
    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 myFunc() {
    // 書き換えテスト
    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.myFunc();
    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 myFunc() {

    // 書き換えテスト
    testval.a = 403;
    testval.b = 415;

    Console.WriteLine("ユーザー定義メソッドにて書き換え実行");
  }
}

// 結果確認用メソッド
class testcheck {
  static public void myFunc2() {

    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.myFunc();
    Console.WriteLine("今、Mainに戻ったあとです");

    Console.WriteLine("結果チェック用メソッドに移動します");
    testcheck.myFunc2();

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

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

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

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

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


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

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

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

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

void myFunc (){

クラス[編集]

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 TestStruct {
  public string name;
  public int price;
  
  public TestStruct(string a, int b){
   name = a;
   price = b;
  }
}

public class Sample {
  public static void Main(string[] args) {
    TestStruct milk = new TestStruct("牛乳", 150);
    Console.WriteLine(milk.name);
    Console.WriteLine(milk.price);
    Console.WriteLine();
    
    TestStruct cmilk = new TestStruct("コーヒー牛乳", 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

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

参照型と値型[編集]

C#には、参照型と値型の2種類の型があります。参照型の変数にはデーター(オブジェクト)への参照が格納され、値型の変数にはデーターが直接格納されます。 参照型では、2つの変数が同じオブジェクトを参照できるため、一方の変数を操作すると、他方の変数が参照するオブジェクトに影響を与えることができます。 一方、値型では、それぞれの変数がデーターのコピーを持っており、一方の変数に対する操作が他方に影響を与えることはありません(in, ref, および out パラメーター変数は、除きます)[1]

参照型の宣言には、以下のキーワードを使用します。

  • class
  • interface
  • delegate
  • record

また,C#は以下の組み込み参照型を提供します。

  • dynamic
  • object
  • string
クラスのインスタンスの代入
using System;

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

public class Sample {
  public static void Main(string[] args) {
    var milk = new NPClass();
    milk.name = "牛乳";
    milk.price = 150;

    var cmilk = milk; // クラスのインスタンスの代入は、別名の生成
    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 NPStruct {
  public string name;
  public int price;
}

public class Sample {
  public static void Main(string[] args) {
    var milk = new NPStruct();
    milk.name = "牛乳";
    milk.price = 150;

    var cmilk = milk; // 構造体の代入は、全てのメンバーの代入。
    cmilk.name = "コーヒー牛乳";
    cmilk.price = 180;

    Console.WriteLine(milk.name);
    Console.WriteLine(milk.price);
    
    Console.WriteLine(cmilk.name);
    Console.WriteLine(cmilk.price);
  }
}
実行結果
牛乳
150
コーヒー牛乳
180
new による新しいインスタンスの生成
using System;

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

public class Sample {
  public static void Main(string[] args) {
    var milk = new NPClass();
    milk.name = "牛乳";
    milk.price = 150;

    var cmilk = new NPClass(); // new で新しいインスタンスを生成
    cmilk.name = "コーヒー牛乳";
    cmilk.price = 180;

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

その他の違い[編集]

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

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

列挙型( enum type )[編集]

列挙型( enum type )は、C# のデーター型の1つで、名前付き定数の集合で定義される値型です。列挙型を定義するには、enum キーワードを使用し、enum メンバーの名前を指定します[2]

Pure Enum[編集]

シンプルな例として、曜日型を定義してみましょう。

Pure Enum の例
using System;

namespace EnumExtension {
  public static class Extensions {

    public static bool weekEnd(this DayOfWeek day) {
      return day == DayOfWeek.Sunday || day == DayOfWeek.Saturday;
    }
  }

  public enum DayOfWeek {
    Sunday,
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday,
  }

  public class EnumTypeEx {

    public static void Main() {
      var aDay = DayOfWeek.Wednesday;
      Console.WriteLine($"{aDay}({typeof(DayOfWeek)}):{(int)aDay}  -- {aDay.weekEnd()}");
      Console.WriteLine();

      for (DayOfWeek day = 0; DayOfWeek.IsDefined(day); day++) {
        Console.WriteLine($"{day}: {day.weekEnd()}");
      }
      Console.WriteLine();
      foreach(DayOfWeek day in Enum.GetValues(typeof (DayOfWeek))) {
        Console.WriteLine($"{day}: {day.weekEnd()}");
      }
    }
  }
}
実行結果
Wednesday(EnumExtension.DayOfWeek):3  -- False

Sunday: True
Monday: False
Tuesday: False
Wednesday: False
Thursday: False
Friday: False
Saturday: True

Sunday: True
Monday: False
Tuesday: False
Wednesday: False
Thursday: False
Friday: False
Saturday: True
列挙型は、多くの言語で反復構文に対応していて、C#でも Enum.GetValues() や Enum.GetNames() を使って foreach の対象にできます。
列挙型の定義中にメソッドは定義できませんが、拡張メソッドの主語に列挙型をすることはできるので、使用感は列挙型のメソッドそのものです。
Pure Enum は、C#の用語ではありませんが、「値を宣言時に指定しない列挙型」の意味です。

switch による網羅性の担保[編集]

switch 式やswitch文と、enumを組合わせると、enumの定義するメンバーを網羅いていることの検証「網羅性の担保」が可能になります。

switch による網羅性の担保 の例
using System;

namespace EnumExtension {
  public static class Extensions {
    public static string japanese(this Seasons season) {
      return season switch {
          Seasons.Spring => "春",
          Seasons.Summer => "夏",
          Seasons.Autumn => "秋",
          Seasons.Winter => "冬",
//          _ => "???",  XXX 網羅性の担保を阻害します
      };
    }
  }

  public enum Seasons {
    Spring,
    Summer,
    Autumn,
    Winter,
  }

  public class EnumTypeEx {
    public static void Main() {
      var season = Seasons.Summer;
      Console.WriteLine($"{season}({typeof(Seasons)}):{(int)season}  -- {season.japanese()}");
      Console.WriteLine();
      foreach(Seasons s in Enum.GetValues(typeof (Seasons))) {
        Console.WriteLine($"{s}: {s.japanese()}");
      }
    }
  }
}
実行結果
Summer(EnumExtension.Seasons):1  -- 夏

Spring: 春
Summer: 夏
Autumn: 秋

Winter: 冬

Backed Enum[編集]

Backed Enum の例
using System;

namespace EnumExtension {
  public static class Extensions {

    public static bool weekEnd(this DayOfWeek day) {
      return day == DayOfWeek.Sunday || day == DayOfWeek.Saturday;
    }
  }

  public enum DayOfWeek : byte {
    Sunday = 1,
    Monday,
    Tuesday,
    Wednesday,
    Thursday,
    Friday,
    Saturday,
  }

  public class EnumTypeEx {

    public static void Main() {
      var aDay = DayOfWeek.Wednesday;
      Console.WriteLine($"{aDay}({typeof(DayOfWeek)}):{(int)aDay}  -- {aDay.weekEnd()}");
      Console.WriteLine();

      for (DayOfWeek day = DayOfWeek.Sunday; DayOfWeek.IsDefined(day); day++) {
        Console.WriteLine($"{day}: {day.weekEnd()}");
      }
      Console.WriteLine();
      foreach(DayOfWeek day in Enum.GetValues(typeof (DayOfWeek))) {
        Console.WriteLine($"{day}: {day.weekEnd()}");
      }
    }
  }
}
実行結果
Wednesday(EnumExtension.DayOfWeek):4  -- False

Sunday: True
Monday: False
Tuesday: False
Wednesday: False
Thursday: False
Friday: False
Saturday: True

Sunday: True
Monday: False
Tuesday: False
Wednesday: False
Thursday: False
Friday: False
Saturday: True
  public enum DayOfWeek : byte {
    Sunday = 1,
values の型を、byte に指定しています。
最初の列挙メンバーの値は無指定では 0 ですが、1 を明示しています。
Backed Enum は、C#の用語ではありませんが、「値を宣言時に指定した列挙型」の意味です。

Flag enums[編集]

Flag enums は Backed enums の延長ですが、少し毛色が違い[Flags]属性を使います。

Flag enums の例
using System;

[Flags]
enum Bits {
    NONE = 0,
    A = 1 << 0, // 0b001
    B = 1 << 1, // 0b010
    C = 1 << 2, // 0b100
    ALL = A | B | C,
}

class FlagsEnumEx {
  static void Main(string[] args) {
    Console.WriteLine($"Bits.A | Bits.B --> {Bits.A | Bits.B}");
    Console.WriteLine($"Bits.A | Bits.B | Bits.C --> {Bits.A | Bits.B | Bits.C}");
    Console.WriteLine($"Bits.ALL ^ Bits.A --> {Bits.ALL ^ Bits.A}");
    Console.WriteLine($"Bits.A ^ Bits.A --> {Bits.A ^ Bits.A}");
  }
}
実行結果
Bits.A | Bits.B --> A, B
Bits.A | Bits.B | Bits.C --> ALL
Bits.ALL ^ Bits.A --> B, C
Bits.A ^ Bits.A --> NONE
集合演算です。
全ビットクリアに NONE, マスクに ALL と名前をつけるとそれっぽい表現になります。
ビットのシフト量が単調増加であることを表現するに手段がないのが残念です(Goならばiotaを使うところ)

脚註[編集]

  1. ^ [https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/reference-types Reference types (C# reference) ]
  2. ^ 18. Enums