vaguely

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

【ASP.NET Core】Razorで遊んでみる

はじめに

ブログとしては久しぶりに ASP.NET Core 関連のお話です。

C# のコードを HTML に書けるということで ASP.NET Core に採用されている Razor 。

実をいうとあんまり使われておらず、 Angular や React などの JavaScriptフレームワークが主流なんじゃないの? と思っていました。

が、とある勉強会にて C# 版 WebAssembly である Blazor の話が盛り上がったのですが、その構文がかなり Razor と共通していると感じました。

で、あれ? もしかして結構本気でこの構文を推し進めようとしているのでは?
(もちろんジョークだと思っていたわけではないですが)
と思ったのでした。

Blazor にも興味があるところですが、まずは情報もよりたくさんあるであろう Razor から調べてみることにしました。

というお話です。

なお Microsoft Docs をはじめ、 Razor 構文は紹介されているところがたくさんあるため、網羅的に試すというよりは気になったものをピックアップして取り上げることにします。

試してみる

では早速試してみることにします。

基本的に C# で書くことのできるコードは、頭に@をつけることで何でも書くことができるようです。

また @ が複数行にわたる場合など、{} や () でまとめることも可能です。

@{
    var text = "Hello ";
    var sample = ViewBag.Sample;
}
< div>
    @(text + sample)
< /div>

{} や () の使用は必須ではないものがほとんどですが、List< string> のように <> が含まれる場合は必ず {} や () 内に入れる必要があります。

@functions

関数は @functions の中に書くことができます。

@functions {
    string GetMessage() {
        return "Hello";
    }    
}
< div>@GetMessage()< /div>

@classes のようなものは用意されていないため、インナークラスを定義することはできないようです...と思いきや、 @functions は実は関数が書ける、というものではなくコードブロックが書けるものであるため、下記のように書くことができてしまいます。

@functions {
    
    public class InnerClassSample {
        public string Name { get; set; }
    }
    
    async Task< InnerClassSample> GetInnerClassAsync() {
        await Task.Delay(500);
        return new InnerClassSample{ Name = "Hello" };
    }    
}

ただし当然ながら? コントローラークラスなどからはアクセスできないため、活躍の場は少なそうです。

Controller から値を渡す

Controller 側から値を渡す方法としては ViewData、ViewBagがあります。

いずれも Controller で値を入れておき、 View 側で取り出す、という形をとります。

HomeController.cs

using System;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using RazorSample.Models;

namespace RazorSample.Controllers {
    [Route("")]
    public class HomeController: Controller {
        public IActionResult Index() {
            // ViewData で渡す値をセット.
            ViewData["SampleUsers"] = Enumerable.Range(0, 5)
                .Select(num => new User {
                    ID = num,
                    Name = "User " + num,
                    Description = "Hello World!!!"
                })
                .ToList();
            return View("/Views/Index.cshtml");
        }
    }
}

Index.cshtml

@foreach (User u in (List< User>)ViewData["SampleUsers"]) {
    < table>
        < tr>
            < td>ID: < /td>
            < td>@u.ID< /td>
        < /tr>
        < tr>
            < td>Name: < /td>
            < td>@u.Name< /td>
        < /tr>
        < tr>
            < td>Description: < /td>
            < td>@u.Description< /td>
        < /tr>
    < /table>
}

ViewData で受け取れる値の型は object のため、キャストしてから取り出します。

ViewData と ViewBag の違いはアクセスの方法です。

ViewData[Sample] = "Hello";
ViewBag.Sample = "Hello";

注意点として、 ViewBag の Sample もどこかで定義されているわけではないため、 入力補完や静的解析は効かない、ということです。

ということで使い分けは気分次第、ということになります。 ( ViewBag は NullReferenceException がとか読んだ気もしますが、 Dictionary も Key が見つからなきゃエラーになりますよね)

コメントアウト

@{
    /* DoSomething(); */
    
    // DoSomething();
}

↑でもコメントアウトできることはできます。

が、

@* @DoSomething() *@

がスマートですね。

動的に呼び出せるか

できません。

ページをロードするときに評価され、 HTML に変換されてしまうので、

< button type="button" onclick=@{ Say(); }>Say< /button>

のように書くと、ページロード時に Say() が実行され、 onclick は空の状態になります(ボタンを押しても反応しない)。

加えて

< !--button type="button" onclick=@{ Say(); }>< /button-->

としてもコメントアウトとみなされないため、注意が必要そうです。

async/await

@functions {    
    string message = "default";

    async Task< string> GenerateMessage() {
        await Task.Delay(1000);
        return "hello";
    }
}
< div>
    @{ message = await GenerateMessage(); }
    @message

    @* これでもOK *@
    @await GenerateMessage()
< /div>

上記のように async/await を使うこと自体はできますが、非同期で表示の内容を切り替えられるわけではないため、 ページのロード完了は await などで待つ必要があります。

非同期での実行がしたい場合は Blazor?

@:

@: を頭につけると、その行は < text>< /text> で囲まれたのと同じ扱いとなります。

@for (var i = 0; i < 3; i++) {
    var person = "person " + i;
    @:Name: @person この行の @: 以降はすべて HTML として出力される
}

HtmlTagHelper

Razor では直接 HTML タグを書く他に、より C# 的に書けるようヘルパーが用意されています。

Index.cshtml

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@model RazorSample.Models.User

@* 
    using を使うことでスコープの終わりに EndForm を自動で実行してくれる 
    BeginForm の引数は 1. アクション( Submit で呼び出す関数)、
        2. コントローラー名、 3. 送信方法( Get 、 Post)、 4. HTML の属性( class とか)
*@

@using(Html.BeginForm(
          "SendMessage",
          "Home",
          FormMethod.Post,
          new { @class = "myclass"})){
              < table>
                  < tr>
                      < td>@Html.LabelFor(m => m.Name)< /td>
                      < td>@Html.TextBoxFor(m => m.Name)< /td>
                  < /tr>
                  < tr>
                      < td>@Html.LabelFor(m => m.Description)< /td>
                      < td>@Html.TextAreaFor(m => m.Description)< /td>
                  < /tr>
              < /table>
              < input type="submit" value="Submit" />
}

User.cs

using System;

namespace RazorSample.Models {
    public class User {
        public int ID { get; set; }
        public string Name { get; set; }
        public string Description { get; set; }
        public DateTime LastUpdateDate { get; set; }
    }
}

HomeController.cs

using System;
using System.Linq;
using Microsoft.AspNetCore.Mvc;
using RazorSample.Models;

namespace RazorSample.Controllers {
    [Route("")]
    public class HomeController: Controller {
        ~省略~

        [HttpPost]
        public void SendMessage(User sampleUser) {           
            Console.WriteLine("Send");
        }
    }
}

Formなどに対してusing を使えるのは便利そうですね。

JavaScript での C# の変数

例えばこんな処理

Index.cshtml

@functions {
    string message = "default message";
    async Task< string> GetMessageAsync() {
        await Task.Delay(500);
        return "hello";
    }
}
< script type="text/javascript">
    var clickSample = function() {
        // '' や "" で囲まないとエラー
        console.log('@message');
    }
< /script>
< div>
    @{ message = await GetMessageAsync(); }
< /div>
< button onclick="clickSample()">Click< /button>

まず地味にハマったのは、 JavaScript の console.log で C# の変数を渡すときに '' や "" で囲んでおらず、「 default 」が定義されていない、とエラーになったことです。

「 default 」ってなんぞ?と思ったら、変数の「 default message 」が半角スペースで区切られたものを変数として扱おうとしていたためのようでした。

やはり動的に型を持つ言語は扱いが違うので慣れていないとびっくりしますね(;'∀')

あと、 JavaScript 内での C# の変数の値は JavaScript のコードがロードされた時に確定されるため、上記のように「 @{ message = await GetMessageAsync(); } 」より前に定義すると値は「default message」に、後に定義すると「 hello 」になります。

async/await などを使う場合は注意が必要ですね。

おわりに

取り留めもなく書いていきましたが、かなり C# 的に HTML が書ける、というのはなかなか面白いです。

とはいえ非同期の処理は使えなかったり(実行はされるが表示に反映されない)、どのように HTML に変換されるかを理解する必要がある、という意味ではやはり HTML をちゃんとわかっておく必要があったりします。

今回取り上げた内容もまだ Razor の一部でしかないと思うので、引き続き追いかけてみるとともに Blazor も試してみたいと思います。

参照

【Unity】画面サイズと異なる大きさの RenderTexture でクリック位置の WorldPoint が取りたい話

はじめに

Unity では基本的に、 uGUI などの GUI はカメラを使って表示する 3D より手前に表示されます。

ただ、例えばポップアップウインドウのような表示がしたいなど、 GUI より手前に 3D を表示したい場合もあります。

それを解決する方法として RenderTexture を使う方法があります(正しい方法かどうかは不明)。

が、それを使って表示した内容に対してクリックし、その位置を 3D(WorldPoint) に変換しようとしたらうまくいかなかったのでメモです。

準備

  • 画面サイズは 1920 * 1200 とします。
  • RenderTexture のサイズは 1100 * 600 とします。
  • Canvas を下記の内容で準備します。
    1. Canvas Scaler の UI Scale Mode を Scale with Screen Size にし、Reference Resolution を 1920 * 1200 とします。
    2. Raw Image を追加し、サイズを 1100 * 600 に設定します。
    3. 2.の Anchors を X = 0, Y = 1 に設定します。
    4. RenderTexture のサイズを 1100 * 600 で作成し、2.の Texture にアタッチします。
  • 今回は特定の位置に置いた Plane 上のクリック位置を取ります。
    1. カメラの正面に Plane を適当なサイズに置きます(カメラから Z 方向に 10 離す)。
    2. クリック位置が分かりやすいように、適当なサイズの Cube を置きます。
      (初期位置はどこでも良いです)

クリック位置を取ってみる

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

public class MainController : MonoBehaviour {
    public Camera MainCamera;
    // RenderTexture
    public RectTransform TextureTransform;
    // クリック位置上に置く Cube.
    public Transform ClickTransform;
    // Plane
    public Transform PlaneTransform;
    
    private Vector2 texturePosition;
    private Vector2 screenScale;
    private Vector3 screenInputPosition = Vector3.zero;
    
    private void Start () {
        // 実行中の画面サイズを 1920 * 1200 にスケーリング.
        screenScale = new Vector2(
            Screen.width / 1920f,
            Screen.height / 1200f);

        // 今回は RenderTexture の位置が動的に変わらないものとする.
        texturePosition = TextureTransform.position;
    }
    private void Update () {
        if(Input.GetMouseButtonUp(0)) {
            // クリック位置が RenderTexture でのどの位置にあるかを算出.
            // 座標値は画面サイズ 1920 * 1200 での値を使う必要がある.
            screenInputPosition.x = (Input.mousePosition.x - texturePosition.x) / screenScale.x;
            screenInputPosition.y = ((Input.mousePosition.y - texturePosition.y) / screenScale.y) +
                TextureTransform.sizeDelta.y;

            // クリック位置を取りたい Plane と カメラの Z 軸の差分を入れることで、 Plane 上の座標値に変換できる.
            screenInputPosition.z = PlaneTransform.localPosition.z - MainCamera.transform.localPosition.z;
            ClickTransform.position = MainCamera.ScreenToWorldPoint(screenInputPosition);
        }
    }
}

注意点としては、 2D の Y 座標は画面上部が 0 、3D は下が 0 になる、というところと、 ScreenToWorldPoint に渡す座標は画面サイズが 1920 * 1200 であったときのものに合わせる必要がある、ということです。

【C#】float における == と Equals

はじめに

先日他の方の書いたコードを見る機会があったのですが、その中で「if(num == 0f)」のような内容を見ました。

ドヤ顔で「if(num.Equals(0f))」の方が良いですよ、と言いかけたのですが、ここで一つ気になりました。

== と Equals でどう変わるんだっけ?

ということで調べてみました。

== と Equals

まずは両者の違いを見てみます。

2つの値が等しいか調べる、等値演算子(==)とEqualsメソッドの違い - .NET Tips (VB.NET,C#...)

Equality Comparisons (C# Programming Guide) | Microsoft Docs

How to: Define Value Equality for a Type (C# Programming Guide) | Microsoft Docs

参照型の比較については参照の比較と値の比較があるものの、今回の float のような値型の比較では、どちらも値を比較するようです(当然といえば当然ですが)。

値を入れて比較してみる

下記のようなコードで、 valueA と valueB の値を入れ替えて結果に違いが出るかを見てみました。

using System;

namespace ConsoleApp1 {
    class Program {
        static void Main(string[] args) {
            float valueA = 0f;
                float valueB = 0f;
            
            Console.WriteLine("result ==: " + (valueA == valueB) + 
                " equals: " + (valueA.Equals(valueB)));
        }
    }
}

比較した値と結果はこちら。

f:id:mslGt:20181002073557p:plain

Java では +0.0 と -0.0 も結果が異なるようですが、 C# では同じ結果となりました。

Float,Doubleのequalsとfloat,doubleの== - souta-bot log

ということで、調べた中では値が float.NaN であった場合のみ結果が異なる、ということになりました。

float.NaN は数値ではないため == では False になり、 Equals の場合は float.NaN の場合の処理が別に用意されているため True になる、ということでしょうか。

いずれにせよ、これなら処理上 float.NaN になることが考えられない場合は == を使う方が良さそうです(値型の場合、こちらの方が速度が速いということなので)。

指数表記

調べる中で興味深かったところとして、例えば下記のコードでは == 、 Equals ともに True になります。

using System;

namespace ConsoleApp1 {
    class Program {
        static void Main(string[] args) {
            float valueA = 10000000.12345f;
                float valueB = 10000000f;
            
            Console.WriteLine("A: " + valueA + " B: " + valueB);
            Console.WriteLine("result ==: " + (valueA == valueB) + 
                " equals: " + (valueA.Equals(valueB)));
        }
    }
}

valueA, valueB の値はどちらも 「1E+07」と出力され、どうやら値が丸められているようです。

値を 1/10 すると、「1000000」となり、結果は False になります。

扱う値によっては注意が必要ですね。

おわりに

今回確認した内容がすべてか?というと自信がないですが、基本的には float の比較は == で良さそう、ということが分かりました。

まぁ Equals を使ったとして問題になることも無いとは思いますが、やっぱりちゃんと把握しないといけませんね。

参照

【C#】値型のローカル変数はどこに置かれるか

はじめに

ふと気になったので。

string など参照型のローカル変数を作った場合、そのデータはメモリのヒープ領域に置かれ、使われなくなるとガーベジコレクションによって回収されます。

では値型の場合は?

ということで調べてみることにしました。

準備

今回の確認に使用するのは、毎度おなじみ Unity の Profiler です。

最初は Visual Studio の診断ツールを使っていたのですが、ヒープ領域の確認は簡単にできたもののスタック領域の確認方法がわからなかったので変更しました。
(こちらはおいおい再挑戦したいと思っています)

環境

Unity2018.2.8f1

ベースのコード

検証用のコードです。

後で触れますが、メンバー変数について調べるためにクラスのインスタンス生成時のプロファイルも取るようにしています。

MainController.cs

using UnityEngine;
using UnityEngine.Profiling;
using UnityEngine.UI;

public class MainController : MonoBehaviour {
    public Button PlayButton;
    private void Start () {
        // クラスのインスタンス生成時の計測.
        Profiler.BeginSample("SamplingProfile1");
        CustomClass c = new CustomClass();
        Profiler.EndSample();
        
        PlayButton.onClick.AddListener(() => {
            // 計測開始.
            Profiler.BeginSample("SamplingProfile");
            c.Say();
            // 計測終了.
            Profiler.EndSample();
        });
    }   
}

こちらに変数を入れたり消したりして計測をしてみます。

CustomClass.cs

public class CustomClass {
    public void Say() {
    }
}

ローカル変数

さっそく下記の3条件で計測してみます。

  1. ローカル変数なし
  2. 値型( float )のローカル変数あり
  3. 参照型( string )のローカル変数あり

ローカル変数なし

CustomClass.cs

public class CustomClass {
    public void Say() {
    }
}

値型( float )のローカル変数あり

CustomClass.cs

public class CustomClass {
    public void Say() {
        float message = 1000000000000;
        float message2 = message * 2000000000;
        float message3 = message2 - 30000000000000000;
    }
}

参照型( string )のローカル変数あり

CustomClass.cs

public class CustomClass {
    public void Say() {
      string message = "1000000000000";
      string message2 = "message * 2000000000";
      string message3 = "message2 - 30000000000000000";
    }
}

結果

f:id:mslGt:20180930093315j:plain

値型のローカル変数ありの場合を見ると、空の場合と比較して Total, Self の割合は増えていますが、 GC Alloc は 0 のままです。

参照型のローカル変数ありの場合では GC Alloc の値が増えており、参照型のローカル変数はヒープ領域に置かれていることが見られます。

メンバー変数

ではメンバー変数の場合はどうでしょうか。

下記によると、参照型であるクラスのメンバー変数の場合は、値型であってもヒープ領域に置かれるようです。

ja.stackoverflow.com

これを確かめてみます。

  1. メンバー変数なし
  2. 値型( float )のメンバー変数あり
  3. 参照型( string )のメンバー変数あり

メンバー変数なし

CustomClass.cs

public class CustomClass {
    public void Say() {
    }
}

値型( float )のメンバー変数あり

CustomClass.cs

public class CustomClass {
    
  private float message = 1000000000000;
    private float message2 = 2000000000;
    private float message3 = -30000000000000000;
    
  public void Say() {
    }
}

参照型( string )のメンバー変数あり

CustomClass.cs

public class CustomClass {
    
  private string message = "1000000000000";
    private string message2 = "message * 2000000000";
    private string message3 = "message2 - 30000000000000000";
    
  public void Say() {       
    }
}

結果

f:id:mslGt:20180930093119j:plain

値型( float )の場合も GC Alloc の量が増えていることが見られます。

ということで、クラスのメンバー変数の場合は、値型の場合であってもヒープ領域に置かれるようです。

おわりに

データがスタック領域に置かれるのかヒープ領域に置かれるのかは、ガーベジコレクションの対象になるかどうか、といった面で重要になります(なる場合があります)。

まぁググれば情報は見つかるわけなのですが、実際に自分で見てみる、というのも楽しいものですね。

一点気になる点として、値型の変数として Decimal を使おうとしていたのですが、ローカル変数の場合でも GC Alloc が増えました。

Decimal が struct であること、Visual Studio の診断ツールではヒープ領域の量が増えないことから値型であることには間違いないと思うのですが。

こちらも何かわかったら追記、または別途書くことにしたいと思います。

参照

UnityでAnimationを作ってみる

はじめに

Unity2017 くらい?から、 Unity でも Maya や Blender などのように 3D アニメーションを作ることができるようになりました。

今回はこれを試してみたいと思います。

環境(いつも書き忘れてますが)

  • Unity 2018.2.8f1

アニメーションを作ってみる

では早速作ってみたいと思います。

Keyframe recording mode を使ったアニメーション作成

まずは Scene に Cube を一つ作成し、それを移動させてみます。

  1. メニューの Window > Animation > Animation から Animation ウインドウを開く
    (ウインドウ下部で、 Curves ではなく Dopesheet が選択されているかも確認する)
  2. Cube を選択した状態で 1.の Add Property ボタンを押し、 Transform > Position の右にある+ボタンを押す
    (anim ファイルの保存を求められるので適当に名前を付けて保存)
  3. Add Property ボタンの上部にある赤丸ボタンを押す
  4. 右側の白い縦線を移動させながら、各時間ごとの Cube の位置を Cube の Inspector > Transform で値を入力して指定する
  5. 完了したらもう一度赤丸ボタンを押して終了する

f:id:mslGt:20180919075056p:plain

再生

作成したアニメーションのプレビューは赤丸ボタンの3つ隣にある再生ボタンでできます。

実際にアニメーションの再生などをコントロールするのは、 anim ファイルと同じ場所に自動で生成される Animator Controller です。

あとは以前 Animator を触った時と同じようにコントロールすれば OK です。

手動でキーを追加する / 削除する

Keyframe recording mode で自動でキーを設定するのではなく、手動でキーを追加したり削除したりしてみます。

上記 1.、2.を実行後、 Add Property ボタンの上に表示された Cube : Position の上で右クリック → Add key でキーを追加、 Delete key でキーを削除できます。

また追加されたキーの値も編集できるので、その気になれば Tween 的な動きもできなくもない気がします。
(素直に DOTween などを使う方が良いとは思いますが)

複数のオブジェクトを動かす

複数のオブジェクトを一度に動かしたい場合。

Hierarchy で複数のオブジェクトを選択し、 Add Property でまとめて。。。と思いきやこれはできません。

anim ファイルが一つの GameObject を対象に作られる(多分)ためです。

ただし、一つの GameObject に子どもとなる GameObject がある場合、親の GameObject に対する anim ファイルを作ると子どもの GameObject にもアニメーションが接敵できるようになります。
(Add Property で子どもの GameObject に対する Position などの設定項目が表示されるようになります)

f:id:mslGt:20180919075039p:plain

ということで、複数の GameObject を同時に動かしたい場合はそれらをまとめて空の GameObject の子どもにしてしまえば OK 、という訳ですね。

おわりに

これでもう Maya も Blender も不要!。。。な訳はありませんが、それらでアニメーションを作る → インポートして動作確認 といったことを繰り返すよりは、可能であれば Unity 内で完結してしまえるのが便利ですね。

Maya や Blender にしても、今回の Unity でのアニメーションにしてもまだ簡単な機能しか使えていないため、どこまでできるのかはまだ不明なのですが。。。

参照

【Unity】【C#】2つのListを見比べて一方にしか存在しないものを検出したい

はじめに

List を検索してガチャガチャやりたい話のメモです。

前提:

  • Id (int型)という共通の要素を持つ2つの List があり、それぞれ searchIds 、 models という名前とする。
  • searchIds の中身は任意で変更できるものとする

やりたいこと:

  • searchIds の中に、 models に含まれない Id を持つものがあれば models にその Id を持つアイテムを追加する
  • models の中に、 searchIds に含まれない Id を持つものがあればそのアイテムを削除する

ベースとなるクラス、変数

searchIds

public 変数とし、 UnityEditor 上から編集 -> ボタンクリックで反映することとします。

public List< int> SearchIds = new List< int> {
    1, 4, 6, 7, 9,
};

models のデータ格納用クラスです。

SampleModel.cs

using UnityEngine;

public class SampleModel: MonoBehaviour {
   public int Id { get; private set; }
   private GameObject model;

   public void Init(int setId, GameObject setModel) {
      Id = setId;
      model = setModel;
   }
   public void DestroyModel() {
      Destroy(model);
      Destroy(gameObject);
   }
}

models

private List models = new List();

ベースのクラス

このクラスの UpdateModels を変更してみて、実行速度や GC Alloc などが変化するかを見てみたいと思います。

ListComparer.cs

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

public class ListComparer : MonoBehaviour {
    public GameObject BaseCube;
    public Button UpdateButton;

    public List< int> SearchIds = new List< int> {
        1, 4, 6, 7, 9,
    };
    private List< SampleModel> models = new List< SampleModel>();

    private void Start() {
        UpdateButton.onClick.AddListener(() => {
      // 計測開始.
            Profiler.BeginSample("SamplingProfile");
            UpdateModels();
      // 計測終了.
            Profiler.EndSample();

            Debug.Log("count: " + models.Count);
            models.ForEach(m => Debug.Log("id: " + m.Id));
        });
    }

    private void UpdateModels() {
        foreach (int i in SearchIds) {
      // 本来は else で既存データの値を更新するため、FirstOrDefault で SampleModel を取得する.
            SampleModel target = models.FirstOrDefault(s => s.Id == i);
            if(target == null) {
                models.Add(GenerateModel(i, Instantiate(BaseCube)));
            }
        }

        List< int> removeIndexes = new List< int>();
        for (int i = 0; i < models.Count; i++) {
            int index = i;
            if (SearchIds.Any(s => s == models[index].Id) == false) {
                removeIndexes.Add(index);
            }
        }

        removeIndexes.Sort();

        // Remove していくので逆から実行.
        for (int i = removeIndexes.Count - 1; i >= 0; i--) {
            models[removeIndexes[i]].DestroyModel();
            models.RemoveAt(removeIndexes[i]);
        }
    }
    private SampleModel GenerateModel(int id, GameObject model) {
        SampleModel newModel = new GameObject("Model")
            .AddComponent();
        newModel.Init(id, model);
        return newModel;
    }
}

試す

さて、さっそく思いついたものをいくつか試してみたいと思います。

検証 1. 削除してから追加してみる

ListComparer.cs

~省略~
  private void UpdateModels() {

        List< int> removeIndexes = new List< int>();
        for (int i = 0; i < models.Count; i++) {
            int index = i;
            if (SearchIds.Any(s => s == models[index].Id) == false) {
                removeIndexes.Add(index);
            }
        }
        removeIndexes.Sort();

        // Remove していくので逆から実行.
        for (int i = removeIndexes.Count - 1; i >= 0; i--) {
            models[removeIndexes[i]].DestroyModel();
            models.RemoveAt(removeIndexes[i]);
        }

        foreach (int i in SearchIds) {
            SampleModel target = models.FirstOrDefault(s => s.Id == i);
            if(target == null) {
                models.Add(GenerateModel(i, Instantiate(BaseCube)));
            }
        }
    }
  ~省略~

Profiler で確認した結果は後でまとめて載せますが、ベースのコードとほぼ同じでした。

まぁ順番を変えたぐらいでは特に変化がないですね。

検索するのが追加・削除のどちらか一方のみであれば変化があるかもしれません。

検証 2. 要素の中身を破棄するのと List から Remove するのを分ける

削除対象のインデックスを取得するときに、合わせて要素の中身を削除すると何か変わるでしょうか。

ListComparer.cs

~省略~
  private void UpdateModels() {

        List< int> removeIndexes = new List< int>();
        for (int i = 0; i < models.Count; i++) {
            int index = i;
            if (SearchIds.Any(s => s == models[index].Id) == false) {
        models[index].DestroyModel();
                removeIndexes.Add(index);
            }
        }
        removeIndexes.Sort();

        // Remove していくので逆から実行.
        for (int i = removeIndexes.Count - 1; i >= 0; i--) {
            models.RemoveAt(removeIndexes[i]);
        }

        foreach (int i in SearchIds) {
            SampleModel target = models.FirstOrDefault(s => s.Id == i);
            if(target == null) {
                models.Add(GenerateModel(i, Instantiate(BaseCube)));
            }
        }
    }
  ~省略~

他と比べて初回が遅いですね。

削除する方は特に影響ないはずなのですが。。。

検証 3. Linq をなくしてみる

検証1. のコードから、 Linq を外してみるとどうなるでしょうか。

ListComparer.cs

~省略~
  private void UpdateModels() {
    
    List< int> removeIndexes = new List< int>();
        for (int i = 0; i < models.Count; i++) {
            bool found = false;
            for (int j = 0; j < SearchIds.Count; j++) {
                if (SearchIds[j] == models[i].Id)   {
                    found = true;
                    break;
                }
            }

            if (found == false) {
                removeIndexes.Add(i);
            }
        }

        removeIndexes.Sort();
        
        // Remove していくので逆から実行.
        for (int i = removeIndexes.Count - 1; i >= 0; i--) {
            models[removeIndexes[i]].DestroyModel();
            models.RemoveAt(removeIndexes[i]);
        }

        for (int i = 0; i < SearchIds.Count; i++) {
            bool idFound = false;

            for (int j = 0; j < models.Count; j++) {
                if (models[j].Id == SearchIds[i])   {
                    idFound = true;
                    break;
                }
            }
            // 見つかったものにも処理を行う場合は models[SearchIds[i]] で.
            if (idFound == false)   {
                models.Add(GenerateModel(SearchIds[i], Instantiate(BaseCube)));
            }
    }
  }
  ~省略~

流石というかなんというか、 GC Alloc のサイズが小さくなり、速度も速くなりました。

とはいえ変数の量が増えたりコードが長くなってしまうため、毎フレーム実行しまくるとか高パフォーマンスが要求される場面でなければ避けたい、というのが個人的な本音です(ー_ー;)

検証 4. 検証3. を removeIndexes を作らずに実行してみる

検証3. では、 models が Id 順に揃っていないこともあり、インデックスを持つ List(removeIndexes) を作っていました。

これを作らないで直接 Remove していくと何か変わるでしょうか。

自作クラスのソート

まず models を Id でソートする必要があります。

以前試したときのコードを基に SampleModel をソートできるようにします。

SampleModelComparer.cs

using System.Collections.Generic;

// ソート用のクラス.
public class SampleModelComparer: IComparer< SampleModel> {
    public int Compare(SampleModel x, SampleModel y) {
        if (x == null) {
            return y == null ? 0 : 1;
        }
        if (y == null) {
            return -1;
        }
        return x.Id.CompareTo(y.Id);
    }
}

ListComparer.cs

~省略~
  private SampleModelComparer comparer = new SampleModelComparer();
~省略~
  private void UpdateModels() {
    // Remove していくので逆から実行.
        for (int i = models.Count - 1; i >= 0; i--) {
        bool found = false;
            for (int j = 0; j < SearchIds.Count; j++) {
                if (SearchIds[j] == models[i].Id) {
                    found = true;
                    break;
                }
            }
            if (found == false) {
                models[i].DestroyModel();
                models.RemoveAt(i);
            }
        }   
        for (int i = 0; i < SearchIds.Count; i++) {
            bool idFound = false;
    
            for (int j = 0; j < models.Count; j++) {
                if (models[j].Id == SearchIds[i]) {
                    idFound = true;
                    break;
                }
            }
            // 見つかったものにも処理を行う場合は models[SearchIds[i]] で.
            if (idFound == false) {
                models.Add(GenerateModel(SearchIds[i], Instantiate(BaseCube)));
            }
        }
    // 要素を追加した後にソート.
        models.Sort(comparer.Compare);
  }
  ~省略~

わざわざソート用のクラスを作った割には。。。という悲しい結果となりました。

もし removeIndexes で int 型の Id ではなく、重いデータを持つものを格納していたら違った結果になるかもしれません。

結果

f:id:mslGt:20180916073529p:plain

今回試した中では、検証3. の Linq をなくした場合が一番良い結果となりました。

ただ検証3. でも触れた通り、コードが長くなったり変数が多くなったりして、ミスが発生しやすいという懸念もありますので、状況に合わせて選択したいと思います。

また今回の検証では Linq の方が遅い、という結果になりましたが、もう少し GC Alloc の量が少なかったり、速く処理できるコードも書けるかもしれません。

こちらについてはもう少し良いものが見つかれば追加して比較してみたいと思います。

【ASP.NET Core】任意のローカルファイルを読み込み・書き出し

はじめに

ASP.NET Core では Static なファイルはデフォルトで wwwroot に置きます。

が、今回は PC の任意の場所に置いたファイルを Static ファイルとして扱うことができるか、ということを試してみました。

ついでなのでクライアント( Unity )に画像を渡し、それを Texture として読み込んで表示してみることにします。

ローカルファイルにアクセスする

例えば Documents ディレクトリに置いたファイルに、ファイル名を指定してアクセスしてみます。

// Documents をルートディレクトリとして物理ファイルのプロバイダーを生成.
using(PhysicalFileProvider provider = new PhysicalFileProvider(@"C:\Users\example\Documents")) {
  // Documents > memo.txt のファイル情報を取得.
  IFileInfo fileInfo = provider.GetFileInfo(@"memo.txt");
  logger.LogInformation("name: " + fileInfo.Name + " exists: " + fileInfo.Exists + " directory?: " + fileInfo.IsDirectory);
}

実行した結果(ログの内容)は以下の通りです。

name: memo.txt exists: True directory?: False

今度はルートディレクトリだけを指定して、その下にあるディレクトリ、ファイルの情報を取得してみます。

なお該当のディレクトリには下記のようなファイル、ディレクトリがあるものとします。

ImageSample ← ルートディレクトリとして指定
  Lsample1.jpg
  Ldir1
    Ldir2
      Lsample2.jpg
    Lsample3.jpg

// ImageSample をルートディレクトリとして物理ファイルのプロバイダーを生成.
using (PhysicalFileProvider provider = new PhysicalFileProvider(@"C:\Users\example\Pictures\ImageSample")) {
  // サブディレクトリを指定する場合は GetDirectoryContents の引数で指定.
  IDirectoryContents contents = provider.GetDirectoryContents(string.Empty);
  foreach (IFileInfo fileInfo in contents){
      logger.LogInformation("name: " + fileInfo.Name + " exists: " + fileInfo.Exists + " directory? " + fileInfo.IsDirectory);
  }
}

実行した結果(ログの内容)は以下の通りです。

name: sample1.jpg exists: True directory?: False
name: dir1 exists: True directory?: True

対象のディレクトリだけが対象となっていて、それより下の階層の情報は取れていません。

全部取得してみる

このノリで、ルートディレクトリ以下のディレクトリ、ファイル情報をまとめて取ってみることにします。

またファイルは、画像ファイルのみをターゲットとしてみます。

まず情報を格納する構造体を用意します。

ImageFile.cs

public struct ImageFile {
    public string DirectoryPath { get; }
    public string FileName { get; }

    public ImageFile(string setDirectoryPath, string setFileName) {
        DirectoryPath = setDirectoryPath;
        FileName = setFileName;
    }
}

別にクラスでもいいのですが、 More Effective C# に従って、データを格納するものは構造体にすることにします。

public List< ImageFile> Load() {
  List< ImageFile> loadedFiles = new List< ImageFile>();

  using (PhysicalFileProvider provider = new PhysicalFileProvider(@"C:\Users\example\Pictures\ImageSample")) {
      loadedFiles.AddRange(
          Load(provider, @"\"));
  }
  // 取得した情報をログに出力してみる.
  foreach (ImageFile f in loadedFiles) {
      logger.LogInformation("directory: " + f.DirectoryPath + " file: " + f.FileName);
  }
  return loadedFiles;
}
private static List< ImageFile> Load(PhysicalFileProvider provider, string parentDirectory) {
    List< ImageFile> loadedFiles = new List< ImageFile>();

    foreach (IFileInfo fileInfo in provider.GetDirectoryContents(parentDirectory)) {
        if (fileInfo.IsDirectory) {
            // ディレクトリならその下にあるファイルを取得する.
            loadedFiles.AddRange(
                Load(provider, parentDirectory + fileInfo.Name + @"\"));
        }
        // IFileInfo では拡張子を取れないようなので、正規表現でファイルを選別.
        else if(Regex.IsMatch(fileInfo.Name, "(jpg|png)$")) {
            loadedFiles.Add(
                new ImageFile(parentDirectory, fileInfo.Name));
        }
    }
    return loadedFiles;
}

ログの出力結果は以下の通りです。

directory: \dir1\dir2\ file: sample3.jpg
directory: \dir1\ file: sample2.jpg
directory: \ file: sample1.jpg

結果の通り、階層が深い方から順に並ぶことになるので、 DirectoryPath でソートした方が良いかもしれません。

取得した情報を JSON として扱う

取得した List< ImageFiles> ですが、クライアント側に渡せるよう、 JSON として扱えるようにしたいと思います。

例えば GET リクエストでアクセスしたときに、 JSON で返す方法は下記のようにできます。

HomeController.cs

namespace WebApplication1.Controllers {
  public class HomeController : Controller {
    ~省略~
    [Route("/getfileinfo")]
    [Produces("application/json")]
    public List< ImageFile> Index() {
      // C:\Users\example\Pictures\ImageSample のデータを取得して返す.
      return Load();
    }
  }
}

ただこの場合、アクセスするたびにデータを作ることになるため、効率が悪いです。

そのため、データを読み込んだ後 JSON ファイルとして一旦保存しておき、アクセスされたときはそれを渡すようにしてみます。

JSON に変換する

DataContractJsonSerializer を使うことで JSON に変換することができます。

まず DataContractJsonSerializer で扱うことができるよう、 ImageFile を変更します。

ImageFile.cs

using System.Runtime.Serialization;

namespace WebApplication1.FileAccessers {
    [DataContract]
    public struct ImageFile {
        [DataMember]
        public string DirectoryPath { get; private set; }
        [DataMember]
        public string FileName { get; private set; }

        public ImageFile(string setDirectoryPath, string setFileName) {
            DirectoryPath = setDirectoryPath;
            FileName = setFileName;
        }
    }
}

注意が必要な点は2つです。

  1. クラスに DataContract 、 変数(プロパティ) に DataMember のアトリビュートをつける必要があります。
  2. 各プロパティに set が必要で、 get だけにしているとエラーになります( private でも OK )。

で、これを使って JSON に変換してみます。

~省略~
using System.Runtime.Serialization.Json;
using System.Text;
~省略~
    private byte[] Serialize(List< ImageFile> originalData) {

        using (MemoryStream ms = new MemoryStream()) {
            // List< ImageFile> を変換する場合は DataContractJsonSerializer の型も List< ImageFile> とする.
            DataContractJsonSerializer serializer = new DataContractJsonSerializer(typeof(List< ImageFile>));

            serializer.WriteObject(ms, originalData);
            return ms.ToArray();
        }
    }
  }
}

ファイルを書き出すときのことを踏まえて byte[] で返していますが、 string でほしい場合は Encoding.UTF8.GetString(json, 0, json.Length) のように変換すれば OK です。

ファイルの書き出し

ではこの JSON をファイルとして書き出します。

ここでは FileStream を使います。

// JSON 変換した byte[] データ.
byte[] result = Serialize(loadedFiles);

using (FileStream stream = new FileStream(@"C:\Users\example\Pictures\ImageSample\createdjson.json", FileMode.Create)) {
    stream.Write(result, 0, result.Length);
}

特に ASP.NET Core でなくても変わらないですね。

ただ今回は同期的に書き込んでいますが、 WriteAsync にした方がよいかもしれません。

ローカルのディレクトリをStaticFilesに追加する

ここまで扱ってきたファイルを、外からも見えるようにしてみたいと思います。

その方法として、ローカルのディレクトリをStaticFilesに追加する、というものがあります。

Startup.cs

~省略~
public void Configure(IApplicationBuilder app, IHostingEnvironment env) {
  ~省略~
  // wwwroot を Static ファイルのルートとして設定する.
  app.UseStaticFiles();

  // ローカルの Pictures にある ImageSample をStatic ファイルのディレクトリとして追加.
  app.UseStaticFiles(new StaticFileOptions {
      FileProvider = new PhysicalFileProvider(
          Path.Combine(@"C:\Users\example\Pictures", "ImageSample")),
      RequestPath = "/img"
  });
  ~省略~
}
~省略~

こうすることで、例えば localhost:5XXX/img/sample1.jpg にアクセスすると C:\Users\example\Pictures\ImageSample\sample1.jpg が表示されるようになります。

アプリの発行後、外部からファイルを差し替えたい、といった場合に利用できそうです。

おわりに

案外簡単にアクセス可能にできるものなのだなぁ。というのが正直な感想です。

場合によってはルートディレクトリ内の特定のディレクトリ/ファイルだけアクセスさせないなどの措置も必要になりそうですね。

こちらについては別途調べて書き残そうかと思っています。

参照