vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

【Unity】DOTweenで画像を点滅させるメモ

はじめに

必要になったのでメモ。

やりたいこと

  • 何かの操作をトリガーに、一定時間で Image を Alpha 0 -> Image のデフォルトの Alpha 値に戻すアニメーションを実行
  • 何かの操作をトリガーに、一定時間で Image の Scale を大きくするアニメーションを実行

今回は DOTween を使用しました。

http://dotween.demigiant.com/index.php

コード

using DG.Tweening;
using UnityEngine;
using UnityEngine.UI;

public class BlinkController : MonoBehaviour
{
    // 点滅対象.
    public Image Limited;

    // アニメーション開始のトリガー.
    public Button StartButton;

    private const float AnimeTimeSec = 1.0f;
    private const int AnimeRepeatCount = 3;
    private int count;
    private float defaultAlpha;
    private Vector3 defaultScale;
    private Vector3 largeScale;

    private void Start ()
    {
        defaultAlpha = Limited.color.a;
        defaultScale = Limited.rectTransform.localScale;

        largeScale = new Vector3(
            defaultScale.x * 1.2f,
            defaultScale.y * 1.2f,
            defaultScale.z * 1.2f);

        StartButton.onClick.AddListener(() =>
        {
            // 実行中のアニメーションをリセット.
            count = 0;
            DOTween.Clear();
            // 点滅アニメーション.
            StartBlinking();
        });
    }

    private void StartBlinking()
    {
        var startColor = Limited.color;
        startColor.a = 0f;
        Limited.color = startColor;

        Limited.rectTransform.localScale = defaultScale;

    // 引数1: アニメーション完了後の Scale 引数2: アニメーションの実行時間.
        Limited.rectTransform.DOScale(largeScale, AnimeTimeSec)
            .SetEase(Ease.Linear);

    // 引数1: Alpha 値変更対象の Color(Getter) 引数2: Setter.
    // 引数3: アニメーション完了後の Alpha 引数4: アニメーションの実行時間.
        DOTween.ToAlpha(() => Limited.color, value => Limited.color = value, defaultAlpha, AnimeTimeSec)
            .SetEase(Ease.Linear)
            .OnComplete(() =>
            {
                count += 1;
                if (count < AnimeRepeatCount)
                {
                    // 一定回数繰り返し.
                    StartBlinking();
                }
                else
                {
          // 完了後の処理が不要なら Else ごと削除しても OK.
                    Debug.Log("finished");
                }
            });
    }
}

非常にシンプルにできてよいですね。

タイマーを追加する

ついでに、ボタンが押されたらタイマーを起動して、一定時間ごとにアニメーションを繰り返すようにしてみます。

using System.Diagnostics;
using DG.Tweening;
using UnityEngine;
using UnityEngine.UI;
using Debug = UnityEngine.Debug;

public class BlinkController : MonoBehaviour
{
    public Image Limited;

    public Button StartButton;
    // アニメーション、タイマーをリセット.
    public Button StopAllButton;

    private const long RepeatAnimeTimeMillisec = 5000L;
    private const float AnimeTimeSec = 1.0f;
    private const int AnimeRepeatCount = 3;
    private int count;
    private float defaultAlpha;
    private Vector3 defaultScale;
    private Vector3 largeScale;

    private bool timerSet;

    private readonly Stopwatch watch = new Stopwatch();

    private void Start ()
    {
        defaultAlpha = Limited.color.a;
        defaultScale = Limited.rectTransform.localScale;

        largeScale = new Vector3(
            defaultScale.x * 1.2f,
            defaultScale.y * 1.2f,
            defaultScale.z * 1.2f);

        StartButton.onClick.AddListener(Play);
        StopAllButton.onClick.AddListener(StopAll);
    }

    private void Update()
    {
        if (timerSet)
        {
            if (watch.ElapsedMilliseconds > RepeatAnimeTimeMillisec)
            {
        // 一定時間が経過したらアニメーション開始.
                Play();
            }
        }
    }
  // タイマーの開始は最初のアニメーション実行後とする.
    private void Play()
    {
        timerSet = false;
        watch.Stop();
        count = 0;
        DOTween.Clear();
        StartBlinking();
    }
  // アニメーション、タイマーをリセット.
    private void StopAll()
    {
        timerSet = false;
        watch.Stop();
        count = 0;
        DOTween.Clear();
    }
    private void StartBlinking()
    {
        var startColor = Limited.color;
        startColor.a = 0f;
        Limited.color = startColor;

        Limited.rectTransform.localScale = defaultScale;

        Limited.rectTransform.DOScale(largeScale, AnimeTimeSec)
            .SetEase(Ease.Linear);
        DOTween.ToAlpha(() => Limited.color, value => Limited.color = value, defaultAlpha, AnimeTimeSec)
            .SetEase(Ease.Linear)
            .OnComplete(() =>
            {
                count += 1;
                if (count < AnimeRepeatCount)
                {
                    StartBlinking();
                }
                else
                {
                    // アニメーションが終わったらタイマー起動.
                    timerSet = true;
                    watch.Reset();
                    watch.Start();
                }
            });
    }
}

参照

Xubuntu17.10 に OpenCV3.2.0を入れようとしたときのメモ

はじめに

え~、諸事情とちょっとした興味から、 XubuntuOpenCV をインストールしてみることにしました。

なんかはるか昔に同じようなことをやった気がするのですが、 やってみるとバージョンの違いからかエラー出しまくりといった感じであったので、メモっておきます。

なお 2/6 時点では CMake 、 make が完了した(っぽい)というだけで、サンプルを動かしたりはできていない状態です。

また後述しますが、ググったときに見つかった情報からインストールしたものが含まれており、 今回インストールしたものすべてが必要かどうかは不明です。

この辺りは、サンプルのビルドやインストールのし直しなどを行う予定なので、
間違いなどを見つけたら順次修正していく予定です。

ということで懺悔おしまい。

インストールしたソフトウェア

とりあえずテキストエディタをインストールします。

Vim とか Visual Studio Code とか。

CMakeのインストール

まずは CMake をインストールすることにします。

  1. CMake のソース(cmake-3.10.2.tar.gz)を https://cmake.org/download/ からダウンロードして任意の場所に展開します。
  2. ビルドに必要なソフトウェアをインストールします。
sudo apt install gcc g++ build-essential git libboost1.63 make libeigen3-dev
  1. ターミナルで Step1. のフォルダに移動して、ビルドします。
./bootstrap && make && sudo make install

OpenCVのビルドに必要そうなファイルをインストール

以前の記事などを参考に、OpenCV のビルドに必要そうなファイルをインストールします。

ただしバージョンの違いのためか、見つからなかったものは除外しています。

この内容はもう少しちゃんと確認が必要ですね(;'∀')。

sudo apt install ffmpeg libopencv-dev libgtk-3-dev python-numpy python3-numpy libdc1394-22 libdc1394-22-dev libjpeg-dev libtiff5-dev libavcodec-dev libavformat-dev libswscale-dev libxine2-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libv4l-dev libtbb-dev qtbase5-dev libfaac-dev libmp3lame-dev libopencore-amrnb-dev libopencore-amrwb-dev libtheora-dev libvorbis-dev libxvidcore-dev x264 v4l-utils unzip

何はともあれ、OpenCV 3.2.0 のソースをダウンロードして、任意の場所に展開します。

https://github.com/opencv/opencv/releases

ググって得た情報によると、展開したフォルダ内に build というフォルダを作成し、その中で cmake . を実行しているものがありました。

実際に実行してみると、 build フォルダ内に依存ファイルなどが複製されていたため、 元のファイルをそのまま置いておくためなのかなぁ、と勝手に思っています。

cmake ../

VTKのインストール

で、 cmake を実行してみると VTK が見つからないとエラーになりました。

そのため、 https://www.vtk.org/download/ から VTK(8.1.0)をダウンロードして展開し、 cmake . を実行したところ、エラーになりました。

どうも必要なソフトウェアが足りていなかったようです。

sudo apt install libxt-dev

【参考】 https://stackoverflow.com/questions/23528248/how-to-install-x11-xt-lib-when-configure-vtk

これでビルドはできたのですが、古い EeePC で実行したためか、完了まで 3, 4 時間はかかりました。。。

ICV

今度こそ!と思うもまたエラーが発生しました。

今度は ippicv_linux_20151201.tgz のダウンロード?に失敗しているようです。

ググってみたところ、下記のような情報が見つかりました。

CMake から https に接続できてない。。。?

対処法としては、 OpenCV のフォルダ内にある、 3rdParty > ippicv フォルダの中身を手動でダウンロードしてきた ippicv_linux_20151201.tgz に差し替える、 というものがあるようです。

一応 CMake はこれで完了し、 make を実行すると warning が途中途中で出はしたものの完了できたようです。

色々雑すぎるのでもう少し確認も必要なのですが、まずは本当に正しくインストールできているかの確認からかなぁ。

ゴリラ本メモ その1 - 4章

はじめに

ゴリラ本でおなじみ(?) 実例で学ぶゲーム3D数学 に再挑戦してみたくなりました。 (たぶん表紙の絵はゴリラではなくマンドリルっぽいのですが)

この本買ったは良いものの わかるわかる -> おや? -> 全然わからないorz -> 挫折 となっていたのですが、
「もう死んだ羊の話ばっかり読むのはいやじゃぁ!」と思いましたので、何とか前の挫折ポイントよりもう少し読み進めたいと思っています。

で、よくわからないまま読み進めてしまうのを防ぐために、
紹介されている式や例をコードとして書き起こして、計算結果を確認してみることにしました。

ベクトルの計算は、 Unity では Vector2 や Vector3 を使うことができます。

また、 NuGet で System.Numerics.Vectors をインポートすることで、
Unity 以外の C# アプリケーションでもベクトル計算ができます。

今回は大部分で System.Numerics.Vectors を使ってみることにしました。

理由? 興味があったからですw

なお今回は 4 章 ベクトルの演算 の、4.8.3 ~ 4 章終わりまでを取り上げます。

凄く中途半端なところから始める理由は、思いついたのが 4 章途中からであったこと、
4 章頭の部分で特にコードに置き換えて面白そうだと感じたところが少なかったためです。

UnityとSystem.Numerics.Vectors

今回登場するメソッドやクラスは、Unity 、 System.Numerics.Vectors ともにほぼ同じです。
(中の処理は違いがあったりするかもしれませんが)

違いとして気づいたのは下記の2つくらいです。

  • Vector2 の 変数 x, y (Vector3 なら x, y, z) が、Unity は小文字、 System.Numerics.Vectors は大文字
  • ベクトルの大きさを取得するのは Unity なら magnitude 、 System.Numerics.Vectors なら Length()

ベクトルから距離を計算する

さて、それではまず、 ある点(点A)から別の点(点B)へのベクトル(ベクトルD)を求めてみます。

原点から点AまでのベクトルをベクトルA、点BまでのベクトルをベクトルBとして、
ベクトルBからベクトルAを引きことで算出できます。

ベクトルDの大きさは、ベクトルDの大きさ(斜辺)の二乗 = 底辺(X)の二乗+高さ(Y)の二乗であることから算出する方法1、Length() を使う方法2があります。

計算用クラス

using System;
using System.Numerics;

public class DistanceSample {
    // 点Aから点Bまでのベクトル(ベクトルD)を取得する 2D
    public Vector2 GetVector(Vector2 pointA, Vector2 pointB) {
        return pointB - pointA;
    }
    // 点Aから点Bまでのベクトル(ベクトルD)を取得する 3D
    public Vector3 GetVector(Vector3 pointA, Vector3 pointB) {
        return pointB - pointA;
    }
    // ベクトルDの大きさを取得する 方法1 2D
    public float GetDistance1(Vector2 point) {
        // Dの長さの二乗 = 底辺(X)の二乗+高さ(Y)の二乗
        var squareDistance = Math.Pow(point.X, 2) + Math.Pow(point.Y, 2);
        var rootDistance = Math.Sqrt(squareDistance);
        return (float) rootDistance;
    }
    // ベクトルDの大きさを取得する 方法2 2D
    public float GetDistance2(Vector2 point) {
        return point.Length();
    }
    // ベクトルDの大きさを取得する 方法1 3D
    public float GetDistance1(Vector3 point) {
        var squareDistance = Math.Pow((double)point.X, 2) + Math.Pow((double)point.Y, 2) + Math.Pow((double)point.Z, 2);
        var rootDistance = Math.Sqrt(squareDistance);
        return (float)rootDistance;
    }
    // ベクトルDの大きさを取得する 方法2 3D
    public float GetDistance2(Vector3 point) {
        return point.Length();
    }
}

呼び出し元

class Program {
    static void Main(string[] args) {
        var pointA = new Vector2(-1, 8);
        var pointB = new Vector2(5, -4);

        var distanceSample = new DistanceSample();

        Vector2 vectorD = distanceSample.GetVector(pointA, pointB);
        float distance1 = distanceSample.GetDistance1(vectorD);
        float distance2 = distanceSample.GetDistance2(vectorD);
    }
}

結果は ベクトルD: Vector2(6, -12) 距離: 13.41641 となりました。

詳しい理屈などは書籍参照ということでm(__)m

内積を計算する

さぁどんどんいきましょう。

次は内積です。

ゴリラ本によると「2つのベクトルがどれぐらい似ているか」を表す、
ということでしたが、それだけだと今一つ分かりませんでしたorz

内積が必要となる例としては、下記の「物体になされた仕事量」や、物体の表面の明るさ、
といったものがわかりやすいと感じました。

内積はベクトル A 、 B の各要素同士を掛けて、それを足し合わせることで算出できます(方法1)。

また、ベクトル A 、 B の大きさと、ベクトル A 、 B のなす角度 θ のコサインをそれぞれかけることでも算出できます。(方法2)

さらに、 Unity や System.Numerics.Vectors の場合は内積を計算してくれる、 Dot() というメソッドもあります。(方法3)

計算用クラス

using System.Numerics;

public class InnerProductSample {
    // 内積を算出する 方法1 2D
    public float GetInnerProduct1(Vector2 pointA, Vector2 pointB) {
        return (pointA.X * pointB.X) + (pointA.Y * pointB.Y);
    }
    // 内積を算出する 方法1 3D
    public float GetInnerProduct1(Vector3 pointA, Vector3 pointB) {
        return (pointA.X * pointB.X) + (pointA.Y * pointB.Y) + (pointA.Z + pointB.Z);
    }
    // 内積を算出する 方法2 2D
    public float GetInnerProduct2(Vector2 pointA, Vector2 pointB, float cosTheta) {
        return pointA.Length() * pointB.Length() * cosTheta;
    }
    // 内積を算出する 方法2 3D
    public float GetInnerProduct2(Vector3 pointA, Vector3 pointB, float cosTheta) {
        return pointA.Length() * pointB.Length() * cosTheta;
    }
    // 内積を算出する 方法3 2D
    public float GetInnerProduct3(Vector2 pointA, Vector2 pointB) {
        return Vector2.Dot(pointA, pointB);
    }
    // 内積を算出する 方法3 3D
    public float GetInnerProduct3(Vector3 pointA, Vector3 pointB) {
        return Vector3.Dot(pointA, pointB);
    }
}

呼び出し元

class Program {
    static void Main(string[] args) {
        var pointA = new Vector2(3, 4);
        var pointB = new Vector2(8, 3);

        var innerProductSample = new InnerProductSample();

        var innerProduct1 = innerProductSample.GetInnerProduct1(pointA, pointB);
        // var innerProduct2 = ??
        var innerProduct3 = innerProductSample.GetInnerProduct3(pointA, pointB);
    }
}

方法2について

方法1、方法3については特に問題ないと思います。

ただ方法2で算出しようとすると、 Cosθ の値が必要になってきます。

θ の角度がわかっている場合、または点Aまたは点Bの、X または Y が 0 であれば問題なさそうですが。。。

なお、ベクトルAとベクトルBの大きさを掛けたもので内積を割ると、 Cosθ を算出できます。

var theta = Math.Acos(innerProduct1 / (pointA.Length() * pointB.Length()));
// radian -> 角度への変換.
var angle = theta * 180d / Math.PI;

すでに何をしたいかわからなくなってきてますが/(^o^)\

投影

θ の角度を求める方法として、投影を使う方法もあります。

点AからベクトルBに対して、垂直になるような線を引いたときにできる三角形の底辺となる ベクトル A|| を用います。

f:id:mslGt:20180201075600p:plain

詳しい計算は書籍を参照していただくとして(そればっかり)、結果としては下記のようになります。

var aLength = pointA.Length();
var bLength = pointB.Length();
var calc = (float) (innerProduct1 / Math.Pow(bLength, 2));
var vBottom = new Vector2 {
    X = pointB.X * calc,
    Y = pointB.Y * calc
};

var theta2 = Math.Acos((double)vBottom.Length() / (double)aLength);
var angle2 = theta2 * 180d / Math.PI;

var innerProduct2 = innerProductSample.GetInnerProduct2(pointA, pointB, (float)Math.Cos(theta2));

外積を計算する

次は外積です。

外積は 3D のみで計算でき(2Dでは計算不可)、ベクトルA、ベクトルBを通る平面に対して垂直のベクトルが得られます。

ということで、両方の Z を 0 にすると、X 方向が 0 のベクトルが得られるはず。

var pointA = new Vector3(3, 4, 0);
var pointB = new Vector3(8, 3, 0);

var outerProductSample = new OuterProductSample();

var outerProduct1 = outerProductSample.GetOuterProduct1(pointA, pointB);
var outerProduct2 = outerProductSample.GetOuterProduct2(pointA, pointB);

結果は (0, 0, -23) となりました。
考えは間違っていなさそうです。

また、外積の大きさはベクトルA・ベクトルBの大きさと、 Sinθ を掛けたものと等しいということです。

というわけで計算してみます。

var pointA = new Vector3(3, 4, 2);
var pointB = new Vector3(8, 3, 5);

var outerProductSample = new OuterProductSample();

var outerProduct1 = outerProductSample.GetOuterProduct1(pointA, pointB);
var outerProduct2 = outerProductSample.GetOuterProduct2(pointA, pointB);


var radian = Math.Acos(
    Vector3.Dot(pointA,pointB) / (pointA.Length() * pointB.Length()));

var length1 = outerProduct1.Length();

var length2 = pointA.Length() * pointB.Length() * Math.Sin(radian);

結果は length1 が 26.9443874 、 length2 が 26.94438715941152 でした。あれ?

う~ん、完全に正確な値が必要でないのなら小数点第二位くらいで丸めて使うのがよさそうです。

おわりに

やっぱりコードであったり、何かの形に落として実際に計算してみる、確かめてみる、というのは大事ですね。

本当は全部自分で計算した方が。。。と思わないでもないですが、心が折れそうなのでいったん後回しに。。。

Unity にしろ System.Numerics.Vectors にしろ、便利なメソッドが用意されているため、
通常はそれを使えばよいわけですが、どういう理屈でそのように計算されるのか、
ということが少しでもわかっていると、うまくいかない場合や応用が必要な時などに役立つのではないかと。

次は5章をとばして6章の行列を試してみる予定です。

5章はベクトルクラスを自作する?内容で、実際にそれを使うタイミングで見た方がわかりやすい気がするので。。。

話が別方向にずれて帰ってこなくなったら。。。お察しくださいm(__)m

参照

内積

外積

【C#】【Unity】DictionaryとListの速度比較

はじめに

ここ最近 Unity 2017 Game Optimization を読んでいるのですが、
その中の一つに格納したデータを検索したりするなら List を使うより Dictionary を使うと良いよ、というものがありました。

これはどんな検索の仕方でも早くなるの?など気になったので調べてみました。

サンプルデータ

Dictionary は Key と Value を1対で持ちます。

List でも同じようにデータを対で持たせるため、下記のようなクラスを使うことにします。

SampleValue.cs

public class SampleValue {
    public int Id { get; set; }
    public string Category { get; set; }
}

で、データは下記のようにセットします(検証内容によって途中で変えますが)。

DictionarySample.cs

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Profiling;
using UnityEngine.UI;
using Debug = UnityEngine.Debug;

public class DictionarySample: MonoBehaviour {
    public Button ListButton;
    public Button DictionaryButton;        
    private List< SampleValue> sampleList;
    private Dictionary< int, string> sampleDictionary;
        
    private void Start() {
        sampleList = new List< SampleValue>();
        sampleDictionary = new Dictionary< int, string>();

        var categoryNum = 0;
        for (var i = 0; i < 100000; i++) {
            var newValue = new SampleValue {
                Id = i,
                Category = "Category" + categoryNum
            };
            sampleList.Add(newValue);
            sampleDictionary.Add(i, "Category" + categoryNum);

            categoryNum++;
            if (categoryNum > 5) {
                categoryNum = 0;
            }
        }
        ListButton.onClick.AddListener(() => {
            Profiler.BeginSample("SamplingProfile");
           
            // 検証用の処理呼び出し.
            OutputListValues();
                
            Profiler.EndSample();
        });
        DictionaryButton.onClick.AddListener(() => {
            Profiler.BeginSample("SamplingProfile");

            // 検証用の処理呼び出し.
            OutputDictionaryValues()

            Profiler.EndSample();
        });
    }        
}

計測1. 格納したデータをそのまま取り出す

まずは格納したデータを、検索などせずにそのまま取り出してみます。

DictionarySample.cs

~省略~
private void OutputListValues() {
    foreach (var s in sampleList) {
        result = s.Category;
    }
}
private void OutputDictionaryValues() {
    foreach (var s in sampleDictionary) {
        result = s.Value;
    }
}
~省略~

f:id:mslGt:20180118003425j:plain

List の方が良い結果となりました。

計測2. 指定したKeyに合致するデータを取り出す

Key (List では Id) の値が合致するデータを取り出してみます。
今回 Id と List の要素番号が同じであるためあまり意味がない部分もありますが、そこは気にしない方向で。

なお Dictionary と List の速度比較の話を検索すると、この方法で検証していることが多いです。

DictionarySample.cs

~省略~
private void OutputListValues() {
    var result = "";
    for (var i = 0; i < sampleList.Count; i++) {
        if (sampleList[i].Id == i) {
            result = sampleList[i].Category;
        }
    }
}
private void OutputDictionaryValues() {
    var result = "";
    for (var i = 0; i < sampleDictionary.Count; i++) {
        result = sampleDictionary[i];
    }
}
~省略~

う~ん?一回目は List の方が速い、という結果になりました(誤差の範囲内かもですが)。

f:id:mslGt:20180118003517j:plain

計測3. 指定したKeyに合致するデータを取り出す(Listは要素番号指定)

今回 List が持つ Id と、要素番号は全く同じです。

このようなデータの場合に、 List の要素番号を指定してデータを取り出すのと Dictionary で Key (Id, 要素番号と同じ) を指定してデータを取り出すのとではどちらが速いでしょうか。

DictionarySample.cs

~省略~
private void OutputListValues() {
    for (var i = 0; i < sampleList.Count; i++) {
        result = sampleList[i].Category;
    }
}

// Dictionaryは上記と同じ.

~省略~

f:id:mslGt:20180118003658j:plain

そもそも List での Id 比較の速度による影響が少なかったようで、
あまり変わりませんでした。

計測4. Valueを検索する

では、 Dictionary の Key ではなく Value から要素を取得する場合はどうでしょうか。

「Category0」という値を持った要素を取得してみることにします。

DictionarySample.cs

~省略~
private void OutputListValues() {
    var result = "";
    foreach (var category in sampleList.Where(c => c.Category == "Category0")) {
        result = category.Category;
    }
}

private void OutputDictionaryValues() {
    var result = "";
    foreach (var category in sampleDictionary.Where(c => c.Value == "Category0")) {
        result = category.Value;
    }
}
~省略~

f:id:mslGt:20180118003817j:plain

GC Alloc は List の方が少なく、 速度は Dictionary の方が速い、という結果になりました。

計測5. Keyをstringにしてみる

今のところ List で良くね?て結果ばかりが出て悲しいので、
もう少し Dictionary の得意分野とされる、 Key によるアクセスをもう少し見てみることにします。

まずは今まで int にしていた Key の型を string にしてみます。

SampleValue.cs

public class SampleValue {
    public string Id { get; set; }
    public string Category { get; set; }
}

DictionarySample.cs

~省略~
private List< SampleValue> sampleList;
private Dictionary< string, string> sampleDictionary;
    
private void Start() {
    sampleList = new List< SampleValue>();
    sampleDictionary = new Dictionary< string, string>();

    var categoryNum = 0;
    for (var i = 0; i < 100000; i++) {
        var newValue = new SampleValue {
            Id = "Category" + i,
            Category = "Category" + categoryNum
        };
        sampleList.Add(newValue);
        sampleDictionary.Add("Category" + i, "Category" + categoryNum);
        categoryNum++;
        if (categoryNum > 5) {
            categoryNum = 0;
        }
    }
    ListButton.onClick.AddListener(() => {
        Profiler.BeginSample("SamplingProfile");
        OutputListValues();
        
        Profiler.EndSample();
    });
    DictionaryButton.onClick.AddListener(() => {
        Profiler.BeginSample("SamplingProfile");

        OutputDictionaryValues();
        
        Profiler.EndSample();
    });
}
private void OutputListValues() {
    var result = "";
    for (var i = 0; i < sampleList.Count; i++) {
        if (sampleList[i].Id == "Category10") {
            result = sampleList[i].Category;
        }
    }
}
private void OutputDictionaryValues() {
    var result = "";
    result = sampleDictionary["Category10"];
}

で、結果はこちら。

f:id:mslGt:20180118004107j:plain

Dictionary の本領発揮、という感じですね。
(ただ、一回目の直前にスパイクが見られるのが気になりますが)

f:id:mslGt:20180118004135p:plain

やはりループしなくても値が取れる、というのは大きい。

計測6. KeyをCustom classにしてみる

次は Key として Custom class を指定してみます。

List でも Value として string の値を持てるよう、下記のクラスを値として持たせるようにします。

SampleValueForList.cs

public class SampleValueForList {
    public SampleValue Key { get; set; }
    public string Value { get; set; }
}

DictionarySample.cs

~省略~
private List< SampleValueForList> sampleList;
private Dictionary< SampleValue, string> sampleDictionary;

private void Start() {
    sampleList = new List< SampleValueForList>();
    sampleDictionary = new Dictionary< SampleValue, string>();

    var categoryNum = 0;
    for (var i = 0; i < 100000; i++) {
        var newValue = new SampleValue(i, "Category" + categoryNum);
        sampleList.Add(new SampleValueForList {
            Key = newValue,
            Value = "Category" + categoryNum
        });
        sampleDictionary.Add(newValue, "Category" + categoryNum);
            
        categoryNum++;
        if (categoryNum > 5) {
            categoryNum = 0;
        }
    }
    ListButton.onClick.AddListener(() => {
        Profiler.BeginSample("SamplingProfile");
        OutputListValues();
        
        Profiler.EndSample();
    });
    DictionaryButton.onClick.AddListener(() => {
        Profiler.BeginSample("SamplingProfile");

        OutputDictionaryValues();
        
        Profiler.EndSample();
    });
}
private void OutputListValues() {
    var result = "";
    var key = new SampleValue(0, "Category0");
    for (var i = 0; i < sampleList.Count; i++) {
        if (sampleList[i].Key.Equals(key)) {
            result = sampleList[i].Value;
            Debug.Log("result " + result);
        }
    }
}
private void OutputDictionaryValues() {
    var result = "";
    var key = new SampleValue(0, "Category0");
    if (sampleDictionary.TryGetValue(key, out result)) {
        Debug.Log("result " + result);
    }
}

ただしそのままだと Dictionary 、 List ともに結果は 0 件になります。

Dictionary で指定した Key が存在するかを確認するのは FindEntry というメソッドですが、
この中でも登場する GetHashCode 、 Equals を override して Key として渡された SampleValue と比較できるようにする必要があります。

Dictionary

~省略~
private int FindEntry(TKey key) {
    if ((object) key == null)
        ThrowHelper.ThrowArgumentNullException(ExceptionArgument.key);
    if (this.buckets != null) {
        int num = this.comparer.GetHashCode(key) & int.MaxValue;
        for (int index = this.buckets[num % this.buckets.Length]; index >= 0; index = this.entries[index].next) {
            if (this.entries[index].hashCode == num && this.comparer.Equals(this.entries[index].key, key))
                return index;
        }
    }
    return -1;
}
~省略~

SampleValue.cs

public class SampleValue {
    public int Id { get; private set; }
    public string Category { get; private set; }

    public SampleValue(int newId, string newCateogry) {
        Id = newId;
        Category = newCateogry;
    }
    
    public override int GetHashCode() {
        return Id;
    }
    public override bool Equals(object obj) {
        return Equals(obj as SampleValue);
    }
    public bool Equals(SampleValue obj) {
        return obj != null && obj.Id == this.Id && obj.Category == this.Category;
    }
    
}

f:id:mslGt:20180118004242j:plain

Log 出力してみるとわかりますが、 Dictionary を検索するとき GetHashCode で HashCode の値を比較 -> 同じ場合は Equals で比較、
という順番で比較が行われます。
(※実行時間の問題から、要素数は減らしてから確認した方が良いと思います)

また、 Dictionary に値を追加するときにも上記の方法で既存の値を確認していることがわかります。

GetHashCode で得られる値は、すべて異なるものである必要はありません。

ただし、異なる値に同じ HashCode 値が設定されてしまうと、 Equals で確認する量が増え、遅くなります。

試しにすべて 0 を設定してみたところ、 Dictionary に値を追加するところで固まってしまい、
時間の計測ができませんでした。

ということで適切な値を設定する必要があります。

計測7. KeyをCustom classにしてみる(ListでGetHashCode -> Equalsの順に検索)

List でも Dictionary で Key を探すように、 GetHashCode -> Equals の順に検索すると同じように速くなるでしょうか。

DictionarySample.cs

~省略~
private void OutputListValues() {
    var result = "";
    var key = new SampleValue(0, "Category0");
    for (var i = 0; i < sampleList.Count; i++) {
        if (sampleList[i].Key.GetHashCode() == key.GetHashCode() &&
            sampleList[i].Key.Equals(key)) {
            result = sampleList[i].Value;
            Debug.Log("result " + result);
        }
    }
}

// Dictionaryは上記と同じ.

~省略~

f:id:mslGt:20180118004429j:plain

Dictionary には及ばないものの、速くなりましたね。

おわりに

今回の結果を元に考えると、Key を指定することができ、かつ期待する検索結果が少ない場合は Dictionary を使うことで高速化が望めそうです。
反対に検索結果が多い、また色んな条件で検索したい場合は List の方が強い、またはあまり変わらない、ということになりそうです。

また今回言及はしませんでしたが、 Dictionary では List や配列のように順序を持たないため、
要素番号で指定したい、また格納されたデータを決まった順番で扱いたい場合は List を使う必要があります。

SortedDictionary という自動で要素が並び替えられるものもありますが、
パフォーマンス面では Dictionary に劣り、かつ要素番号での指定はできません。

Unity 2017 Game Optimization でも指摘されていますが、
それぞれの特性を活かして効果的に使い分けていきたいところです。

参照

Dictionary

GetHashCode

【C#】自作クラスのグループ化と並び替え

はじめに

2017年に収まらなかったので新年一発目の記事となります。

今年もふと気になったあれこれ、やってみたあれこれを雑多に書き残していきますのでよろしくお願いいたします。

さてさて今回のお題ですが、自作クラスの並べ替えを雑にやろうとしたところ、
全然入れ替わらなかったりうまくいかなかったので、
もう少しちゃんと見てみるかぁ、という内容です。

  • 文字列の List を並び替える
  • 文字列 > int の2つの要素で並び替える
  • 文字列をグループ化した後 int で並び替える

文字列のListを並び替える

まずは文字列の Sort について見てみます。

例えばこのような List があった場合。

var sampleList = new List< string> {
    "カテゴリー1",
    "カテゴリー2",
    "カテゴリー",
    "カテゴリー1",
    "カテゴリー2",
    "カテゴリー5",
};

これを下記のように Sort() を引数なしで実行すると、 List が昇順に並び替えられます。

sampleList.Sort();

これが何をしているのかを見たかったのですが、
List クラスが IComparer< T> を null にして Array.Sort< T> を呼んでいる辺りで処理を見失ってしまいました。

というわけで、それはいったん置いておいて msdn のサンプルを見てみることにします。

Sort メソッドに ICompare を継承し、 int を返す Compare メソッドを渡すことで、
並び替える方法がカスタマイズできます。

サンプルでは比較する2つの値が Null かどうかなども確認していますが、
両方 Null ではない場合の比較は下記の 2 種類です。

int retval = x.Length.CompareTo(y.Length);

if (retval != 0) {
    return retval;
}
else {
    return string.Compare(x, y, StringComparison.Ordinal);
}
  • x は第一引数の文字列、 y は第二引数の文字列です。

上は簡単で、文字数が異なる場合にその差分を返しています。
下は unsafe メソッドを使って、先頭から一文字ずつ比較をしていました。

パフォーマンス的な理由から、文字数などで違いが判別できる場合はそちらを使っているようです。

Compareメソッドの戻り値

Compare メソッドが返す値は大きく分けて 3 種類です。

  • 0 未満: x が y より前に設定される
  • 0: 順序変わらず
  • 0 より大きい: x が y より後に設定される

後で少し触れますが、例えば -1 を返したときと -5 を返したときで、
Sort() で比較対象として引数に渡される回数が変わっていました。

Sort() は何回実行されるのか

最大で O(n ^ 2) (要素数の 2 乗 * 係数)とのことですが、上記のコードの場合は要素数 6 で 20 回実行されました。

自作クラスのListを並び替える

では、下記のような自作クラスを並び替えてみます。

SampleValue.cs

public class SampleValue {
    public int Id { get; set; }
    public string Category { get; set; }
}

で、これを Category > Id の順に並び替えてみます。

using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

public class SortSample : MonoBehaviour {
    private void Start () {
            var sampleList = new List< SampleValue>{
                    new SampleValue {
                        Id = 0,
                        Category = "カテゴリー1",  
                    },
                    new SampleValue {
                        Id = 3,
                        Category = "カテゴリー2",
                    },
                    new SampleValue {
                        Id = 5,
                        Category = "カテゴリー",
                },
                    new SampleValue {
                        Id = 1,
                        Category = "カテゴリー1",
                    },
                    new SampleValue {
                        Id = 2,
                        Category = "カテゴリー2",
                    },
                    new SampleValue {
                        Id = 4,
                        Category = "カテゴリー5",
                    },
        };  
        var compare = new CompareSample();
            // Listを並び替える.
        sampleList.Sort(compare.Compare);

        sampleList.ForEach(v => Debug.Log("ID: " + v.Id + " C: " + v.Category));
    }
}
// 並び替えるためのクラス.
public class CompareSample : IComparer< SampleValue> {
    public int Compare(SampleValue x, SampleValue y) {
        int retval = x.Category.Length.CompareTo(y.Category.Length);
        
        if (retval != 0) {
            return retval;
        }
        else {
            int compareCategory = string.Compare(x.Category, y.Category, StringComparison.Ordinal);
                    // Categoryの値を比較して同じであった場合にIdで並び替え.
            return compareCategory == 0 ? x.Id.CompareTo(y.Id) : compareCategory;
        }
    }
}
  • Sort() は List の中の順番を入れ替えるため戻り値の取得などは行いません。
  • まず Category 同士を比較し、同じ Category であった場合に Id で並び替えています。

実行結果です。

  1. Id: 5 Category: カテゴリー
  2. Id: 0 Category: カテゴリー1
  3. Id: 1 Category: カテゴリー1
  4. Id: 2 Category: カテゴリー2
  5. Id: 3 Category: カテゴリー2
  6. Id: 4 Category: カテゴリー5

どんな順番で実行されるのか

上で実行回数について触れましたが、どんな組み合わせで実行されるのでしょうか。

実際のところは Sort のアルゴリズムを学ばないと理解できないところではありますが、
とりあえず今回のサンプルを動作させた結果を載せておきます。

f:id:mslGt:20180103083245j:plain

List の一番最初に来る「カテゴリー」は 11 回呼ばれているのに対し、
最後に来る「カテゴリー5」は 3 回のみ、かつ第一引数( x )としては一度も呼ばれていませんでした。

この結果だけで考えると、文字列の List を並び替える場合、
それぞれの違いが小さければ小さいほど実行回数は増えるのでは?という感じがしました。

自作クラスのListをグループ化して並び替える

上記では、 Category という文字列に対しても並び替えられていました。

それでは、Category は並び替えず、
同じ Category を持つ Id で並び替えるにはどのようにすれば良いでしょうか。

グループ化する

今回は Linq の GroupBy を使ってみました。

何も考えずに書こうとすると、下記のように書きたくなるのですが、
これだとエラーになります。

// 戻り値が  IEnumerable< IGrouping< string, SampleValue>> であるためエラー.
sampleList.GroupBy(v => v.Category)
    .OrderBy(v => v.Id);

sampleList.GroupBy(v => v.Category) から、 List< SampleValue> の値を取得するには、
SelectMany を使う必要があります。

var sortedList = sampleList.GroupBy(v => v.Category)
    .SelectMany(v => v);

v => v のところが少し変な感じもしますね。

なお、 foreach を使って取得することもできます。

var sortedList = sampleList.GroupBy(v => v.Category);

foreach(var k in sortedList) {
    foreach (var v in k) {
        Debug.Log(v.Category + " Id " + v.Id);
    }
}

結果は下記の通りです。

  1. Id: 0 Category: カテゴリー1
  2. Id: 1 Category: カテゴリー1
  3. Id: 3 Category: カテゴリー2
  4. Id: 2 Category: カテゴリー2
  5. Id: 5 Category: カテゴリー
  6. Id: 4 Category: カテゴリー5

グループ化した List を並び替える

あとはこれをグループごとに並び替えます。

といってこのような感じで並び替えてしまうと、
グループ化が解除されて Id のみで並び替えられてしまいます。

var sortedList = sampleList.GroupBy(v => v.Category)
            .SelectMany(v => v)
            .OrderBy(v => v.Id);

※今回のデータの作りがまずく、このまま実行すると一見そろっているように見えますが、
Category の値を変えるとグループ化されていないのがわかります。

これを防ぐためには、 SelectMany の中で並び順を指定する必要があります。

var sortedList = sampleList.GroupBy(v => v.Category)
            .SelectMany(v => v.OrderBy(x => x.Id));

結果は下記の通りです。

  1. Id: 0 Category: カテゴリー1
  2. Id: 1 Category: カテゴリー1
  3. Id: 2 Category: カテゴリー2
  4. Id: 3 Category: カテゴリー2
  5. Id: 5 Category: カテゴリー
  6. Id: 4 Category: カテゴリー5

SelectManyについて

では今回活躍してくれた、 SelectMany についてもう少し見てみることにします。

SelectMany を使うことで、 IEnumerable< IGrouping< string, SampleValue>> という型で戻ってくる GroupBy の結果を、
IEnumerable< SampleValue> という型に変換できます。

で、この SelectMany のソースをたどってみると、先ほど値を取り出したときと同じように、
foreach を使って値を取り出していました。

Enumerable.cs

private static IEnumerable< TResult> SelectManyIterator< TSource, TResult>(IEnumerable< TSource> source, Func< TSource, IEnumerable< TResult>> selector) {
    foreach (TSource source1 in source) {
        foreach (TResult result in selector(source1))
            yield return result;
    }
}

そのため、下記のようにすれば SelectMany を使わず同様のことができます。
(需要があるかはわかりませんが)

private void Start() {
    ~省略~

    var sortedList = GetSortedSampleValues(sampleList.GroupBy(v => v.Category));
}
private IEnumerable< SampleValue> GetSortedSampleValues(IEnumerable< IGrouping< string, SampleValue>> groupedValue) {
    foreach(var k in groupedValue) {
        // IGrouping< string, SampleValue> を IdでSort.
        foreach (var v in k.OrderBy(v => v.Id)) {
            yield return v;
        }
    }
}

一応両者を計測してみたところ、若干 Foreach の方が良い結果になっていました。

f:id:mslGt:20180103083531j:plain

おわりに

Linq は良いぞ

…まぁ使用状況によっては for を使う場合に比べて GC alloc の量が増えたり、
いつでもどこでも使おうぜ!とまでは言い難いです。

ただ、今回のように複雑な処理がわずか数行で書けてしまうのはすごいですね。

参照

Sort

GroupBy

SelectMany

Others

【Xamarin.iOS】Xibで作ったTableViewCellを動的に追加したい

はじめに

以前書きましたが、 iPhone X 買いました。

マスクしてると顔認証してくれないとか不満が完全に無いわけではありませんが、
まぁ良好、といったところです。

で、せっかくなので何かアプリを作りたいですよね?

ということで、まずは目覚ましアプリに挑戦してみることにしました。

環境と仕様

開発言語

言語ですが、通常なら Swift となるわけですが、今回は Xamarin.iOS を使ってみることにしました。

ここ最近 Xamarin.Forms を使っていて Xamarin.iOS にも興味があった、ということが大きな理由です。

また、お仕事でもごくまれに Swift を使ってアプリを作る時があるのですが (3D を使わず、かつ BLE などネイティブの機能が必要な場合) 、
ほとんど触ったことのない Swift を言語仕様を調べながら書くほうが良いのか、
曲がりなりにも日常的に触れている C# を使って、 Xamarin.iOS の仕様を調べながら書くほうが良いのかは調べてみたいと思っていました。

なおXamarin.Forms を選ばなかった理由は、単に Xamarin.iOS を使ってみたかったからですw

Swift は Swift で注目すべき言語の一つだとは思いますので、別途追いかけたいところです。

開発環境

WindowsMacVisual Studio を使って開発しています。

本当は Windows からリモート接続で開発をしたいところなのですが、
ネットワーク状況からかなかなか接続できない時があったりするため、
Mac と接続していないとできない Storyboard 操作の部分は Mac でやっちゃうようにしています。

Mac だけにしない理由は家の Mac がラップトップでない (Mac mini) ことと、
2012 年モデルなのでさすがに動きがちょっともっさりしているというのが理由です。

仕様

まずはできるだけシンプルに作ることにします。

  • セットした時間になったら目覚ましが鳴る
  • 目覚ましが鳴る時間を追加、編集、削除できる
  • 目覚ましを曜日で指定できる
  • 目覚ましの On/Off ができる

とりあえず日本時間のみを考慮し、サマータイムなどは考えないことにします。
(うるう年とかは考慮が必要)

あと、次の 24 時間だけ目覚ましをキャンセルする機能も追加したいと思います。

Android 標準の目覚ましではこの機能があり、便利に使っていたためです。
(朝にあんまり強くないので、目覚ましを複数回設定するので)

現状、とりあえずの見た目はこんな感じです。

f:id:mslGt:20171230074435p:plain

親となる View にヘッダーと UITableView を設置し、
UITableView に UITableViewCell を追加しています。

StoryboardとViewController

Swift や Objective-C でアプリを作る場合、
Storyboard にボタンなどを追加し、 Outline で ViewController に紐づけます。

Xamarin.iOS も同じかな?と思ったのですが、実は Xamarin.Forms の XAML とコードビハインドの関係と同じように、
Storyboard で Identity の Name に名前を設定しておくと、
ViewController からその名前でアクセスすることができます。

すごい!

コードでボタンを追加する

追加ボタンを押したときに、動的に要素を追加したい。

ということで、まずは試しに ViewController からボタンを追加してみます。

ViewController.cs

using System;
using UIKit;

namespace XamarinIOsSample
{
    public partial class ViewController : UIViewController
    {
        private UIButton button;
        
        public ViewController(IntPtr handle) : base(handle)
        {
            // 親となるViewに対する設定.
            View.BackgroundColor = UIColor.White;
            Title = "My Custom View Controller";
        }

        public override void ViewDidLoad()
        {
            base.ViewDidLoad();
            
            // UITableViewの背景色をセット.
            MainView.BackgroundColor = UIColor.Red;

            // ボタンを作成して TableView に追加する.
            button = UIButton.FromType(UIButtonType.System);
            button.Frame = new CoreGraphics.CGRect(20, 20, 280, 44);
            button.SetTitle("Click Me", UIControlState.Normal);
            MainView.AddSubview(button);

            button.TouchUpInside += (sender, e) => {
                // ボタン押下時の処理.
            };
        }
        public override void DidReceiveMemoryWarning()
        {
            base.DidReceiveMemoryWarning();
            // Release any cached data, images, etc that aren't in use.
        }
    }
}

UITableView への背景色の設定やボタンの追加は ViewDidLoad() で行っています。

これは、コンストラクターで実行してしまうとそのあと上書きされてしまい、
設定した内容が反映されない(ように見える)ためです。

なお公式ページにはコードだけで Window から追加していく方法が紹介されています。
この充実ぶりもすごいです。

コードでXibを追加する

さてそれでは実際にアイテムを追加できるようにしてみます。

ただ、先ほどのようにボタンやラベルを一つずつ追加して位置を指定していくのは、
コード量も多くなるしあまりに大変。

Unity の Prefab のように、一緒に扱う UI をグループ化して取り扱う方法として、
Xib を使う、というものがあります。
(TableView にセットする UITableViewCell の場合)

と、ここまでは良かったのですが。

Xibを作成する

追加 > クラス から UITableViewCell のテンプレートを選択してファイル作成すると、
Xib ファイルとコードビハインドが生成されます。

この時「Cell」という名前にするのが良さそうです。(理由は後述)

とりあえず下記のようにアイテムを並べて、それぞれ名前を付けておきます。

f:id:mslGt:20171230074519p:plain

で、これを UITableView に追加してみます。

UITableViewSource

UITableView を使う場合、そのデータのソースとなる UITableViewDataSource と、
ふるまいを指定する UITableViewDelegate を継承したクラスを作成し、セットする必要があります。

Xamarin.iOS における UITableViewSource は、 UITableViewDataSource と UITableViewDelegate を1つのクラスにまとめたもの、
ということなので、これを継承したクラスを作ってあげれば OK です。

C# ではクラスの多重継承ができない、という理由もあるのでしょうが、
ただ Swift や Objective-C の代わりに C# で書ける、
という以外にも細かい部分で改善されている部分があるのが良いですね。

DataSource.cs

using System;
using Foundation;
using UIKit;

using System.Collections.Generic;
using XamarinIOsSample.Value;

namespace XamarinIOsSample
{
    public class DataSource : UITableViewSource
    {
        private readonly List< AlarmItem> objects = new List< AlarmItem>();
        
        public IList< AlarmItem> Objects => objects;

        public override nint RowsInSection(UITableView tableview, nint section)
        {
            return objects.Count;
        }
        public override UITableViewCell GetCell(UITableView tableView, NSIndexPath indexPath)
        {
            // UITableViewCellを作成したXibのクラスとして取得する.
            if (!(tableView.DequeueReusableCell(Cell.Key) is Cell cell))
            {
                return null;
            }
            // ラベルやボタンに対する設定を行う.
            cell.TimeLabelText = objects[indexPath.Row].TimeSetText;
            cell.DayOfTheWeekLabelText = objects[indexPath.Row].DayOfTheWeekText;
            cell.OnOffSwitchStatus = objects[indexPath.Row].OnOffSwitchStatus;

            return cell;
        }

        public override bool CanEditRow(UITableView tableView, NSIndexPath indexPath)
        {
            // Return false if you do not want the specified item to be editable.
            return true;
        }
    }
}

とりあえず必要そうなメソッドだけで作成してみました。

ラベルのテキストなどに設定する値を格納するクラスの変数をリスト化して Cell の数ぶん保持しています。

AlarmItem.cs

namespace XamarinIOsSample.Value
{
    public class AlarmItem
    {
        public string TimeSetText { get; set; }
        public string DayOfTheWeekText { get; set; }
        public bool OnOffSwitchStatus { get; set; }
    }
}

あとは Xib のコードビハインドでこれらの値をセットできるようにします。

Cell.cs

using System;

using Foundation;
using UIKit;

namespace XamarinIOsSample
{
    public partial class Cell : UITableViewCell
    {
        public static readonly NSString Key = new NSString("Cell");
        public static readonly UINib Nib;

        public string TimeLabelText { 
            get => TimeLabel.Text;
            set => TimeLabel.Text = value;
        }

        public string DayOfTheWeekLabelText
        {
            get => DayOfTheWeekLabel.Text;
            set => DayOfTheWeekLabel.Text = value;
        }
        public bool OnOffSwitchStatus
        {
            get => OnOffSwitch.On;
            set => OnOffSwitch.On = value;
        }

        static Cell()
        {
            Nib = UINib.FromName("Cell", NSBundle.MainBundle);
        }

        protected Cell(IntPtr handle) : base(handle)
        {
            // Note: this .ctor should not contain any initialization logic.
        }
    }
}

直接ラベルなどを返すようにした方が良いのかな?とも思いますが、
まず今回はこのような形にしました。

最後に ViewController から UITableView への追加や Table 行の高さなどを設定してみます。

ViewController.cs

using System;
using UIKit;
using XamarinIOsSample.Value;

namespace XamarinIOsSample
{
    public partial class ViewController : UIViewController
    {
        private DataSource dataSource;
~省略~
        public override void ViewDidLoad()
        {
            base.ViewDidLoad();

            // Xibの読み込み.
            MainView.RegisterNibForCellReuse(Cell.Nib, Cell.Key);
            MainView.Source = dataSource = new DataSource();
            
            // UITableView.AutomaticDimensionだと正しく設定されなかったのでいったんベタ打ち.
            MainView.RowHeight = 124;

            var newItem = new AlarmItem();
            newItem.TimeSetText = "06:30";

            dataSource.Objects.Insert(0, newItem);

            var newItem2 = new AlarmItem();
            newItem2.TimeSetText = "07:00";

            dataSource.Objects.Insert(1, newItem2);

        }
~省略~
    }
}

NSUnknownKeyException

上記で UITableViewCell を作るときに、名前を「Cell」にした理由です。

なぜか別の名前で作成してしまうと、
ViewController や DataSource での名前を変更しても NSUnknownKeyException が発生しました。

どこかに「Cell」という名前で紐づけがあるのだと思うのですが、
それがどこかはわからず。。。

これについては解決しましたら追記か別途書くようにしたいと思います。

おわりに

Storyboard や Xib など iOS アプリ開発に関する知識や Xamarin.iOS についての知識はともかく、
日常的に使い慣れた言語で書けるというのは良いですね。

もう少し作りこんだら、 Swift で書くとどうなるの?というのも試してみたいと思います。

参照

【C#】Listを検索してList(またはIEnumerable)を返したい

はじめに

この記事は C# Advent Calendar 2017 の21日目の記事です。

qiita.com

すでに何度かネタにしている Effective C# から。

第四章の29項目で、 Collection を作って返すよりも Iterator メソッドを返した方が良い、というものがあります。

理由としては遅延評価であることや、呼び出す側で都合が良いように加工しやすい、といったところがあるようです。

ただそこで気になったのがパフォーマンス。

例えばデータを保持するクラスの List があったとして、
その中の1要素だけを取ってきて for や foreach で処理したい。

この時、下記の条件で実行すると実行速度や GC Alloc のデータ量にどの程度違いがあるのでしょうか。

  1. あらかじめ対象の要素だけを持った List を作成しておき、それを返す
  2. IEnumerable を使って Iterator メソッドとして返す
  3. IEnumerable を使って Iterator メソッドとして返す (Linq を使用)
  4. 対象の要素だけを持った List をメソッド内で生成し、それを返す
  5. 対象の要素だけを持った List をメソッド内で生成し、それを返す (Linq を使用)

  6. 1.について、 List を生成する時間は計測せず、 List を返すメソッドの実行時間のみを計測します。

感覚的には番号順通りに速い、という気がしますがどうでしょうか。

実行環境

  • Windows 10 Pro 64bit
  • Unity 2017.2.0f3

Unity を使うのは、計測に Profiler を使いたいのと、
重いデータとして 3D モデルを持たせるようにしたいためです。

データを格納するクラス

名前は特に気にしない方向で...。

Breadman.cs

using System.Collections.Generic;
using UnityEngine;

public class Breadman{
    public int Id { get; set; }
    public GameObject BreadmanModel { get; set; }
    public List SpecialMoves { get; set; }
}

こんな感じでデータを入れておきます。
また検証 1. のための List も作成します。

MainController.cs

using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.UI;

public class MainController : MonoBehaviour {
    public GameObject BreadmanModel1;
    public GameObject BreadmanModel2;
    public GameObject BreadmanModel3;
    ~省略~
    private List breadmen;
    private List breadmanNames;
    
    private void Start () {

        breadmen = new List {
            new Breadman {
                Id = 0,
                BreadmanModel = BreadmanModel1,
                SpecialMoves = new List {
                    "パンチ",
                    "キック",
                }
            },
            new Breadman {
                Id = 1,
                BreadmanModel = BreadmanModel2,
                SpecialMoves = new List {
                    "空を飛ぶ",
                }
            },
            new Breadman {
                Id = 2,
                BreadmanModel = BreadmanModel3,
                SpecialMoves = new List {
                    "水に濡れる",
                }
            },
            new Breadman {
                Id = 3,
                BreadmanModel = BreadmanModel4,
                SpecialMoves = new List {
                    "力が出ない",
                }
            },
            new Breadman {
                Id = 4,
                BreadmanModel = BreadmanModel5,
                SpecialMoves = new List {
                    "新しいの",
                }
            }
        };
        // データを複製して増やす.
        var breadmen2 = new List(breadmen);
        breadmen.AddRange(breadmen2);
        var breadmen3 = new List(breadmen);
        breadmen.AddRange(breadmen3);
        var breadmen4 = new List(breadmen);
        breadmen.AddRange(breadmen4);
        var breadmen5 = new List(breadmen);
        breadmen.AddRange(breadmen5);

        breadmanNames = breadmen.Select(breadman => breadman.BreadmanModel.name).ToList();
        ~省略~
    }
    ~省略~
}

値を生成するメソッド

検証対象となる、値を生成するメソッドです。

前述の 1. ~ 5. に加えて、2. の Foreach ではなく For を使うもの(検証 2-1)、
さらにそれを逆転させたもの (検証 2-2) を追加しています。

MainController.cs

    // 検証1. あらかじめ対象の要素だけを持った List を作成しておき、それを返す.
    private List GetBreadmanNames_CreatedList() {
        return breadmanNames;
    }
    // 検証2. IEnumerable を使って Iterator メソッドとして返す.
    private IEnumerable GetBreadmanNames_IEnumerable() {
        foreach (var breadman in breadmen) {
            yield return breadman.BreadmanModel.name;
        }
    }
    // 検証3. IEnumerable を使って Iterator メソッドとして返す (Linq を使用).
    private IEnumerable GetBreadmanNames_IEnumerable_Linq() {
        return breadmen.Select(breadman => breadman.BreadmanModel.name);
    }
    // 検証4. 対象の要素だけを持った List をメソッド内で生成し、それを返す.
    private List GetBreadmanNames_Create() {
        var newBreadmanNames = new List();
        foreach (var breadman in breadmen) {
            newBreadmanNames.Add(breadman.BreadmanModel.name);
        }
        return newBreadmanNames;
    }
    // 検証5. 対象の要素だけを持った List をメソッド内で生成し、それを返す (Linq を使用).
    private List GetBreadmanNames_Create_Linq() {
        return breadmen.Select(breadman => breadman.BreadmanModel.name).ToList();
    }
    // 検証2-1. IEnumerable を使って Iterator メソッドとして返す.
    // ForeachではなくForを使って返してみる.
    private IEnumerable GetBreadmanNames_IEnumerable_For() {
        for(var i = 0; i < breadmen.Count; i++) {
            yield return breadmen[i].BreadmanModel.name;
        }
    }
    // 検証2-2. IEnumerable を使って Iterator メソッドとして返す.
    // 順番が逆になってしまうが、速度・GC Allocを計測してみる.
    private IEnumerable GetBreadmanNames_IEnumerable_For_Reverse() {
        for(var i = breadmen.Count - 1; i >= 0; i--) {
            yield return breadmen[i].BreadmanModel.name;
        }
    }

計測する"

ボタンのクリックイベントを利用して計測してみます。

MainController.cs

~省略~
public class MainController : MonoBehaviour {
    ~省略~
    public Button Sample1Button;
    public Button Sample2Button;
    public Button Sample3Button;
    public Button Sample4Button;
    public Button Sample5Button;
    public Button Sample6Button;
    public Button Sample7Button;
    ~省略~
    private void Start() {
        ~省略~
        // 検証1. あらかじめ対象の要素だけを持った List を作成しておき、それを返す.
        Sample1Button.onClick.AddListener(() => {
            Profiler.BeginSample("ProfileSampling");
            foreach(var breadmanName in GetBreadmanNames_CreatedList()) {
                Debug.Log("name " + breadmanName);
            }
            Profiler.EndSample();
        });
        // 検証2. IEnumerable を使って Iterator メソッドとして返す.
        Sample2Button.onClick.AddListener(() => {
            Profiler.BeginSample("ProfileSampling");
            foreach (var breadmanName in GetBreadmanNames_IEnumerable()) {
                Debug.Log("name " + breadmanName);
            }
            Profiler.EndSample();
        });
        // 検証3. IEnumerable を使って Iterator メソッドとして返す (Linq を使用).
        Sample3Button.onClick.AddListener(() => {
            Profiler.BeginSample("ProfileSampling");
            foreach (var breadmanName in GetBreadmanNames_IEnumerable_Linq()) {
                Debug.Log("name " + breadmanName);
            }
            Profiler.EndSample();
        });
        // 検証4. 対象の要素だけを持った List をメソッド内で生成し、それを返す.
        Sample4Button.onClick.AddListener(() => {
            Profiler.BeginSample("ProfileSampling");
            foreach (var breadmanName in GetBreadmanNames_Create()) {
                Debug.Log("name " + breadmanName);
            }
            Profiler.EndSample();
        });
        // 検証5. 対象の要素だけを持った List をメソッド内で生成し、それを返す (Linq を使用).
        Sample5Button.onClick.AddListener(() => {
            Profiler.BeginSample("ProfileSampling");
            foreach (var breadmanName in GetBreadmanNames_Create_Linq()) {
                Debug.Log("name " + breadmanName);
            }
            Profiler.EndSample();
        });
        // 検証2-1. IEnumerable を使って Iterator メソッドとして返す(For使用).
        Sample6Button.onClick.AddListener(() => {
            Profiler.BeginSample("ProfileSampling");
            foreach (var breadmanName in GetBreadmanNames_IEnumerable_For()) {
                Debug.Log("name " + breadmanName);
            }
            Profiler.EndSample();
        });
        // 検証2-2. IEnumerable を使って Iterator メソッドとして返す(Forを逆転させて使用).
        Sample7Button.onClick.AddListener(() => {
            Profiler.BeginSample("ProfileSampling");
            foreach (var breadmanName in GetBreadmanNames_IEnumerable_For_Reverse()) {
                Debug.Log("name " + breadmanName);
            }
            Profiler.EndSample();
        });
    }
    ~省略~

起動 -> ボタンを5回押す -> Profiler の結果を見る 、という内容で確認してみました。

結果

結果は下記の通り。

f:id:mslGt:20171224115042j:plain

やっぱりというか、あらかじめ作成済みの List を返すのが速いようです。

ただ、どのケースを見ても GC Alloc のデータ量は変わらなかったのは少し驚きました。
( List を生成する方がデータ量が多そうだったので)

また、 Foreach や For を使った場合と Linq を使う場合とを比較すると、
最初の一回は Foreach や For を使うほうが速く、それ以降は Linq の方が速い傾向がありました。

ただ速度に関しては、このあと何度か試すと必ずしも上記の通りとはならなかったので、
(検証1. も他と同じくらいの速度になったり)
どの方法を採るか、というのはもう少し検証が必要そうです。

おわりに

少なくとも今回の内容からすると、検証1. の List として持たせるのが速度を最優先するなら良さそうです。

ただ、それぞれを List に、とかし始めるとバグの温床にもなりそうですし、
そもそもデータを格納するためのクラスを作る意味もなくなってくるという。。。

またキャッシュしておくデータの量も増えてしまいますね。

メソッド内で生成する検証2. ~ 検証7. を見ると、ほとんど変わらないように思います。

ということで、検証2. か検証3. の方法で IEnumerable を返して、
呼び出し元で都合が良いように加工する、というのが良さそうです。

やっぱり実際測ってみると意外な結果が見えたりして面白いですね。

最後に、検証のネタに使った Breadman を載せておきます。

www.instagram.com

参照