- 投稿日:2021-01-13T21:26:09+09:00
[WPF] ウインドウメッセージハンドラをフックする
やりたいこと
Windowsのメッセージを受け取って、その中に含まれているデータを取り出したいということがあったので、まずメッセージを受け取れるようにしたい。
イメージとしては、別の記事に書いた「C++のダイアログベースのひな型」の中に出てきたメッセージループ(ダイアログプロシージャ)を、C#からでも使えるようにする感じ。
ちょっと調べると、Windowsのメッセージのハンドラ(メッセージループ)をフックする、というやり方で実現できるようなので一度やってみる。
やったこと
ざっくり手順は下記。
- フック時に行う処理を書いたメソッドを作成する
- 使うメッセージの番号を定数定義する
- 作成したメソッドを
HwndSource
を使い、フック時に実行するメソッドとして登録するサンプルプログラム
ここでは、例として
WM_PAINT
を取った。
WM_PAINT
は0x000F
なので、その値を最初に定義している。using System; using System.Diagnostics; using System.Windows; using System.Windows.Interop; namespace WpfApp61 { public partial class MainWindow : Window { // メッセージの定数を定義 private const int WM_PAINT = 0x000F; public MainWindow() { InitializeComponent(); // メッセージループをフックするメソッドを登録 //var hWnd = new WindowInteropHelper(Application.Current.MainWindow).EnsureHandle(); var hWnd = new WindowInteropHelper(this).EnsureHandle(); HwndSource source = HwndSource.FromHwnd(hWnd); source.AddHook(new HwndSourceHook(WndProc)); } // メッセージループを記述したメソッド private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { if (msg == WM_PAINT) { Debug.WriteLine("WM_PAINT"); // wparamやlparamでなにかを判定or取得する場合は下記のようにする if (wParam.ToInt32() == 0) { // なにかする(※WM_PAINTはwParamもlParamもなにも入ってない) } } return IntPtr.Zero; } } }これを実行すると、WM_PAINTが来たとき=画面に描画が必要になったとき(=ウインドウを画面の端っこにもっていって画面外に出た部分がまた画面内に戻ってきたとき等)に、WM_PAINTが来ているのを見れる。
参考
ダイアログベースのひな型
https://qiita.com/tera1707/items/003961a385e6bbf8a160HwndSource クラス
https://docs.microsoft.com/ja-jp/dotnet/api/system.windows.interop.hwndsource?view=net-5.0WPFアプリケーションでウィンドウプロシージャをフックする
https://qiita.com/tricogimmick/items/86141bc33c0e06e9d2e9
- 投稿日:2021-01-13T21:12:11+09:00
"Hello World"でみるプログラミング言語
はじめに
プログラミングを学ぶものなら誰しも通る道、Hello world。今回はそんなHello worldを出力するためのプログラムで、5つのプログラミング言語を比較していきます。皆さんが使っていなそうな言語を選びました。が、あくまでも独断と偏見によって決定されています。
1.C
いわずと知れた言語ですね。
#include <stdio.h> int main(void) { printf("Hello world"); }2.Python
簡潔に書けることが利点ですね。
print("Hello world")3.Ruby
日本生まれの言語ですね。
puts "Hello world"4.FORTRAN
最初の高水準言語として有名ですね。まだ息してたのか...
program hello print *, 'Hello world' end program hello5. PHP
HTML/CSSと一緒に使われたりしますね。
<?php echo 'Hello world'; ?>終わりに
以上です。いかがでしたでしょうか?
Ruby
はやっぱり短かったですね。それが長所ですよね。PHP
のechoてエコーって読むんですね。初めて知りました。若輩者ゆえ、もしかしたら間違いがあるかもしれないので、その時はコメントなんかで教えてください。
- 投稿日:2021-01-13T15:05:24+09:00
ReactiveProperty<>からの一つの判定をかませてReactiveCommandを作る覚え書き
Listとかで選択されるとCommandが有効になるとか
今更とか言わないで。
例えば、
<DataGrid SelectedItem="{Binding SelectedData.Value, Mode=TwoWay}" /> <Button Command="{Binding ItemSelectedCommand}" Content="Execute" />みたいな。
すると、public ReactiveProperty<ItemData> SelectedData { get; } = new ReactiveProperty<ItemData>(); public ReactiveCommand ItemSelectedCommand { get; private set; }として、
ItemSelectedCommand = SelectedData .Any(data => data != null) .ToReactiveCommand();とすればよいようだ。
なるほど。
- 投稿日:2021-01-13T13:58:29+09:00
C#でまともなアプリ開発を行うために
はじめに
C#は「Windowsでしか開発出来ない言語」などクローズドなイメージから一転し、.NET Coreの誕生によって様々なことが出来るようになりました。最近ではokazukiさんのC# で出来ること一覧が大きな反響を呼び、C#であらゆるアプリが作れることを多くの方に知ってもらえたかと思います。C#のよさを知ってもらうことは嬉しい限りではありますが、他言語をやってきた身としてはC#を取り巻くエコシステムの情報が少ないと感じることが多く、新たに参入される方にとって抵抗があるのも事実だと思います。この記事ではまともなアプリ開発を行うために必要なエコシステムなどご紹介し、C#でも快適に開発出来ることを知ってもらえれば嬉しいです。筆者はWEB開発を主としているので、WEB寄りな話が多くなる可能性がありますがご了承下さい。
「まとも」とは?
タイトルにも書いたまともなアプリ開発とは、一定の品質を担保する仕組みで開発出来ることです。具体的には
- 開発環境が特定のOSに依存しないこと
- 環境構築が容易であること
- 一定の品質を満たすことをCIで確認出来ること
- コードフォーマット
- 静的解析
- テスト(カバレッジが正しく検知出来ること)
です。また個人的には「OSS(無料)であること」を入れたいなと思っています。やはり、有料なものに比べ、情報量の多さとブラックボックスに包まれていない(議論されている)状態であることは、開発者にとって有益であると思うからです。
前提
C#の開発では基本的にVisual Studioで完結出来ることが多いですが、まともな開発をする上で特定のOSに依存しないことは絶対条件なので、この記事はLinux、Macで使うことを前提にご紹介します。もちろん、Windowsでも利用出来るものを選定しています。また、各ライブラリは詳しくは説明しません。
開発環境が特定のOSに依存しないこと
一般的にはVisual Studio、Visual Studio Codeが多いですが、特定のエディタ、IDEには依存しないので好きなエディタで開発することが出来ます。ですが、流石にvim、Emacsを使うような玄人は見たことがありませんが、開発は出来ます。
特定のOS関連ではありませんが、インストーラーを用いて.NET Coreをインストールする場合にOSのロケール値がセットされてしまうので、
en-us
に設定することをおすすめします。C#で開発するにあたって、情報の少なさを痛感することが多いので、より検索結果がヒットする英語に設定しておくことは重要だと思います。なので英語を読めなければいけないということではなく、スムーズに開発するための手段だと思ってもらえると嬉しいです。# fishの例 set -x DOTNET_CLI_UI_LANGUAGE en-us環境構築が容易であること
環境構築を容易にするために、まずはコンテナ化が挙げられると思います。MSの公式にコンテナ化のチュートリアルが載っているので、比較的簡単にコンテナ化することが出来ます。
一定の品質を満たすことをCIで確認出来ること
一定の品質を保つためには、第三者に自分のコードを見てもらうこと、つまりコードレビューが必須です。しかし、人間は愚かなので必ずミスが発生します。そのため、最低限は機械的にチェックする仕組みを導入し、人間の考えることを極力減らすことが重要だと思います。CI上でチェックするためにCLIでフォーマット、静的解析、テストを利用出来ることは絶対条件です。.NET Core 2.1から導入された.NETツールによって、今までCLIで操作出来なかったものが使えるようになりましたので、後述するツールは全てCLI上で操作出来るものを紹介します。CIに関してはGithub Actions、CircleCI、CodeBuildなどお好きなCIを選定していただけばと思います。筆者は自分のプロダクトにはGithub Actionsを利用していますので、ご参考になれば幸いです。
GitHub Actionsで.NET CoreアプリをAzureにデプロイする
コードフォーマット
dotnet-format
公式の.NET Platformから出ているフォーマットツールです。.NETツールで利用出来ます。デフォルトでも利用出来ますが.editorconfigを設定することも出来ます。
グローバルインストール
プロジェクト毎に適用する場合は-g
は不要です。$ dotnet tool install -g dotnet-format実行
$ dotnet-format <project>
静的解析
Roslynator
Roslyn1を利用した静的解析ツールです。StyleCop、FxCopとC#における静的解析ツールはいくつかありますが、無償2でCLIで提供されている唯一のツールです。さらにVSCodeの拡張もあります。まだまだ開発途上という感じではありますが、活発に開発されているので伸びしろもあると思います。
インストール
$ dotnet add package Roslynator.Analyzers $ dotnet add package Roslynator.CodeAnalysis.Analyzers $ dotnet add package Roslynator.Formatting.Analyzers # .NETツール $ dotnet tool install Roslynator.DotNet.Cli.NETツールに関して、ローカルインストール(
-g
オプションを付けない)でないとCIで利用出来ないので、導入はこちらを参考にして下さい。診断
ビルド後に診断しないと正しい結果が出ないのでご注意下さい。# 診断 $ dotnet roslynator analyze # 修正 $ dotnet roslynator fixStyleCopAnalyzers
StyleCopの後身プロジェクトで、今なお活発に開発されているプロジェクトです。しかし、CLIがなくIssueにも挙がっていたのですが、開発されることはなさそうな雰囲気です。
rookxさんが作成したStyleCopAnalyzers.CLIはStyleCopAnalyzersをCLIとして利用することを想定されているので、現在StyleCopAnalyzersを使われている方は使ってみるのはいかがでしょうか?
テスト
xUnit
nUnit、mstestなど様々なテストフレームワークが存在しますが、現在においてxUnitが一番メジャーかなと思います。
実行
カバレッジを収集し、閾値を100%にするdotnet test /p:CollectCoverage=true /p:Threshold=100カバレッジが通っているか確認したい場合はレポートに出力することが出来ます。
- reportgeneratorインストール
ReportGeneratorというツールを用いて、カバレッジを可視化出来るようにします。
dotnet tool install -g dotnet-reportgenerator-globaltool
- カバレッジライブラリをインストール
$ dotnet add package coverlet.collector $ dotnet add package coverlet.msbuild
- テスト実行&レポート化
$ dotnet test /p:CollectCoverage=true /p:CoverletOutput=result/ /p:CoverletOutputFormat=lcov /p:Threshold=100 $ reportgenerator -reports:./result/coverage.info -targetdir:./result/ start ./result/index.htmこちら簡単にですが、プロジェクトを作成しましたので興味ある方はご覧いただければと思います。
https://github.com/ikuosaito1989/.netcore-unittest
まとめ
最近になり、やっと自分の中で納得する一定の品質を担保する仕組みを構築することが出来ました。おそらく遅れをとっていた理由として、OSSになったのが最近であること、C#だけが前に進み、周辺ツールが先を行くC#に追いつけない状況が続いたことが原因だと思います。C#で作れないものはないと断言出来るほど、幅広い範囲を網羅してるので周辺ツールには目を瞑ってましたが、OSSで議論されながらより洗練されたライブラリが普及し、ディファクトスタンダードとなれば今まで以上にC#という選択肢が増えると思っています。なによりも、静的型付け言語なのに、品質を保つ仕組みを構築する情報が少なさが改善されたことが個人的には嬉しく思います。
陳謝
ここまで読んで頂きありがとうございます。上級者のような口ぶりで紹介しましたが、筆者は個人サービスでC#を利用しているだけの趣味プログラマーです。.NETにまつわる情報をキャッチしているつもりですが、わからない部分や間違っていることもあると思いますので、お気軽にご指摘いただけると嬉しいです。
- 投稿日:2021-01-13T10:44:19+09:00
WPF で Prism を使い UserControl を動的に生成して任意の座標に配置したい
今日は以下の内容をネタに記事を書いてみようと思います。
記事のネタに丁度いいかなって思ってブックマークしてたのですが、あれからも1月以上がたってしまいました…#WPF でPrismを使い #UserControl を動的に作成して任意の座標に配置するとかドンピシャで参考になる事例が出てこない。
— なべひろ (@HRK_66622) December 3, 2020
あっても断片的なコードで参考にならん。
WinForms?今時?wって小馬鹿にした言葉を良く見かけるが、WinFormsの方が情報が豊富でWPFより精神的に楽だわ。WPF 的な考え方
座標とか表示するデータを管理するクラスを定義して
ObservableCollection<T>
に突っ込みましょう。
突っ込んだら後は Canvas を ItemsPanel に設定した ItemsControl に表示すれば OK です。今回はサンプルなので表示用データは以下のようなシンプルなレコードにしました。
Item.csnamespace PureWpf { // これを表示していく public record Item(int X, int Y, string Content); }ViewModel も作ります。INotifyPropertyChanged や ICommand の実装を自分でやるのはめんどくさいので Prism.Core パッケージを参照して BindableBase と DelegateCommand は拝借しました。
AddCommand が実行されたらランダムな座標をもった Item クラスを作って追加しています。
MainWindowViewModel.csusing Prism.Commands; using Prism.Mvvm; using System; using System.Collections.ObjectModel; namespace PureWpf { public class MainWindowViewModel : BindableBase { public ObservableCollection<Item> Items { get; } = new(); private DelegateCommand _addCommand; public DelegateCommand AddCommand => _addCommand ??= new(AddExecute); // 表示位置をランダムにするための Random クラス private Random Random { get; } = new(); private void AddExecute() => // ランダムな位置に、とりあえず現在時間の文字列を出すようなデータを作る Items.Add(new(Random.Next(500), Random.Next(500), DateTime.Now.ToString())); } }ここまで出来たら、あとはそれをどのように表示するのかというのは XAML の仕事です。コレクションを表示するのは ItemsControl (およびその派生クラス) でやることが多いです。
要素の選択が必要とかツリー状に表示したいとか仮想化したいとか用途に応じて選んでいきます。今回は表示出来ればいいだけなので一番シンプルな ItemsControl にします。ItemsControl 系のコントロールでは、要素をどのように並べるかという Panel を差し替え可能です。今回は指定した座標に題したので Panel を Canvas にしています。Canvas は、まさに指定した座標に要素を配置するためのコントロールです。
MainWindow.xaml<Window x:Class="PureWpf.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:local="clr-namespace:PureWpf" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" Title="MainWindow" Width="800" Height="450" mc:Ignorable="d"> <Window.DataContext> <local:MainWindowViewModel /> </Window.DataContext> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition /> </Grid.RowDefinitions> <Button Command="{Binding AddCommand}" Content="Add" /> <ItemsControl Grid.Row="1" ItemsSource="{Binding Items}"> <ItemsControl.ItemsPanel> <!-- 要素の並びは Canvas で好きな座標に出せるようにする --> <ItemsPanelTemplate> <Canvas /> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> <ItemsControl.ItemTemplate> <DataTemplate DataType="local:Item"> <!-- ここに表示したい UserControl を設定する。今回は別途作るのがめんどいので WPF のコントロールを直接並べてます --> <Border Width="100" Height="100" Background="Red"> <TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" Text="{Binding Content}" /> </Border> </DataTemplate> </ItemsControl.ItemTemplate> <ItemsControl.ItemContainerStyle> <!-- Canvas 上での表示位置の設定は Canvas に直接乗るコンテナに指定 --> <Style TargetType="ContentPresenter"> <Setter Property="Canvas.Top" Value="{Binding Y}" /> <Setter Property="Canvas.Left" Value="{Binding X}" /> </Style> </ItemsControl.ItemContainerStyle> </ItemsControl> </Grid> </Window>実行してボタンをぽちぽち押すと、こんな感じで現在時刻がランダムな位置に表示されます。
Prism 的な考え
Prism でも基本的に同じです。ですが、例えば Prism の Region 内の指定した座標に View を表示したいという要望であれば先ほど紹介した内容と Prism を組み合わせてやる感じになります。
Prism の Region に ItemsControl が指定できるので ItemsPanel を Canvas にしておきます。
MainWindow.xaml<Window x:Class="PrismApp.Views.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:prism="http://prismlibrary.com/" Title="{Binding Title}" Width="525" Height="350" prism:ViewModelLocator.AutoWireViewModel="True"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="Auto" /> <RowDefinition /> </Grid.RowDefinitions> <Button Command="{Binding AddCommand}" Content="Add" /> <!-- レイアウト用の Panel を Canvas にした ItemsControl を Region にする --> <ItemsControl Grid.Row="1" prism:RegionManager.RegionName="ContentRegion"> <ItemsControl.ItemsPanel> <ItemsPanelTemplate> <Canvas /> </ItemsPanelTemplate> </ItemsControl.ItemsPanel> </ItemsControl> </Grid> </Window>ViewModel 側では ContentRegion に対して ViewA を表示するように要求する AddCommand を追加しておきます。表示位置とか表示内容はナビゲーションのパラメーターで指定するようにしました。
MainWindowViewModel.csprivate DelegateCommand _addCommand; public DelegateCommand AddCommand => _addCommand ?? (_addCommand = new DelegateCommand(ExecuteAddCommand)); private Random Random { get; } = new Random(); private void ExecuteAddCommand() { _regionManager.RequestNavigate("ContentRegion", "ViewA", new NavigationParameters { { "x", Random.Next(500) }, { "y", Random.Next(500) }, { "message", DateTime.Now.ToString() }, }); }ViewA の ViewModel はこんな感じです。
ViewAViewModel.csusing Prism.Mvvm; using Prism.Regions; using System.Diagnostics; namespace PrismApp.Main.ViewModels { public class ViewAViewModel : BindableBase, INavigationAware { private string _message; public string Message { get { return _message; } set { SetProperty(ref _message, value); } } private int _x; public int X { get { return _x; } set { SetProperty(ref _x, value); } } private int _y; public int Y { get { return _y; } set { SetProperty(ref _y, value); } } public ViewAViewModel() { } public void OnNavigatedTo(NavigationContext navigationContext) { // パラメーターから表示位置や表示する内容を取得してプロパティに保持 if (navigationContext.Parameters.TryGetValue<int>("x", out var x)) { X = x; } if (navigationContext.Parameters.TryGetValue<int>("y", out var y)) { Y = y; } if (navigationContext.Parameters.TryGetValue<string>("message", out var message)) { Message = message; } } // ここで true を返すとナビゲーション時に View が再利用されるので断固拒否 public bool IsNavigationTarget(NavigationContext navigationContext) => false; public void OnNavigatedFrom(NavigationContext navigationContext) { } } }普通の WPF とちょっと違う点としては、Canvas の位置指定を行うための Canvas.Top, Canvas.Left 添付プロパティを設定する場所です。Prism では Region に指定している ItemsControl の Items に直接 View のインスタンスを追加するので、ItemsContainer でラップされないため View で直接添付プロパティを設定します。
ということで ViewA.xaml は以下のようになります。
ViewA.xaml<UserControl x:Class="PrismApp.Main.Views.ViewA" 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:local="clr-namespace:PrismApp.Main.Views" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:prism="http://prismlibrary.com/" Canvas.Left="{Binding X}" Canvas.Top="{Binding Y}" Width="100" Height="100" d:DesignHeight="300" d:DesignWidth="300" prism:ViewModelLocator.AutoWireViewModel="True" Background="LightBlue" mc:Ignorable="d"> <Grid> <TextBlock HorizontalAlignment="Center" VerticalAlignment="Center" Text="{Binding Message}" /> </Grid> </UserControl>実行するとこんな感じになります。
まとめ
ということで、WPF の基本である(と個人的に思ってる)データの管理は C# でやって、それをどのように表示するかは XAML でどうにでもなるという原則がよく出ている例になるかなと思いました。
ソースコードは以下のリポジトリに上げています。PureWpf プロジェクトが Prism を使ってないもので PrismApp が Prism を使っているものになります。
https://github.com/runceel/WPFPrismUserControlSample
それでは、良い WPF & Prism ライフを!
追記
この記事のポイントは ItemsControl の理解と、Prism の Region への応用という感じです。WPF で ItemsControl や ListBox などを使って柔軟にデータを表示できる例としてインパクトが大きいもの例に ListBox で都道府県選択 UI を作って見た目を日本地図を表示した Yamaki さんの記事があるので紹介しておきます。(13 年前の記事になるのか…)
- 投稿日:2021-01-13T08:24:30+09:00
C#で書くものはRubyでこう書く
注意
個人的によく使う書き方のみ記述します。
まだ未完成です。暇だった時に更新します。標準ストリーム
標準出力
C#Console.Write("改行なし"); Console.WriteLine("改行あり");Rubyprint "改行なし" puts "改行あり"標準入力
C#// 値がひとつだけの場合 var single = Console.ReadLine(); // 複数ある場合 var multiple = Console.ReadLine().Split(",");Ruby# 値がひとつだけの場合 single = gets # 複数ある場合 multiple = gets.split(",")空白で区切られてるだけの場合、こういう書き方もできるらしい。
multiple = gets.split
Rubyって凄いね。ループ処理
for文
C#for (var i = 0; i < 10; i++) { Console.WriteLine(i); }Rubyfor i in 0..9 puts i endforeach文
C#var array = new int[5] {1, 2, 3, 4, 5}; foreach (var i in array) { Console.WriteLine(i); }Rubyarray = (1..5) array.each do |i| puts i endWhile文
C#var num = 0; while (num < 5) { Console.WriteLine(num); num++; }Rubynum = 0 while num < 5 puts num num += 1 endRubyにはインクリメントとデクリメントがないみたい。
その代わり?1を追加するのとは別で、succとnextてのがあるらしい。succ
Rubynum = 1 apt = "a" letter = "z" puts num.succ # 出力: 2 puts apt.succ # 出力: b puts letter.succ # 出力: aaこれはこれで便利。zの次がaaってのも面白い(何処で使うかわからないけど……)
ただ、デクリメントに対応するものはないっぽい。
近いものがあるみたい……pred
Rubynum = 1 puts num.pred # 出力: 0ただ、アルファベットでやるとエラーが出た。
まぁそんな問題はないはず!条件分岐
if文
C#var name = "たけし"; if (name == "たけし") { Console.WriteLine("ガタガタガタガタガタガタガタ"); } else if (name == "たくろう") { Console.WriteLine("残念なイケメン"); } else { Console.WriteLine("みか"); }Rubyname = "たけし" if name == "たけし" puts "ガタガタガタガタガタガタガタ" elsif name == "たくろう" puts "残念なイケメン" else puts "みか" end個人的にRubyはちょっと見にくいかも……
Switch文
C#var energyDrink = "Monster"; switch (energyDrink) { case "Monster": Console.WriteLine("いっぱい味がある"); break; case "Zone": Console.WriteLine("安くてデカい"); break; default: Console.WriteLine("レッドブルが値下げするらしい"); break; }RubyenergyDrink = "Monster" case energyDrink when "Monster" puts "いっぱい味がある" when "Zone" puts "安くてデカい" else puts "レッドブルが値下げするらしい" endいちいちbreak書くのめんどくさかったからありがたい。
三項演算子
C#var num = 10; var results = (num == 10) ? "true" : "false";Rubynum = 10; results = (num == 10) ? "true" : "false";こいつは流石に変わらなかった。
- 投稿日:2021-01-13T03:40:37+09:00
Poisson 過程のシミュレーション
Poisson 過程で遊んでみた。
Poisson 過程
Poisson 過程では強度関数 $\lambda (t)$ により特徴づけられる。強度関数は与えられた区間内の $\forall t \in [0, T]$ に対して $\lambda (t) \geq 0$ を満たす。
強度関数 $\lambda (t)$ の Poisson 過程では、ある微小区間 $[t, t+\delta]$ 内でイベントが起きた回数を $N(t, t+\delta)$ とし、イベントが起きる確率を以下で表す。\begin{align} P\left[N(t, t+\delta) = 1\right] &= \lambda (t)\delta + o(\delta)\\ P\left[N(t, t+\delta) \geq 2\right] &= o(\delta) \end{align}これより、 $\delta$ が微小だとして1次のみを考えれば
\begin{align} P\left[N(t, t+\delta) = 0\right] &= 1 - \lambda (t)\delta \approx \exp \left(-\lambda(t)\delta\right) \end{align}となる。強度関数が定数の場合を定常 Poisson 過程と呼び、そうでない場合を非定常 Poisson 過程と呼ぶ。上記の表式を使って①イベント数 $N$ とイベントの発生時刻の分布②イベント数の分布③イベント間の時間間隔の分布の3つの表式を得よう。
イベント数とイベントの発生時刻の分布
時間区間 $[0, T]$ 内で $N$ 個のイベントがそれぞれ時刻 $t_1< t_2<...<t_N$ に発生した場合を考えれば、その確率密度関数は以下で得られる。
\begin{align} P(N, t_1, ..., t_N) &= \Pi_{i=1}^N P\left[N(t_i, t_i+\delta) = 1\right] \Pi_{t \neq t_1, ..., t_N} P\left[N(t, t+\delta) = 0\right]\\ &\approx \Pi_{i=1}^N \lambda(t_i)\delta \Pi_{t \neq t_1, ..., t_N} \exp\left[-\lambda(t)\delta\right]\\ &\propto \Pi_{i=1}^N \lambda(t_i)\delta \exp\left(-\int_0^T \lambda(t)dt\right)\\ \end{align}イベント数の分布
①の分布で時間の引数全てを積分で落としてやれば良い。以下のように強度が $\int_0^T \lambda(t)dt$ の Poisson 分布が得られる。
\begin{align} P(N) &= \int_0^T dt_1 ... \int_0^{t_{N-1}}dt_N P(N, t_1, ..., t_N)\\ &= \int_0^T dt_1 ... \int_0^{t_{N-1}}dt_N \Pi_{i=1}^N \lambda(t_i)\delta \exp\left(-\int_0^T \lambda(t)dt\right)\\ &= \frac{1}{N!}\int_0^T dt_1 ... \int_0^Tdt_N \Pi_{i=1}^N \lambda(t_i)\delta \exp\left(-\int_0^T \lambda(t)dt\right)\\ &= \frac{\left(\int_0^T \lambda(t)dt\right)^N}{N!}\exp\left(-\int_0^T \lambda(t)dt\right) \end{align}イベント間の時間間隔の分布
イベントが $t_i$ に起きたとし、次に起きる時間 $t_{i+1}$ との差 $\tau_i = t_{i+1} - t_i$ が従う分布を考える。 $t_i \leq \tau$ となる分布 $P(\tau\geq\tau_i)$ は、
\begin{align} P(\tau\leq\tau_i) &= 1 - P(\tau > \tau_i)\\ &= 1 - P((\tau_i,\tau_{i+1}) で N = 0)\\ &= 1 - \exp\left(-\int_{t_i}^{t_{i+1}} \lambda(t) dt\right) \end{align}これを $\tau$ で微分すれば時間間隔の確率密度関数が得られ、 $dP(\tau\leq\tau_i)/dt = \lambda(t_i + \tau_i)\exp\left(-\int_{t_i}^{t_{i+1}} \lambda(t) dt\right)$ となる。これは定常 Poisson 過程の場合は指数分布になる。
間引き法
間引き法は非定常 Poisson 分布からイベント発生時刻のサンプルを得る方法のひとつ。詳しくは参考文献を参照。
区間は $[0, T]$ とする。間引き法は、棄却法の点過程バージョンのようなイメージを持つと良いかもしれない?間引き法はサンプリングをしたい非定常 Poisson 過程に対し、別の非定常 Poisson 過程でサンプリングしたものをある確率で受容すると、その受容されたサンプルはサンプリングしたい非定常 Poisson 過程から得られたサンプルだと保証することができる。もちろん実用上では、上に書いた”別の非定常 Poisson 過程” は定常 Poisson 分布を使用しても良い。ここではサンプリングしたい非定常 Poisson 過程の強度関数を $\lambda (t)$、棄却法における提案分布に対応する別の非定常 Poisson 分布の 強度関数を $\lambda^* (t)$ で書き分ける。
証明のスケッチ(スケッチという名の逃げ)
両強度関数が満たなさくてはならない条件は、対象とする区間の任意の点 $t$ に於いて $\lambda^* (t) \geq \lambda(t)$ である。
強度関数 $\lambda^* (t)$ に従う非定常 Poisson 分布から得られたサンプル $N$ 個 $t_1, ..., t_N$ に対し、強度関数 $\lambda (t)$ に従う非定常 Poisson 分布をそれぞれ $\lambda(t) / \lambda^*(t)$ の確率で受容する。$N$ 個のサンプルのうち $n$ 個が受容されたとし、受容されなかったものは積分して落とすことで、 $n$ 個が受容された場合の確率分布は以下のように得られる。\begin{align} p_N(n, t_1, ..., t_n) &= \int_0^T \frac{dt_{n+1} ... dt_N}{(N-n)!} \Pi_{i=1}^N \lambda^* (t_i)\exp\left(-\int_0^T \lambda^* (s) ds\right) \Pi_{j=1}^n \frac{\lambda(t_j)}{\lambda^* (t_j)} \Pi_{k=n+1}^N \left(1 - \frac{\lambda(t_k)}{\lambda^*(t_k)}\right)\\ &= \int_0^T \frac{dt_{n+1} ... dt_N}{(N-n)!} \exp\left(-\int_0^T \lambda^* (s) ds\right) \Pi_{j=1}^n \lambda(t_j) \Pi_{k=n+1}^N \left(\lambda^*(t_k) - \lambda(t_k)\right)\\ &= \Pi_{j=1}^n \lambda(t_j)\exp\left(-\int_0^T \lambda^* (s) ds\right) \frac{\left(\int_0^T \left[\lambda^*(t_k) - \lambda(t_k)\right] dt\right)^{N-n}}{(N-n)!}\\ \end{align}これを $N$ を $n$ から $\infty$ まで足し上げたものは以下の通り強度関数 $\lambda(t)$ の非定常 Poisson 分布のイベント発生時刻の分布と同一になる。
\begin{align} p(n, t_1, ..., t_n) &= \sum_{N=n}^\infty p_N(n, t_1, ..., t_n)\\ &= \Pi_{j=1}^n \lambda(t_j)\exp\left(-\int_0^T \lambda^* (s) ds\right) \sum_{N=n}^\infty \frac{\left(\int_0^T \left[\lambda^*(t_k) - \lambda(t_k)\right] dt\right)^{N-n}}{(N-n)!}\\ &= \Pi_{j=1}^n \lambda(t_j)\exp\left(-\int_0^T \lambda(t_k) dt\right)\\ \end{align}アルゴリズム
定常 Poisson 過程のイベント間隔は指数分布に従うため、簡単にサンプルを得ることができる。そのため定常 Poisson 過程を提案分布に採用する。この場合強度関数をどうするかでいろいろと手法があるようだが、ここでは $\lambda^* = \max_{t\in[0, T]}$ \lambda(t)$ とする。
- 強度 $\lambda^*$ の定常 Poisson 分布からイベント発生時刻のサンプル ${t_i}$ を作成する。
- サンプル ${t_i}$ を $\lambda(t_i)/ \lambda^*$ の確率で受容する。
上の方法では一気にサンプルを取得し、一気に受容するかを選択しているが、ひとつひとつ行っても良い。
シミュレーション
行ったこと
以下の場合の非定常 Poisson 過程からのサンプリングのシミュレーションを行った。
\begin{align} T &= 1\\ \lambda(t) &= 24 \left(t-\frac{1}{2}\right)^2 \end{align}$\int_0^1 \lambda(t) = 2$ のため、イベント発生数は平均2の Poisson 分布に従う。10000回のサンプリングをした結果、イベント間隔の平均が1.99、標準偏差が1.41 が得られたためおそらく正しくサンプリングされているだろう(ほんとうは分布の同一性の検定をしたいが、まだ実装していないので行えない。。。)
コード
非定常 Poisson 過程のサンプリング部分
private IEnumerable<double> GetEventSample(NonStationaryPoissonProcessConfig config) { var maxIntensity = config.Intensity(FindIntensityMaximumTime(config)); var uniform = new Probability.Distribution.Uniform(); var uniformParam = new Probability.Parameter.Uniform(0, 1); var stPoissonProc = new StochasticProcess.PointProcess.StationaryPoissonProcess(); var sample = stPoissonProc.GetEventSamples(new StationaryPoissonProcessConfig(maxIntensity, config.Start, config.End), 1).First(); return sample.Where(t => config.Intensity(t) / maxIntensity >= uniform.GetSamples(uniformParam, 1).First()); }定常 Poisson 過程のサンプリング部分
private IEnumerable<double> GetEventSample(StationaryPoissonProcessConfig config) { var t = config.Start; var exp = new Probability.Distribution.Exponential(); var expParam = new Probability.Parameter.Exponential(1.0 / config.Intensity); while (t <= config.End) { t = t + exp.GetSamples(expParam, 1).First(); if(t <= config.End) yield return t; } }実際に数値計算をした部分
var size = 10000; var npp = new StochasticProcess.PointProcess.NonStationaryPoissonProcess(); var nppStart = 0.0; var nppEnd = 1.0; Func<double, double> nppIntensity = (double t) => 3.0 * Math.Pow(t - (nppEnd + nppStart) / 2, 2) / Math.Pow((nppEnd + nppStart) / 2, 3); var nppConfig = new StochasticProcess.PointProcessConfig.NonStationaryPoissonProcessConfig(nppIntensity, nppStart, nppEnd); var nppTimeSamples = npp.GetEventSamples(nppConfig, size).Select(t => t.ToList()).ToList(); var nppTimeSamplesCount = nppTimeSamples.Select(t => t.ToList().Count()).ToList(); Console.WriteLine(nppTimeSamplesCount.Average()); Console.WriteLine(nppTimeSamplesCount.StandardDeviation());参考文献
- 投稿日:2021-01-13T03:03:51+09:00
Unity 2020.1から、自動実装プロパティのバッキングフィールドにSerializeField属性をつけた時のEditor上の表示が良くなったが、シリアライズ名的に使ってはいけない
前提
- Unityのフィールドに付与するSerializeField属性がわかる
- C#のプロパティ、自動実装プロパティ、バッキングフィールドがわかる
- C# 7.3から、自動実装プロパティに属性をつけられるようになったことを知っている
以上の内容は、自分の『C# 7.3から自動実装プロパティのバッキングフィールドに属性をつけられるようになった。UnityでSerializeFieldとそれをいい感じに使いたかった』で説明してます。
本題
次のようなコードを、自動実装プロパティのバッキングフィールドにSerializeField属性をつけて、シリアライズを試みます。
using UnityEngine; public class Player : MonoBehaviour { [field: SerializeField] public int Level { get; private set; } }Unity 2019.4までで、自動実装プロパティのバッキングフィールドにSerializeField属性をつけた時のEditor上の表示はこんな感じ。
そして.unityファイル中(YAMLファイル)などでのシリアライズはこんな感じ。
「
<Level>k__BackingField
」!これは自動実装プロパティのバッキングフィールドの名前です。ILとしては有効でも、C#のフィールド名としては不正な名前です。
この名前でEditor上で表示され、そしてシリアライズされます。
ここまではUnity 2019.4までの話。
ここからはUnity 2020.1以降の話。
Unity 2020.1以降では、先のコンポーネントのUnity Editor上の表示はこんな感じになります。
なんと表示が変わっています。「Level」という表示になりました。断定はできませんが内部実装で
UnityEditor.ObjectNames.NicifyVariableName
を使っている可能性があります。// Levelと表示された Debug.Log(ObjectNames.NicifyVariableName("<Level>k__BackingField"));さぁ、ではUnity 2020.1では、どんな名前でシリアライズされるのでしょうか?
残念ながら以前と変わらず、ILとしては有効でも、C#のフィールド名としては不正な「
<Level>k__BackingField
」っていう名前でシリアライズされるようです。
自動実装プロパティのバッキングフィールドに対してSerializeFieldをつけた時の、Unity 2019.4 => Unity 2020.1の仕様変更は・・・
- Unity Editor上の表示は変更(プロパティ名になり、簡潔になった。)
- .unityや.prefabなどでシリアライズされている名前は変わらず
となります。
さて、タイトルが自分の主張なのですが、「Unity 2020.1から、自動実装プロパティのバッキングフィールドに、SerializeField属性をつけた時のEditor上の表示は良くなったが、シリアライズ名的に使ってはいけない」です。
ぱっと見のUnity Editor上の表示は良い感じに見えます。しかし、シリアライズは「
<Level>k__BackingField
」という名称で行われます。そのため、自動実装プロパティのバッキングフィールドに、SerializeField属性を単純につけることは、避けることを強くおすすめします。自分は「シリアライズされる名前」・「シリアライズされたり、永続化される際のデータフォーマット」というのは非常に重要と考えています。コードを変更するコストに比べて、シリアライズ・永続化されたデータのフォーマットを変更するというのは、影響範囲が広く、過去・未来のことを考慮する必要があり、非常に面倒だからです。
<Level>k__BackingField
という自動実装プロパティのバッキングフィールドの名前は
- 冗長
- 扱いにくい
- C#のフィールド名として不正
- C#の自動実装プロパティのバッキングフィールドの命名が仕様化されていない
という欠点があります。そのため、自分は「シリアライズされる名前」・「シリアライズされたり、永続化される際のデータフォーマット」として不適切だと考えています。
多少冗長になりますが、こんな感じで書くことをおすすめします。
using UnityEngine; public class Player : MonoBehaviour { [SerializeField] private int level; public int Level => level; }こんなことができればよかったのに
2017年から主張しているけれど、「Unityでシリアライズ名を指定する機能が欲しい」。
SerializeFieldが引数をとって、シリアライズ名を指定できればよかったのに・・・
public class Player : MonoBehaviour { // こんなことができればよかったのに [field: SerializeField("field")] public int Level { get; private set; } }FormerlySerializedAsAttributeってのがあるけれど、残念ながらこれはダメ。「以前シリアライズされていた名前を指定するもの」だから。
ちなみにもし、「自動実装プロパティのバッキングフィールドにSerializeFieldをつけた時の名前が、勝手に良い感じになれば良いのにー」って思った人がいたら、それはNot Goodだと思います。「SerializeFieldをつけた時のシリアライズされる名前はフィールド名で」っていう原則が崩れちゃうので。(自動実装プロパティのバッキングフィールドもフィールドだから)
やっぱり必要なのは明示的にシリアライズする名前を指定する方法。(Unity 2020.1やUnity 2020.2の時点でそんな方法はない、ですよね?あったら教えてください。)