3rd term 3rd week - dsuz/csharp GitHub Wiki
- アップキャスト ダウンキャスト
- ボクシング アンボクシング
-
イテレーター
- IEnumerator(とコルーチンの関係)
- IEnumerable(と Collectionの関係)
CSharp3-3.unitypackage をダウンロードして Unity のプロジェクトにインポートする。Assets/3-3 IEnumerator IEnumerable/ 以下に今回の内容がある。
あるクラスのインスタンスは、その基底クラス型の変数に代入することができる。これをアップキャストという。Unity では、以下のような場面でよく使われる。
- BoxCollider, SphereCollider, MeshCollider などのコンポーネントは Collider クラスを継承しているので、Collider クラスとして扱える。例えば OnTriggerEnter メソッドの引数の型は Collider であるが、この引数には BoxCollider, SphereCollider, MeshCollider などの変数が渡されてくる。「Collider クラス」そのものは Component クラスを継承してはいるが、GameObject にアタッチするコンポーネントとしては使えない。つまり、Add Component ボタンを押しても選択肢には出てこない。
- SpriteRenderer, MeshRenderer, LineRenderer などのコンポーネントは Renderer クラスを継承しているので、Renderer 型の変数に入れることができる。
特に、値型の変数(つまり構造体)を参照型にキャストすることをボクシング、その逆をアンボクシングという。
- アップキャスト・ダウンキャスト
- 独習 C#
- 8.2.7 - 参照型における変換
- 8.2.8 - 型の判定
- 独習 C#
- ボクシング・アンボクシング
- 独習 C# 9.6.5 - ボクシングとアンボクシング
IEnumerator は繰り返し処理(反復処理)をサポートするインターフェイスである。IEnumerator は MoveNext() というメソッドを持つ。IEnumerator を戻り値とするメソッドを呼び、戻り値を受け取ってそのインスタンスの MoveNext() を呼ぶと、関数内の次の yield return まで処理が実行される。つまり、一つのメソッド内の処理が MoveNext() を呼ぶ度に分割して実行される。MoveNext() を呼ぶ度に、前回の続きから処理が実行される。
教材のサンプルシーン "IEnumerator IEnumerable" を実行して「IEnumerator のみを使った非同期処理」ボタンをクリックすると、非同期で(時間のかかる)素数判定処理が実行される。アニメーションが止まらないことにより非同期であることがわかる。この処理はコルーチン等を使わずに非同期を実現している。IEnumerator の特性を利用して、時間のかかる素数判定処理のループを「Update() が実行される度に一回ずつ」回すことにより疑似的に並列処理を実現している。
Unity のコルーチンはこのような処理(Update() ごとの MoveNext())を内部的に隠すようなやり方で実現している。
ただし処理によっては、このやり方だとパフォーマンスが落ちる(完了までに時間がかかる)。今回の素数判定の処理においては、非同期的に行った素数判定にかかる時間と、同期処理した素数判定では、非同期的に処理した方が圧倒的に時間がかかることによりわかる。これは Update() ごとに MoveNext() を行っているためで、Update() が呼ばれる頻度がフレームレートであるためである。
コルーチンを使った場合も、同様にパフォーマンスが落ちることから、Unity は内部的に Update() で MoveNext() を呼んでいるであろうことが推測できる。
IEnumerable は反復処理をサポートする列挙子を公開するインターフェイスである。というと難しいが、この型の値を foreach ループの in の後ろに置くことで、各要素に対して処理を実行し、最後の要素に対する処理が終わったらループを抜けるという処理を書ける。
foreach ループの in の後ろには、配列・List・Dictionary 等の Collection を置くことができる。この事は言い換えると、「in の後ろには IEnumerable インターフェイスを実装した型の値を置くことができる。Collection は IEnumerable を実装している。IEnumerable を実装していない型の値は in の後ろに置くことはできない。」ということである。
Linq を使った時には戻り値に IEnumerable がよく出てくる。以下のようなケースでは前述した IEnumerable の使い方をすることがある。
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
List<int> list = new List<int>() { 0, 3, 7, 2, 4, 8 };
// 偶数のみを抽出する
var result = list.Where(i => i % 2 == 0); // 戻り値の型は IEnumerable<int> であり、List<int> でも int[] でもない
foreach (int n in result) // IEnumerable<int> のままでも foreach の in の後ろに置ける
{
Console.WriteLine(n);
}
// もちろん、result.ToList() / result.ToArray() を使って List や配列に変換してから foreach で処理しても構わない(処理としては無駄があるが)
}
}foreach では処理対象のコレクションを(おそらく)IEnumerable にアップキャストして処理しているのだろう。断定を避けているのは、本当にそうなっているのかは外側からはわからないからである。
IEnumerable を意識したコーディングとしては、配列やリストなどを引数として受け取りたい時、引数の型を List や int[] にはせず、以下のように IEnumerable として受け取ると、どちらで渡されても処理できるように書ける。
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
int[] array = { 0, 3, 7, 2, 4, 8 };
List<int> list = array.ToList();
Test<int>(array); // どちらでも呼べる
Test<int>(list); // どちらでも呼べる
Test(array); // 手前の <int> は省略可
Test(list); // 手前の <int> は省略可
}
// 引数の型に IEnumerable を指定する
static void Test<T>(IEnumerable<T> col)
{
}
}- 独習 C#
- 7.6.9 - イテレーター