3rd term 3rd week - dsuz/csharp GitHub Wiki

今回のテーマ

  • アップキャスト ダウンキャスト
    • ボクシング アンボクシング
  • イテレーター
    • IEnumerator(とコルーチンの関係)
    • IEnumerable(と Collectionの関係)

準備

CSharp3-3.unitypackage をダウンロードして Unity のプロジェクトにインポートする。Assets/3-3 IEnumerator IEnumerable/ 以下に今回の内容がある。

アップキャスト・ダウンキャスト

あるクラスのインスタンスは、その基底クラス型の変数に代入することができる。これをアップキャストという。Unity では、以下のような場面でよく使われる。

  1. BoxCollider, SphereCollider, MeshCollider などのコンポーネントは Collider クラスを継承しているので、Collider クラスとして扱える。例えば OnTriggerEnter メソッドの引数の型は Collider であるが、この引数には BoxCollider, SphereCollider, MeshCollider などの変数が渡されてくる。「Collider クラス」そのものは Component クラスを継承してはいるが、GameObject にアタッチするコンポーネントとしては使えない。つまり、Add Component ボタンを押しても選択肢には出てこない。
  2. SpriteRenderer, MeshRenderer, LineRenderer などのコンポーネントは Renderer クラスを継承しているので、Renderer 型の変数に入れることができる。

特に、値型の変数(つまり構造体)を参照型にキャストすることをボクシング、その逆をアンボクシングという。

参考資料

  • アップキャスト・ダウンキャスト
    • 独習 C#
      • 8.2.7 - 参照型における変換
      • 8.2.8 - 型の判定
  • ボクシング・アンボクシング
    • 独習 C# 9.6.5 - ボクシングとアンボクシング

イテレーター

IEnumerator

IEnumerator は繰り返し処理(反復処理)をサポートするインターフェイスである。IEnumerator は MoveNext() というメソッドを持つ。IEnumerator を戻り値とするメソッドを呼び、戻り値を受け取ってそのインスタンスの MoveNext() を呼ぶと、関数内の次の yield return まで処理が実行される。つまり、一つのメソッド内の処理が MoveNext() を呼ぶ度に分割して実行される。MoveNext() を呼ぶ度に、前回の続きから処理が実行される。

コルーチンとの関係

教材のサンプルシーン "IEnumerator IEnumerable" を実行して「IEnumerator のみを使った非同期処理」ボタンをクリックすると、非同期で(時間のかかる)素数判定処理が実行される。アニメーションが止まらないことにより非同期であることがわかる。この処理はコルーチン等を使わずに非同期を実現している。IEnumerator の特性を利用して、時間のかかる素数判定処理のループを「Update() が実行される度に一回ずつ」回すことにより疑似的に並列処理を実現している。

Unity のコルーチンはこのような処理(Update() ごとの MoveNext())を内部的に隠すようなやり方で実現している。

ただし処理によっては、このやり方だとパフォーマンスが落ちる(完了までに時間がかかる)。今回の素数判定の処理においては、非同期的に行った素数判定にかかる時間と、同期処理した素数判定では、非同期的に処理した方が圧倒的に時間がかかることによりわかる。これは Update() ごとに MoveNext() を行っているためで、Update() が呼ばれる頻度がフレームレートであるためである。

コルーチンを使った場合も、同様にパフォーマンスが落ちることから、Unity は内部的に Update() で MoveNext() を呼んでいるであろうことが推測できる。

IEnumerable

IEnumerable は反復処理をサポートする列挙子を公開するインターフェイスである。というと難しいが、この型の値を foreach ループの in の後ろに置くことで、各要素に対して処理を実行し、最後の要素に対する処理が終わったらループを抜けるという処理を書ける。

Collection との関係

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 - イテレーター
⚠️ **GitHub.com Fallback** ⚠️