24. Learn C# in Unity
C#におけるクラス
・属性(アトリビュート)
C#はアトリビュートという機能を使って、クラス自体やそのメ
ンバーにメタデータを付けることができます。Unityでは自分で
作ったクラスにSerializable属性を付けることでインスペクタ
に表示できるようにしたり、エディタの拡張で多用します。
[ExecuteInEditMode()] // この属性を付けると、エディタ上で非実行時もStartとかUpdateが呼ばれる
public class NewBehaviourScript : MonoBehaviour
{
[System.Serializable()] // この属性を付けないと、Foo型のフィールドがインスペクタに表示されない
public class Foo
{
public int hoge;
}
public Foo f;
}
24/57
25. Learn C# in Unity
C#における構造体
・構造体の定義の仕方
public struct Point
{
// メンバーフィールドの宣言
public int x, y;
// メンバーメソッドの定義
public Point(int x, int y)
{
this.x = x;
this.y = y;
}
}
基本的にはクラスの定義と同じだが、構造体はクラスのような
実装の継承はできない。インターフェース(後述)を継承して、
自分で実装を書くことはできる。
またクラスは参照型だが、構造体は値型であるという違いがあ
る(後述)。
25/57
26. Learn C# in Unity
C#におけるインターフェース
・インターフェースの定義の仕方
interface ICollidable
{
// メンバーメソッドの宣言(定義はできない)
bool IsCollide(GameObject other);
}
public class NewBehaviourScript : MonoBehaviour, ICollidable
{
// インターフェースの実装
public bool IsCollide(GameObject other)
{
if (gameObject.collider == null || other.collider == null)
return false;
return IsCollide(gameObject.collider, other.collider);
}
}
インターフェースはそれを継承したオブジェクトがどんなメン
バーメソッド、プロパティ、インデクサー、イベントを持つか
明言するためのもの。メンバーフィールドは宣言できず、実装
もできないため、単体では機能しない。
26/57
27. Learn C# in Unity
クラス or 構造体
・参照型と値型
クラスは参照型、構造体は値型です。この2つの違いは、メ
ソッドの引数や戻り値としてオブジェクトが渡される時の挙動
に影響します。
例えばオブジェクトFooからBarへ変数が渡される場合…
■参照型 ■値型
複製
Transform Vector3 Vector3
Foo Bar Foo Bar
27/57
28. Learn C# in Unity
クラス or 構造体
・値型の参照渡し
refキーワードやoutキーワードを使って、構造体のような値型
の参照を渡す方法もあります。
public class Foo
{
Vector3 myVector;
public AddVectorTo(ref Vector3 target)
{
target += myVector;
}
}
public class Bar
{
Foo foo = new Foo();
Vector3 myVector;
public Bar()
{
foo(ref myVector);
}
}
28/57
29. Learn C# in Unity
クラス or 構造体
・Null許容型
値型の変数でも、まだ初期化されていないことを明示したい場
合があります。そういう時はNull許容型を使います。
public class Foo
{
Vector3? myVector; // 変数の型の後ろに「?」を付けるとNull許容型になる
public Vector3 GetVector()
{
if (myVector == null)
myVector = Vector3.up;
return myVector.Value;
}
}
ただこれはデータベースとのやりとりを簡単にするために用意
されたものなので、Unityで使うことはあまりない。使わざるを
えないような状況になったらおそらく設計が悪い。
29/57
31. Learn C# in Unity
クラス or 構造体
・ボックス化・ボックス化解除
値型をobject型に変換することをボックス化、object型を元の
値型へキャストし直すことをボックス化解除といい、どちらも
とてもコストが高い操作なので極力避けるようにします(ボッ
クス化されるとインスタンスのサイズも増えます)。
object o = 1; // ボックス化
int i = (int)o; // ボックス化解除
// ArrayListは全ての要素をobject型に変換して格納するコレクションクラス
ArrayList list = new ArrayList();
list.Add("hoge"); // だから文字列も数値も同じArrayListに格納できるけど…
list.Add(100); // ←ここでボックス化が発生!気づきにくい!
後述するジェネリックという機能を使えば不要なボックス化を
減らせるので、積極的に使いましょう。
31/57
32. Learn C# in Unity
クラス or 構造体
・どちらを選ぶべきか
基本はクラスを使い、以下の条件を全て満たす場合のみ構造体
にします(とMicrosoftのドキュメントに書いてある…)。
1.整数や浮動小数点数のような論理的に単一な値を表す
2.インスタンスのサイズが16バイト未満である
3.継承により振る舞いを変更する必要がない
4.頻繁にボックス化する必要がない
例えば「二次元座標を表すPointオブジェクト」のようなもの
は構造体に向いています。
32/57
33. Learn C# in Unity
C#の便利な機能
・Enum:一連の定数を1つにまとめた型
public class Foo
{
// 関連のある定数群をこう書くよりも…
const int DamageTypeSlash = 1;
const int DamageTypeBlunt = 2;
const int DamageTypeExplosion = 3;
// enumにしたほうが見通しが良い
enum DamageType
{
Slash = 1, // 値を明言することもできる。明言しないと前の値+1になる。基底は0。
Blunt, // 直前の値が1なのでこいつは2。
Explosion,
}
DamageType damageType; // DamageType型のどれか1つの値を取るメンバーフィールド、という意味
// Enum型の各値には、「型名.名前」という形式でアクセスする。
public Foo() { damageType = DamageType.Blunt; }
// 整数へのキャストは保証されているが、あまり感心できる使い方ではない…
int GetDamageTypeID() { return (int)damageType; }
}
33/57
34. Learn C# in Unity
C#の便利な機能
・namespace:名前空間
namespace MyClasses //関連するオブジェクトを1つの名前空間に収めることで検索性を上げたり名前の衝突を防ぐ
{
public class Foo { }
public class Bar { }
}
// 名前空間の中の要素にはこのように書いてアクセス
MyClasses.Foo f;
// 別のファイルでも同じ名前空間を宣言することができる
namespace MyClasses
{
public class Hoge { }
}
// ファイルの先頭でusingディレクティブを使うと、アクセス時の名前空間を省略できる
using MyClasses;
Foo f;
ファイルの先頭でusing UnityEngine;とかやってるのはコレ
34/57
35. Learn C# in Unity
C#の便利な機能
・キャスト、型情報の利用
キャスト(型の明示的な変換)はCの書き方と似ています。
またis演算子、as演算子という型チェックの簡単な書き方があ
ります。
// 普通のキャストは「(目的の型)」で行う
Collider c = (Collider)gameObject.AddComponent("BoxCollider");
// is演算子でインスタンスの型が指定したものかどうかチェックできる
if (c is BoxCollider)
boxCollider = c as BoxCollider; // インスタンスを指定した型にキャスト、できなかったらnull
// 型情報を取り出して自分で判定することも
if (c.GetType() == typeof(BoxCollider))
print("OK");
// ジェネリックを使えばそもそもキャストはいらなかったり
c = gameObject.AddComponent<BoxCollider>();
35/57
36. Learn C# in Unity
C#の便利な機能
・var
型が明確なローカル変数は、型名をvarで代用することが可能
public class Foo
{
void Hoge()
{
// これは
Transform t = transform;
// こう書いてもいい
var t = transform;
// その1文ではvarが実際何の型になるのかわからない書き方はNG
var i;
i = 0;
}
public var i = 0; // これもNG。varが使えるのはローカル変数のみ。
}
36/57
37. Learn C# in Unity
C#の便利な機能
・パーシャルクラス・パーシャルメソッド
1つのクラスの定義を複数のファイルに分けて書くことが可能
public partial class Foo public partial class Foo
{ {
void Hoge() void Mage()
{ {
} }
} }
// もちろんクラスFooはHoge、Mage両方のメソッドをメンバに持っている
Foo f = new Foo();
f.Hoge();
f.Mage();
パーシャルクラスのメソッドにpartial修飾子を付けると、実装
を別のファイルで定義しても、定義しなくても(!)いい。
public partial class Foo
{
partial void Hoge();
}
37/57
39. Learn C# in Unity
C#の制御構文
・if文、for文、while・do~while文
他言語と比べて別段取り立てるような差異はありません。
contiue、break、gotoなども使えます。
・switch文
caseに文字列が使えたり、基本的にはbreakを省略できなかっ
たり、他言語と比べて少し特殊です。
switch (hoge)
{
case 0: // case文の間に処理が挟まらない場合だけフォールスルーできる
case 1: // caseを繋げて書いてあるので、hogeの値が0か1の場合、以下のDoSomething()が呼ばれる
DoSomething();
break; // これは省略できない(Cなどでは省略すると処理が下に繋がる)
case 2:
DoSomething2();
goto case 0; // 何か処理をした後どうしても他のcaseに進みたい場合はgotoを使う
default: // 上記のどのcaseにも当てはまらなかった場合、という書き方
DoNothing();
break;
}
39/57
40. Learn C# in Unity
C#の制御構文
・foreach文
配列やコレクションなどの全ての要素に対して繰り返し処理を
行います。
foreach (AudioSource a in GetComponents<AudioSource>())
{
a.Stop();
}
後述するLinqと組み合わせるとfor文がほとんど要らなくなる。
// アタッチされているAoudioSouceのうち、再生中の物に対してだけStop()を呼び出す、という例
foreach (var a in from a in GetComponents<AudioSource>() where a.isPlaying select a)
{
a.Stop();
}
ループ文の中でif文を使って分岐させると、ループ処理自体が長
くなってしまうので、可能ならLinqであらかじめリストを整形
してからforeach文で処理すると見通しが良くなります。
40/57
41. Learn C# in Unity
C#の制御構文
・try~catch~finally文
C#での例外処理を制御する構文だけど、Unityが例外をほとん
ど意識しなくて良い設計になっているのであまり使いません。
StreamReader sr = null;
try
{
sr = File.OpenText(path);
string s = "";
while ((s = sr.ReadLine()) != null)
print(s);
}
catch (FileNotFoundException e)
{
print("File Not Found!");
}
finally
{
sr.Dispose();
}
41/57
42. Learn C# in Unity
C#の制御構文
・using文
名前空間の省略や型の別名定義にもusingというキーワードを
使うので混乱しやすいけど、メソッド内に出てくるusing文は
Dispose()の呼び出しを保証するもの。UnityEngineには
IDisposableを実装したクラスがないのであまり使わない…。
■using版 ■非using版
using (StreamReader sr = File.OpenText(path)) StreamReader sr = null;
{ try
string s = ""; {
while ((s = sr.ReadLine()) != null) sr = File.OpenText(path);
print(s); string s = "";
} while ((s = sr.ReadLine()) != null)
print(s);
}
finally
{
sr.Dispose();
}
42/57
43. Learn C# in Unity
ジェネリック
・ジェネリックとは
不要なキャストを減らし、コンパイル時に型チェックができ、
コードが最適化されパフォーマンスが良くなる、大変素晴らし
い機能です。大変素晴らしいです。
BoxCollider c;
// 非ジェネリック版(長い。しなくてもいいキャストは見苦しい…)
c = (BoxCollider)gameObject.AddComponent(typeof(BoxCollider));
c = gameObject.AddComponent(typeof(BoxCollider)) as BoxCollider;
// ジェネリック版
c = gameObject.AddComponent<BoxCollider>();
しかしUnityはジェネリック版のメソッドをあまり用意していな
い…ぐぬぬ…。
43/57
44. Learn C# in Unity
ジェネリック
・自分でジェネリック対応する
// 例えばこんなメソッドをジェネリック版にする場合
Object FindObjectOf(Type type)
{
return FindObjectOfType(type);
}
// 型パラメータの名前をTとしてあるのは慣習(スタンダードなルールがある)
T FindObjectOf<T>() where T: Component // 型パラメータに制限を課すことができる(任意)
{
return (T)FindObjectOfType(typeof(T));
}
// 上記ジェネリックメソッドの呼び出し方
var renderer = FindObjectOf<Renderer>(); // 型が自明なのでvarが使えるしキャストもない!
// 型パラメーターに制限をかけてあるので以下はコンパイル時エラー。素晴らしい!
int i = FindObjectOf<int>(); // intはComponentを継承していない
44/57
45. Learn C# in Unity
ジェネリック
・非ジェネリックコンテナは使うな
ボックス化のところで説明したように、ArrayListなどの非ジェ
ネリックコンテナは、知らないうちにパフォーマンスが悪化し
たり、本来必要ないキャストが増えたりしてよろしくありませ
ん(キャストが増えるとバグが増える)。
明確な理由がない場合は、System.Collections.Generic名前
空間にある、ジェネリック版のコンテナを使いましょう。
using System.Collections.Generic;
List<int> list;
list.Add(100); // ボックス化無し!
list.Add("hoge"); // コンパイル時エラー!
int i = list[0]; // もちろんボックス化解除も無し!
45/57
46. Learn C# in Unity
デリゲート
・メソッドを参照できる型
メソッドの参照を保存しておいて後から呼び出したり、複数の
メソッドを1つの変数に入れておいてまとめて呼び出せるよう
にする機能です。
// デリゲート型の定義
delegate void SomeDelegate(float f);
// 定義した型の変数を宣言
SomeDelegate someDelegate;
// 例えばこんなメソッドを先ほどの変数へ代入することができる
void Hoge(float value) { } // 型さえあってれば引数の名前は同じじゃなくてもいい
someDelegate = Hoge;
// インスタンスメソッドも代入できる
public class Foo
{
public void Bar(float amount) { }
}
Foo f = new Foo();
someDelegate += f.Bar; // 「+=」で追加代入できる
// デリゲート変数に格納されているメソッドを呼ぶ(Hogeとf.Barが順番に呼ばれる)
someDelegate(1.0f);
46/57
52. Learn C# in Unity
LINQ
・データベース向けに導入された機能だけど
要するに大量のデータの中から必要な物だけ抽出したり、複数
のリストをマージしたり、データの塊を整形するための機能で
す。うまく使うとコードが非常にすっきりする。
// 例えばこんなループは
var renderers = GetComponentsInChildren<Renderer>();
foreach (var r in renderers)
{
if (r != null && r.material != null)
r.material.mainColor = Color.red;
}
// LINQだとこう書ける
var materials = from r in GetComponentsInChildren<Renderer>()
where r != null && r.material != null
select r.material;
foreach (Material m in materials)
m.mainColor = Color.red;
ループの中で分岐するより先にリストを整形してしまった方が
間違いが起こりにくい。
52/57
53. Learn C# in Unity
LINQ
・匿名クラスと組み合わせるとさらに強力に
注目したいメンバーだけ抽出した匿名クラスを作って返せば、
余計なメンバーにアクセスしてバグることもなくなる。
var materialSets = from Renderer r in FindObjectsOfType(typeof(Renderer))
where r.material != null && r.sharedMaterial != null
select new { r.material, r.sharedMaterial };
foreach (var m in materialSets)
{
m.material.mainTexture = texture;
m.sharedMaterial.mainTexture = texture;
}
ただし軽い処理ではないので使いどころに注意する必要がある
53/57
54. Learn C# in Unity
コルーチン
・C#のイテレーター構文を流用している
C#のyield returnの本来の目的は、イテレータを気軽に実装す
るためのものです。途中まで処理して一旦メソッドを抜けて、
次またそこ(メソッドの途中)から再開できる、という特性が
マルチタスク処理に向いていたので、Unityではコルーチンに流
用されたようです。コルーチンはC#の機能ではありません。
// 本来の使い方
IEnumerator Count10()
{
for (int i = 10; i >= 0; --i)
yield return i;
}
foreach (var i in Count1to10())
{
print(i); // 10, 9, 8, 7...
}
54/57
55. Learn C# in Unity
コルーチン
・コンパイル時に暗黙のクラスが作られている
yield文が含まれたメソッドは、実はコンパイル時に暗黙の列挙
用クラスに変換されています。
IEnumerator CountUp() class CountUpEnumerator : IEnumerator
{ {
print(1); int state = 0;
yield return null; bool MoveNext()
print(2); {
yield return null; switch (state++)
print(3); {
} case 0:
print(1);
return null;
case 1:
print(2);
return null;
case 2:
print(3);
}
}
}
IEnumerator CountUp()
{
return new CountUpEnumerator();
}
55/57
56. Learn C# in Unity
コルーチン
・コルーチンはMoveNextを毎フレーム呼んでるだけ
おそらくGameObjectが今実行中のコルーチン(先ほどの列挙
用クラスのインスタンス)のリストを持っていて、毎フレーム
それらのMoveNext()メソッドを呼んでいるだけ。似たような
処理を自分でも書ける。コルーチンごとに停止再開などを管理
したい場合は、こっちのほうが柔軟性が高い。
public class Foo : MonoBehaviour
{
List<IEnumerator> coroutines = new List<IEnumerator>();
void Start()
{
coroutines.Add(MyCoroutine1());
coroutines.Add(MyCoroutine2());
}
void Update()
{
coroutines.RemoveAll(c => !c.MoveNext());
}
}
56/57