概要
クラスとはオブジェクトを作るための設計図のようなもので、 オブジェクト指向プログラミングの中心となるものです。
ポイント
-
クラス: オブジェクトを作るための設計図。
-
インスタンス: 設計図を基に作られた個々の実体。
-
例えば、
-
class Point { public int X; public int Y; }
でクラスを作って、 -
Point p = new Point();
でインスタンスを作る。
-
-
構造体との違いは「値型」か「参照型」か、継承できるかどうか。
クラスとインスタンス
「オブジェクト指向とは」で述べたように、 操作の対象となるものをオブジェクトといいます。 オブジェクトを作る場合、まず設計図が必要になります。 内部がどういう構造になっているのか、外部からどのような操作をすることが出来るのかを決めてやるわけです。 このようなオブジェクトの設計図のことをクラス(class)といいます。 それに対し、設計図を元に作られたオブジェクトの実体のことをインスタンス(instance)といいます。
クラス | インスタンス |
---|---|
製品規格 | 個々の製品 |
人間 | 松井君、空知君、畑君、田辺さん・・・ |
実数全体 R | 実数値x, y,・・・ |
初等関数 | sin, cos, exp, log, ・・・ |
クラス定義
C#では以下のようにしてクラスを定義します。
class クラス名
{
クラスの実装
}
クラスの実装にはメンバ変数の定義とメソッド(メンバー関数)の定義などをします。 メンバー変数とはクラスの内部で宣言される変数のことで、 メソッドの定義はクラスの内部で宣言される関数のことだと思ってもらって結構です。 以下に例を示します。
class Sample
{
// メンバー変数の定義 ここから↓
private int x;
private int y;
// メンバー変数の定義 ここまで↑
// メソッドの定義 ここから↓
public int GetX()
{
return x;
}
public int GetY()
{
return y;
}
public void Set(int a, int b)
{
x = a;
y = b;
}
// メソッドの定義 ここまで↑
}
private
やpublic
といったキーワードについては「実装の隠蔽」で解説します。
メンバー変数によってオブジェクトの内部の実装を記述し、メソッドによって外部から行える操作を記述するわけです。 以下、具体的な例を挙げるために複素数を表すクラスを作ってみましょう。 まず、複素数に対する操作には何があるかを列挙してみましょう。
-
実部の取り出し・変更
-
虚部の取り出し・変更
-
絶対値の取り出し・変更
-
偏角の取り出し・変更
-
四則演算
-
共役複素数の計算
次に、複素数を実装する方法を考えて見ましょう。
-
実部と虚部を記憶しておく
-
絶対値と偏角を記憶しておく
いきなりすべてを実装するのは大変ですから、 まず、実部と虚部の取り出し・変更と、絶対値の取り出しを、 実部と虚部を記憶しておく方式で実装してみます。
class Complex
{
public double re; // 実部を記憶しておく(外部からの読み出し・書き換えも可能)
public double im; // 虚部を記憶しておく(外部からの読み出し・書き換えも可能)
// 絶対値を取り出す
public double Abs()
{
return Math.Sqrt(re*re + im*im);// Math.Sqrt は平方根を求める関数
}
}
最初ということで、シンプルになるように実装しましたが、今後、徐々にちゃんとした形のものにしていきます。
クラスの利用
クラスを利用するためには、 インスタンスを作成しなければなりません。 そのためにまず、インスタンスを格納するための変数を定義します。 変数定義の仕方は以下のような構文になります。
クラス名 変数名;
(ちなみに、C# 9.0 からは new
の後ろのクラス名を省略できることがあります。)
次に、new
キーワードでインスタンスを作成し、用意した変数に格納します。
ここで注意すべきことは、C# において、変数というのはただの入れ物であって、
変数を宣言しただけではインスタンスは作成されません。
(空っぽの入れ物だけができる。)
以下のように、new
して始めてインスタンスが生成されます。
変数 = new クラス名();
そして、以下のように変数の後に「 .
」で区切ってメンバー名を書くことでメンバー変数やメンバー関数を利用できます。
変数名.メンバー名
例として先ほど作成した複素数クラスのインスタンスを生成し、利用してみましょう。
Complex z; // インスタンスを格納するための変数を定義
z = new Complex(); // new を使ってインスタンスを生成
z.re = 3; // 実部の値を変更
z.im = 4; // 虚部の値を変更
double abs = z.Abs(); // z の絶対値を取得
Console.Write("abs = {0}\n", abs); // abs = 5 と表示される
また、組込み型や配列と同様に変数の宣言と同時にインスタンスを作成して初期化することも出来ます。
int n = 5;
string s = "abcde";
int[] array = new int[]{1, 2, 3, 4, 5};
Complex z = new Complex();
サンプル
using System;
/// <summary>
/// 複素数クラス
/// </summary>
class Complex
{
public double re; // 実部
public double im; // 虚部
/// <summary>
/// 絶対値を返す
/// </summary>
public double Abs()
{
return Math.Sqrt(re*re + im*im);
}
/// <summary>
/// 文字列化する
/// </summary>
public override string ToString()
{
if(im >0)
return re.ToString() + "+i" + im.ToString();
if(im < 0)
return re.ToString() + "-i" + (-im).ToString();
return re.ToString();
}
}// class Complex
//================================================
class ClassSample
{
static void Main()
{
Complex z = new Complex();
z.re = GetDouble("実部を入力してください : ");
z.im = GetDouble("虚部を入力してください : ");
Console.Write("|{0}| = {1}\n", z, z.Abs());
}
// 「関数」のところで作った実数入力用関数
static double GetDouble(string message)
{
double x;
while(true)
{
try
{
// 入力を促すメッセージを表示して、値を入力してもらう
Console.Write(message);
x = double.Parse(Console.ReadLine());
}
catch(Exception)
{
// 不正な入力が行われた場合の処理
Console.Write(
"error : 正しい値が入力されませんでした\n入力しなおしてください\n");
continue;
}
break;
}
return x;
}
}
null
前節で、クラスを使う際にはまず「new
キーワードでインスタンスを作る」と説明しましたが、
インスタンスを持たない(作るのを後回しにしたり、使い終わったものを手放したりする)場合の話もしておきます。
C# では、「有効なインスタンスを持っていない」という状態をnull(ヌル: 空っぽ、0)と呼び、null
キーワードで表します。
変数 = null;
よくある用途としては、必要になるまでインスタンス生成を遅らせたり(遅延初期化と言ったりします)です。 プロパティやnull 合体演算子を使うことが多く、今は「こういう書き方がある」くらいの説明しかできませんが、以下のような使い方ができます。
using System;
using System.ComponentModel;
using System.Reflection;
// System.Type から、自分のプログラムで使う属性とかを抽出するためのクラス
class TypeInfo
{
private readonly Type _type;
public TypeInfo(Type type) => _type = type;
// 初期状態で null にしておく。
private string _description = null;
// このメソッドの処理はだいぶ重たい。
// なので必要になるぎりぎりまで呼びたくない。
private string GetDescription() => _type.GetCustomAttribute<DescriptionAttribute>().Description;
// 始めてこのプロパティが呼ばれたときに、まだ _description が null のときだけ GetDescription を呼ぶ。
public string Description => _description ??= GetDescription();
}
また、「有効なインスタンスを取れなかった」ということを表すのに使ったりもします。
class Program
{
static void Main()
{
// "abcdefg" が条件を満たすのでこれが返ってくる。
var a = FirstLongString(new[] { "a", "abcd", "abcdefg", "abc" });
// 条件を満たすものがないので null が返ってくる。
var b = FirstLongString(new[] { "a", "abcd", "abcd", "abc" });
}
// 配列中から特定の条件を満たす最初のインスタンスを探す。
// (例として、長さ 5 以上の文字列を探す。)
static string FirstLongString(string[] items)
{
foreach (var x in items)
{
if (x.Length >= 5) return x;
}
// 条件を満たすものがなかったことを表すために null を返す。
return null;
}
}
詳しくは「null の取り扱い」というページもあるのでこちらも参照してみてください。
Ver. 2.0
元々は参照型にしかなかった概念ですが、 C# 2.0 からはnull許容値型という機能を使うことで値型でも null を使えるようになりました。
Ver. 8.0
また、C# 8.0 では「nullが本当に必要かどうか」を明示的に指定できるように、 null許容参照型という機能が入りました。
this アクセス
クラス中では、this
というキーワードが特別な意味を持ちます。
this
は、英単語の意味(これ、この)通り「このインスタンス自身」を表す特別な変数になります。
通常はあってもなくてもいいものなんですが、 例えば、ローカル変数と同名のフィールドがあったときに、フィールドの方を参照するために使えます。
class Point
{
// 小文字 x, y でフィールドを定義
int x;
int y;
// 同じ x で引数を定義
// y の方は名前を変えてみる
public Point(int x, int a)
{
// this. が付いている方はフィールド
// ついていない方は引数
this.x = x;
// y の方は this. を付けなくても、他に候補がないのでフィールドの y
y = a;
// この場合、this. を付けても y と同じ意味
var b = this.y;
}
}
あるいは、メソッドの引数に自分自身を渡したりするときに使います。
class Point
{
// 前略
// Point を引数として受け取るメソッドがあったとして、
public static void Write(Point p)
{
System.Console.WriteLine($"({p.x}, {p.y})");
}
// そのメソッドに「自分自身」を渡す
public void Write() => Write(this);
}
その他、インデクサーや拡張メソッドなど、this
を常に必要とする構文があります。
using System;
class Point
{
public int X, Y;
public void M()
{
var x = this[0]; // インデクサーの呼び出し
var l = this.LengthSquared(); // 拡張メソッドの呼び出し
}
// インデクサー
public int this[int i]
=> i == 0 ? X
: i == 1 ? Y
: throw new IndexOutOfRangeException();
}
static class PointExtensions
{
// 拡張メソッド
public static int LengthSquared(this Point p) => p.X * p.X + p.Y * p.Y;
}
クラスと構造体
ここまでの説明を見て、 「クラス」と「構造体」の類似性に気付いた方もいるかと思います。 実際、メンバー変数やメソッドの定義は構造体でもできます。
クラスと構造体の違いを説明するためには、 継承や多態性などのオブジェクト指向の概念や、 値型と参照型というプログラミングの概念の理解が必要になります。 これらの概念の詳細は、 「継承」、 「多態性」、 「値型と参照型」 などで説明することにして、 ここでは簡単に概要だけを表にまとめます。
クラス | 構造体 | |
---|---|---|
型の分類 | 参照型 | 値型 |
継承 | できる | できない |
多態性 | 使える | 使えない |
迷うようならクラスにしておけばいいと思います。 構造体は、以下の条件がそろっている場合にのみ使います。
-
データのサイズが小さい(目安としては16バイト程度以下)
-
絶対に継承しないと分かっている
-
変数への代入がコピーを生むというのが許容できる
クラスの分割定義
Ver. 2.0
C# 1.1 以前は、クラスの定義は全て1つのファイルに収める必要がありました。 まあ、1つのクラスの中身が、複数のファイルに散在するとソースファイルの可読性が低くなるので、 通常は、むしろ、クラス定義を複数のファイルに分割できない方がいいのですが、 時折、クラス定義を分割したい場面があります。
例えば、Visual Studio などの統合開発環境を利用していると分かると思いますが、 ソースファイルの一部分はプログラマーの手書きではなく、 開発ツールが自動的に生成してくれる部分があります。 そして、この自動生成部分と手書きの部分は、別ファイルに分かれている方が都合が良かったりします。 たとえば、プログラマのミスで開発支援ツールのコード自動生成機能に不具合が生じたり、 ツールのバグでプログラマのコードが破壊されたりといった問題が生じる可能性を極力減らすことにつながります。
そこで、C# 2.0 では、クラス定義時に partial
というキーワードを付けることで、
クラス定義を複数に分割することができるようになりました。
(部分クラス)
例えば、(少々無理やりな例ですが)上述の Complex
クラスを2つに分割してみましょう。
partial class Complex
{
public double re;
public double im;
}
partial class Complex
{
public double Abs()
{
return Math.Sqrt(re * re + im * im);
}
}
この2分割された Complex
クラスは、別々のファイル中にあっても構いません。
ただし、どの分割された部分にも、必ず partial
というキーワードをつける必要があります。
class Complex // partial の付いていないクラスに、
{
public double re;
public double im;
}
partial class Complex // 別途、partial を追加することはできない。エラーになる。
{
public double Abs()
{
return Math.Sqrt(re * re + im * im);
}
}
すなわち、元々 partial
を想定せずに作ったクラスに、
別ファイルに後付けでコードを加えることは出来ません。
メソッドの実装の分離
Ver. 3.0
C# 3.0 で 部分メソッド(partial method)という機能も追加されました。
どういうものかというと、
部分クラス内限定で、
メソッドに partial
を付けることでメソッドの宣言と定義を分けれるというものです。
定義の仕方と、制限事項は以下の通り。
-
partial
修飾子を付けてメソッドを宣言する。 -
必ず部分クラス内になければならない。
-
アクセシビリティの指定はできない(自動的に必ず
private
扱い)。 -
戻り値は
void
以外不可。 -
引数は自由に取れる。
ref
,this
,params
も利用可能。ただし、out
引数は不可。 -
静的メソッド(
static
)でもインスタンス メソッド(非static
)でも OK。
例えば、まずクラスの部分定義で以下のようなコードを書いたとします。
partial class Program
{
static void Main(string[] args)
{
OnBeginProgram();
Console.Write("program body\n");
OnEndProgram();
}
static partial void OnBeginProgram();
static partial void OnEndProgram();
}
この状態でプログラムをコンパイル → 実行すると、「program body」の文字だけが表示されます。
ここで特筆すべきは、
OnBeginProgram
、OnEndProgram
のメソッド呼び出しはコンパイル後の実行ファイルからはきれいさっぱり消えます。
(メタデータすら残さず、完全に消える。
Conditional
属性でも似たようなことができますが、こちらは少なくともメタデータは残ります。)
ここで、以下のような部分定義を追加して、 部分メソッドに実装を与えます。
partial class Program
{
static partial void OnBeginProgram()
{
Console.Write("check pre-condition\n");
}
static partial void OnEndProgram()
{
Console.Write("check post-condition\n");
}
}
すると、OnBeginProgram
、OnEndProgram
が呼ばれるようになります。
実行結果は以下の通り。
check pre-condition program body check post-condition
利用場面としては、 宣言側は人手で書いて、 定義側はツールで自動生成というようなものを想定しているようです。
実は、これと似たようなことは「仮想メソッド」や「イベント」とクラスの継承を使っても可能です。 ただ、多少煩雑な処理が必要ですし、 実行パフォーマンスは悪くなります。 その点、部分メソッドなら、 必要な場合は普通のメソッド呼び出しになるだけですし、 不要な場合は完全削除されるわけで、 パフォーマンスに関しては非常に優れています。
ただ、結構制約も多いですし、利用場面は限られています。
部分メソッドの引数で副作用を起こす場合
「不要な場合は完全削除」という仕様には、1つ奇妙な動作を招く点があります。 問題が起こり得るのは、部分メソッドの呼び出しの際に引数で副作用を起こす場合です。
例えば、以下のコードの実行結果はどうなるでしょう。
partial class Program
{
static void Main(string[] args)
{
int x = 1;
A(x = 2);
Console.Write("{0}\n", x);
}
static partial void A(int x);
}
これ、A
の実装がある場合には 2 に、ない場合には 1 になります。
部分クラスなので、当然、実装は別ファイルにあってもかまいません。
自分以外の誰かがどこか別のところで実装を書くかもしれませんし、
誰も書かないかもしれません。
要するに、自分の知らないところで実行結果が変えられてしまう可能性がある。
まあ、メソッド呼び出しの ()
内で代入なんてするなよって話ではあるんですが。
こういう副作用があることも覚えておいてください。
(あるいは、この副作用を積極的に利用したトリッキーなコードも書けるでしょうが・・・。
個人的には非推奨。)
あくまで、人手での記述とツールでの自動生成の混在開発で使うものだと思います。 元々、部分クラス自体がそういう用途のための機能ですし。
部分メソッドの拡張
Ver. 9
前節で説明した部分メソッドは「開発ツールが生成したコードが先にあって、そこに手書き処理を足したいときに使う物」です。
一方、C# 9.0 世代ではソースコード生成機能が入ったことでこの逆があり得ます。 すなわち、「ソースコード生成してもらう前提で、手書きでは不完全な C# コードを書きたい」という場面が出てきました。
C# 9.0 では、そのための「不完全なメソッド」を書く方法として partial
キーワードを再利用することにしました。
旧来の部分メソッドとの差はアクセシビリティ修飾子(public
とか private
とか)を持つかどうかです。
// (1) ツールが事前に生成する想定のコード
partial class PartialClass
{
public void PreGeneratedMethod()
{
OnPreGeneratedMethod();
// ツール生成のコード
}
// ツール生成のコードの前に何か手書き処理を足したければこのメソッドの中身を書く
partial void OnPreGeneratedMethod();
}
// (2) 手書き前提のコード
partial class PartialClass
{
#if DEBUG
// ツール生成コード前に、Debug 時のみログを仕込む。
// これを書かなければ OnPreGeneratedMethod は呼ばれる痕跡すら残らない。
partial void OnPreGeneratedMethod()
{
System.Console.WriteLine(
"PreGeneratedMethod が呼ばれた直後"
+ WantSourceGenerated());
}
#endif
// C# コードが先にあって、これを元にソースコード生成してほしいメソッド。
private partial string WantSourceGenerated();
}
// (3) C# からのソースコード生成が前提のコード
partial class PartialClass
{
private partial string WantSourceGenerated() => "手書きはしづらしくて、ソースコード生成なら楽な文字列";
}
コード解析・コード生成の利用で紹介している StringLiteralGenerator はこの新しい部分メソッドを使っています。
ちなみに、コード生成と手書きの期待される順序が逆になっただけで、結構できること・できないことが変わります。 以下の表に違いをまとめます。
旧(アクセス修飾子なし) | 新(アクセス修飾子あり) |
---|---|
アクセシビリティの指定は不可 | アクセシビリティの指定が必須(private 含む) |
戻り値は void のみ | 任意の戻り値を使える |
ref 引数、out 引数を持てない | ref 引数、out 引数を持てる |
本体を持っていなくてもいい。なければ完全に消える。 | どこか1か所で本体を持たないとダメ。なければコンパイル エラー。 |
アクセシビリティ修飾子の有無だけでここまで差があることには少し抵抗があって、 文法をどうするかは C# チームも結構迷ったようです。 ただ、最終的には、下手にキーワードを追加したり全然違う文法を導入するよりはマシという判断が下りました。
partial キーワードの位置
部分クラス・部分メソッドの仕様は C# 2.0 から追加されたものです。
partial
というキーワードも 2.0 からの後付けなわけで、完全に予約語(変数などの名前に使えない単語)にしてしまうと、1.0 時代に書かれたコードを壊す可能性がありました。
そこで、partial
は文脈キーワードになっています。
partial
という単語がキーワード扱いされるのは、class
、struct
、interface
、void
の直前だけです。
その結果、以下のように、語順に制約があります。
// OK
public static partial class Ok1 { }
static public partial class Ok2 { }
// コンパイル エラー
public partial static class Ng1 { }
partial public static class Ng2 { }
static partial public class Ng3 { }
partial class X
{
// OK
static partial void Ok();
// コンパイル エラー
partial static void Ng();
}
匿名型
Ver. 3.0
C# 3.0 では匿名型(anonymous type)を作成できるようになりました。 匿名型の作り方は以下の通りです。
var x = new { FamilyName = "糸色", FirstName="望"};
このようなコードから、自動的に、以下のような型が生成されます。
// ↓この __Anonymous という名前はプログラマが参照できるわけではない。
class __Anonymous1
{
private string f1;
private string f2;
public __Anonymous1(string f1, string f2)
{
this.f1 = f1;
this.f2 = f2;
}
public string FamilyName
{
get { return this.f1}
};
public string FirstName
{
get { return this.f2}
};
// あと、Equals, GetHashCode, ToString も実装
}
この機能的は「LINQ」とともに利用することで真価を発揮します。 単体で使う場面はそれほど多くないと思いますが、 例えば、以下のような書き方ができます。
var rectangle = new { Width = 2, Height = 3 };
Console.Write("幅 : {0}\n高さ: {1}\n面積: {2}\n",
rectangle.Width,
rectangle.Height,
rectangle.Width * rectangle.Height);
演習問題
問題1
「データの構造化」のデータの構造化の問題 1で作成した Triangle
構造体をクラスで作り直せ。
(Point
構造体は構造体のままで OK。)
注1:現時点では、 単に struct が class に変わるだけで、特にメリットはありませんが、 今後、 「継承」」や「多態性」を通して、 クラスのメリットを徐々に加えていく予定です。
注2: クラスにした場合、メンバー変数をきちんと初期化してやらないと正しく動作しません。 (構造体でもメンバー変数の初期化はきちんとする方がいいんですが。) 初期化に関しては、次節の「コンストラクターとデストラクター」で説明します。