- 投稿日:2021-01-01T23:25:16+09:00
『UniTask v2』の色々な .ToUniTask
はじめに
UniTask にはあらゆるものを UniTask に変換する
.ToUniTask
というメソッドがあります
同じものを変換するときでもオーバーロードがあったりでややこしいので備忘録としてまとめてみました
GetAwaiter
やWithCancellation
なども書いておきます(実装を書くのは省きます)環境
以下の環境で動作しています
- Unity 2019.4.15f1
- UniTask 2.0.37
目次
準備
ダミー変数var token = this.GetCancellationTokenOnDestroy(); var uniTask = UniTask.Run(() => "hoge");こんな感じで変数
token
は何かしらのCancellationToken
が、
変数uniTask
には何かしらのUniTaskが入っていると考えてくださいCancellationToken
CancellationToken
を UniTask に変換できますToUniTask
// 実装 public static (UniTask, CancellationTokenRegistration) ToUniTask(this CancellationToken cancellationToken) // 使用例 var (task,registration) = token.ToUniTask(); registration.Dispose(); // CancellationTokenRegistrationから破棄もできる await task;CancellationToken がキャンセル状態になるまで await される UniTask が返ってきます
返り値は(UniTask, CancellationTokenRegistration)
というタプルですWaitUntilCanceled
UniTask型ではありませんが、awaitableなCancellationTokenの型が返ってきます
// 実装 public static CancellationTokenAwaitable WaitUntilCanceled(this CancellationToken cancellationToken) // 使用例 await token.WaitUntilCanceled(); // 直接awaitできる。ToUniTaskはできない逆変換
UniTask を CancellationToken に変換できます
// 実装 public static CancellationToken ToCancellationToken<T>(this UniTask<T> task) public static CancellationToken ToCancellationToken<T>(this UniTask<T> task, CancellationToken linkToken) // 使用例 var ct = uniTask.ToCancellationToken(); var ct2 = uniTask.ToCancellationToken(linkToken);コルーチン
ダミーのコルーチンprivate IEnumerator HogeCoroutine(){ yield break; }ToUniTask , GetAwaiter , WithCancellation
UniTask では
StartCoroutine
を使わずにコルーチンを起動することができます// 実装 public static UniTask ToUniTask(this IEnumerator enumerator, PlayerLoopTiming timing = PlayerLoopTiming.Update, CancellationToken cancellationToken = default(CancellationToken)) // 使用例 await HogeCoroutine(); // コルーチン起動 await HogeCoroutine().ToUniTask(PlayerLoopTiming.FixedUpdate, token); await HogeCoroutine().WithCancellation(token);下記は
MonoBehaviour
とコルーチンを結び付けるためのメソッドです
MonoBehaviourでStartCoroutine
をするように、ゲームオブジェクトの寿命とコルーチンが結びつきます// 実装 public static UniTask ToUniTask(this IEnumerator enumerator, MonoBehaviour coroutineRunner) // 使用例 await HogeCoroutine().ToUniTask(this); // ゲームオブジェクトと結び付けられる逆変換
UniTask を IEnumerator に変換できます
// 実装 public static IEnumerator ToCoroutine<T>(this UniTask<T> task, Action<T> resultHandler = null, Action<Exception> exceptionHandler = null) public static IEnumerator ToCoroutine(this UniTask task, Action<Exception> exceptionHandler = null) // 使用例 StartCoroutine(uniTask.ToCoroutine(Debug.Log,Debug.LogError)); StartCoroutine(uniTask.ToCoroutine(Debug.LogError));エラーハンドリングや結果を得るためのデリゲートを渡せるオーバーロードが用意されてます
AsyncOperation
AsyncOperation
を UniTask に変換できますUniTask には 様々な AsyncOperation が awaitable になっています。
- AsyncOperation
- ResourceRequest
- AssetBundleRequest
- AssetBundleCreateRequest
- UnityWebRequest
シグネチャは拡張メソッドのthisキーワードが付いた引数の型の違いしかなく、使い方は全部一緒なので省きます。
// 実装 public static UniTask ToUniTask(this AsyncOperation asyncOperation, IProgress<float> progress = null, PlayerLoopTiming timing = PlayerLoopTiming.Update, CancellationToken cancellationToken = default(CancellationToken)) // 使用例 await SceneManager.LoadSceneAsync("Game"); await SceneManager.LoadSceneAsync("Game").ToUniTask(cancellationToken : token); await SceneManager.LoadSceneAsync("Game").WithCancellation(token); await SceneManager.LoadSceneAsync("HogeScene") .ToUniTask(Progress.Create<float>(n => { Debug.Log($"現在{n * 100}%"); }), PlayerLoopTiming.Update, token);
SceneManager.LoadSceneAsync
の例です。
.ToUniTaskにProgress
を入れて進行状況を取ることができます。AsyncLazy
UniTask に用意されている
AsyncLazy型
です。ToUniTask ではなく.Taskプロパティ
から取得できます.Taskプロパティ
var lazy = UniTask.Lazy(() => uniTask); var task = lazy.Task; // UniTaskに変換逆変換
UniTask から AsyncLazy に変換できます
uniTask.ToAsyncLazy(); // AsyncLazyに変換IObservable
IObservable
を UniTask に変換できます。ToUniTask
// 実装 public static UniTask<T> ToUniTask<T>(this IObservable<T> source, bool useFirstValue = false, CancellationToken cancellationToken = default) // 使用例 await subject.ToUniTask(cancellationToken:token); // OnCompletedが発行されるまで待つ await subject.ToUniTask(true,token); //次に発行される最初のメッセージを待つ第一引数を
true
にすると、次に発行される最初のメッセージだけを待機できます。逆変換
UniTask を IObservable に変換できます。
uniTask.ToObservable();JobHandle
ジョブの実行が終了するまで待機できます
// 実装 public static UniTask ToUniTask(this JobHandle jobHandle, PlayerLoopTiming waitTiming) public static async UniTask WaitAsync(this JobHandle jobHandle, PlayerLoopTiming waitTiming, CancellationToken cancellationToken = default) // 使用例 await jobHandle; // CancellationTokenが指定できない await jobHandle.ToUniTask(PlayerLoopTiming.FixedUpdate); // 指定したタイミングに切り替えてjobHandle.Complete();される await jobHandle.WaitAsync(PlayerLoopTiming.FixedUpdate,token);実装を見ればどれも違うのがわかりますが、自分はJobHandleに詳しくないので有識者の方、コメントや編集リクエストをお願いします!
AsyncGPUReadbackRequest
AsyncGPUReadbackRequest
を UniTask に変換できます。ToUniTask
// 実装 public static UniTask<AsyncGPUReadbackRequest> ToUniTask(this AsyncGPUReadbackRequest asyncOperation, PlayerLoopTiming timing = PlayerLoopTiming.Update, CancellationToken cancellationToken = default(CancellationToken)) await AsyncGPUReadback.Request(src); await AsyncGPUReadback.Request(src).ToUniTask(cancellationToken : token); await AsyncGPUReadback.Request(src).WithCancellation(token);逆変換
UniTask を AsyncGPUReadbackRequest に変換できます
var asyncGpuReadbackRequest = AsyncGPUReadback.Request(src); asyncGpuReadbackRequest.ToUniTask(PlayerLoopTiming.FixedUpdate,token);おわりに
文章書くの下手すぎて読みにくいかもです。すいません。
間違いがあったら優しく教えてください。
- 投稿日:2021-01-01T18:58:03+09:00
1サンプル t-検定のお気持ちと実装
t-検定の数理的な面は この記事 が詳しい。ここでは t-検定が良さそうだというお気持ちと実装、数値検証を載せた。
1サンプルの t 検定のお気持ち
母集団の平均値が $\mu_0$ と等しいことを帰無仮説とする。サンプル数 $N$ が非常に多い場合、t-検定の統計量がどのように振る舞うかを考える。なお数学的には厳密ではない、あくまでお気持ち計算である。
確率変数 $X_i\ (i=1, ..., N)$ ($N$ はサンプル数) が平均 $\mu$ 標準偏差 $\sigma$ を持つ場合に、以下の t-検定の統計量 $t$\begin{align} t &:= \frac{\sqrt{N}\left(\bar X - \mu_0\right)}{s}\\ \bar X &:= \frac{1}{N}\sum_{i=1}^N X_i\\ s^2 &:= \frac{1}{N-1}\sum_{i=1}^N\left(X_i-\bar X\right)^2 \end{align}を考える。サンプル数のリーディングオーダーのみを考える。サンプル数が非常に大きい場合、$E[s^2] = \sigma^2$, $Var[s^2] = o\left(\frac{1}{N}\right)$ (分散の導出はこちら) となる。サンプル数のリーディングオーダーのみを見れば中心極限定理より
t \approx \sqrt{N} \frac{\bar X - \mu_0}{\sigma} \sim N\left(\sqrt{N} (\mu - \mu_0), 1\right)となる。帰無仮説が正しい場合 $t$ は平均0の正規分布に漸近的に従うが、正しくない場合は平均値が0から大きく乖離する。そのため帰無仮説が正しいとして統計量 $t$ を評価すれば、その帰無仮説がどの程度正しいのか正しくないのかを評価することができる。もちろん上記の計算では確率変数を正しく扱ってはいなく、サンプル数が非常に大きい場合は真の平均、分散にほぼ等しいから定数として置いたりなどがあるため厳密には正しくないが、サンプル数が非常に大きい場合は近似的に正しいはず。
サンプル数が十分に多くない場合は上の議論は使えない。しかし各サンプルが正規分布に従っている場合 $t$ はt-分布に従う。
C# による実装
t-検定を C# でスクラッチで実装し、テストをおこないました。。。
public partial class Parametric { public static StatisticalTestResult TTestOneSample(TTestOneSampleNullHypothesis nullHypothesis) { var n = nullHypothesis.Samples.Count(); var sampleMean = nullHypothesis.Samples.Average(); var sampleStandardDeviation = nullHypothesis.Samples.StadardDeviation() * Math.Sqrt(n / (n - 1.0)); var statistics = (sampleMean - nullHypothesis.PopulationMean) / (sampleStandardDeviation / Math.Sqrt(n)); var tDist = new Probability.Distribution.T(); var tCdf = tDist.GetCumulativeDistributionFunction(new Probability.Parameter.T(0, 1, n - 1)); var pValue = 1 - Math.Abs(tCdf(statistics) - 0.5) * 2; return new StatisticalTestResult(statistics: statistics, pValue: pValue); } }[TestMethod] public void TTestOneSample() { // following data from https://tanuhack.com/ttest-one-sample/ var samples = new double[] { 210.9, 195.4, 202.1 , 211.3, 195.5, 212.9, 210.9, 198.3, 202.1, 215.6,204.7, 212.2, 200.7, 206.1, 195.8}; var popMean = 200; var expectedStatistics = 2.751076959309973; var expectedPValue = 0.015611934395473872; var result = Statistics.StatisticalTest.Parametric.TTestOneSample(new Statistics.StatisticalTest.NullHypothesis.TTestOneSampleNullHypothesis(samples, popMean)); Assert.AreEqual(expectedStatistics, result.Statistics, 1.0e-10); Assert.AreEqual(expectedPValue, result.PValue, 1.0e-10); }
- 投稿日:2021-01-01T17:17:05+09:00
ツイキャスAPI 試すよ!
Program.csusing System; using RestSharp; using RestSharp.Authenticators; using System.Text.Json; using System.Diagnostics; using System.Collections.Generic; namespace ConsoleApp2 { class Program { #region アクセストークンJSONクラス /// <summary> /// アクセストークンのレスポンスクラス /// </summary> public class AccessTokenJson { public string token_type { get; set; } public int expires_in { get; set; } public string access_token { get; set; } } #endregion #region コメントJSON_クラス public class From_user { public string id { get; set; } public string screen_id { get; set; } public string name { get; set; } public string image { get; set; } public string profile { get; set; } public int level { get; set; } public string last_movie_id { get; set; } public bool is_live { get; set; } public int supporter_count { get; set; } public int supporting_count { get; set; } public int created { get; set; } } public class Comment { public string id { get; set; } public string message { get; set; } public int created { get; set; } public From_user from_user { get; set; } } public class Root { public string movie_id { get; set; } public int all_count { get; set; } public List<Comment> comments { get; set; } } #endregion static void Main(string[] args) { ////<ClientID> //string client_id = <ClientID>; ////<ClientSecret> //string client_secret = <ClientSecret>; ////以下のURLにブラウザでアクセス //string GetCodeUrl = @"https://apiv2.twitcasting.tv/oauth2/authorize?client_id=" + client_id + "&response_type=code&state=" + client_secret; //// ブラウザでTwitterアプリ連携認証ページを開く //Process.Start(@"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe", GetCodeUrl); ////コールバック値 //http://localhost:1220/?code=<code>&state=<ClientSecret> ////コールバックに渡された<code>の値 //string CallbackCode = <code>; ////アクセストークンの取得 //var GetAccessTokenClient = new RestClient("https://apiv2.twitcasting.tv/oauth2/access_token"); //RestRequest AccessTokenRequest = new RestRequest() { Method = Method.POST }; //AccessTokenRequest.AddHeader("Content-Type", "application/x-www-form-urlencoded"); //AccessTokenRequest.AddParameter("grant_type", "authorization_code"); //AccessTokenRequest.AddParameter("code", CallbackCode); //AccessTokenRequest.AddParameter("redirect_uri", "http://localhost:1220/"); //AccessTokenRequest.AddParameter("client_id", client_id); //AccessTokenRequest.AddParameter("client_secret", client_secret); ////アクセストークンをリクエスト //var AccessTokenResponse = GetAccessTokenClient.Execute(AccessTokenRequest); ////レスポンスをデシリアライズ //var JsonAccessTokenData = JsonSerializer.Deserialize<AccessTokenJson>(AccessTokenResponse.Content); //コメントを取得したいMovieID string movie_id = "653542611"; //コメントを取得エンドポイント var GetCommentsClient = new RestClient("https://apiv2.twitcasting.tv/movies/" + movie_id + "/comments"); RestRequest CommentsRequest = new RestRequest() { Method = Method.GET }; string AccessToken = JsonAccessTokenData.access_token; CommentsRequest.AddHeader("Accept", "application/json"); CommentsRequest.AddHeader("X-Api-Version", "2.0"); CommentsRequest.AddHeader("Authorization", "Bearer " + AccessToken); //コメント情報を取得 var GetCommentsresponse = GetCommentsClient.Execute(CommentsRequest); //レスポンスをデシリアライズ var JsonCommentsData = JsonSerializer.Deserialize<Root>(GetCommentsresponse.Content); //メッセージを取得 foreach (var comment in JsonCommentsData.comments) { //コンソールに出力 Console.WriteLine(comment.message); } } } }Follow @miki1220jp
↑よろしくお願いしますぜひぜひ
▼ 参考リンク!ありがとうございます
https://apiv2-doc.twitcasting.tv/ - TwitCastingのAPIv2ドキュメント
https://qiita.com/yonex/items/dd8f815f54a27f8c8690 - ツイキャスAPIを使うまで
https://www.366service.com/jp/qa/e06557fdcb36cee569334dd6f1e4a6ea - C#でOAuth 2.0認証について
https://json2csharp.com/json-to-csharp - JSON文字列をC#のクラスにしてくれる
- 投稿日:2021-01-01T16:11:10+09:00
Visual Studioの開発環境メモ
1. はじめに
以前にJavaの開発環境メモという記事を作成した。
自分用の備忘録として役に立っているので、Visual Studio版を作成する。1-1. 環境
Visual Studio2019 Community
2. Visual Studioの設定
2-1.Visual Studioの設定
2-1-1.dotnet-format
dotnet-formatとは
editorconfigファイルを使用して、ソリューション内のファイルを一括して整形することが可能なツール。
インストール
- スタートメニューから「Developer Command Prompt for VS 2019」を起動。
- ソリューションフォルダに移動。
- マニフェストファイルを作成するため、以下を入力する。「dotnet new tool-manifest」
- 以下を入力し、インストールする。「dotnet tool install dotnet-format」
実行
「dotnet format」を入力し、実行する。
参考サイト
https://blog.shibayan.jp/entry/20200322/1584875344
2-2.ソリューションの設定
2-2-1.コード分析規則セットを追加
- ソリューションを右クリックし、[追加]-[新しい項目]
- 「コード分析規則セット」を選択
- ルールセットを設定する
2-3.プロジェクトの設定
2-3-1.Properties
- [アプリケーション]-[既定の名前空間]
- [ビルド]-[XMLドキュメントファイル]をチェック(SA0001対応)
- [コード分析]-[アクティブな規則]
3.Nuget
3-1.StyleCop.Analyzers
3-1-1.StyleCopAnalyzersとは
C#の静的分析ツール。
バージョンは最新の安定板である1.1.118を使用する。(2021/1/1)
2019/4/30から更新されていない。
以前にStykeCopを使用したことがあるが、StyleCop.Analyzersとの違いを調べてみた。
StyleCopの後継。3-1-2.参考URL
https://opcdiary.net/stylecopanalyzers%E3%81%AE%E4%BD%BF%E3%81%84%E6%96%B9/
https://qiita.com/github129/items/8270dc74599e71af52853-1-3.特定のルールを無効化する方法
コード分析規則セットを作成(2-2-1参照)し、プロジェクトでそのファイルを適用する。
1. プロジェクトの「Properties」をダブルクリック
2. [コード分析]-[アクティブな規則]
3. 参照でファイルを選択
4. 無効にしたいルールの操作を「非表示」に変更する3-1-4.無効にするルール
番号 ルール 無効理由 SA1200 UsingDirectivesMustBePlacedCorrectly usingディレクティブがnamespaceエリア外に記述されている。初期状態がそうなっていて、特に困らないので SA1623 PropertySummaryDocumentationMustMatchAccessors プロパティのコメントルール。Gets or setsで始める必要がある。不要 SA1629 DocumentationTextMustEndWithAPeriod ドキュメントの最後をピリオドにする。不要 SA1633 FileMustHaveHeader ファイル先頭の著作権表示は不要なルールなので SA1642 ConstructorSummaryDocumentationMustBeginWithStandardText コンストラクタのドキュメントの書き方。不要 SA1643 DestructorSummaryDocumentationMustBeginWithStandardText デストラクタも同様に不要 4.Git
5.Jenkins
6.コーディングルール
6-1.名前付けのガイドライン
基本は以下のマイクロソフトのルールを使用する。
https://docs.microsoft.com/ja-jp/dotnet/standard/design-guidelines/naming-guidelines
- 名前は英語で付ける。(同じ意味の英単語が複数ある場合は、辞書を作成する)
- 1単語ごとの頭文字を大文字にする。(Pascal表記)
6-2.書く順番
まずは項目ごとに並べる。同じ項目の場合は、アクセスレベルごとに並べる。
アクセスレベルが同じならstaticを上にする。
なので、internal staticのフィールドとpublicのフィールドがある場合は、publicを上にする。6-2-1.項目の順番(SA1201)
フィールド
コンストラクター
ファイナライザー (デストラクター)
デリゲート
イベント
列挙型
インターフェイス
プロパティ
インデクサー
メソッド
構造体
クラス*(C#9のレコードを含む)6-2-2.アクセスレベルの順番(SA1202)
public
internal
protected internal
protected
private protected
private6-2-3.staticの順番(SA1204)
staticと非staticはstaticを上に書く
6-3.StyleCopでよく見かけるエラーと対応方法
番号 ルール 対応方法 SA1513 ClosingBraceMustBeFollowedByBlankLine 閉じかっこの後に空白行が無い SA1600 ElementsMustBeDocumented XMLドキュメント(///)を作成する
- 投稿日:2021-01-01T14:38:21+09:00
C# Blazor 書初め
C# Blazor 書初め
正月ってことで書初め。自分メモとして履歴残します。
やったこと
https://docs.microsoft.com/ja-jp/aspnet/core/tutorials/build-a-blazor-app?view=aspnetcore-5.0
これをやって要点をまとめました。コピペで10分で確認できるようにまとめてあります。
入れたもの
フォントは最近お試しで使い始めているもの。ゼロ0とオーOが違うなどのPG向き。
- VSCodeUserSetup-x64-1.52.1
- dotnet-sdk-3.1.404-win-x64
- JetBrainsMono-2.221
TL;DR
こまけぇこたぁいいんだ。これだけやれば理解できた気になれる。
プロジェクト生成
- cd /c/git
- dotnet new blazorserver -o TodoList
- cd TodoList
アプリ起動
watch でファイル変更時にリコンパイルかかる。
https://docs.microsoft.com/ja-jp/aspnet/core/tutorials/dotnet-watch?view=aspnetcore-5.0
- dotnet watch run
page追加
- dotnet new razorcomponent -n Todo -o Pages
Pages/Todo.razor
@page "/todo" <h3>Todo</h3> @code { }@page "/todo" を書くことでURLが解釈される。書かないと
Sorry, there's nothing at this address.
が出てページなしエラーとなる。
ルーティングを各ページに書くのね・・・メモメモ。https://docs.microsoft.com/ja-jp/aspnet/core/blazor/fundamentals/routing?view=aspnetcore-5.0
menu追加
Shared/NavMenu.razor
<li class="nav-item px-3"> <NavLink class="nav-link" href="todo"> <span class="oi oi-list-rich" aria-hidden="true"></span> Todo </NavLink> </li>ここまで書けば、メニュークリックで新しい表示が出来ます。
処理追加
プロジェクト (TodoList フォルダー) のルートに TodoItem.cs ファイルを追加します。
TodoItem.cs
public class TodoItem { public string Title { get; set; } public bool IsDone { get; set; } }razor側にもコード追加
Pages/Todo.razor
@page "/todo" <h3>Todo (@todos.Count(todo => !todo.IsDone))</h3> <ul> @foreach (var todo in todos) { <li> <input type="checkbox" @bind="todo.IsDone" /> <input @bind="todo.Title" /> </li> } </ul> <input placeholder="Something todo" @bind="newTodo" /> <button @onclick="AddTodo">Add todo</button> @code { private IList<TodoItem> todos = new List<TodoItem>(); private string newTodo; private void AddTodo() { if (!string.IsNullOrWhiteSpace(newTodo)) { todos.Add(new TodoItem { Title = newTodo }); newTodo = string.Empty; } } }え、インポート文とかいらないの?F12で参照ジャンプできるんですけど、なにこれすごい(語彙力
残課題
仕事用の検証としてはまだまだ確認できていない事があるのでメモ。
- Material Design な UI コンポーネントライブラリ(MatBlazor)を使ってみる
https://www.matblazor.com/- APIの非同期通信と、通信中のグルグル表示
- IndexedDBでローカル保存
- サーバーサイドレンダリングとクライアントSPAの違い、メリデメ比較
まとめ
Reactで普通だと思ってたことが、若干「竹槍」に感じるくらいに楽にコード書ける(かもしれない。まだ検証不足)
ReactはCordovaでネイティブアプリ作れるメリットがあるので、住み分けするならBlazorをWeb用で高速開発、Cordovaでスマホアプリか。
xamarin? おや、誰かきたようだ
- 投稿日:2021-01-01T08:01:19+09:00
【C#】 dynamic型の裏側
言語機能
dynamic
型C#では通常、コンパイル時にすべての呼び出しがチェックされ、無効なものがあればコンパイルエラーとなります。
例えば、object型にはToString
メソッドが含まれますが、GetString
のように書き間違えてしまった場合などはこのチェックによってコンパイル時に発見することができます。
一方でJavaScriptやPythonのような動的な言語では、コンパイル時に呼び出しが間違っていてもスルーされてしまいます。というかそもそもコンパイル言語ではないですが…(IEDなどではエラー表示になるかもしれませんがそれとは関係なく、「実行」することはできてしまします)これはC#をはじめ静的な言語が安全といわれる要素の一つであり、非常に有難い機能なのですが、時にこれが邪魔になってしまうことがあります。
// Class AもBも同じ名前のメンバを持っていても、同一扱いすることはできない // (或いは、Interfaceを付けるしかない) class A { public string Text => "A"; } class B { public string Text => "B"; } void LogText(object A_Or_B) { if(A_Or_B is A a) Log(a.Text); else if(A_Or_B is B b) Log(b.Text); } // 実行してみないとメンバ名が分からない場合に都合が悪い // 文字列でアクセスしても大して変わらないといえば変わらないが、普段の記法に馴染まない object unknownObject = Json.Parse("{ 'a' : 100 }"); Log(unknownObject.a); // Compile NGそこで
dynamic
型を使うと、(見かけ上)コンパイル時のチェックをスルーすることができます。void LogText(dynamic A_Or_B) => Log(A_Or_B.Text); dynamic unknownObject = Json.Parse("{ 'a' : 100 }"); Log(unknownObject.a); // Compile OKしかし、これはあくまでC#の機能であり、コンパイラが良しなに「文字列によるアクセス」に変換してくれているだけにすぎません。
具体的にどのような変換がなされ、プログラマはそれをどのように制御すればよいのか見ていきます。デフォルトの
dynamic変数
普通のオブジェクトインスタンス実体をdynamic変数に入れたときの挙動です。(実体が
DynamicObject
の派生オブジェクトではない)機能
機能としては、メンバの名前チェックをスルーし、実行時リフレクションによる文字列解決されます。メンバが特定されると、アクセス処理が動的に生成され(DLR)それでアクセスされるとのことです。(ちなみに、動的生成は最初の一度のみで、二回目からはキャッシュされるとのことです。)
コンパイル
dynamic変数へアクセスするとコンパイラにより、以下のような変換が行われます。
dynamic value = this; value.Execute(0); // ↓ using static Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfoFlags; static CallSite<Func<CallSite, object, object>> DelegateCache; object CS2ILCompileResult() { if (Cache.Delegate == null) { // ★ CallSiteBinder binder = Binder.InvokeMember( flags: CSharpBinderFlags.None, name: "Execute", // 名前解決! typeArguments: null, context: typeof(Test), argumentInfo: new [] { CSharpArgumentInfo.Create(None, null), // 0個目はダミーになる CSharpArgumentInfo.Create(UseCompileTimeType|Constant, null); }); DelegateCache= CallSite<Func<CallSite, object, int, object>>.Create(binder); } DelegateCache.Target(Cache.Delegate, this, 0); }
Binder.InvokeMember
でCallSiteBinder
という、リフレクションでいうMemberInfo
のようなものを構築し、CallSite<TFunc>.Create
でデリゲート化しています。
Binder.cs#L156 CallSite.cs#L192
CallSite<TFunc>.Create
では、CreateCustomUpdateDelegate
が呼び出され、Expression APIでDelegateのILコードが動的に生成されキャッシュされます。どのようなコードに対応するデリゲートを生成しているかは、コメントに記載されています。
CallSite.cs#L358二段構えのキャッシュになっているのと、最適化のためなのか分かりませんが結構読みづらい処理になっていますが、要約すると…
- L1キャッシュのキャッシュプールが
CallSiteOps.GetRules(this)
- L2キャッシュのキャッシュプールが
CallSiteOps.GetRuleCache(@this).GetRules()
- それぞれ、
CallSiteOps.GetMatch
を使ってヒットか否かを判定- ヒットしなかった場合には、
this.Binder.BindDelegate(@this, args)
で新しいDelegateを生成つまり、ここまでは前座ということで、実際の呼び出しの部分をどうするかはこの先ということになります。
ここで出てくる
this.Binder
が、★マークのbinder
になります。そしてこれはRuntimeが提供するNativeのAPIのようです。
Dynamicが呼び出し部分のコードをキャッシュしてくれるというのは「この先(C++)でキャッシュされている」ということでしょう。
該当のCppコードはこちらになると思われます。(本当にキャッシュする処理が入っているかは未確認)
Binder.cs,22カスタマイズされた
dynamic変数
(DynamicObject
)C#のdynamic変数は、中身のオブジェクトが特定の型やインターフェースを実装していることで任意のふるまいをさせることができます。
詳しくはこのPDFにかかれています。
DynamicObject
とIDynamicMetaObjectProvider
というのがあるのですが、単純なDynamicObject
についてみてみましょう。コンパイル結果
デフォルトと同じです。
class MyDynamic : DynamicObject {} dynamic dy = new MyDynamic(); var m = dy.hoge; // ↓ if (DelegateCache == null) { Type typeFromHandle = typeof(C); CSharpArgumentInfo[] array = new CSharpArgumentInfo[1]; array[0] = CSharpArgumentInfo.Create(None, null); DelegateCache = CallSite<Func<CallSite, object, object>>.Create(Binder.GetMember(None, "hoge", typeFromHandle, array)); } m = DelegateCache.Target(DelegateCache, arg);
DynamicObject
のメンバ
Method 説明 GetDynamicMemberNames メンバを列挙(いつ使われるのか不明) GetMetaObject IDynamicMetaObjectProviderへの変換?(いつ使われるのか不明) TryBinaryOperation 二項演算(四則演算や論理演算) TryConvert キャスト TryCreateInstance コンストラクタ(dynamicはnewできないのでこの場合不要) TryDeleteIndex ?(いつ使われるのか不明) TryDeleteMember ?(いつ使われるのか不明) TryGetIndex インデクサによるアクセス( []
)TrySetIndex インデクサによるアクセス( []
)TryGetMember プロパティやフィールドへのアクセス TrySetMember プロパティやフィールドへのアクセス TryInvoke Delegateのようにそのまま呼び出し( ((dynamic)obj)(args)
)TryInvokeMember メソッドの呼び出し TryUnaryOperation 単項演算(否定やビット反転など) これらが空の仮想メソッドとして実装されているので、必要なものについてオーバライドすることで任意のふるまいをさせることができます。
例えば、デフォルトのdynamicはプライベートメンバにアクセスすることはできませんが、上記をプライベートを貫通するリフレクションで実装しプライベート貫通式dynamicを作ることができます。(実装例)
まとめ
dynamicの内部実装と、カスタマイズの仕方を見てみました。
とはいえ、コンパイラチェックを回避してしまうのでIDEの支援をうけられない、実行するまでtypoに気が付けないなどのデメリットがあります。
使うべきところはテストや普段書き換えが発生しないようなコードに限定するべきで、できるだけインターフェイスやコード生成で代替しておくのがベターだと思います。参考
- 投稿日:2021-01-01T07:02:57+09:00
.NET 5.0とVSCodeでデスクトップアプリ(ランチャー)
■はじめに
Visual Studio Code
(VSCode
)と.NET 5.0
のWPF
でGUIアプリ(アプリケーションランチャー)を作ります。
■環境
- Windows 10(Version 20H2)
- .NET 5.0~
- Visual Studio Code
■準備
◇.NET SDKのインストール
.NET 5.x
のリンクを選択します。
SDK 5.x
-Windows
-Installers
のx64
のリンクを選択します。
インストールします。
◇VSCodeのインストール、日本語化
VSCodeをインストールします。
「エクスプローラーのディレクトリコンテキストメニューに[Codeで開く]アクションを追加する」はチェックを入れておいてください。
他のチェックは任意です。
VSCodeが起動したら、左のアクティビティ バーの「拡張機能」を選択し、検索ボックスに
japanese
と入力、
「Japanese Language Pack for VS Code」をインストールします。
◇プロジェクトフォルダの作成、VSCodeの起動
エクスプローラーでプロジェクトを格納するフォルダ、
WrapLauncher
を作成します。
WrapLauncherフォルダ内で右クリックし、「Codeで開く」を選択してVSCodeを起動します。
■プロジェクトの作成
ターミナルが表示されていない場合はメニューの「表示」-「ターミナル」でターミナルを表示します。
ターミナルに以下のコマンドを入力し、バージョンが5.xであることを確認します。
dotnet --versionターミナルに以下のコマンドを入力し、プロジェクトを作成します。
dotnet new wpf■拡張機能インストール
左のファイル一覧から「MainWindow.xaml.cs」をクリックします。
右下に「C#にお勧めの拡張機能」通知が表示された場合は「インストール」してください。
「Required assets to ....」(ビルドとデバッグに必要なアセットがありません。追加しますか?)の通知が表示されたら「Yes」を選択してください。
アクティビティ バーで「拡張機能」を選択し、検索ボックスに
xaml
と入力し、「Pretty XML」をインストールします。
これはXAMLの整形用に使います。
■とりあえず実行
◇デバッグ設定修正
デバッグの設定を修正します。
アクティビティ バーから「実行」を選択し、上の歯車マークを選択します。
「launch.json」が開くので、「program」のパスの「Debug」フォルダの後ろを
net5.0-windows/WrapLauncher.exe
に修正し、保存します。
◇実行
▷の「デバッグの開始」ボタンを押すか、F5キーを押してデバッグ実行します。
空の画面が表示されました。
起動した画面を閉じてください。■プロジェクト設定
アクティビティ バーで「エクスプローラー」を選択し、「WrapLauncher.csproj」を選択します。
「ProjectGroup」の中に<Nullable>enable</Nullable>
を入力して保存します。
■画面の作成
「MainWindow.xaml」を開き、画面を作成します。
作成し終わったらF1でコマンドパレットを表示し、xml
と入力して「Prettify XML」を実行します。
XAMLが整形されました。
MainWindow.xaml(整形後)<Window x:Class="WrapLauncher.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:WrapLauncher" mc:Ignorable="d" Title="ランチャー" Height="450" Width="800" ResizeMode="CanResizeWithGrip" Loaded="Window_Loaded"> <Window.Resources> <!-- ボタンのスタイル --> <Style TargetType="Button"> <Setter Property="Margin" Value="5" /> <Setter Property="Padding" Value="3" /> </Style> <!-- グループ見出しのスタイル --> <Style x:Key="GroupTitleStyle" TargetType="TextBlock"> <Setter Property="Margin" Value="5,10,5,0" /> <Setter Property="FontWeight" Value="Bold" /> <Setter Property="Foreground" Value="DarkBlue" /> </Style> <!-- コンテキストメニュー --> <ContextMenu x:Key="CtxMenu"> <MenuItem Header="設定再読み込み(_R)" Click="MenuReload_Click" /> <Separator /> <MenuItem Header="ヘルプ(_H)" Click="MenuHelp_Click" /> </ContextMenu> </Window.Resources> <ScrollViewer Margin="0,0,0,16"> <!-- この中にボタン等を追加していく --> <StackPanel x:Name="MainContainer" /> </ScrollViewer> </Window>■ロジックの作成
新しいファイルを追加します。
ConfigFile.cs
というファイル名にします。
ConfigFile.csusing System.Collections.Generic; using System.IO; namespace WrapLauncher { public class ConfigFile { /// <summary> /// 設定ファイル名 /// </summary> public const string ConfigFileName = "WrapLauncher.path"; /// <summary> /// 区切り文字 /// </summary> public const char Delimiter = '\t'; /// <summary> /// グループ見出しの先頭記号 /// </summary> public const string GroupTitleHeader = "//"; /// <summary> /// グループ見出しのカラム位置 /// </summary> public const int GroupTitleColumnIndex = 0; /// <summary> /// カラム位置 /// </summary> public static IReadOnlyDictionary<string, int> Columns = new Dictionary<string, int> { {"Color", 0}, {"ButtonTitle", 1}, {"Path", 2}, }; /// <summary> /// 設定ファイルパス取得 /// </summary> /// <returns>設定ファイルのフルパス。存在しない場合は空文字列を返す。</returns> public string GetPath() { // EXEのパス取得 string? appPath = System.IO.Path.GetDirectoryName( System.Reflection.Assembly.GetExecutingAssembly().Location); if (appPath is null) { throw new DirectoryNotFoundException("実行ファイルのパス取得失敗"); } // 設定ファイルのフルパス組み立て string cfgFilePath = System.IO.Path.Combine(appPath, ConfigFileName); // 設定ファイル存在チェック if (System.IO.File.Exists(cfgFilePath)) { return cfgFilePath; } else { // 設定ファイルなし return string.Empty; } } /// <summary> /// グループ見出し判定 /// </summary> /// <param name="values"></param> /// <returns>グループ見出しならtrue</returns> public bool IsGroupTitle(string[] values) { return values[GroupTitleColumnIndex].StartsWith(GroupTitleHeader); } /// <summary> /// グループ見出し取得 /// </summary> /// <param name="values"></param> /// <returns></returns> public string GetGroupTitle(string[] values) { return values[GroupTitleColumnIndex].Substring(GroupTitleHeader.Length); } } }MainWindow.xaml.csusing System; using System.IO; using System.Linq; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Media; namespace WrapLauncher { /// <summary> /// Interaction logic for MainWindow.xaml /// </summary> public partial class MainWindow : Window { private ConfigFile _cfg = new ConfigFile(); // ヘルプテキスト private const string HelpText = @"設定ファイルはEXEと同じ場所に「WrapLauncher.path」のファイル名で配置する。 文字コードはBOM無しのUTF8。 データ構造 ------------------------------ //グループ見出し 色名 ボタンテキスト 起動プログラム/フォルダのフルパス 色名 ボタンテキスト 起動プログラム/フォルダのフルパス : //グループ見出し 色名 ボタンテキスト 起動プログラム/フォルダのフルパス : ------------------------------ グループ見出しは先頭「//」で始める。 色名、ボタンテキスト、起動するパスはTABで区切る。 [色名一覧] "; /// <summary> /// コンストラクタ /// </summary> public MainWindow() { InitializeComponent(); // コンテキストメニュー設定 this.ContextMenu = (ContextMenu)(this.Resources["CtxMenu"]); } /// <summary> /// 起動時 /// </summary> private void Window_Loaded(object sender, RoutedEventArgs e) { try { // 設定読み込み、画面に反映 LoadConfig(); } catch (Exception ex) { ShowException(ex); } } /// <summary> /// コンテキストメニュー「設定再読み込み」 /// </summary> private void MenuReload_Click(object sender, RoutedEventArgs e) { // 画面クリア MainContainer.Children.Clear(); try { // 設定読み込み、画面に反映 LoadConfig(); } catch (Exception ex) { ShowException(ex); } } /// <summary> /// コンテキストメニュー「ヘルプ」 /// </summary> private void MenuHelp_Click(object sender, RoutedEventArgs e) { ShowHelp(); } /// <summary> /// エラー情報表示 /// </summary> private void ShowException(Exception ex) { MainContainer.Children.Clear(); var txt = new TextBox(); txt.TextWrapping = TextWrapping.Wrap; txt.Margin = new Thickness(5); txt.IsReadOnly = true; txt.Text = ex.ToString(); MainContainer.Children.Add(txt); } /// <summary> /// ヘルプ内容表示 /// </summary> private void ShowHelp() { MainContainer.Children.Clear(); var helpText = new RichTextBox(); helpText.IsReadOnly = true; helpText.FontSize = 16; // テキストの説明文追加 helpText.Document.Blocks.Add(new Paragraph(new Run(HelpText))); var bc = new BrushConverter(); var docList = new System.Windows.Documents.List(); // 色名一覧作成 var bList = typeof(Brushes).GetProperties().OrderBy(x => x.Name); foreach (var b in bList) { if (b.Name == "Transparent") { continue; } var li = new ListItem(); var p = new Paragraph(); var r = new Run("■ "); r.Foreground = (Brush)bc.ConvertFromString(b.Name); p.Inlines.Add(r); p.Inlines.Add(new Run(b.Name)); li.Blocks.Add(p); docList.ListItems.Add(li); } helpText.Document.Blocks.Add(docList); MainContainer.Children.Add(helpText); } /// <summary> /// 設定読み込み、画面に反映 /// </summary> private void LoadConfig() { // 設定ファイルのフルパス取得 string cfgFilePath = _cfg.GetPath(); // 設定ファイルが見つからなければ終了 if (string.IsNullOrEmpty(cfgFilePath)) { throw new FileNotFoundException("設定ファイルなし"); } // 設定ファイル読み込み using var reader = new StreamReader(cfgFilePath); WrapPanel? btnContainer = null; while (!reader.EndOfStream) { // TABで分解 var item = reader.ReadLine()?.Split(ConfigFile.Delimiter); if (item is null || item.Length < 1) { // データなしの行 continue; } /* MainContainer ..... StackPanel grpContainer ..... StackPanel 見出し ..... TextBlock btnContainer ..... WrapPanel ボタン ボタン : grpContainer ..... StackPanel 見出し ..... TextBlock btnContainer ..... WrapPanel ボタン ボタン : */ // グループ作成するローカル関数 void MakeGroup(ref WrapPanel? btnContainer, string grpTitle = "") { // グループコンテナ作成 var grpContainer = new StackPanel(); // グループコンテナをメインコンテナに追加 MainContainer.Children.Add(grpContainer); if (string.IsNullOrEmpty(grpTitle) == false) { // グループ見出しを生成し、グループコンテナに追加 grpContainer.Children.Add(CreateGroupTitle(grpTitle)); } // ボタンコンテナを作成 btnContainer = new WrapPanel(); // ボタンコンテナをグループコンテナに追加 grpContainer.Children.Add(btnContainer); } if (_cfg.IsGroupTitle(item)) { // 見出し // グループ作成 MakeGroup(ref btnContainer, _cfg.GetGroupTitle(item)); } else if (item.Length == ConfigFile.Columns.Count) { // ボタン // まだボタンコンテナが作成されていない? if (btnContainer is null) { // グループ作成(見出し無し) MakeGroup(ref btnContainer); } // ボタン作成 Button btn = CreateLaunchButton( item[ConfigFile.Columns["Color"]], item[ConfigFile.Columns["ButtonTitle"]], item[ConfigFile.Columns["Path"]]); // ボタンコンテナにボタンを追加 btnContainer?.Children.Add(btn); } else { throw new Exception($"カラム数不正\n{string.Join(ConfigFile.Delimiter, item)}"); } } } /// <summary> /// グループ見出し生成 /// </summary> /// <param name="title"></param> /// <returns></returns> private TextBlock CreateGroupTitle(string title) { var txt = new TextBlock(); txt.Style = (Style)(this.Resources["GroupTitleStyle"]); txt.Text = title; return txt; } /// <summary> /// ボタン作成 /// </summary> /// <param name="colorName">色名</param> /// <param name="text">ボタンテキスト</param> /// <param name="execute">起動プログラムのファイルパス</param> private Button CreateLaunchButton(string colorName, string text, string execute) { var btn = new Button(); var txtContainer = new StackPanel(); txtContainer.Orientation = Orientation.Horizontal; // ■テキスト作成 var txtMark = new TextBlock(); txtMark.Text = "■"; txtMark.Margin = new Thickness(0,0,2,0); try { // 色指定が有効なら■の色を変える。無効な色名なら例外発生で終了させる。 var bcnv = new BrushConverter(); txtMark.Foreground = (Brush)bcnv.ConvertFromString(colorName); } catch { throw new Exception($"無効な色名[{colorName}]"); } txtContainer.Children.Add(txtMark); // ボタン名テキスト作成 var txt = new TextBlock(); txt.Text = text; txtContainer.Children.Add(txt); // ボタンテキスト設定 btn.Content = txtContainer; // ボタンクリック時の処理 btn.Click += (_, _) => { var p = new System.Diagnostics.Process(); p.StartInfo.FileName = execute; p.StartInfo.UseShellExecute = true; try { p.Start(); } catch { MessageBox.Show( $"起動に失敗しました。\n{execute}", "エラー", MessageBoxButton.OK, MessageBoxImage.Error); } }; return btn; } } }ここまでで一度ビルドしてエラーがないか確認しておきます。
ターミナルでdotnet build
を実行します。
■設定ファイルの作成
「bin\Debug\net5.0-windows」に
WrapLauncher.path
という名前でファイルを作成します。
設定ファイルに起動したいフォルダパスやドキュメントファイルパス、プログラムパスなどを記述します。
ファイルの文字コードはBOM無しUTF-8。
色名、ボタン名、パスの間はTAB区切りです。設定ファイルのフォーマット//グループ見出しA 色名 ボタン名1 パス1 色名 ボタン名2 パス2 : //グループ見出しB 色名 ボタン名3 パス3 :TAB入力でSPACEになってしまう場合は、「ファイル」-「ユーザー設定」-「設定」で検索ボックスに
insert
を入力し、「Editor:Insert Spaces」のチェックを外してください。
ついでにwhite
で検索して「Render Whitespace」をall
にしておくとタブ文字が見えるようになります。
■実行
設定ファイルを保存したら実行します。
ウィンドウの幅を狭めるとボタンが折り返して表示されます。
リリースビルドするときはターミナルで
dotnet build -c Release
を実行します。
「Debug」フォルダの「WrapLauncher.path」を「Release」フォルダにもコピーしておきます。
「WrapLauncher.exe」を実行すればランチャーが起動します。
- 投稿日:2021-01-01T02:47:24+09:00
C#/DxLIbでNLuaを動かす
みなさんあけましてお願いします。今年もよろしくお願いします。
さて、今回はC#/DXライブラリでLuaを呼び出して使おうと思います。準備編
今回はLuaのライブラリとして、
NLua
を使います。あとVS2019をつかっています。
導入方法は、NuGetで、NLua
と検索していただくと出てくるはずです。
一番上の奴ですね。実装編
C#からluaを呼び出す
NLuaを使用するには、
using NLua
を先頭に記述してください。
C#/NLuaでのLuaの使用は以下の通りです。
今回はオブジェクト指向らしく(?)main関数から始めます。Lua.csclass luaTest { Lua lua = new Lua(); LuaFunction main; public void Init() { lua.DoFile("script.lua");//実行するluaファイル main = lua.GetFunction("main");//実行する関数名 } public void Main() { main.Call(); } public void Finalize() { lua.Close(); } }Main.csusing DxLibDLL; namespace Program{ class Program { static void Main() { DX.ChangeWindowMode(1); DX.DxLib_Init(); DX.SetDrawScreen(DX.DX_SCREEN_BACK); luaTest LuaTest = new luaTest(); LuaTest.Init(); while (true) { DX.ClearDrawScreen(); LuaTest.Main(); DX.ScreenFlip(); if (DX.ProcessMessage() < 0) break; } LuaTest.Finalize(); DX.DxLib_End(); } } }これで、C#側からの準備はできました。
次に、Lua側を準備しましょう。といっても普通に書くだけです。Script.luafunction main() --ここに処理をかく endですが、これではDxLibの関数をなにも使えません。なので、Luaに追加しましょう。
LuaからDxLibを使えるようにする
まぁこいつがなかなか面倒くさくて、C#側で関数を一つずつ登録しなきゃならんのです。いい方法があったら教えてほしいです。
Function.csusing DxLibDLL; class Func { public bool KeyLeftPress() { if(DX.CheckHitKey(DX.KEY_INPUT_LEFT) != 0) { return true; } else { return false; } } public bool KeyRightPress() { if (DX.CheckHitKey(DX.KEY_INPUT_RIGHT) != 0) { return true; } else { return false; } } public bool KeyDownPress() { if (DX.CheckHitKey(DX.KEY_INPUT_DOWN) != 0) { return true; } else { return false; } } public bool KeyUpPress() { if (DX.CheckHitKey(DX.KEY_INPUT_UP) != 0) { return true; } else { return false; } } public void DrawCircle(int x, int y, int r, uint color) { DX.DrawCircle(x, y, r, color); } // 以下省略 必要に応じて追加 }と、こんな感じでいちいち書かんといかんのです・・・。めんどくさいこと極まりない。我慢してください。
これをまとめてLuaに追加しましょう。
Lua.cs
を次のように変更して下さい。Lua.cspublic void Init() { lua["Func"] = new Func(); //関数を追加 lua.DoFile("script.lua");//実行するluaファイル main = lua.GetFunction("main");//実行する関数名 }これで、LuaからDxLibの関数を使えるようになりました。
Script.luax = 255 y = 255 function main() if Func:KeyLeftPress() == true then x = x - 4; elseif Func:KeyRightPress() == true then x = x + 4; end if Func:KeyUpPress() == true then y = y - 4; elseif Func:KeyDownPress() == true then y = y + 4; end Func:DrawCircle(x, y, 16, 0x00ff00); endでは実行してみましょう
十字キーを押すと円が移動するのが確認できたかと思います。
あとは関数を追加したりして頑張ってください。最後に
調べてもあまり出てこなかったので、困ってる人がいたら何かの助けになれば幸いです。指摘、改善点などコメント待ってます。