vaguely

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

【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 が表示されるようになります。

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

おわりに

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

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

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

参照

ASP.NET Core でのログ出力( ILogger + NLog )

はじめに

今回はログ出力を追ってみることにします。

.NET でログを出力するライブラリはたくさんあるようですが、今回は組み込みである ILogger を使ってみます。

内容としては ILogger の使い方、特にファイルに内容を出力する方法が中心になる予定です。

httpsについて

いきなり違う話から入るのですが、 ASP.NET Core2.1 でプロジェクトを作ると https://localhost:5XXXhttps でページを開くようになります。
(Visual Studio でプロジェクトを作る場合、プロジェクト生成時にチェックを外すと http で開くよう設定を変更できます)

それは良いのですが、 Firefox では証明書が不正となりページが開けません。

ググってみたところ、 https://localhost:(ポート番号) を例外として設定する以外に開く方法はなさそうです。

localhost なんだし多めに見て、という気持ちはありますが、まぁ仕方ありませんね。

実際のリリース時には正しい証明書などを用意することとして、開発中は例外設定してしまいたいと思います。

ログ出力してみる

気を取り直して、とりあえずログを出力してみます。

例えば HomeController.cs からログを出力したい場合、 HomeController クラスのコンストラクタに ILogger< HomeController> を渡します。

HomeController.cs

using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;

namespace WebApplication1.Controllers {
    public class HomeController : Controller {
        private readonly ILogger< HomeController> logger;

        // DI でインスタンスをセット.
        public HomeController(ILogger< HomeController> logger) {
            this.logger = logger;
        }
        public string Index() {
            // INFO レベルでログを出力.
            logger.Log(LogLevel.Information, "Hello work!");

            return "hello";
        }
        ~省略~
    }
}
  • ILogger< TCategoryName> の型は、ログの「カテゴリ」を指定するためのもので、
    文字列で指定することもできるが、ログを書き込むクラスを指定する必要があるため型を渡す方が簡単にできます。
  • 「カテゴリ」が何を指すのかは正確にはわからなかったのですが、そのログがどのクラスから出力されたのかを判別するもの、
    というくらいのものであるようです(そのためクラスの完全修飾名が必要)。

Service への追加?

DI ということで、以前と同じように Startup クラスで Service に追加…

する必要はありません。

Program クラスで下記を使用している場合、デフォルトのログ出力設定が CreateDefaultBuilder の中で行われます。

Program.cs

using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;

namespace WebApplication1 {
    public class Program {
        public static void Main(string[] args) {
            CreateWebHostBuilder(args).Build().Run();
        }
        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            WebHost.CreateDefaultBuilder(args)
                .UseStartup< Startup>();
    }
}

WebHost.cs

// Decompiled with JetBrains decompiler
// Type: Microsoft.AspNetCore.WebHost
~省略~
public static IWebHostBuilder CreateDefaultBuilder(string[] args) {
    IWebHostBuilder hostBuilder = new WebHostBuilder()
        ~省略~
      .ConfigureLogging((Action) ((hostingContext, logging) =>
      {
        logging.AddConfiguration((IConfiguration) hostingContext.Configuration.GetSection("Logging"));
        logging.AddConsole();
        logging.AddDebug();
      }))
    ~省略~
}

ILoggerFactory

なおググっている中で、 Startup の Configure の引数として ILoggerFactory を渡しているものもありました。

Startup.cs

~省略~
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) {
  ~省略~  
}

で、この ILoggerFactory でも AddConsole や AddDebug を実行することはできるようでした。

両者の使い分けに悩むところですが、少なくとも Microsoft Doscs を見る限りだと前者を使う方が良さそうです。
(後者はダメとまでは書かれていませんが、特に触れられていないため)

docs.microsoft.com

NLogを試してみる

Console に出力する分には問題がないのですが、ファイル出力したい今回のような場合、 Provider を自分で作る(またはサードパーティーのものを使う)必要があるようです。

今回は NLog を使ってみることにしました。

NLog の Wiki を基に進めていきます。

github.com

インストール

NuGet で NLog.Web.AspNetCore をインストールします
(いくつか種類がありますが、 ASP.NET Core 用のものを選びます。 ver.4.6 でした)。

ファイルを追加する

1.NLog の設定ファイル( nlog.config )をプロジェクト直下に作ります。 2.nlog.config のプロパティで、 詳細設定 > 出力ディレクトリにコピー の値を「常にコピーする」に変更します。

f:id:mslGt:20180906043538j:plain

※画像は Visual Studio ですが、 Rider の場合はファイル上で右クリック > Properties > Copy to output directory から設定できます。

3.Program.cs で NLog を有効にします。

Program.cs

using System;
using System.IO;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Logging;
using NLog.Web;

namespace WebApplication1 {
    public class Program {
        public static void Main(string[] args) {
            // nlog.configの読み込み.
            var logger = NLog.Web.NLogBuilder.ConfigureNLog("nlog.config").GetCurrentClassLogger();
            try {
                CreateWebHostBuilder(args).Build().Run();
            }
            catch (Exception ex) {
                logger.Error(ex, "Stopped program because of exception");
                throw;
            }
            finally {
                NLog.LogManager.Shutdown();
            }
        }

        public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
            new WebHostBuilder()
                .UseKestrel()
                .UseContentRoot(Directory.GetCurrentDirectory())
                .UseIISIntegration()
                .UseStartup< Startup>()
                .ConfigureLogging((hostingContext, logging) => {
                    // NLog 以外で設定された Provider の無効化.
                    logging.ClearProviders();
                    // 最小ログレベルの設定.
                    logging.SetMinimumLevel(LogLevel.Trace);
                })
                // NLog を有効にする.
                .UseNLog();
        }
    }
}

nlog.configを読む

Wiki の「2.Create a nlog.config file.」を見てみると、結構複雑な感じがしてきます。

とはいえ個別にコメントを書いてくれているのと、 Wiki に nlog.config の個別ページも用意されているため、追いかけるのは容易そうです。

github.com

  • nlog: nlog.config 全体の設定、 internal-nlog.txt の出力設定を行います。
  • extensions: nlog.config の中で使われる、「${ }」のような表現方法を有効にするための機能を有効にします。
  • targets: ログの出力タイプ(ファイルとかコンソールとか)、名前( rules でこの値を使ってターゲットを指定します)、ファイルの場合はその出力先などを指定します。
  • rules: targets の中で設定したそれぞれのターゲットに、どのレベル( Info, Warning など)のログを出力するかなどのルールを指定します。

出力されるファイルは下記の三つです。

  • internal-nlog.txt: NLog の内部処理に関するログが出力されます。
  • nlog-all-${shortdate}.log: Trace レベルのログを出力します。
  • nlog-own-${shortdate}.log: Microsoft.* 名前空間下の Info レベルのログを除く、Trace レベルのログを出力します。

ログレベルについては下記参照、ということなのですが、Trace レベルが「メソッド X の始め、メソッド X の終わり、etc.」のように書かれていて、あまりよく分かりませんでした(- _ -;)

github.com

出力されたものを見ると、呼ばれたメソッドの情報が出力される、ということのようでした。

また rules で設定されるルールで、 nlog-all-${shortdate}.log と nlog-own-${shortdate}.log のルールは同じように見えるのですが、間に final="true" を持ったルールが追加されることで、Microsoft.* 名前空間下の Info レベルのログが除かれる結果となっています。

nlog.config

~省略~
  < rules>
    < !--All logs, including from Microsoft-->
    < logger name="*" minlevel="Trace" writeTo="allfile" />

    < !--Skip non-critical Microsoft logs and so log only own logs-->
    < logger name="Microsoft.*" maxLevel="Info" final="true" /> < !-- BlackHole without writeTo -->
    < logger name="*" minlevel="Trace" writeTo="ownFile-web" />
  < /rules>
  ~省略~

nlog.configを書く

ではこのサンプルを基に、自分でも nlog.config を書いてみようと思います。

nlog.config

< ?xml version="1.0" encoding="utf-8" ?>
< !-- NLog 内部のログはいったん出力しないこととする -->
< nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      autoReload="true">

  < extensions>
    < add assembly="NLog.Web.AspNetCore"/>
  < /extensions>

  < targets>
    < !-- コンソールに出力  -->
    < target xsi:type="Console" name="alloutput"
            layout="${longdate}|${event-properties:item=EventId_Id}|${uppercase:${level}}|${logger}|${message} ${exception:format=tostring}" />

    < target xsi:type="File" name="ownFile-web" fileName="C:\temp\logs\${date:format=yyyy}\${date:format=MM}\nlog-own-${shortdate}.log"
            layout="${longdate}|${event-properties:item=EventId_Id}|${uppercase:${level}}|${logger}|${message} ${exception:format=tostring}|url: ${aspnet-request-url}|action: ${aspnet-mvc-action}" />

    < target xsi:type="File" name="ownFile-access" fileName="C:\temp\accessLogs\${date:format=yyyy}\${date:format=MM}\nlog-access-${shortdate}.log"
            layout="${longdate},${message} ${exception:format=tostring}" />
  < /targets>
  < rules>
    < !--コンソールには Trace レベル以上のログすべてを出力-->
    < logger name="*" minlevel="Trace" writeTo="alloutput" />
    < !--Microsoft.* のクラスの Info レベル以下のログはスキップ-->
    < logger name="Microsoft.*" maxLevel="Info" final="true" />
    < logger name="*" minlevel="Info" writeTo="ownFile-web" />
    < !-- HomeController に対するアクセスログ( Info レベル)のみ記録 -->
    < logger name="WebApplication1.Controllers.HomeController" minLevel="Info" maxLevel="Info" writeTo="ownFile-access" />
  < /rules>
< /nlog>

これで例えば今日( 2018.09.06 )実行した場合、下記の場所にファイルが出力されます。

  • C:\temp\accessLogs\2018\09: HomeController にアクセスしたときのログ
  • C:\temp\logs\2018\09: Microsoft.* 以下のクラスを除くアプリケーション全体での、Info レベル以上のログ

※ NLog 内部処理のログを出力しない設定にしていますが、本来出すべきだとは思います。

おわりに

一応これでログ出力はできるようになりましたが、まだいくつか課題はあります。

  1. ログがどのように出力されるかわかっていない → Program.cs に追加した NLog 関連のコードからたどっていくのが良さそう?
  2. ログを出力するレベルや、フォーマットなどログ設計について学習が必要
  3. Visual Studio で nlog.config を編集しているときに、設定が正しく反映されないことがあった
  4. Rider で nlog.config を開くと xsi:type="Console" などでエラーとして表示(ビルドや実行には問題なし)

3.については Program.cs でログ出力すると直ったり、4.は nlog.config に限らず IntelliJAndroid Studio などでも見かけた現象だったりするので、改善の余地もありそうです。

これらについてはおいおい解決していきたいと思います。

また今回は触れませんでしたが、 NLog の出力先として DB やメールなども利用可能(らしい)ですので、そちらも試してみたいと思います。

参照

https

ILogger

NLog

Rider2018.2で日本語コメントがTypo扱いされる

はじめに

Rider2018.2 にアップデートしたところ、日本語がほぼ全て Typo 扱いされるようになりました。

f:id:mslGt:20180830071802j:plain

この辺りの話を見ていると、スペルチェックの機能が変更されたのが原因のようです。

もんりぃ先生👨‍🏫マンガでわかるUnity連載中! on Twitter: "Rider 2018.2 EAP は日本語コメントが軒並み typo 扱いになる…。まぁ、「英語で書けやー!」って言われたら「あ、はい。ごめんなさい。」って感じだけどw"

blog.jetbrains.com

今後の更新で英語以外でも正しくスペルチェックが効くようになるんじゃないかな?と期待はしているのですが、現状の対処法としては

  1. Typo 扱いされても気にしない
  2. コメントを片っ端から辞書登録
  3. コメントに対するスペルチェックを無効にする

があると思っています。

1.が最強ではありますが、やっぱり気になる。2.は現実的ではないし。。。ということで、3.をやってみたのでそのメモです。

設定を変更する

スペルチェックの項目というと、Setting > Editor > Spelling > ReSpeller が引っかかるのですが、今回の目的には合致しないようでした。

コメントに対するスペルチェックを無効にするには、 Settings > Inspection Settings > Inspection Severity > C# > Spelling Issues > Typo in comment のチェックを外します。

f:id:mslGt:20180830073040j:plain

あとは Save ボタンを押せば OK です。

せっかくの機能を止めてしまう、というのは心苦しくはあるので、もう少し良い解決方法があればとは思うのですが。

参照