- 投稿日:2021-01-14T22:12:01+09:00
ObservableCollection を ObservableDictionary 的に使用する拡張メソッド
何番煎じか解りませんが、ObservableCollection をディクショナリー的に使用する拡張メソッド。てか、有名どころのライブラリに含まれてそう。
こんな使い方ができるようにする。var dictionary = new ObservableCollection<KeyValuePair<string, object>>(); dictionary.Add("aaa",new object()); dictionary.Remove("aaa");以下、コード。
using System; using System.Collections.Generic; using System.Collections.ObjectModel; namespace DjKaosun.Extentions.ObservableCollection { /// <summary> /// ObservableCollection<KeyValuePair<TKey, TValue>> をディクショナリ的に使うための拡張メソッド。 /// </summary> public static class ObservableCollectionExtentions { /// <summary> /// キーに対応する値を取得します。(インデクサーの代替) /// </summary> /// <param name="observableCollection">拡張メソッドの基となるオブジェクト。</param> /// <param name="key">キー。</param> /// <returns>対応する値。</returns> public static TValue GetValue<TKey, TValue>(this ObservableCollection<KeyValuePair<TKey, TValue>> observableCollection, TKey key) { foreach (var keyValuePair in observableCollection) { if (keyValuePair.Key.Equals(key)) return keyValuePair.Value; } throw new KeyNotFoundException("The given key '" + key + "' was not present in the dictionary.S"); } /// <summary> /// キーに対応する値を更新します。(インデクサーの代替) /// </summary> /// <param name="observableCollection">拡張メソッドの基となるオブジェクト。</param> /// <param name="key">キー。</param> /// <param name="value">新しい値。</param> public static void UpdateValue<TKey, TValue>(this ObservableCollection<KeyValuePair<TKey, TValue>> observableCollection, TKey key, TValue value) { foreach (var keyValuePair in observableCollection) { if (keyValuePair.Key.Equals(key)) { observableCollection.Remove(keyValuePair); observableCollection.Add(new KeyValuePair<TKey, TValue>(key, value)); return; } } throw new KeyNotFoundException("The given key '" + key + "' was not present in the dictionary.S"); } /// <summary> /// キーと値のペアを追加します。 /// </summary> /// <param name="observableCollection">拡張メソッドの基となるオブジェクト。</param> /// <param name="key">キー。</param> /// <param name="value">値。</param> public static void Add<TKey, TValue>(this ObservableCollection<KeyValuePair<TKey, TValue>> observableCollection, TKey key, TValue value) { if (ContainsKey(observableCollection, key)) { throw new ArgumentException("An item with the same key has already been added. Key: " + key); } observableCollection.Add(new KeyValuePair<TKey, TValue>(key, value)); } /// <summary> /// キーが既に存在する場合は更新、存在しない場合は追加します。 /// </summary> /// <param name="observableCollection">拡張メソッドの基となるオブジェクト。</param> /// <param name="key">キー。</param> /// <param name="value">値。</param> public static void AddOrUpdate<TKey, TValue>(this ObservableCollection<KeyValuePair<TKey, TValue>> observableCollection, TKey key, TValue value) { foreach (var keyValuePair in observableCollection) { if (keyValuePair.Key.Equals(key)) { observableCollection.Remove(keyValuePair); break; } } observableCollection.Add(new KeyValuePair<TKey, TValue>(key, value)); } /// <summary> /// キーが含まれるか判断します。 /// </summary> /// <param name="observableCollection">拡張メソッドの基となるオブジェクト。</param> /// <param name="key">キー。</param> /// <returns>キーが含まれる場合 true。含まれない場合は false。</returns> public static bool ContainsKey<TKey,TValue>(this ObservableCollection<KeyValuePair<TKey, TValue>> observableCollection, TKey key) { foreach (var keyValuePair in observableCollection) { if (keyValuePair.Key.Equals(key)) return true; } return false; } /// <summary> /// 値が含まれるか判断します。 /// </summary> /// <param name="observableCollection">拡張メソッドの基となるオブジェクト。</param> /// <param name="value">値。</param> /// <returns>値が含まれる場合 true。含まれない場合は false。</returns> public static bool ContainsValue<TKey,TValue>(this ObservableCollection<KeyValuePair<TKey, TValue>> observableCollection, TValue value) { foreach (var keyValuePair in observableCollection) { if (keyValuePair.Value.Equals(value)) return true; } return false; } /// <summary> /// キーに対応するキー/値ペアを削除します。 /// </summary> /// <param name="observableCollection">拡張メソッドの基となるオブジェクト。</param> /// <param name="key">キー。</param> /// <returns>削除された場合 true。削除されなかった場合は false。</returns> public static bool Remove<TKey, TValue>(this ObservableCollection<KeyValuePair<TKey, TValue>> observableCollection, TKey key) { foreach (var keyValuePair in observableCollection) { if (keyValuePair.Key.Equals(key)) { observableCollection.Remove(keyValuePair); return true; } } return false; } /// <summary> /// キーに対応するキー/値ペアを削除します。削除する対象が見つかったらその値を value パラメーターにコピーします。 /// </summary> /// <param name="observableCollection">拡張メソッドの基となるオブジェクト。</param> /// <param name="key">キー。</param> /// <param name="value">削除された値。</param> /// <returns>削除された場合 true。削除されなかった場合は false。</returns> public static bool Remove<TKey, TValue>(this ObservableCollection<KeyValuePair<TKey, TValue>> observableCollection, TKey key, out TValue value) { foreach (var keyValuePair in observableCollection) { if (keyValuePair.Key.Equals(key)) { value = keyValuePair.Value; observableCollection.Remove(keyValuePair); return true; } } value = default; return false; } } }
- 投稿日:2021-01-14T22:12:01+09:00
ObservableCollection をディクショナリー的に使用する拡張メソッド
何番煎じか解りませんが、ObservableCollection をディクショナリー的に使用する拡張メソッド。てか、有名どころのライブラリに含まれてそう。
using System; using System.Collections.Generic; using System.Collections.ObjectModel; namespace DjKaosun.Extentions.ObservableCollection { /// <summary> /// ObservableCollection<KeyValuePair<TKey, TValue>> をディクショナリ的に使うための拡張メソッド。 /// </summary> public static class ObservableCollectionExtentions { /// <summary> /// キーと、それに対応する値を登録します。 /// </summary> /// <param name="observableCollection">拡張メソッドの基となるオブジェクト。</param> /// <param name="key">キー。</param> /// <param name="value">値。</param> public static void Add<TKey, TValue>(this ObservableCollection<KeyValuePair<TKey, TValue>> observableCollection, TKey key, TValue value) { observableCollection.Add(new KeyValuePair<TKey, TValue>(key, value)); } /// <summary> /// キーが含まれるか判断します。 /// </summary> /// <param name="observableCollection">拡張メソッドの基となるオブジェクト。</param> /// <param name="key">キー。</param> /// <returns>キーが含まれる場合 true。含まれない場合は false。</returns> public static bool ContainsKey<TKey,TValue>(this ObservableCollection<KeyValuePair<TKey, TValue>> observableCollection, TKey key) { foreach (var keyValuePair in observableCollection) { if (keyValuePair.Key.Equals(key)) return true; } return false; } /// <summary> /// 値が含まれるか判断します。 /// </summary> /// <param name="observableCollection">拡張メソッドの基となるオブジェクト。</param> /// <param name="value">値。</param> /// <returns>値が含まれる場合 true。含まれない場合は false。</returns> public static bool ContainsValue<TKey,TValue>(this ObservableCollection<KeyValuePair<TKey, TValue>> observableCollection, TValue value) { foreach (var keyValuePair in observableCollection) { if (keyValuePair.Value.Equals(value)) return true; } return false; } /// <summary> /// キーに対応するキー/値ペアを削除します。 /// </summary> /// <param name="observableCollection">拡張メソッドの基となるオブジェクト。</param> /// <param name="key">キー。</param> /// <returns>削除された場合 true。削除されなかった場合は false。</returns> public static bool Remove<TKey, TValue>(this ObservableCollection<KeyValuePair<TKey, TValue>> observableCollection, TKey key) { foreach (var keyValuePair in observableCollection) { if (keyValuePair.Key.Equals(key)) { observableCollection.Remove(keyValuePair); return true; } } return false; } /// <summary> /// キーに対応するキー/値ペアを削除します。削除する対象が見つかったらその値を value パラメーターにコピーします。 /// </summary> /// <param name="observableCollection">拡張メソッドの基となるオブジェクト。</param> /// <param name="key">キー。</param> /// <param name="value">削除された値。</param> /// <returns>削除された場合 true。削除されなかった場合は false。</returns> public static bool Remove<TKey, TValue>(this ObservableCollection<KeyValuePair<TKey, TValue>> observableCollection, TKey key, out TValue value) { foreach (var keyValuePair in observableCollection) { if (keyValuePair.Key.Equals(key)) { value = keyValuePair.Value; observableCollection.Remove(keyValuePair); return true; } } value = default; return false; } } }
- 投稿日:2021-01-14T20:57:57+09:00
【C#入門】初学者がASP.NETでWebアプリを作る:第1回
概要
転職してC#で開発することになったものの、C#は触ったことがないので、練習として簡単にWebアプリを作っていきたいと思います。
やってみた結果、こうやったらできたよ、ではなく、やりながらやったことをそのまま書いていきたいと思います。
(最後まで読むと成長過程がわかる…きっと。)作るもの
・給与管理アプリケーション
会社が従業員の給与を管理するものではなく、従業員が自分の給与を管理するアプリを作っていきます。
毎月の給与や賞与の明細を登録、蓄積していきます。
データを見るところは、明細を確認するだけはアプリの機能として実装して、グラフとかで見えたらいいなと思うので、そこはBIツールを使う予定。
アプリの機能としては、下記3機能を持たせます。
1.アカウント管理
2.給与・賞与データ登録
3.給与・賞与データ参照使うもの
・Visual Studio 2019
Community版を使います。
・ASP.NET Core
・Entity Framework
PostgreSQLを使うのでNpgsqlを使います。
・IdentityASP.NET Core(アカウント認証はこれを使うとできるって聞いたけどほんとに使うか不明)
・PostgreSQL(あえてSQL Serverは使わない挑戦)プロジェクトを作る
Visual Studio 2019を起動し、新しいプロジェクトの作成
ASP.NET Core Web アプリケーションを選択、次へ
Web アプリケーション(モデルビューコントローラー)を選択し作成
(認証のところを変更しておくかどうか迷ったけど一旦スルー、まずは登録機能だけ作り上げて、アカウント管理機能は後で作る。)
プロジェクトの作成完了。
Npgsqlを導入する
プロジェクトを右クリックして、NuGetパッケージの管理を開き、
Npgsqlを検索、インストール
エラーが出なければ完了。
Modelを作る
参考文献:https://qiita.com/Kei18/items/1a9b936216bd2458ec08
まずはSalaryモデルを作っていきます。
ソリューションエクスプローラのModelsフォルダを右クリック→追加→新しい項目を選択、
クラスを選択して名前を記入し追加
下記のようなクラスにしました。
Salary.csusing System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace SalaryManagementSystem.Models { public class Salary { // 受給者 public string Beneficiary { get; set; } // タイプ(給与、賞与) public string PaymentType { get; set; } // 支給日 public DateTime PaymentDate { get; set; } // 支給額 public decimal PaymentAmount { get; set; } // 交通費 public decimal TravelExpence { get; set; } // 健康保険料 public decimal HealthInsurancePremium { get; set; } // 厚生年金料 public decimal WelfarePension { get; set; } // 雇用保険料 public decimal EmploymentInsurancePremium { get; set; } // 所得税 public decimal IncomeTax { get; set; } // 住民税 public decimal ResidentTax { get; set; } // 総支給 public decimal TotalPaymentAmount { get; set; } // 時間外手当 public decimal OvertimeAllowance { get; set; } // 深夜手当 public decimal MidnightAllowance { get; set; } // 休日手当 public decimal HolidayAllowance { get; set; } // 備考 public string Remarks { get; set; } // 登録日時 public DateTime RegisterDate { get; set; } // 登録ユーザID public string RegisterUser { get; set; } // 更新日時 public DateTime UpdateDate { get; set; } // 更新ユーザID public string UpdateUser { get; set; } } }スキャフォールディング機能を使用するため、一度ビルドしておく。
Controllerを作る
ソリューションエクスプローラのControllersを右クリック→追加→新規スキャフォールディングアイテムを選択
Entity Frameworkを使用した、ビューがあるMVCコントローラーを選択し追加
下記のように設定し追加
エラーになった
どうやらIdという項目(自動的に主キーにする項目)がないといけないらしいので追加した。
エラー解消!Salary.csusing System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace SalaryManagementSystem.Models { public class Salary { // ID public int Id { get; set; } // 受給者 public string Beneficiary { get; set; } // タイプ(給与、賞与) public string PaymentType { get; set; } // 支給日 public DateTime PaymentDate { get; set; } // 支給額 public decimal PaymentAmount { get; set; } // 交通費 public decimal TravelExpence { get; set; } // 健康保険料 public decimal HealthInsurancePremium { get; set; } // 厚生年金料 public decimal WelfarePension { get; set; } // 雇用保険料 public decimal EmploymentInsurancePremium { get; set; } // 所得税 public decimal IncomeTax { get; set; } // 住民税 public decimal ResidentTax { get; set; } // 総支給 public decimal TotalPaymentAmount { get; set; } // 時間外手当 public decimal OvertimeAllowance { get; set; } // 深夜手当 public decimal MidnightAllowance { get; set; } // 休日手当 public decimal HolidayAllowance { get; set; } // 備考 public string Remarks { get; set; } // 登録日時 public DateTime RegisterDate { get; set; } // 登録ユーザID public string RegisterUser { get; set; } // 更新日時 public DateTime UpdateDate { get; set; } // 更新ユーザID public string UpdateUser { get; set; } } }とりあえず実行してみる
画面上部のIIS Expressボタンを押下
証明書がどうとか聞かれるがOK的な方を選ぶ
自動的に開いたページのURL末尾に/Salariesを追加して開くと…エラー。
DBにアクセスできない的なエラー。そりゃDB作ってないからな。。。
まとめ
・プロジェクトを作成した
・Modelクラスを作成した
・Controllerはスキャフォールディングで作成したがうまくいってない???次回予告
次回はスキャフォールディングでうまくいかなかった調査と、CRUDの作りこみをしていきます。
- 投稿日:2021-01-14T20:44:26+09:00
全ては公式に書いてあったんや…(Slack API編)
はじめに
新しいAPIや関数を使用する際に、
自分だと、そのAPIや関数をググって、誰かが書いたわかりやすい解説記事を使ってみることが多い。しかし、記事には一部情報がピックアップされていたりして、欲しい情報が思うように得られないことが多々ある。
そんな時は公式ドキュメントを見れば全て書いてあるということに最近気づいた。
きっとそういう人は自分だけではないはず。(と、信じている。)何を書いたか
今回はSlackのfiles.uploadを例に読み方を紹介した。
多くの公式ドキュメントには渡すべき、必須あるいは任意の引数や、返値が書いてある。
また、APIなら使うのに必要な設定も書かれており、困ったら一度公式に立ち返ってみることをお勧めする。files.upload公式ドキュメント
今回は"files.upload"の公式ドキュメントを元に、
構築に必要な情報を抜粋して(というか、自分が全部読めていないだけなのだが)紹介する。ドキュメントは下記URLを参照のこと
https://api.slack.com/methods/files.uploadFacts
ドキュメント上にある"Facts"には対象のAPIの基本情が記載されている。
MethodURL
プログラムが投げるべきURLが記載されている。プログラムはMethodURLをもとにクエリを作成する。Preferred HTTP method
推奨されるHTTPメソッドが記載されている。"POST"と記載されていれば、
推奨通りにPOSTメソッドでリクエストを送ろう。Accepted content types
受け入れ可能なコンテンツのタイプが記載されている。
content typesを指定する際はどちらかを使えば良い。rate limiting
レート制限のことらしい(この項目に関しては特に必要としたことがない)
"Tier2"の場合、20回/minのリクエストが保証されるらしい。
※項目をクリックすれば、詳細が見れるworks with
構築するAPI Appに付与する権限が記載されるている。Bot/User のどちらで構築する際でも"files:write"の権限の付する必要がある。API Appへの権限付与にはOAuthスコープを指定すればよい。
詳細な設定方法は以下がわかりよい。
http://dotnsf.blog.jp/archives/1074688701.htmlArguments
Argumentsには引数が記載されている。
数が多いので全ての紹介は避けるが、これ等のうち、少なくとも"Required"を指定する。例えば、tokenは必ず必須でchannelsにはチャンネル名もしくはIDを、(複数指定なら、コンマ区切りで)引数として設定できる
Response
Responseには返値の形式と中身の例が記載されている。
引値で必要な値がある時はここを参照するとよい。
ちなみに、エラーが発生したときの返値については下記のように帰ってくる
"ok":false にはリクエストが失敗したこと、
また、"error"にはその内容が記載されている。
Error
最後に、Errorの説明をする。
Errorには文字通り、エラー内容が記載されている。
リクエストに失敗する場合、返値の”error”から該当する内容を
探すと解消方法のヒントになるだろう。
最後に
以上がfiles.upload公式ドキュメントの読み方の紹介である。
公式を読めば全てが解決するとは限らないが、
少なくとも足掛かりにはなると思うので、
つまったら公式ドキュメントを一度見ることを推奨した。
- 投稿日:2021-01-14T19:41:57+09:00
(備忘録)Unity(C#)でのシングルトンパターン
概要
- ゲームの全体を通してパラメータなどを管理するクラスの作成
- シングルトンパターンでの実装
- prefabからのオブジェクトの取得も可
- 別で基となる基底クラス(親クラス)を作成しない(ジェネリックを使わない)
*注意点
筆者はプログラミング苦手でUnityもC#も初心者ですし、備忘録なので、雰囲気で書いています。あまり信用しすぎないでください...したいこと
したいことは、Unityでパラメータやprefabから取得したゲームオブジェクトをゲーム全体を通して管理できるクラスの作成です。
ただし色々調べたところ、ゲーム全体を通して管理するクラスのインスタンスは一つしか存在しない方が良いとのこと。
Unity 2Dアクションの作り方【ゲームマネージャーを作ろう】
UnityのMonoBehaviourクラスをシングルトン化する
インスタンスが複数存在している場合、そのうちの一つを変更しても他のインスタンスは何も変更されず、異なった値を持つパラメータが複数存在することになってしまいます。これでは、どれを参照すれば良いかわかりません。
そこで、インスタンスを1つしか存在できないようにしてやろう!ということです。*補足
もう少し細かいことを言うと、パラメータを唯一にしたければ、インスタンス毎に割り振られる(または生成される)インスタンス変数を用いるのではなく、クラスに固有で全てのインスタンスに共通するクラス変数を用いるという方法もあります。もっと言うと、インスタンス変数を含まないクラスは静的クラスというものにすることができ、インスタンスの生成が一切できないクラスになりますので、これを利用してもいいです。これらの方法のほうがシンプルですね。
ですが、私の試した限りでは、prefabに入れてあるゲームオブジェクトをクラス変数に格納できませんでした。
なので仕方なくシングルトンパターンを利用しています。
また、通常UnityでC#スクリプトを書く場合、MonoBehaviourというクラスを継承した新しいクラスを作成するという形になるのですが、このMonoBehaviourというクラスの継承クラスは静的にできないらしいです。(
MonoBehaviourを継承しないという選択肢【Unity】)
ちなみにクラス変数にするには、変数定義の際にstaticを入れればいいです。また、静的クラスはクラス定義の際にstaticを入れればいいです。(詳しく解説はしないです。)コードとコメント
以下がゲーム全体を通してパラメータやprefabを管理するクラスのシングルトンパターンでの実装コードです。ここではクラス名はゲームで扱う物を保管する場所という意味でGameStorageとしています。基本的には、上に挙げた記事を参考にして作成しました。
ちなみに、Unityのバージョンは2021.1.0b1です。GameStorageusing System.Collections; using System.Collections.Generic; using UnityEngine; public class GameStorage : MonoBehaviour { private GameObject Prefab_closed; private float parameter_closed; public GameObject Prefab { get {return Prefab_closed; } } public float parameter { get {return parameter_closed; } } private GameStorage() { } private static GameStorage Instance_closed; public static GameStorage Instance { get { return Instance_closed; } } private void Awake() { if (Instance_closed != null && Instance_closed != this) { Destroy(this.gameObject); } Instance_closed = this; DontDestroyOnLoad(this.gameObject); this.Prefab_closed = Resources.Load("OriginalObject") as GameObject; this.parameter_closed = Prefab.transform.localScale.y; } }まず、一番外枠の
using System.Collections; using System.Collections.Generic; using UnityEngine; public class GameStorage : MonoBehaviour { }は、UnityでC#スクリプトを作成すると自動で書かれているやつですね。
次にprivate GameObject Prefab_closed; private float parameter_closed; public GameObject Prefab { get {return Prefab_closed; } } public float parameter { get {return parameter_closed; } }の部分で、ゲーム全体を通して管理したいパラメータなどを格納するための変数を導入しています。最初の2行でprivate(クラスの内部だけで参照できる)インスタンス変数Prefab_closedとparameter_closedを導入します。Prefab_closedには後でprefab化したオブジェクトOriginalObjectを取得して入れます。parameter_closedにはPrefab_closedに入れたOriginalObjectのインスタンス変数を入れます。
そして、privateな変数だけでは外部からアクセスが一切できないので、次の部分でpravateなインスタンス変数Prefabとparameterを導入して外部からアクセスできるようにします。getを使うことで、Prefabやparameterという窓口を通してPrefab_closedやparameter_closedを取得するイメージです。ここで、勝手に外部から編集されたくないので、setは使わずに、外部への読み取りのみ許可するという形になっています。
次に、private GameStorage() { }ですが、これはGameStorageクラスのコンストラクタです。publicではなくprivateであることが重要で、こうすることにより、クラス外で新しくGameStorageクラスのインスタンスを生成することを禁止しています。今回コンストラクタにはこの役割しか持たせないので、中括弧の中には何も書いていません。この部分は、Javaの解説記事のようですがこちらの記事を参考にしています。
5. Singleton パターン
次に、private static GameStorage Instance_closed; public static GameStorage Instance { get { return Instance_closed; } }ですが、一行目でGamaStorageクラスのインスタンスInstance_closedをprivateな変数として生成しています。先ほど、コンストラクタをprivateにすることでクラス外部からのインスタンス生成を禁止すると言いましたが、ここはGameStorageクラスの定義文の中なので、インスタンスを生成することが可能なわけですね。さて、Instance_closedは外部からアクセスできないので、インスタンス変数の場合と同様に、publicはインスタンスInstanceを生成して、このInstanceを通してのみInstance_closedにアクセスできるようにします。ここでは、getのみを用いて、setは用いず、読み取りのみ可にしています。ここでもう一つ重要なのが、staticにすること。Unityでは、staticなインスタンスにしなければ、GetComponentやGameObject.Findなどを用いてインスタンスを探してくる必要がありますが、staticなインスタンスをクラス内で生成していくことで、
GameStorage.Instanceと書くだけでどこからでもアクセスが可能になります。
最後に、private void Awake() { if (Instance_closed != null && Instance_closed != this) { Destroy(this.gameObject); } Instance_closed = this; DontDestroyOnLoad(this.gameObject); this.Prefab_closed = Resources.Load("OriginalObject") as GameObject; this.parameter_closed = Prefab.transform.localScale.y; }の部分に関してですが、まず、このAwake()はUnityで使う関数で、UnityでC#スクリプトを作成すると自動で書かれているStart()やUpdate()の仲間です。Start()よりは実行されるタイミングが早いということらしいですが、詳しいことは調べていないです。
さて、最初のIf文で、Instance_closedに自分自身以外のインスタンスが格納されていないかを調べています。もし別のインスタンスが入っていたらそのインスタンスが付随するゲームオブジェクトをDestroyすることで、複数のインスタンスが存在しないようにしています。この部分は(Unityで、シングルトンパターンを正しく実装するにはどうすればよいですか?)で書かれていた例を参考にしました。
その次の部分で、privateなインスタンスInstance_closedに実際に自分自身を入れています。
その下の行でDontDestroyOnLoadを用いることで、このクラスのインスタンスをゲームオブジェクトごとシーンを跨いで存在できるようにしています。
その下の部分で、最初の方に定義したインスタンス変数Prefab_closedとparameter_closedに、それぞれプレハブ化されたオブジェクトとそのオブジェクトのy軸方向の位置情報(transform.localScale.y)を代入しています。以下、いくつか注意点です。
- DestroyやDontDestroyOnLoadはシーン間遷移がある場合に必要な物だそうですが、また複数シーンを作成して遷移を行うテストをしていないため、上手くいくかわかりません。今後、確かめてみて問題が起きたら記事を修正するかもしれません。
- インスタンスやインスタンス変数への代入ですが、コンストラクタやAwake()の外で行うとエラーが出ました。
以上です。
- 投稿日:2021-01-14T02:36:39+09:00
C#のクラスの内部構造を黒魔術で分析してみた
全てはこの疑問から始まった。
皆さんはC#プログラマをやっているなら一度は疑問に思ったことがあるでしょうそうでしょう。
「インスタンスってどうやって変数を保持してるんだろう」
と。
え?思ったことないって?それは粛(殴)
そもそもC#という言語はポインタなんぞ面倒なことは気にしなくてもコードがかけてしまう、そんな素敵な言語なのです。
従って我々C#プログラマは、このインスタンスはどこに保存されているいるんだ?だとか、Fieldってどこに保存されてるの?とか、ましてやTypeってどうやって管理されているの?なんてことは気にしなくて良いのです。実際、そんなことはネット上に記事で載ってないです。
ただ時に「無駄なメモリなんぞ1bitも使いたくない!」とか「boxing?なにそれおいしいの?するわけないじゃん笑」とか「純粋にクラスってどうなってるんだ」とか考える変態さん(褒め言葉)がいらっしゃるわけです。
そこでこの記事では、そんな変態さんのためにそもそも論に立ち返り、
「クラス」の根本的な仕組み
をポインタという概念から紐解いていきたいと思います。
この記事を読んだ暁には皆さんも最適化の沼に嵌っていることでしょうきっと。
※記事の内容に間違いなどありましたらコメントにて指摘いただけると幸いです。
この記事を読んでわかること
- インスタンスはどのようにして変数を保持しているのか
- Type、FieldInfoの根本的な仕組み
- Boxingなんてもってのほか、ILを使って最適化? -->Unsafeを使えば一発で解決
インスタンスとは何ぞや
C#はオブジェクト指向な言語です。従って、クラスという構造(正確にはType)を定義し、そのインスタンスを作ることで、個別にデータを管理できるわけです。(とっても便利でわかりやすい)
しかし、そんな恩恵を受けられるが故、裏ではちょっと複雑なことをしているのです。
値型と参照型(理論)
さて、質問です。クラスの中で実体のあるデータはどんなデータでしょう?
例えば下のようなクラスがあったとします。
Data.cspublic class Data { public int val; public Nested nested; public class Nested { public int val2; } }実際にデータ(値)を持っているのは
val
とval2
だけです。nested
は値を持ってはいません。
即ち、実体のあるデータは「値を持っている」データのみです。
このように、直接値を持たなくても良い変数を参照型変数、値を持つ変数を値型変数と言います。値型はスタック領域(実際に値を持つメモリ領域)に、参照型はヒープ領域(インスタンスを保持するメモリ領域)という仮想メモリ領域に保持されています。
そして、この参照型はヒープ領域にいるインスタンスの参照(ポインタ)を保持しているのです。
値型と参照型(現実)
しかし、現実の保持のされ方を見てみるとこの理論ではわかりにくい部分があります。
もっと直感的に示した図を載せます。
因みにこちらの図に示している構造は私がポインタを解析して出した答えなので間違っていたらごめんなさい。
そもそもデータは基本(ローカル変数以外は)インスタンスの中に保持されます。値はむき出しにならず大体の場合はクラスの中に入ってしまいます。ですので、スタックとかヒープに分けて考えるより、こっちの方が直感的かなと思います。
C#のインスタンスではまず、先頭に自分のインスタンスのポインタをInt64のサイズで保存しています(8byte)。そして、その後の8byteでType Handlerのポインタを保持しています。(後の項で説明します)
その後、そのインスタンス内に保持しているインスタンス(ネストしたインスタンス)へのポインタを保持、最後に実データを保持しています。
このように、インスタンスはポインタとデータのbyte配列で構成されているのです。
はい、これでインスタンスのデータを保持することができるようになりました。
(インスタンスはどのようにして変数を保持しているのかの答え)Typeとは何ぞや
C#プログラマにとって、Typeは大事な存在です。
こいつがなければType-safeなプログラムがかけず、勿論我らがIntelliSense君(コードを予測して出してくれるアレ)も機能してくれません。
しかし、実際にコードを走らせる際、「このTypeとこのフィールドを...」とかやっていたら重くて仕方ありません。そこで、C#はRuntimeTypeHandleというものをメモリ上に生成することで素早いコード実行を実現しています。
そして、インスタンスを生成する時は必ず、先頭から9 byte目から16 byte目までの8 byte区間にTypeHandleのポインタを書き込みます。
因みにこのTypehandleには
code.cs//Handleにアクセス typeof(T).TypeHandle; //IntPtrにアクセス typeof(T).TypeHandle.Value;でPublicにアクセスできますので見てみるといいかもしれないです。
また、Fieldなどについても同様にFieldInfoの中にFieldHandleがありますので是非確認してみると幸せになれるかもしれません(たぶんなれない)
(Type、FieldInfoの根本的な仕組みの答え)
Unsafeを使った黒魔術
さて皆さんお待たせいたしました。
実践編です。
Reflection?なにそれおいしいの。
Reflectionはよく耳にするFieldなどの動的取得法です。
いいところ:簡単。誰でもできる。
悪いところ:遅い。黒魔術じゃない。はい、皆さんなら使いませんね。(強制)
というのは投げやりなので一応説明しておきます。
Reflectionを使えばクラスの内部構造を取得し、その取得した結果に基づいてFieldの値を取得できてしまいます。
こんな感じで取得します。
FieldGetterReflection.cspublic class FieldGetter { void Getter() { FieldInfo fieldInfo = typeof(T).GetField("FieldName"); T obj = "Data"; var data = fieldInfo.GetValue(obj); } }後はfieldInfoとobjに適切なデータを入れてください。
ね?とっても簡単でしょう?しかし、ここに重要な落とし穴があります。
fieldInfo.GetValue(obj);
この部分のコード定義を見てみましょう。ふぁっ!?なんと?
返り値がobjectじゃないですかやだーそう。これがReflectionが遅くなる原因です。
Fieldの中身が先ほど話した参照型であれば大した問題はおきません。
しかし、これが値型だった場合は大きな問題が生じます。値型はスタック領域にデータがいます。
しかし、objectは参照型であるため、値型のデータを引っ張ってきてわざわざヒープ領域に持っていき、さらにわざわざ値型に直すハメになるのです。コードでこれを再現するとこんな感じ。hogehoge.cs// 100をdataの中に入れる int data = (int)(object)100;あ ほ く さ。
はい。これが理由です。
なんで
GetValue<T>
を実装しなかったんだろう()。ということでReflectionを使うのがおすすめできない理由はこれです。
IL生成?やっぱやりたくない。
よく最適化をするにあたって、ILを使うという解決策があります。
ILとは共通中間言語(intermediate language)の略で、.Netで最も低水準な命令を書くのに使われる言語です。
そこで直接ILを書き、動的にコンパイルさせちゃえばいいじゃんということになるわけです。
因みに、フィールドの変数を取ってくるILコードを作っておいたので参考までに。
(中身は単純で、インスタンスをスタックに積み上げ、Ldfldに渡してRetというものを書いてるだけです)FieldGetterIL.cspublic class FieldGetter { void Getter() { FieldInfo fieldInfo = typeof(T).GetField("FieldName"); T obj = "Data"; var met = new System.Reflection.Emit.DynamicMethod("Get", fieldInfo.FieldType, new Type[1] { typeof(FieldInfo) }); var il = met.GetILGenerator(); il.Emit(System.Reflection.Emit.OpCodes.Ldarg_0); il.Emit(System.Reflection.Emit.OpCodes.Ldfld, fieldInfo); il.Emit(System.Reflection.Emit.OpCodes.Ret); var del = (Func<Delegate, int>)met.CreateDelegate(typeof(Func<Delegate, int>)); var func = new Func<T>(obj); del.Invoke(func); } }TにはオブジェクトのType、objには実際のオブジェクトを入れればOKです。
後は出来たdelをちゃんとキャッシュしておけば問題ありません。実際大変便利な、便利すぎる手法なのですが、残念ながらC#が多く活躍するUnityで使われるIL2CPP(c++に直してくれるというもの、iOSなどでは良く使われる)では使用できません。
また、せっかくポインタでやればいいのに、動的コンパイルの時間が無駄です。おすすめしたいのはやまやまですが、IL2CPPで使えないのが痛すぎる。ので、却下です。
Unsafeという黒魔術(真)
最後に残された手段、Unsafeクラスです。
これはそもそもデフォルトで使えないようになっているのでNuGetとかでSystem.Runtime.CompilerServices.Unsafeと調べ、インスコしちゃいましょう。こんな感じです。
すると、
System.Runtime.CompilerServices.Unsafe
が使えるようになります。
それでは先ほどの例を基にコードを作っていきましょう。
まず、Dataのインスタンス、dataインスタンスのポインタを取得してみましょう。Unsafe.csUnsafe.As<Data, IntPtr>(ref data)こんな感じです。これでポインタ(を安全に使える)IntPtrが返ってきます。
このIntPtrが「自分のポインタ」の部分であるハズです。従って、nestedを示すポインタは
Unsafe.csunsafe { var data_ptr = (byte*)Unsafe.As<Data, IntPtr>(ref data).ToPointer(); var nested_ptr_arr = new byte[8]; for (int i = 0; i < 8; i++) { //先頭の16byte分ずらしたところから8byte分だけ切り取る nested_ptr_arr [i] = *((byte*)data_ptr + 16 + i); } var nested_ptr = (void*)BitConverter.ToInt64(nested_ptr_arr, 0); }こんな感じで取得できます。(OffsetとかBitConverterを使わないとかそういう最適化をすればもっと早いですが。)
こういう感じで、ポインタを駆使しながら解析をしてしまえば、理論上最速でデータをそのまま持ってくることができます。また、同様にvalのポインタ(val_ptr)を計算し、Marshalで適当に確保したbyte領域に対しコピーして突っ込んであげれば
Copy.csvar dest = Marshal.AllocHGlobal(4); Unsafe.CopyBlock(dest.ToPointer(), val_ptr, 4);こんな感じで理論上最速でbyte変換(実際はそもそも変換をしない。stackにいるbyteデータをそのままコピっているだけなので)しちゃえばOKです。
私が考え付く限り、これが最速のチューニング方法です。
結局?
長々とお付き合いいただきありがとうございました。
「インスタンスってどうやって変数を保持してるんだろう」
の疑問を解決することは出来ましたでしょうか?
Unsafeを好きになってくれましたでしょうか?
この記事を読んで、少しでも好きになってくれたら私としてはとても嬉しい限りです。
最速というところに焦点を置き、仕組みを一から研究してみるというのも面白いC#の遊び方なのではないでしょうか?
是非これを機に、皆さんも黒魔術の沼にはまってみてはいかがでしょうか?