20190209のC#に関する記事は8件です。

TodoListを自動配信するソフトを作るために.NETとソフトウェアパッケージ化で詰まったよ

 前書き

初心者脱却のために、どうしてもGUIプログラムを作りたい・・・!と思ったのとせっかくなので、TodoListdonをグレードアップしてみようと思いました

電気系プログラミングガチ素人学生が初心者脱却のためのコアのプログラムを作ったら思いっきり脱線した話

 この記事で伝えたいこと

  • 干渉関係は解決が案外難しい
  • 人に提供するときはインストーラー使うと便利やで?
  • 初心者脱却はできてるのか・・・?

話はいい。ソースはどこだ?

こちらに。
https://github.com/sanbongazin/TodoListDon-GUI

先ずは設計だ

コマンドで設計した内容ができているのだから、その内容をGUIに落とし込むだけ!と考えていたのがアホの極みでした。
画面の置き方をデザインビューで行います。今回はフォームアプリを利用しています。

 画面遷移ってどうすればいいのよ・・・・

まず設計からつまづきました。コマンドプロンプトではクラス化すればいいだけだもの・・・ウィンドウどうすれば動くんや!!!って思ったら簡単にわかりました。

form2.ShowDialog();

これで表示できるし、閉じられるまで待ってくれるので、エラーが起きづらい!!めっちゃ楽!!!

追加は手軽にできるように

TodoListをまずは追加したいだろ!って思いながら、まずは、リストを作ってそれを追加します。また、TodoListを裏方でListに格納しています。

        private void ADD_button_Click(object sender, EventArgs e)
        {
            string text_value = Add_Text.Text;
            //TodoList.Add(text_value);
            TodoList_GUILIST.Items.Add (text_value);
            Add_Text.Clear();
            TodoList = this.TodoList_GUILIST.Items.Cast<string>().ToArray();
        }

Todoの一通りの機能をどうやって実現するのか。

追加、削除はListBoxで追加や、削除は一行で済みますが、問題は保存と読み込み。そこで、裏でリストを格納させ、リストとデータをリンクさせ、テキストファイルとして保存させることで、保存、読み込みを可能としています。

image.png

なぜ必要?

保存の際にリストにしておくことで、foreach処理で一度文字数を連結することを可能にしています。

        public void Toot() {
            registeredApp = ApplicaionManager.RegistApp(host, "TodoListDon", Scope.Read | Scope.Write | Scope.Follow).Result;
            var client = new MastodonClient(host, AccessToken);

            UserName = HashTagOption.Text;

            int i = 0;
            var TodoString = ""; 
            foreach (var s in TodoList) {
                i++;
                TodoString += i + ":[ ]" + s + Environment.NewLine;
            }
            client.PostNewStatus(status: TodoString +"#"+ UserName +"_On_TodoListDon");
        }

思いつく簡単な処理がこれでしたので、今回はこの形にしてみました。

トークンどうやって管理しようか

Mastdonで、トゥートするにも権限を付与したアクセストークンが必要です。そこで以前はテキストに裸で格納しているという危険な手法をとっていたので、それらを一般用で頒布するのはとんでもない方法なのでやめました。そこで、Qiitadonの方々に、レジストリを使ってみては?という案をいただきました。

そこで、使ってみることにしました。

            try
            {
                //以下の行でレジストリを取り扱う
                Microsoft.Win32.RegistryKey regkey = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(@"Software\TodoListDon\sub", false);
                //読み込む
                AccessToken = (string)regkey.GetValue("AccessToken");
                host = (string)regkey.GetValue("hostname");
                Toot();
            }
            catch (NullReferenceException e)
            {
                    form2.FormClosed += new FormClosedEventHandler(Form2_FormClosed);
                    try
                    {
                        form2.ShowDialog();
                    }
                    catch (Exception)
                    {
                        form2.Visible = false;
                    }
            }

tryセクションで、レジストリを読みに行きます。今回はSoftwareにkeyを作成しました。そこでhostの情報と、アクセストークンを取得するようにしました。また、もし取得できない場合、catchします。新規作成の場合はダイアログを作成します。ダイアログのソースコードは以下のようになります。この認証のソースコードは結構再利用できます。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using Mastodot.Enums;
using Mastodot.Utils;
using Mastodot.Entities;
using Mastodot.Exceptions;
using Microsoft.Win32;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO;

namespace WindowsFormsApplication1
{
    public partial class Oauth : Form
    {
        public string host;
        public Mastodot.Entities.RegisteredApp registeredApp;
        public string code;
        private MainWindow main = new MainWindow();

        public Oauth()
        {
            InitializeComponent();
        }

        public void GenerateButton_Click(object sender, EventArgs e)
        {
            host = InstanceInput.Text;
            try
            {
                registeredApp = ApplicaionManager.RegistApp(host, "TodoListDon", Scope.Read | Scope.Write | Scope.Follow).Result;
            }
           catch (AggregateException) {
                MessageBox.Show("正しい値を入力してください。",
                "エラー",
                MessageBoxButtons.OK,
                MessageBoxIcon.Error);
            }
            var url = ApplicaionManager.GetOAuthUrl(registeredApp);

            OAuthlink.Text = url;
        }

        private void OAuthlink_LinkClicked(object sender, LinkLabelLinkClickedEventArgs e)
        {
            //リンク先に移動したことにする
            OAuthlink.LinkVisited = true;
            //ブラウザで開く
            System.Diagnostics.Process.Start(OAuthlink.Text);
        }

        private async void OAuthButton_ClickAsync(object sender, EventArgs e)
        {

            // 操作するレジストリ・キーの名前
            string registryKeyName = @"Software\TodoListDon\Sub";
            // 取得処理を行う対象となるレジストリの値の名前
            string registryValueName = "hostname";

            //キー(HKEY_CURRENT_USER\Software\Sample)を開く
            using (RegistryKey registryKey = Registry.LocalMachine.CreateSubKey(registryKeyName))
            {
                // レジストリの値を設定
                registryKey.SetValue(registryValueName, host);
            }


            code = OAuthInput.Text;
            var tokens = await ApplicaionManager.GetAccessTokenByCode(registeredApp, code);



            // 取得処理を行う対象となるレジストリの値の名前
            registryValueName = "AccessToken";

            //キー(HKEY_CURRENT_USER\Software\Sample)を開く
            using (RegistryKey registryKey = Registry.LocalMachine.CreateSubKey(registryKeyName))
            {
                // レジストリの値を設定
                registryKey.SetValue(registryValueName, tokens.AccessToken);
            }

            Close();
        }

        private void Cancel_Button_Click(object sender, EventArgs e)
        {
            Close();
        }
    }
}

簡易的ですが、これでレジストリに記録が可能になります。テキストボックスにリンクを入力し、アクセストークンを取得します。アクセストークンは、レジストリに格納されます。

さて、完成したぞ

コンパイルしようと思ったら何故かこんなエラーが・・・
image.png

なにこれ???と思いつつ、検索しても出てこない。どうすればええんや・・・と思ったのですが、VS2017にするとなぜか実行可能に・・・

ソフトウェアが頒布されるために・・・

ソフトウェアを頒布したいならやはりインストーラは必要ではと感じたため、どうやってインストーラを作ろうかと考えました。しかし、VisualStudioならそういったパッケージも簡単に作成可能のようです。
image.png

拡張機能からダウンロード可能です。さて、これを活用しましょう。

image.png
ソリューションを右クリックして、新しいプロジェクトを追加しましょう。

image.png
その他のプロジェクトにセットアップを追加できます。

そうすると3つのフォルダーが出現します

image.png

ApplicationFolderを右クリックすることで、メニューを開き、

image.png

プロジェクト出力をクリックしましょう。これで出力されたソフトウェアをインストールパッケージに加えることができます。ソフトウェアをデスクトップやスタートメニューに加えることも可能です。その際は以下のようにショートカットを作り、それぞれのフォルダーに入れましょう。

image.png

また、PDFをフォルダーに追加することも可能です。これにより、説明書なども入れることができます。

これで初めてのソフトウェアづくりは完遂できました・・・・

これで学びになったの?

はじめて初心者向けのチュートリアルを一つクリアできたというところでしょうか・・・学んだことは

  • ソフトウェア設計はコンソールベースで作成すると、バックグラウンドでの処理を意識しやすい
  • GUIとCUIではソフトウェアの管理の煩雑さが段違い
  • それでもC#はかなり設計を形にしやすい言語でもあると感じる
  • APIをたたくときは、中身のソフトウェアの動作を見ることも重要である
  • 構築するIDEは最新版を使おうね
  • ソフトウェアパッケージは意外に作成が簡単だよ  

あたり前なものもあるかもしれません。しかし、電気系プログラムガチ素人である私にはとても新鮮なことも多かったです。また、コーディングもまだまだ非効率であったり、穴もありそうです。まだまだ学ぶことは多そうですね・・・

次回の課題は

今度は動作OSを拡大したいところです。また、ソフトウェアデザインの必要性を強く感じました。あとは・・・フォームじゃなくてXamlで作りたくなりました・・・できるだけ新しい技術に触れていたいところです。

参考文献

Visual Studio 2017でインストーラ作成
https://www.osadasoft.com/visual-studio-2017%E3%81%A7%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%A9%E4%BD%9C%E6%88%90/

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[C#Tips.ログ]Disposable-usingを使った簡単計測ログ出力方法

動機

ログ

みなさんログは出してますか?自分は割と出しています。だってお客さんからクレーム来た時に何が起きたか、証言だけじゃ理解できないし、人間の記憶なんて適当ですもんね。ということでこのボタン押したとかコンボボックスを選択したとかの操作ログはうまいこと仕組み作って自動的にログに出るようにしてます。

計測ログ

で、今回は操作ログではなくて計測系ログの話。開発末期に「(他人の書いたコードの)パフォーマンスの最適化」という名の汚れ仕事を任されたりするんですが、そういう時にどこの処理が重いかを見極めるために自分がまず行うのが、原始的ですが計測ログを仕込んでいくこと。プロファイラだとノイズが多すぎて見極めにくいので。。(いつもプロファイラを使う→うーんわからん→ログ仕込むという流れになってます。。XAMLの最適化の場合はVS標準のプロファイラは必須だけどそれはまた別の話)。

ということでどんな感じで実装しているかを紹介してみたいと思います。

Log using using (usingを使ったログ出力)

ポイント:計測前ログを出力し、計測終了ログをIDisposableで返し、usingを使うことにより、usingブロック抜けたときに計測終了ログが出力される

ご存知の通りC#にはusing({Disposable})構文があり、usingブロックを抜けたときにDisposeが実行されるという特性を使います。アイデアはReactive Extensionsのイベント購読関数Subscribeの戻り値がIDisposableになっていて、それをDisposeするとイベントが解除されるというところから。ということでコード例。

ActionからDisposeを生成するクラス

まず下準備としてSystem.ReactiveをNugetからインポートする、もしくは以下のようなクラスを作ります。
要はActionを渡せばDispose時に実行してくれるクラスです。

    /// <summary>Disposableで実行するアクションをラップ</summary>
    /// <seealso cref="System.IDisposable" />
    public class DisposableAction : IDisposable
    {
        /// <summary>空オブジェクト</summary>
        public static readonly DisposableAction Empty = new DisposableAction(() => { });

        /// <summary>Disposeで実行する処理</summary>
        private readonly Action _disposableAction;

        /// <summary>Initializes a new instance of the <see cref="DisposableAction"/> class.</summary>
        /// <param name="disposableAction">TDisposeで実行する処理</param>
        /// <exception cref="ArgumentNullException">disposableAction</exception>
        public DisposableAction(Action disposableAction)
        {
            _disposableAction = disposableAction ?? throw new ArgumentNullException(nameof(disposableAction));
        }

        public void Dispose() => _disposableAction.Invoke();
    }

シンプルな例:コンソール出力でメソッドの処理時間計測

さて、Disposableができたら以下のようなメソッドを定義してみます。

  • 機能

    • 渡されたメソッド名をコンソール出力する
    • 戻り値のIDisposableをDisposeすると、メソッド名と経過時間をコンソール出力する
        /// <summary>指定文字列をコンソール出力</summary>
        /// <param name="methodName">Name of the method.</param>
        /// <returns>指定文字と経過時間をコンソール出力するDisposable</returns>
        public static IDisposable WriteLineMeasureTimeAsDisposable([CallerMemberName]string methodName = "")
        {
            Console.WriteLine($"Start [{methodName}]");

            // ここで開始時間を保存しておく
            var startDateTime = DateTime.Now;
            return new DisposableAction(() =>
            {
                // ここで経過時間を求める
                var collapsedTime = DateTime.Now - startDateTime;
                Console.WriteLine($"End [{methodName}] took {collapsedTime.TotalMilliseconds:F2}ms");
            });
        }

※ System.Reactiveを参照している場合、Disposableを生成する方法が以下のようにDiposable.Create(Action)となります。これ以降この記事ではnew DisposableActionの方を使います。

        /// <summary>指定文字列をコンソール出力</summary>
        /// <param name="methodName">Name of the method.</param>
        /// <returns>指定文字と経過時間をコンソール出力するDisposable</returns>
        public static IDisposable WriteLineMeasureTimeAsDisposable([CallerMemberName]string methodName = "")
        {
            Console.WriteLine($"Start [{methodName}]");
            var startDateTime = DateTime.Now;
            return Disposable.Create(() =>
            {
                var collapsedTime = DateTime.Now - startDateTime;
                Console.WriteLine($"End [{methodName}] took {collapsedTime.TotalMilliseconds:F2}ms");
            });
        }

上記関数は以下のように使います。

    [TestClass]
    public class SimpleDisposableLogDemo
    {
        [TestMethod]
        public async Task SomeHeavyMethodDemo()
        {
            await this.SomeHeavyMethod();
        }

        private async Task SomeHeavyMethod()
        {
            // ここで先ほどの計測関数を実装
            using (WriteLineMeasureTimeAsDisposable())
            {
                await Task.Delay(2000);
            }
        }
    // ・・・・

↓コンソール出力

Start [SomeHeavyMethod]
End [SomeHeavyMethod] took 2013.49ms

というようにメソッドをusingを使って計測ログ関数で包むことにより、その間の経過時間が計測できます。CallerMemeberNameを使用することにより、呼び出し元のメソッド名が自動的に入るので、多くの場合引数を渡す必要も無い!

実践的な例

上記例ではアイデアの肝を示すためにシンプルな機能しかありませんが、実際に自分がどんな感じのメソッドを定義して使っているかを紹介します。

まず下準備としてロガーインターフェースがあって、例としてコンソール出力するロガーを実装してみます。

    /// <summary>ロガーインターフェース</summary>
    public interface ILogger
    {
        /// <summary>デバッグログ出力</summary>
        /// <param name="log">ログ</param>
        void Debug(string log);

        /// <summary>情報ログ出力</summary>
        /// <param name="log">ログ</param>
        void Info(string log);

        /// <summary>警告ログ出力</summary>
        /// <param name="log">ログ</param>
        /// <param name="ex">例外情報</param>
        void Warn(string log, Exception ex = null);

        /// <summary>エラーログ出力</summary>
        /// <param name="log">ログ</param>
        /// <param name="ex">例外情報</param>
        void Error(string log, Exception ex = null);

        /// <summary>致命ログ出力</summary>
        /// <param name="log">ログ</param>
        /// <param name="ex">例外情報</param>
        void Fatal(string log, Exception ex = null);
    }

    /// <summary>コンソール出力するロガー</summary>
    /// <seealso cref="TNakUtility.Logger.ILogger" />
    public class ConsoleLogger : ILogger
    {
        public void Debug(string log) => System.Diagnostics.Debug.WriteLine(CreateLogString(log));

        public void Info(string log) => ConsoleWriteLine(log);

        public void Warn(string log, Exception ex = null) => ConsoleWriteLine(log, ex);

        public void Error(string log, Exception ex = null) => ConsoleWriteLine(log, ex);

        public void Fatal(string log, Exception ex = null) => ConsoleWriteLine(log, ex);

        private void ConsoleWriteLine(string log, Exception ex = null, [CallerMemberName] string header = "")
        {
            // ログ出力
            Console.WriteLine(CreateLogString(log, header));
            if (ex != null)
            {
                // 例外情報出力
                Console.WriteLine(CreateLogString(ex.ToString(), header));
            }
        }

        /// <summary>ログに時間、ヘッダ(メソッド名)を付与</summary>
        /// <param name="log">The log.</param>
        /// <param name="header">The header.</param>
        /// <returns>[{時間}][{メソッド名}]{ログ}</returns>
        private static string CreateLogString(string log, [CallerMemberName] string header = "") =>
            $"[{DateTime.Now:G}][{header}]{log}";
    }

このロガーインターフェースに拡張クラスとして計測ログ出力関数を実装します。
キモはWriteLogAsDisposable関数で、この第一引数に文字列を引数に取ってログ出力する関数を渡せば使いまわせるようになっています。
以下のような機能を追加しています。

  • 引数無しの場合、クラス名(ファイル名の末尾)、メソッド名が自動で出力される。
  • 引数にパラメータ情報を渡したり、結果を取得する関数を渡したりすることでパラメータ情報、処理結果情報も出力される
  • ネストしてログ出力されている場合vや^の数でネストのレベルがわかる
    public static class ILoggerExtensions
    {
        /// <summary>計測ログ出力(デバッグ)</summary>
        /// <param name="self">ロガー</param>
        /// <param name="processName">処理名(メソッド名)</param>
        /// <param name="categoryName">カテゴリ名(ファイル名)</param>
        /// <param name="parameterName">パラメータ情報文字列</param>
        /// <param name="getResult">結果を取得する関数</param>
        /// <returns>計測終了ログ出力</returns>
        /// <exception cref="ArgumentNullException">self</exception>
        public static IDisposable DebugMeasureTimeAsDisposable(this ILogger self,
                                                               [CallerMemberName] string processName = "",
                                                               [CallerFilePath] string categoryName = "",
                                                               string parameterName = null,
                                                               Func<object> getResult = null)
        {
            if (self == null)
            {
                throw new ArgumentNullException(nameof(self));
            }

            return WriteLogAsDisposable(self.Debug, processName, categoryName, parameterName, getResult);
        }

        /// <summary>計測ログ出力(Info)</summary>
        /// <param name="self">ロガー</param>
        /// <param name="processName">処理名(メソッド名)</param>
        /// <param name="categoryName">カテゴリ名(ファイル名)</param>
        /// <param name="parameterName">パラメータ情報文字列</param>
        /// <param name="getResult">結果を取得する関数</param>
        /// <returns>計測終了ログ出力</returns>
        /// <exception cref="ArgumentNullException">self</exception>
        public static IDisposable InfoMeasureTimeAsDisposable(this ILogger self,
                                                              [CallerMemberName] string processName = "",
                                                              [CallerFilePath] string categoryName = "",
                                                              string parameterName = null,
                                                              Func<object> getResult = null)
        {
            if (self == null)
            {
                throw new ArgumentNullException(nameof(self));
            }

            return WriteLogAsDisposable(self.Info, processName, categoryName, parameterName, getResult);
        }

        private static int NestLevelCounter = 0;

        /// <summary>計測ログ出力</summary>
        /// <param name="writeLogAction">ログ書き込み処理</param>
        /// <param name="processName">処理名(メソッド名)</param>
        /// <param name="categoryName">カテゴリ名(ファイル名)</param>
        /// <param name="parameterName">パラメータ情報文字列</param>
        /// <param name="getResult">結果を取得する関数</param>
        /// <returns>計測終了ログ出力</returns>
        /// <exception cref="ArgumentNullException">writeLogAction</exception>
        private static IDisposable WriteLogAsDisposable(Action<string> writeLogAction,
                                                        [CallerMemberName] string processName = "",
                                                        [CallerFilePath] string categoryName = "",
                                                        string parameterName = null,
                                                        Func<object> getResult = null)
        {
            if (writeLogAction == null)
            {
                throw new ArgumentNullException(nameof(writeLogAction));
            }

            var parameterNameString = string.IsNullOrWhiteSpace(parameterName) ? string.Empty : $"_param:{parameterName}";
            var processNameString =
                $"[{Path.GetFileName(categoryName)}]({processName}{parameterNameString})";
            var startTime = DateTime.Now;
            var tempNestLevel = ++NestLevelCounter;
            var startArrowString = new string('v', tempNestLevel);
            writeLogAction.Invoke($"{startArrowString}Start {processNameString}");
            return new DisposableAction(() =>
            {
                var endArrowString = new string('^', tempNestLevel);
                --NestLevelCounter;
                var collapsedTime = DateTime.Now - startTime;
                var resultString = getResult != null ? $"=> Result:[{getResult()}]" : string.Empty;
                writeLogAction(
                    $"{endArrowString}End   {processNameString} took {collapsedTime.TotalMilliseconds:f2}ms {resultString}");
            });
        }
    }

実際の使用例は以下のような感じ

    [TestClass]
    public class LoggerDemo
    {
        private ILogger _logger;

        [TestInitialize]
        public void Initialize()
        {
            _logger = new ConsoleLogger();
        }

        /// <summary>この関数が起点</summary>
        /// <returns></returns>
        [TestMethod]
        public async Task TestMethod1()
        {
            await HeavyMethodMain();
        }

        /// <summary>計測メソッドの起点</summary>
        /// <returns></returns>
        private async Task HeavyMethodMain()
        {
            using (_logger.InfoMeasureTimeAsDisposable())
            {
                await this.Multiply(6, 4);
                await this.HeavyMethodSub2();
            }
        }

        /// <summary>掛け算:名称、パラメータ、結果指定の例</summary>
        /// <param name="v1">The v1.</param>
        /// <param name="v2">The v2.</param>
        /// <returns></returns>
        private async Task<decimal> Multiply(decimal v1, decimal v2)
        {
            var ret = 0m;
            using (_logger.InfoMeasureTimeAsDisposable(
                processName: "MultiplyProcessName",
                categoryName: "MultiplyCategoryName",
                parameterName: $"(v1:{v1}, v2:{v2})",
                getResult: () => ret))
            {
                await Task.Delay(millisecondsDelay: 120);
                return ret = v1 * v2;
            }
        }

        /// <summary>引数無しの例</summary>
        /// <returns></returns>
        private async Task HeavyMethodSub2()
        {
            using (_logger.InfoMeasureTimeAsDisposable())
            {
                await Task.Delay(160);
            }
        }
    }

このTestMethod1を実行すると、以下のようにコンソール出力されます。

[2019/02/09 15:05:55][Info]vStart [LoggerDemo.cs](HeavyMethodMain)
[2019/02/09 15:05:55][Info]vvStart [MultiplyCategoryName](MultiplyProcessName_param:(v1:6, v2:4))
[2019/02/09 15:05:55][Info]^^End   [MultiplyCategoryName](MultiplyProcessName_param:(v1:6, v2:4)) took 132.46ms => Result:[24]
[2019/02/09 15:05:55][Info]vvStart [LoggerDemo.cs](HeavyMethodSub2)
[2019/02/09 15:05:56][Info]^^End   [LoggerDemo.cs](HeavyMethodSub2) took 160.93ms 
[2019/02/09 15:05:56][Info]^End   [LoggerDemo.cs](HeavyMethodMain) took 296.96ms 
  • 引数を指定していないHeabyMethodMain、HeavyMethodSub2関数ではファイル名とメソッド名が自動的に出力されている
  • Multiply関数では引数や結果が出力される
  • ネストレベルはvや^の数で示される。 => マルチスレッド未対応。。。まだ必要になったことが無いので。。

まとめ

こんな感じの仕組みを作っておいて計測ログを基本デバッグログとして仕込んでいき、必要な部分は情報ログとして出力するようにしています。
書式は好みなので自由にすればいいと思います。キモはあくまでDisposable-usingを使うこと、あとはCallerMemberName等使って引数無しでもある程度使えるようになっていること。
なおログを仕込むときはResharperのサラウンドテンプレートを使えば楽ちん。
(TODO:ファイルテンプレート、サラウンドテンプレートの使い方、作り方のチュートリアル作成。)

それでは良いプログラミングライフをノシ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unity コールバックの受け取り方あれこれ

Unityゲーム開発に使えるコールバックの備忘録

System.Action

System.ActionSystem.Func を使えば、デリゲートの形式を宣言する必要がないので楽。

使用例

DoTweenと組み合わせて、フェードアウト・インしつつ何かを実行するメソッドを作ってみた。
引数でColorを渡してやるようにすれば、ブラックアウト、ホワイトアウト好きなものにカスタマイズもできる。

FadeController.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
using DG.Tweening;

namespace Sample
{
    public class FadeController : MonoBehaviour
    {
       [SerializeField] CanvasGroup fadeCanvas;
       [SerializeField] Image image;
        System.Action action;

        public void FadeAction(System.Action action)
        {
            image.color = Color.black;

            var sequence = DOTween.Sequence();
            sequence.Append(fadeCanvas.DOFade(1.0f, 2.0f))
                    .AppendCallback(() =>
                    {
                        action();  //画面が真っ暗になったら、引数のactionメソッドを実行
                    })
                    .Append(fadeCanvas.DOFade(0.0f, 2.0f))
                    .OnComplete(() => SetActive(false));
        }
    }
}

System.Func

返り値がある場合には System.Func を使用します。
値を渡して、それが ~~ ならば、最後の引数を返す  ...といった処理が書ける。

使用例

Sample.cs
using System;

public class Sample 
{

    public void Calculate()
    {
        Func<bool,bool,string> callBack = (valueA,valueB) =>
         {
            if (valueA && valueB)
             {
                 return "TRUE!";
             }
             return "FALSE!";
         };
    }
}

UnityEvent

シーンに保存することができる引数を持たない永続的なコールバック。

...スクリプリファレンス分からない。

UnityEventとSystem.Actionの違いについては下記で紹介されていた。
【Unity】UnityEventの用法と用量

特徴
- 機能としての違いはない
- Unityのインスペクター上に表示されるため、直感的
- 複数のイベントを簡単に登録できる。
- Invoke()を使うと登録したメソッドが一斉に実行される。

UnityEventSample.cs
using UnityEngine;
using UnityEngine.Events;
using System.Collections;

public class UnityEventSample: MonoBehaviour
{
    [SerializeField]UnityEvent m_MyEvent;

    void Start()
    {
        if (m_MyEvent == null)
            m_MyEvent = new UnityEvent();

        m_MyEvent.AddListener(Ping); //Ping()メソッドを登録
    }

    void Update()
    {
        if (Input.anyKeyDown &amp;&amp; m_MyEvent != null)
        {
            m_MyEvent.Invoke(); //イベントの実行
        }
    }

    void Ping()
    {
        Debug.Log("Ping");
    }
}

UnityのUGUIボタンにあるような奴がインスペクター上に現れる。
スクリーンショット 2019-02-07 17.51.33(2).png

ExampleClass.cs
using UnityEngine;
using UnityEngine.Events;

[System.Serializable]
public class MyIntEvent : UnityEvent<int> //引数の型を指定しておく
{
}

public class ExampleClass : MonoBehaviour
{
    public MyIntEvent m_MyEvent;

    void Start()
    {
        if (m_MyEvent == null)
            m_MyEvent = new MyIntEvent();

        m_MyEvent.AddListener(Ping);
    }

    void Update()
    {
        if (Input.anyKeyDown &amp;&amp; m_MyEvent != null)
        {
            m_MyEvent.Invoke(5);
        }
    }

    void Ping(int i) //登録するメソッドにint型引数を持たす
    {
        Debug.Log("Ping" + i);
    }
}

UnityAction

UnityActionというコールバック関数もある。

UnityEvent で使用される引数なしのデリゲートです。

違いや使い分けについては、UnityEventコールバック関数の設定方法についてが参考になった。

参考

UnityEvent でコールバックする
C# 備忘録 コールバックについて
UnityEventコールバック関数の設定方法について

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

管理者権限のないプレーンなWindowsでWebサーバを立てる戦い

ある事務職エンジニアの嘆き

非IT企業に勤務する事務職という名のエンジニアの皆様におかれましては、以下のようなきわめて過酷な環境に置かれていることと思います。

  • 管理者権限のないWindows
  • ソフトウェアのダウンロード・インストール不可。
  • Webは仮想環境でしか閲覧不可。
  • PHPもPythonもRubyもNode.jsも使用不可。

当然VisualStudioもVSCodeもSublimeTextさえ使えず、あるものといえばWordとExcelとメモ帳と付箋くらい。

このような牢獄のような環境下で、事務職エンジニアはExcelVBAやHTA、WSH、コマンドプロンプト、PowerShellといった、プレーンなWindowsでも使える技術を手に、日夜メモ帳と戦っていることでしょう。

特にわたしはWebが好きで、html/css/javascriptが得意なので、HTA(Webの技術で超簡単にデスクトップアプリがつくれるやつ。)であれこれとアプリをつくっていました。

しかし、IEも開発が終了し、HTAのようなレガシー技術も廃れに廃れて、いつまでサポートされるかも分からないご時世。

やっぱり、そろそろHTAを捨てて移住先を探さなければ…!

しかし環境は過酷だ

前提として、わたしのPC環境は以下のとおりです。

  • Windows7
  • 管理者権限は使えません。
  • Webは仮想環境でしかアクセスできません(ローカルとは完全分離)。
  • Webアプリはフィルタリングされて一切使えません。
  • アプリケーションのダウンロードやインストールはできません。

時代はクラウドですがWebアプリは一切使うことが許されていません。
Windowsに標準で入ってる「付箋」をデスクトップに貼り付けてタスク管理ツールとして使うというナレッジが口伝でシェアされているような職場です。
思うところはありますが、ここはクリエイターとしてアプリ開発に熱があがると前向きに捉えましょう('、3_ヽ)_

FireFoxが入ってる

わたしのPCにはIE9の他にFireFoxが入っています。
FireFoxならば、最新のJavaScriptとそのフレームワークを使って、ローカルで動作するいい感じのWebアプリがつくれるはず。
もう「クラス構文が使えない〜」とか「アロー関数が使えない〜」とか悩む必要はないのです。
Babel? そんなの使えるわけないでしょッ!
わたしはHTAを捨てて、最新のブラウザと共に歩む道を選ぶのだ!!

しかしローカルファイルにはアクセスできない。

ブラウザの宿命ですが、ローカルファイルにはアクセスが許されません。
データを保存するだけだったらLocalStorageとかIndexedDBとかで事足りますが、これらはドメイン単位で保存されているため、ローカルで使う場合はファイルの置き場所が変わったらアクセスできなくなります。
そしてやっぱり、ブラウザの用意するストレージではなくて、ローカルのファイルにアクセスしたいという欲も出てきます。
IEに実装されているActiveXという技術のことは…もう忘れましょう。

越えるしかない。HTTPの壁を…!

ローカルファイルを自在に操るには、HTTPの壁を超えてサーバサイドと連携するしかありません。
localhostに立てたサーバを使って、サーバ経由でローカルファイルを操作するのです。
そしてそのためには、HTTPを越えるためのWebサーバが必要不可欠です。

プレーンなWindowsでWebサーバを立てる

茶番が長かったですが、ここから本題です。
サーバサイドの言語がインストールされていれば、httpサーバ程度ならワンライナーで立てられるのですが、最初に説明したとおり、わたしのPCには何の言語もインストールされていません。

しかしこの環境下でも、httpサーバを立てる手段はあります。
.NETの中に、HttpListenerというシンプルなhttpサーバを立てられるクラスがあるらしいので、これを使います。

PowerShellでサーバを立てる

PowerShellは最近のWindowsには標準で入っているスクリプト言語かつコマンドプロンプトの全コマンドを網羅するという完全上位互換のシェルです。
PowerShellのやばいところは、.NETのクラスにアクセスして使うことができるというところで、何でもできるのですがキメラ化していて底知れぬ不気味さがあります。
これを使ってlocalhostにWebサーバを立ててみましょう。

server.ps1
$listener = New-Object Net.HttpListener
$listener.Prefixes.Add("http://localhost:8000/")
try {
    $listener.Start()
    while ($true) {
        $context = $listener.GetContext()
        $response = $context.Response
        $content = [System.Text.Encoding]::UTF8.GetBytes('hello world!')
        $response.OutputStream.Write($content, 0, $content.Length)
        $response.Close()
    }
}
catch {
    Write-Error($_.Exception)
}

上のコードは、localhost:8000/以下にアクセスすれば何があってもhello world!と返すだけのおめでたいWebサーバの例です。
powershellを開いて.\server.ps1で起動させれば、環境によっては上の例だけでもブラウザから動作を確認できると思います。

が、動かない…!

試しに上のコードを書いてFireFoxで覗いてみたところ、「プロキシサーバへの接続を拒否されました」とのエラーが。
手元にあるIE9では正常に見れるのに…。
IEで見れても仕方ないんだよ…。

調査したところ、社内のプロキシサーバ側の設定でlocalhostへのアクセスが許可されていない(?)ようでした。
当然、しがない事務職のわたしにはプロキシサーバをいじくる権限などありません。

FireFox側の設定を変更して、localhostにアクセスする際はプロキシを使わないようにすることもできるはずなのですが、この操作には管理者権限が必要とされているらしく、ユーザ権限ではプロキシの設定をいじれませんでした。

IPアドレスでのアクセスを試みる

localhostがダメならば、ループバックアドレスか自分のIPで直接アクセスすればいいじゃん!
ということで、FireFoxから127.0.0.1:8000へアクセスしてみました。

が、ダメッ!!

今度はブラウザにBad Request -Invalid Hostnameの表示が…。
どうやらこれは、上のコードで書いた

$listener.Prefixes.Add("http://localhost:8000/")

の設定により、「リクエストされたホスト名が設定と違うからダメ!」と怒られてしまっているようです。
しぶといですね…('、3_ヽ)_

HttpListenerの設定を変える

localhost:8000と設定されているのに、127.0.0.1とか10.x.x.xとかでアクセスしてるからダメ」ということならば、こっちの設定を変えてしまいましょう。

server.ps1
$listener.Prefixes.Add("http://127.0.0.1:8000/")

こっちに変えて試してみます。
今度こそいけそうな気がします。

が、ダメッ!!

今度はそもそもHttpListenerを立てる段階で以下のようなエラーが出てしまいました。

"0" 個  の引数を指定して "Start" を呼び出し中に例外が発生しました: 
"アクセスが拒否されました。"

調べたところ、ユーザ権限では127.0.0.1を指定してポートを開放することが許可されていない(?)みたいです。
localhostならいけるのに、なんでやねん!!
自分のIP(10.x.x.x)でも試してみましたが、こちらも同様の結果。
ユーザ権限でポートを開放できるように設定を書き換えることもできるみたいですが、こちらもやはり管理者権限が必要とされるらしいです。

絶望

試した 結果
localhostでサーバを立ててlocalhostにアクセス プロキシの設定でアクセスが拒否される。
localhostでプロキシを使わないようFireFoxの設定を変える 管理者権限が必要で操作できない。
localhostでサーバを立ててループバックアドレスにアクセス Invalid Hostname
自分のIPかループバックアドレスでサーバを立てる ポートを開放する権限がない。
ユーザにポート開放の権限を与える 管理者権限が必要で操作できない。

八方塞がりだ…('、3_ヽ)_

見たところ、localhostで立ててループバックアドレスか自分のIPアドレスでアクセスした場合であれば、HttpListenerまではリクエストが飛んできているみたいなので、これが一番芽がありそうです。
他のは…プロキシの設定変えたり管理者権限を入手したりしないとダメっぽいので、どうにもならなさそう。

http://+:80/Temporary_Listen_Addresses/

ここまでやってダメなのか~…と半ば諦めながらググっていたところ、StackOverflowで耳寄りな情報を発見。どうやら

http://+:80/Temporary_Listen_Addresses/なら誰でもアクセスできるよ!
でも他のアプリと競合する場合があるから気をつけて!

ということらしい。
試しにこの設定でHttpListenerを立ててみます。

server.ps1
$listener.Prefixes.Add("http://+:80/Temporary_Listen_Addresses/")

起動すると、何事もなくサーバがListenを開始します。
さらにこの状態で、FireFoxから127.0.0.1/Temporary_Listen_Addresses/にアクセスすると、ついにhello world!の文字が…!

長かったけどこれで無事、管理者権限のないWindowsでlocalhostにWebサーバを立てることができました。
あとはリクエストURLを参照してファイルを返したりRESTなAPIを構築したりと、やりたい放題ですね!(۶•̀ᴗ•́)۶

オマケ(C#でHttpListenerを構築する)

PowerShellが嫌な場合は、C#で書いたコードをWindows標準で入ってるcsc.exeを使って実行ファイルにすることができます。
やってることはPowerShellとほとんど同じなのですが、いちおうサンプルを乗っけておきます。

server.cs
using System;
using System.Net;
using System.Text;

class Server
{
    static void Main()
    {
        try
        {
            HttpListener listener = new HttpListener();
            listener.Prefixes.Add("http://+:80/Temporary_Listen_Addresses/");
            listener.Start();

            while (true)
            {
                HttpListenerContext context = listener.GetContext();
                HttpListenerResponse response = context.Response;
                byte[] content = Encoding.UTF8.GetBytes("hello world!");
                response.OutputStream.Write(content, 0, content.Length);
                response.Close();
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine("Error: " + ex.Message);
            Console.ReadKey();
        }
    }
}

これをcsc.exeでコンパイルしてserver.exeをつくります。

 C:\Windows\Microsoft.NET\Framework\v4.0.30319\csc.exe server.cs

あとがき

せめてPythonだけでも使えれば…('、3_ヽ)_

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

C# async/await でつまずきそうなところからの逆引き解説

async/await による非同期処理に関する記事はたくさん存在していますが、うまくいかない結果からの逆引きスタイルの記事が見当たらなかったのでまとめてみました。

次のようなことで困っていませんか?

この呼び出しを待たないため…というコンパイラ警告が検出される

Visual Studio から次のような警告が検出されます。

この呼び出しを待たないため、現在のメソッドの実行は、呼び出しが完了するまで続行します。呼び出しの結果に 'await' 演算子を適用することを検討してください。

警告が検出されるコード
private void BtnHeavyAction_Click(object sender, EventArgs e)
{
    Debug.WriteLine("BtnHeavyAction_Click enter");
    // 次のコンパイル警告が検出されます。
    // この呼び出しを待たないため、現在のメソッドの実行は、呼び出しが完了するまで続行します。
    // 呼び出しの結果に 'await' 演算子を適用することを検討してください。
    HeavyActionAsync();
    Debug.WriteLine("BtnHeavyAction_Click exit");
}
private async Task HeavyActionAsync()
{
    Debug.WriteLine("HeavyActionAsync enter");
    await Task.Delay(3000);
    Debug.WriteLine("HeavyActionAsync exit");
}

実行してみると、HeavyActionAsync メソッドの終了を待たずに BtnHeavyAction_Click メソッドが終了していることが分かります。

出力結果
BtnHeavyAction_Click enter
HeavyActionAsync enter
BtnHeavyAction_Click exit
HeavyActionAsync exit

原因

コンパイラがコーディングミスの可能性があると解釈するため。

対処方法

  1. HeavyActionAsync メソッドが終わるまで待ちたい場合は await をつけて呼び出します。
  2. HeavyActionAsync メソッドは別スレッドで実行させておき、次へ進めたい場合は戻り値を受けます。

1. await 呼び出しで待機する

HeavyActionAsync メソッドの終了を待ちたいのであれば、警告の内容のとおり、await をつけます。BtnHeavyAction_Click メソッドには async をつけます。await 呼び出しを含むメソッドには async をつける必要があります。async はコンパイラに対して await 呼び出しを行うことを明示的に示すためのキーワードです。面倒だと感じることもありますが、コーディングミスを減らすためのものだと思います。

HeavyActionAsyncの終了を待つコード
// async をつけます。
private async void BtnHeavyAction_Click(object sender, EventArgs e)
{
    Debug.WriteLine("BtnHeavyAction_Click enter");
    // await をつけます。
    // BtnHeavyAction_Click に async をつけないと次のコンパイルエラーが検出されます。
    // 'await' 演算子は、非同期メソッド内でのみ使用できます。
    // このメソッドに 'async' 修飾子を指定し、戻り値の型を 'Task' に変更することを検討してください。
    await HeavyActionAsync();
    Debug.WriteLine("BtnHeavyAction_Click exit");
}
private async Task HeavyActionAsync()
{
    Debug.WriteLine("HeavyActionAsync enter");
    await Task.Delay(3000);
    Debug.WriteLine("HeavyActionAsync exit");
}

HeavyActionAsync メソッドが終了してから BtnHeavyAction_Click メソッドが終了していることが分かります。

出力結果
BtnHeavyAction_Click enter
HeavyActionAsync enter
HeavyActionAsync exit
BtnHeavyAction_Click exit

2. 戻り値を受ける(待機しない)

HeavyActionAsync メソッドの戻り値である Task を変数で受け取るようにすると、コンパイラは呼び出しを待つ必要がないことが明示的に示されたと解釈し、警告として検出しなくなります。但し、この方法には HeavyActionAsync メソッドで例外が発生したときに呼び出し元でキャッチできないという問題があります。これについては後述します。

警告が検出されなくなるコード
private void BtnHeavyAction_Click(object sender, EventArgs e)
{
    Debug.WriteLine("BtnHeavyAction_Click enter");
    // 戻り値を受けます。
    // このような場合、私は nowait のような変数名にすることが多いです。
    // 人間に対しても「待つ必要がない」ということが伝わるからです。
    Task nowait = HeavyActionAsync();
    Debug.WriteLine("BtnHeavyAction_Click exit");
}
private async Task HeavyActionAsync()
{
    Debug.WriteLine("HeavyActionAsync enter");
    await Task.Delay(3000);
    Debug.WriteLine("HeavyActionAsync exit");
}

実行結果は警告が表示されていたときと同じです。HeavyActionAsync メソッドの終了を待たずに BtnHeavyAction_Click メソッドが終了します。

出力結果
BtnHeavyAction_Click enter
HeavyActionAsync enter
BtnHeavyAction_Click exit
HeavyActionAsync exit

呼び出したメソッドが終了しない

Task を返すメソッドが終了するまで待機しようとして Wait() を呼び出したとき、いつまでたってもそのメソッドが終了せずにアプリケーションがフリーズします。

フリーズするコード1
private void BtnHeavyAction_Click(object sender, EventArgs e)
{
    Debug.WriteLine("BtnHeavyAction_Click enter");
    // ここでフリーズします。
    HeavyActionAsync().Wait();
    Debug.WriteLine("BtnHeavyAction_Click exit");
}
private async Task HeavyActionAsync()
{
    Debug.WriteLine("HeavyActionAsync enter");
    await Task.Delay(3000);
    Debug.WriteLine("HeavyActionAsync exit");
}

HeavyActionAsync メソッドが終了せず、アプリケーションがフリーズします。

出力結果
BtnHeavyAction_Click enter
HeavyActionAsync enter

Task<T> を返すメソッドの場合、戻り値を Result で取得しようとしたところでフリーズします。

フリーズするコード2
private void BtnHeavyFunc_Click(object sender, EventArgs e)
{
    Debug.WriteLine("BtnHeavyFunc_Click enter");
    // ここでフリーズします。
    int result = HeavyFuncAsync(2, 3).Result;
    Debug.WriteLine($"result={result}");
    Debug.WriteLine("BtnHeavyFunc_Click exit");
}
private async Task<int> HeavyFuncAsync(int x, int y)
{
    Debug.WriteLine("HeavyFuncAsync enter");
    await Task.Delay(3000);
    Debug.WriteLine("HeavyFuncAsync exit");
    return x * y;
}

HeavyFuncAsync メソッドが終了せず、アプリケーションがフリーズします。

出力結果
BtnHeavyFunc_Click enter
HeavyFuncAsync enter

原因

デッドロックが発生するため。Task.Wait() や Task.Result は Task が終わるまでスレッドをブロックします。その Task の内部で await 呼び出しを行っている場合、その呼び出しが終わるときに呼び出し元のスレッドに戻ろうとします。しかし、ブロックされているため戻ることができません。Task が終わらなくなり、フリーズします。

対処方法

前述のように await 呼び出しで待機しましょう。

await呼び出しでブロックすることなく待機
// async をつけます。
private async void BtnHeavyAction_Clickobject sender, EventArgs e)
{
    Debug.WriteLine("BtnHeavyAction_Click enter");
    // await をつけます。
    await HeavyActionAsync();
    Debug.WriteLine("BtnHeavyAction_Click exit");
}
private async Task HeavyActionAsync()
{
    Debug.WriteLine("HeavyActionAsync enter");
    await Task.Delay(3000);
    Debug.WriteLine("HeavyActionAsync exit");
}
出力結果
BtnHeavyAction_Click enter
HeavyActionAsync enter
HeavyActionAsync exit
BtnHeavyAction_Click exit

Task.Wait() や Task.Result は扱いが難しいです。この例のようなシンプルな非同期処理であればどのような結果になるかがわかりやすいですが、同時に複数の非同期処理が実行されるようなケースで安全にコントロールすることは難しいです。そもそも UI をブロックしないようにするために非同期処理を利用しているのに、その非同期処理が終わるまでブロックしてしまっては本末転倒です。

補足

HeavyActionAsync メソッドの内部の await 呼び出しに対して ConfigureAwait(false) をつけると、とりあえずフリーズすることは回避できます。ConfigureAwait(false) を簡単に説明すると、メソッドの終了後に呼び出し元に戻ってこなくてもよいということを指定します。ブロックされたスレッドに戻ろうとしなくなるため、HeavyActionAsync メソッドは終了し、BtnHeavyAction_Click メソッドに戻ってくるようになります。

HeavyActionAsyncは終了するようになるコード
private void BtnHeavyAction_Click(object sender, EventArgs e)
{
    Debug.WriteLine("BtnHeavyAction_Click enter");
    // HeavyActionAsync の実行中 UI はブロックされますが、フリーズすることはなくなります。
    HeavyActionAsync().Wait();
    Debug.WriteLine("BtnHeavyAction_Click exit");
}
private async Task HeavyActionAsync()
{
    Debug.WriteLine("HeavyActionAsync enter");
    // ConfigureAwait(false) をつけます。
    await Task.Delay(3000).ConfigureAwait(false);
    Debug.WriteLine("HeavyActionAsync exit");
}

例外をキャッチできない

Task を返すメソッドを await をつけずに呼び出した場合、そのメソッド内で例外が発生しても呼び出し元でキャッチすることはできません。

例外をキャッチできないコード
private void BtnHeavyAction_Clickobject sender, EventArgs e)
{
    Debug.WriteLine("BtnHeavyAction_Click enter");
    try
    {
        Task nowait = HeavyActionAsync();
    }
    catch (Exception ex)
    {
        // ここではキャッチできない。
        Debug.WriteLine($"{ex.GetType().Name}:{ex.Message}");
    }
    Debug.WriteLine("BtnHeavyAction_Click exit");
}
private async Task HeavyActionAsync()
{
    Debug.WriteLine("HeavyActionAsync enter");
    await Task.Delay(3000);
    throw new Exception("HeavyActionAsyncで例外が発生しました。");
    Debug.WriteLine("HeavyActionAsync exit");
}

Visual Studio の出力コンソールには「例外がスローされました」と出力されますが、これはフレームワーク内部から出力されたものです。

出力結果
BtnHeavyAction_Click enter
HeavyActionAsync enter
BtnHeavyAction_Click exit
例外がスローされました: 'System.Exception' (Sample.exe の中)

原因

Task を用いた非同期処理では、発生した例外は一旦 Task で管理され、非同期処理が完了したときに呼び出し元へ排出される形でスローされます。await をつけない呼び出しでは Task から排出される機会がなくなり、結果的に例外は飲み込まれます。

対処方法

やはりこの場合も await 呼び出しを行いましょう。

補足

Task を操作したときに例外がスローされる

「この呼び出しを待たないため…」の警告を消す方法として Task を変数で受ける方法を紹介しましたが、Task には Wait, Result や IsComleted, IsFault など、タスクの完了を参照するメソッドやプロパティがあります。タスク内で例外が発生した場合、これらにアクセスしたタイミングで呼び出し元に例外が排出されます。

Taskから例外が排出されるコード
private void BtnHeavyAction_Clickobject sender, EventArgs e)
{
    Debug.WriteLine("BtnHeavyAction_Click enter");
    try
    {
        Task task = HeavyActionAsync();
        // タスクが完了するまでポーリングしたりすると例外が排出されます。
        // ※ただ待ちたいだけなら await を使いましょう。
        while (task.IsCompleted) {
        }
        // await task;
    }
    catch (Exception ex)
    {
        // ここで例外がキャッチされます。
        Debug.WriteLine($"{ex.GetType().Name}:{ex.Message}");
    }
    Debug.WriteLine("BtnHeavyAction_Click exit");
}
private async Task HeavyActionAsync()
{
    Debug.WriteLine("HeavyActionAsync enter");
    await Task.Delay(3000);
    throw new Exception("HeavyActionAsyncで例外が発生しました。");
    Debug.WriteLine("HeavyActionAsync exit");
}

catch 句の中で出力したデバッグメッセージが出力され、キャッチできていることが分かります。

出力結果
BtnHeavyAction_Click enter
HeavyActionAsync enter
BtnHeavyAction_Click exit
例外がスローされました: 'System.Exception' (Sample.exe の中)
例外がスローされました: 'System.Exception' (mscorlib.dll の中)
Exception:HeavyActionAsyncで例外が発生しました。
BtnHeavyAction_Click exit

Wait や Result で完了を待っていた場合は、発生した例外が AggregateException にラップされてスローされます。これは Task クラスの仕様です。

Taskから例外が排出されるコード
private void BtnHeavyAction_Clickobject sender, EventArgs e)
{
    Debug.WriteLine("BtnHeavyAction_Click enter");
    try
    {
        // Wait で完了を待つ。
        HeavyActionAsync().Wait();
    }
    catch (Exception ex)
    {
        // ここで例外がキャッチされます。
        Debug.WriteLine($"{ex.GetType().Name}:{ex.Message}");
    }
    Debug.WriteLine("BtnHeavyAction_Click exit");
}
private async Task HeavyActionAsync()
{
    Debug.WriteLine("HeavyActionAsync enter");
    await Task.Delay(3000);
    throw new Exception("HeavyActionAsyncで例外が発生しました。");
    Debug.WriteLine("HeavyActionAsync exit");
}

catch 句の中で出力したデバッグメッセージから、例外の型が AggregateException であることが分かります。発生した例外を取得するには、AggregateException の InnerExceptions を参照してください。

出力結果
BtnHeavyAction_Click enter
HeavyActionAsync enter
BtnHeavyAction_Click exit
例外がスローされました: 'System.Exception' (Sample.exe の中)
例外がスローされました: 'System.AggregateException' (mscorlib.dll の中)
AggregateException:1 つ以上のエラーが発生しました。
BtnHeavyAction_Click exit

コントロールにアクセスすると例外がスローされる

非同期処理の中でコントロールにアクセスしたときに次のような例外がスローされます。

有効ではないスレッド間の操作: コントロールが作成されたスレッド以外のスレッドからコントロール '' がアクセスされました。

例外がスローされるコード
private async void BtnHeavyAction_Clickobject sender, EventArgs e)
{
    Debug.WriteLine("BtnHeavyAction_Click enter");
    try
    {
        await HeavyActionAsync();
    }
    catch (Exception ex)
    {
        Debug.WriteLine($"{ex.GetType().Name}:{ex.Message}");
    }
    Debug.WriteLine("BtnHeavyAction_Click exit");
}
private async Task HeavyActionAsync()
{
    Debug.WriteLine("HeavyActionAsync enter");
    // コントロールにアクセス
    btnHeavyAction.Enabled = false;
    await Task.Delay(3000).ConfigureAwait(false);
    // コントロールにアクセス
    btnHeavyAction.Enabled = true;
    Debug.WriteLine("HeavyActionAsync exit");
}

"HeavyActionAsync enter" は出力され、"HeavyActionAsync exit" は出力されていません。Task.Delay の後の btnHeavyAction.Enabled = true で例外が発生していることが分かります。

出力結果
BtnHeavyAction_Click enter
HeavyActionAsync enter
例外がスローされました: 'System.InvalidOperationException' (System.Windows.Forms.dll の中)
例外がスローされました: 'System.InvalidOperationException' (mscorlib.dll の中)
InvalidOperationException:有効ではないスレッド間の操作: コントロールが作成されたスレッド以外のスレッドからコントロール '' がアクセスされました。
BtnHeavyAction_Click exit

原因

例外メッセージの通り、フォームやコントロールは UI スレッド(≒アプリケーションのメインスレッド)以外からアクセスすることができません。この例では Task.Delay の呼び出しに対して ConfigureAwait(false) を指定しています。これがポイントです。Task.Delay の後は呼び出し元のスレッドには戻らずに別のスレッドで実行されますので、コントロールにアクセスすると例外が発生します。

// ここまでは呼び出し元スレッド
await Task.Delay(3000).ConfigureAwait(false);
// ここから後は別のスレッド

対処方法

この例では ConfigureAwait(false) をつけなければ呼び出し元スレッドに戻りますので例外は発生しなくなります。ただ、複雑な非同期処理ではどのスレッドで実行されるかが分かりにくくなる場合があります。Control の InvokeRequired プロパティと Invoke メソッドを使用し、確実に UI スレッドで実行されるようにします。

  • Control.Invoke メソッド
    • 指定された処理をそのコントロールが生成されたスレッドで実行します。
    • 多少オーバーヘッドが発生します。
  • Control.InvokeRequired プロパティ
    • そのコントロールにアクセスするときに Invoke を使用する必要があるかどうかを取得します。
必要に応じてInvokeするコード
private async Task HeavyActionAsync()
{
    Debug.WriteLine("HeavyActionAsync enter");
    InvokeIfRequired(btnHeavyAction, () => btnHeavyAction.Enabled = false);
    await Task.Delay(3000).ConfigureAwait(false);
    InvokeIfRequired(btnHeavyAction, () => btnHeavyAction.Enabled = true);
    Debug.WriteLine("HeavyActionAsync exit");
}
private void InvokeIfRequired(Control control, Action action)
{
    if (control.InvokeRequired)
    {
        control.Invoke(action, new object[] { });
    }
    else
    {
        action();
    }
}

InvokeIfRequired メソッドを Control に対する拡張メソッドとして定義しておくと、簡潔に記述できるようになります。

Controlに対する拡張メソッド
/// <summary>
/// 
/// </summary>
public static class ControlExtensions
{
    public static void InvokeIfRequired(this Control control, Action action)
    {
        if (control.InvokeRequired)
        {
            control.Invoke(action, new object[] { });
        }
        else
        {
            action();
        }
    }

    public static T InvokeIfRequired<T>(this Control control, Func<T> func)
    {
        if (control.InvokeRequired)
        {
            return (T)control.Invoke(func, new object[] { });
        }
        else
        {
            return func();
        }
    }
}
拡張メソッドで書き換え
private async Task HeavyActionAsync()
{
    Debug.WriteLine("HeavyActionAsync enter");
    btnHeavyAction.InvokeIfRequired(() => btnHeavyAction.Enabled = false);
    await Task.Delay(3000).ConfigureAwait(false);
    btnHeavyAction.InvokeIfRequired(() => btnHeavyAction.Enabled = true);
    Debug.WriteLine("HeavyActionAsync exit");
}

これから非同期処理を学びたいと考えている人へ

これから学ぶなら async/await

.NET Framework ではいくつかの非同期処理の仕組みが提供されています。.NET Framework 4.5 以降であれば、機能面や使い勝手から考えると async/await 一択になると思います。ただ、異なる仕組みを混在させると混乱のもとになります。もしあなたが関わっているプロジェクトで async/await 以外の仕組みで非同期処理を制御しており、それがプロジェクト標準ルールである場合、無理に async/await を使うことはお勧めしません。プロジェクトメンバーと十分に検討してください。

インターネットなどの情報を参考にするとき、その説明に次のような型やメソッドが現れた場合は古い時代の仕組みである可能性が高いです。

  • Thread.Start メソッド
  • ThreadStart クラス
  • AsyncCallback クラス
  • IAsyncResult インターフェース
  • BeginInvoke メソッド
  • EndInvoke メソッド
  • BackgroundWorker クラス

もっと詳しい情報が知りたくなった人へ

【Qiita】Taskを極めろ!async/await完全攻略
【Qiita】C# 今更ですが、await / async
【Qiita】C# Taskの待ちかた集
【SE(たぶん)の雑感記】async、awaitそしてTaskについて(非同期とは何なのか)
【kekyoの丼】できる!C#で非同期処理(Taskとasync-await)
【kekyoの丼】技術解説 – 非同期

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

別バイナリの enum。メンバー属性の取得 には気をつける

3年に一回くらいの周期で、沼にハマってるのでメモ。

構成

info

  • [DLL] に 列挙型を定義する
  • 列挙型の各メンバーには 属性 が指定されている
  • [exe など] は、DLL の 列挙型から 属性 を取得して利用する

ハマったこと

デプロイ後、[DLL] 内の列挙型にメンバーを追加してビルドしなおす。
[exe など] はビルドせず、[DLL] を差し替えて実行すると、意図した結果が得られないパターンがある。

当システムの初期状態

ソース

enumA.dll.cs
namespace KakimoDLL
{
  /// <summary>
  /// カスタム属性
  /// </summary>
  public class TempAttribute : Attribute
  {
    /// <summary>
    /// コンストラクタ
    /// </summary>
    /// <param name="val"></param>
    public TempAttribute(string val)
    {
      Value = val;
    }
    /// <summary>
    /// メンバ
    /// </summary>
    public string Value { get; set; }
  }
  /// <summary>
  /// 検証用列挙型
  /// </summary>
  public enum EnumA
  {
    [Temp("001")]
    Member1,
    [Temp("002")]
    Member2,
    [Temp("003")]
    Member3
  }
}
exeとか.cs
using KakimoDLL;

namespace KakimoExe
{
  class Program
  {
    static void Main(string[] args)
    {
      var enumVal = EnumA.Member2;  // Member2 メンバの属性を取得してみる

      var enumType = enumVal.GetType();
      var enumField = enumType.GetField(Enum.GetName(typeof(EnumA), enumVal));

      var attr =(TempAttribute[])enumField.GetCustomAttributes(typeof(TempAttribute), false);
      Console.WriteLine(attr[0].Value);   //
    }
  }
}

実行結果

002

DLL 改修後も、一応動くパターン

ソース (列挙型部分のみ)

  /// <summary>
  /// 検証用列挙型
  /// </summary>
  public enum EnumA
  {
    [Temp("001")]
    Member1,
    [Temp("002")]
    Member2,
    [Temp("003")]
    Member3,
    [Temp("004")]
    Member4,
  }

実行結果

002

DLL 改修後、意図しない結果になるパターン

ソース (列挙型部分のみ)

  /// <summary>
  /// 検証用列挙型
  /// </summary>
  public enum EnumA
  {
    [Temp("000")]
    Member0,
    [Temp("001")]
    Member1,
    [Temp("002")]
    Member2,
    [Temp("003")]
    Member3,
  }

実行結果

001
(期待する結果は '002' )

原因

列挙型はプログラムの内部では整数として扱われていて、 整数型に変換することでその値を取り出すことが出来ます。 特に値や型を指定しなければ、列挙型は int として扱われ、 各メンバーは宣言した順番に 0, 1, 2, …, n となります。

列挙型 - ++C++; // 未確認飛行 C

[exe など] がコンパイルされた時点で EnumA.Member2 == 1 です。
[exe など] が実行されると、EnumA の 値が 1 のメンバーに追加されている属性 を取得しようとします。

"DLL 改修に意図しない結果になるパターン" のように、EnumA.Member2 の宣言順をかえてしまうようなメンバー追加をしてしまうと、[exe など] が意図していた結果が取得できなくなります。

対応策

  1. enum メンバーの順番が変わらないように追加宣言する
  2. enum メンバーに明示的に値を指定しておく
  3. そもそもの実装を考えなおす
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Unityの教科書レベル2 メモ

fの必要性について

変数の初期化について小数を代入するときは後ろに"f"をつける
例) float height = 1.0f;
これをつけないとdouble型だと認識されてしまいエラーが出るため
(C#ではdouble型の値をfloat型に代入することは禁じられている)

配列の準備(基礎中の基礎)

例だけ示す
例) int[] hairetsu = new int[10];

メソッドは引数は複数個渡せるが返り値は1つだけ

クラスの扱いについて

クラスは関係のある変数とメソッドをひとまとめにできる。
作成したクラスはintやstringのように型として扱える
例) Player myplayer = new Player();
(ここでPlayerはクラス名、 myplayerは変数名の扱いである)

magnitudeメンバ変数について

ベクトルの長さを求めることができる
例) float dir = new Vector2(3.0f, 4.0f).magnitude;

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Unity(C#)】VRカメラから3Dカメラに切り替え

  
  
  
この記事は

『プログラミング完全未経験からUnityでの開発現場に迎え入れてもらえた世界一の幸せ者』

の記事です。そのつもりでお読みください。
  

HMDをトラッキングしてVR空間に!

まず最初にこれはVIVEというVR専用のWearable Deviceでのお話です。
会社でいつも何気なく使ってますが家庭用ゲーム機として手の届く値段じゃないですね...

HMDとは Head Mounted Displayの略称で頭部装着ディスプレイのことらしいです。
三か月働いてて今日初めて知った略称です。早速見出しにぶち込んでやりました。

☆下準備☆

①Asset StoreからSteam VR PluginをDL+Import
②ImportしたSteam VR PluginのPrefabから[CameraRig]をHierarchyにドラッグ&ドロップ
cameraRig.jpg

これでエディター画面のを押せばHMDにVRの世界が広がるはずです。

なぜVRカメラから3Dカメラに切り替える必要があるのか?

今回私が陥った状況としては、
・VRのカメラをオフにして別アングル(別のカメラ)からVR空間内をゲームビューに映したい
です。

さらにかみ砕いて言えば、
・VRコンテンツのPVを作りたいのにやり方がわからない!
です。

VRのPVはあまり一人称視点を導入するべきではないという意見があり、
私もいろいろなPVを見ましたが、賛同しております。(まあまあ古い記事ですが...→MoguraVRさん)

一人称視点のPVはあんまりおもしろそうじゃないです。
そのゲームをプレイしたことがない人からしたら、何をやっているかあまり伝わってきません。

なので、VR空間内を一人称視点以外で撮影する方法が必要となります。

そこで、
[CameraRig]を非アクティブにする
②新しい3D用のカメラを別で用意しておいてアクティブにする

で可能だと思い、試しましたが無理でした。

VRカメラから3Dカメラに切り替え

XRSettingsをオン、オフ切り替えでいけました。
今回はキーボードで切り替えにしてます。

using UnityEngine.XR;

void Update () {


        if (Input.GetKeyDown(KeyCode.F)) //VRのカメラオフ
        {
            XRSettings.enabled = false;
        }

        if (Input.GetKeyDown(KeyCode.T)) //VRのカメラオン
        {
            XRSettings.enabled = true;
        }
    }

もっといい方法があれば教えてください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む