構造体
構造体
class Program
{
static void Main(string[] args)
{
var p1 = new Point(10);
// 参照ではなく、コピーした値がp2に割り当てられる
var p2 = p1;
// p1のプロパティを更新してもp2には影響がない
p1.X = 1;
// p1.X=1
// p2.X=10
// 構造体の初期値はnullにはなり得ない
var points = new Point[10];
// point[0] != null
// point[0].X == 0 // 各フィールドはデフォルト値で初期化される
// [参考]一方、クラスの初期値はnullである
var uris = new System.Uri[10];
// uris[0] == null
}
struct Point
{
private int x;
public int X
{
get { return x; }
set { x = value; }
}
public Point(int x)
{
this.x = x;
}
}
}
|
構造体は、クラスによく似た構造だが、値型でありヒープ割り当てを必要としない。そのため、値を持つ小規模なデータ構造に適している。
構造体は値型であるため、構造体を参照する変数を別の変数に割り当てると、参照ではなくコピーした値が代入される。サンプルコードでは、変数p1
をp2
に代入しているが、その後p1
のプロパティを変更してもp2
は変更されない。
また、値型であるため、構造体自身の初期値はnullではなく、構造体の各フィールドはそれぞれのデフォルト値で初期化した値となる。そのため構造体を要素とする配列を初期化した時点で、配列の各要素には構造体の初期値が代入されている。
継承とインターフェース
継承と仮想、override、new
using System;
class Program
{
static void Main(string[] args)
{
var my = new MyClass1();
my.Test2(); // MyClass1.Test2
var my2 = new MyClass2();
my2.Test2(); // 1
}
}
public class MyBase
{
public int Value { get; set; } = 0;
public void Test1()
{
Console.WriteLine("MyBase.Test1");
}
public virtual void Test2()
{
Console.WriteLine("MyBase.Test2");
}
}
public class MyClass1 : MyBase
{
public new int Value { get; set; } = 1;
public override void Test2()
{
Console.WriteLine("MyClass1.Test2");
}
}
public class MyClass2 : MyClass1
{
public override void Test2()
{
Console.WriteLine(Value);
}
}
|
オブジェクト指向の特徴の一つであるクラスの継承は、クラス名の後に:
で基底クラスを宣言することで記述できる。C#ではvirtual
を省略したメソッドは、非仮想メソッドであり継承したクラスでオーバーライドすることはできない。そのため、オーバーライドを許可するメソッドは基底クラスの側でvirtual
修飾子を付けてオーバーライドを許可する必要がある。オーバーライドする側のメソッドはoverride
修飾子を付けるが、さらに継承先のクラスでオーバーライドすることもできる。
また、継承したクラスで基底クラスのメソッドやプロパティと同じ名前のメンバーを宣言することも可能だ。このとき、基底クラスのメンバーは隠ぺいされてしまうため、継承したクラスではメソッドやプロパティなどのメンバーにnew
修飾子を付けて隠ぺいしていることを明示できる。
抽象クラスとシールクラス、シールメソッド
using System;
class Program
{
static void Main(string[] args)
{
var my = new MyClass();
my.Test1(); // MyAbstractClass.Test1
my.Test2(); // MyClass.Test2
my.Test3(); // MyClass.Test3
var my2 = new MyClass2();
my2.Test1(); // MyAbstractClass.Test1
my2.Test2(); // MyClass2.Test2
my2.Test3(); // MyClass.Test3
}
}
public abstract class MyAbstractClass
{
public void Test1()
{
Console.WriteLine("MyAbstractClass.Test1");
}
public abstract void Test2();
public abstract void Test3();
}
public class MyClass : MyAbstractClass
{
public override void Test2()
{
Console.WriteLine("MyClass.Test2");
}
public sealed override void Test3()
{
Console.WriteLine("MyClass.Test3");
}
}
public sealed class MyClass2 : MyClass
{
public override void Test2()
{
Console.WriteLine("MyClass2.Test2");
}
}
|
抽象クラスは直接インスタンス化できないクラスで、通常のメソッド定義に加え抽象メソッドや抽象プロパティをメンバーに持つことができる。これは継承先のクラスが持つ共通の振る舞いをあらかじめ定義しておくときに役立つ。抽象メソッドを継承したクラスでオーバーライドするときはoverride
修飾子を付ける。
抽象クラスからの継承に限らず、定義したクラスを継承することを禁止する場合、sealed
修飾子を付けてシールクラスにすることができる。また、メソッド単位でオーバーライドを禁止する場合も、sealed
修飾子を付けてシールメソッドにすることができる。
インターフェースと明示的実装
using System;
class Program
{
static void Main(string[] args)
{
var s1 = new Surface();
s1.Name = "a";
s1.Paint(); // Paint
((ISurface)s1).Paint(); // Paint
((IPaintable)s1).Paint(); // Paint
var s2 = new Surface2();
s2.Paint(); // Paint
((ISurface)s2).Paint(); // Paint
((IPaintable)s2).Paint(); // IPaintable.Paint
}
}
interface IPaintable
{
void Paint();
}
interface ISurface
{
string Name { get; set; }
void Paint();
}
public class Surface : ISurface, IPaintable
{
public string Name { get; set; }
public void Paint()
{
Console.WriteLine("Paint");
}
}
public class Surface2 : ISurface, IPaintable
{
public string Name { get; set; }
public void Paint()
{
Console.WriteLine("Paint");
}
void IPaintable.Paint()
{
Console.WriteLine("IPaintable.Paint");
}
}
|
インターフェースはメソッドやプロパティのコントラクト(=実装すべき規約)を定義でき、インターフェースを実装するクラスや構造体は、必ずそのコントラクトに従って実装する必要がある。C#では継承するクラスは1つのみだが、インターフェースは複数実装できる。
インターフェースを複数実装する場合、異なるインターフェースが同じコントラクトを持っていることがある(サンプルコードではIPaintable
/ISurface
インターフェースの両方にvoid Paint()
メソッドがある)。この場合、特に指定せずに実装すると両者のインターフェースは同じ実装メソッドを呼び出す。しかし、それぞれのインターフェースで異なる実装を行いたい場合は、明示的に実装可能だ。サンプルコードのSurface2
クラスのIPaintable.Paint
メソッドがその例で、IPaintable
インターフェースにキャストして呼び出すと、明示的実装が呼び出されることになる。
列挙型
列挙型の宣言
class Program
{
static void Main(string[] args)
{
var c1 = Color.Red;
var c2 = Color.Blue;
// 定義されていない値をキャスト可能だが、非推奨
var t3 = (LongType)23;
}
}
public enum Color
{
Red,
Blue,
Orange
}
public enum LongType : long
{
Solid = int.MaxValue + 1L,
Soft = 0, // 必ず0を定義することが推奨される
Hard = Solid
}
|
列挙型(=列挙体)は、定数のリストを名前付きで管理するための構造である。例えばリスト15-1のColor
列挙型の各値には名前を付けているが、その実体はint
型もしくはlong
型の数値である。数値を明示的に指定しない場合は、宣言された順に0から1つずつ増えた値が割り当てられる。明示的に指定する場合、0が割り当てられる列挙型の値を必ず定義することが推奨されている。
列挙型は数値であるため、数値を列挙型にキャストでき、実際には定義されていない値であってもキャスト可能である。しかし、これは予期しない実行時エラーを起こしかねないので推奨されていない。
Flags属性
using System;
class Program
{
static void Main(string[] args)
{
var colors = Color.Red | Color.Blue;
colors.HasFlag(Color.Red); // True
var f1 = (colors == Color.Red); // False
colors.HasFlag(Color.Yellow); // False
}
}
[Flags]
public enum Color
{
Red,
Blue,
Yellow
}
|
列挙型の値を利用する際に、複数の値を持っている状態を表現したい場合がある。その場合は、列挙型の宣言にFlags
属性を付けることでビットフラグを表現できる。複数の値を持つ値は|
演算子で記述でき、複数とり得る値の中で指定した列挙型の値が含まれているかどうかは、HasFlag
メソッドで検査できる。
イテレーター
イテレーターの宣言とyield
using System;
using System.Collections;
using System.Collections.Generic;
class Program
{
static void Main(string[] args)
{
// a b c z
var samples = MyCollection.GetSamples();
// 1, 9, 25, 49, 81
foreach (var element in new MyCollection())
{
Console.WriteLine(element);
}
}
}
public class MyCollection : IEnumerable<int>
{
public static IEnumerable<string> GetSamples()
{
yield return "a";
yield return "b";
yield return "c";
yield return "z";
}
public IEnumerator<int> GetEnumerator()
{
for (int i = 0; i < 10; i++)
{
if (i % 2 == 0)
continue;
yield return i * i;
}
}
// IEnumerableインターフェースのGetEnumerator()メソッドを実装
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
|
返り値がIEnumerable
型およびそのジェネリスク型のIEnumerable<T>
である場合、yield return
キーワードで反復表現を記述できる。yield return
するたびにIEnumerable
型オブジェクトの要素1つを返すことができ、メソッドが完了した時点で要素の列挙が終わったことになる。
また、クラスがIEnumerable
インターフェースもしくはそのジェネリクス型のインターフェースを実装する場合の、GetEnumerator
メソッドの実装にもyield return
を実装できる。
例外処理
throwとtry-catch
using System;
class Program
{
static void Main(string[] args)
{
try
{
ThrowException();
}
catch (MyException ex) when (ex.Value >= 1)
{
Console.WriteLine("Catch MyException");
}
catch (Exception ex)
{
Console.WriteLine("Catch Exception: " + ex.Message);
throw; // 再スロー
}
}
static void ThrowException()
{
throw new MyException() { Value = 1 };
}
}
public class MyException : Exception
{
public int Value { get; set; }
}
|
例外の発生をthrow
句で記述できる。C#ではメソッド宣言にスローされる例外を記述することはできず、任意の例外がスローされる可能性がある。スローする例外は、定義済みの例外もしくはException
クラスを継承したユーザー定義の例外をスローできる。
例外処理はtry-catch
構文で記述し、try
ブロックの中で発生した例外をcatch
ブロックで処理できる。catch
ブロックは、まず例外の型により複数宣言でき、スローされた例外が代入可能な最初のcatch
ブロックが実行される。代入可能なcatch
ブロックがない場合はメソッド呼び出し元に例外がスローされる。C# 6.0からcatch
ブロックにwhen
キーワードでさらにcatchするかどうかの条件を記述できるようになった。
catch
ブロック内で例外を再スローしたい場合は、throw
句を記述する。throw
句の後の例外インスタンスは省略でき、省略した場合は呼び出し階層を記録したスタックトレースが複雑にならないため、特別の理由がない限りは例外インスタンスを省略した方がいいだろう。
リソースの解放
finallyとusingによるリソースの解放
using System;
class Program
{
static void Main(string[] args)
{
var resource = new MyResource();
try
{
resource.Execute();
}
finally
{
resource.Close();
}
using (var r1 = new MyDisposableResource())
using (var r2 = new MyDisposableResource())
{
r1.Execute();
r2.Execute();
}
}
}
public class MyResource
{
public void Execute()
{
Console.WriteLine("MyResource.Execute");
}
public void Close()
{
Console.WriteLine("MyResource.Close");
}
}
public class MyDisposableResource : IDisposable
{
public void Execute()
{
Console.WriteLine("MyDisposableResource.Execute");
}
public void Dispose()
{
Console.WriteLine("MyDisposableResource.Dispose");
}
}
|
利用したインスタンスに対し、リソースの解放などのために特定のメソッドを呼び出す必要がある場面を考えよう。例外がスローされる状況でも必ず解放処理を行うために、try-finally
句を使うことができる。finally
句内の処理は例外が起きた場合でも必ず実行される。しかしこの構文は少し冗長であり、変数のスコープもtry
の外側に及んでしまう。
そのため、C#ではusing
ステートメント(※前述のusing
ディレクティブとは異なるキーワード)が用意されている。IDisposable
インターフェースを実装したクラスはusing
内に記述でき、ブロックを抜ける際に必ずその実装クラスのDispose()
メソッドが呼ばれるようになっている。
- 前のページへ
- 次のページへ
- 【記事のページ一覧】
- 1 プログラムの実行と制御/型と変数
- 2 演算子/ステートメント
- 3 名前空間/クラス
- 4 メソッド/プロパティ/イベント/インデクサー/演算子オーバーロード/コンストラクターコンストラクタとデストラクターデストラクタ
- 5 構造体/継承とインターフェース/列挙型/イテレーター/例外処理/リソースの解放
- 6 ラムダ式/非同期処理(async/await)