- 投稿日:2021-03-03T21:31:30+09:00
[C#/WPF/prism] ボタン押したときに時間がかかる処理をawaitでやるときに、ボタン連打を防止したい
もくじ
https://qiita.com/tera1707/items/4fda73d86eded283ec4fやりたいこと
prism(6.3)を使ったWPFアプリで、下記のような感じでボタンを押したときの処理を書きたい。
UserControl1の一部.xaml<Button Content="ボタン" Command="{Binding ButtonCommand}"/>UserControl1のビューモデル.cs// コンストラクタ public UserControl1ViewModel() { this.ButtonCommand = new DelegateCommand(async () => { // 時間のかかる処理 await Task.Delay(500); }); }が、上記をそのまま実行して、ボタンを連打すると、「時間のかかる処理」が終わってボタンの処理を抜ける前に何度も「時間のかかる処理」が走ってしまう。
これを、一度ボタンを押したら一連のボタン処理が終わるまでボタンを押せないように(ボタンを押したときの処理が同時に複数回走ってしまわないように)したい。
今回やったやり方
DelegateCommand
が持つObservesProperty
メソッドとDelegateCommand
の引数が2つある方のコンストラクタの第二引数canExecuteMethod
を使用する。
サンプル
// 有効無効を設定するプロパティを一つ作成する private bool _flag = true; public bool Flag { get { return this._flag; } set { this.SetProperty(ref _flag, value); } } ・ ・ ・ // コンストラクタ public UserControl1ViewModel() { this.ButtonCommand = new DelegateCommand(async() => { Flag = false; // ボタン押下直後に有効無効フラグをOFF(無効)にする await Task.Delay(500); // 時間のかかる処理 Flag = true; // 処理終了後に有効無効フラグをON(有効)にする }, () => Flag) // CanExecuteでFlagを見て有効無効を制御 .ObservesProperty(() => Flag); // ObservesPropertyでFlagを監視するよう指定する }やったことは言葉で書くと下記のとおり。
- 準備
- 有効無効を設定するためのプロパティを1つ作成する
- DelegateCommandをnewするときに
- 第一引数の、ボタンを押したときの処理を書くActionの冒頭でフラグをON、最後でフラグをOFFさせる
- 第二引数のCanExecuteで、そのフラグを返す(フラグのONOFFで有効無効を制御できるようにする)
- ObservesPropertyで、Flagが変化したらCanExecuteを実行してくれるようにする
備考
時間のかかる処理を実行中に「無効」になっている間、スタイルによってはボタンがグレーアウトしているように見える。
そういうのが気になるという場合は、無効でも見た目が変わらないようにするとか、さらなる逃げをしないといけなくなる可能性がある。またなにより、そういうことをしたいボタンが一つだったらこのやり方も現実的ではあるが、たくさんこういうボタンが画面上にあったとしたら、似たフラグだらけになりそう。
もっとうまいことできないものか...
- 投稿日:2021-03-03T19:18:11+09:00
【C#入門】初学者がASP.NETでWebアプリを作る:第7回
今回やること
・アカウント登録後、ログイン状態にせずログインさせる
・アカウントと給与データを紐づけるアカウント登録後、ログイン状態にせずログインさせる
必要なことは2つと考えました。
①アカウント登録後、ログインしていない状態にする
②アカウント登録後、ログイン画面に遷移する①アカウント登録後、ログインしていない状態にする
ログイン処理(Register.cshtml.csのOnPostAsyncメソッド)で下記の記述があるのでログインしてしまっています。
なのでこれを削除します。await _signInManager.SignInAsync(user, isPersistent: false);また、ホームページにリダイレクトしているところを、ログインページにリダイレクトするようにします。
public async Task<IActionResult> OnPostAsync(string returnUrl = null) { returnUrl ??= Url.Content("~/Identity/Account/Login"); ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList(); if (ModelState.IsValid) { var user = new IdentityUser { UserName = Input.UserName, Email = "" }; user.PasswordHash = new PasswordHasher<IdentityUser>().HashPassword(user, Input.Password); var result = await _userManager.CreateAsync(user, Input.Password); if (result.Succeeded) { _logger.LogInformation("アカウント登録が完了しました。"); //var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); //code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); //var callbackUrl = Url.Page( // "/Account/ConfirmEmail", // pageHandler: null, // values: new { area = "Identity", userId = user.Id, code = code, returnUrl = returnUrl }, // protocol: Request.Scheme); //await _emailSender.SendEmailAsync(Input.Email, "Confirm your email", // $"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>."); if (_userManager.Options.SignIn.RequireConfirmedAccount) { //return RedirectToPage("RegisterConfirmation", new { email = Input.Email, returnUrl = returnUrl }); } else { //await _signInManager.SignInAsync(user, isPersistent: false); return LocalRedirect(returnUrl); } } foreach (var error in result.Errors) { ModelState.AddModelError(string.Empty, error.Description); } } // If we got this far, something failed, redisplay form return Page(); }ログイン画面が表示されました。
右上を見るとログイン状態にもなっていませんので実装完了です。
アカウントと給与データを紐づける
やることは2つ。
①給与データ登録時に"DUMMY"としていた登録ユーザID・更新ユーザIDを、ログイン中のユーザ名で設定する
②一覧ページの表示時に、ログイン中のユーザの情報のみを表示するようにする
※ログインしていない状態で一覧ページには行けないようにしますがそれは後で。
今回はログイン中のユーザー名が取得できなければ何も表示しない(=引っかかるデータがない)にします。①給与データ登録時に登録ユーザID・更新ユーザIDをログイン中のユーザ名で設定する
SalariesControllerのCreateメソッドを書き換えます。
SalariesController.cspublic async Task<IActionResult> Create([Bind("Id,Beneficiary,PaymentType,PaymentDate,PaymentAmount,TravelExpence,HealthInsurancePremium,WelfarePension,EmploymentInsurancePremium,IncomeTax,ResidentTax,TotalPaymentAmount,OvertimeAllowance,MidnightAllowance,HolidayAllowance,Remarks,RegisterDate,RegisterUser,UpdateDate,UpdateUser")] Salary salary) { salary.RegisterDate = DateTime.Now; salary.RegisterUser = User.Identity.Name ?? "DUMMY"; salary.UpdateDate = DateTime.Now; salary.UpdateUser = User.Identity.Name ?? "DUMMY"; if (ModelState.IsValid) { _context.Add(salary); await _context.SaveChangesAsync(); return RedirectToAction(nameof(Index)); } return View(salary); }登録してみます。桁間違えて総支給50兆円になっちゃったけど5000兆円ほしい。
まだいじっていない編集画面で確認すると、ログイン中のユーザ名が登録されていることがわかります。
②一覧ページの表示時にログイン中のユーザの情報のみを表示する
同じくSalaryControllerのIndexメソッドを修正します。
登録ユーザIDがログイン中のユーザと同じものだけを返します。SalaryController.cspublic async Task<IActionResult> Index() { //return View(await _context.Salary.ToListAsync()); return View(await _context.Salary.Where(x => x.RegisterUser == (User.Identity.Name ?? "DUMMY")).ToListAsync()); }ログインしていない状態だと引っかかるデータがないため、なにも表示されません。
ログイン状態だとログイン中のユーザが登録したデータが表示されます。
簡単な修正をいくつか
数値の表示を整数にする
モデルクラスの各数値項目に、アノテーションを付けます。
// 支給額 [Display(Name = "支給額")] [Required(ErrorMessage = "支給額は必須入力です")] // ↓これをつけると3桁カンマあり小数0埋めなしになる [DisplayFormat(DataFormatString = "{0:#,##0.#}", ApplyFormatInEditMode = true)] public decimal PaymentAmount { get; set; }アカウント管理画面の修正
今のところパスワード変更くらいしか使わないので、それ以外消します。
_ManageNav.cs.htmlからPassword以外消します。
_ManageNav.cs.html@inject SignInManager<IdentityUser> SignInManager @{ var hasExternalLogins = (await SignInManager.GetExternalAuthenticationSchemesAsync()).Any(); } <ul class="nav nav-pills flex-column"> <!--<li class="nav-item"><a class="nav-link @ManageNavPages.IndexNavClass(ViewContext)" id="profile" asp-page="./Index">Profile</a></li> <li class="nav-item"><a class="nav-link @ManageNavPages.EmailNavClass(ViewContext)" id="email" asp-page="./Email">Email</a></li>--> <li class="nav-item"><a class="nav-link @ManageNavPages.ChangePasswordNavClass(ViewContext)" id="change-password" asp-page="./ChangePassword">パスワード</a></li> <!--@if (hasExternalLogins) { <li id="external-logins" class="nav-item"><a id="external-login" class="nav-link @ManageNavPages.ExternalLoginsNavClass(ViewContext)" asp-page="./ExternalLogins">External logins</a></li> } <li class="nav-item"><a class="nav-link @ManageNavPages.TwoFactorAuthenticationNavClass(ViewContext)" id="two-factor" asp-page="./TwoFactorAuthentication">Two-factor authentication</a></li> <li class="nav-item"><a class="nav-link @ManageNavPages.PersonalDataNavClass(ViewContext)" id="personal-data" asp-page="./PersonalData">Personal data</a></li>--> </ul>_LoginPartial.cs.htmlで右上のボタンを押したときの遷移先をChangePasswordにします。
_LoginPartial.cs.html@using Microsoft.AspNetCore.Identity @inject SignInManager<IdentityUser> SignInManager @inject UserManager<IdentityUser> UserManager <ul class="navbar-nav"> @if (SignInManager.IsSignedIn(User)) { <li class="nav-item"> <a id="manage" class="nav-link text-dark" asp-area="Identity" asp-page="/Account/Manage/ChangePassword" title="アカウント管理">@UserManager.GetUserName(User) としてログイン中</a> </li> <li class="nav-item"> <form id="logoutForm" class="form-inline" asp-area="Identity" asp-page="/Account/Logout" asp-route-returnUrl="@Url.Action("Index", "Home", new { area = "" })"> <button id="logout" type="submit" class="nav-link btn btn-link text-dark">ログアウト</button> </form> </li> } else { <li class="nav-item"> <a class="nav-link text-dark" id="register" asp-area="Identity" asp-page="/Account/Register">アカウント登録</a> </li> <li class="nav-item"> <a class="nav-link text-dark" id="login" asp-area="Identity" asp-page="/Account/Login">ログイン</a> </li> } </ul>ヘッダー部分(Manage your accountとかの部分)も変更します。
_Layout.cshtml@{ if (ViewData.TryGetValue("ParentLayout", out var parentLayout)) { Layout = (string)parentLayout; } else { Layout = "/Areas/Identity/Pages/_Layout.cshtml"; } } <h2>アカウント管理</h2> <div> <h4>アカウント情報を変更します。</h4> <hr /> <div class="row"> <div class="col-md-3"> <partial name="_ManageNav" /> </div> <div class="col-md-9"> @RenderBody() </div> </div> </div> @section Scripts { @RenderSection("Scripts", required: false) }ChangePassword画面も日本語にしておきます。
ChangePassword.cshtml@page @model ChangePasswordModel @{ ViewData["Title"] = "パスワード変更"; ViewData["ActivePage"] = ManageNavPages.ChangePassword; } <h4>@ViewData["Title"]</h4> <partial name="_StatusMessage" for="StatusMessage" /> <div class="row"> <div class="col-md-6"> <form id="change-password-form" method="post"> <div asp-validation-summary="All" class="text-danger"></div> <div class="form-group"> <label asp-for="Input.OldPassword"></label> <input asp-for="Input.OldPassword" class="form-control" /> <span asp-validation-for="Input.OldPassword" class="text-danger"></span> </div> <div class="form-group"> <label asp-for="Input.NewPassword"></label> <input asp-for="Input.NewPassword" class="form-control" /> <span asp-validation-for="Input.NewPassword" class="text-danger"></span> </div> <div class="form-group"> <label asp-for="Input.ConfirmPassword"></label> <input asp-for="Input.ConfirmPassword" class="form-control" /> <span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span> </div> <button type="submit" class="btn btn-primary">パスワード変更</button> </form> </div> </div> @section Scripts { <partial name="_ValidationScriptsPartial" /> }ホームをSalaties/Indexにする
今の時点では、「localhost:ポート番号/」にアクセスすると、ようこそ的な画面になりますが、
Indexページを表示するようにします。
デフォルトをSalaries/Indexにするようにしました。(Homeはいらない。)
ヘッダーの「ホーム」やシステム名のところは直接Home/Indexを呼ぶようになっているので、それも直します。Startup.csapp.UseEndpoints(endpoints => { endpoints.MapControllerRoute( name: "default", pattern: "{controller=Salaries}/{action=Index}/{id?}"); endpoints.MapRazorPages(); });_Layout.cshtml<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>@ViewData["Title"] - 給与管理システム(SalaryManagementSystem)</title> <link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" /> <link rel="stylesheet" href="~/css/site.css" /> </head> <body> <header> <nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3"> <div class="container"> <a class="navbar-brand" asp-area="" asp-controller="Salaries" asp-action="Index">給与管理システム(SalaryManagementSystem)</a> <button class="navbar-toggler" type="button" data-toggle="collapse" data-target=".navbar-collapse" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between"> <ul class="navbar-nav flex-grow-1"> <li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-controller="Salaries" asp-action="Index">ホーム</a> </li> <li class="nav-item"> <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a> </li> </ul> <partial name="_LoginPartial" /> </div> </div> </nav> </header> <div class="container"> <main role="main" class="pb-3"> @RenderBody() </main> </div> <footer class="border-top footer text-muted"> <div class="container"> © 2021 - 給与管理システム(SalaryManagementSystem) - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a> </div> </footer> <script src="~/lib/jquery/dist/jquery.min.js"></script> <script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script> <script src="~/js/site.js" asp-append-version="true"></script> @await RenderSectionAsync("Scripts", required: false) </body> </html>ログアウト時にログイン画面に遷移
ログアウトのPOSTリクエストに対する処理を書き換えて、ログイン画面に遷移します。
なんだか無理やりすぎな気がする…Logout.cshtml.cspublic async Task<IActionResult> OnPost(string returnUrl = null) { await _signInManager.SignOutAsync(); _logger.LogInformation("User logged out."); if (returnUrl != null) { return LocalRedirect(Url.Content("~/Identity/Account/Login")); } else { return RedirectToPage(); } }未ログイン状態でのアクセス制御
基本、未ログイン状態でSalary操作系のページにはアクセスしてほしくありません。
まずはSalary操作系のページ全てに対して匿名ユーザのアクセスができないようにします。
SalariesControllerクラスにAuthorize属性を付けます。SalariesController.csnamespace SalaryManagementSystem.Controllers { [Authorize] public class SalariesController : Controller {ホーム(Indexにリダイレクトされる)を開くと、上記のアクセス制御によりログイン画面に遷移します。
次回予告
次回はデータを可視化する方法を考えます。
- 投稿日:2021-03-03T14:14:34+09:00
Discord.Net 備忘録
前置き
作りながら得た知識の備忘録なので、一部不正確な情報があるかもしれません。本記事の内容をうのみにするのではなく、API開発者のドキュメントを確認するようお願いします。なお本記事は1から作ってみよう!といった内容ではなく、APIの解説寄りになっています。
感想、指摘等コメントお待ちしております。Discord Botとは
Discordを使う上でロールの変更など、手動でやるのは面倒な操作がありますよね。そんな時はプログラムに働いてもらいましょう!
こちらのDeveloper PortalからBotを簡単に作成できます。詳細の手順が気になる場合は、他の方の記事を確認してみてください。Discord.Netについて
Discord Botを作るためのAPIは様々なものが公開されています。Discord.pyやDiscord.jsがとくに有名なようです。
今回はMicrosoftの.NETで動くDiscord.Netを採用することにします。理由は私が.NET好きだというのと、Linqなど扱いやすいライブラリが多い(個人差あり)からです。
ちなみに同じく.NET用APIでDSharpPlusというものも公開されています。0. サンプル
1. 初期設定
Visual Studio 2019で.NET Frameworkもしくは.NET Coreのコンソールアプリケーション用プロジェクトを作成します。本記事執筆時点でフレームワークのバージョンは.NET Core 3.1を採用しています。
Discord.NETのライブラリはNuGetから落とせます。NuGetパッケージマネージャーを開き、Discord.Net
とMicrosoft.Extensions.DependencyInjection
をプロジェクトにインポートします。2. 初期化
Botを動かすために必要なものは大きく分けて3種類あります。順を追って説明していきます。
1. Bot本体
Discord.WebSocket.DiscordSocketClient
クラスはユーザーとしての基本機能を提供します。基本的にこのクラスのインスタンスを通してBot自身への操作をしていくことになります。2. コマンド
Discord.Commands.CommandService
クラスは後述するコマンド群を管理するために使用されます。コマンドの登録はDependency Injection(依存性注入)を利用して自動で行われます。3. サービスプロバイダー
Dependency Injectionによってコマンドを登録、取得するために使用されます。
これらをコードにすると以下のようになります。
private DiscordSocketClient client; private CommandService commands; private IServiceProvider serviceProvider; public async Task MainAsync(){ serviceProvider = new ServiceCollection().BuildServiceProvider(); commands = new CommandService(); await commands.AddModulesAsync(Assembly.GetEntryAssembly(), serviceProvider); client = new DiscordSocketClient(); }3. 処理の登録
クライアントにはメッセージ受信時をはじめ、様々なタイミングで呼ばれるイベントが実装されています。ここに処理を登録することでBotに固有の処理をさせることができます。
public async MainAsync(){ client.Log += Log; client.MessageReceived += CommandReceived; } private async Task Log(LogMessage message){ Console.WriteLine(message.ToString()); } private async Task CommandReceived(SocketMessage message){ var msg = msg as SocketUserMessage; if(message == null) return; if(message.Author.IsBot) return; int argPos = 0; var command = new CommandContext(client, msg); await commands.ExecuteAsync(command, argPos, serviceProvider); }4. コマンドの作成
Discord.Commands.ModuleBase
を継承したクラスにメソッドを追加することで、独自のコマンドを実装することができます。public class CommandModule : ModuleBase{ /// <summary> /// Helloと返す /// </summary> [Commands("hello")] //コマンドとして使用する文字列をアトリビュートで設定 public async Task SayHelloAsync(){ await ReplyAsync("Hello!"); } }引数付きのコマンドを作成する場合は以下のようにします。
public class CommandModule : ModuleBase{ /// <summary> /// 入力したロールが付与されたユーザー名を列挙する。 /// </summary> [Commands("show")] public async Task EchoAsync([Summary("対象のロール名")] string rollName){ var role = Context.Guild.Roles.FirstOrDefault(rl => rl.ToString() == rollName); var users = Context.Guild.GetUsersAsync(); foreach(var _user in users){ var user = _user as SocketGuildUser; if(user.Roles.Contains(role)){ await ReplyAsync(user.Username); } } } }詳細設定
一部API機能の使用にはDiscord Gateway Intentを有効にする必要があります。例えば上記のコマンド例で使用した、ユーザーリストを取得するメソッドを有効化するには、これらのインテントを有効化が必要です。
Discord Developer Portalの個別アプリケーションのページから設定を操作できます。その他
コマンドの登録のために使用しているDependency Injection(依存性注入)は、クラス間の依存関係を疎結合にし、単体テストをしやすくするためにしばしば使われます。詳細はDependency Injectionについてまとめている記事をご確認ください。
- 投稿日:2021-03-03T11:33:40+09:00
連続した処理待ちで中間をキャンセルする排他制御
表現が難しい…。
タスクが非同期で複数殺到して処理待ちになるような状況で、現在処理中のタスクと一番最後に来たタスクだけ残して、その他(中間)はキャンセルとする。というものを実装したくなった。
実現したいこと
下記のようなイメージ。
・タスク1がやってくる 1 → ( ) ・タスク1が重めの処理を開始、続けてタスク2がやってくる 2 → (1) ・タスク2はタスク1が処理中なので待機、続けてタスク3がやってくる 3 → [2](1) ・この時点で、 3[2](1) ・タスク2は無かったことになり消滅 3[ ](1) ・タスク3がタスク1の次で待機 [3](1)例えばスライダーコントロールとかで、値が変化するたびに値に応じて何らかの処理をしないといけないけど、その処理が若干重い場合。
全部律儀にこなしてたらガクガクになっちゃうので、1発目と最後のだけ処理して、他は捨てたい、そんな時。アルゴリズム
セマフォを2つ使う。1つ目は初期値2、2つ目は初期値1にする。そしてセマフォとは別にカウンタXを用意して、3つ目以降のタスクが来たら増やすようにする。
wait1、wait2はそれぞれセマフォ1、セマフォ2のwaitの箇所。・タスク1がやってくる カウンタX wait1 wait2 処理 0 2 1 1 → [ ] [ ] ( ) ・タスク1が2か所のwaitを通過し、重めの処理を開始。タスク2が来る カウンタX wait1 wait2 処理 0 1 0 2 → [ ] [ ] (1) ・タスク2はwait1を通過するが、wait2のカウンタは0なので足止め、タスク3が来る カウンタX wait1 wait2 処理 0 0 0 3 → [ ] [2] (1) ・wait1の直前でタスク3はwait1のカウンタを見て、0なら… カウンタX wait1 wait2 処理 0 0 0 3[ ] [2] (1) ・カウンタXを1増やし、セマフォ2とセマフォ1のカウンタを1ずつ解放 カウンタX wait1 wait2 処理 1 1 1 3[ ] [2] (1) ・タスク2とタスク3がそれぞれwaitを通過するが… カウンタX wait1 wait2 処理 1 0 0 [ ]3 [ ]2 (1) ・wait2の出口には罠が仕掛けてあり、もしカウンタXが0じゃなかった場合、 タスク2はカウンタXを1減らして、自らは退散する タスク3はwait2で足止め。つまりタスク3が来る前の状態に戻る カウンタX wait1 wait2 処理 0 0 0 [ ] [3]* (1) ・処理を終えたタスク1は、セマフォ2とセマフォ1のカウンタを1ずつ解放して退散 カウンタX wait1 wait2 処理 0 1 1 [ ] [3] ( )* ・タスク3がwait2を通過して処理に入る。これはタスク2が来る前の状態と同じ カウンタX wait1 wait2 処理 0 1 0 [ ] [ ] (3) ・タスク3も処理を終えるとセマフォのカウンタを解放して退散 これは一番最初の状態に戻ったことになる カウンタX wait1 wait2 処理 0 2 1 [ ] [ ] ( )*こんな感じでイケるんじゃないかと!
実装
C#でやってみたけど、まあ他の言語でも出来そう。
//-------------------- private System.Threading.SemaphoreSlim semaphore1 = new System.Threading.SemaphoreSlim(2, 2); private System.Threading.SemaphoreSlim semaphore2 = new System.Threading.SemaphoreSlim(1, 1); private int count_x = 0; //-------------------- private void CutTask() { Task.Run(() => { if (semaphore1.CurrentCount == 0) { System.Threading.Interlocked.Increment(ref count_x); semaphore2.Release(); semaphore1.Release(); } semaphore1.Wait(); semaphore2.Wait(); if (count_x > 0) { System.Threading.Interlocked.Decrement(ref count_x); return; } HeavyProcess(); semaphore2.Release(); semaphore1.Release(); }); } //--------------------感想
4つ目以降のタスクが来たら処理順が変わっちゃうんじゃないかとも思ったけど、3つ目が来てから2つ目が抹殺されて詰められるまでは多分一瞬なので、基本的に一度に3つ以降のタスクが長時間溜まるような状況は無いんじゃないかと思う。あまり自信無し。
- 投稿日:2021-03-03T10:33:20+09:00
【C#】魔王となってモンスターを産み出し、インスタンスを理解する
はじめに
プログラムを学び始めると、「インスタンスって何や?」という疑問に必ずぶつかると思う。
よく「実体」と訳され、教則本などでも紹介されるが正直いまいちピンとこないだろう。
また、クッキーの調理にも例えられ、「クラスは型抜きでインスタンスはクッキー」と表現されることも多い。
ある程度慣れてきたら「ああそういうことだったのね」となるが、初学者には正直意味が分からないと思う。僕もそうだった。
この記事では、インスタンスの概念を知るきっかけとして簡単なプログラムを紹介しようと思う。設定
あなたは魔王です。
世界を征服するため、まずは部下のモンスターを10匹産み出すことになりました。
しかし、産み出されたモンスターのHPは最初たったの「3」しかありません。
産み出されたモンスターはみんな同じHPなので無個性です。
そこで魔王は、闇の力を使ってモンスターに追加でランダムなHPを与えることにしました。
するとHPが高いモンスター、低いモンスターが産み出され、個性豊かになりました。サンプルプログラム
Program.csusing System; using System.Collections.Generic; using static System.Console; class Program { static void Main(string[] args) { MonsterGenerator(10); ReadKey(); } /// <summary> /// モンスターを生成する /// </summary> /// <param name="number">生成したいモンスターの数</param> static void MonsterGenerator(int number) { Random rd = new Random(); var MonsterList = new List<Monster>(); //モンスターを格納するリスト for (int i = 0; i < number; i++) { //リストに生成したモンスターのインスタンスを追加 MonsterList.Add(new Monster()); } int RoopCount = 0; ; foreach (var Monster in MonsterList) { //Randomクラスからランダム値を生成 //HP値が最大100になるように範囲を設定 int randomValue = rd.Next(97); RoopCount++; string AddHP = ""; for (int i = 1; i <= randomValue; i++) { //ランダム値の分だけ「|」を生成してAddHPに追加 AddHP = AddHP + "|"; } //出力 WriteLine("Moster" + Convert.ToString(RoopCount) + "HP:" + Monster.initHP + AddHP); } } } /// <summary> /// モンスタークラス パラメータとして初期HP「|」を3本所持 /// </summary> class Monster { public string initHP = "|||"; }解説
MonsterGeneratorメソッドを使ってモンスターを作成する。
/// <summary> /// モンスタークラス パラメータとして初期HP「|」を3本所持 /// </summary> class Monster { public string initHP = "|||"; }モンスターの元となるのはMonsterクラスであり、initHP(初期HP)というパラメータが設定されている。
このMonsterクラスがモンスターの「設計図」にあたる。MonsterGeneratorメソッド内の
for (int i = 0; i < number; i++) { //リストに生成したモンスターのインスタンスを追加 MonsterList.Add(new Monster()); }の部分で、設計図(Monsterクラス)からモンスターを産み出している。
これがいわゆる「インスタンス」である。
「設計図から実際に産み出されたもの」なので、これが「実体」と訳される理由となるのだ。ただ、これだけではせっかく10匹のモンスターを産み出してもみんなHPが「3」で無個性である。
いうなれば、産み出されたモンスターは全員ただのスライムであるということだ。
スライムだけじゃなくてドラキーとかドラゴンとか、もっと色んな種類のモンスターが欲しくなるだろう。foreach (var Monster in MonsterList) { //Randomクラスからランダム値を生成 //HP値が最大100になるように範囲を設定 int randomValue = rd.Next(97); RoopCount++; string AddHP = ""; for (int i = 1; i <= randomValue; i++) { //ランダム値の分だけ「|」を生成してAddHPに追加 AddHP = AddHP + "|"; } //出力 WriteLine("Moster" + Convert.ToString(RoopCount) + "HP:" + Monster.initHP + AddHP); }この部分で、リストに格納したスライム達(Monsterクラスのインスタンス)を順番に取り出し、闇の力を使ってランダムに追加のHPを与えている。
そうすることでスライムはHPがたくさん増えてドラゴンになったり、ちょっとしか増えずにドラキーになったりするわけだ。出力結果
Moster1HP:|||||||||||||||||||||||||||||||||||||||||||||||||||||||| Moster2HP:|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| Moster3HP:||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| Moster4HP:||||||||||||||||||||||||||||||||||||||||||||||||||||| Moster5HP:|||||||||||||||||||||||||||||||||||||| Moster6HP:|||||||||||||||||||||| Moster7HP:||||||||||||||||||||||||||||||| Moster8HP:||||||||||||| Moster9HP:||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| Moster10HP:||||||||||||||||||||いろいろなHPを持ったモンスターが産み出されて、多種多様になった。
世界征服はここから始まる!おわりに
今回のプログラムではシンプルにモンスターのHPだけを設定したが、応用すれば攻撃力や防御力など様々なパラメータを与えることも可能になる。
プログラムの世界では、自分が魔王(もしくは神)になったつもりでプログラミングをするとイメージがつきやすいかもしれない。
オブジェクト指向はまだまだ奥が深く、こんな記事ではなかなか理解に到達するには程遠いが、最初の一歩として理解を進めていくきっかけになればと思う。
- 投稿日:2021-03-03T07:49:12+09:00
【Unity tips】 Cloth機能を使ってみよう
今回は、Cloth【クロス】機能の使い方を見ていきます。
クロス機能は知ってますかね?
オブジェクトを布のような動きにすることができる機能です。
これを使えば
キャラクターの髪の毛をゆらゆらしたり
動物の尻尾をゆらゆらしたり
スカートをひらひらひらひらしたりできるんです。
あとは、オブジェクトを布で包んだりもできますね。
それでは、今回は試しにプレーンをゆらゆらしていきます。
まずはプレーンを入れて
インスペクターにある Meshcolliderの凸状にチェック
【ついていなければMeshcolliderを入れていきます】
そしてトリガーにも、チェックを入れます。
そして コンポーネントを追加から
Clothを入れます。
Clothを入れると、自動的にskinnedも入ります。
で次にですね。
Clothの右下にある緑矢印のアイコンを選択します。
そうすると、 シーンビューにクロスのセルフコリジョンというメニューが
出てくるので、 そこで選択を選びます。
そして、 囲みたい所をドラッグして選択していきます。
そして囲んだら すぐ下にある
セルフコリジョンとインターコリジョンにチェックを入れます。
この画像ですと、 プレーンの上側だけを囲んでいます。
囲む所は、固定したい所になります。
囲んだ所は青い点に変わります。
つまり、囲んでいない所がゆらゆらするということです。
ではこれで再生してみましょう。
この状態でプレーンを動かしてみると
画像のように、 プレーンが 布のようにゆらゆらしていると思います。
このようになっていれば成功です。
※注意点としては、 1回囲んでしまうと、ポインターが消えてしまうので
また設定をやり直す必要があるんですよね。
PS
Unityの寺子屋ユニコーンもやってまーす。
一部、Unity講座なども値下げしましたよ。
- 投稿日:2021-03-03T04:16:17+09:00
データサイエンス100本ノックを C# でやってみた
データサイエンス100本ノック を C# でも
データサイエンティスト協会 様が公開してくれている
データサイエンス100本ノックを C# でもやってみました。
まだ日本語の情報は少ないですが C# でも データサイエンス関連のライブラリがすこしづつ充実してきている感じです。
今回 MathNet.Numerics を利用しました。
その Jupyter Notebook の ページがこちら 『 データサイエンス100本ノック C# 実行結果 (Notebook Viewer) 』
Visual Studio Code 上に 環境を構築すると LINQ を使って わりとサクサク 回答する事ができました。
Docker イメージと 回答例 を github 上に公開 しています。
Docker 環境構築が 面倒な方でも Web 上で 直接実行できる環境があります。
このボタン
をクリックして 画面が表示されるまで しばらくお待ちください。
実際に データサイエンス 100本ノック C# 版 の コードを WEB 上で動かす事ができます。Visual Studio Code で Jupyter Notebook 環境構築手順
Visual Studio Code をインストールし、
.NET Interactive Notebooks
パッケージを追加し.NET 5 SDK
を インストールするとVisual Studio Code で サクサク開発ができます。
自動補完の機能が便利です。
Jupyter Notebook の 実行環境 Docker Image 説明
Jupyter Notebook を 簡単に試すための Docker Image を作成しました。
Docker Desktop を インストールしている環境で
git clone https://github.com/kkato233/100knocks-data-scientist-cs cd 100knocks-data-scientist-cs docker-compose up -d --buildとすると
http://localhost:8889/
の アドレスで .NET 対応版の Jupyter Notebook が開きます。Jupyter Notebook の環境構築は Docker が便利ですね。
Python 複数バージョンが競合したり、ライブラリが変に競合する事がありません。
誰かが作ってくれたイメージをありがたく使わせてもらいましょう。
- 投稿日:2021-03-03T02:34:19+09:00
Accessランタイム2019でaccdb へアクセス(C#)
Accessランタイムの場合、Access.Application = new Access.Applicationで落ちるのでその対策
private void button1_Click(object sender, EventArgs e) { Application ac = null; IWshShell3 ws = (WshShell)Interaction.CreateObject("WScript.Shell", ""); ws.Run(@"C:\work\test.accdb"); ac = (Application)Marshal.GetActiveObject("Access.Application"); //アクセスの処理 ac.Quit(); }accdbかmdbを開いておく必要があるので、ダミーファイルが必要になってしまうのと、
Run実行後にAccessのダイアログが入るのがいまいち。
開くを押下後でないとGetActiveObjectで例外エラーになるのでダイアログをどうにかする必要がある。
レジストリ書き換えるか何かほかに方法があるか...