- 投稿日:2020-07-30T18:52:35+09:00
[C#] 逆ポーランド演算器をジェネリック化して遊ぶ
謝辞
- 本稿は、【逆ポーランド記法を利用した数式の計算】で紹介されている逆ポーランド記法(= Reverse Polish Notation, RPN)の計算を、【C# のジェネリック演算】で紹介されている手法を用いて「なんちゃってジェネリック化」して遊ぼうというテーマでお送りする。
- クラス設計や実装方法においては、上記記事を存分に参考にさせて頂いた。逆ポーランド記法の定義や計算方法については前者、数値型のジェネリッククラスの作成方法は後者の記事リンクを参照されたい。
ジェネリッククラスの実装
クラスのの全容は GitHub へ公開した.
https://github.com/Takuto168/Takuto168park/blob/master/RpnCalculator.cs以下に,その実装方法と検証結果を示す.
クラスの作成
- 演算クラスは、静的ジェネリッククラスとした。
- 四則演算の式木を組み立てるために
System.Linq.Expressions
名前空間を、また文字列から数値への変換を行うTryParse
メソッドを利用するためにSystem.Reflection
名前空間を追加しておく。- 数値型でのクラス制約はできないため、仕方なく構造体での制約とした。代わりに、コンストラクタ内で型判定を行う。
RpnCalculator.csusing System; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; using System.Reflection; namespace RpnCalculator { /// <summary> /// 逆ポーランド記法を計算する機能を提供します。 /// </summary> public static class RpnCalculator<N> where N : struct { /// <summary> /// 逆ポーランド記法の演算に対応し得る型のリスト。 /// </summary> private static Type[] _availableTypes => new[] { typeof(int), typeof(uint), typeof(short), typeof(ushort), typeof(long), typeof(ulong), typeof(decimal), typeof(double), typeof(float)}; /// <summary> /// 静的クラスの生成時に、指定した数値型が演算に対応しているか判定します。 /// </summary> static RpnCalculator() { if (!_availableTypes.Contains(typeof(N))) throw new NotSupportedException(); } } }数値型ジェネリッククラスの作成
- 数値型の四則演算と文字列変換をサポートする静的ジェネリッククラス
NumericalGeneric
を作成する。今回はRpnCalculator
の内部クラスとした。- 内部メンバには、各四則演算のデリゲートと、文字列変換メソッドのリフレクション実行を用意する。
RpnCalculator.cs/// <summary> /// 算術四則演算と文字列からの変換を行うためのジェネリック数値型クラスです。 /// </summary> private static class NumericalGeneric { /// <summary> /// 算術加算演算を行います。 /// </summary> public static Func<N, N, N> Add { get; } /// <summary> /// 算術減算演算を行います。 /// </summary> public static Func<N, N, N> Subtract { get; } /// <summary> /// 算術乗算演算を行います。 /// </summary> public static Func<N, N, N> Multiply { get; } /// <summary> /// 算術除算演算を行います。 /// </summary> public static Func<N, N, N> Divide { get; } /// <summary> /// 指定したジェネリック型を取得します。 /// </summary> private static Type _type => typeof(N); /// <summary> /// 指定した数値型における文字列からの変換メソッドを取得します。 /// </summary> private static MethodInfo _tryParseInvoker => _type.GetMethod("TryParse", new[] { typeof(string), _type.MakeByRefType() }); /// <summary> /// 静的クラスの生成時に、算術四則演算デリゲートを作成します。 /// </summary> static NumericalGeneric() { var p1 = Expression.Parameter(typeof(N)); var p2 = Expression.Parameter(typeof(N)); Add = Expression.Lambda<Func<N, N, N>>(Expression.Add(p1, p2), p1, p2).Compile(); Subtract = Expression.Lambda<Func<N, N, N>>(Expression.Subtract(p1, p2), p1, p2).Compile(); Multiply = Expression.Lambda<Func<N, N, N>>(Expression.Multiply(p1, p2), p1, p2).Compile(); Divide = Expression.Lambda<Func<N, N, N>>(Expression.Divide(p1, p2), p1, p2).Compile(); } /// <summary> /// 数値の文字列形式を、それと等価なジェネリック数値型に変換します。 戻り値は、変換が成功したかどうかを示します。 /// </summary> /// <param name="s">変換する数値を格納する文字列。</param> /// <param name="result">変換が成功した場合、このメソッドが返されるときに、s に格納された数値と等価のジェネリック数値を格納します。変換に失敗した場合は 0 を格納します。</param> /// <returns>s が正常に変換された場合は true。それ以外の場合は false。</returns> public static bool TryParse(string s, out N result) { if (_tryParseInvoker == null) { // Reflection で N.TryParse メソッドを取得できなかった場合 result = default(N); return false; } var args = new object[] { s, null }; if (!(bool)_tryParseInvoker.Invoke(null, args)) { // 変換失敗 result = default(N); return false; } result = (N)args[1]; return true; } }トークン構造体の作成
- トークンは、演算子または数値を示す。
- 構造体メンバには、演算子とその実行処理のマッピングを実装し、与えられた演算を実行できるようにした。
- 補助機能として、指定したトークン文字列を数値に置き換えるためのマッピングを与えることで、数値への代入を可能にした。
RpnCalculator.cs/// <summary> /// 逆ポーランド記法における1つのトークンを表す構造体です。 /// </summary> private struct Token { /// <summary> /// 演算子を表す文字列とその実行処理のマッピング。 /// </summary> private static readonly Dictionary<string, Func<N, N, N>> _operaters = new Dictionary<string, Func<N, N, N>>() { { "+", (d1, d2) => NumericalGeneric.Add(d1, d2) }, { "-", (d1, d2) => NumericalGeneric.Subtract(d1, d2) }, { "*", (d1, d2) => NumericalGeneric.Multiply(d1, d2) }, { "/", (d1, d2) => NumericalGeneric.Divide(d1, d2) } }; /// <summary> /// トークンが演算子であるかどうかを取得します。 /// </summary> public bool IsOperater => !string.IsNullOrEmpty(this.Operater); /// <summary> /// トークンが演算子であるとき、その文字列を取得します。 /// </summary> public string Operater { get; } /// <summary> /// トークンが数値であるとき、その値を取得します。 /// </summary> public N Value { get; } /// <summary> /// 逆ポーランド記法の文字列からトークンを生成します。 /// </summary> /// <param name="s">逆ポーランド記法のトークンを表す文字列。</param> /// <param name="replacePrams">指定したトークン文字列を数値に置き換えるためのマッピング。</param> public Token(string s, Dictionary<string, N> replacePrams) { if (_operaters.ContainsKey(s)) { // 演算子の場合 this.Value = default(N); this.Operater = s; } else { // 数値の場合 if (replacePrams?.ContainsKey(s) ?? false) this.Value = replacePrams[s]; // 指定したトークン文字列を数値に置き換え else if (NumericalGeneric.TryParse(s, out var t)) this.Value = t; // N.TryParse によって変換に成功 else throw new FormatException(); // 認識できない文字列 this.Operater = null; } } /// <summary> /// 2つのトークンに対してトークンの示す算術演算を行い、その結果から新たなトークンを作成します。 /// 引数は、Stack<T>から取り出されることを想定して順序が判定していることに留意してください。 /// </summary> /// <param name="d2">2つ目の数値。</param> /// <param name="d1">1つ目の数値。</param> /// <returns></returns> public Token Operate(N d2, N d1) => new Token(_operaters[this.Operater](d1, d2)); /// <summary> /// 数値型のトークンを生成します。 /// </summary> /// <param name="value">数値。</param> private Token(N value) => (this.Value, this.Operater) = (value, null); }演算処理の実装
- 逆ポーランド記法では
stack
を用いて演算を行う。- 数値の場合はその値を
stack
にpush
し、演算子の場合はstack
から値を2つ取り出し計算を行い、その結果をstack
にpush
する。RpnCalculator.cs/// <summary> /// 逆ポーランド記法の演算を行います。 /// </summary> /// <param name="exp">式。</param> /// <param name="replaceParams">指定したトークン文字列を数値に置き換えるためのマッピング。</param> /// <returns>結果値。</returns> private static N CalculateInvoker(string exp, Dictionary<string, N> replaceParams) { var stack = new Stack<Token>(); foreach (var s in exp.Split(' ').Where(s => !string.IsNullOrEmpty(s))) { var token = new Token(s, replaceParams); stack.Push(token.IsOperater ? token.Operate(stack.Pop().Value, stack.Pop().Value) : token); } return stack.Pop().Value; }外部公開用メソッドの実装
- 指定したトークン文字列を数値に置き換えるためのマッピングを与えるために、いくつかの
public
な静的メソッドをオーバーロードした。RpnCalculator.cs/// <summary> /// 逆ポーランド記法の演算を行います。 /// </summary> /// <param name="exp">式。</param> /// <returns>結果値。</returns> public static N Calculate(string exp) => CalculateInvoker(exp, null); /// <summary> /// 逆ポーランド記法の演算を行います。 /// </summary> /// <param name="exp">式。</param> /// <param name="replaceParam">指定したトークン文字列を数値に置き換えるためのマッピング。</param> /// <param name="replaceParams">指定したトークン文字列を数値に置き換えるためのマッピング。</param> /// <returns>結果値。</returns> public static N Calculate(string exp, (string Key, N Value) replaceParam, params (string Key, N Value)[] replaceParams) { var valueList = new Dictionary<string, N>(replaceParams.Length + 1); valueList.Add(replaceParam.Key, replaceParam.Value); valueList.AddRange(replaceParams); return CalculateInvoker(exp, valueList); } /// <summary> /// 逆ポーランド記法の演算を行います。 /// </summary> /// <param name="exp">式。</param> /// <param name="replaceParams">指定したトークン文字列を数値に置き換えるためのマッピング。</param> /// <returns>結果値。</returns> public static N Calculate(string exp, IEnumerable<(string Key, N Value)> replaceParams) => CalculateInvoker(exp, replaceParams.ToDictionary(t => t.Key, t => t.Value)); /// <summary> /// 逆ポーランド記法の演算を行います。 /// </summary> /// <param name="exp">式。</param> /// <param name="replaceParam">指定したトークン文字列を数値に置き換えるためのマッピング。</param> /// <param name="replaceParams">指定したトークン文字列を数値に置き換えるためのマッピング。</param> /// <returns>結果値。</returns> public static N Calculate(string exp, N replaceParam, params N[] replaceParams) { var valueList = new List<N>(replaceParams.Length + 1); valueList.Add(replaceParam); foreach (var item in replaceParams) valueList.Add(item.Key, item.Value); return Calculate(exp, valueList); } /// <summary> /// 逆ポーランド記法の演算を行います。 /// </summary> /// <param name="exp">式。</param> /// <param name="replaceParams">指定したトークン文字列を数値に置き換えるためのマッピング。</param> /// <returns>結果値。</returns> public static N Calculate(string exp, IEnumerable<N> replaceParams) => CalculateInvoker(exp, replaceParams.Select((Item, Index) => new { Item, Index }).ToDictionary(v => v.Index.ToString("{0}"), v => v.Item));検証
せっかくジェネリック化したので、様々な型で計算を行ってみる。
整数演算
- 単純な整数の計算を行ってみる。パラメータは以下のように代入可能。
Console.WriteLine(RpnCalculator<int>.Calculate("1 2 + 3 4 + *")); // 21 Console.WriteLine(RpnCalculator<int>.Calculate("{0} {1} + {2} {3} + *", 1, 2, 3, 4)); // 21 Console.WriteLine(RpnCalculator<int>.Calculate("A B + C D + *", ("A", 1), ("B", 2), ("C", 3), ("D", 4))); // 21オーバーフローとアンダーフロー
エラーは起きないが、オーバーフローとアンダーフローはしっかり起きていることが分かる。
Console.WriteLine(RpnCalculator<int>.Calculate("{0} 1 +", int.MaxValue, 1)); // -2147483648 Console.WriteLine(RpnCalculator<long>.Calculate("{0} 1 +", int.MaxValue, 1)); // 2147483648 Console.WriteLine(RpnCalculator<int>.Calculate("{0} 1 -", int.MinValue, 1)); // 2147483647 Console.WriteLine(RpnCalculator<long>.Calculate("{0} 1 -", int.MinValue, 1)); // -2147483649型による演算結果の相違
- 浮動小数型によって桁差が発生している。また、小数文字列から整数型への変換は失敗している。
Console.WriteLine(RpnCalculator<int>.Calculate("1 3 /")); // 0 Console.WriteLine(RpnCalculator<float>.Calculate("1 3 /")); // 0.3333333 Console.WriteLine(RpnCalculator<double>.Calculate("1 3 /")); // 0.333333333333333 Console.WriteLine(RpnCalculator<decimal>.Calculate("1 3 /")); // 0.3333333333333333333333333333 Console.WriteLine(RpnCalculator<int>.Calculate("3.14 3.14 +")); // FormatException Console.WriteLine(RpnCalculator<int>.Calculate("pi pi +", ("pi", (int)Math.PI))); // 6 Console.WriteLine(RpnCalculator<float>.Calculate("pi pi +", ("pi", (float)Math.PI))); // 6.283185 Console.WriteLine(RpnCalculator<double>.Calculate("pi pi +", ("pi", Math.PI))); // 6.28318530717959 Console.WriteLine(RpnCalculator<decimal>.Calculate("pi pi +", ("pi", (decimal)Math.PI))); // 6.283185307179580除算
- 当然エラーになるが、
double
型では非数を実装しているため、表示が下記の3パターンとなった。Console.WriteLine(RpnCalculator<int>.Calculate("1 0 /")); // DivideByZeroException Console.WriteLine(RpnCalculator<double>.Calculate("0 0 /")); // NaN Console.WriteLine(RpnCalculator<double>.Calculate("1 0 /")); // ∞ Console.WriteLine(RpnCalculator<double>.Calculate("-1 0 /")); // -∞
- 投稿日:2020-07-30T18:06:34+09:00
(基礎 1-1)UnityでのC# のお作法 ~Input~
入力値のコードとしてinput
sample1.csInput.GetAxis("Horizontal");上記は -1 ~ 1 nの間の不動小数点で入力値が取得できる
(Horizontalなので左右の水平方向の数値)sample2.csInput.GetAxisRaw("Vertical");上記の記入方法だと-1または1のせい数値のみが取得できる値となる
(こちらはVerticalなので上下の垂直方向の数値)※初めての投稿なのでこんな感じでまずはやってみました
- 投稿日:2020-07-30T17:45:07+09:00
MVVM をリアクティブプログラミングで快適に ReactiveProperty オーバービュー 2020 年版 中編
この記事は 2020 年の ReactiveProperty のオーバービューの全 3 編からなる記事の 2 つ目の記事です。
他の記事はこちらです。
- MVVM をリアクティブプログラミングで快適に ReactiveProperty オーバービュー 2020 年版 前編
- MVVM をリアクティブプログラミングで快適に ReactiveProperty オーバービュー 2020 年版 中編 (この記事)
- MVVM をリアクティブプログラミングで快適に ReactiveProperty オーバービュー 2020 年版 後編
コマンド
ReactiveProperty には MVVM アプリケーションを開発するときには必須のコマンドを提供しています。型引数の有無はありますが、大きくわけて非同期処理対応じゃないものと非同期処理対応のものの 2 種類のコマンドを提供しています。
- ReactiveCommand : 非同期処理対応じゃないもの
- AsyncReactiveCommand : 非同期処理対応のもの
ReactiveCommand
普通のコマンドです。特徴としては ICommand の実行可否を表す CanExecute と CanExecuteChanged イベントを
IObservable<bool>
から発行された値をもとに指定できるというものがあります。ReactiveCommand を作成するには
IObservable<bool>
に対してToReactiveCommand()
か 型引数ありのToReactiveCommand<T>()
を呼び出すことでコマンドの生成が出来ます。型引数がないものはコマンドパラメーターを受け取らないコマンドで、型引数を指定するとコマンドパラメーターを受けるコマンドになります。また、IObservable<T>
を実装していて、コマンドの Execute が呼ばれると OnNext が実行されます。そのため Subscribe でコマンドの実行処理を書けたり、Where などの LINQ のメソッドで加工や合成が可能になります。Subscribe メソッド
ReactiveCommand の特徴についてまとめます。
IObservable<bool>
からToReactiveCommand
メソッドで作成可能IObservable<bool>
から true が発行されると実行可能になりIObservable<bool>
から false が発行されると実行不可能になるIObservable<T>
を実装していて Command の Execute が呼ばれるとコマンドパラメーターの値を発行する- Subscribe でコマンド実行処理がかけたり Where/Select/Zip/Concat/etc... で合成ができる
動きは以下のようになります。
using Reactive.Bindings; using System; using System.Reactive.Subjects; namespace RxPropLab { class Program { static void Main(string[] args) { var commandSource = new Subject<bool>(); // 初期状態 var command = commandSource.ToReactiveCommand(true); command.Subscribe(() => Console.WriteLine("Execute が呼ばれたよ。")); command.CanExecuteChanged += (_, __) => Console.WriteLine("CanExecuteChanged が呼ばれたよ!"); // CanExecute の値は最後に発行された値 Console.WriteLine($"CanExecute: {command.CanExecute()}"); // ソースから値が発行されると CanExecuteChanged が呼ばれる commandSource.OnNext(false); commandSource.OnNext(true); command.Execute(); // Subscribe が呼ばれる } } }実行結果は以下のようになります。
CanExecute: True CanExecuteChanged が呼ばれたよ! CanExecuteChanged が呼ばれたよ! Execute が呼ばれたよ。ToReactiveProperty の型引数を指定するとコマンドパラメーターありになります。
動作確認のコードは以下のようになります。コマンドパラメーター有り版using Reactive.Bindings; using System; using System.Reactive.Subjects; namespace RxPropLab { class Program { static void Main(string[] args) { var commandSource = new Subject<bool>(); // 初期状態 var command = commandSource.ToReactiveCommand<string>(true); command.Subscribe(x => Console.WriteLine($"Execute が呼ばれたよ。: {x}")); command.CanExecuteChanged += (_, __) => Console.WriteLine("CanExecuteChanged が呼ばれたよ!"); // CanExecute の値は最後に発行された値 Console.WriteLine($"CanExecute: {command.CanExecute()}"); // ソースから値が発行されると CanExecuteChanged が呼ばれる commandSource.OnNext(false); commandSource.OnNext(true); command.Execute("ぱらめーたー"); // Subscribe が呼ばれる。パラメーターも渡せる } } }実行すると以下のようになります。
CanExecute: True CanExecuteChanged が呼ばれたよ! CanExecuteChanged が呼ばれたよ! Execute が呼ばれたよ。: ぱらめーたーXAML プラットフォームでは、この ReactiveCommand を Button などの Command プロパティとバインドして使用します。
実際に ViewModel クラスで定義した例を以下に示します。MainWindowViewModel.csusing Reactive.Bindings; using System; using System.ComponentModel; using System.Reactive.Linq; namespace RxPropLabWpf { // WPF のメモリリーク対策で INotifyPropertyChanged は実装しておく public class MainWindowViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; // コマンドのソース用 public ReactivePropertySlim<bool> IsChecked { get; } // コマンドを押したときに更新するメッセージ public ReadOnlyReactivePropertySlim<string> Message { get; } // コマンド public ReactiveCommand SampleCommand { get; } public MainWindowViewModel() { // デフォルト値が true の設定 IsChecked = new ReactivePropertySlim<bool>(true); // ReactiveProperty は IObservable なので ReactiveCommand にできる SampleCommand = IsChecked.ToReactiveCommand(); // ReactiveCommand は IObservable なので Select で加工して ReactiveProperty に出来る Message = SampleCommand.Select(_ => DateTime.Now.ToString()) .ToReadOnlyReactivePropertySlim(); } } }XAML で使ってみましょう。
<Window x:Class="RxPropLabWpf.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:RxPropLabWpf" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800"> <Window.DataContext> <!-- ViewModel を設定して --> <local:MainWindowViewModel /> </Window.DataContext> <StackPanel Margin="10"> <!-- ReactivePropertySlim とバインド --> <CheckBox IsChecked="{Binding IsChecked.Value, Mode=TwoWay}" Content="CanExecute" Margin="5"/> <!-- コマンドとバインド --> <Button Content="Command" Command="{Binding SampleCommand}" Margin="5"/> <!-- ReadOnlyReactivePropertySlim とバインド --> <TextBlock Text="{Binding Message.Value}" Margin="5"/> </StackPanel> </Window>実行してみると以下のように動きます。チェックボックスとボタンの活性非活性が
ReactivePropertySlim<bool>
から作られたReactiveCommand
の CanExecute 軽油で同期されます。そしてボタンのクリックをIObservable<string>
に Select で変換してToReadOnlyReactivePropertySlim
でReactivePropertySlim
にしたものとバインドすることでコマンド実行と同時に一番下の TextBlock にメッセージが表示されます。この他に ReactiveCommand には WithSubscribe メソッドがが定義されています。これはコマンドのインスタンス生成から Subscribe までをメソッドチェーンで定義できる便利メソッドです。以下のように利用します。
// コマンドのソース var source = new Subject<bool>(); // WithSubscribe を使わない方法 var command1 = source.ToReactiveCommand(); command.Subscribe(() => 何か処理); // WithSubscribe を使う方法 var command2 = source.ToReactiveCommand() .WithSubscribe(() => 何か処理);便利。
最後に常に実行されていればいいコマンドは普通に
new ReactiveCommand()
で作成できます。AsyncReactiveCommand
次に非同期対応のコマンドです。このコマンドは、Subscribe に非同期メソッドが指定できます。そして非同期メソッドが実行している間は CanExecute が false を返してボタンなどが自動的に非活性になります。便利。
以下にコード例を示します。using Reactive.Bindings; using System; using System.ComponentModel; using System.Reactive.Linq; using System.Threading.Tasks; namespace RxPropLabWpf { // WPF のメモリリーク対策で INotifyPropertyChanged は実装しておく public class MainWindowViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; public AsyncReactiveCommand SampleCommand { get; } public ReactivePropertySlim<string> Message { get; } public MainWindowViewModel() { SampleCommand = new AsyncReactiveCommand() // 非同期処理を Subscribe 可能 .WithSubscribe(async () => { Message.Value = "開始"; await Task.Delay(3000); Message.Value = "終了"; }); } } }DataContext に設定して適当にバインドします。
<Window x:Class="RxPropLabWpf.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:RxPropLabWpf" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800"> <Window.DataContext> <!-- ViewModel を設定して --> <local:MainWindowViewModel /> </Window.DataContext> <StackPanel Margin="10"> <!-- コマンドとバインド --> <Button Content="Command" Command="{Binding SampleCommand}" Margin="5"/> <!-- ReadOnlyReactivePropertySlim とバインド --> <TextBlock Text="{Binding Message.Value}" Margin="5"/> </StackPanel> </Window>実行すると以下のようになります。ちゃんと非同期処理が実行中はボタンが非活性になっています。
注意点としては、AsyncReactiveCommand は
IObservable<bool>
ではないので ReactiveCommand のように Select などは出来ません。Subscribe か WithSubscribe をするだけになります。実行可否状態の共有
非同期操作を行うコマンドが画面内に複数個あって、どれかが実行されている間は全部のボタンを押せないようにしないという要件は結構あると思います。
AsyncReactiveCommand は、その機能を組み込みでサポートしています。AsyncReactiveCommand を生成するためのメソッドやコンストラクターには
IReactiveProperty<bool>
を受け取るオーバーロードやIReactiveProperty<bool>
専用の ToAsyncReactiveCommand 拡張メソッドがあります。IReactiveProperty<T>
はReactiveProperty<T>
クラスとReactivePropertySlim<T>
が実装しているインターフェースです。つまりReactiveProperty<bool>
かReactivePropertySlim<T>
を渡せるということです。
IReactiveProperty<bool>
を使って作った AsyncReactiveCommand は、実行可否の状態を、このIReactiveProperty<bool>
を通して共有します。
例えば、コマンドの実行可否を共有する 2 つの AsyncReactiveCommand を作るようなコードは以下のようになります。using Reactive.Bindings; using System.ComponentModel; using System.Threading.Tasks; namespace RxPropLabWpf { // WPF のメモリリーク対策で INotifyPropertyChanged は実装しておく public class MainWindowViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; public AsyncReactiveCommand LongTimeProcess1Command { get; } public AsyncReactiveCommand LongTimeProcess2Command { get; } public MainWindowViewModel() { // 同じ ReactiveProperty<bool> から非同期コマンドを作成 var sharedCanExecuteReactiveProperty = new ReactivePropertySlim<bool>(true); LongTimeProcess1Command = sharedCanExecuteReactiveProperty.ToAsyncReactiveCommand() .WithSubscribe(async () => await Task.Delay(3000)); LongTimeProcess2Command = sharedCanExecuteReactiveProperty.ToAsyncReactiveCommand() .WithSubscribe(async () => await Task.Delay(3000)); } } }この 2 つのコマンドをボタンにバインドしてみましょう。
<Window x:Class="RxPropLabWpf.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:RxPropLabWpf" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800"> <Window.DataContext> <!-- ViewModel を設定して --> <local:MainWindowViewModel /> </Window.DataContext> <StackPanel> <Button Content="1" Command="{Binding LongTimeProcess1Command}" /> <Button Content="2" Command="{Binding LongTimeProcess2Command}" /> </StackPanel> </Window>実行すると以下のようになります。コマンドの実行可否のステータスが共有化されていることがわかります。
もう少し複雑な例を紹介したいと思います。例えば 2 つの入力項目があって、そこに入力エラーが無かったら押せる AsyncReactiveCommand に紐づいたボタンが複数個あって、それの実行可否を共有するようにしてみましょう。
以下のようなコードになります。
using Reactive.Bindings; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Reactive.Linq; using System.Threading.Tasks; namespace RxPropLabWpf { // WPF のメモリリーク対策で INotifyPropertyChanged は実装しておく public class MainWindowViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; [Required] public ReactiveProperty<string> Input1 { get; } [Required] public ReactiveProperty<string> Input2 { get; } public AsyncReactiveCommand LongTimeProcess1Command { get; } public AsyncReactiveCommand LongTimeProcess2Command { get; } public MainWindowViewModel() { // 必須入力チェックつき ReactiveProperty Input1 = new ReactiveProperty<string>() .SetValidateAttribute(() => Input1); Input2 = new ReactiveProperty<string>() .SetValidateAttribute(() => Input2); // ObservehasErrors は ReactiveProperty の入力エラーに変化があるたびに値が発行される IObservable<bool> // それをもとに、入力エラーがない状態を表す IObservable<bool> を作成 var allInputsAreValid = Input1 .ObserveHasErrors.CombineLatest(Input2.ObserveHasErrors, (x, y) => !x && !y); // 入力エラーがないときに実行できるコマンドを作りつつ、IReactiveProperty<bool> を使って状態を共有するようにする var sharedCanExecuteReactiveProperty = new ReactivePropertySlim<bool>(true); LongTimeProcess1Command = allInputsAreValid.ToAsyncReactiveCommand(sharedCanExecuteReactiveProperty) .WithSubscribe(async () => await Task.Delay(3000)); LongTimeProcess2Command = allInputsAreValid.ToAsyncReactiveCommand(sharedCanExecuteReactiveProperty) .WithSubscribe(async () => await Task.Delay(3000)); } } }XAML で適当にバインドします。
<Window x:Class="RxPropLabWpf.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:RxPropLabWpf" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800"> <Window.DataContext> <!-- ViewModel を設定して --> <local:MainWindowViewModel /> </Window.DataContext> <StackPanel> <TextBox Text="{Binding Input1.Value, UpdateSourceTrigger=PropertyChanged}" Margin="5" /> <TextBox Text="{Binding Input2.Value, UpdateSourceTrigger=PropertyChanged}" Margin="5" /> <Button Content="1" Command="{Binding LongTimeProcess1Command}" Margin="5" /> <Button Content="2" Command="{Binding LongTimeProcess2Command}" Margin="5" /> </StackPanel> </Window>実行すると以下のようになります。エラーのないときだけ押せて、なおかつどちらかしか押せないボタンが出来ました。
コレクション
ReactiveProperty では、いくつかの便利なコレクションクラスを適用しています。以下の 3 つをよく使うことになると思います。
- ReactiveCollection: UI スレッド上でコレクションの追加・削除などを行う機能をもつコレクション
- ReadOnlyReactiveCollection: 別のコレクションから型変換を行い読み取り専用コレクションとして動くコレクション。コレクションの変更通知は UI スレッド上で行う。
- IFilteredReadOnlyObservableCollection: 別のコレクションを指定した条件でフィルタリングした内容を表示するコレクション
ReactiveCollection
ObservableCollection に UI スレッド上でのコレクション操作を行うメソッドを追加したものになります。
AddOnScheduler や RemoveOnScheduler などのように、普通のコレクション操作を行うメソッドに対して OnScheduler がついたメソッドが定義されています。このメソッドを呼び出すことで自動的に UI スレッド上でコレクション操作が行われます。通常はバックグラウンドのスレッドからコレクション操作を行うと、そのコレクションが UI 要素にバインドされているとアプリが落ちますが、ReactiveCollection の OnScheduler のついているメソッドを使うことでバックグラウンドスレッドからも割と気軽にコレクションが操作できます。例を以下に示します。
using Reactive.Bindings; using System; using System.ComponentModel; using System.Threading.Tasks; namespace RxPropLabWpf { // WPF のメモリリーク対策で INotifyPropertyChanged は実装しておく public class MainWindowViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; public ReactiveCommand SampleCommand { get; } public ReactiveCollection<DateTime> Timestamps { get; } public MainWindowViewModel() { Timestamps = new ReactiveCollection<DateTime>(); SampleCommand = new ReactiveCommand() .WithSubscribe(() => { Task.Run(() => // UI スレッド以外でコレクション操作 // XxxOnScheduler メソッドで UI スレッド上でコレクション操作を行う Timestamps.AddOnScheduler(DateTime.Now)); }); } } }これを適当に画面にバインドします。
<Window x:Class="RxPropLabWpf.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:RxPropLabWpf" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800"> <Window.DataContext> <!-- ViewModel を設定して --> <local:MainWindowViewModel /> </Window.DataContext> <DockPanel Margin="10"> <!-- コマンドとバインド --> <Button Content="Command" Command="{Binding SampleCommand}" Margin="5" DockPanel.Dock="Top" /> <ListBox ItemsSource="{Binding Timestamps}" Margin="5" /> </DockPanel> </Window>ボタンを押すと UI スレッド以外からコレクション操作が行われますが例外が起きることはありません。
この他に
IObservable<T>
からReactiveCollection<T>
を生成する ToReactiveCollection メソッドも提供しています。このメソッドを呼ぶとIObservable<T>
から値が発行されたらコレクションに要素が追加されます。
例えば先ほどの MainWindowViewModel を以下のように書き換えても同じ動作になります。using Reactive.Bindings; using System; using System.ComponentModel; using System.Reactive.Linq; using System.Threading.Tasks; namespace RxPropLabWpf { // WPF のメモリリーク対策で INotifyPropertyChanged は実装しておく public class MainWindowViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; public ReactiveCommand SampleCommand { get; } public ReactiveCollection<DateTime> Timestamps { get; } public MainWindowViewModel() { SampleCommand = new ReactiveCommand(); Timestamps = SampleCommand.Select(_ => DateTime.Now) .ToReactiveCollection(); } } }ReactiveCollection のスレッドを自動で切り替える機能は便利ですが、スレッドを切り替えるということは追加や削除などのコレクション操作はメソッドを呼び出しても即座には行われないという点に注意が必要です。
例えば AddOnScheduler で 10 個の要素を追加して、そのままコレクションの Count を参照しても要素は増えていません。例えば、先ほどの ViewModel を以下のように書き換えてみます。
using Reactive.Bindings; using System; using System.ComponentModel; using System.Linq; using System.Reactive.Linq; using System.Threading.Tasks; using System.Windows; namespace RxPropLabWpf { // WPF のメモリリーク対策で INotifyPropertyChanged は実装しておく public class MainWindowViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; public ReactiveCommand SampleCommand { get; } public ReactiveCollection<DateTime> Timestamps { get; } public MainWindowViewModel() { Timestamps = new ReactiveCollection<DateTime>(); SampleCommand = new ReactiveCommand() .WithSubscribe(() => { foreach (var i in Enumerable.Range(1, 10)) { Timestamps.AddOnScheduler(DateTime.Now); } MessageBox.Show($"{Timestamps.Count}"); }); } } }コマンドでコレクションに 10 個の要素を AddOnScheduler で追加して Count プロパティを MessageBox で表示しています。素直に考えると 10 と表示されてほしいところですが、先ほど言った理由から 0 と表示されます。
現状では、コレクションの操作の完了を待つ方法はありません。
ReadOnlyReactiveCollection
ObservableCollection のような変更通知の機能を持ったコレクションと同期する読み取り専用のコレクションが作成できます。また、ReadOnlyReactiveCollection では自動的にコレクションの変更通知イベントを UI スレッド上で実行します。ReadOnlyReactiveCollection を生成するには、元になるコレクションに対して ToReadOnlyReactiveCollection 拡張メソッドを呼び出します。ToReadOnlyReactiveCollection メソッドの引数にはラムダ式が指定できて、ここで元になるコレクションの要素に対する変換処理が指定できます。
例えば Guid を保持する ObservableCollection を元にして Guid を表示用に加工した文字列を保持する同期した ReadOnlyReactiveCollection を作るコードは以下のようになります。
using Reactive.Bindings; using System; using System.Collections.ObjectModel; using System.ComponentModel; using System.Threading.Tasks; namespace RxPropLabWpf { // WPF のメモリリーク対策で INotifyPropertyChanged は実装しておく public class MainWindowViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; public ReactiveCommand SampleCommand { get; } public ObservableCollection<Guid> Guids { get; } public ReadOnlyReactiveCollection<string> Views { get; } public MainWindowViewModel() { // 元になる ObservableCollection Guids = new ObservableCollection<Guid>(); // 同期する読み取り専用コレクションを作成 // ToReadOnlyReactiveCollection の引数で変換処理も指定可能 Views = Guids.ToReadOnlyReactiveCollection(x => $"Guid: {x}"); SampleCommand = new ReactiveCommand() .WithSubscribe(() => Task.Run(() => { // 別スレッドから元になるコレクションを操作 Guids.Add(Guid.NewGuid()); })); } } }XAML は以下のようになります。
<Window x:Class="RxPropLabWpf.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:RxPropLabWpf" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800"> <Window.DataContext> <!-- ViewModel を設定して --> <local:MainWindowViewModel /> </Window.DataContext> <DockPanel Margin="10"> <!-- コマンドとバインド --> <Button Content="Command" Command="{Binding SampleCommand}" Margin="5" DockPanel.Dock="Top" /> <ListBox ItemsSource="{Binding Views}" Margin="5" /> </DockPanel> </Window>実行すると以下のようになります。元になるコレクションを UI スレッド以外から操作してもエラーにならないことが確認できます。
ReadOnlyReactiveCollection から要素が削除されるタイミングで、ReadOnlyReactiveCollection は削除される要素が IDisposable を実装している場合に Dispose メソッドを呼び出します。何か要素の後始末が必要な場合は IDisposable を実装してください。
注意!!
今回は例のために ObservableCollection を UI スレッド外から更新しましたが、ObservableCollection はスレッドセーフなコレクションではないので複数スレッドから要素の追加や削除などを行うと中身が壊れる可能性があります。(というか壊れます)
気を付けましょう。複数スレッドからデータを追加する場合はロックをかけるなどの対策が必要です。
IFilteredReadOnlyObservableCollection
IFilteredReadOnlyObservableCollection は INotifyPropertyChanged を実装したクラスの ObservableCollection に対してリアルタイムでフィルタリングを行うコレクションです。使い方は簡単で INotifyPropertyChanged を実装したクラスの ObservableCollection に対して ToFilteredReadOnlyObservableCollection をフィルタリングの条件のラムダ式と共に呼び出すだけです。
例えば以下のように一定間隔で Value プロパティが変わる Sensor クラスがあるとします。
using System; using System.Collections.Generic; using System.ComponentModel; using System.Text; using System.Threading; using System.Threading.Tasks; namespace RxPropLabWpf { public class Sensor : INotifyPropertyChanged, IDisposable { private CancellationTokenSource _cancellationTokenSource; public string Name { get; } private int _value; public int Value { get => _value; private set { _value = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value))); } } public event PropertyChangedEventHandler PropertyChanged; public Sensor(string name) { Name = name; // Dispose されるまで3秒間隔でランダムに Value を更新する _cancellationTokenSource = new CancellationTokenSource(); Start(_cancellationTokenSource.Token); } private async void Start(CancellationToken token) { var random = new Random(); while (!token.IsCancellationRequested) { Value = random.Next(100); await Task.Delay(3000); } } public void Dispose() => _cancellationTokenSource.Cancel(); } }このクラスの ObservableCollection と Value が 50 以上のみを表示する IFilteredReadOnlyObservableCollection を作成するコードは以下のようになります。
using Reactive.Bindings; using Reactive.Bindings.Helpers; using Reactive.Bindings.Extensions; using System; using System.Collections.ObjectModel; using System.ComponentModel; using System.Threading.Tasks; namespace RxPropLabWpf { // WPF のメモリリーク対策で INotifyPropertyChanged は実装しておく public class MainWindowViewModel : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; public ObservableCollection<Sensor> Sensors { get; } public IFilteredReadOnlyObservableCollection<Sensor> AlertTargets { get; } public ReactiveCommand SampleCommand { get; } public MainWindowViewModel() { Sensors = new ObservableCollection<Sensor>(); AlertTargets = Sensors.ToFilteredReadOnlyObservableCollection(x => x.Value >= 50); SampleCommand = new ReactiveCommand() .WithSubscribe(() => { Sensors.Add(new Sensor(Guid.NewGuid().ToString())); }); } } }これを XAML にバインドします。
<Window x:Class="RxPropLabWpf.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:RxPropLabWpf" mc:Ignorable="d" Title="MainWindow" Height="450" Width="800"> <Window.DataContext> <!-- ViewModel を設定して --> <local:MainWindowViewModel /> </Window.DataContext> <Window.Resources> <DataTemplate x:Key="sensorTemplate" DataType="local:Sensor"> <StackPanel> <TextBlock Text="{Binding Name}" /> <TextBlock Text="{Binding Value}" /> </StackPanel> </DataTemplate> </Window.Resources> <DockPanel Margin="10"> <!-- コマンドとバインド --> <Button Content="Command" Command="{Binding SampleCommand}" Margin="5" DockPanel.Dock="Top" /> <Grid> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition /> </Grid.ColumnDefinitions> <ListBox ItemsSource="{Binding Sensors}" ItemTemplate="{StaticResource sensorTemplate}" Margin="5" /> <ListBox ItemsSource="{Binding AlertTargets}" ItemTemplate="{StaticResource sensorTemplate}" Grid.Column="1" Margin="5" /> </Grid> </DockPanel> </Window>実行すると以下のように左側には全部の要素が表示されますが、右側には Value が 50 以上の要素しか表示されていないことが確認できます。
UI スレッドについて
ReactiveProperty の提供するクラスで以下のものは UI スレッドへの自動ディスパッチ機能があります。
- ReactiveProperty
- ReadOnlyReactiveProperty
- ReactiveCollection
- ReadOnlyReactiveCollection
- ReactiveCommand
前編でも書きましたが、これは
ReactivePropertyScheduler.SetDefault(IScheduler defaultScheduler)
メソッドで任意のスケジューラーに置き換えることが出来ます。デフォルトが UI スレッドになっています。
アプリケーションのエントリーポイントでReactivePropertyScheduler.SetDefault(ImmediateScheduler.Instance);
のようなコードを指定することで UI スレッドへのディスパッチを辞めて即座に実行するように変更することが出来ます。グローバルに一括で設定するほかに各クラスのインスタンスを生成する際に IScheduler 型の引数を受け取るオーバーロードがあるので、それを使ってディスパッチ先を変えることが出来ます。
例えば UWP で UI スレッドが異なるウィンドウに紐づく ViewModel の場合は現在の UI スレッドに紐づく UI スレッドを使うようにすることで対応可能です。例えば以下のようになります。
public class MainPageViewModel { private readonly IScheduler _uiThreadScheduler; public ReadOnlyObservableCollection<string> Items { get; } public MainPageViewModel(IScheduler uiThreadScheduler = null) { // null の場合は現在の SynchronizationContext に紐づいたスケジューラーを使う _uiThreadScheduler = uiThreadScheduler ?? new SynchronizationContextScheduler(SynchronizationContext.Current); // ReactiveProperty 系のクラスを作るときは、このスケジューラーを指定して作る Items = Model.Items.ToReadOnlyReactiveCollection(scheduler: _uiThreadScheduler); } } public static class Model { // 今回は手抜きでグローバルな位置にあるコレクション public static ObservableCollection<string> Items { get; } = new ObservableCollection<string>(); }このようにすることで、各画面ごとの UI スレッドにディスパッチされるようになります。
ReactiveProperty の後始末
ReactiveProperty や ReactiveCommand や ReactiveCollection などの殆どのクラスは IDisposable インターフェースを実装しています。これらのクラスで Dispose を呼ぶと例えば ReactiveProperty が IObservable から作られている場合は、IObservable と切り離されます。また、ReactiveProperty を Subscribe している人たちへ OnCompleted を発行して切り離します。
なので、不要になったタイミングで Dispose を呼び出しましょう。
Dispose をまとめて呼ぶ方法
Reactive Extensions には IDisposable を一括で Dispose してくれる CompositeDisposable があります。この CompositeDisposable に対して ReactiveProperty や ReactivePropertySlim や ReactiveCommand などを Add することで、任意のタイミングで一括で Dispose 出来ます。
また ReactiveProperty では IDisposable の拡張メソッドとして CompositeDisposable に簡単に登録できる AddTo 拡張メソッドを提供しています。これを使うことで ReactiveProperty や ReactiveCommand の生成のメソッドチェーンに CompositeDisposable への追加処理を自然に書くことが出来ます。
コード例を以下に示します。
// ReadOnlyReactivePropertySlim のソース var rpSource = new Subject<string>(); // 一括 Dispose 用 var disposables = new CompsisteDisposable(); // 大文字の ReactiveProperty を生成 var upperProp = rpSource.Select(x => x?.ToUpper()) .ToReadOnlyReactivePropertySlim() .AddTo(disposables); // AddTo で CompositeDisposables へ追加 // 小文字の ReactiveProperty を生成 var lowerProp = rpSource.Select(x => x?.ToLower()) .ToReadOnlyReactivePropertySlim() .AddTo(disposables); // AddTo で CompositeDisposables へ追加 // 一括で Dispose 可能 disposables.Dispose();一般的には ViewModel クラスのフィールドやプロパティで CompositeDisposable を保持して、不要になったタイミングで Dispose を呼んで一括で購読などを解除します。以下のようなコードになります。
public class MainWindowViewModel : INotifyPropertyChanged, IDisposable { ... 省略 ... private CompositeDisposable Disposables { get; } = new CompositeDisposable(); public ReactiveCommand SampleCommand { get; } public ReactivePropertySlim<string> Input { get; } public ReadOnlyReactivePropertySlim<string> Output { get; } public MainWindowViewModel() { // メソッドチェーンの最後で AddTo をして CompositeDisposable に登録 Input = new ReactivePropertySlim<string>() .AddTo(Disposables); Output = Input.Select(x => x?.ToUpper()) .Delay(TimeSpan.FromSeconds(3)) .ObserveOnUIDispatcher() .ToReadOnlyReactivePropertySlim() .AddTo(Disposables); // Command 自体を Dispose すれば普通は問題ないが WithSubscribe を個別に開放する方法もあります SampleCommand = Input.Select(x => string.IsNullOrEmpty(x)) .ToReactiveCommand() .WithSubscribe(() => { ...省略... }, Disposables.Add) // WithSubscribe は第二引数で Disposable を追加するメソッドを受け取れる .AddTo(Disposables); // コマンド自身を追加する場合は AddTo } // 今回の ViewModel は不要になったタイミングで Dispose が呼ばれる想定 public void Dispose() => Disposables.Dispose(); }Dispose を呼び出さないことによる弊害
単純にずっと IObservable の処理がつながっていると、何かの値が発行されるたびに動く処理が多くなるので性能は悪くなるでしょう。
もっと深刻な例では寿命の長いオブジェクトの発行する値を購読して ReactiveProperty や ReactiveCommand を作っているとメモリリークの原因になります。例えば寿命の長いオブジェクトに ObserveProperty をして ReactiveProperty への変換したとします。この時 ObserveProperty は寿命の長いオブジェクトの PropertyChanged イベントを購読しています。
PropertyChanged イベントを購読しているということは、PropertyChanged イベントの発行先として寿命の長いオブジェクトに発行先として登録されるということです。これは ToReadOnlyReactivePropertySlim で作られた ReadOnlyReactivePropertySlim を Dispose するまで開放されません。
なので、ライフサイクルの長いオブジェクトから作った ReactiveProperty などは必ず不要になったタイミングで Dispose しましょう。
まとめ
中編では主にコレクションや、コマンドについて説明しました。スレッドや後始末など意外と重要なことも入っているのでもしかしたらシリーズの中で一番重要なところかもしれません。
残るはプラットフォーム固有機能と、便利クラスたちの紹介になります。
- 投稿日:2020-07-30T16:24:27+09:00
【初記事】目標【20’07/30】
1.前置き
今年の年始に、初詣をしたときのことでした。
私の家庭では、大晦日の夜に家族で初詣に行くことが恒例行事となっていました。
また、初詣後におみくじを引くことが一つの楽しみでもありました。
去年は、半吉(記憶は曖昧)という比較的パッとしない結果だったので、「今年こそは」と意気込んでおみくじ売り場に行きました。
結果としては、「大吉」を引くことが来ました。
しかし、いざその内容を見てみると...「仕事も勉強も恋愛も何もかもうまくいかない」というような、散々なことが書かれていたのです。
その時はショックを隠せず、家族に内容を聞かれても、話半分で返事をしていました。
ただ、そこにはこうも書かれていました。「目標をもって行動すれば自ずと結果もついてくる」と。
そこで、今回のタイトルに繋がるのですが、何かを始める前に、まず最終目標を決めてから物事に取り組むことにしようと決意したのです。
2.目標
今回の目標は「Apple Watchのアプリ製作」です。
具体的には、iPhoneとBluetoothで接続されたセンサーの値をリアルタイムにApple Watchで表示させるようなアプリを制作します。自分はあまりプログラムが得意ではないのでゆっくりにはなると思いますが、Web上の様々な記事を参考にしながら進めていきたいと考えています。
参考にした記事のパクリ記事にならないように気をつけながら、更新して行けたらと思います。
3.終わりに
基本、見てくれる人はいないと考えて、自分の日記・備忘録代わりとして使用していきます。
そのため、読みづらい箇所も多々あると思いますがご了承ください。
※もし見てくださる方がいらっしゃっれば、記事に対するアドバイスやコメント頂ければ幸いです。
- 投稿日:2020-07-30T13:41:52+09:00
【C#】 Socket通信プログラムで最低限必要な事とHello World!するまでを解説。
自己紹介
こんにちは。tetraです。
最近C#の学習を始めました。C#歴4ヶ月目の新卒エンジニアです。
サーバーとクライアントで通信プログラムを作成する事になりました。全くやり方が分からなかったので、同じ思いをする人の支えになればいいと思い作成しました。この記事でわかること
1.ソケット通信プログラムを作成する際、最低限必要なことが分かる。
2.サーバーサイドで何をするべきか分かる。
3.クライアントサイドで何をするべきか分かる。知っておくべきこと
『クライアント』『サーバー』の概念があり、相互で通信を行う。
サーバーサイド
- ソケットを作成する。
- ソケットにポート番号を割り当てる。
- クライアントから接続要求を受けた時に受け入れる準備をしておく。
- 〜計算などやりたい処理を行う。〜(今回はHello World!を受け取り、HELLO WORLD!にして返す。)
- ソケットを削除し、終了。
クライアントサイド
- ソケットの作成
- サーバーとの接続を確立する。
- 〜やりたい処理を行う。〜(今回はHello World!を送り、HELLO WORLD!を受け取る。)
- ソケットを削除し終了。
完成形
サーバーサイド
server.csusing System; using System.Net; using System.Net.Sockets; using System.Text; namespace socketS { class S { public static void Main() { SocketServer(); } public static void SocketServer() { //ここからIPアドレスやポートの設定 // 着信データ用のデータバッファー。 byte[] bytes = new byte[1024]; IPHostEntry ipHostInfo = Dns.GetHostEntry(Dns.GetHostName()); IPAddress ipAddress = ipHostInfo.AddressList[0]; IPEndPoint localEndPoint = new IPEndPoint(ipAddress, 11000); //ここまでIPアドレスやポートの設定 //ソケットの作成 Socket listener = new Socket(ipAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp); //通信の受け入れ準備 listener.Bind(localEndPoint); listener.Listen(10); //通信の確率 Socket handler = listener.Accept(); // 任意の処理 //データの受取をReceiveで行う。 int bytesRec = handler.Receive(bytes); string data1 = Encoding.UTF8.GetString(bytes, 0, bytesRec); Console.WriteLine(data1); //大文字に変更 data1 = data1.ToUpper(); //クライアントにSendで返す。 byte[] msg = Encoding.UTF8.GetBytes(data1); handler.Send(msg); //ソケットの終了 handler.Shutdown(SocketShutdown.Both); handler.Close(); } } }クライアントサイド
client.csusing System; using System.Net; using System.Net.Sockets; using System.Text; namespace socketC { class C { public static void Main() { //今回送るHello World! string st = "Hello World!"; SocketClient(st); } public static void SocketClient(string st) { //IPアドレスやポートを設定している。 IPHostEntry ipHostInfo = Dns.GetHostEntry(Dns.GetHostName()); IPAddress ipAddress = ipHostInfo.AddressList[0]; IPEndPoint remoteEP = new IPEndPoint(ipAddress, 11000); //ソケットを作成 Socket socket = new Socket(ipAddress.AddressFamily, SocketType.Stream, ProtocolType.Tcp); //接続する。失敗するとエラーで落ちる。 socket.Connect(remoteEP); //Sendで送信している。 byte[] msg = Encoding.UTF8.GetBytes(st + "<EOF>"); socket.Send(msg); //Receiveで受信している。 byte[] bytes = new byte[1024]; int bytesRec = socket.Receive(bytes); string data1 = Encoding.UTF8.GetString(bytes, 0, bytesRec); Console.WriteLine(data1); //ソケットを終了している。 socket.Shutdown(SocketShutdown.Both); socket.Close(); } } }いかがでしょうか?
いかがでしょうか。経験が浅いため至らぬところがまだまだあります。
もし間違い等に気がつきましたら、コメント又はtetraまでお知らせください。
- 投稿日:2020-07-30T12:51:25+09:00
Vuforia EngineのArea Targetsを使ってみた
まえがき
Vuforia EngineのArea Targetsという機能を用いて、オフィスの共用部分をスキャンしてオブジェクトを表示してみました。
Area Targetsは、スキャンした空間全体をターゲットにすることができる機能です。
2020/7/11現在では、Matterport Pro2という3Dカメラでスキャンした空間のみターゲットにすることができます。
また、Matteport Pro2でスキャンしたデータをArea Targetsで利用するためには、Matterport Pro Professional(69ドル/月)と、スキャンデータをダウンロードするのにMatter Pak(Professional Planでは49ドル)が必要です。
また、Matterport側にAPIの利用申請をメールで送っておく必要があります。
実行環境
- Unity 2019.3.0f3
- Matterport Pro2
- Vuforia Engine 9.0
- iPhone 11 Pro
Vuforia Engineとは
Vuforia Engineは、iOSやAndroid、PC、HoloLensといったAR/MRデバイスに対応したAR開発プラットフォームです。
Vuforia EngineはPTCが提供していますが、元々はQualcommが開発しており、2015年にPTCがVuforia事業を6500万ドルで買収しています。
マーカートラッキングを非常に得意としており、特徴点の多い(複雑な)画像であればほとんどぶれることなく3Dオブジェクトを配置することができます。
詳しくはこちらをご覧ください!
画像をターゲットにするImage Targetsを使っていきたいと思います。
ライセンスの取得とライセンスキーをUnityに設定する方法関しては、ラズパイ4をVuforia Engineで分身させてみた ~Object Recognition編~こちらをご覧ください。
Area Targetsとは
Image TargetsやModel Targetsは視界に捉えられる被写体に対するものに限られており、広い空間をトラッキングするために設計されたものではありません。
Area Targetsはスキャンした空間をトラッキング対象として、AR表現を行うことが可能になります。
今回は、このようにオフィスの共用部をスキャンしてArea Targetsを使用してみました。
スキャンからUnityで実行するまで
Matterport Pro2による撮影
シェアオフィスの共用エリアをMatterport Pro2を用いて3Dスキャンしました。
画像のようにMatterport Pro2を設置します。
そして、iPhoneのMatterport Captureというアプリでスキャンを始めることができます。
実際に撮影している風景がこちらです。
Matterport Pro2によるスキャン風景 pic.twitter.com/a2fIDL7gwX
— こーや (@koyataroo) July 19, 202060度ずつ回転してスキャンをしているようです。
実際に撮影する場合は、自分が写ったらダメなのでこのように物陰に隠れて撮影をします。
物陰に隠れてMatterport Pro2で撮影 pic.twitter.com/PhSKcJOfyL
— こーや (@koyataroo) July 30, 2020複数箇所スキャンすると、以下のようにアプリ上にスキャン結果が表示されます。
公式では、2m間隔でスキャンすることが推奨されています。
窓の位置は、手動で設定をしました。
そうすることで窓ガラスの外を認識してしまった場合、不要な部分は削除することができます。
スキャンデータをクラウドにアップロード
スキャンデータをMatterportのサーバーにアップロードすることで、スキャンデータを作成することができます。
ここで、Matteport Pro2で撮影したデータをアップロードする場合には、69ドル/月のProfessional Planに加入している必要があります。
アプリからアップロードすることができ、3-4時間でスキャンデータの作成が完了しました。
スキャンデータをArea Targetsで利用できるようにする
スキャンデータをクラウドからダウンロードするには、49ドルが必要になります。(Matter Pak)
Matter Pakの購入後、Area Target Generatorを用いてスキャンデータをArea Targetsで利用できる形式に変換します。
MatterportにAPI利用申請のメールを送って承認されると、Tokenが取得できるのでそれを入力します。
Scanned Spaceには、クラウド上のスキャンデータのIDを入力します。(どこにあるのかわからず、URLの???の部分を入力したらいけましたhttps://my.matterport.com/models/???)
すると.unitypackageファイルを作成することができます。
Area TargetsをUnityで使う
Unityでプロジェクトを作成した後、先ほどの.unitypackageを読み込みます。
すると、Area Targetsを配置することができます。
あとは通常の開発同様、Area Targets配下にオブジェクトを配置していくといった流れです。
Area Targetsを用いて実装
作成したもの
Vuforia Area Targetsのデモ pic.twitter.com/uAspSz4Al3
— こーや (@koyataroo) July 19, 2020実装内容
- 室内の温度の表示
実際の気温とは関係なく、1秒おきに変化させています。
今回は行っていませんが、IoTデバイスと連携して、温度センサー情報を取得して表示をする想定です。
アニメーション付きで変化させてみると見栄えもよくなりそうだと感じました。
- 座席の予約切り替え
座席のエリアをタップすることで、AvailableとReservedの切り替えをすることができます。
- プロジェクターの予約切り替え
プロジェクター横のレバーをタップして切り替えることで、プロジェクターの使用可否を切り替えることができます。
- 危険エリアの表示
Dangerと表示されているエリアは侵入禁止エリアを想定しています。
- 目的地(プリンタ)へのナビゲーション
Find Printerボタンを押すと、プリンタの位置を矢印が差してくれます。
Area Targetsによって、カメラがどちらの方向を向いているのかがわかるために実現できています。
ルートを指し示す場合は、通ることのできる道の定義、ルート探索のアルゴリズムを実装することで実現できます。
オクルージョン
オクルージョンとは、現実世界の物体の奥に3Dオブジェクトが存在する場合に、重なっている3Dオブジェクトを表示しない機能です。
こちらの画像のように、壁の奥のDangerの部分は隠れる機能です。
Area TargetsのSimulate Occlusionをオンにすると、オクルージョンされるようになります。
この場合のオクルージョンは、スキャン時に存在していた物体に対してのみ適用されます。
そのため、スキャン時に存在していなかった物体が存在していたり、スキャン時にあったものがなくなってしまった場合、少し違和感のあるオクルージョンが発生します。
スキャン時に存在していなかった物体が存在する場合は、その物体に対してのオクルージョンが行われません。
また、スキャン時にあったものがなくなってしまった場合は、スキャン時の位置に存在していた物体通りにオクルージョンが行われます。
なので、基本的には机など動く可能性のあるものはスキャンに含めない方がよいかと思います。
Image Targetとの併用
Vuforia Area TargetsとImage Targetsの併用 pic.twitter.com/ut2PWUj5ga
— こーや (@koyataroo) July 30, 2020このように、Area Targetsを利用しているときに、別のTargetも使用することができます。
空間に配置するものはArea Targetsで、何か物体やマーカーに動的に追随するものはImage TargetsやModel Targetsを使うといったユースケースが考えられます。
まとめ
Vuforia EngineのArea Targetsは、精度よくオブジェクトを配置することができました。
Area Targetsは、オフィスや、工場、美術館といった場所で、ゲームやナビゲーションを表示するのにとても有用であると感じました。
現状では、Matterport Pro2と月額69ドルが必要であるため試すのにもハードルがありますが、試す価値は十分にあると思います!
開発者向けのVuforia Slackコミュニティがあり、僕も参加しているので、記事の内容で質問があればそちらから質問下さい!
https://www.it-ex.com/promo/vuforia/to-developers/
また今回のコードはGithubに載せているので是非クローンしてお手元で試してみてください!