20201218のC#に関する記事は11件です。

【.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 でカスタムコントロールを作成しています。

お薦めは①の方になります。「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 Class

MultiSelectComboBox.png

Wpf

お薦めは②の方になります。①と②は作者は同じなんですが、②の方が記事の更新が新しくコンボボックスの右側に「▼」が追加されています。
③は、リストではないですが複数の値を選択できるということ挙げました。

MultiSelectComboBoxWpf.png

①のコメント欄にあった、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.cs
using 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));
        }
    }
}

最後に

思ったより日本語で検索しても見つからないもんですね。見つかりやすいタイトルにしてみました。

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

真っ新な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

ステップ

  1. Unityのインストール
  2. Visual Studio Codeのインストール
  3. Unityの起動と設定
  4. Visual Studio Codeの起動と設定
  5. MacBookAir向けにビルドして動作確認
  6. Android向けにビルドして動作確認
  7. iPhone向けにビルドして動作確認

1.Unityのダウンロードとインストール

  1. 以下からUnity Hubをダウンロードし、UnityHubSetup.dmgを実行してインストール
  2. UnityHubを起動してアカウントの設定(Unityアカウントを持っていなければ作成)
  3. UnityHubからUnity2019.4.16f1をインストール
    1. Visual Studio for Mac をアンチェック
    2. Android Build Supportにチェック
    3. iOS Build Supportにチェック
    4. ドキュメンテーションをアンチェック
    5. 日本語をチェック

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をエディタに設定する

  1. 適当な名前でプロジェクトを作成
  2. Unity > Preferences… > External Tools から External Script EditorにVisual Studio Codeを選択
  3. 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持っていないので、家族に借りてこんどやってみる

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

UnityのDebug機能を便利にしたい!紹介編

現状のUnityEngine.Debugの問題

皆さんUnityのDebugクラスよく使いますよね? Debug.Log()とか。
でもたまに不便だな~って思う時ありません?

配列を表示したい時とか

        Debug.Log(new string[] {"a", "b", "c"});

image.png
↑配列なことぐらいわかってるわ!要素が欲しいんじゃ!
こういう場合は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);
    }

image.png

Dictionary

    void Start() {
        Dictionary<string, int> dict1 = new Dictionary<string, int>() {
            {"normalSword", 5},
            {"superSword", 2},
            {"apple", 100},
            {"water", 1}
        };

        DebugEx.Log(dict1);
    }

image.png

Dictionaryの冒頭2行は、KeyとValueの型情報を表示してくれています。
突っ込むだけで、簡単に要素を表示してくれるのでかなり便利になりました。

しかし、筆者の表示形式のセンスが無いので、あまり統一感がありません。
この表示形式は、これからのアップデートで改善していく予定です。

2. ログのテキスト保存

DebugEx.RecordStart();
処理
DebugEx.RecordStop();

image.png

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. その他設定

image.png
LogSavePath: ログファイルを保存するパス
SaveStackTrace: ログの下の部分にくっついている、呼び出し元のトレース情報をログファイルに書き込むか
Dictionary Key,Value: Dictionaryをコンソールに表示するときの色

最後に

結構頑張ればもっと使いやすくなると思うので、これからブラッシュアップしていけたらなと思います。
リリースできるようになれば、アセットストアなどに出す予定です。

!!告知!!
現在、チームでShoutousというFPSゲームを開発しています!

~Soutousとは?~
◉大規模な現代戦を題材にした
◉どんな遊び方でも皆活躍できる
◉今のFPSの良いとこ取りをした
◉e-Sports味の全くない、貴方の為の
◉意識低い系高画質お祭りFPSゲーム!

公式ツイッターのフォローよろしくお願いいたします!
Twitter: https://twitter.com/ShoutousJP

3Dデザイナーや、Unityエンジニアなども募集しているので、興味があればぜひTwitterID:@harumar0nにDMいただけるとありがたいです!

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

よく忘れる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);

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

借金の利息がバーチャルペットになって画面を散歩するアプリを作ります

この記事は

「クソアプリ2 Advent Calendar 2020」の19日目の記事となります。

はじめに

親の教えとして「何事もポジティブに」と教育を受けてきました。
おかげか大体のことはポジティブに考えることがきました。

しかし、世の中には無理じゃね?というものもあります。

       借 金

どうポジティブにとらえたらいいんだ。。。
負のものしか含まれていないぞ。
ポケ〇ンをやりながら考えていました。

モ、モンスターにおきかえてみるか。

コンセプト

借金をするほど強くなる(ポジティブに見える)アプリをつくる。

開発ツール

Unity

生まれたもの

7.gif

タイトル通り散歩します、借金が。

5千兆円借金してみる

7.gif

う、うわああああああああああ。

色々とバグってますがあれです。
クソアプリなんで許して。

最低限の借金返済アプリの体裁は一応整えていたりします

モンスターをタッチすると借金返済できます。
ギリギリ使えるレベルにするのもクソアプリ製作の楽しいところですよね。
8.gif

実装

カレンダーの実装は下サイトから頂戴しました
Unityのカレンダーアセット作った!-alberttecの日記
今回のクソアプリで見た目部分も着手できたのはカレンダー配布がめちゃ大きいです。神配布感謝です。

キャラクターはアセットストアで購入(7$)
Pixel Mobs-UnityeAssetStore

キャラクターの移動
SetNextPosition()で移動先を指定してLerpで移動。
移動が完了したらRecenter()で移動先を再指定です。

mob.cs
void 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字以内で検索ワードを記述できるようです。

実際に記述したもの

ギャンブル,サラ金,デビット,プロミス,モビット,リボ,リボモン,リボルビング,レイク,ローン,借入,借金,債務,元金,利子,利息,残高,消費者金融,計算,負債,返済,金利

この世の地獄みたいなキーワードランキングがあったら上位に食い込めそうです。

最後に

借金が増えるほどモンスターがいっぱいになっていきます。
なんか楽しい気持ちになってきた気がします。
これで少しは借金をポジティブに考えられるようになった気がします。
わーい。
めでたしめでたし。

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

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の技術的な取り組みに興味を持った方は、是非フォローお願いします。

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

セッション管理とスレッドモデル(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>

結果

04.jpg

ko0.aspx が処理されてから、ko1.aspxが処理されているのがわかる。

時々、セッションがko[0|1].aspxに引き継がれない場合もある...(その時は、個別のセッションなので、ko[0|1].aspxは同時並行に処理される)

つまり

同一セッションで大量にアクセスしても・・・早く終わるわけではない、ということ

考えてみたら・・・

Sessionオブジェクトに複数スレッドから同時アクセスを制御するようなロック処理がないのだから当然といえば当然か・・・

このあたりはClassicASPと互換性(sessionオブジェクトには排他制御がないけど、Applicationオブジェクトにはある・・・とか)を維持しているのかもしれない。

戻る

セッション管理とスレッドモデル

以上

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

【逆アセ】 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
        ret

x64アセンブラ: 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.0

C言語

ちなみに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版は今のところなさそうです。

image.png

    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], edx

x86の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

参考文献

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.0

ldc.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; //0b10000000000000000000000000000000

2147483648では負数が出てきます。可読性がぐんと下がります。

        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; //0b11111111111111111111111110000000

4294967168は何の変哲もない数字に見えます。どうなるか予想できますか。

        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.0

ldc.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.0

ldc.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バイトも使います。

image.png

負数

では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

image.png

おまけ

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();
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【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);  // 3

string ⇔ 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());  // Winter

int/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;
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

C#でObserverパターンをきちんと理解して実装する

Observerパターンとは

Observerパターンとは、「クラスから通知を発行する仕組み」と、「他のクラスから発行された通知を受け取る仕組み」を実現するためのデザインパターンです。
このデザインパターンは、主にクラス間のデータのやり取りをするときに使われますが、以下のような状況において特に有用です。

  1. 双方向ではなく、一方からもう一方へと一方通行でデータを発行する場合
  2. 1回ではなく、複数回データを発行する場合
  3. 任意のタイミングでデータを発行したい場合
  4. 発行されたデータを、複数のクラスが同時に受け取りたい場合

特に1番はObserverパターンの必須要件で、双方向のデータのやり取りには使用できません。

ObserverとObservable

このデザインパターンでは、「データを受け取るクラス」と「データを発行するクラス」を明確に分けて考えます。
このうち、「データを受け取るクラス」のことを「Observer」、「データを発行するクラス」のことを「Observable」と呼びます。

Observerとは、「監視者」や「観察者」といった意味の英単語で、データを発行するクラス(Observable)からの通知を受け取る(つまり、通知を観察する、監視する)ことからその名が付けられています。
対してObservableとは、Observerの-able系なので、「観察可能」といった意味になります。
Observableは「観察者」であるObserverにデータを発行するので、Observerから「観察されることができる」、「観察可能」といったことから、この名がつけられています。

ObserverとObservableは非常に重要なので、以下のようにまとめます。

Observer Observable
単語の意味 観察者 観察可能
観察されることができる
クラスの意味 通知を受け取るクラス 通知を発行するクラス

基本的な仕組み

Observerパターンの基本的な仕組みは、次の3ステップとなります。

  1. ObserverがObservableにデータの発行先として登録する(=購読する)
  2. Observableは登録されたすべての発行先に値を発行する
  3. Observerは受け取った値を使用して任意の処理を実行する

次の項から詳しく説明します。

1. ObserverがObservableを購読する

Observer(値を受信したいクラス)は、欲しい値を発行しているクラス(Observable)に対して、「僕に値を発行・通知してください」と自分自身を通知先として登録します
この「配信登録」することを、「購読(Subscribe)する」と言います。

現実世界で「AさんはS社の雑誌を定期購読する」といった表現はよく使われます。
この場合も、AさんはS社に対して「僕に雑誌を定期的に送ってください」と自分自身を送付先として登録しますね。それと同じような意味になります。

Observerパターン.png

そして、一つの値の発行元(Observable)に対して、複数のObserverが値の発行先として登録することができることも、Observerパターンの特徴の一つです。

Observerパターン_複数登録.png

2.Observableは登録されたすべての発行先に値を発行する

Observable(値を発行するクラス)は、発行する値ができると、値の発行先として登録されたすべてのObserverに対して一斉に値を発行します
こうして、ObserverはObservableからの通知(値)を受け取ることができます。
Observerパターン_値の発行.png

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の型引数は合致している必要があるため、ここでもTintとします。

public class Observable : IObservable<int>
{
    public IDisposable Subscribe(IObserver<int> observer)
    {
        throw new NotImplementedException();
    }
}

3.値の発行先を覚えておく仕組みを作成する

Observableの役目は、値の発行先として登録された(購読された、Subscribeされた)IObserver<T>を記憶しておき、発行する値が生じたときに、そのすべてのIObserver<T>に対して値を発行することです。
※「値を発行する」とは、具体的にはIObserver<T>OnNextOnCompleted、もしくは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はこのIDisposableDisposeすることによって購読を解除することができます。

したがって、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();
    }
}

やっている内容としては、コードを見たとおりですが

  1. 値の購読者(Observer)をAさん、Bさん、Cさんの3つを作成
  2. 値の発行者(Observable)を作成
  3. Aさん〜CさんがObservableを購読
  4. Observableに値を発行させる
  5. Aさん、購読を解除する
  6. Observableに再び値を発行させる
  7. Bさんも購読を解除する
  8. 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の記事も書いたらリンクを貼るのでそちらも良ければ御覧ください。

何か間違いやご指摘、ご質問あればコメントにお願いします。

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

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についての印象など


あまり内容が無く、宣伝メインの記事になってしまい、すみません。

明日はneueccさんの「ソースジェネレーター Part2」とのことです。

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