クラス
最終更新:
atachi
クラス
class MyClass {
}
コンストラクタ・デストラクタ
コンストラクタとは、クラスのインスタンスが作成される際に呼び出される特殊なメソッドで、インスタンスを初期化するための処理を実装する。
デストラクタとは、インスタンスがメモリから破棄される際に呼び出される特殊なメソッドで、インスタンスの後処理をするための実装する。
ただし、インスタンスがメモリから破棄されるタイミングはプログラマは制御できないので、デストラクタが呼び出されるタイミングは不定。
コンストラクタはクラス名と同じ名前のメソッドで、戻り値の指定をせずに記述する。
コンストラクタはオーバーロードで定義できる。
デストラクタはクラス名に「~」をつけたメソッドで、戻り値・引数の両方とも指定出来ない。
class MyClass {
public MyClass() {
// コンストラクタ
}
public MyClass(int num) {
// コンストラクタ
}
~MyClass() {
// デストラクタ
}
}
静的コンストラクタ
静的コンストラクタは、プログラムの開始時に1度だけ実行されるコンストラクタで、プログラム内でクラスがインスタンス化されるかされないかに関わらず必ず1度だけ実行されます。
静的変数の初期化はコンストラクタ初期化子で行うか静的コンストラクタ内で行う。
class MyClass {
static int numInstance;
static MyClass() {
// 静的なメンバ変数のみ呼び出し可能。
MyClass.numInstance = 0;
}
}
メンバ変数
静的メンバ変数
実装したクラスのすべてのインスタンスで共有される変数である。
メンバ変数の定義時に「static」をつける。
次のソースはMyClassのメモリ上のインスタンス数を数えることができる。(デストラクタで変数をデクリメントしているが、デストラクタが呼び出されるタイミングは不定なので、この値が有効な変数数を示しているわけではない)
class MyClass {
public static int instanceNum = 0;
public MyClass() {
MyClass.instanceNum++;
}
~MyClass() {
MyClass.instanceNum--;
}
}
// メインの処理
int c0 = MyClass.instanceNum; // c0 = 0;
MyClass inst1 = new MyClass();
int c1 = MyClass.instanceNum; // c1 = 1;
MyClass inst2 = new MyClass();
int c2 = MyClass.instanceNum; // c2 = 2;
inst1 = null;
// ガベージコレクタ(GC)が呼び出されそうな処理
// GCが呼び出された場合は、inst1のデストラクタが呼び出されている。
int c3 = MyClass.instanceNum; // c3 = 1;
readonlyメンバ
readonlyメンバ変数は初期化時・コンストラクタ時にのみ値を設定することができ、以降は読み取ることしかできないメンバ変数である。
class MyClass {
readonly int num;
readonly string targetText = "ウサテイ";
readonly ClassA obj;
public MyClass() {
this.num = 10; // コンストラクタでの初期値設定
// コンストラクタ内ならば、何回でも再設定可能
this.num = 11;
// インスタンスも初期化可能
obj = new ClassA();
}
public MyClass(string value) {
this.targetText = value; // 初期化時の値を、コンストラクタ時に再設定することが可能。
}
}
メンバ関数
静的メンバ関数
メソッドの定義時に「static」をつける。
プロパティ
呼び出し側からはただのメンバ変数に見えるが、実際はロジックが含まれたメンバ関数が呼び出されるもの。
class MyTest {
private int _num;
// プロパティの定義
public int num
{
//
// アクセサの定義
//
set {
// valueは特殊な変数で、setterの定義でのみ使用できる。
// アクセサを使用すると、値の設定が可能であるか検証するコードを実行できる。
if( value > 0 ) {
this._num = value;
}
}
get {
// getterではreturnによって、値を返す
return this._num;
}
}
}
アクセッサのgetterとsetterで異なるアクセスレベルを設定することができる(C#2)
アクセスレベルを別々にすると、外部から値を設定できないメンバ変数を定義し、クラス内部からのみsetterを呼び出して値の検証を行うといった処理が可能となる。
public int num
{
get { return this._num; }
protected set { // setterは内部からのみアクセスを許可。
if( value > 0 ) {
this._num = value;
}
}
C#3からはプロパティのアクセサを簡単に宣言できる自動プロパティという記述ができる。
自動プロパティを使用すると、アクセサの中身を記述する手間を省け、将来的にアクセサに何らかの処理を行わせたいケースが発生した場合に拡張しやすくなる。
public int num {get; set; } // 自動プロパティ
public int num {get; protected set; } // setterのアクセス可能性をprotectedに設定。
演算子のオーバーロード
匿名クラスの定義
JavaScriptのようなプロトタイプ型プログラミング言語のようなクラス(っぽいもの)を定義できる。
var x = new {Age = 10, Name = "ケミストリー" };
int age = x.Age;
String name = x.Name;
変数xは匿名クラスです。型は存在しないためvarキーワードを使って変数を宣言する。
この匿名クラスはAgeとNameというメンバを持っているため、変数xを使用してそれらにアクセスができる。
静的なクラスの定義
インスタンスの作成不可能なクラスを静的クラスと呼ぶ。静的クラスでは静的なメンバ関数・静的なメンバ変数のみがクラスのメンバに実装できます。
静的クラスを定義するにはstaticキーワードを指定してクラスを定義すればよい。
static class MyClass {
static int num;
public static int getNum() {
return MyClass.num;
}
static MyClass() {
num = 0;
}
}
拡張メソッド
静的クラスを使用すると、あたかもインスタンスのクラスに定義してあるかのように振舞うメソッドをインスタンスのクラスの定義外に実装できます。
拡張メソッドの定義には静的クラスに静的メソッドを実装し、引数に「this」をつける。
static class StringExtensions
{
// string型に対する拡張メソッド
public static string ToggleCase(this string s) {
System.Text.StringBuilder sb = new System.Text.StringBuilder();
foreach(char c in s)
{
if(char.IsUpper(c)) {
sb.Append(char.ToLower(c));
} else if(char.IsLower(c)) {
sb.Append(char.ToUpper(c));
} else {
sb.Append(c);
}
}
return sb.ToString();
}
}
int main() {
string s = "My name is Tom.";
string s1 = StringExtensions.ToggleCase(s); // 通常の呼び出し方。ユーティリティっぽい。
string s2 = s.ToggleCase(); // 拡張メソッドでの呼び出し方。
// string.ToggleCase()を呼び出しているように見えるが、
// string型にはToggleCaseなんてメソッドは存在しない!
}
あたかもstringクラスにToggleCase()が実装されているように見えるため、あまり乱用してしまうとどこで定義されているか見失う可能性がある。他にも、本当にstringクラスがToggleCaseメソッドを実装したら既存のコードはどうなるか?など問題点が多いため、拡張メソッドの使用はなるべく避けるべき。
抽象クラスの定義
抽象クラスは通常のクラスと同じようにメソッドやメンバ変数を持つがインスタンスが作成できない。
抽象クラスは、同じ処理を必要とする派生クラスを作成する場合などに使用する。
抽象クラスを定義するにはabstractキーワードを使用する。
abstract class MyClass {
}
抽象メソッドの定義
抽象メソッドとはメソッドの宣言のみを抽象クラスで行い、メソッドの実装は派生クラスに任せるといった場合に使用する。
abstract class MyClass {
abstract string sayHello(); // 抽象メソッド
メソッドだけでなく、プロパティも抽象メソッドとして宣言することが可能である。
クラスの継承
class MyParent{
}
class MyChild : MyParent { // MyParentを継承したMyChildクラスを定義
}
コンストラクタの呼び出し順序
継承によってクラスツリーを構成したクラスのコンストラクタは「基底クラス→派生クラス」という順番に呼び出される。
通常は自動的に基底クラスのコンストラクタを呼び出しますが、これは基底クラスにデフォルトコンストラクタが定義されている場合である。
基底クラスにデフォルトコンストラクタが定義されていない場合や基底クラスに複数のコンストラクタが定義されている場合、任意のコンストラクタを明示的に呼び出す必要がある。
class MyParent{
public MyParent(int config){
}
}
class MyChild : MyParent { // エラー!
// MyParentにデフォルトコンストラクタがない
}
class MyChild : MyParent {
public MyChild() : base(0) { // MyParent(0)を明示的に呼び出す
}
public MyChild(int config) : base(config) {
}
呼び出し可能な基底クラスのメンバ
派生クラスは基底クラスが定義しているすべてのメンバへのアクセスが可能というわけではない。
派生クラスがアクセス可能な基底クラスのメンバは次の通り。
- publicで宣言されたメンバ変数・メンバ関数
- protectedで宣言されたメンバ変数・メンバ関数
class Base
{
public int public_val;
protected int protected_val;
private int private_val;
void BaseTest()
{
public_val = 0; // OK
protected_val = 0; // OK
private_val = 0; // OK
}
}
class Derived : Base
{
void DerivedTest()
{
public_val = 0; // OK
protected_val = 0; // OK (protected は派生クラスからアクセス可能)
private_val = 0; // エラー! (private は派生クラスからアクセス不能)
}
}
class Test
{
static void Main()
{
Base b = new Base();
b.public_val = 0; // OK
b.protected_val = 0; // エラー!(protected は外部からアクセス不能)
b.private_val = 0; // エラー!(private は外部からアクセス不能)
}
隠蔽とオーバーライド
派生クラスが基底クラスのメンバを上書きすることをメソッドの隠蔽と呼ぶ。
class Base {
public void hello() {
Console.Write("Base. Hello!");
}
}
class Derived : Base {
public new void hello() { // Baseクラスのhelloメソッドをオーバーライド
Console.Write("Derived. Hello!");
}
public void hello2() {
hello(); // Derivedクラスのhelloメソッド
base.hello(); // Baseクラスのhelloメソッドを呼び出す(次項で説明)
}
}
派生クラスからオーバーライドしたメソッドを呼び出すと、派生クラスで実装した処理が呼び出される。
基底クラスのメソッドを呼び出したい場合は、明示的に
baseキーワード
を使用する必要がある。
メソッドの隠蔽と同じように派生クラスが基底クラスのメンバを上書きする際に、仮想メソッド宣言してメソッドを上書きすることをオーバーライドと呼ぶ。
オーバーライドを許可するメソッドにはvirtualキーワードを修飾します。また、オーバーライドするメソッドにはオーバーライドしていることを明示的に示すためoverrideキーワードを修飾する。
class Base {
public virtual void hello() {
Console.Write("Base. Hello!");
}
}
class Derived : Base {
public override void hello() { // Baseクラスのhelloメソッドをオーバーライド
Console.Write("Derived. Hello!");
}
public void hello2() {
hello(); // Derivedクラスのhelloメソッド
base.hello(); // Baseクラスのhelloメソッドを呼び出す(次項で説明)
}
}
このように派生クラスでメソッドを上書きする仕組みには2種類あるが、これらの違いはクラスのインスタンスを使用する際に現れる。
隠蔽やオーバーライドはメンバ関数だけでなくプロパティにも使用できる。
最基底クラス「object」
すべてのクラスは基底クラスであるobjectクラスを継承しています。
objectクラスにはオブジェクトを文字列で表示するためのToString()などのメソッドが定義されています。
これらは仮想メソッド宣言されているため、派生クラスでオーバーライドすることができます。
継承の禁止
派生クラスを作成してほしくないようなクラスを定義する場合にはsealedキーワードを指定する。
sealed class MyTest {
}
列挙型
// 列挙型の定義
enum MyEnum {
Test1,Test2,Test3
}
// アクセス方法
MyEnum e = MyEnum.Test1;
列挙型はC++のように各要素に値を設けることができる。
// 列挙型の定義
enum MyEnum : int{
WMA_CREATE = 1 // 0001
,WMA_BLACK = 2 // 0010
,WMA_CHILD = 4 // 0100
,WMA_COLD = 8 // 1000
}
void main() {
byte pos = 4;
Console.Write("フラグ名={0}",(MyEnum)pos); // "WMA_CHILD"
}
Enumの操作
System.Enumでは列挙型の操作を行える。
Enum.GetName | string s = Enum.GetName(typeof(MyEnum),4) | 列挙型の指定位置の要素名を取得 |
Enum.GetValues | int[] i = Enum.GetValues(typeof(MyEnum)) | 列挙型の値をすべて取得する |
Enum.GetNames | int[] i = Enum.GetNames(typeof(MyEnum)) | 列挙型の要素名をすべて取得する |
フラグ
各要素には任意の順番を振ることができる。
この仕様を活かし、列挙型は重複のない数値を管理するために使用できる。
enum MyEnum {
X = 1, // 0x001
Y = 2, // 0x010
Z = 4, // 0x100
}
このような列挙型はOR演算によってフラグとして利用する際に便利です。
MyEnum flag = MyEnum.X | MyEnum.Z; // 011
上記のコードで得られた変数flagには「3」という値が格納されますがMyEnumには3に相当する要素がないため、警告はされませんが不明な値を持った変数であるように見えることがあります。
MyEnum flag = MyEnum.X | MyEnum.Z; // 011
System.write(flag); // flag.ToString()によって「3」が出力される。
MyEnumを定義する際にFlags属性(FlagAttribute)をつけておくと、ToString()の出力結果が変わりわかりやすくなります。
[Flags]
enum MyEnum {
X = 1, // 0x001
Y = 2, // 0x010
Z = 4, // 0x100
}
MyEnum flag = MyEnum.X | MyEnum.Z; // 011
System.write(flag); // 「X, Y」と出力される
Flags属性を持ったEnumではビット演算を行うことができます。
[Flags]
enum MyEnum {
X = 1, // 0x001
Y = 2, // 0x010
Z = 4, // 0x100
}
MyEnum flag = MyEnum.X | MyEnum.Z;
if( (flag & MyEnum.X) == MyEnum.X)
System.write("Xがセットされています。");
if( (flag & MyEnum.Y) == MyEnum.Y)
System.write("Yがセットされています。");
if( (flag & MyEnum.Z) == MyEnum.Z)
System.write("Zがセットされています。");
上記のような算術記法によるビット演算は、コードが複雑化しますのでこちらで説明するようなメソッドを使用するとよいです。