- 投稿日:2020-12-18T23:55:37+09:00
【.NET】コンボボックスで複数選択する方法
はじめに
これは、Visual Basic Advent Calendar 2020の18日目の記事となります。
同僚から複数選択のコンボボックスってないですか?って聞かれて、標準のコンボボックスは複数選択できないんだったっけと調べたんですが、出来ませんでした。
検索すれば、どこかにあるだろうって「c# コンボボックス 複数選択」で検索するも見当たらない。キーワードを英語に変更「c# custom control combobox multiselect」するなどして、ようやく下記サイトから辿ることができた。
how to do Multi select dropdown list/Combobox C# windows application【2020/12/19追記】
18日の記事に合わせるために慌てて書いたので、翌日に再度落ち着いて調べ直しました。
検索キーワードとしては、「c# checkedlistbox DropDown」で調べるともう少し情報が得られました。
WinFormは、③と④を追加、Wpfは全般見直し。WinForm
①と②は C# でカスタムコントロールを作成しています。
どちらも「CodeProject」に登録されているので、ダウンロードするにはアカウントが必要になります。
③と④は、VB.NET でカスタムコントロールを作成しています。
- ① CheckBox ComboBox Extending the ComboBox Class and Its Items
- ② A ComboBox with a CheckedListBox as a Dropdown
- ③ Make DropDown CheckedListBox in VB.net
- ④ VB.NET 版 CheckedComboBox - 周回遅れのブルース
お薦めは①の方になります。「CheckBoxComboBox.dll」が作成されるので、Visual Basicでも参照に追加することで使用できます。
作成時期が古いのでターゲットフレームワークは「.NET Framework 3.5」になっていますが、そこは最新の「.NET Framework 4.8」に変更するなりしてください。Public Class Form1 Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load cmbManual.Items.Add("Item 1") cmbManual.Items.Add("Item 2") cmbManual.Items.Add("Item 3") cmbManual.Items.Add("Item 4") cmbManual.Items.Add("Item 5") cmbManual.Items.Add("Item 6") cmbManual.Items.Add("Item 7") cmbManual.Items.Add("Item 8") cmbManual.CheckBoxItems(1).Checked = True End Sub End ClassWpf
- ① Multi Select ComboBox in WPF
- ② Multi Select ComboBox in WPF - CodeProject
- ③ C# WPF で複数のタグを選択できるコンボボックスを作る
- ④ WPF CheckListBox/RadioListBox + CheckComboBox/RadioComboBox
お薦めは②の方になります。①と②は作者は同じなんですが、②の方が記事の更新が新しくコンボボックスの右側に「▼」が追加されています。
③は、リストではないですが複数の値を選択できるということ挙げました。①のコメント欄にあった、dictionary型よりList型が一般的とのソースコードを以下に書き直しました。
This is Great but the code as is is not exploitable.In the normal life we use List not dictionary and it should work properly on MVVM.The solution is in MultiSelectComboBox.xaml.cs
MultiSelectComboBox.xaml.csusing System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Windows; using System.Windows.Controls; using System.Windows.Data; using System.Windows.Documents; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Windows.Navigation; using System.Windows.Shapes; using System.Collections.ObjectModel; using System.ComponentModel; namespace MultiSelectComboBox { /// <summary> /// Interaction logic for MultiSelectComboBox.xaml /// </summary> public partial class MultiSelectComboBox : UserControl { private readonly ObservableCollection<Node> _nodeList; public MultiSelectComboBox() { InitializeComponent(); _nodeList = new ObservableCollection<Node>(); } #region Dependency Properties public static readonly DependencyProperty ItemsSourceProperty = DependencyProperty.Register("ItemsSource", typeof(IList), typeof(MultiSelectComboBox), new FrameworkPropertyMetadata(null, OnItemsSourceChanged)); public static readonly DependencyProperty SelectedItemsProperty = DependencyProperty.Register("SelectedItems", typeof(IList), typeof(MultiSelectComboBox), new FrameworkPropertyMetadata(null, OnSelectedItemsChanged)); public static readonly DependencyProperty TextProperty = DependencyProperty.Register("Text", typeof(string), typeof(MultiSelectComboBox), new UIPropertyMetadata(string.Empty)); public static readonly DependencyProperty DefaultTextProperty = DependencyProperty.Register("DefaultText", typeof(string), typeof(MultiSelectComboBox), new UIPropertyMetadata(string.Empty)); public IList ItemsSource { get { return (IList)GetValue(ItemsSourceProperty); } set { SetValue(ItemsSourceProperty, value); } } public IList SelectedItems { get { return (IList)GetValue(SelectedItemsProperty); } set { SetValue(SelectedItemsProperty, value); } } public string Text { get { return (string)GetValue(TextProperty); } set { SetValue(TextProperty, value); } } public string DefaultText { get { return (string)GetValue(DefaultTextProperty); } set { SetValue(DefaultTextProperty, value); } } #endregion #region Events private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { MultiSelectComboBox control = (MultiSelectComboBox)d; control.DisplayInControl(); } private static void OnSelectedItemsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { MultiSelectComboBox control = (MultiSelectComboBox)d; control.SelectNodes(); control.SetText(); } private void CheckBox_Click(object sender, RoutedEventArgs e) { CheckBox clickedBox = (CheckBox)sender; if (clickedBox.Content != null && clickedBox.Content.ToString() == "All") { if (clickedBox.IsChecked.HasValue && clickedBox.IsChecked.Value) { foreach (var node in _nodeList) node.IsSelected = true; } else { foreach (var node in _nodeList) node.IsSelected = false; } } else { var selectedCount = _nodeList.Count(s => s.IsSelected && s.Title != "All"); var node = _nodeList.FirstOrDefault(i => i.Title == "All"); if (node != null) node.IsSelected = selectedCount == _nodeList.Count - 1; } SetSelectedItems(); SetText(); } #endregion #region Methods private void SelectNodes() { if (SelectedItems == null) return; foreach (var item in SelectedItems) { var node = _nodeList.FirstOrDefault(i => i.Title == item.ToString()); if (node != null) node.IsSelected = true; } } private void SetSelectedItems() { SelectedItems.Clear(); foreach (var node in _nodeList) { if (!node.IsSelected || node.Title == "All") continue; if (ItemsSource.Count <= 0) continue; var source = ItemsSource.Cast<object>().ToList(); SelectedItems.Add(source.FirstOrDefault(i => i.ToString() == node.Title)); } } private void DisplayInControl() { _nodeList.Clear(); if (ItemsSource.Count > 0) _nodeList.Add(new Node("All")); foreach (var item in ItemsSource) { var node = new Node(item.ToString()); _nodeList.Add(node); } MultiSelectCombo.ItemsSource = _nodeList; } private void SetText() { if (SelectedItems != null) { var displayText = new StringBuilder(); foreach (var s in _nodeList) { if (s.IsSelected == true && s.Title == "All") { displayText = new StringBuilder(); displayText.Append("All"); break; } if (s.IsSelected != true || s.Title == "All") continue; displayText.Append(s.Title); displayText.Append(','); } Text = displayText.ToString().TrimEnd(new char[] { ',' }); } // set DefaultText if nothing else selected if (string.IsNullOrEmpty(this.Text)) { this.Text = this.DefaultText; } } #endregion } public class Node : INotifyPropertyChanged { private string _title; private bool _isSelected; #region ctor public Node(string title) { Title = title; } #endregion #region Properties public string Title { get { return _title; } set { _title = value; NotifyPropertyChanged("Title"); } } public bool IsSelected { get { return _isSelected; } set { _isSelected = value; NotifyPropertyChanged("IsSelected"); } } #endregion public event PropertyChangedEventHandler PropertyChanged; protected void NotifyPropertyChanged(string propertyName) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } } }最後に
思ったより日本語で検索しても見つからないもんですね。見つかりやすいタイトルにしてみました。
- 投稿日:2020-12-18T23:00:33+09:00
真っ新なMacBookAirにUnityをインストールしてスマホ向けアプリの動作確認をするまで
自分の好みの快適な開発環境にする方法と動作確認までメモ
環境
構築する開発環境
- MacBook Air 2019 (Intelチップ)
- Mac OS Catalina 10.15.7
- Unity 2019.4.16f1 (LTS)
- Android Build Support
- iOS Build Support
- Visual Studio Code version 1.5.2
作ったアプリの動作確認環境
- Mac : MacBook Air 2019 (Intelチップ)
- Mac OS Catalina 10.15.7
- Android : Pixel 3a
- Android OS Version 11
- iPhone : iPhone 8
- iPhone OS xxx
ステップ
- Unityのインストール
- Visual Studio Codeのインストール
- Unityの起動と設定
- Visual Studio Codeの起動と設定
- MacBookAir向けにビルドして動作確認
- Android向けにビルドして動作確認
- iPhone向けにビルドして動作確認
1.Unityのダウンロードとインストール
- 以下からUnity Hubをダウンロードし、UnityHubSetup.dmgを実行してインストール
- UnityHubを起動してアカウントの設定(Unityアカウントを持っていなければ作成)
- UnityHubからUnity2019.4.16f1をインストール
- Visual Studio for Mac をアンチェック
- Android Build Supportにチェック
- iOS Build Supportにチェック
- ドキュメンテーションをアンチェック
- 日本語をチェック
2.Visual Studio Codeのインストール
以下からMac向けのVisual Studio Codeをダウンロードし、VSCode-darwin.dmjを実行してインストール
https://code.visualstudio.com/download以下の拡張機能を入れる(その他の自分好みの設定や拡張については別途)
- C#
- Debugger For Unity
- Mono Debug
3.Unityの起動と設定
Visual Studio Codeをエディタに設定する
- 適当な名前でプロジェクトを作成
- Unity > Preferences… > External Tools から External Script EditorにVisual Studio Codeを選択
- Genetate .csproj files forの下にあるチェックボックスをすべてチェック
Android NDKエラーが出ている場合
Unity > Preferences… > External Tools から External ToolsからAndroid NDK installed with Unityで警告が出ていたので確認
表示されているパスを確認するとなぜかNDKが空っぽなのでUnity Hubから一度Android Build Supportを削除して、もう一度インストールするとちゃんと入っていた
理由は不明(UnityHubにフォルダアクセス権限がなかった?でも他のものは生成できているので謎)4. Visual Studio Codeの起動と設定
- 適当にC#スクリプトを作成
- Unityの上のメニューバーからAssets > Open C# Projectから起動 ←重要
インテリセンス(補完機能)が効かない場合
- Home Brewが入っていない場合はターミナルから下のコマンドでインストール
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
- monoをターミナルからHome brewコマンドでインストール
brew install mono
- Visual Studio CodeのSetting.jsonに以下を追記(コマンドパレットから検索で開ける
"omnisharp.useGlobalMono": "always", "omnisharp.monoPath": "/usr/local/Cellar/mono/6.8.0.105"これでインテリセンスが聞くようになった
5. MacBookAir向けにビルドして動作確認
- UnityでFile > Build Setting
- PlatformをPC, Mac & Linux Standaloneが選択された状態にする
- Build and Run
6. Android向けにビルドして動作確認
- UnityでFile > Build Setting
- PlatformをAndroidが選択された状態にする
- まだしていなければAndroid端末を開発者モードにする(ビルド番号を7回タップ)
- USBケーブルでMacとAndroid端末をつなぐ
- Build And Run
7. iPhone向けにビルドして動作確認
まだ
iPhone持っていないので、家族に借りてこんどやってみる
- 投稿日:2020-12-18T22:53:00+09:00
UnityのDebug機能を便利にしたい!紹介編
現状のUnityEngine.Debugの問題
皆さんUnityのDebugクラスよく使いますよね?
Debug.Log()
とか。
でもたまに不便だな~って思う時ありません?配列を表示したい時とか
Debug.Log(new string[] {"a", "b", "c"});
↑配列なことぐらいわかってるわ!要素が欲しいんじゃ!
こういう場合はforeachなどで回すか、LINQでゴニョゴニョするとかしか無いんですよね、、、非常にめんどくさい!!
というわけで作りました。その名もDebugExtentions!
まだ制作途中で、機能が豊富とは言えませんが、一部をgithubに公開いたしましたので今回はそれをご紹介したいと思います。DebugExtensions機能一覧
・普通のDebugクラスの機能全部(もしかしたら実装漏れがあるかも)
・命名の関係上ClearDeveloperConsole()
→ClearConsoleError()
になっています。
・多少制限はあるけど、structも表示できます。
・Colorクラスの表示が見やすくなった
・Array, List, HashSet, Dictionary
の要素表示(Format系は対応してないですごめんなさい、、)
・ログのテキスト保存
・コンソール全クリア
・リッチテキストを簡単に使えるぞ1. Array, List, HashSet, Dictionaryの要素表示
個人的に一番最初に実装したかった機能。
こんな感じで表示されます。
Array・List・HashSet
void Start() { string[] wordArray = new[] { "apple", "blue", "cactus", "default", "emission" }; DebugEx.Log(wordArray); }
Dictionary
void Start() { Dictionary<string, int> dict1 = new Dictionary<string, int>() { {"normalSword", 5}, {"superSword", 2}, {"apple", 100}, {"water", 1} }; DebugEx.Log(dict1); }Dictionaryの冒頭2行は、KeyとValueの型情報を表示してくれています。
突っ込むだけで、簡単に要素を表示してくれるのでかなり便利になりました。しかし、筆者の表示形式のセンスが無いので、あまり統一感がありません。
この表示形式は、これからのアップデートで改善していく予定です。2. ログのテキスト保存
DebugEx.RecordStart(); 処理 DebugEx.RecordStop();
RecordStart()
とRecordStop()
の間で、ロギングが行われた場合、情報が保存されます。
RecordStop()
が実行されたときに保存されるので注意。
デフォルトではDebugExtensions/Logs
に保存されますが、設定で変えることができます。3. コンソール全クリア
DebugEx.ClearConsoleAll();その名の通り、まとめてログを全削除します。
4. リッチテキストを簡単に使えるぞ
"Alpha".Color(Color.red) //色の変更 "Bravo".Bold() //太字にする "Charlie".Italic() //イタリック体にする "Delta".Size(12) //文字のサイズを変更する "Echo".RemoveRichText(); //リッチテキストを取り除くConsoleではリッチテキストを使うことができるので、string型の変数に上記のようなメソッドを連結することで、リッチテキストに変換することができます。
コンソールの文字をちょっといじりたいときなどに便利です。5. その他設定
LogSavePath: ログファイルを保存するパス
SaveStackTrace: ログの下の部分にくっついている、呼び出し元のトレース情報をログファイルに書き込むか
Dictionary Key,Value: Dictionaryをコンソールに表示するときの色
最後に
結構頑張ればもっと使いやすくなると思うので、これからブラッシュアップしていけたらなと思います。
リリースできるようになれば、アセットストアなどに出す予定です。!!告知!!
現在、チームでShoutousというFPSゲームを開発しています!~Soutousとは?~
◉大規模な現代戦を題材にした
◉どんな遊び方でも皆活躍できる
◉今のFPSの良いとこ取りをした
◉e-Sports味の全くない、貴方の為の
◉意識低い系高画質お祭りFPSゲーム!公式ツイッターのフォローよろしくお願いいたします!
Twitter: https://twitter.com/ShoutousJP3Dデザイナーや、Unityエンジニアなども募集しているので、興味があればぜひTwitterID:
@harumar0n
にDMいただけるとありがたいです!
- 投稿日:2020-12-18T18:53:16+09:00
よく忘れるstring.Format() メソッドの書式設定
VisualStudio デコーディングしている時、よく使うけれど忘れてしまう書式設定についてメモレベルで記録しておく
数値
カンマ区切り、小数点以下固定
sample.cs// 123,567.111 string numFormat = string.Format("{0:#,0.000}", 123456.111111);sample.cs// 0.000 string numFormat = string.Format("{0:#,0.000}", 0);0の場合、「0」だけ表示
sample.cs// 0 string numFormat = string.Format("{0:#,0.###}", 0);0の場合、空白
sample.cs// stringl.Empty string numFormat = string.Format("{0:#,#.###}", 0);先頭0埋め(コード化等に使用)
sample.cs// 0123 string numFormat = string.Format("{0:0000}", 123);日付
年4桁、月日2桁、24時間2桁、分秒2桁、ミリ秒表示
sample.cs// 2021/01/01 13:01:01.123 string dateFormat = string.Format(@"{0:yyyy/MM/dd HH:mm:ss.fff}", DateTime.Now);
- 投稿日:2020-12-18T18:07:35+09:00
借金の利息がバーチャルペットになって画面を散歩するアプリを作ります
この記事は
「クソアプリ2 Advent Calendar 2020」の19日目の記事となります。
はじめに
親の教えとして「何事もポジティブに」と教育を受けてきました。
おかげか大体のことはポジティブに考えることがきました。しかし、世の中には無理じゃね?というものもあります。
借 金
どうポジティブにとらえたらいいんだ。。。
負のものしか含まれていないぞ。
ポケ〇ンをやりながら考えていました。モ、モンスターにおきかえてみるか。
コンセプト
借金をするほど強くなる(ポジティブに見える)アプリをつくる。
開発ツール
Unity
生まれたもの
タイトル通り散歩します、借金が。
5千兆円借金してみる
う、うわああああああああああ。
色々とバグってますがあれです。
クソアプリなんで許して。最低限の借金返済アプリの体裁は一応整えていたりします
モンスターをタッチすると借金返済できます。
ギリギリ使えるレベルにするのもクソアプリ製作の楽しいところですよね。
実装
カレンダーの実装は下サイトから頂戴しました
Unityのカレンダーアセット作った!-alberttecの日記
今回のクソアプリで見た目部分も着手できたのはカレンダー配布がめちゃ大きいです。神配布感謝です。キャラクターはアセットストアで購入(7$)
Pixel Mobs-UnityeAssetStoreキャラクターの移動
SetNextPosition()で移動先を指定してLerpで移動。
移動が完了したらRecenter()で移動先を再指定です。mob.csvoid Update() { float interval = 5; elapsedTime += Time.deltaTime / interval ; Vector2 tmpPos = Vector2.Lerp(beforePosition, nextPosition, elapsedTime); rectTransform.localPosition = new Vector3(tmpPos.x, tmpPos.y, 0); if (IsMoved) { Init(); } } private void Init() { SetNextPosition(); elapsedTime = 0; } public void Recenter() { beforePosition = new Vector2(rectTransform.localPosition.x, rectTransform.localPosition.y); elapsedTime = 0; } private void SetNextPosition() { beforePosition = new Vector2(rectTransform.localPosition.x, rectTransform.localPosition.y); int randX = Random.Range(-MaxX, MaxX); int randY = Random.Range(-MaxY, MaxY); nextPosition = new Vector2(randX, randY); }借金の計算式系
コア部分を抜粋したものです。
LoanPrincipalListには元金
LoanAnnualInterestListには利率
です。
最初、doubleではなくfloatで型指定した結果、1000万以上借金をすると100円になる素敵なバグが発生しました。LoanCalculation.cs// 元金 public double GetPrincipal(int index) { return LoanPrincipalList.Count == 0 ? 0 : LoanPrincipalList[index]; } // 1日あたりの利息 public double GetOneDayDebt(int index) { double pricipal = LoanPrincipalList.Count != 0 ? LoanPrincipalList[index] : 0; double annualInterest = LoanAnnualInterestList.Count != 0 ? LoanAnnualInterestList[index] : 0; double oneYearDebt = pricipal * (annualInterest / 100);// 100は百分率になおしている return Math.Round(oneYearDebt / OneYear);// 365は1年 } public double GetTotalMoney(int index) { double pricipal = LoanPrincipalList.Count == 0 ? 0 : LoanPrincipalList[index]; double annualInterest = LoanAnnualInterestList.Count == 0 ? 0 : LoanAnnualInterestList[index]; double totalDebt = GetTotalDebt(index); return pricipal + totalDebt; } // 利息の合計 public double GetTotalDebt(int index) { double diffTotalDays = GetDifferenceTotalDays(index);// 現在日から支払日までの差 double oneDayPricipal = GetOneDayDebt(index);// 一日あたりの年利 return Math.Ceiling(diffTotalDays * oneDayPricipal);// 年利で発生した金額 } // 現在日から支払日の差 private double GetDifferenceTotalDays(int index) { DateTime today = DateTime.Today; DateTime loanLastRepaymentHistory = (LoanLastRepaymentHistory.Count != 0 ? LoanLastRepaymentHistory[index] : today); TimeSpan diffDay = (today - loanLastRepaymentHistory); return diffDay.TotalDays; }余談
せっかくつくったのでAppleStoreに申請しようとたところキーワードという欄があるそうです。
アプリ検索用に100字以内で検索ワードを記述できるようです。実際に記述したもの
ギャンブル,サラ金,デビット,プロミス,モビット,リボ,リボモン,リボルビング,レイク,ローン,借入,借金,債務,元金,利子,利息,残高,消費者金融,計算,負債,返済,金利この世の地獄みたいなキーワードランキングがあったら上位に食い込めそうです。
最後に
借金が増えるほどモンスターがいっぱいになっていきます。
なんか楽しい気持ちになってきた気がします。
これで少しは借金をポジティブに考えられるようになった気がします。
わーい。
めでたしめでたし。
- 投稿日:2020-12-18T15:47:28+09:00
Unity iOSのデバッグビルドでのunsafeコードの速度低下とその対策
この記事はDeNA Advent Calendar 2020の20日目の記事です。
大竹悠人(@Trapezoid)です。 DeNAではゲーム用のライブラリ/SDK開発やセキュリティ対策、開発効率化などを横断的に行いつつ、トラブルシューターとして活動しています。
今日はUnityエンジニア向けに、死ぬほどニッチな小ネタを書こうと思います。TL;DR
- IL2CPPでは、高負荷なロジックをunsafeにしたらiOSのデバッグビルドで激烈に遅くなることがある
- IL2CPPがinline化を前提とした癖のあるコードを生成するが、それがデバッグビルドで最適化が無効になることでinline化が阻害されるのが原因
- 一般的な最適解としてはそもそも最適化を有効にするか、Burstやネイティブプラグイン化で対策した方が望ましい
- 様々な事情により、局所解として手動inline化とも言える力技を行って解決した
経緯
パフォーマンス的にナイーブな計算量の多い場面で、C#のunsafeを使った最適化を行うことは稀にあるかと思います。
僕も特に暗号化やハッシュ化などのために稀によく頻繁に使うのですが、ある時利用者からiOSでの実行時だけ該当ロジックが異常に重いので調査してほしい
という報告を受けました。
いくつか原因切り分けをしてもらったところ、 (Xcode上の)DebugビルドでiOSビルドを行った場合のみ この現象が起こることが分かってきました。
Debug設定時のみとはいえ、続行を諦めたくなるほど遅くなっていたので、速やかな対処が必要です。発生していた問題
Debugビルドのみで起こるというと何かしら最適化と相性の悪い何かがあるのかな...とは思いつつ、再現させてプロファイリングを行ったところ、unsafeコードとして記述している区間がボトルネックになっていることが分かりました。
問題になったのは、stackallocで固定長の小さなバッファを用意して、そのバッファをひたすらこねるといった、暗号化などでよくあるワークロードでした。
起こっていたことを、次のようなコードを使って説明します。
(今回は問題を単純化するために、ただ引数をstackallocしたバッファに添字指定でコピーするだけのmemcpyすれば?って感じの意味のないコードにしています。実際はバッファへのコピーも単純なコピーではないです)public unsafe void Test1(byte* x) { var buffer = stackalloc int[16]; buffer[0] = x[0]; buffer[1] = x[1]; buffer[2] = x[2]; // ... buffer[15] = x[15]; //bufferをこね回す重い処理が以降に入る }このコードにIL2CPPをかけると、次のようなC++コードに変換されます。
// var buffer = stackalloc int[16]; int8_t* L_0 = (int8_t*) alloca((((uintptr_t)((int32_t)64)))); memset(L_0, 0, (((uintptr_t)((int32_t)64)))); // buffer[0] = x[0]; int8_t* L_1 = (int8_t*)(L_0); int32_t* L_2 = ___x0; int32_t L_3 = *((int32_t*)L_2); *((int32_t*)L_1) = (int32_t)L_3; // buffer[1] = x[1]; int8_t* L_4 = (int8_t*)L_1; int32_t* L_5 = ___x0; int32_t L_6 = *((int32_t*)((int32_t*)il2cpp_codegen_add((intptr_t)L_5, (int32_t)4))); *((int32_t*)((int8_t*)il2cpp_codegen_add((intptr_t)L_4, (int32_t)4))) = (int32_t)L_6; // buffer[2] = x[2]; int8_t* L_7 = (int8_t*)L_4; int32_t* L_8 = ___x0; int32_t L_9 = *((int32_t*)((int32_t*)il2cpp_codegen_add((intptr_t)L_8, (intptr_t)((intptr_t)il2cpp_codegen_multiply((intptr_t)(((intptr_t)2)), (int32_t)4))))); *((int32_t*)((int8_t*)il2cpp_codegen_add((intptr_t)L_7, (intptr_t)((intptr_t)il2cpp_codegen_multiply((intptr_t)(((intptr_t)2)), (int32_t)4))))) = (int32_t)L_9; // buffer[3] = x[3]; int8_t* L_10 = (int8_t*)L_7; int32_t* L_11 = ___x0; int32_t L_12 = *((int32_t*)((int32_t*)il2cpp_codegen_add((intptr_t)L_11, (intptr_t)((intptr_t)il2cpp_codegen_multiply((intptr_t)(((intptr_t)3)), (int32_t)4))))); *((int32_t*)((int8_t*)il2cpp_codegen_add((intptr_t)L_10, (intptr_t)((intptr_t)il2cpp_codegen_multiply((intptr_t)(((intptr_t)3)), (int32_t)4))))) = (int32_t)L_12; //...単なる代入だけの単純な処理だったはずが、大量の
il2cpp_codegen_add
及びil2cpp_codegen_multiply
の呼び出しが発生しており、
IL2CPPを通ったunsafeなメモリアクセスが、intptr_tにキャストした上でアドレスを計算しなおすように変換されていることが分かります。
il2cpp_codegen_add
及びil2cpp_codegen_multiply
の定義を見てみると、次のようなinline指定されたtemplateになっています。template<typename T, typename U> inline typename pick_bigger<T, U>::type il2cpp_codegen_multiply(T left, U right) { return left * right; } template<typename T, typename U> inline typename pick_bigger<T, U>::type il2cpp_codegen_add(T left, U right) { return left + right; }inlineキーワードのみのinline関数は
-O0
指定時はインライン展開されないため、これは-O0
で最適化が無効化された場合にはunsafeコード中でのアドレス演算の度に複数回の関数呼び出しが行われるということを意味します。unsafeコード中のアドレス演算が手計算される関係で、この回数は想像よりも更に多くなります今回の問題の原因はこのように、XcodeでBuild ConfigurationをDebugにしてビルドした場合、
-O0
で最適化が無効化されるようになっているため、呼出回数の多いポインタへのインデックスアクセスのコストが爆増したことにありました。また、そもそもプリミティブ型の四則演算に関しては、unsafeでない場合にも概ね同じルールで変換されるため、unsafeに限らず純粋なプリミティブ型の演算を大量に行う場合はこの問題にあたることが多くなりそうです。
対策
対策は複数考えられます。
Debugビルド時の最適化レベルを
-O2
以上にする
-O2
以上であればinline関数は概ねinline化される為、関数呼出によるオーバーヘッドはなくなり、根本的な対策にはなります。また、問題になるような負荷の高いロジック以外でも、四則演算全般の速度がある程度向上すると思われます。
ただし、デバッガビリティはある程度犠牲になります。今回は共通基盤となるライブラリのコード内での問題でしたので、これを利用者側への強制するのは流石に避けたいという思いがありました。また、元々パフォーマンスに振り切ったかなりナイーブな実装をメソッドに閉じてしていた為、ある程度ナイーブな対策を追加で行っても相対的には問題ないという判断をしました。
Burst / ネイティブプラグインで書く
そもそも負荷の高いナイーブな処理であれば、Burstを使ってIL2CPPを避けて最適化されたLLVM Bitcodeを出力させたり、直接C/C++でネイティブプラグインとして書く、というのも手です。
そもそもの最適化という意味では非常に良い選択肢ですし、そもそも問題が起こり得る領域を考えると大抵の場合の最適解となり得ると思っています。
ですが、今回はUnityに依存しない、.NET Coreからも使われるコードベースであった為、Burst化やネイティブプラグイン化はUnity特化として追加する形で行う必要がありました。このため、検討はしつつも今回の解決策としては見送りました。
アドレス演算の回数を少なくする
問題となったコードでは、stackallocした領域にかなり回数アクセスする一方で、アクセスするアドレスの範囲は非常に限られており、数も固定されていました。
このため、今回はアクセスする全てのアドレスのポインタをそれぞれ1つだけスタック上に確保しておき、それらのポインタを経由して値を読み書きするという死ぬほど泥臭い対応を行うことで解決しました。
サンプルとして出した例で書き直すなら、以下のような形になります。
public unsafe void Test1(int* x) { var buffer = stackalloc int[16]; var bp0 = &buffer[0]; var bp1 = &buffer[1]; var bp2 = &buffer[2]; //... var bp15 = &buffer[15]; *bp0 = x[0]; *bp1 = x[1]; *bp2 = x[2]; //... *bp15 = x[15]; //bp~経由でbufferをこね回す重い処理が以降に入る }IL2CPPを通してみると、以下のようなコードになります。
int32_t* V_0 = NULL; int32_t* V_1 = NULL; int32_t* V_2 = NULL; int32_t* V_3 = NULL; //... { // var buffer = stackalloc int[16]; int8_t* L_0 = (int8_t*) alloca((((uintptr_t)((int32_t)64)))); memset(L_0, 0, (((uintptr_t)((int32_t)64)))); // var bp0 = &buffer[0] ; int8_t* L_1 = (int8_t*)(L_0); V_0 = (int32_t*)(((uintptr_t)L_1)); // var bp1 = &buffer[1] ; int8_t* L_2 = (int8_t*)L_1; V_1 = (int32_t*)(((uintptr_t)((int8_t*)il2cpp_codegen_add((intptr_t)L_2, (int32_t)4)))); // var bp2 = &buffer[2] ; int8_t* L_3 = (int8_t*)L_2; V_2 = (int32_t*)(((uintptr_t)((int8_t*)il2cpp_codegen_add((intptr_t)L_3, (intptr_t)((intptr_t)il2cpp_codegen_multiply((intptr_t)(((intptr_t)2)), (int32_t)4)))))); // var bp3 = &buffer[3] ; int8_t* L_4 = (int8_t*)L_3; V_3 = (int32_t*)(((uintptr_t)((int8_t*)il2cpp_codegen_add((intptr_t)L_4, (intptr_t)((intptr_t)il2cpp_codegen_multiply((intptr_t)(((intptr_t)3)), (int32_t)4)))))); //... // *bp0 = x[0]; int32_t* L_17 = V_0; int32_t* L_18 = ___x0; int32_t L_19 = *((int32_t*)L_18); *((int32_t*)L_17) = (int32_t)L_19; // *bp1 = x[1]; int32_t* L_20 = V_1; int32_t* L_21 = ___x0; int32_t L_22 = *((int32_t*)((int32_t*)il2cpp_codegen_add((intptr_t)L_21, (int32_t)4))); *((int32_t*)L_20) = (int32_t)L_22; // *bp2 = x[2]; int32_t* L_23 = V_2; int32_t* L_24 = ___x0; int32_t L_25 = *((int32_t*)((int32_t*)il2cpp_codegen_add((intptr_t)L_24, (intptr_t)((intptr_t)il2cpp_codegen_multiply((intptr_t)(((intptr_t)2)), (int32_t)4))))); *((int32_t*)L_23) = (int32_t)L_25; // *bp3 = x[3]; int32_t* L_26 = V_3; int32_t* L_27 = ___x0; int32_t L_28 = *((int32_t*)((int32_t*)il2cpp_codegen_add((intptr_t)L_27, (intptr_t)((intptr_t)il2cpp_codegen_multiply((intptr_t)(((intptr_t)3)), (int32_t)4))))); *((int32_t*)L_26) = (int32_t)L_28; //... }stackallocした領域のポインタが全てスタック上に確保されて、使い回されていることがわかります。
これはもちろん、
sizeof(IntPtr) * 要素数
byteのスタックを追加で消費することになるので、要素数が少ないときにしか使えない、場面が非常に限定される対策ではあります。ですが、暗号学的なアルゴリズムでは
比較的サイズが固定的で小さいステートに対して流し込むデータのサイズに比例する回数の操作を行う
というものが多くありますし、それらを実装する場面(どちらにせよニッチですが...)では同様に使えることもあるのではないか...と思います。最後に
ここまで読んでくれた皆様、ありがとうございました。
ニッチな事例のための特殊な対応を紹介していきましたが、いかがでしたでしょうか。
僕はBurst最高だと思います。皆さんBurstを使いましょう。DeNA 公式 Twitter アカウント @DeNAxTech では、Blog記事だけでなく色々な勉強会での登壇資料も発信しています。
この記事でDeNAの技術的な取り組みに興味を持った方は、是非フォローお願いします。
- 投稿日:2020-12-18T14:51:34+09:00
セッション管理とスレッドモデル(ASP.NET編)
セッション管理とスレッドモデル(ASP.NET編)
結論
複数のセッションの処理はマルチスレッド処理(平行処理)が行われているかもしれないが、同一セッションの内部では処理は一つずつシリアルで処理されているということ。
実験
親のaspxでセッションを作成、その後、ko0.aspxとko1.aspxに対して同時にアクセスされるはずなので、その様子を見てみよう。
oya.aspx<%@ Page Language="C#" %> <script runat="server"> </script> <html> <head> </head> <body> <form id="form1" runat="server"> SessionID= <%= this.Session.SessionID %><br><hr> <iframe src="ko0.aspx"></iframe><br><hr> <iframe src="ko1.aspx"></iframe> </form> </body> </html>ko0.phpとko1.phpは同じ内容で、10秒間スリープするだけの処理
ko0.aspx<%@ Page Language="C#" %> <script runat="server"> </script> <html> <head runat="server"> </head> <body> <form id="form1" runat="server"> <% Response.Write("SessionID=" + this.Session.SessionID + "<br>"); System.DateTime dateTime1 = System.DateTime.Now; Response.Write(dateTime1.Hour.ToString() + ":" + dateTime1.Minute.ToString() + ":" + dateTime1.Second.ToString()+ "<br>"); Session["aaa"] += "abc"; Response.Write(Session["aaa"] + "<br>"); System.Threading.Thread.Sleep(10000); System.DateTime dateTime2 = System.DateTime.Now; Response.Write(dateTime2.Hour.ToString() + ":" + dateTime2.Minute.ToString() + ":" + dateTime2.Second.ToString()+ "<br>"); %> </form> </body> </html>結果
ko0.aspx が処理されてから、ko1.aspxが処理されているのがわかる。
時々、セッションがko[0|1].aspxに引き継がれない場合もある...(その時は、個別のセッションなので、ko[0|1].aspxは同時並行に処理される)
つまり
同一セッションで大量にアクセスしても・・・早く終わるわけではない、ということ
考えてみたら・・・
Sessionオブジェクトに複数スレッドから同時アクセスを制御するようなロック処理がないのだから当然といえば当然か・・・
このあたりはClassicASPと互換性(sessionオブジェクトには排他制御がないけど、Applicationオブジェクトにはある・・・とか)を維持しているのかもしれない。
戻る
以上
- 投稿日:2020-12-18T11:35:04+09:00
【逆アセ】 C#でlongに代入してみる
はじめに
さて皆さん。次のコードはどんなふうにコンパイルされると思いますか。
long value = 1L;「うーん、こんな感じ?」
;x64 movq $1, -8(%rbp);x86 sub rsp, 24 mov DWORD PTR a$[rsp], 1正解です。
しかし、C#では違います。
結構違います。さっき知ったこの驚きを共有しようと思って書きました。
そんな記事です。
結論から言えば、単なる定数代入が7パターンにコンパイルされます。sharplab.ioも開きながら読めば分かりやすいかもしれません。
Cの逆コンパイルはCompiler Explorerを利用させていただきました。逆コンパイル結果一覧
C#: ソースコードlong value; value=0; value=1; value=8; value=9; value=127; value=128; value=2147483647L;//0b1111111111111111111111111111111 value=2147483648L;//0b10000000000000000000000000000000 value=4294967167L;//0b11111111111111111111111101111111 value=4294967168L;//0b11111111111111111111111110000000 value=4294967294L;//0b11111111111111111111111111111110 value=4294967295L;//0b11111111111111111111111111111111 value=4294967296L;//0b100000000000000000000000000000000 value=-1; value=-2; value=-128; value=-129; value=-2147483648L; value=-2147483649L;MSIL: 逆アセンブル結果
.class private auto ansi '<Module>' { } // end of class <Module> .class private auto ansi abstract sealed beforefieldinit '<Program>$' extends [System.Private.CoreLib]System.Object { .custom instance void [System.Private.CoreLib]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = ( 01 00 00 00 ) // Methods .method private hidebysig static void '<Main>$' ( string[] args ) cil managed { // Method begins at RVA 0x2050 // Code size 102 (0x66) .maxstack 1 .entrypoint .locals init ( [0] int64 'value' ) IL_0000: ldc.i4.0 IL_0001: conv.i8 IL_0002: stloc.0 IL_0003: ldc.i4.1 IL_0004: conv.i8 IL_0005: stloc.0 IL_0006: ldc.i4.8 IL_0007: conv.i8 IL_0008: stloc.0 IL_0009: ldc.i4.s 9 IL_000b: conv.i8 IL_000c: stloc.0 IL_000d: ldc.i4.s 127 IL_000f: conv.i8 IL_0010: stloc.0 IL_0011: ldc.i4 128 IL_0016: conv.i8 IL_0017: stloc.0 IL_0018: ldc.i4 2147483647 IL_001d: conv.i8 IL_001e: stloc.0 IL_001f: ldc.i4 -2147483648 IL_0024: conv.u8 IL_0025: stloc.0 IL_0026: ldc.i4 -129 IL_002b: conv.u8 IL_002c: stloc.0 IL_002d: ldc.i4.s -128 IL_002f: conv.u8 IL_0030: stloc.0 IL_0031: ldc.i4.s -2 IL_0033: conv.u8 IL_0034: stloc.0 IL_0035: ldc.i4.m1 IL_0036: conv.u8 IL_0037: stloc.0 IL_0038: ldc.i8 4294967296 IL_0041: stloc.0 IL_0042: ldc.i4.m1 IL_0043: conv.i8 IL_0044: stloc.0 IL_0045: ldc.i4.s -2 IL_0047: conv.i8 IL_0048: stloc.0 IL_0049: ldc.i4.s -128 IL_004b: conv.i8 IL_004c: stloc.0 IL_004d: ldc.i4 -129 IL_0052: conv.i8 IL_0053: stloc.0 IL_0054: ldc.i4 -2147483648 IL_0059: conv.i8 IL_005a: stloc.0 IL_005b: ldc.i8 -2147483649 IL_0064: stloc.0 IL_0065: ret } // end of method '<Program>$'::'<Main>$' } // end of class <Program>$C言語: ソースコード
void test() { long a=0; a=1; a=2147483647L; a=2147483648L; a=4294967296L; }x64アセンブラ: x86-64 gcc 10.2
_Z4testv: test(): pushq %rbp movq %rsp, %rbp movq $0, -8(%rbp) movq $1, -8(%rbp) movq $2147483647, -8(%rbp) movl $2147483648, %eax movq %rax, -8(%rbp) movabsq $4294967296, %rax movq %rax, -8(%rbp) nop popq %rbp retx64アセンブラ: x86-64 gcc 4.1.2
_Z4testv: test(): pushq %rbp movq %rsp, %rbp movq $0, -8(%rbp) movq $1, -8(%rbp) movq $2147483647, -8(%rbp) movl $-2147483648, -8(%rbp) movl $0, -4(%rbp) movl $0, -8(%rbp) movl $1, -4(%rbp) leave ret同時公開のAdvent Calendar記事「【C#】 演算子のオーバーロードで遊ぶ」もどうぞ。
正数
0~8 : ldc.i4.*+conv.i8
パターン1: ldc.i4.*+conv.i8
long value = 1L;1の代入はこうなります。
IL_0000: ldc.i4.1 IL_0001: conv.i8 IL_0002: stloc.0よく分かりませんね。
1行目のldc.i4.1
というのは、ldc.i4.1
Push 1 onto the stack as int32 (0x17)だそうです。つまり、「スタックにint32で1をプッシュする」という操作です。
つまりdotnetの中間言語、MSIL(共通中間言語)には、「1をスタックにプッシュする」なんて命令があるんです。
-1~8まであります。2行目はそれをint64に変換する命令。
conv.i8
Convert to int64, pushing int64 on stack (0x6A)3行目はスタックから値を取り出して変数に代入する命令です。
stloc.0
Pop a value from stack into local variable 0 (0x0A)変数の型と名前もきちんと残っています。これのおかげでdotnetの逆コンパイルはローカル変数まで見えます。
.locals init ( [0] int64 'value' )最初は特定の値をスタックに積む専用命令があって、しかもわざわざ型変換をしてるのには驚きました。
おそらくメリットは即値にほぼ0の8バイトを消費するより容量が小さくて済むことでしょう。
実在のCPUでこんな命令はなさそうです(というかCPUが型を認識したりしない)。
さらに言えば、アセンブラっぽいのに変数という概念があるのも不思議ですが、MSILってのはこういうものみたいです。long value = 8L;なお8の代入はこうなります。-1~8まで同様。
IL_0000: ldc.i4.8 IL_0001: conv.i8 IL_0002: stloc.0C言語
ちなみにCでは最初に書いた通りこうなります(x64)。
movq $1, -8(%rbp)movqってのは
Move Quadword
で8バイトの値のコピーです。
%rbp
というのはベースポインタ、つまり「rbpは関数内においてスタック領域を扱う処理の基準」だそうです。
ヒープはプラス方向に、スタックはマイナス方向に延びるので+8ではなく-8。
普通ですね。ちなみにこの一行で8バイトの命令になります。
なぜ8バイトの値をコピーするのに8バイトで済むのか疑問ですが、-8(%rbp)
みたいな場合の即値は4バイトだからのようです。2147483647までは特に面白い事は起きません。
ちなみにC#のJITコンパイル結果 (x86)はこうなります。
ちなみにC#のJITコンパイル結果 (x86)はこうなります。
sharplab.ioでJIT Asmを選択した結果です。x64版は今のところなさそうです。L001f: mov dword ptr [ebp-0x10], 1 L0026: mov eax, [ebp-0x10] L0029: mov edx, [ebp-0x10] L002c: sar edx, 0x1f L002f: mov [ebp-0xc], eax L0032: mov [ebp-8], edxx86のmovはx64と引数の順序が逆なこと(2番目の引数を1番目の場所に代入)、リトルエンディアンなのでメモリアドレスが多い方に上位ビットが来ることに注意してください。
ebpはrbpの32ビット版です(x64にもあります)。4バイト程度ずれてるようですが気にしないでおきます。SARは
Shift Arithmetic Right
で算術右シフトのことです。すなわち正の場合は0で負の場合は1でシフトしたビットをセットするビットシフトです。
つまりsar edx, 0x1f
は正の場合は0に、負の場合は0xffffffff
(-1)になるわけです。
conv.i8
の指示通り、律儀にintからlongに変換しているわけですね。
やたら行数が多いですし、わざわざメモリを経由する必要があるのか、定数なんだからsar
使わずにxor
とかで0を作れば良さそうですし、色々謎ですね。
まぁJITですし、ILを複数行見るような処理はやらない設定なんでしょう多分。intの場合は普通に1行になります。
32ビットCPUで64ビット型を使うのは、単なるストアの時点から命令が増えて遅いだろうなという話ですね。L001c: mov dword ptr [ebp-8], 1参考文献
- yone-ken (2007) 「[IL6] 定数のロードあれこれ」 KEN's .NET
- yone-ken (2007) 「[IL13] 型と型変換」 KEN's .NET
- @tobira-code (2016) 「x86-64プロセッサのスタックを理解する」 qiita
- 「X86アセンブラ/x86アーキテクチャ」 WIKIBOOKS
- @Nina_Petipa「Tips IA32(x86)汎用命令一覧 Sから始まる命令 SAL/SAR/SHL/SHR命令 」 0から作るソフトウェア開発 日々勉強中。。。
- Intel (2004) 「IA-32 インテル®アーキテクチャソフトウェア・デベロッパーズ・マニュアル 上巻」 7-14ページ
9~127 : ldc.i4.s+conv.i8
パターン2: ldc.i4.s+conv.i8
long a = 8L;9からは即値を使います。ただし、即値は1バイトです。
IL_0009: ldc.i4.s 9 IL_000b: conv.i8 IL_000c: stloc.0
ldc.i4.s
はint8の即値をint32としてスタックにプッシュする命令です。これは127まで続きます。
地味にメモリの節約になりそうです。ldc.i4.s
Push num onto the stack as int32, short form (0x1F )128~2147483647 : ldc.i4+conv.i8
パターン3: ldc.i4+conv.i8
long value = 8L;128からはint32の即値をスタックにプッシュする命令を使うようになります。
これは2147483647(int.MaxValue
)まで続きます。IL_0011: ldc.i4 128 IL_0016: conv.i8 IL_0017: stloc.0ldc.i4
Push num of type int32 onto the stack as int32 (0x20 )2147483648~4294967167 : ldc.i4+conv.u8
パターン4: ldc.i4+conv.u8
long value = 2147483648L; //0b100000000000000000000000000000002147483648では負数が出てきます。可読性がぐんと下がります。
IL_001f: ldc.i4 -2147483648 IL_0024: conv.u8 IL_0025: stloc.0たまたま符号のあるなしだけみたいになってますが、2147483649だと-2147483647です。
conv.u8
はその名の通りunsigned int64
に変換する命令です。
負数を符号なし整数型に変換することで大きな値を得ているわけですね。conv.u8
Convert to unsigned int64, pushing int64 on stack (0x6E)C言語
C言語ではコンパイラーによって結果が異なるようです。
x86-64 gcc 4.1.2
movl $-2147483648, -8(%rbp) movl $0, -4(%rbp)4バイトずつ代入しています。直観的です。
ちなみにこの2行で14バイトになります。x86-64 gcc 10.2 / x86-64 clang 11.0.0
movl $2147483648, %eax movq %rax, -8(%rbp)EAXは、RAXに使っているレジスタの下位32ビットのことです。代入すると上位32ビットもクリアされる…のだと思います。
下位32ビットに代入し、64ビットコピーすることで目的の値を得ているわけですね。
この2行で9バイトです。上より5バイト節約、1を代入するだけより1バイト多いだけです。
コンパイラが進歩してるんだなぁと感じます。メモリの節約はCPUキャッシュなどでも大事です。ちなみにC#のJITコンパイル結果 (x86)はこうなります。
L001f: mov eax, 0x80000000 L0024: xor edx, edx L0026: mov [ebp-0xc], eax L0029: mov [ebp-8], edx
xor edx, edx
は0を作っているだけです。
4バイトずつ書き込んでるだけですね。4294967168~4294967294 : ldc.i4.s+conv.u8
パターン5: ldc.i4.s+conv.u8
long value = 4294967168L; //0b111111111111111111111111100000004294967168は何の変哲もない数字に見えます。どうなるか予想できますか。
IL_002d: ldc.i4.s -128 IL_002f: conv.u8 IL_0030: stloc.0そうです
ldc.i4.s
の復活です。
見た目が変わり、(コンパイル結果が)短くなるだけでそんなに意味はないですね。4294967295 : ldc.i4.m1+conv.u8
パターン6: ldc.i4.m1+conv.u8
long value = 4294967295L;//0b11111111111111111111111111111111上で書きましたが、-1だけは特別な命令があるのでこうなります。
IL_0035: ldc.i4.m1 IL_0036: conv.u8 IL_0037: stloc.0ldc.i4.m1
Push -1 onto the stack as int32 (0x15)4294967296~ : ldc.i8
パターン7: ldc.i8
long value = 4294967296L;//0b100000000000000000000000000000000これ以降は普通に8バイトの即値をストアするだけの簡単な作業です。
IL_0038: ldc.i8 4294967296 IL_0041: stloc.0ldc.i8
Push num of type int64 onto the stack as int64 (0x21 )C言語
C言語ではこうなります。
x86-64 gcc 4.1.2
movl $0, -8(%rbp) movl $1, -4(%rbp)こっちはいっしょ。
x86-64 gcc 10.2 / x86-64 clang 11.0.0
movabsq $4294967296, %rax movq %rax, -8(%rbp)こちらは
movabsq
という長い名前の命令が出てきます。
単純に64ビットの即値を取るmovですね。恐怖の10バイト命令です。
2行で14バイトも使います。負数
では0からマイナス方向へ行きましょう。
ちなみに「-2~-128」のように絶対値の順に書きます。
基本、正数と同じなのでサクサク紹介していきましょう。-1 : ldc.i4.*+conv.i8
パターン1: ldc.i4.*+conv.i8
long value = -1;IL_0042: ldc.i4.m1 IL_0043: conv.i8 IL_0044: stloc.0
ldc.i4.m1
という命令があります。-2以降はありません。
既に書きましたね。-2~-128 : ldc.i4.s+conv.i8
パターン2: ldc.i4.s+conv.i8
long value = -2;IL_0045: ldc.i4.s -2 IL_0047: conv.i8 IL_0048: stloc.0「9~127」と同じ。
2の補数表現なので絶対値が1ずれます。-129~-2147483648 : ldc.i4+conv.i8
パターン3: ldc.i4+conv.i8
long value = -129;IL_004d: ldc.i4 -129 IL_0052: conv.i8 IL_0053: stloc.0「128~2147483647」と同じ。
-2147483649~ : ldc.i8
パターン7: ldc.i8
long value = -2147483649L;IL_005b: ldc.i8 -2147483649 IL_0064: stloc.0「4294967296~」と同じ。
負数をconv.u8
にするテクニックが使えないので、8バイトの即値を持つ命令を正数より広い範囲で使わざるを得ません。感想
単なる定数のストアにこんなに種類があるのは驚きでした。7パターンもあります。
出力されるILのサイズをかなり真剣に削っているようです。
さらに-1~8の為の専用命令ってのも不思議ですね。驚きました。
一方で、JITコンパイルの結果はちょっと見た限りではあまり洗練されていないように感じました。MSILはちょくちょく読みはするんですが、これくらい読み込む機会はないので新鮮でした。
普通のアセンブラもですね。
アセンブラ初心者なので用語とか知識とか間違っていれば申し訳ないです。以前似た話を(岩永さんの?)ツイートで見たような記憶があるので既出かもしれません。
ただ知ってる人が多いわけでもなさそうなので良いんじゃないでしょうか。これは元々Advent Calendar用の企画で演算子オーバーロードの調査をしている時に見つけたのですが、面白かったので記事にしたものです。
それなりの分量になったので、本題が間に合わなさそうならこっちを代わりにします。公開時期も遅らせときます。表
命令長/パターンは以下になります。
数値 パターン 命令長(合計) 命令 -9,223,372,036,854,775,808 7 9 ldc.i8 -2,147,483,649 7 9 ldc.i8 -2,147,483,648 3 6 ldc.i4+conv.i8 -129 3 6 ldc.i4+conv.i8 -128 2 3 ldc.i4.s+conv.i8 -2 2 3 ldc.i4.s+conv.i8 -1 1 2 ldc.i4.m1+conv.i8 8 1 2 ldc.i4.8+conv.i8 9 2 3 ldc.i4.s+conv.i8 127 2 3 ldc.i4.s+conv.i8 128 3 6 ldc.i4+conv.i8 2,147,483,647 3 6 ldc.i4+conv.i8 2,147,483,648 4 6 ldc.i4+conv.u8 4,294,967,167 4 6 ldc.i4+conv.u8 4,294,967,168 5 3 ldc.i4.s+conv.u8 4,294,967,294 5 3 ldc.i4.s+conv.u8 4,294,967,295 6 2 ldc.i4.m1+conv.u8 4,294,967,296 7 9 ldc.i8 9,223,372,036,854,775,807 7 9 ldc.i8 おまけ
C#上でILを手軽に触る方法もあります。
using System.Reflection.Emit; using System.Reflection; DynamicMethod method = new DynamicMethod("DynamicMethod", typeof(void), Type.EmptyTypes); ILGenerator il = method.GetILGenerator(); il.DeclareLocal(typeof(long)); il.Emit(OpCodes.Ldc_I4_S, (sbyte)-128); il.Emit(OpCodes.Conv_U8); il.Emit(OpCodes.Stloc_0); il.Emit(OpCodes.Ldloc_0); il.Emit(OpCodes.Call, typeof(Console).GetMethod("WriteLine", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.Public, null, new Type[] { typeof(Int64) }, null)); il.Emit(OpCodes.Ret); Action action = (Action)method.CreateDelegate(typeof(Action)); action();
- aharisu (2009) 「DynamicMethodで末尾再帰」 aharisuのごみ箱
- 投稿日:2020-12-18T09:53:58+09:00
【C#】Enumでよく使うけど忘れがちなもののメモ
はじめに
毎回「あれ、どう書くんだっけ」と調べているので、まとめておこうと思います。
// 各サンプルコードで使用するEnum public enum Seasons { Spring, Summer, Autumn, Winter }Enumにメソッドを実装(拡張メソッド)
public static class SeasonsExt { public static string GetJapaneseName(this Seasons param) { switch (param) { case Seasons.Spring: return "春"; case Seasons.Summer: return "夏"; case Seasons.Autumn: return "秋"; case Seasons.Winter: return "冬"; } } } // 使い方 var season = Seasons.Spring; var japaneseName = season.GetJapaneseName();定義されている値かどうかチェック
Console.WriteLine(Enum.IsDefined(typeof(Seasons), 0)); // True Console.WriteLine(Enum.IsDefined(typeof(Seasons), 4)); // False Console.WriteLine(Enum.IsDefined(typeof(Seasons), "Spring")); // True Console.WriteLine(Enum.IsDefined(typeof(Seasons), "spring")); // False(大文字・小文字区別される)型変換
int ⇔ Enum変換
// int -> Enum System.Console.WriteLine((Seasons)0); // Spring System.Console.WriteLine((Seasons)4); // 4(定義されていない値でも例外は発生しない) // Enum -> int System.Console.WriteLine((int)Seasons.Winter); // 3string ⇔ Enum変換
// string -> Enum if (Enum.TryParse<Seasons>("Summer", out var season)) { System.Console.WriteLine(season); // Summer } // 数値の文字列も変換できる if (Enum.TryParse<Seasons>("1", out var season)) { System.Console.WriteLine(season); // Summer } // Enum -> string System.Console.WriteLine(Seasons.Winter.ToString()); // Winterint/string -> Enumの変換用メソッド作ってみた
なんだか少しまどろっこしいかんじになってしまいました。
public static T? ConvertToEnum<T>(object value) where T : struct { // TがEnumかどうかチェック // where T : Enum にすると、戻り値をNullableにできないので... if (!typeof(T).IsEnum) return null; string stringValue; switch (value) { case int intVal: stringValue = intVal.ToString(); break; case string stringVal: stringValue = stringVal; break; default: // int, string以外は処理対象外とする return null; } // TryParseする前に、定義されている値かチェックする // ※ 数値の場合、定義されていない値でもTryParseが成功してしまうため if (!Enum.IsDefined(typeof(T), value)) return null; if (Enum.TryParse<T>(stringValue, out var result)) { return result; } return null; }
- 投稿日:2020-12-18T00:41:46+09:00
C#でObserverパターンをきちんと理解して実装する
Observerパターンとは
Observerパターンとは、「クラスから通知を発行する仕組み」と、「他のクラスから発行された通知を受け取る仕組み」を実現するためのデザインパターンです。
このデザインパターンは、主にクラス間のデータのやり取りをするときに使われますが、以下のような状況において特に有用です。
- 双方向ではなく、一方からもう一方へと一方通行でデータを発行する場合。
- 1回ではなく、複数回データを発行する場合。
- 任意のタイミングでデータを発行したい場合。
- 発行されたデータを、複数のクラスが同時に受け取りたい場合。
特に1番はObserverパターンの必須要件で、双方向のデータのやり取りには使用できません。
ObserverとObservable
このデザインパターンでは、「データを受け取るクラス」と「データを発行するクラス」を明確に分けて考えます。
このうち、「データを受け取るクラス」のことを「Observer」、「データを発行するクラス」のことを「Observable」と呼びます。Observerとは、「監視者」や「観察者」といった意味の英単語で、データを発行するクラス(Observable)からの通知を受け取る(つまり、通知を観察する、監視する)ことからその名が付けられています。
対してObservableとは、Observerの-able系なので、「観察可能」といった意味になります。
Observableは「観察者」であるObserverにデータを発行するので、Observerから「観察されることができる」、「観察可能」といったことから、この名がつけられています。ObserverとObservableは非常に重要なので、以下のようにまとめます。
Observer Observable 単語の意味 観察者 観察可能
観察されることができるクラスの意味 通知を受け取るクラス 通知を発行するクラス 基本的な仕組み
Observerパターンの基本的な仕組みは、次の3ステップとなります。
- ObserverがObservableにデータの発行先として登録する(=購読する)
- Observableは登録されたすべての発行先に値を発行する
- Observerは受け取った値を使用して任意の処理を実行する
次の項から詳しく説明します。
1. ObserverがObservableを購読する
Observer(値を受信したいクラス)は、欲しい値を発行しているクラス(Observable)に対して、「僕に値を発行・通知してください」と自分自身を通知先として登録します。
この「配信登録」することを、「購読(Subscribe)する」と言います。現実世界で「AさんはS社の雑誌を定期購読する」といった表現はよく使われます。
この場合も、AさんはS社に対して「僕に雑誌を定期的に送ってください」と自分自身を送付先として登録しますね。それと同じような意味になります。そして、一つの値の発行元(Observable)に対して、複数のObserverが値の発行先として登録することができることも、Observerパターンの特徴の一つです。
2.Observableは登録されたすべての発行先に値を発行する
Observable(値を発行するクラス)は、発行する値ができると、値の発行先として登録されたすべてのObserverに対して一斉に値を発行します。
こうして、ObserverはObservableからの通知(値)を受け取ることができます。
3.Observerは受け取った値を使用して任意の処理を実行する
Observableから発行された値を受け取ったObserverは、受け取った値を使用して任意の処理を実行することができます。
このようにして、ObservableからObserverへとデータの受け渡しが実現されます。
Observerパターンを実装してみる
Observerパターンの基本的な仕組みの大枠は理解頂けたかと思います。
それでは、これらの仕組みを実際にどのように実装するかを説明していきます。
IObserver<T>
インターフェイスとIObservable<T>
インターフェイスObserverパターンは、「データを受け取るクラス」と「データを発行するクラス」を明確に分けて考えるデザインパターンと書きました。
実は、.NET Framework4.0以降には、Observerパターンで使えるIObserver<T>
インターフェイスとIObservable<T>
インターフェイスがSystem
名前空間に標準で用意されています。
Observerパターンを実装したい場合、これらのインターフェイスをデータの受信側、データの発行側のクラスにそれぞれ実装すれば良いことになります。これらのインターフェイスの定義は次のようになっています。
public interface IObserver<in T> { //データの発行が完了したことを通知する void OnCompleted(); //データの発行元でエラーが発生したことを通知する void OnError(Exception error); //データを通知する void OnNext(T value); } public interface IObservable<out T> { //データの発行を購読する IDisposable Subscribe(IObserver<T> observer); }
IObserver<T>
インターフェイスのOnNext
メソッドは、データを新しく通知する際に呼び出すメソッドです。
OnCompleted
メソッドは、データの発行がすべて完了し、これ以上通知するものがない場合に呼び出します。
万が一、データの発行元で何らかの例外が発生してしまった場合は、OnError
メソッドを呼び出して、エラーが発生したことを通知します。
IObservable<T>
インターフェイスのSubscibe
メソッドは、データの発行元からの通知を受け取りたいときに呼び出します。
引数には、通知の発行先となるIObserver<T>
オブジェクトを指定します。ここで、以下のような違和感を覚える方がいらっしゃるかもしれません。
IObserver<T>
はデータを受け取るクラスが実装するのに、なぜデータを発行する系のメソッドを実装するのか?
逆にIObservable<T>
はデータを発行するクラスが実装するのに、なぜデータの発行を購読するメソッドを実装するのか?その答えは、これらのメソッドは、実装するクラス自身が使用するのではなく、お互いに相手が使用するものだからです。
例えば、「データを受け取るクラス(Observer)」が実装した
IObserver<T>
インターフェイスのOnNext
メソッドは、データを発行するクラス(Observable)が呼び出すことで、Observerに対して値を通知することができます。
逆に、「データを発行するクラス(Observable)」が実装したIObservable<T>
インターフェイスのSubscribe
メソッドは、データを受け取るクラス(Observer)が呼び出すことで、自分自身をデータの発行先として登録することができます。まだ違和感が拭えないかもしれませんが、実際に実装してみると理解が深まるかもしれません。
それでは、実際にIObserver<T>
インターフェイスとIObservable<T>
インターフェイスを用いて、Observerパターンを実装してみます。Observerを実装する
まずはObserver(通知を受け取るクラス)を作成します。
1.クラスを作成する
public class Observer { }2.
IObserver<T>
インターフェイスを実装するこのクラスはObserverなので、
IObserver<T>
インターフェイスを実装します。
型引数T
には受信したい値の型を指定します。ここでは、int
型とします。public class Observer : IObserver<int> { public void OnCompleted() { throw new NotImplementedException(); } public void OnError(Exception error) { throw new NotImplementedException(); } public void OnNext(int value) { throw new NotImplementedException(); } }3.値を受け取ったときのコールバック処理を記述する
あとは、各メソッドにそれぞれ通知が来たときに実行したい処理を自由に記述します。
例なので、ここでは次のようにコンソールにメッセージを出力するだけの処理を実装しました。public class Observer : IObserver<int> { public void OnCompleted() { Console.WriteLine($"通知の受け取りが完了しました"); } public void OnError(Exception error) { Console.WriteLine($"次のエラーを受信しました:{error.Message}"); } public void OnNext(int value) { Console.WriteLine($"{value}を受け取りました"); } }これだけでObserverの実装は完了です。
複数のObserverを用意できるようにする
冒頭にも書きましたが、Observerパターンでは、発行された値を複数のクラス(Observer)が同時に受け取ることができます。
これを試すために、次のようにObserverの名前をコンストラクタで指定するようにして、どのObserverがメッセージを受け取ったかを識別できるようにしておきます。public class Observer : IObserver<int> { private string m_name; public Observer(string name) { m_name = name; } public void OnCompleted() { Console.WriteLine($"{m_name}が通知の受け取りを完了しました"); } public void OnError(Exception error) { Console.WriteLine($"{m_name}が次のエラーを受信しました:{error.Message}"); } public void OnNext(int value) { Console.WriteLine($"{m_name}が{value}を受け取りました"); } }Observableを実装する
次にObservable(通知を発行するクラス)を作成します。
1.クラスを作成する
public class Observable { }2.
IObservable<T>
インターフェイスを実装するこのクラスはObservableなので、
IObservable<T>
インターフェイスを実装します。
型引数T
には発行したい値の型を指定します。ObserverとObservableの型引数は合致している必要があるため、ここでもT
はint
とします。public class Observable : IObservable<int> { public IDisposable Subscribe(IObserver<int> observer) { throw new NotImplementedException(); } }3.値の発行先を覚えておく仕組みを作成する
Observableの役目は、値の発行先として登録された(購読された、
Subscribe
された)IObserver<T>
を記憶しておき、発行する値が生じたときに、そのすべてのIObserver<T>
に対して値を発行することです。
※「値を発行する」とは、具体的にはIObserver<T>
のOnNext
、OnCompleted
、もしくはOnError
メソッドを呼び出すことですしたがって、Observableなクラスに
Subscribe
で指定されたIObserver<int>
を覚えておける仕組みを作成する必要があります。
とはいっても、単にList<IObserver<T>>
に溜めておくだけで十分です。public class Observable : IObservable<int> { //購読されたIObserver<int>のリスト private List<IObserver<int>> m_observers = new List<IObserver<int>>(); public IDisposable Subscribe(IObserver<int> observer) { if(!m_observers.Contains(observer)) m_observers.Add(observer); } }これで、
Subscribe
で指定されたIObserver<int>
を覚えておくことができるようになりました。4.購読解除用の
IDisposable
なクラスを用意するところで、
Subscribe
メソッドの戻り値はIDisposable
になっています。
今まで説明しませんでしたが、Observerパターンは、値の発行先として登録するSubscribeの対となる機能として、値の発行を停止してもらう「購読解除」も可能となっています。
Subscribe
メソッドの戻り値として返すIDisposable
は、Subscribe
したObserverが、「もう値はいらないです」と購読を解除するときに使用するものです。
ご存知の通り、IDisposable
インターフェイスの中身はDispose
メソッドただ一つのみであり、購読を解除したいObserverはこのIDisposable
をDispose
することによって購読を解除することができます。したがって、
Dispose
されたときに購読を解除する仕組みを作成する必要があります。
「購読を解除する」とは、Observableなクラス視点で言えば、値の発行先リストであるList<IObserver<T>>
から購読を解除したいIObserver<T>
をRemove
することに他なりません。では、どのように実装すればよいでしょうか?
まず、
IDisposable
インターフェイスを返さなければなりませんから、当然、IDisposable
インターフェイスを実装したクラスが必要になります。
そこで、以下のように「購読を解除する責務を持ったIDisposable
なクラス」を作成します。class Unsubscriber : IDisposable { public void Dispose() { throw new NotImplementedException(); } }そして、この
Dispose
メソッドの中に、購読者リストからRemove
する処理を書けば完成です。
具体的には、発行先リストList<IObserver<T>>
とDispose
されたときにRemove
するターゲットとなるIObserver<T>
を、コンストラクタで引き渡して、以下のように実装します。class Unsubscriber : IDisposable { //発行先リスト private List<IObserver<int>> m_observers; //DisposeされたときにRemoveするIObserver<int> private IObserver<int> m_observer; public Unsubscriber(List<IObserver<int>> observers, IObserver<int> observer) { m_observers = observers; m_observer = observer; } public void Dispose() { //Disposeされたら発行先リストから対象の発行先を削除する m_observers.Remove(m_observer); } }そして、この新しく作った購読解除用の
IDisposable
なクラスのインスタンスを、Subscribe
の戻り値として返します。public class Observable : IObservable<int> { //購読されたIObserver<int>のリスト private List<IObserver<int>> m_observers = new List<IObserver<int>>(); public IDisposable Subscribe(IObserver<int> observer) { if(!m_observers.Contains(observer)) m_observers.Add(observer); //購読解除用のクラスをIDisposableとして返す return new Unsubscriber(m_observers, observer); } }こうすることで、この
IDisposable
を受け取ったObserverは、値が要らなくなった時点でDispose
メソッドを呼び出すことで、自分自身を発行先リストから削除することができるようになります。ところで、この
Unsubscriber
クラスは、Observable
クラス以外から生成されることはありません。
したがって、下記のようにObservable
クラスの内部クラスにしてしまいます。public class Observable : IObservable<int> { //購読されたIObserver<int>のリスト private List<IObserver<int>> m_observers = new List<IObserver<int>>(); public IDisposable Subscribe(IObserver<int> observer) { if(!m_observers.Contains(observer)) m_observers.Add(observer); //購読解除用のクラスをIDisposableとして返す return new Unsubscriber(m_observers, observer); } //購読解除用内部クラス private class Unsubscriber : IDisposable { //発行先リスト private List<IObserver<int>> m_observers; //DisposeされたときにRemoveするIObserver<int> private IObserver<int> m_observer; public Unsubscriber(List<IObserver<int>> observers, IObserver<int> observer) { m_observers = observers; m_observer = observer; } public void Dispose() { //Disposeされたら発行先リストから対象の発行先を削除する m_observers.Remove(m_observer); } } }5.通知を発行する処理を記述する
最後に、発行したい情報や値があった場合に通知を発行する処理を記述します。
ここでは例として、SendNotice
メソッドが呼ばれたときに“int”型の1,2,3を連続で通知を発行するようにします。値を発行するには、
OnNext
メソッドを呼び出します。
すべての発行先に対してOnNext
メソッドを呼び出すので、以下のようにforeach
を使用すればOKです。public class Observable : IObservable<int> { //購読されたIObserver<int>のリスト private List<IObserver<int>> m_observers = new List<IObserver<int>>(); public IDisposable Subscribe(IObserver<int> observer) { if(!m_observers.Contains(observer)) m_observers.Add(observer); //購読解除用のクラスをIDisposableとして返す return new Unsubscriber(m_observers, observer); } public void SendNotice() { //すべての発行先に対して1,2,3を発行する foreach (var observer in m_observers) { observer.OnNext(1); observer.OnNext(2); observer.OnNext(3); } } //購読解除用内部クラス private class Unsubscriber : IDisposable { //発行先リスト private List<IObserver<int>> m_observers; //DisposeされたときにRemoveするIObserver<int> private IObserver<int> m_observer; public Unsubscriber(List<IObserver<int>> observers, IObserver<int> observer) { m_observers = observers; m_observer = observer; } public void Dispose() { //Disposeされたら発行先リストから対象の発行先を削除する m_observers.Remove(m_observer); } } }使ってみる
これまで作成したObserverとObservableを使用して、実際に値の購読と発行、受け取りの流れを実演します。
ここでは、以下のようなコンソールアプリケーションを作成しました。
class Program { static void Main(string[] args) { //値を受け取るクラスを3つ作成 Observer observerA = new Observer("Aさん"); Observer observerB = new Observer("Bさん"); Observer observerC = new Observer("Cさん"); //値を発行するクラスを作成 Observable observable = new Observable(); //3つのObserverが、自分自身を発行先として登録する(=購読) IDisposable disposableA = observable.Subscribe(observerA); IDisposable disposableB = observable.Subscribe(observerB); IDisposable disposableC = observable.Subscribe(observerC); Console.WriteLine("Aさん〜Cさんが値を購読しました"); Console.WriteLine("値を発行させます"); //Observableに値を発行させる observable.SendNotice(); Console.WriteLine("Aさんが購読解除します"); //Aさんが購読解除する disposableA.Dispose(); Console.WriteLine("値を発行させます"); //再び値を発行させる observable.SendNotice(); Console.WriteLine("Bさんが購読解除します"); //Bさんが購読解除する disposableB.Dispose(); Console.WriteLine("値を発行させます"); //再び値を発行させる observable.SendNotice(); Console.ReadKey(); } }やっている内容としては、コードを見たとおりですが
- 値の購読者(Observer)をAさん、Bさん、Cさんの3つを作成
- 値の発行者(Observable)を作成
- Aさん〜CさんがObservableを購読
- Observableに値を発行させる
- Aさん、購読を解除する
- Observableに再び値を発行させる
- Bさんも購読を解除する
- Observableに再び値を発行させる
となります。
これを実行すると、次のような出力が得られます。
Aさん〜Cさんが値を購読しました 値を発行させます Aさんが1を受け取りました Aさんが2を受け取りました Aさんが3を受け取りました Bさんが1を受け取りました Bさんが2を受け取りました Bさんが3を受け取りました Cさんが1を受け取りました Cさんが2を受け取りました Cさんが3を受け取りました Aさんが購読解除します 値を発行させます Bさんが1を受け取りました Bさんが2を受け取りました Bさんが3を受け取りました Cさんが1を受け取りました Cさんが2を受け取りました Cさんが3を受け取りました Bさんが購読解除します 値を発行させます Cさんが1を受け取りました Cさんが2を受け取りました Cさんが3を受け取りましたObserverパターンが期待通りに動作していることが確認できます。
さいごに
以上、少し長くなってしまいましたがObserverパターンの解説を行いました。
僕自身としては、Observerパターンをこのようにそのまま利用することは少なく、ReactiveExtensionsというObserverパターンをベースとした非常に強力なライブラリを通じて利用することがほとんどです。
もともとは、このReactiveExtensionsの記事を書いていたのですが、ベースとなるObserverパターンの説明がとても長くなってしまったため、別記事として抜き出しました。
またReactiveExtensionsの記事も書いたらリンクを貼るのでそちらも良ければ御覧ください。何か間違いやご指摘、ご質問あればコメントにお願いします。
- 投稿日:2020-12-18T00:09:08+09:00
C#とMobile Blazor BindingsでWindowsとMac用の動画ビューワーを作成したお話
この記事は C# Advent Calendar 2020 12月18日分の記事です。
自己紹介
はじめまして。
kawaと申します。
主にゲーム系のプログラマーとして働いてきました。 (お仕事募集中なので何かお話などいただけると助かります)
業務ではNintendo 3DS・WiiU・Nintendo Switch向けソフトの開発やスマートフォン向けアプリの開発を行ってきました。
また、プライベートではC#でアプリケーションを作ったり、Unityをいじって動画やゲームを作ったりしていました。
一通り使用できる言語はC#, C++, ちょっとできるのはJava, javascript, PHPなどですが、言語の中ではC#が特に好きです。
Twitterプロフィール
Githubプロフィールやったこと
C#とBlazorでホロライブファン向けの動画ビューワー『ホロビューワー』を開発しました
こちらの記事にある通り、Blazorを使用してWindows/Macで動作する動画ビューワーを作成しました。
こちらの記事も読んでいただけると嬉しいです。なぜやった
(上記の記事と一部重複しますが)
- Blazorの勉強がしたかった
- C#とHTMLが組み合わせられるとか面白そう
- C#に関連する新しいものなのでとりあえず触ってみたかった
- WindowsとMac両対応のアプリケーションを作成してみたかった
- 開発で苦労するポイントなどを知って経験値を貯めたかった
- 邪な考えとして、アプリケーションを使ってもらえる可能性のある層を広げたかった
Mobile Blazor Bindingsとは
リポジトリページ
ドキュメントページ
要はBlazorとXamarinを組み合わせることでWebアプリケーションだけじゃなくて、Windows・Mac・Android・iOSアプリを開発できるようにするフレームワークだよ とのことです。注意
リポジトリページやドキュメントページにも書かれている通りMobile Blazor Bindingsは「Experimental」日本語に訳すと「実験的」なフレームワークです。
なので、機能的にもまだまだ足りないところや不具合と思われる動作などが色々と見られます。
もしご利用の際にはその点にご注意ください。やってみた感想やBlazorについての印象など
- C#を混ぜてWebページを記述できるのでBlazor最高
- 実験的なフレームワークでも割となんとかなる (↓なんとかなったり、ならなかった例)
- 将来的にBlazorが色々なところで使われるようになってほしい (100%個人的願望)
- ただ、VisualStudioでの開発中にソースコードのインデントがちょくちょくバグることがあったので、そこが直ってくれると嬉しい
あまり内容が無く、宣伝メインの記事になってしまい、すみません。
明日はneueccさんの「ソースジェネレーター Part2」とのことです。