表1.4 C#のユーザー定義型
ユーザー定義型 | 概要 | 本書の該当箇所 |
---|---|---|
クラス | いわゆる複合型、一番利用頻度の高いユーザ定義型 | 第4章 |
インターフェイス | 抽象メンバーだけを持つ型 | 第5章 |
構造体 | 値型の複合型 | 第8章 |
列挙型 | 特定の値だけを取ることができる型 | 第8章 |
デリゲート | メソッドを参照するための型 | 第7章 |
Point 本章で説明した「変数」や、後述する「フィールド」のスコープは、狭い方がプログラム全体を把握しやすくなる。そこで、スコープをできる限り狭くとどめるため、以下のような点を心がけよう。
- メソッドは短くする
- 静的フィールドの利用を避ける
- アクセシビリティは必要最小限にする
大きな範囲への型変換は問題がでないが、逆の場合、桁落ちして正しい数値を表せなくなる可能性がある。そこで、C#では桁落ちしない、より大きな範囲を扱える型への変換だけが暗黙的に行える。
short m = 365;
long n = m;
double x = n; // OK: longからdoubleへの型変換
int i = x; // NG: コンパイルエラー
明示的な型変換としてキャスト式というものがある。
int i = 365; // int型の変数i
short j = (short)i; // 変数iをshort型にキャスト
扱える範囲が狭い方への変換では、場合によってはオーバーフローが起きる。例えば、byte型は0から255の範囲を扱える型で、int型などからの型変換をかけると、256で割ったあまりの値になる。
int m = 365;
byte n = (byte)m; // 365を256で割ったあまりの109になる
オーバーフローをエラーにするかどうかを選択する手段は2つある。1つはコンパイルオプションを指定する方法。
もう1つは、checked
, unchecked
というキーワードを使う方法。こちらは、コード内の特定の部分だけを挙動を切り替える。
check式、またはステートメント中でオーバーフローが発生した場合、リスト2.4のように、OverflowExceptionという例外が発生する。
リスト2.4 OverflowException例外の発生
using System;
class CheckedSample
{
static void Main()
{
try
{
checked
{
sbyte a = 64;
sbyte b = 65;
sbyte c = (sbyte)(a + b); // オーバーフローにより例外発生
}
}
catch (OverflowException ex)
{
Console.Write(ex.Message); // 例外への対応
}
}
}
実行結果
演算操作中の結果オーバーフローが発生しました
意図的なオーバーフロー
リスト2.5 擬似乱数の生成
using System;
class Random
{
uint seed;
public Random(uint seed)
{
this.seed = seed;
}
public long Next()
{
// ここでわざとオーバーフローを無視する
seed = unchecked(seed * 1664525 + 1013904223);
return seed;
}
}
整数と異なり、浮動小数点数の場合はには、オーバーフローを起こした時、結果が無限大扱いされる。逆に、値が小さすぎて精度的に扱えない場合は0扱いになる。
浮動小数点数の場合、checkedステートメント中でもオーバーフローによる例外は発生しない。
C#では、列挙型(enumeration type)と呼ばれるものを利用することで、曜日などの特定の値しか取らないデータを表現することができる。
例えば、曜日は月・火・水・木・金・土・日の7つの値しか取らない。
これらの値は、バラバラの変数を宣言して使うよりも、まとめて宣言できた方が便利である。また、「曜日型の変数に性別の値を代入」というような不正な処理ができないようにもしたい。このような場合に使うのが列挙型である。
列挙型の利用例
using System;
enum Month : byte
{
January = 1, Feburary, March, April,
May, June, July, August,
September, October, November, December
}
class EnumSample
{
static void Main ()
{
for(int i=1; i<12; ++i)
Console.Write("{0}月 {1}\n", i, (Month)i);
// 列挙型Monthのメンバーを文字列化して利用
}
}
実行結果
1月 January
2月 Feburary
.
.
.
11月 November
クラス定義の際、修飾子(modifier)というものを付けることで、クラスの性質をいくつか指定できる。クラスに対する修飾子には以下のようなものがある。
- アクセシビリティ : public, protected, internal, private
- 継承 : abstract, sealed, new
- 静的クラス : static
- 分割定義 : partial
- 安全ではないコード : unsafe
C#2.0からクラス定義時にpartial
というキーワードを付けることで、クラス定義を複数に分割できるようになった。例えば、以下のように書ける。
partial class Complex
{
public double re;
public double im;
}
partial class Complex
{
public double Abs()
{
return Math.Sqrt(re * re + im * im);
}
}
それぞれのクラスは別ファイルにあっても良いが、必ずpartialキーワードを付けないといけない。
C#3.0からパーシャルメソッドという機能も追加された。
C#のクラスは基本的に、常に継承して派生クラスを作ることができるので、場合によっては絶対に継承されたくないということもある。このような場合、クラス定義時にsealed(=封印された)という修飾子を付けることで、継承を禁止することができる。
sealed class SealedClass { } // sealed修飾子を付けて定義
class Derived : SealedClass // SealedClassは継承不可なのでエラーになる
{
}
基底クラスの変数に派生クラスのインスタンスを渡すことは可能。これをアップキャスト(upcast)とと呼ぶ。一方、それとは逆に、派生クラスの変数に基底クラスのインスタンスを渡すことをダウンキャスト(downcast)と呼ぶ。
アップキャストは暗黙的に行えるが、ダウンキャストは失敗する可能性があるので、明示的なキャスト式が必要。
is
演算子はキャスト可能かどうかを調べるための演算子
is演算子を適用した結果はbool型になり、左辺の変数が右辺の変数の型にキャスト可能ならばtrueを、不可ならばfalseを返す。
as
演算子はキャスト式と同じような働きをする演算子で、以下のようにして使用する。
変換先の変数 = 変換元の変数 as 型名
キャストとの違いは、もし型変換ができない場合には結果がnullになるということ。
Note
キャストとas演算子は、変換失敗が意図したものかどうかで使い分ける。
仕様的に失敗の可能性があり、かつ、失敗した場合の代替処理が用意できる場合にはas演算子を使う。一方、失敗しないはず、あるいは、失敗したならプログラムが続行不能であるような場合にはキャストを使う。
class Base
{
public void Test()
{
Console.Write("Base.Test()\n");
}
}
class D
- 演算子のオーバーロード : ユーザ定義型に対して演算子を自由に定義できる
- コレクション : 配列と同様のことを任意のユーザー定義型で行えるようにする
- コレクション初期化子 : {}を使って要素の初期化を行う
- インデクサー : []を使って添字アクセスできるようにする
- foreach : 任意のユーザー定義型に対して、foreachステートメントを使った要素の列挙ができるようにする
このように、ユーザー定義型を組み込み定義型と区別しないように扱うことを指して、「全ての型が第一級市民(first-class citizon)である」というような言い方をする。
複素数クラス(Complex)に、加算演算子「+」を定義したい場合、以下のように書く
class Complex
{
public static Complex operator+ (Complex z, Complex w)
{
return new Complex(z.Re + w.Re, z.Im + w.Im);
}
}
var x = new List<int> { 1, 2, 3 };
この{}の部分をコレクション初期化子(collection initializer)と呼ぶ。コレクション初期化子を使える条件は以下のとおりである。
- IEnumerableインターフェイス(System.Collections名前空間)を実装している
- Addメソッドを持っている
この条件を満たす最低限の実装例をリスト6.3に示す。
リスト6.3 コレクション初期化子を使える最低限のクラス実装例
using System;
using System.Collections;
class Program
{
static void Main()
{
var x = new CollectionInitializable
{
"abc",
"def",
"ghi",
};
}
}
class CollectionInitializable : IEnumerable
{
public void Add(string item)
{
Console.WriteLine("{0} Added", item);
}
public IEnumerator GetEnumerator()
{
throw new NotImplementedException();
}
}
実行結果
abc Added
def Added
ghi Added
コレクション初期化子は、Addメソッド呼び出し相当のコードに展開される。
インデクサーの利用例
using System;
class Program
{
static void Main()
{
// インデクサーの利用
var x = new RangeArray<int>(1, 3);
x[1] = 1;
x[2] = 2;
x[3] = 4;
Console.WriteLine(x[1]);
}
}
class RangeArray<T>
{
T[] array;
int lower;
public RangeArray(int lower, int length)
{
this.lower = lower;
array = new T[length];
}
// インデクサーの定義
public T this[int i]
{
set { this.array[i - lower] = value; }
get { return this.array[i - lower]; }
}
}
実行結果
1
本章では関数を主役とした機能について3つ紹介する
- 1つ目は「デリゲート」といって、メソッドを変数に格納するようなもの
- 2つ目は「イベントハンドラ」といって、ユーザーの操作や出来事に対応した処理を書くためもの
- 3つ目は「拡張メソッド」といって、既存クラスに外部からメソッドに追加できるもの
デリゲート(delegate=委譲、代理人)。この機能を使うことで、メソッドをオブジェクトの一種として扱うことができる。
「委譲」という言葉の通り、デリゲートは「他のメソッドに処理を丸投げするためのオブジェクト」を作るためのもの。具体的には、メソッドを代入できる変数が作れる。「関数ポインタ」のようなもの。
デリゲートを使用するためにはまず、デリゲート型を定義する。デリゲート型の定義には、delegate
キーワードを用いて行う。
デリゲートの使用例
using System;
delegate void SomeDelegate(int a); // SomeDelegateというデリゲート型を定義
class DelegateTest
{
static void Main()
{
SomeDelegate a = A; // 暗黙にSomeDelegate型に変換
a(256);
}
static void A(int n)
{
Console.Write("A({0}) が呼ばれました。\n", n);
}
}
実行結果
A(256)が呼ばれました。
デリゲートには、クラス(static)メソッドとインスタンス(非static)メソッドのどちらでも代入することができる。
デリゲートには、+=
演算子を用いることで複数のメソッドを代入することができる。複数のメソッドを代入した状態でデリゲート呼び出しを行うと、代入した全てのメソッドが呼び出される。このように、複数のメソッドを格納した状態のデリゲートのことをマルチキャストデリゲートと呼ぶ。
マルチキャストデリゲートの例
using System;
// メッセージを表示するだけのデリゲート
delegate void ShowMessage(); // デリゲート型を定義
class Person
{
string name;
public Person(string name){this.name = name;}
public void ShowName(){Console.Write("名前: {0}\n", this.name);}
}
class DelegateTest
{
static void Main()
{
Person p1 = new Person("John");
Person p2 = new Person("Cathy");
// デリゲートに複数のメソッドを代入
ShowMessage show = new ShowMessage(p1.ShowName);
show += new ShowMessage(p2.ShowName);
show += new ShowMessage(funcA);
show += new ShowMessage(funcB);
show();
}
static void funcA() {Console.Write("funcAが呼ばれました。\n");}
static void funcB() {Console.Write("funcBが呼ばれました。\n");}
}
`
実行結果
名前: John
名前: Cathy
funcAが呼ばれました。
funcBが呼ばれました。
ActionやFuncという、汎用的に使えるジェネリックなデリゲートが標準で提供されている。多くの場合、ActionとFuncだけあれば十分で、デリゲート型を自分で定義する必要は殆ど無い。
Action
は戻り値を返さないデリゲートで、System名前空間内で、以下のように定義されている。(型パラメータの前のin/outは「変性」を指定する修飾子)
public delegate void Action();
public delegate void Action<in T>(T obj);
public delegate void Action<in T1, in T2>(T1 arg1, T2 arg2);
...同様に、パラメータが8個のものまで定義
一方、Func
は戻り値を返すデリゲートで、同じくSystem名前空間内で、以下のように定義されている。
public delegate TResult Func<out TResult>();
public delegate TResult Func<in T, out TResult>(T arg);
public delegate TResult Func<in T1, in T2, out TResult>(T1 ar1g, T2 arg2);
...同様に、パラメータが8個のものまで定義
その他、EventHandlerとPredicate(いずれもSystem名前空間)というデリゲートもよく使われている。
EventHandler
は、後述する「イベント処理」に使うもの、以下のように定義されている。
public delegate void EventHandler<TEventArgs>(object sender, TEventArgs e);
Predicate
は、論理学用語の述語(predicate:「xxであるか否か」という命題を表す)に由来するもので、以下のような、bool型を返す1パラメータのデリゲートである。
public delegate bool Predicate<in T>(T obj);
デリゲートは主に以下のような用途で用いる。
- 処理を部分的に差し替える(外部から与える)
- 処理をどこか別のタイミング・別の場所で実行してもらう
これらの処理は、インターフェイスを使うことでも同様のことが実現できる。しかし、メソッドを直接渡せたり、匿名関数を使って書けたりするので、デリゲートを使うのが便利。
void Log(string message)
{
var d = Logger;
if (d != null)
d(message); // デリゲート越しにログを記録
}
public Action<string> Logger { get; set; } // 外部からデリゲートを渡す
ikeda wrote: 機能としてLog関数内にdが存在していて、その実装はデリゲート型に収める形で記述する。つまり、デリゲートを用いることで、機能と実装の分離が行えるということ
こうすることで、外部からデリゲート越しに記録先を指定し、用途ごとに差し替えることができる。
デリゲートを使って、条件を外部から渡す。
static int Sum(IEnumerable<int> data, Predicate<int> pred)
{
int sum = 0;
foreach (var x in data)
if (pred(x))
sum += x;
return sum;
}
こうすることで、様々な条件を外から与えて、汎用的な集計処理ができるようになる。
ikeda wrote: 機能としてSum関数内にpredが存在していて、その実装は別の所で記述されたものを引数として利用する。デリゲートを用いることで、機能と実装の分離が行えるということ
Note 以下のコードは説明のために簡単化したもので、実際には、スレッド安全のために、複数のスレッドから同時に呼ばれても問題が起きないように、もう少し複雑になる。
private Action<Key> _keyDown; // 登録されたデリゲート(マルチキャスト)
public void AddKeyDownHandler(Action<Key> handler) // 登録メソッド
{
_keyDown += handler;
}
public void RemoveKeyDownHandler(Action<Key> handler) // 登録解除メソッド
{
_keyDown -= handler;
}
private void RaiseKeyDown(Key key) // イベント通知メソッド
{
var d = _keyDown;
if (d != null) d(key);
}
デリゲート型のフィールドやプロパティを直接公開するのではなく、登録口となるAdd/Removeというメソッドのペアを用意する。外部から意図しない書き換えや呼び出しがされることを避けるためである。
これは、プロパティと似ている。フィールドは直接公開せず、Get/Setメソッド(アクセサー)を介して読み書きすべきで、そのアクセサーの実装を簡単にするための構文がプロパティである。同様に、イベントに対しても、Add/Removeメソッドの実装を簡単化するための構文として、次節に説明する「イベント構文がある」
前節で説明したようなイベントハンドラの登録口は、C#の場合、以下のように、eventキーワードを使って1行で書くことができる。
public event Action<Key> KeyDown; // イベント構文
private void RaiseKeyDown(Key key)
{
var d = KeyDown;
if (d != null) d(key);
}
これで、次のような性質を持つようになる
- クラス内部からは、通常のデリゲート型のフィールドのように参照可能
- クラス外部からは、追加(+演算子を使う)と削除(-演算子を使う)のみ可能
Note ここではコードを簡素化するためにAction型(System名前空間)のデリゲートを使っているが、.NETコーディング規約としてはEventHandler型(System名前空間)の利用を推奨する。
C#3.0で、静的メソッドをインスタンスメソッドとして同じ形式で呼び出せる、拡張メソッド(extension method)という機能が追加された。
拡張メソッドは、以下の条件を満たす特別な静的メソッドとして定義する。
- 非ジェネリックな静的クラスのpublicな静的メソッドとして定義する
- 第1引数にthis修飾子をつける
メソッドをで複数の値を返したいという場面にで、C#では参照引数(ref修飾子)ではなく、出力引数(out修飾子)を使う。
メソッド内で変数を初期化する予定である場合、以下のようにout
修飾子を用いて出力用の変数であることを明示する。
リスト8.6 出力引数を用いた例
using System;
class ByValueTest
{
static void Main()
{
int a;
Test(out a); // 参照渡しをしているので、Test内でaに書き込みがある
Console.Write("{0}\n", a);
}
static void Test(out int a)
{
a = 10; // outを使った場合、メソッド内で必ず値を代入しなければならない
}
}
out修飾子を用いて宣言された引数は、ref修飾子と同様に、参照渡しになる。ref修飾子との違いは、メソッド呼び出し前に初期化する必要がなく、メソッド内で必ず値を割り当てる必要がある点。
例えば、メソッドで複数の値を返したい場合、戻り値では1つしか値を返せないので出力変数を使う。j
通常、値型はnull値(無効な値)を取れない。ところが、データベース等、一部のアプリケーションでは、値型の通常の(有効な)値とnull(無効な値)を取るような型が欲しいときがある。そこで、C#2.0以降では、「Nullable型」(null許容型)という特殊な型が用意された。
Nullable
型は、値型の型名の後ろに?
を付けることで、元の型の値またはnullの値を取れる型になるというもの。int型を例に取ると、以下のような書き方ができる。
int? x = 123;
int? y = null;
Nullable型にできるのは、null非許容がtあ(Nullable型を除く値型)のみ。したがって、例えば参照型であるstring型をNullable型にすることはできず、string?という型は定義できない。また、Nullable型をさらにNullableにすることはできず、int??(Nullable型のNullable型)という書き方もエラーになる。
T?という書き方で得られるNullable型は、コンパイル結果的には、Nullable構造体(System名前空間)と等価になる。そして、この型は、HasValueというbool型のプロパティと、ValueというT型のプロパティを持っている。
表8.3 Nullable型のメンバー
戻り値の型 | プロパティ名 | 説明 |
---|---|---|
bool | HasValue | 有効な(null)でない値を持っていればtrue、値がnullならばfalseを返す |
T | Value | 有効な値を返す。もし、HasValueがfalse(値がnull)だった場合、例外InvalidOperationExceptionをスローする |
また、int? x = 123;
という書き方ができることから用意に想像が付くように、T?型とT型の間には暗黙の型変換が存在する。T→T?の変換は常に可能で、以下のようなコードの下2行は等価になる。
int x;
x = 123;
x = new int?(123); // x = 123;と等価
その逆、T?→Tの変換は、HasValueがtrueの時のみ可能で、HasValueがfalseの時には、InvalidOperationExceptionがスローされる。
int? x = 123;
int? y = null;
int z;
z = x; // OK
z = y; // 例外が発生
ファイルなどのリソースを破棄する必要がある。
リソースの破棄の手順をまとめると以下のようになる(ただし、Resource
はリソース管理用クラスで、Disposeメソッドによりリソースの破棄を行うものとする)。
usingステートメントを使わない場合
Resource r = new Resource();
try
{
リソースに対する処理
}
finally
{
if(r != null)
r.Dispose();
}
リソースの破棄は、必ずこの手順で行う。しかし、毎回同じ手順を繰り返すのは面倒である。そこで、C#ではこの手順を自動的に行なってくれる構文が用意されている。この構文はusingステートメントと呼ばれ、以下のようにして用いる。
usingステートメント
using(Resource r = new Resource())
{
リソースに対する操作
}
usingステートメントを用いると、コンパイラが自動的に上述のリソース破棄用のコードに展開してくれる。ただし、usingステートメントで使うリソース管理用クラス(上述のコードの場合、Resource型)はIDisposalbeインターフェイス(System名前空間)を実装している必要がある。
Note FileStreamなどのクラスライブラリ中のクラスは、IDisposalbeインターフェイスを実装している。
C#2.0で、ジェネリック(generics=総称性)、あるいは、総称プログラミングと呼ばれる機能が導入された。これは、様々な型に対応するために、型をパラメータとして与え、その型に対応したクラスやメソッドを生成するという機能である。型だけが異なり、処理内容が同じものを作る時に使うと便利である。
配列など、複数の値をひとまとめにして管理するクラスのことを、コレクションクラスまたはコンテナクラスと呼ぶ。これらに対して、ジェネリックでは依存性・相関性を低くすることができる。このようなことを「**直交性が高い」という。ジェネリックの利点は、このような要素・方式・操作などの直交性を最大限に引き出せることである。
例えば、2つの値の比較が必要な場合、そのクラスはIComparableインターフェイスを実装しているはずなので、以下のように、「クラスTypeはIComparableを実装している」という制約を課す。
static Type Max<Type>(Type a, Type b)
where Type : IComparable // この制約条件のおかげで
{
return a.CompareTo(b) > 0 ? a : b; // Type型はComperableToメソッドを持っていることがわかる。
}
C#4.0から、ジェネリックの型パラメータに「共変性/反変性」を持たせることができるようになった。
Ikeda Wrote:
読んでも、共変性、反変性の意味がわからなかったので、自分で調べておく
Ikeda Wrote:
C#でのイテレータ型の話
IEnumerable
というインターフェイスと、IEnumerator
というインターフェイスを実装することで、イテレータ型を実現する。
大量のデータに対して、検索や加工、集計などの処理が必要になる場面はよくある。このようなデータ処理を簡単に行うため、C#3.0で、LINQ(Language Integrated Querry=言語統合問い合わせ)と呼ばれる一連の機能が追加された。
LINQは単純なプログラミング言語機能ではなく、言語構文、命令規約集、ライブラリ実装といった要素から成り立っている。
- 言語構文の追加 (クエリ式)
- メソッドの命令規約の策定 (標準クエリ演算子)
- ライブラリ実装 (LINQプロバイダ)
リスト10.15A クエリ式
var query =
from c in cards
where c.Rarity == Rarity.Rare
select new { c.Cost, c.Power } into c
group c by c.Cost into g
select new
{
Cost = g.Key,
Average = g.Average(c => c.Power),
};
リスト10.15B リスト10.15Aに等価なメソッド呼び出し
var query =
cards
.Where(c => c.Rarity == Rarity.Rare)
.Select(c => new { c.Cost, c.Power })
.GroupBy(c => c.Cost)
.Select(g => new
{
Cost = g.Key,
Average = g.Average(c => c.Power),
});
標準クエリ演算子を実装して、LINGのデータソースとして使えるライブラリのことをLINQプロバイダと呼ぶ。.NET標準ライブラリで、IEnumerableインターフェイスやIQueryableインターフェイスに対する標準クエリ演算子の実装があるため、これらのインターフェイスを実装する(もしくは実装したデータを返す)だけでもLINQプロバイダといえる。
IEnumerableインターフェイス(System.Colllections.Generic名前空間)に対する標準クエリ演算子の実装(つまり、Enumerable (System.Linq名前空間)で定義されたメソッド群)は、LINQ to Object と呼ばれる。配列も含めて、.NETのコレクションはほとんどIEnumerableインターフェイスを実装しているので、LINQ to Objectsを使ったデータ操作ができる。
IEnumerableインターフェイスがラムダ式を普通にデリゲートとして、受け取るのに対して、IQuerableインターフェイスはラムダ式を式ツリーとして受け取る。
Ikeda Wrote:
ラムダ式とはどこから出てきたか、わからにあ。式ツリーの意味もよくわかっていない。
Note 「式ツリー」とは、ラムダ式をデータとして受け取って、その内容を見て動的な処理を行うための機能である。
一度作ったスレッドを可能な限り使いまわす仕組みを使う。この仕組みをスレッドプールと呼ぶ。特に、.NET Framework 4.0以降では、スレッドプールを簡単に利用するためのTaskクラス(System.Threading.Tasks名前空間)というものが追加されている。例えば、時間がかかる2つの処理Task1とTask2があったとして、この2つを同時に実行するには以下のような書き方をする。
var t1 = Task.Run(() => Task1());
var t2 = Task.Run(() => Task2());
Task.WhenAll(t1, t2).Wait();
また、Taskクラス以外にも、Parallelクラス(System.Threading.Tasks名前空間)なども利用する。
マルチスレッドプログラムでは、同時に並列して処理を行っているため、複数のスレッドが1つのデータに対して操作することがある。同時並行といっても、対象となるデータは1つしかないので、交代しながら処理する。
この際に、何も考えずにただ素直にプログラミングを行うと、意図しない結果になることがある。
using System;
using System.Threading;
using System.Threading.Tasks;
class TestThread
{
// ThreadNum個のスレッドを立てる
// それぞれのスレッドの中でnumをLoopNum回インクリメントする
static void Main()
{
const int ThreadNum = 20;
const int LoopNum = 20;
int num = 0; // 複数スレッドから同時にアクセスされる
Parallel.For(0, ThreadNum, i =>
{
for (int j = 0; j < LoopNum; j++)
{
// numをインクリメント
// 実行結果が顕著に出るように、途中でSleepをはさむ
int tmp = num;
Thread.Sleep(1);
num = tmp + 1;
}
});
Console.Write("{0} (期待値{1})\n", num, ThreadNum * LoopNum);
// numとThreadNum * LoopNumは一致せずnumはより小さい値となる
}
}
実行結果(例)
21 (期待値400)
複数のスレッドがどういう順番で実行されるかが決まっていないため、このような結果となる。
このような問題を解決するためには排他制御(exclusive operation)が必要となる。1つのスレッドの処理がきちんと終わるまで、他のスレッドの処理に切り替わらないようにすれば、実行結果が意図通りになることを保証できる。
複数のスレッドが同時に行ってはいけない一連の処理が記述された部分のことをクリティカルセクション(critical section)と呼ぶ。
排他ロックをかけるために使うのがMonitor
クラスである。Monitorクラスにはオブジェクトにロック取得のためのEnter
メソッド(クリティカルセクションに入るという意味)と、ロック解放のためのExit
メソッド(クリティカルセクションから出る)という2つの静的メソッドがあり、これらを用いることで排他ロック制御を行う。
Monitorクラスでは、参照型の任意の変数を同期オブジェクトとして使用できる。同期オブジェクトを何にするか迷う場合には、適当なスコープのobject型変数を用意してnewobject()とでもしておく。
Note ここで用意するobject型変数は、ロックがクラス内で完結するならばprivate変数にする。インスタンスメソッド内で使うならメンバー変数(フィールド)に、静的メソッド内で使うならば、静的変数を使う。
リスト11.3 リスト11.2に排他制御を追加
using System;
using System.Threading;
using System.Threading.Tasks;
class TestThread
{
// ThreadNum個のスレッドを立てる
// それぞれのスレッドの中でnumをLoopNum回インクリメントする
static void Main()
{
const int ThreadNum = 20;
const int LoopNum = 20;
int num = 0; // 複数スレッドから同時にアクセスされる
var syncObject = new object();
Parallel.For(0, ThreadNum, i =>
{
for (int j = 0; j < LoopNum; j++)
{
Monitor.Enter(syncObject); // ロック取得
try
{
// クリティカルセクション
int tmp = num;
Thread.Sleep(1);
num = tmp + 1;
}
finally
{
Monitor.Exit(syncObject); // ロック解放
}
}
});
Console.Write("{0} (期待値{1})\n", num, ThreadNum * LoopNum);
// numとThreadNum * LoopNumは一致せずnumはより小さい値となる
}
}
実行結果
400 (期待値400)
用語解説 【スレッド安全】 マルチスレッドプログラムにおいて、同時並行にスレッドを実行しても問題が生じないことを「スレッド安全」または「スレッドセーフ」という。
上記のMonitorを利用する処理を簡略化するために、C#にはlockステートメントという排他制御のための専用の構文がある。
lockステートメントを用いると、コンパイラが自動的にMonitorクラスを用いた排他制御用のコードを生成してくれる。例として、先ほどのMonitorクラスを用いて書き直したプログラムを、さらにlock文を使って書き換えると以下のようになる。
リスト11.4 リスト11.3にlock文を使用
using System;
using System.Threading;
using System.Threading.Tasks;
class TestThread
{
// ThreadNum個のスレッドを立てる
// それぞれのスレッドの中でnumをLoopNum回インクリメントする
static void Main()
{
const int ThreadNum = 20;
const int LoopNum = 20;
int num = 0; // 複数スレッドから同時にアクセスされる
var syncObject = new object();
Parallel.For(0, ThreadNum, i =>
{
for (int j = 0; j < LoopNum; j++)
{
lock(syncObject)
{
// クリティカルセクション
int tmp = num;
Thread.Sleep(1);
num = tmp + 1;
}
}
});
Console.Write("{0} (期待値{1})\n", num, ThreadNum * LoopNum);
// numとThreadNum * LoopNumは一致せずnumはより小さい値となる
}
}
実行結果
400 (期待値400)
コンパイラは、コードの最適化の過程で、不要な部分を丸々削除してしまう場合がある。通常は、不要な部分は削除されてよいが、マルチスレッドプログラミングにおいては、一見不要に見えても実は必要な場合がある。
例えば、1つのスレッド内では値を読み込むだけで書き込みはしないが、他のスレッドから値を書き込むという場合、コンパイラは他のスレッドのことまでは知ることができないので、コンパイラからすると、値を書き換えもしないのに何度も読み出している無駄なコードに見える。
こういった状況を想定して、一見無駄に見えても、他のスレッドで値が更新されている可能性のある変数にはvolatile
(揮発性、変わりやすい)という修飾子を与える。volatile修飾子の付いた変数への値の読み書きは、コンパイラの最適化によって削除されることはない。
Ikeda Wrote:
よくわからないかった