- 投稿日:2019-12-11T23:06:34+09:00
AvaloniaUIでマルチプラットフォームなGUI (入門)
この記事はSFC-RG Advent Calendar 2019の10日目です。
概要
AvaloniaUIで簡単になんか作る。
動機
マルチプラットフォームなGUIを作りたい。
子プロセスで別のプログラム動かしたい。
HTMLとCSSが苦手。
C#が好き。注意
筆者もそこまでAvaloniaUIとか詳しくない。
MVVMを全く理解していない。
XAMLの書き方も我流。間違ってたら優しく教えてくれると嬉しいです。
環境構築
DotnetCoreSDKを入れる。(本記事では3.0.100)
https://dotnet.microsoft.com/downloadAvaloniaUIのテンプレートを入れる。
cd ~ mkdir workspace cd workspace git clone https://github.com/AvaloniaUI/avalonia-dotnet-templates dotnet new --install ~/workspace/avalonia-dotnet-templatesプロジェクトの作成
MyAppという名前でプロジェクトを作成
cd ~/workspace dotnet new avalonia.mvvm -o MyApp cd MyApp初期状態はこんな感じ
> ls App.xaml Assets MyApp.csproj ViewLocator.cs Views App.xaml.cs Models Program.cs ViewModels nuget.config動かしてみる
dotnet runwindowが出ました。
UIを作っていく
Views/MainWindow.xamlを弄っていく。
WidthとHeightを追加してwindowのサイズを決める。(ここでは800 * 800)
<Window xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="clr-namespace:MyApp.ViewModels;assembly=MyApp" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" Width="800" Height="800" x:Class="MyApp.Views.MainWindow" Icon="/Assets/avalonia-logo.ico" Title="MyApp">ボタンを足してやる。あとTextBlockは邪魔なので消す。
Window.Stylesでは各パーツのデザインをクラス毎にまとめて設定できる。
(ここではButtonパーツのtestというクラス)<Design.DataContext> <vm:MainWindowViewModel/> </Design.DataContext> <Window.Styles> <Style Selector="Button.test"> <Setter Property="Width" Value="100" /> <Setter Property="Height" Value="25" /> </Style> </Window.Styles> <Button Classes="test"> Click Me! </Button> </Window>ど真ん中にボタンが登場。
バインドしてみる
ボタンに名前をつける。
<Button Classes="test" Name ="testButton"> Click Me! </Button>MainzWindow.xaml.csを弄る。
using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; using Avalonia.Interactivity; namespace MyApp.Views { public class MainWindow : Window { private Button _testButton; public MainWindow() { InitializeComponent(); // xaml内から該当のボタンを探す _testButton = this.FindControl<Button>("testButton"); // バインドする _testButton.Click += TestButtonClicked; } private void InitializeComponent() { AvaloniaXamlLoader.Load(this); } private void TestButtonClicked(object sender, RoutedEventArgs e) { // クリックされたらボタンのサイズを変える _testButton.Height = 200; } } }実行
dotnet runまとめ
とりあえず簡単にバインドするところまで。
デフォルトでファイル選択ダイアログとかも使えるし、グラフとかもhttps://github.com/AvaloniaUI/oxyplot-avalonia あたり使えば書けるので簡単なGUIはこれで足りそう。
ただ、バージョンがが1.0未満でドキュメントがほとんどないのでgithubで他の人のコードを漁ることになる。
xamlの書き方がUWPと微妙に違うので結構困る。
困ったら、公式のAPI等をみることになる。
https://avaloniaui.net/api/あと公式からチュートリアルも出てるのでそちらも是非に。
https://avaloniaui.net/docs/tutorial/気が向いたら続き書きます。
- 投稿日:2019-12-11T22:58:12+09:00
C# 8 で ベジェ線の計算
2つ以上の点からベジェ線を計算するコード。
public static class Bezier { public static Vector3 Get(float t, params Vector3[] points) => Get(t, points.AsSpan()); public static Vector3 Get(float t, ReadOnlySpan<Vector3> points) => (points.Length, t) switch { _ when t < 0 || t > 1 => throw new Exception(""), var (i, _) when i < 2 => throw new Exception("Points must be >= 2"), var (i, _) when i == 2 => points[0] + t * (points[1] - points[0]), var (i, _) => Get(t, points.Slice(0, i - 1)) + t * (Get(t, points.Slice(1, i - 1)) - Get(t, points.Slice(0, i - 1))), }; }だいぶ関数型っぽいコードになってきた気がする。
- 投稿日:2019-12-11T22:24:04+09:00
Blazorにおける認証まとめ(Client版) FirebaseAuthenticationを使ったサンプル付き
概要
Blazorにおけるログイン認証の手法の個人的なまとめです。
下記のようなログイン画面を実装する際のメモです。前提
.NET Core SDK 3.1.100
Microsoft.AspNetCore.Blazor.Templates::3.1.0-preview4.19579.2
Visual Studio 2019 16.4.0その他、記事内で使用しているパッケージ
Microsoft.AspNetCore.Components.Authorization 3.1.0
Blazored.LocalStorage 2.1.1
FirebaseAuthentication.net 3.4.0
MatBlazor 2.0.0WebAssembly版(Client版)での話となります。
SPAにおける認証の実装
過去VueやNuxtで作成したSPAは下記のような流れで実装を行いました。
(どこまで一般的か不明ですが…)
- ユーザIDとパスワードで認証
- CognitoやFirebaseAuthenticationのようなサービスや自作API等でユーザを認証する
- 認証が通るとトークンが発行されるのでローカルストレージと状態管理ストア(Vuex,Reduxなど)に保存
- 各WEB-APIを呼び出す時にトークンをヘッダ(bearerがよく使われる)に入れることでAPIが認可されて実行可能となる
- トークンが期限切れ等の場合、ログイン画面に戻る
(リフレッシュトークンによる自動認証場合は割愛)今回も、上記のような考え方で実装を行います。
Blazorにおける認証
準備
BlazorのWebAssembly版(Client版)のテンプレートからプロジェクトを作成後、
認証関係のモジュールのインストールが必要です。
Nugetから下記のモジュールをインストールしてください。Microsoft.AspNetCore.Components.Authorization
インストール後、_imports.razorに下記の参照を追加
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.AuthorizationUI要素
まずは、UI側に関する説明を行います。
AuthorizeView
ページ内の特定の要素だけ認証時に表示させたい内容には、下記のようにAuthorizeViewタグを付与することで実現できます。
@context.User.Identityでユーザ名等を使用することが可能です。<AuthorizeView> <Authorized> <h1>Hello, @context.User.Identity.Name!</h1> <p>You can only see this content if you're authenticated.</p> </Authorized> </AuthorizeView>Authorize属性
ページ全体を認証時にのみ表示を行いたい場合には、[Authorize]属性を付与します。
なお、未認証時にログイン画面にリダイレクトさせたい場合の方法は下記で紹介しています。
https://qiita.com/nobu17/items/d43b18b8d42e7d0b4535@page "/counter" @attribute [Authorize] <h1>Counter</h1> <p>Current count: @CurrentCount</p> <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>AuthenticationStateProviderによる認証
先ほど、紹介したUI要素が認証及び未認証といった状態を認識するための仕組みとして
AuthenticationStateProviderといった抽象クラス提供されています。
このクラスを実装することで認証の仕組みが実現できます。namespace Microsoft.AspNetCore.Components.Authorization { public abstract class AuthenticationStateProvider { protected AuthenticationStateProvider(); public event AuthenticationStateChangedHandler AuthenticationStateChanged; public abstract Task<AuthenticationState> GetAuthenticationStateAsync(); protected void NotifyAuthenticationStateChanged(Task<AuthenticationState> task); } }GetAuthenticationStateAsync
認証が必要となったタイミングでこのメソッドが呼び出されます。
戻り値に返されるAuthenticationStateの値により認証されたかどうかを判断します。NotifyAuthenticationStateChanged
ログイン/ログアウト等で認証状態が変化した場合にこのメソッドを呼び出すことで状態変更がAuthorizeViewなどに通知されます。
実装例
AuthenticationStateProvider
AuthenticationStateProviderを継承した認証プロバイダを作成します。
前述のとおり、ローカルストレージに認証情報を保存するために、Blazored.LocalStorageを使用します。
Nugetからインストールしてください。public class SpaAuthticateProvider : AuthenticationStateProvider { private readonly HttpClient _httpClient; private readonly ILocalStorageService _localStorage; public SpaAuthticateProvider(HttpClient httpClient, ILocalStorageService localStorage) { _httpClient = httpClient; _localStorage = localStorage; } public async override Task<AuthenticationState> GetAuthenticationStateAsync() { // ローカルストレージからトークンとユーザ名を取得 var savedToken = await _localStorage.GetItemAsync<string>("authToken"); var userID = await _localStorage.GetItemAsync<string>("userID"); // トークンのチェックを入れる場合ここで一度だけ実施 // トークンが見つからい場合は未ログイン if (string.IsNullOrWhiteSpace(savedToken)) { return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity())); } // HTTPの認証用のトークンを設定 _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", savedToken); // 認証情報を返す return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(ClaimTypes.Name, userID) }, "apiauth"))); } public async Task MarkUserAsAuthenticated(string userID, string authToken) { // ローカルストレージに認証情報を保持して変更通知を行う await _localStorage.SetItemAsync("userID", userID); await _localStorage.SetItemAsync("authToken", authToken); NotifyAuthenticationStateChanged(GetAuthenticationStateAsync()); } public async Task MarkUserAsLoggedOut() { // ローカルストレージの認証情報を削除して変更通知を行う await _localStorage.RemoveItemAsync("userID"); await _localStorage.RemoveItemAsync("authToken"); if (_httpClient.DefaultRequestHeaders.Authorization != null) { _httpClient.DefaultRequestHeaders.Authorization = null; } NotifyAuthenticationStateChanged(GetAuthenticationStateAsync()); } }ILocalStorageService及びHttpClientをコンストラクタからDIします。
HttpClientをDIする理由は、認証用のトークンをヘッダに設定するためです。
(HttpClientはインスタンスが共有される。)ローカルストレージに認証情報を保持することで画面をリロードしてもトークンが期限切れになるまで再度ログインする必要がなくなります。
また、外部のサービスから認証変更を通知するためのメソッドを追加します。認証サービスとの連携サンプル
認証するWEBAPIと連携してAuthenticationStateProviderに認証場を渡すクラスを作成します。
まずは下記のようなログイン用のインタフェースとモデルを定義します。public interface IAuthService { Task<LoginResult> LoginAsync(LoginModel loginModel); Task LogoutAsync(); } public class LoginModel { public string UserID { get; set; } public string Password { get; set; } } public class LoginResult { public bool IsSuccessful { get; set; } public Exception Error { get; set; } public string IDToken { get; set; } }実際のAPIをコールせずにダミーの応答を返すような場合は下記となります。
public class DummyAuthService : IAuthService { private readonly AuthenticationStateProvider _authenticationStateProvider; public DummyAuthService(AuthenticationStateProvider authenticationStateProvider) { _authenticationStateProvider = authenticationStateProvider; } public async Task<LoginResult> LoginAsync(LoginModel loginModel) { // 3秒待機させて本当の応答のように見せる await Task.Delay(3000); if (loginModel.UserID == "demo" && loginModel.Password == "demo") { var res = new LoginResult() { IsSuccessful = true, IDToken = "hoge" }; await ((SPAAuthticateProvider)_authenticationStateProvider).MarkUserAsAuthenticated(loginModel.UserID, res.IDToken); return res; } else { return new LoginResult() { IsSuccessful = false, Error = new AuthenticationException("NotAuthrized") }; } } public async Task LogoutAsync() { await ((SPAAuthticateProvider)_authenticationStateProvider).MarkUserAsLoggedOut(); }依存性注入
最後にStartup.csにおいてクラスの指定を行います。
AddAuthorizationCoreメソッドも追加してください。Startup.cspublic class Startup { public void ConfigureServices(IServiceCollection services) { services.AddScoped<AuthenticationStateProvider, SpaAuthticateProvider>(); services.AddScoped<ILocalStorageService, LocalStorageService>(); services.AddScoped<IAuthService, DummyAuthService>(); services.AddScoped<ILocalStorageAuth, LocalStorageAuthticateProvider>(); services.AddAuthorizationCore(); } }ログイン画面からの呼び出し
作成したDummyAuthServiceをログイン画面から呼び出すことで認証処理を行います。
下記に例を示します。
(UIはMatBlazorを使用しています。)@page "/login" @inherits LoginViewModel // LoadingScreenはスピナーの表示を行う自作モジュール <LoadingScreen IsLoading="@IsLoading" /> <div class="mat-layout-grid"> <div class="mat-layout-grid-inner"> <div class="mat-layout-grid-cell mat-layout-grid-cell-span-12 mat-layout-grid-align-center"> <h3>Login</h3> </div> <EditForm Model="@LoginData" OnValidSubmit="SubmitAsync" class="mat-layout-grid-cell mat-layout-grid-cell-span-12"> <DataAnnotationsValidator /> <ValidationSummary /> <div class="mat-layout-grid-inner"> <div class="mat-layout-grid-cell mat-layout-grid-cell-span-12"> <MatTextField FullWidth="true" Label="UserID" @bind-Value="@LoginData.UserID"></MatTextField> </div> <div class="mat-layout-grid-cell mat-layout-grid-cell-span-12"> <MatTextField FullWidth="true" Label="Password" @bind-Value="@LoginData.Password" Type="password"></MatTextField> </div> <div class="mat-layout-grid-cell mat-layout-grid-cell-span-12 mat-layout-grid-align-right"> <MatButton Label="Login" Outlined="true" Type="submit"></MatButton> </div> </div> <p style="color:red;">@ErrorMessage</p> </EditForm> </div> </div>public class LoginViewModel : ComponentBase { [Inject] protected NavigationManager NavigationManager { get; set; } [Inject] protected IAuthService AuthService { get; set; } public LoginData LoginData { get; set; } = new LoginData(); public string ErrorMessage { get; set; } public bool IsLoading { get; set; } = false; public async Task SubmitAsync() { IsLoading = true; var model = new LoginModel() { UserID = LoginData.UserID, Password = LoginData.Password }; var result = await AuthService.LoginAsync(model); if (result.IsSuccessful) { NavigationManager.NavigateTo("/"); } else { ErrorMessage = "ログインに失敗しました。"; } IsLoading = false; } } public class LoginData { [Required(ErrorMessage = "ユーザIDを入力してください。")] [StringLength(32, ErrorMessage = "ユーザIDが長すぎます。")] public string UserID { get; set; } [Required(ErrorMessage = "パスワードを入力してください。")] [StringLength(32, ErrorMessage = "パスワードが長すぎます。")] public string Password { get; set; } }FirebaseAuthenticationを使用した場合の例
先ほどはダミーの認証サービスを使用しましたが、FirebaseAuthenticationを使用した場合の例になります。
予め、FirebaseAuthentication.netをNugetからインストールしてください。
認証APIを呼び出して、結果からトークンを取得しています。Firebase自体の細かい仕組みなどは、下記などが参考になります。
http://kmycode.hatenablog.jp/entry/2017/02/09/205655public class FirebaseAuthService : IAuthService { private readonly AuthenticationStateProvider _authenticationStateProvider; public FirebaseAuthService(AuthenticationStateProvider authenticationStateProvider) { _authenticationStateProvider = authenticationStateProvider; } public async Task<LoginResult> LoginAsync(LoginModel loginModel) { try { var provider = new FirebaseAuthProvider(new FirebaseConfig("ApiKEYを入れる")); var firebaseResult = await provider.SignInWithEmailAndPasswordAsync(loginModel.UserID, loginModel.Password); // トークンを取得 var res = new LoginResult() { IsSuccessful = true, IDToken = firebaseResult.FirebaseToken }; await ((SPAAuthticateProvider)_authenticationStateProvider).MarkUserAsAuthenticated(loginModel.UserID, res.IDToken); return res; } catch (FirebaseAuthException e) { return new LoginResult() { IsSuccessful = false, Error = e }; } } public async Task LogoutAsync() { await ((SPAAuthticateProvider)_authenticationStateProvider).MarkUserAsLoggedOut(); } }権限設定
認証の仕組みによっては、ユーザ毎に権限を設定して、権限に応じて表示があると思います。
(例:管理者だけが操作可能な画面)
Blazorでは以下の2種類の権限の仕組みを提供します。
- Role
- Policy
Role
下記のようにRolesで表示を許可するRoleを指定します。
@page "/counter" @attribute [Authorize(Roles = "Admin,SuperUser")] <h1>Counter</h1><AuthorizeView Roles="Admin,SuperUser"> <Authorized> // 略 </Authorized> </AuthorizeView>下記にAuthenticationStateProviderにRoleを認識させる場合の例にを示します。
public class SpaAuthticateProvider : AuthenticationStateProvider { // 略 public async override Task<AuthenticationState> GetAuthenticationStateAsync() { // 保存したロールを取得 var roles = await _localStorage.GetItemAsync<List<string>>("roles"); var claims = new List<Claim>(); claims.Add(new Claim(ClaimTypes.Name, userID)); foreach(var role in roles) { claims.Add(new Claim(ClaimTypes.Role,role)); } return new AuthenticationState(new ClaimsPrincipal(new ClaimsIdentity(claims, "User"))); }トークンと同じく、認証サービス側から取得した情報をローカルストレージに保存しておいて使用するといった形になります。
Policy
PolicyはRoleを発展させたもので、複数のRoleをまとめて扱ことが可能です。
複数のロールがある場合に、纏める場合に便利です。(例:IsAdminポリシーはAdminロールとSuperUserロールに対して割り当てる)
UIへの割り当てはRoleと違いはありません。
@page "/counter" @attribute [Authorize(Policy = "IsAdmin")] <h1>Counter</h1><AuthorizeView Policy="IsAdmin"> <Authorized> // 略 </Authorized> </AuthorizeView>StartupのAddAuthorizationCoreメソッド内で、ポリシーに対して対応するRoleを指定します。
Startup.cspublic class Startup { public void ConfigureServices(IServiceCollection services) { services.AddAuthorizationCore(config => { config.AddPolicy("IsAdmin", policy => policy.RequireRole("Admin", "SuperUser")); }); } }後はRoleの場合と同様に、認証のタイミングでユーザに割り当てるロール設定を行います。
CascadingAuthenticationState
認証状態をコンポーネント内の独自のロジックに組み込みたい場合、CascadingAuthenticationStateを使用します。
下記のようにCascadingAuthenticationStateタグで要素を囲むことで使用可能となります。App.razorを囲むことで全てのコンポーネント内で使用可能になりますが、特定のコンポーネント内で使用することで部分的に有効化することも可能です。
App.razor<CascadingAuthenticationState> <Router AppAssembly="@typeof(Program).Assembly"> // 略 </Router> </CascadingAuthenticationState>各コンポーネント内で、状態を取得するには下記のようにします。
@page "/cascade" <input type="button" @onclick="@DisplayAuth" value="display state" /> <p>@Message</p> @code { [CascadingParameter] Task<AuthenticationState> AuthenticationStateTask { get; set; } string Message { get; set; } async Task DisplayAuth() { var user = (await AuthenticationStateTask).User; Message = string.Empty; //認証済みの場合 if (user.Identity.IsAuthenticated) { Message += $"こんにちは、{user.Identity.Name} さん。"; if (user.IsInRole("Admin")) { Message += "あなたは管理者です。"; } } else { Message += "こんにちは ゲスト さん。"; } } }CascadingParameter属性を付与したTask型の変数を定義することで、認証情報が取得可能になります。
まとめ
BlazorのWebAssembly版における認証の方法を紹介しました。
VueやNuxtを利用していると、自前でVuexのStoreでログイン状態の管理を自前で実装する必要があったりと手間でしたが、フレームワーク側で定義されているとある程度、楽ができていいですね。おまけ (Cognitoにおける認証)
当初はFirebaseAuthenticationではなく、Cognitoでの認証を考えていました。
Client版のBlazorの場合、HttpClientはそのままでは使用できず、ブラウザ用のHttpClientとしてinjectする必要があるので、内部でHttpClientが使用されていると動きません。下記を参考にCognitoのSDK内で使用しているHttpClientを置き換えることで認証まではできましたが、非同期処理なのにリクエスト中に画面がフリーズしてしまい、スピナーによるローディング画面が表示できず諦めました。
将来的に使えるようになると良いのですが・・・。https://github.com/aws/aws-sdk-net/issues/1307
一応ですがCognitoの場合に試したコードも下記にいれてあります。
ソースコード参考資料等
https://docs.microsoft.com/ja-jp/aspnet/core/security/blazor/?view=aspnetcore-3.1&tabs=visual-studio
https://gunnarpeipman.com/client-side-blazor-authorizeview/
https://gist.github.com/SteveSandersonMS/175a08dcdccb384a52ba760122cd2eda
https://chrissainty.com/securing-your-blazor-apps-configuring-role-based-authorization-with-client-side-blazor/
https://chrissainty.com/securing-your-blazor-apps-configuring-policy-based-authorization-with-blazor/
- 投稿日:2019-12-11T21:12:42+09:00
ASP.NET Core RazorPagesの偽造防止トークン
これまで、「ASP.NET Core 3.0 RazorPages事始め」ってタイトルを使ってたけど、今回から「事始め」は取ることにします。
formタグヘルパーの偽造防止トークン
Razor Pages では、FormTagHelperが form要素に偽造防止トークンを挿入してくれます。
<form method="post"> ... </form>これで、以下のような非表示のinputタグ(偽造防止トークン)が自動挿入されます。
<input name="__RequestVerificationToken" type="hidden" value="CfDJ8Kws0PPAm1NChsmiTNfRcsX0TTepdsJf6F51WcaEnzEJH9W0IvKaCaoa7btzRNswqHlUXgzrHgA6rfcka5Jqt3u_93IMOSpOLtDRapBkHEPPhl7sGZ1lwyYlgzEcYpAcGKNeK-zL_8IaHnwMa0-viyY" />これは、クロスサイトリクエストフォージェリ(Cross site request forgeries、CSRF)と呼ばれる脆弱性対策を行うためのものです。
試しに、ブラウザの開発者ツールを使ってトークンを書き替えてsubmitしてみます。
HTTP ERROR 400 のエラーになりました。
AutoValidateAntiforgeryToken 属性
調べてみたら、
AutoValidateAntiforgeryToken
とかValidateAntiForgeryToken
という属性があるんですが、自動生成されたソースにはこれらの属性を使っている個所はどこにもありません。デフォルトで、
AutoValidateAntiforgeryToken
が適用されてるようです。偽造防止トークンを生成しない
asp-antiforgery="false"
次のように書くと偽造防止トークンは生成されません。
<form method="post" asp-antiforgery="false"> ... </form>まあ、あえてそうする理由は普通はないと思いますが...
IgnoreAntiforgeryToken 属性
IgnoreAntiforgeryToken
属性を使うと、特定のページ あるいは、特定のページハンドラ(どうもこの名前が覚えられない...)だけ、偽造防止トークンの必要性を無くすことができます。試しに、チュートリアルで作成したプログラムの
EditModel
クラスにIgnoreAntiforgeryToken
属性を適用してみます。[IgnoreAntiforgeryToken] public class EditModel : PageModel {今度は、ブラウザの開発ツールで、トークンを書き替えてもエラーにならずに、データを更新することができました。
この属性も前述の
asp-antiforgery
と同様に、通常の RazorPages アプリケーションでは、これを使うことはほとんど無いように思います。
- 投稿日:2019-12-11T19:54:05+09:00
C#でトランプを表現する。
初めに
更新の頻度を聞かれると微妙ではありますが、MSのチュートリアルって勉強になるなと感じているので紹介します。
テーマは表題通り、トランプの実装です。
https://docs.microsoft.com/ja-jp/dotnet/csharp/tutorials/working-with-linqトランプの生成
まず、トランプを生成する元となる列挙型的な物の準備から。
題材となるコードはチュートリアルに乗っ取るため、各々突っ込みどころはあると思いますが(Enumを使わないから適切な条件分岐書けなそう・Rankは数字で持ちたい等)、一旦LINQの使い方を学ぶという事で置いておきましょう。
KindOfCard.csstatic IEnumerable<string> Suits() { yield return "clubs"; yield return "diamonds"; yield return "hearts"; yield return "spades"; } static IEnumerable<string> Ranks() { yield return "two"; yield return "three"; yield return "four"; yield return "five"; yield return "six"; yield return "seven"; yield return "eight"; yield return "nine"; yield return "ten"; yield return "jack"; yield return "queen"; yield return "king"; yield return "ace"; }yield returnの挙動が分からない場合は、公式より以下の記事の様な個人ブログの方が馴染みやすいのでおすすめです。
https://tyheeeee.hateblo.jp/entry/2013/08/07/C%23%E3%81%AB%E3%81%8A%E3%81%91%E3%82%8Byield_return%E3%81%AE%E6%8C%99%E5%8B%95#fn-3dffedb5この二つを利用して、トランプを生成してくださいと言われたら、あなたならどう書きますか?
LINQを使わない例がこちら。
※Trumpクラスの実装は割愛します。NoLINQ.csvar startingDeckNoLINQ = new List<Trump>(); foreach(var suit in Suits()) { foreach(var rank in Ranks()) { startingDeckNoLINQ.Add(new Trump() { Suit = suit,Rank = rank}); } }単純な二重for文です。ここにLINQを使う場合はどう書けば良いでしょうか?
LINQを覚えたての人向けの書き方がこちら。HalfLINQ.csvar startingDeckHalfLINQ = new List<Trump>(); foreach(var suit in Suits()) { startingDeckHalfLINQ.AddRange( Ranks().Select(rank => new Trump() { Suit = suit,Rank = rank}) ); }トランプのマークだけFor文で回して、数字をSelectで回すパターン。
これであれば、デバッグする時にマーク毎に実行されるので追いやすいですね。最後に、for文なしの書き方です。
AllLINQ.csvar startingDeckAllLINQ = Suits().SelectMany(suit => Ranks() .Select(rank => new Trump(){ Suit = suit, Rank = rank }));慣れるまで頭が混乱してしまいますが、SelectManyを利用すれば複数の列挙データを組み合わせたデータを一度に作成する事が出来ます!!
SelectManyについて詳しく知りたい方は以下などを参考にしてください。
https://ufcpp.net/study/csharp/sp3_stdquery.html今回の内容は以上です。
次回はシャッフルの実装についてまとめます。
- 投稿日:2019-12-11T19:44:36+09:00
ASP.NET MVC5でTodoリストを作成
はじめに
こんにちは。初めて投稿致します。
ラクスAdvent Calendar 2019の12日目の記事です。昨日は@soachrさんのPostgreSQLに関する記事でした。
私は今年の半ばごろまでjavascriptに触れてきましたが、今度は業務でC#を使用することとなり現在絶賛勉強中です。
今回は業務で使用しているC#のフレームワークであるASP.NET MVC5を用いてTodoリストを作成いたしました。使用したツール
- Visual Studio Community 2019
- SQL Server 2017 Developer
- SQL Server Management Studio 18(SSMS)
DB及びTableの作成
まずSQLServerにDBとテーブルを作成します。
使用したCreate文は以下の通りです。CREATE TABLE dbo.MVCList( ID INT NOT NULL PRIMARY KEY IDENTITY(1,1), DoneFlg bit NOT NULL, TodoName nvarchar(50) NOT NULL, DeadLine date NULL, Remarks nvarchar(50) NULL );プロジェクトの作成
- VisualStudioを開き「新しいプロジェクトの作成」をクリックします。
- 次にASP.NET Webアプリケーション(.NET Framework)を選択しプロジェクト名を入力します。
![]()
- MVCを選択して作成を押します。
![]()
すると、下記のようなディレクトリ構成のプロジェクトができあがります。
EntityFrameworkとjQueryのインストール
下記手順でEntityFrameworkと(必要な場合は)jQueryをインストールして下さい。
1. ソリューションを右クリックし、ソリューションのNuGetパッケージの管理を選択。
2. EntityFrameworkとjQueryを検索しインストールを実施
3. ソリューションを右クリックしビルドを行う。Modelの作成
- Modelフォルダを右クリック ⇒ 追加 ⇒ 新しい項目を選択します。
- 「データ」の「ADO.NET Entity Data Model」を選択しファイル名を入力します。
![]()
- モデルのコンテンツの選択画面で「データベースからCode First」を選択します。
- Modelフォルダ配下にModelとDBContextが生成されます。
ControllerとViewの作成
- Controllersフォルダを右クリックし追加 ⇒ コントローラを選択します。
- 次に「Entity Frameworkを使用した、ビューがあるMVC5コントローラ」を選択します。
![]()
- モデルクラスとデータコンテキストクラスをリストから選択し、ファイル名を入力します。
![]()
※名前が紛らわしいですが、MVCTodoがデータコンテキストになります。
4. コントローラと一緒にViewであるcshtmlファイルも生成されます。
※今回はTodoリストの作成なのでDetail.cshtmlは削除します。
生成されたファイルの手直し
Modelファイル
・IDと完了フラグ以外の項目に [DisplayName("題名")]を追加しTodoリストの題名を日本語で表示するようにしました。
・期日に[DataType(DataType.Date)]を追加しdate型で表示・入力できるようにしました。MVCList.csnamespace MVCTodoList.Models { using System; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; [Table("MVCList")] public partial class MVCList { // ID public int ID { get; set; } // 完了チェック public bool DoneFlg { get; set; } // やること [DisplayName("やること")] [Required] [StringLength(50)] public string TodoName { get; set; } // 期日 [DisplayName("期日")] [DataType(DataType.Date)] [Column(TypeName = "date")] public DateTime? DeadLine { get; set; } // 備考 [DisplayName("備考")] [StringLength(50)] public string Remarks { get; set; } } }一覧(Index)ファイル
@section scripts{}でjavascriptファイルを読み込むようにしました。
チェック欄はDisplayForのままだとチェックボックスが非活性なので、CheckboxForへ変更し活性にしました。
更にcheckboxForの引数にnew{id=item.ID}を入れてidにテーブルのIDが振られるように変更しました。Index.cshtml@model IEnumerable<MVCTodoList.Models.MVCList> @{ ViewBag.Title = "Index"; } @section scripts{ <script type="text/javascript" src="../Scripts/own/MvcList.js"></script> } <h2>やることリスト</h2> <p> @Html.ActionLink("やること追加", "Create") </p> <table> <tr> <th></th> <th> @Html.DisplayNameFor(model => model.TodoName) </th> <th> @Html.DisplayNameFor(model => model.DeadLine) </th> <th> @Html.DisplayNameFor(model => model.Remarks) </th> <th></th> </tr> @foreach (var item in Model) { <tr name="row"> @*チェック欄*@ <td> @Html.CheckBoxFor(modelItem => item.DoneFlg,new { id=item.ID}) </td> @*やったこと*@ <td name="name"> @Html.DisplayFor(modelItem => item.TodoName) </td> @*期日*@ <td name="deadline"> @Html.DisplayFor(modelItem => item.DeadLine) </td> @*備考*@ <td name="remarks"> @Html.DisplayFor(modelItem => item.Remarks) </td> <td> @Html.ActionLink("修正", "Edit", new { id = item.ID }) | @Html.ActionLink("削除", "Delete", new { id = item.ID }) </td> </tr> } </table>Ajax通信用JSファイル
チェック欄のAjax通信用に新たにjavascriptファイルを作成しました。jQueryで実装しています。
MVCList.js$(function () { // チェックボックスの状態をchangeイベントで監視 $('[name="item.DoneFlg"]').change(function (ele) { // チェック状態を取得 var checkFlg = !!ele.target.checked; // チェンジイベントが走ったデータのIDを取得 var intId = parseInt(ele.target.id); var param = { Id: intId, DoneFlg : checkFlg } $.ajax({ url: '/MVCLists/Check', type: "POST", dataType: "json", data: param }) }) });Controllerファイル
一覧画面のチェックボックスの状態を登録するCheckメソッドを追加しました。
詳細(Detail)関連のメソッドは削除しました。MVCListsController.csusing System; using System.Collections.Generic; using System.Data; using System.Data.Entity; using System.Data.Entity.Validation; using System.Linq; using System.Net; using System.Web; using System.Web.Mvc; using MVCTodoList.Models; namespace MVCTodoList.Controllers { public class MVCListsController : Controller { private MVCContext db = new MVCContext(); // 一覧画面の表示 public ActionResult Index() { return View(db.MVCList.ToList()); } // 一覧画面のチェックボックスの状態を登録 [HttpPost] public ActionResult Check(int Id, bool DoneFlg) { MVCList mvcList = db.MVCList.Find(Id); mvcList.DoneFlg = DoneFlg; db.Entry(mvcList).State = EntityState.Modified; db.SaveChanges(); return Json(mvcList); } // 新規登録画面の表示 public ActionResult Create() { return View(); } // 新規登録 [HttpPost] [ValidateAntiForgeryToken] public ActionResult Create([Bind(Include = "ID,DoneFlg,TodoName,DeadLine,Remarks")] MVCList mVCList) { if (ModelState.IsValid) { db.MVCList.Add(mVCList); db.SaveChanges(); return RedirectToAction("Index"); } return View(mVCList); } // 編集画面の表示 public ActionResult Edit(int? id) { if (id == null) { return new HttpStatusCodeResult(HttpStatusCode.BadRequest); } MVCList mVCList = db.MVCList.Find(id); if (mVCList == null) { return HttpNotFound(); } return View(mVCList); } // リストの内容の編集 [HttpPost] [ValidateAntiForgeryToken] public ActionResult Edit([Bind(Include = "ID,DoneFlg,TodoName,DeadLine,Remarks")] MVCList mVCList) { if (ModelState.IsValid) { db.Entry(mVCList).State = EntityState.Modified; db.SaveChanges(); return RedirectToAction("Index"); } return View(mVCList); } // 削除画面の表示 public ActionResult Delete(int? id) { if (id == null) { return new HttpStatusCodeResult(HttpStatusCode.BadRequest); } MVCList mVCList = db.MVCList.Find(id); if (mVCList == null) { return HttpNotFound(); } return View(mVCList); } // 削除処理 [HttpPost, ActionName("Delete")] [ValidateAntiForgeryToken] public ActionResult DeleteConfirmed(int id) { MVCList mVCList = db.MVCList.Find(id); db.MVCList.Remove(mVCList); db.SaveChanges(); return RedirectToAction("Index"); } protected override void Dispose(bool disposing) { if (disposing) { db.Dispose(); } base.Dispose(disposing); } } }画面
データは事前に新規作成画面から入れました。
まだ二つのデータに完了チェックが入っていないので、テーブルのDoneFlgは0のままです。
「ノートを買う」にチェックを入れました。
すると、Ajax通信により「ノートを買う」のDoneFlgが「1」へ変更されました。
まとめ
今回は仕事で触っているC#のフレームワークであるASP.NET MVC5とjQueryを使ってTodoリストを作成しました。
C#はまだまだ勉強中ですので、これからも精進致します。
- 投稿日:2019-12-11T17:42:17+09:00
Exam Ref 70-483『C#でのプログラミング』の試験対策書籍の私的まとめ その3
はじめに
この記事は、Exam Ref 70-483『C#でのプログラミング』の試験対策書籍の私的まとめ その2の続編となります。
Ch.3 Debug applications and implement security
学べるスキル
- アプリケーションへの入力の検証(Validation)
- 対称および非対称暗号化の実行
- アセンブリの管理
- アプリケーションのデバッグ
- アプリケーション診断の実装
要約
- データ検証(バリデーション)は、安全なアプリケーションの重要な要素である。 これはアプリケーション起動時の段階で必ず組み込まなけらばならない。
- JavaScript Object Notation(JSON)は、C#クラス構造を、クラス内の要素に一致する一連の名前と値のペアを含むドキュメント文字列に変換する方法を提供する。型のプライベート要素は自動的に保存されることはないが、保存することもできる。
- Newtonsoft JSONライブラリを使用して、クラスとJSON文字列を相互に変換できる。不正なJSON文字列デコードが指定されると、例外がスローされる。 例外には、エラーのソーステキスト内の位置を説明する情報が含まれている。
- XML(eXtensible Markup Language)は、クラスをテキストドキュメントに変換できるもう1つの手段である。 例外には、エラーのソーステキスト内の位置を説明する情報が含まれている。
- プログラムは、プログラムの実行時に例外を発生させるのではなく、型の使用に関するエラーがコンパイル時に検出されるように、型付きコレクションクラスを使用する必要がある。
- アプリケーションが複数のスレッドを使用する可能性がある場合、コンカレントコレクションクラスを使用する必要がある。
- 正しい形式のコレクション(キュー、スタック、インデックスストレージ、リンクリスト、または辞書)を選択すると、パフォーマンスが向上し、コーディングが容易になる。
- ASP.NET(Active Server Pages)アプリケーションは、Model View Controller(MVC)パターンのModel要素を提供するEntity Frameworkデータストア上に構築できる。このマッピングでは、データのビューはHTML Webページによって提供され、コントローラー(ビューからの入力に応答し、データモデルを更新する)はC#クラスとして作成される。
- オブジェクトの整合性を保持するためにどれだけの労力をかけるかを決定するときは、整合性障害のリスクと、そのような障害がもたらす影響を考慮することが重要である。リスクを特定し、その影響を評価したら、プロジェクトの開始時にリスクを軽減するための労力を割り当てる必要がある。
- 正規表現を使用してデータを編集したり、特定のデータ要素と照合してその構造と内容を検証することもできる。
- C#は、データを文字列値から変換先の型に変換するために
Parse
およびTryParse
動作のビルドを利用できる。変換が失敗する場合、parse
メソッドは例外をスローする。かなり一般的な問題とみなされるべきものに対処するために作成した例外ハンドラの管理が容易となるTryParse
メソッドは、True
またはFalse
を返す。- C#は、データ変換を実行する多くのメソッドを含むConvertクラスを提供している。
- Entity Frameworkアプリケーションのデータクラスには、バリデーションアクションを示す追加の属性を指定できる。
- 暗号化は、盗聴者が読み取れないメッセージを送信している。 暗号解析は、あなたたちが読むことができないはずのメッセージを読み込んでいる。暗号化とは、プレーンテキスト(メッセージ)を暗号化されたデータに変換するプロセスであり、暗号化されたデータを読み取るには解読する必要がある。 復号化は暗号化の逆である。
- キーは、暗号化および復号化プロセスで使用されるアイテムである。 キーの使用を伴わない暗号化は、言語のパターンおよび会話する当事者の慣習を通じて暗号化のテクニックを明らかにする統計的手法に対して脆弱となる。
- 対称暗号化では、暗号化された会話の両端で同じキーが使用される。 これにより、高レベルのセキュリティが提供され、コンピューターの実行が容易になります。 ただし、会話の両側にキーのコピーが必要である。
- 対称暗号化を実行する.NETライブラリが存在する。 暗号化アルゴリズムは、
SymmetricAlgorithm
抽象クラスの子クラスの形式で公開されているため、暗号化アルゴリズムを簡単に変更できる。暗号化プロセス自体はストリームとして提供されるため、暗号化をデータ転送に簡単に統合できる。 推奨される暗号化メカニズムは、AES(Advanced Encryption Standard)である。- 非対称暗号化では、送信者と受信者に異なるキーが使用される。 一方のキーで暗号化されたデータは、もう一方のキーで読み取ることができる。 キーを生成するために使用される数学的手法により、一方のキーを他方から推測すること、または暗号化されたデータからいずれかのキーを決定することは非常に計算上困難となる。
- 非対称暗号化を実行するには、より多くのコンピューター処理能力が必要であり、比較的小さなデータパケットにのみ適している。 対称キー値を転送し、デジタル文書に署名してそれらを検証し、その起源を証明するために使用される。
- パブリック/プライベート暗号化では、パーティ(ここではアリスと呼ぶ)が1つの非対称キーを公開し、もう1つのキーをプライベートにする。他の関係者は、この公開鍵を使用して、アリスのみが読み取れるメッセージを暗号化できる。 これは、アリスに暗号化された会話を行うために使用できる対称キーをアリスに送信する方法です。
RSACryptoServiceProvider
クラスは、バイトブロックのRSA暗号化を提供します。但し、 ストリームを暗号化するものではない。 キー値(パブリックまたはプライベート)を記述するXMLを生成できる。ユーザーまたはマシンごとに、コンピューターのキーストアにキー値を保存することもできる。- パブリック/プライベートメカニズムは、安全な通信を確立する便利な方法ですが、身元確認の形式を提供しません。誰でも公開鍵を作成して、アリスであると主張できます。 証明機関は、特定の関係者の身元を検証できる信頼できる関係者です。 パーティー(ここではボブと呼ぶ)は、証明機関によってホストされている証明書によって署名された公開キーをアリスに送信できる。
markcert
プログラムを使用して、開発中に使用するテスト証明書を生成できる。 これらはファイルに保存することも、証明機関がホストすることもできる。- 文書に署名することにより、受信者は文書の送信者、および内容が変更されていないことを判別できる。これを実現するには、プログラムはドキュメントの「ハッシュ」を計算する必要がある。 ハッシュ関数はデータのブロックに適用され、そのブロックに固有のはるかに小さな値(数十バイト)を生成する。 これは「ブロックマニフェスト」と呼ばれる。
- ハッシュ関数は攻撃に対して脆弱である可能性がある。ハッシュ関数を変更せずにドキュメントの内容を変更できる可能性があり、検出できない焼き戻しの可能性が高まる。ブロックマニフェストが長いほど、優れている。 SHA1(Secure Hash Algorithm 1)は危険にさらされているため、使用を推奨しません。 SHA2の方が優れていますが、SHA3は将来標準になる。 実装はGitHubを介して利用可能であるが、SHA3は現在.NETライブラリでサポートされていない。
- アセンブリは、.NETアプリケーションの基本的な構成要素である。 エントリポイントを持つことができ、アプリケーションの開始点として機能する。その場合、拡張子
.exe
を持つか、アセンブリにクラスのライブラリを含めることができる。クラスのライブラリは、必要に応じて動的にロードされ、その場合、アセンブリファイルの拡張子は.dll
となる。- アセンブリファイルには、アセンブリの内容を記述し、そのアセンブリ内のコードで使用される他のアセンブリを識別するマニフェストが含まれている。
- アセンブリ内のプログラムコードは、Microsoft Intermediate Language(MSIL)で記述されている。これは、.NET互換言語で記述できる元のプログラムソースコードをコンパイルすることによって生成される。
- アセンブリにはバージョン情報が含まれている。 厳密な名前を持つアセンブリは、各アセンブリが対応する公開キーを介して一意に識別できるようにする秘密キーを使用して署名される。 これにより、アセンブリファイルの内容の変更も防止される。
- アセンブリは、グローバルアセンブリキャッシュ(GAC)に保存できる。 これにより、マシン上のアプリケーション間でアセンブリを共有できる。 同じアセンブリの複数のバージョンをGACに「平行」的に保存でき、それぞれが数学上の異なるアプリケーションによって適切に使用される。
- WInRTは、コンパイルされたC ++コードに基づくランタイムシステムである。 .NETアセンブリと同じメタデータの基本を使用して、オブジェクトの動作を表現することができる。動作は、WinMD(Windows MetaData)型のファイルで表現される。これは、既存のC ++のラッパー(Windows APIの場合)にすることも、WinMDファイルにコードを含めることもできる。Windowsランタイムコンポーネント(コードを表す要素。Windowsランタイムコンポーネント(WinMDファイルで動作を表す要素)をC#クラスから作成することは可能だが、WinRTで使用できるC#オブジェクトの動作には制限がある。
- C#コンパイラは、プロセッサディレクティブを理解する。 ディレクティブは、
#
文字が前に付いたコマンドである。#if
ディレクティブを使用して、プログラムのどのコード要素を実際にコードにコンパイルするかを制御できる。 このディレクティブは与えられたシンボルが#define
ディレクティブを使用して定義されているかどうかをテストする。#if
ディレクティブを使用して、プログラムの診断要素のオンとオフを切り替えたり、プログラムのカスタムバージョンを作成できる。 ただし、コードを混乱させる可能性があるため、使いすぎないようにすべし。DEBUG
シンボルは、プログラムがデバッグオプションでコンパイルされている場合に定義される。これは、プログラムのデバッグ時に実行されるコードをアクティブにするために使用できる。#waning
ディレクティブは、プログラムのビルド時に警告メッセージを生成する。#error
ディレクティブは、プログラムのビルド時に指定されたメッセージでコンパイルエラーを生成する。 条件オプションの互換性のない選択が含まれている場合、プログラムがコンパイルされないようにするために使用することができる。#progma
ディレクティブを使用すると、プログラマーは、ソースコードのブロックを識別でき、識別した場合、コンパイラーの警告は無視される。#line
ディレクティブを使用すると、開発者はソースファイルのコンパイルおよびトレース中にVisual Studioによって報告される行番号を設定できる。ソースコードファイルがビルドプロセスの間に自動的に挿入するコードを生成した場合に有用である。#line
ディレクティブを使用して、デバッグプロセスからソースコード行を非表示にすることができる。#line
ディレクティブを使用して、診断メッセージによって拒否されるソースファイルの名前を設定することもできる。- Visual Studioプロジェクトには、デバッグオプションとターゲットプラットフォームを指定する多数のビルド構成を含めることができる。 新しく作成されたプロジェクトには、デバッグ構成とリリースビルド構成が含まれる。 Visual Studioの構成マネージャーを使用して、ビルド構成を設定し、新しい構成を作成できる。
- デバッグビルドによって生成されるアセンブリには、呼び出されない未使用の変数とメソッドを実装するコードが含まれる。 リリースビルドは未使用の要素を破棄し、プログラムが実行される。 このため、リリースビルドではデバッグブレークポイントを設定できない。
- プログラムがビルドされると、Visual Studioはプログラムデバッグデータベースファイル(PDBファイル)を作成する。このファイルには、プログラムで使用されるシンボルの名前と、それらの値が保存される場所が含まれる。このファイルには、ソースコードステートメントとそれらを実装するMSILへのマッピングも含まれている。 特定のデータベースファイルは、両方のファイルに格納されているGUID(グローバル一意識別子)を使用して特定のソースコードファイルにマップされる。 プログラムデータベースファイルがある場合、プログラムは実行されるが、それがないとデバッグできない。
- プログラムデータベースファイルには、パブリック要素とプライベート要素を含めることができる。パブリック要素とは、パブリックメソッドなど、アセンブリ自体に表示される要素である。プライベート要素には、ローカル変数などが含まれます。 プライベート要素を含むデータベースにアクセスすると、コードがどのように機能するかについての洞察が得られる可能性があり、これは潜在的なセキュリティ問題になる可能性がある。 Debugging Tools for Windowsの一部であるpdbcopyプログラムを使用して、データベースファイルをコピーし、パブリック要素を削除することができる。
System.Diagnostics
ライブラリのDebug
クラスとTrace
クラスを使用して、プログラムの実行時にメッセージを生成できる。 アサーションテストにも使用できる。- デバッグオプションを使用してプログラムをビルドすると、プログラム実行時にデバッグコードが実行される。 デバッグまたはリリースオプションを使用してプログラムをビルドすると、トレースコードが実行される。
Trace
オブジェクトは、出力の情報、警告、およびエラーレベルを提供する。- リスナーは、
Debug
クラスとTrace
クラスにバインドすることができる。 出力の表示、出力のコンマ区切りリスト、およびXML形式のデータとしてのログ出力に使用できるさまざまな形式のリスナーが存在する。TraceSource
クラスは、トレースイベントの生成を管理する。 通常の操作中に生成される情報イベントから、警告、エラー、および重大なイベントまで、いくつかのタイプのイベントが存在する。1つ以上のTraceListener
インスタンスをTraceSource
インスタンスにバインドして、イベントの通知を受け取ることができる。TraceSwitch
クラスを使用して、プログラムによって実行されるトレースのレベルを決定できる。プログラムコードは、TraceSwitch
インスタンスの値をテストして、生成するトレース出力のレベルを決定できる。SourceSwitch
クラスは、TraceSource
インスタンスによって生成されるトレースメッセージを制御するために使用される。SourceSwitch
クラスのインスタンスをTraceSource
に関連付けて、生成するトレースイベントを決定できる。- トレース出力のレベルは、アプリケーションの構成ファイルに
SourceSwitch
値を作成することでも定義できる。 これにより、アプリケーションの実際のプログラムコードを変更することなく、プログラムのトレース動作を変更できる。- アプリケーションのプロファイリングは、プログラムの実行時にプロセッサー時間を最も消費している要素を特定するために使用される。これらの要素は、必要に応じてパフォーマンスを改善するために特に最適化できる。 プロファイリングの最も単純な形式は、
Stopwatch
クラスを使用して、プログラムの実行時に経過時間を測定することである。- Visual Studioには、さまざまなプロファイリングツールが用意されています。 デフォルトのプロファイラーでは、多くのプロファイリングオプションを利用できる。 通常、プログラムがほとんどの時間を費やしている場所を特定するには、最も単純なCPUサンプリングで十分である。このオプションは、コンピューターへの読み込みのプロファイルを作成し、「ホットパス」(実行頻度の高い箇所や実行時間の長い箇所)を表示する。これにより、プロセッサ時間のほとんどを消費するメソッドが特定される。
- パフォーマンスカウンターはオペレーティングシステムによって維持され、メモリやプロセッサの使用など、ホストコンピューターに関するさまざまな情報を表示するために使用できる。
- アプリケーションは、独自のパフォーマンスカウンターを作成および更新できる。 それらの値はオペレーティングシステムによって保持される。値をカウントするだけでなく、1秒あたりのイベント数を評価するものなど、さまざまな種類のカウンターが存在する。プログラムが独自のパフォーマンスカウンターを作成するには、管理者権限が必要となる。
- パフォーマンスカウンターの値は、パフォーマンスモニター
perfmon
プログラムを使用して表示できる。プログラムは、パフォーマンスカウンターを読み取って独自のダッシュボード表示を作成することも可能である。- オペレーティングシステムは、イベントログも管理する。 プログラムは、独自のイベントカテゴリとイベントタイプを作成し、イベントをログに追加できる。 プログラムがイベントを読み取り、リスナーメソッドを特定のイベントにバインドして、そのイベントが記録されたときに通知を受け取ることも可能である。
関連記事
参考書籍
『Exam Ref 70-483 Programming in C#, 2nd Edition』 By Rob Miles
- 投稿日:2019-12-11T14:57:03+09:00
Xamarin.Forms色々やってみた。
Xamarin.Formsというのはiosとandroidの両方共通で使えるもの。
HelloWorld
Labelは文字列を表示する
HorizontalOptionsフィールドは横の位置を表し、VerticalOptionsフィールドは縦位置を表す。
Textフィールドは表示するテキストを表す。MainPage.xaml<?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:d="http://xamarin.com/schemas/2014/forms/design" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" x:Class="App5.MainPage"> <StackLayout> <!-- Place new controls here --> <Label Text="Welcome to Xamarin.Forms!" HorizontalOptions="Center" VerticalOptions="CenterAndExpand" /> </StackLayout> </ContentPage>InitializeComponentでxaml側を描画している。
MainPage.xaml.csusing System.ComponentModel; using Xamarin.Forms; namespace App5 { // Learn more about making custom code visible in the Xamarin.Forms previewer // by visiting https://aka.ms/xamarinforms-previewer [DesignTimeVisible(false)] public partial class MainPage : ContentPage { public MainPage() { InitializeComponent(); } } }実行結果
Binding
ちっとBindingは欲しいし、適当にやってみようか。ということでBindingできるか検証。
MainPage.xaml<?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:d="http://xamarin.com/schemas/2014/forms/design" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" x:Class="App5.MainPage"> <StackLayout> <!-- Place new controls here --> <Label Text="{Binding Name}" HorizontalOptions="Center" VerticalOptions="CenterAndExpand" /> </StackLayout> </ContentPage>WPFと同じようにINotifyPropertyChangedを実装して、登録を行う。
ただし、DataContextがないのでBindingContextというそれっぽいやつにBindingを行う。MainPage.xaml.csusing System.ComponentModel; using Xamarin.Forms; namespace App5 { // Learn more about making custom code visible in the Xamarin.Forms previewer // by visiting https://aka.ms/xamarinforms-previewer [DesignTimeVisible(false)] public partial class MainPage : ContentPage { private MainVM MyVM = new MainVM(); public MainPage() { InitializeComponent(); BindingContext = MyVM; } } public class MainVM : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; private string _Name = "HelloWorld"; public string Name { get => _Name; set { _Name = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name))); } } } }実行結果
Binding本当にできているのか?
表示時はできたが、はたして本当にできているのか不安なのでボタンをクリックしてViewModelを変更することによってxamlが変更されるかどうかを確かめる。
MainPage.xaml<?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:d="http://xamarin.com/schemas/2014/forms/design" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" x:Class="App5.MainPage"> <StackLayout> <!-- Place new controls here --> <StackLayout HorizontalOptions="Center" VerticalOptions="CenterAndExpand" Orientation="Vertical"> <Label Text="{Binding Name}" /> <Button Text="ボタン" Clicked="Button_Clicked"></Button> </StackLayout> </StackLayout> </ContentPage>MainPage.xaml.csusing System.ComponentModel; using Xamarin.Forms; namespace App5 { // Learn more about making custom code visible in the Xamarin.Forms previewer // by visiting https://aka.ms/xamarinforms-previewer [DesignTimeVisible(false)] public partial class MainPage : ContentPage { private MainVM MyVM = new MainVM(); public MainPage() { InitializeComponent(); BindingContext = MyVM; } private void Button_Clicked(object sender, System.EventArgs e) { MyVM.Name = "HelloWorld2"; } } public class MainVM : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; private string _Name = "HelloWorld"; public string Name { get => _Name; set { _Name = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Name))); } } } }ボタンをクリックしてHelloWorldがHelloWorld2になっていれば成功。
ListView
stringのリストを表示
ItemSourceにリストを入れる。
MainPage.xaml<?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:d="http://xamarin.com/schemas/2014/forms/design" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" x:Class="App4.MainPage"> <StackLayout> <!-- Place new controls here --> <ListView ItemsSource="{Binding Fruits}"> </ListView> </StackLayout> </ContentPage>VMクラスにリストを入れる。
MainPage.xaml.cspublic class MainVM:INotifyPropertyChanged { private ObservableCollection<string> _Fruits = new ObservableCollection<string>() { "apple", "banana", "cherry", "drian" }; public ObservableCollection<string> Fruits { get => _Fruits; set { _Fruits = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Fruits))); } } public event PropertyChangedEventHandler PropertyChanged; }Converterを使ってクラスで特定のフィールドを表示
MainPage.xaml<?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:d="http://xamarin.com/schemas/2014/forms/design" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:App4" mc:Ignorable="d" x:Class="App4.MainPage"> <ContentPage.Resources> <ResourceDictionary> <local:FruitToStringConverter x:Key="FruitToStringConverter" /> </ResourceDictionary> </ContentPage.Resources> <StackLayout> <ListView ItemsSource="{Binding Fruits, Mode=OneWay, Converter={StaticResource FruitToStringConverter}}"> </ListView> </StackLayout> </ContentPage>コンバータークラスを作成
Convertはソースからxaml側へ
ConvertBackはxamlからソース側へ。
ConvertBackは今回使わないのでExceptionで残しておく。MainPage.xaml.cspublic class FruitToStringConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { var json = JsonConvert.SerializeObject(value); return JsonConvert.DeserializeObject<List<Fruit>>(json).Select(x => x.Name); } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new Exception("エラーです。"); } }実行結果は上のstringのリスト表示と同じ。
ViewCellを使ってクラスの複数フィールドを表示
ViewCell直下ではListの中のクラスのFruitのフィールド名をBindingできる。
MainPage.xaml<?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:d="http://xamarin.com/schemas/2014/forms/design" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:App4" mc:Ignorable="d" x:Class="App4.MainPage"> <ContentPage.Resources> <ResourceDictionary> <local:FruitToStringConverter x:Key="FruitToStringConverter" /> </ResourceDictionary> </ContentPage.Resources> <StackLayout> <ListView ItemsSource="{Binding Fruits, Mode=OneWay}"> <ListView.ItemTemplate> <DataTemplate> <ViewCell Height="150"> <StackLayout Orientation="Horizontal"> <Label Text="{Binding Id}"></Label> <Label Text="{Binding Name}"></Label> <Label Text="{Binding Price}"></Label> </StackLayout> </ViewCell> </DataTemplate> </ListView.ItemTemplate> </ListView> </StackLayout> </ContentPage>VMクラス再掲
MainPage.xaml.cspublic class MainVM:INotifyPropertyChanged { private ObservableCollection<Fruit> _Fruits = new ObservableCollection<Fruit>() { new Fruit(){Id = 1, Name = "apple", Number = 6, Price = 180}, new Fruit(){Id = 2, Name = "banana", Number = 7, Price = 220}, new Fruit(){Id = 3, Name = "cherry", Number = 5, Price = 210} }; public ObservableCollection<Fruit> Fruits { get => _Fruits; set { _Fruits = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Fruits))); } } public event PropertyChangedEventHandler PropertyChanged; }ページ遷移
NavigationPageによる遷移
MainPageをNavigationPageに変える。
App.xaml.cspublic App() { InitializeComponent(); MainPage = new NavigationPage(new MainPage()); }MainPageの他に、Page1,Page2を作り遷移していく
MainPage.xaml<?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:d="http://xamarin.com/schemas/2014/forms/design" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:local="clr-namespace:App4" mc:Ignorable="d" x:Class="App4.MainPage"> <StackLayout> <Button Text="Page1へ" Clicked="Button_Clicked"></Button> </StackLayout> </ContentPage>Navigation.PushAsync(Page, true)でPageに遷移する。第2引数はアニメーションのありなし。
MainPage.xaml.cspublic partial class MainPage : ContentPage { private MainVM MyVM = new MainVM(); public MainPage() { InitializeComponent(); BindingContext = MyVM; } private void Button_Clicked(object sender, EventArgs e) { Navigation.PushAsync(new Page1(), true); } }Page1.xaml<?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:d="http://xamarin.com/schemas/2014/forms/design" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" x:Class="App4.Page1"> <ContentPage.Content> <StackLayout Orientation="Vertical"> <Button Text="Mainへ" Clicked="Button_Clicked"></Button> <Button Text="Page2へ" Clicked="Button_Clicked_1"></Button> </StackLayout> </ContentPage.Content> </ContentPage>Page1.xaml.csusing System; using Xamarin.Forms; using Xamarin.Forms.Xaml; namespace App4 { [XamlCompilation(XamlCompilationOptions.Compile)] public partial class Page1 : ContentPage { public Page1() { InitializeComponent(); } private void Button_Clicked(object sender, EventArgs e) { Navigation.PopToRootAsync(); } private void Button_Clicked_1(object sender, EventArgs e) { Navigation.PushAsync(new Page2(), true); } } }Page2.xaml<?xml version="1.0" encoding="utf-8" ?> <ContentPage xmlns="http://xamarin.com/schemas/2014/forms" xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml" xmlns:d="http://xamarin.com/schemas/2014/forms/design" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d" x:Class="App4.Page2"> <ContentPage.Content> <StackLayout Orientation="Vertical"> <Button Text="Mainへ" Clicked="Button_Clicked"></Button> <Button Text="Page1へ" Clicked="Button_Clicked_1"></Button> </StackLayout> </ContentPage.Content> </ContentPage>Page2.xaml.csusing System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using Xamarin.Forms; using Xamarin.Forms.Xaml; namespace App4 { [XamlCompilation(XamlCompilationOptions.Compile)] public partial class Page2 : ContentPage { public Page2() { InitializeComponent(); } private void Button_Clicked(object sender, EventArgs e) { Navigation.PopToRootAsync(); } private void Button_Clicked_1(object sender, EventArgs e) { Navigation.PushAsync(new Page1(), true); } } }上の青いところが出てしまう。
Android実機デバッグ
以下の記事が参考になる。
https://tech-blog.cloud-config.jp/2017-06-06-execute-xamarin-helloworld-with-android-device/
- 投稿日:2019-12-11T14:34:29+09:00
速習 AAA : Arrange-Act-Assert による読みやすいテスト
こちらは C# その2 Advent Calendar 2019 の 12 日目の記事です。
本記事では C# による単体テストの記述にあまり馴染みのない人を対象とし、シンプルかつ効果的なプラクティスの一つ Arrange-Act-Assert (AAA) パターンについて紹介します。
バリバリ書いている方は得るところがあまりないと思いますので、代わりにツッコミとか頂けますと幸いです。まず単体テストを書く
単体テストを書くこと、それ自体は難しくありません。文字列を int に変換するメソッドのテストもすぐに書けます:
Assert.AreEqual(123, int.Parse("123"));ではもう少し複雑なものはどうでしょうか:
Assert.AreEqual("01234567-89ab-cdef-0123-456789abcdef", new Guid("01234567-89ab-cdef-0123-456789abcdef").ToString("D"));
Guid
のコンストラクタをテストしているのか、それともToString("D")
をテストしているのか。
もしかしたら両方かも知れませんが、テストコードを見ても判断できません。
(意味のあるテスト名を記述していればそこで判りますが、だからと言ってテストコードが読み難くて良いとはならないので、ここではGuidTest
という曖昧なテスト名だったとします)Arrange-Act-Assert パターン
Arrange-Act-Assert (AAA) パターンというのを訊いた事があるでしょうか。大まかには下記のようなものです:
// Arrange var guid = new Guid("01234567-89ab-cdef-0123-456789abcdef"); // Act var actual = guid.ToString("D"); // Assert Assert.AreEqual("01234567-89ab-cdef-0123-456789abcdef", actual);
Guid
の何をテストしているのか判然としなかった先ほどのテストも、AAA パターンで整理する事で一目瞭然となります。
// Act
と書かれているコメント直下が Act セクションになり、ここではGuid.ToString("D")
のテストをしている事が判るようになりました。同様に// Assert
と書かれている Assert セクションでは、Act の戻り値の文字列actual
が期待値"01234567-89ab-cdef-0123-456789abcdef"
と一致しているかの検査をしている事が判ります。AAA パターンについては Microsoft Docs でも 単体テストの基本 や 単体テストのベストプラクティス で紹介されています:
- 単体テスト メソッドの Arrange セクションでは、オブジェクトを初期化し、テスト対象のメソッドに渡されるデータの値を設定します。
- Act セクションでは、設定されたパラメーターでテスト対象のメソッドを呼び出します。
- Assert セクションでは、テスト対象のメソッドの操作が予測どおりに動作することを検証します。
https://docs.microsoft.com/ja-jp/visualstudio/test/unit-test-basics?view=vs-2019#write-your-tests
こちらも参照してみて下さい。
テストケースのパラメーター化
上記
Guid
の例では特定のパラメーターのみでテストしましたが、他のパラメーターでもテストを行いたいと思います。その際、テストメソッドを分割しそれぞれに適したテスト名を付けるのがセオリーです。一方で AAA セクションが同一であれば、テストケースをパラメーター化する事で効率的にテストを記述する事もできます:
// Arrange var guid = new Guid("01234567-89ab-cdef-0123-456789abcdef"); void TestCase(int testNumber, string format, string expected) { // Act var actual = guid.ToString(format); // Assert Assert.AreEqual(expected, actual, $"No.{testNumber}"); } // Test cases TestCase(1, "D", expected: "01234567-89ab-cdef-0123-456789abcdef"); TestCase(2, "B", expected: "{01234567-89ab-cdef-0123-456789abcdef}"); TestCase(3, "P", expected: "(01234567-89ab-cdef-0123-456789abcdef)"); TestCase(4, "N", expected: "0123456789abcdef0123456789abcdef");例外ケースのパラメーター化
format
パラメーターに未知のフォーマット"A"
を入力した場合、Guid.ToString(format)
はFormatException
をスローします。しかし、上記のテストコードでは例外テストは考慮されていない為、単に例外がスローされテストは失敗に終わります。例外テストも考慮したテストケースのパラメーター化は次のように書く事ができます:
... void TestCase(int testNumber, string format, string expected = default, Type expectedExceptionType = default) { // Act string ret = default; Type exceptionType = default; try { ret = guid.ToString(format); } catch (Exception ex) { exceptionType = ex.GetType(); } // Assert Assert.AreEqual(expectedExceptionType, exceptionType, $"No.{testNumber}"); Assert.AreEqual(expected, ret, $"No.{testNumber}"); } // Test cases ... TestCase(5, "A", expectedExceptionType: typeof(FormatException));ここまでで例外テストを考慮したテストケースのパラメーター化ができましたが、代償として Act セクションに定型文が追加されてしまいました。Act を記述するたびにこれを書くのは面倒なので、ここでは TestAA を使用して記述を簡略化したいと思います。
TestAA による Act-Assert
TestAA は AAA パターンのうち Act-Assert のサポートを目的として作成した、単体テスト向けのシンプルなライブラリです。
先ほどのテストコードを TestAA で書き換えてみます:
// Arrange var guid = new Guid("01234567-89ab-cdef-0123-456789abcdef"); void TestCase(int testNumber, string format, string expected = default, Type expectedExceptionType = default) { TestAA .Act(() => guid.ToString(format)) .Assert(expected, expectedExceptionType, message: $"No.{testNumber}"); } // Test cases TestCase(1, "D", expected: "01234567-89ab-cdef-0123-456789abcdef"); TestCase(2, "B", expected: "{01234567-89ab-cdef-0123-456789abcdef}"); TestCase(3, "P", expected: "(01234567-89ab-cdef-0123-456789abcdef)"); TestCase(4, "N", expected: "0123456789abcdef0123456789abcdef"); TestCase(5, "A", expectedExceptionType: typeof(FormatException));
.Act()
に渡されるラムダ式によって Act セクションが表され、.Assert()
に渡される戻り値の期待値ならびに例外型の期待値によって Assert セクションがコンパクトに表現されています。先ほどまで Act セクションに記述されていた try ~ catch は
TestAA.Act()
にカプセル化された為、Act セクションがとてもシンプルになりました。Act セクションの結果はTestActual<TReturn>
の形で戻され、この中に戻り値または生じた例外が含まれています:TestActual<string> actual = TestAA.Act(() => guid.ToString("A")); // actual.Return: default(string) // actual.Exception: FormatException instanceAssert セクションは
TestActual<TReturn>.Assert(@return, exception)
の形で表す事ができます。@return
パラメーターは戻り値の期待値を、exception
パラメーターは例外型の期待値をそれぞれ渡す事で、内部で actual との比較検証が行われます。actual.Assert(@return: default(string), exception: typeof(FormatException));今回は TestAA をパラメーター化されたテストケースで使用しましたが、通常のテストコードでも AAA セクション分けとテスト検証をサポートしてくれます。
// Arrange var guid = new Guid("01234567-89ab-cdef-0123-456789abcdef"); TestAA.Act(() => guid.ToString("D")).Assert("01234567-89ab-cdef-0123-456789abcdef");詳細は README.md に記載してあります。ご活用いただければ幸いです (宣伝)
総括
単体テストにおいて、AAA パターンはとてもシンプルでありながら強力なプラクティスです。それは書き手にとっても読み手にとっても多くのメリットをもたらします。
// Arrange ... // Act ... // Assert ...突き詰めればこれだけですので、初めて知った方や実践した事の無い方は是非一度試してみて下さい。
余談: 続)単体テストを書く
単体テストは人によっては重視されない事もありますが、自分の実装が自分の意図通り・設計通りである事を証明する為にとても有効な手段だと考えています。またそれは回帰テストという形で、将来の変更に対する容易さにも繋がります。
という事は誰しも朧気ながらに感じているところと思いますが、では実際に単体テストを書こうとすると、どうやって書いたら良いかで手が止まる事も少なくありません。
ちょうど 1 年ほど前に書いた リーダブルテスト という記事で、自分が業務で実践している読みやすい・書きやすいテストの指針を示しました。これに倣う事である程度は定型化した単体テストの記述が可能となり、迷いが減らせる事と思います。お悩みの方はこちらも一度ご覧頂ければと思います。
- 投稿日:2019-12-11T12:14:48+09:00
CakeBuildのTaskについて
始めに
以前CakeBuildについて全体的な記事を書いたが、Taskについてあまり掘り下げていなかったと思うので、この記事でもう少し詳しく書きたいと思う。
Taskとは
一言でいうと、CakeBuildにおける処理の一塊のこと。
CakeBuildではこのTaskを単位に成功、失敗判定を行う。Taskの登録
まず初めにしなければならないことは、TaskをCakeBuildのコンテキストに登録することである。
これは、Task("TaskName");
とすることで、TaskName
というTaskを登録できる。
登録しただけでは何もしないので、Task("TaskName")
の後にメソッドチェーンの形でどんどん設定を追加していく。Taskの名前に文字種制限は特にないので、グループ化したい場合は"."や"/"で区切ることもできる。
タスクのリストを取得したい場合、
ICakeContext.Tasks
経由で取得が可能で、グローバルで暗黙的に使用可能。Task("Task1/a"); Task("Task1/b"); Task("Task2/c"); // "Task1"から始まるタスクのみ実行する foreach(var t in Tasks.Where(x => x.Name.StartsWith("Task1")) { RunTarget(t.TaskName); }
cake --showtree
で、依存関係等も含めてタスクが一覧表示される。Taskの依存関係
例えば
Task1
を実行した後にTask2
を実行したい等、タスク間で依存関係を作りたい場合は、IsDependentOn
とIsDependeeOf
の二つの方法がある。
IsDependentOn("TaskName")
指定した名前のTaskが成功した後に、Taskを実行するという設定を追加する。
Task("Task1"); Task("Task2") .IsDependentOn("Task1");上記のようにすれば、
Task1
が成功した後、Task2
を実行する、という動作になる。
Task1
が失敗した場合、それ以降のTask(Task2
)は実行されない。
IsDependeeOf("TaskName")
IsDependentOn
とは逆の動作をする。つまり、指定したタスクの前に実行し、失敗すればそれ以後のタスクを行わなくなる、というような動作になる。Task("Task1"); Task("Task2") .IsDependeeOf("Task1");上記のようにすれば、
Task2
がTask1
の前に実行され、失敗すればそれ以降のタスクは行わないという動作になる。
使用場面としては、何か特定のタスクにフック的に動作させたい場合だろうか。タスクが重複した場合
タスクが増えてくると、下記のように同じタスクが依存ツリーに重複して出てくる場合がある。
Task("Task1"); Task("Task2").IsDependentOn("Task1"); Task("Task3") // Task1の依存が重複した .IsDependentOn("Task1") .IsDependentOn("Task2");上記のようになった場合も、cakebuildの方で関係を整理してくれるので、実行は
Task1
→Task2
→Task3
となり、Task1
は重複して実行されない。また、下記のように循環依存になった場合は、実行時エラーとなる。
Task("Cyclic/1").IsDependentOn("Cyclic/3"); Task("Cyclic/2").IsDependentOn("Cyclic/1"); Task("Cyclic/3").IsDependentOn("Cyclic/2");Taskの実行内容の登録
Taskに実際の動作を登録するには、
Does
をTask("TaskName")
の後に続ける。
この時、指定できる形は以下のようになる。最も単純な形
最も単純な形は、
Does(Action act)
となる。具体的には以下Task("TaskName").Does(() => Information("Hello World"));また、
Does(Func<System.Threading.Tasks.Task> act)
という形も取れるため、async awaitも可能Task("TaskName").Does(async () => await System.Threading.Tasks.Task.Delay(100));なお、
System.Threading.Tasks
名前空間はデフォルトでusingされてないので注意。また、他の
Does
にも言えることだが、以下のように複数繋げることも可能。
複数繋げた場合、書かれた順番に実行がされる。Task("TaskName") .Does(() => Information("1")) .Does(() => Information("2")) ;型付の引数をとる形
また、自分で設定した型を取ることができる。その場合は以下のような手順となる。
- パラメーターとなる型を宣言
Setup<T>(Func<ISetupContext, T> act)
でパラメーターの実体を作るDoes<T>(Action<T> act)
でパラメーターを受け取るコードは以下のようになる。
// 型の宣言 class MyClass { public string X; } Setup<MyClass>(ctx => new MyClass(){ X = ctx.Argument("X", "") }); Task("TaskName").Does<MyClass>(x => Information($"{x.X}");なお、
Setup
とTask
を書く順番は、前後しても問題ないが、同じ型で二回Setup<T>()
すると実行時にエラーになる。
ICakeContext
をとる形スクリプト全体の状態を保持する
ICakeContext
を引数に取ることもできる。Task("TaskName").Does( ctx => Information("Hello {0}", ctx.Argument("X", "default"))); Task("TaskName").Does<MyClass>( (ctx, x) => Information("Hello {0}, {1}", ctx.Argument("X", "default"), x.X));コレクションの個別要素を引数にとる形
DoesForEach
を使用することにより、IEnumerable<T>
の個別の要素について、タスクが実行できる。Task("TaskName").DoesForEach(GetFiles("*.txt"), fpath => Information($"{fpath}"));条件によって実行する、しないを決定する
例えばあるフラグが渡された場合や、特定のファイルがある場合等、条件によってタスクを実行する/しないを決定したい場合がある。
そういう時はWithCriteria
を使用する。
これもDoes
同様、型付引数をとれるもの、ICakeContext
をとれるオーバーライドが存在する。// "IsRelease"が引数に来た場合に実行されるようにする Task("Task1") .WithCriteria(() => Argument<bool>("IsRelease", false)) .Does(() => Information("release task")); Task("Task2") .IsDependentOn("Task1") .Does(() => Information("release task"));上記の通りにすれば、
dotnet cake -IsRelease -Target=Task2
とされた場合のみTask1
が実行される。ただし、
RunTarget
で指定したターゲットそのものがCriteriaがfalseになってスキップされると、cakebuildはタスク全体を失敗とみなすので注意が必要。よって、
WithCriteria
を使うTaskは、直接Targetに指定しない方が良い。必ず最後に実行される処理を入れる
try - finallyのように、エラー如何に関わらずTaskの最後に実行したい処理がある場合、
Finally
を使用する。Task("Task1") .Finally(() => Information("Finally"));ただし、依存元のTaskが失敗した場合は実行されずに終わるので注意。
Task("Task1").Does(() => throw new Exception("")); Task("Task2").IsDependentOn("Task1").Finally(() => Information("Finally"));上記の場合、
Task2
のFinally
が実行されずに終了する。例外を捕捉する
独自に処理する
Error
を使えば、Task内で例外が起こった場合、ブロック内でキャッチして無視することもできる。Task("TaskName").OnError(er => Warning($"{er}"));ただし、これは同一Task内でのみ有効な設定なので、依存元のTaskの中でエラーが出た場合は、そのまま終了してしまうので注意
Task("Task1").Does(() => throw new Exception("")); Task("Task2") .IsDependentOn("Task1") .OnError(er => Warning($"{er}"));上記の場合、
Task2
のOnErrorまでは来ないで、Task1の実行完了時点で止まる。ログ出力のみして継続する
ログ出力のみして継続したいのであれば、
ContinueOnError
を指定するだけで良い。Task("TaskName").ContinueOnError();独自に処理してそのままエラー終了する
エラーを受け取ってログ出力するだけで、そのまま終了させたい場合、
ReportOnError
を使用する。Task("TaskName").ReportOnError(er => Warning($"{er}"));最後まで実行してからエラーとして処理する
エラーが起こった場合、同一タスク内のアクションは通常スキップされるが、そうしたくない場合は、
DeferOnError
を使う。Task("TaskName") .Does(() => throw new Exception("")) .Does(() => Information("1") .Does(() => Information("2") .DeferOnError();上記のようにすると、
.Does(() => Information("1")
と.Does(() => Information("2")
の部分が実行されてからエラーとして処理される。ReportOnError()
またはOnError()
と併用することも可能。Taskの実行
RunTarget("TaskName")
とすることで、コンテキストに登録されたタスクを実行する。
固定で引数を指定することもできるが、大抵の場合引数で分けたいはずなので、RunTarget(Argument("Target", "Default"))
のような形になる。
この時、コンソールにログは出るが、更に実行結果をXMLに出力したいなどの特殊な処理をしたい場合、戻り値としてCakeReport
を受け取ることができるので、これを元にカスタム出力を行うことができる。
ただし、エラーの場合は例外が送出されるため、結果が受け取れないことに注意var report = RunTarget("TaskName"); foreach(var entry in report) { // Task,Setup,Teardownのどれか Information($"{entry.Category}"); // 所要時間 Information($"{entry.Duration}"); // Executed, Delegated, Skippedのどれか Information($"{entry.ExecutionStatus}"); // タスクの名前 Information($"{entry.TaskName}"); }SetupとTeardown
CakeBuildは、大まかに
- グローバル処理
- RunTarget
- Setup
- TaskSetup
- Task
- TaskTeardown
- Teardown
の順で処理が実行される。
Setupは全てのTaskが実行される前に、全体で一回だけ実行される処理で、主に型付きパラメーターの登録等を行う。
逆にTeardownは、全てのTaskが実行された後に一回だけ実行される処理となる。
TaskSetup/TaskTeardownは、各Taskが実行される前と後で実行される。
Teardown/TaskTeardownは、Setup及びTaskが失敗したとしても実行される。コード例は以下のようになる。
Task("Default"); Setup(ctx => Information($"global setup")); TaskSetup(ctx => Information($"task setup: {ctx.Task.Name}")); TaskTeardown(ctx => Information($"task teardown: {ctx.Task.Name}, {ctx.Successful}, {ctx.Skipped}, {ctx.ThrownException}")); Teardown(ctx => Information($"global teardown {ctx.Successful}, {ctx.ThrownException}")); RunTarget(Argument("Target", "Default"));なお、全てのSetup,Teardown,TaskSetup,TaskTeardownの登録は、RunTargetの前に行う必要があるので注意。
終りに
これで一通りのTaskに関する記述は出来たと思う。色々書いたけど、まあ
Task()
、IsDependentOf()
、Does()
だけでそれなりに書けたりする。
後は気が向いたら自分なりのベストプラクティスなんか書ければいいかなと思っている。
- 投稿日:2019-12-11T09:03:58+09:00
Unityでデスクトップマスコットを作ってみた
はじめに
はじめまして。木更津高専 Advent Calendar 2019 11日目担当の、わくと です。
初めてのQiita記事です。がんばります!概要
コーディング中・ブラウジング中に画面の右下あたりが寂しくなることはないですか?
そんなときにぴったりな、いつでも見守ってくれるデスクトップマスコットを作りました!開発環境
- Windows10
- Unity2019.2.14f1
使用したツール・モデル
デモ
ころねさんのデスクトップマスコット作ってみました?#戌神ころねMMD pic.twitter.com/CFdLYOP3rS
— わくと (@otukaw) December 11, 2019
まばたきとマウス追従をしています。実装
ボーン・モーフなどはアニメーションなどを使わず、すべてスクリプトで動かしています。
また、ウィンドウの透過などの処理はWindowsAPIを叩いています。モデル取り込み
まず、MMDのモデルをMMD4Mecanimを使ってインポートします。この記事がわかりやすかったです。
取り込む前にreadmeをしっかり確認します。表情
表情のスクリプトです。長いので簡略化してあります。
MorphController.csusing System.Collections; using System.Collections.Generic; using UnityEngine; public class MorphController : MonoBehaviour { MMD4MecanimModel model; MMD4MecanimModel.Morph[] morph; Dictionary<string, int> morphIndex = new Dictionary<string, int>(); // モーフのインデックス番号 HashSet<int> changedMorphIndex = new HashSet<int>(); void Start() { model = GetComponent<MMD4MecanimModel>(); morph = model.morphList; int i = 0; foreach (var tmp in morph) { morphIndex.Add(tmp.morphData.nameJp, i++); } } void Update() { int screenX = Screen.width; int screenY = Screen.height; Vector3 mouse = Input.mousePosition; mouse.x = mouse.x / screenX / 2 - 1; mouse.y = mouse.y / screenY / 2 - 1; setMorph("瞳_上", mouse.y); setMorph("瞳_下", -mouse.y); setMorph("瞳_左", mouse.x); setMorph("瞳_右", -mouse.x); } // Morphの名前を渡すとvalueをセットしてくれる関数 void setMorph(string name, float value) { model.morphList[morphIndex[name]].weight = value; changedMorphIndex.Add(morphIndex[name]); } }マウスの座標を、中心が(0,0)、縦方向・横方向それぞれを-1.0~1.0の範囲になるように正規化して、そのまま目のモーフに代入しています。
実際は眉毛など他の場所も操作していますが、省略します。
このスクリプトをモデルにアタッチします。体関節
表情とほぼ一緒ですが、ボーンを動かすため回転情報を渡している点が違います。
MorphController.cs// 表情のスクリプトのつづき Dictionary<string, int> boneIndex = new Dictionary<string, int>(); // ボーンのインデックス番号 HashSet<int> changedBoneIndex = new HashSet<int>(); float mag = 6.0f; // 回転情報を渡すときの倍率 void Start() { // 略 foreach (var tmp in model.boneList) { boneIndex.Add(tmp.boneData.nameJp, tmp.boneID); } } void Update() { // 略 setBone("頭", Mathf.Abs(mag * -mouse.y) <= 20f ? mag * -mouse.y : 20f * -Mathf.Sign(mouse.y), Mathf.Abs(mag * -mouse.x) <= 10f ? mag * -mouse.x : 10f * -Mathf.Sign(mouse.x), Mathf.Abs(mag / 2 * mouse.x) <= 5f ? mag / 2 * mouse.x : 5f * Mathf.Sign(mouse.x)); setBone("腰", Mathf.Abs(mag / 2 * -mouse.y) <= 3f ? mag / 2 * -mouse.y : 3f * -Mathf.Sign(mouse.y), Mathf.Abs(mag / 2 * -mouse.x) <= 10f ? mag / 2 * -mouse.x : 10f * -Mathf.Sign(mouse.x), Mathf.Abs(mag / 2 * mouse.x) <= 5f ? mag / 2 * mouse.x : 5f * Mathf.Sign(mouse.x)); } // ボーンの名前を渡すと値をセットしてくれる関数 void setBone(string name, float x, float y, float z) { Vector3 value = new Vector3(x, y, z); model.boneList[boneIndex[name]].userEulerAngles = value; changedBoneIndex.Add(boneIndex[name]); }回転しすぎないようにするために三項演算子を使っているため少し複雑に見えますが、やっていることはマウスの座標に倍率をかけて代入しているだけです。
なでなで
個人的に考えるのが一番大変だったところです。
なでなで処理は頭の上でのマウスの移動距離を使っています。NadeNadeController.csusing System.Collections; using System.Collections.Generic; using UnityEngine; public class NadeNadeController : MonoBehaviour { public static bool isNadeNade = false; // なごみ状態にあるか public static float mouseMov = 0; // なでなで中のマウス移動距離 bool hasNadeing = false; // なでなでしているか int noNadeframe = 0; // なでなでしてないフレーム数 public int nadeRate = 10; // なでなでを検知するしきい値 public int noNadeRate = 5; // なでなでしていない状態を検知するしきい値 Vector2 mousePos; // 現在のフレームのマウス座標 Vector2 prevMousePos; // 一つ前のフレームのマウス座標 void Update() { if(noNadeframe * Time.deltaTime > 0.5f) { isNadeNade = false; mouseMov = 0; } if(!hasNadeing) { noNadeframe++; } if(mouseMov >= 1000) isNadeNade = true; else isNadeNade = false; } private void OnMouseEnter() { hasNadeing = true; noNadeframe = 0; prevMousePos = Input.mousePosition; } private void OnMouseOver() { mousePos = Input.mousePosition; float distance = Mathf.Abs(Vector2.Distance(mousePos, prevMousePos)); // マウスの移動した距離 mouseMov += distance < Screen.width / nadeRate ? 0 : distance; if(distance < Screen.width / noNadeRate) { noNadeframe++; } else { noNadeframe = 0; } prevMousePos = mousePos; } private void OnMouseExit() { hasNadeing = false; noNadeframe = 0; } }特に難しいことはしていませんが、一定時間以上マウスが動かないでいると普通の表情に戻ります。
当たり判定は下の画像のようになっています。
おわりに・感想
頑張ればがんばった分だけ可愛くなるので作っていて楽しかったです。
簡単に作れるので、自分の推しで作ってみるのはいかがでしょうか?
まだまだ、待機モーションなど付け足したい機能があるので、これからも開発を続けていこうと思います!
なにかあったらTwitterに連絡してもらえると助かります。(自分の書いたコードを公開するって結構恥ずかしいんですね…)
参考サイト
https://qiita.com/hiroyuki_hon/items/931c79164b0ffe19517f
https://qiita.com/mkt_/items/82f4057f51b1657c971e
https://qiita.com/gatosyocora/items/7cbe14914f8e603f2eab
http://chokuto.ifdef.jp/urawaza/api/著作権表記
©2019 cover corp.
- 投稿日:2019-12-11T05:14:35+09:00
UnityでglTFローダを作ってみたはなし
はじめに
この記事は、ドワンゴ Advent Calendar 2019の11日目の記事です。
ドワンゴではniconicoの課金システムの開発をしています。
課金関係のことに触れられればよかったのですが、普段は趣味でUnity+C#を触っているため、
今回はそちらに関する記事です。
シンプルなglTFローダの作り方などを解説している記事はありますが、複雑なモデル描画まで行っている記事が見当たらず、
かなり苦戦したため書き残しておきます。ようは備忘録です。
Qiita記事はドワンゴに来る前に非公開記事として共有することにしか使ってなかったため、他記事に比べ読みづらいと思いますが生暖かい目で見守ってください。何故やろうとしたか
VR向けアバターフォーマットであるVRMはglTFをベースとしているため、glTF自体に興味はあったが
触る機会がなく、ふわっとしか理解していなかったので、リファレンスを参考に自分で作ってみようと思いました。
(VRMのロード時間最適化したいといったのもありますが)。実際にやったこと
GLBの処理
GLBファイルのバイト配列をSpanにし、ヘッダ部分を
Slice
してMemoryMarshal
でCastすることで構造体として扱いました。
Unityでも、System.Memory
をnugetから落としてくれば利用できるためぜひ使ってみてください。var headerSpan = glbSpan.Slice(0, Marshal.SizeOf<Format.GLBHeader>()); var header = MemoryMarshal.Cast<byte, Format.GLBHeader>(headerSpan)[0];チャンクを処理する際も、
MemoryMarshal
で処理することでほぼ処理速度を気にせず構造体配列として扱えます。
この時、フォーマットの種類であるJSON
orBIN
の判定を、intとして処理することで1byteずつASCIIで判定することなく行えます(つまり、enumで判定できます)ので、フォーマット部はintで行うことをおすすめします。JSON: 0x4e4f534a BIN: 0x004e4942glTFのJSON部処理
mebiusbox/gltf(Github) で公開されている以下の画像を参考にパーサを実装しました。
最初はglTFのjsonスキーマを読みながら実装をしていたのですが、このクイックリファレンスのおかげでかなり楽に行えました。メッシュ処理
メッシュには
Primitive
が配列として格納されているため、それをループで処理します。
Primitive
= Unityで言うサブメッシュを表します。先にvertex数分確保が必要ですが、Primitive1個目のPosition数*Primitive数で初期化しました。
ここでは、Position
とNormal
、TexCoord0
をこの基準で格納しています。
Indices
はPrimitiveの数分のジャグ配列を初期化し、それぞれ格納しています。実際にUnity上のメッシュにする際は、
subMeshCount
にPrimitive
の数をセットし、SetTriangles
メソッドでIndices
をセットしました。var umesh = new Mesh { vertices = position, normals = normal, uv = texcoord0.ToArray(), subMeshCount = mesh.Primitives.Count }; for (var i = 0; i < mesh.Primitives.Count; i++) umesh.SetTriangles(indices[i], i);そのまま描画しようとすると座標系の違いにより左右反転して表示されるため、変換を行う必要があります。
ここで変換が必要なのはPosition
とNormal
です(TangentはVRMの仕様上、含まれないため省いています)
単純にPositionのx座標を反転し、IndicesをVector3として見立てた状態での、X座標とZ座標を入れ替えることで行いました。困ったこと
右手系座標を左手系座標へ変換する際のSpanのパフォーマンス
Accessorの値配列を得るために、Spanを用いることで高速化しようとしましたが、書き換え時のパフォーマンスがよくありませんでした
(1000ループで約3000ns)
MemoryMarshal.Cast
でfloat
にキャストした後、該当位置に書き込みなども行いましたが、まったくパフォーマンスがよくならず、頭を抱えました。
結果としては、現在のUnityでの.NET Standard
バージョンが2.0
であり、ランタイムに最適化が入っていないことによるパフォーマンス低下であり、一度通常の配列として持ち、それをポインタ経由でいじることでパフォーマンスがよくなりました。
そのため、事前に一定のヒープを確保しておき、そこに保持することで高速化することが出来ました。まとめ
実際に自分の手でローダを作ってみることで、glTF自体や、Unityの描画周りなどを知ることができました。
車輪の再発明ではありますが、パフォーマンスチューニングを含む様々な知見を得られたため、かなりプラスでした。まだ全体の実装が出来てないため、出来上がったらコード込みの詳細の記事を出そうと思います。
- 投稿日:2019-12-11T01:14:44+09:00
UnityのScriptable Render Pipeline(SRP)でライトの情報を取り出すメモ
前回、SRPの最初の導入を行ってみました。
https://qiita.com/Shibash/items/d6bdfc956859138ad6ff前回の結果画面を見てみると、のっぺりしていて立体感がありません。
なので、立体感を出すために、簡単にライティングをしてみます。Unityが用意しているUniversalRPは、いつも通りSpotLightやDirectionalLightを利用することが出来ます。
同じように、UnityのDirecitonal Lightの情報を使うようにして、ライティングを行わせるには
どうすれば良いかメモ書きです。Unityバージョンは2019.3.0b6
com.unity.render-pipelines.core 7.1.1
com.unity.render-pipelines.lightweight 7.1.1
com.unity.render-pipelines.universal 7.1.1シェーダの改良
ライティングを行うために、前回のシェーダの改良を行います。
長くなるので内容は割愛しますが、少なくともシェーダにライトが持つ方向などのパラメータを渡す必要があります。コードを確認
・ライトの情報をシェーダに渡している箇所はどこなのか?
ForwardLightsクラスのSetupMainLightConstants関数がその処理を行っていそうです
Vector4 lightPos, lightColor, lightAttenuation, lightSpotDir, lightOcclusionChannel; InitializeLightConstants(lightData.visibleLights, lightData.mainLightIndex, out lightPos, out lightColor, out lightAttenuation, out lightSpotDir, out lightOcclusionChannel); cmd.SetGlobalVector(LightConstantBuffer._MainLightPosition, lightPos); cmd.SetGlobalVector(LightConstantBuffer._MainLightColor, lightColor);cmdはCommandBufferで、このクラスを通してシェーダの変数を渡すことが出来ます。
CommandBufferPoolというクラスに、CommandBufferの管理を行わせていて
プールして使いまわすようにしているようです。・ライトの情報を取得しているところはどこなのか?
LightData構造体が、VisibleLightという構造体を持っています。
UniversalPipelineのInitializeRenderingDataで受け取っていてCullingResultsから取り出せるようです。var visibleLights = cullResults.visibleLights;これは前回の記事で行った、カリング結果にデータが入ってきています。
https://qiita.com/Shibash/items/d6bdfc956859138ad6ffライティングの方向はShadowUtilsのSetupShadowCasterConstantBuffer関数で渡しているようです。
行列の内部に含まれるデータの扱いは、以下の記事が参考になります。
http://marupeke296.sakura.ne.jp/DXG_No39_WorldMatrixInformation.htmlVector3 lightDirection = -shadowLight.localToWorldMatrix.GetColumn(2); cmd.SetGlobalVector("_ShadowBias", shadowBias); cmd.SetGlobalVector("_LightDirection", new Vector4(lightDirection.x, lightDirection.y, lightDirection.z, 0.0f));まとめ
以下の2つを押さえておけば、ライトの情報を取り出してシェーダに渡すことが出来ました。
・CommandBufferを使って、シェーダのグローバル変数を渡すことが出来る。
・ライト情報はCullingResultが持っている。Directionalライト
Directional、Spot、Pointライト
参考サイト
ほぼほぼ自分メモになってしまいましたが
以下に、SRPを利用したライティングがしっかりまとめられているので、こちらを読めば色々出来ると思います。
https://catlikecoding.com/unity/tutorials/scriptable-render-pipeline/lights/↓ちょっと新しい版
https://catlikecoding.com/unity/tutorials/custom-srp/directional-lights/
- 投稿日:2019-12-11T00:45:18+09:00
関数を返す関数。
この記事は C# Advent Calendar 2019 の11日目です。
C#で「関数を返す関数」を作成するには、ActionやFuncなどのデリゲートを返す関数を作成します。「関数を返す関数」を作成することにより、一部の処理をシンプルに記述することができます。どのように使うのでしょうか。例を見てみましょう。
Actionを返す関数
1秒間隔でコンソールに現在時刻を表示するプログラムを作成します。まずは通常?の書き方です。
using System.Timers; class Foo { private Timer MyTimer; public void TimerStart() { MyTimer = new Timer(1000); MyTimer.Elapsed += (s, e) => Console.WriteLine(DateTime.Now.ToString()); MyTimer.Start(); } public void TimerStop() { MyTimer.Stop(); } }TimerStart()で開始し、TimerStop()で停止します。呼び出すのは以下のようになります。
var foo = new Foo(); foo.TimerStart(); Console.ReadKey(); foo.TimerStop();これを「Actionを返す関数」で書きかえてみましょう。TimerStart()は、myTimerを停止させるためのActionを返す、という考え方です。
using System.Timers; class Foo { public Action TimerStart() { var myTimer = new Timer(1000); myTimer.Elapsed += (s, e) => Console.WriteLine(DateTime.Now.ToString()); myTimer.Start(); // myTimerを停止するActionを返す。 return () => myTimer.Stop(); } }はじめの方法からの変更点としては
- タイマーを停止するTimerStop()がなくなった
- MyTimerがローカル変数になった。
- TimerStart()しない限り、TimerStop()を呼び出せなくなった。
と、少しシンプルに記述することができるようになりました。その代り、呼び出し側に少し注意が必要です。これを呼び出す側は以下のように変化します。
var foo = new Foo(); var stop = foo.TimerStart(); Console.ReadKey(); stop();myTimerはローカル変数だから、TimerStart()の終了後は利用できないのでは?と思われるかもしれません。しかし問題はありません。詳しい説明は省きますが、myTimerは戻り値のAction内で利用されているため、TimerStart()が終了しても開放されないのです。
この方法は初めの方法に比べパフォーマンスが若干劣ります。しかしゲームなどのパフォーマンスが追及される場合でない限り、問題ないと考えます。Funcを返す関数
先ほどの例は、関数が関数を返しました。これを進めると、関数の数珠つなぎにすることができます。別の例を示します。テキストファイルでログを作成するプログラムです。
public Func<string, Action> LogStart(string path) { var writer = new System.IO.StreamWriter(path); return text => // Funcを返す { writer.WriteLine(text); return () => writer.Dispose(); // Actionを返す }; }LogStart()はかなり複雑そうに見えますが、大丈夫です。よく見ると、LogStart()は「ある関数」を返し、ある関数はまた「別の関数」を返しています。詳しく見てみましょう。
LogStart()は、ログファイルを新規作成し、「ログに文字を書き込む関数」を返します。「ログに文字を書き込む関数」はさらに、「ログをクローズする関数」を返します。
これを利用する方法は以下の通りです。
var log = LogStart(@"C:\Debug\test.log"); log("Hello"); // LogにHelloを書き込む。 log("World")(); // LogにWorldを書き込んで、閉じる。log("World")()の部分、こんな書き方してもいいんでしょうか?いいんです。でも少しわかりづらいですね。さすがにここまでくると、LogStartはActionを二つ返したほうがわかりやすいでしょう。
class LogControl { public Action<string> Write { get; set; } public Action Close { get; set; } } public LogControl LogStart(string path) { var writer = new System.IO.StreamWriter(path); return new LogControl() { Write = text => writer.WriteLine(text), Close = () => writer.Dispose() }; }var log = LogStart(@"C:\Debug\test.log"); log.Write("Hello"); log.Write("World"); log.Close();またはタプルで返すという方法もあります。
public (Action<string> Write, Action Close) LogStart(string path) { var writer = new System.IO.StreamWriter(path); // ファイルを作成し、 Action<string> write = text => writer.WriteLine(text); // テキストを出力して、 Action close = () => writer.Dispose(); // 閉じる。 return (write, close); }この書き方は、コメントに書きましたが、一つの関数の中で処理が順番に記述できるためわかりやすい、と私は感じています。
まとめ
以上、「関数を返す関数」の例を見てきました。「関数を返す関数」は、関数内で作成したオブジェクトを、その関数が終わってからも利用可能にします。単なる関数ではなく、内部に状態を持つ小さなクラスのようにふるまいます。通常の関数と違い呼び出し方に注意点が必要ですが、よろしければ利用を検討してみてください。