- 投稿日:2021-03-02T19:10:44+09:00
BlazorアプリからPostGraphile(GraphQL)とTelerikのGridコンポーネントを使ってみた
記事の内容
- Visual Studio 2019でBlazorアプリのテンプレートを選んでプロジェクトを新規作成すると、データソースがjsonファイルになりましたが、それをPostGraphile(GraphQL API)に置き換えます。
- その後、Telerik UI for Blazor(有償商品です)のGridコンポーネントを使用して、データをグリッドで一覧表示するWebページを追加します。サードパーティーのコンポーネントを導入すると、定型的な処理の開発時間を大幅に短縮でき、プログラマが追加でコードを記述すれば小回りも利きますので、ローコードの感覚になります。
※ PostGraphileのトップページに「No N+1 problem」と書かれています。いいですね~
※ HasuraもPostGraphileと類似のプロダクトと認識していますが、本記事では私が利用経験のあるPostGraphileを使用しました。
※ 私はTelerikの回し者ではありません。ソースコード
GitHubに置きました。
参考ページ(感謝します)
C#(ASP.NET Core)で GraphQL API を提供する
Blazorアプリのプロジェクトを新規作成する
Visual Studio 2019のプロジェクト新規作成画面で、以下のようにBlazorアプリのテンプレートを選択します。
プロジェクト名を「SamplePostGraphile」、ソリューション名を「SamplePostGraphile_sol」にしましたが、名前は何でも良いです。
Blazor WebAssembly Appを選択します。今回はhttpsは外しました。
以下のファイルが自動生成されました。
ソースコードを読むと、データソースとしてwwwroot\sample-data\weather.jsonが使用されています。このままビルドして動かしてみます。
左メニューから「Fetch data」を選択すると、以下のように右側にjsonファイルのデータが一覧表示されました。この時点でgit commitしました。
手順は、Visual Studioのgitメニューからgitリポジトリを作成し、GitHubにpushしました。
以下のコミットメッセージは、Visual Studioが自動生成したものです。PostgreSQLにデータを用意し、PostGraphileを立ち上げる
ERモデリングツールでの作業
ERモデリングツールはA5:SQL Mk-2を使用します。
それでは、BlazorアプリのデータソースをPostGraphileに置き換えます。
以下のjsonファイルの中を見ながら、これと類似のテストデータをPostgreSQLに用意します。wwwroot\sample-data\weather.json[ { "date": "2018-05-06", "temperatureC": 1, "summary": "Freezing" }, { "date": "2018-05-07", "temperatureC": 14, "summary": "Bracing" }, { "date": "2018-05-08", "temperatureC": -13, "summary": "Freezing" }, { "date": "2018-05-09", "temperatureC": -16, "summary": "Balmy" }, { "date": "2018-05-10", "temperatureC": -2, "summary": "Chilly" } ]以下のER図を描きました。エンティティ1つだけですね。
ER図メニューから「DDLを作成する」を選択します。
RDBMS種類でPostgreSQLを選択し、DDL生成ボタンを押します。
以下のDDLが生成されました。
-- RDBMS Type : PostgreSQL -- Application : A5:SQL Mk-2 /* BackupToTempTable, RestoreFromTempTable疑似命令が付加されています。 これにより、drop table, create table 後もデータが残ります。 この機能は一時的に $$TableName のような一時テーブルを作成します。 */ -- WeatherForecast --* BackupToTempTable DROP TABLE if exists weather_forecasts CASCADE; --* RestoreFromTempTable CREATE TABLE weather_forecasts ( id integer NOT NULL , dt date NOT NULL , temperature_c double precision NOT NULL , summary character varying NOT NULL , CONSTRAINT weather_forecasts_PKC PRIMARY KEY (id) ) ; COMMENT ON TABLE weather_forecasts IS 'WeatherForecast'; COMMENT ON COLUMN weather_forecasts.id IS 'Id'; COMMENT ON COLUMN weather_forecasts.dt IS 'Date'; COMMENT ON COLUMN weather_forecasts.temperature_c IS 'TemperatureC'; COMMENT ON COLUMN weather_forecasts.summary IS 'Summary';PostgreSQLの作業
本記事ではLinux上のPostgreSQLを使用します。
psqlを起動します。psql --host=localhost --username=postgres --passwordデータベースを作成します。名前を「sample_db」にしましたが、何でも良いです。
CREATE DATABASE sample_db;カレントデータベースを、作成したsample_dbに切り替えます。
\c sample_db先ほどERモデリングツールが生成したDDLをpsqlにコピペして実行します。
以下のINSERT文を流して、2000年1月1日から150日分のテストデータを作成します。
テーブルにはidの降順でINSERTしてみます。INSERT INTO weather_forecasts ( id, dt, temperature_c, summary ) SELECT id, ('1999-12-31'::DATE + (id::TEXT || ' days')::INTERVAL)::DATE AS dt, (random() * 75 - 20)::INT AS temperature_c, CASE (random() * 1000)::INT % 10 WHEN 0 THEN 'Freezing' WHEN 1 THEN 'Bracing' WHEN 2 THEN 'Chilly' WHEN 3 THEN 'Cool' WHEN 4 THEN 'Mild' WHEN 5 THEN 'Warm' WHEN 6 THEN 'Balmy' WHEN 7 THEN 'Hot' WHEN 8 THEN 'Sweltering' WHEN 9 THEN 'Scorching' END AS summary FROM generate_series(1, 150) AS id ORDER BY id DESC;以下のSELECT文を流して、データが作成されたか確認します。
SELECT * FROM weather_forecasts;以下のようにidの降順で表示されましたが、順番に意味はありません。
psqlから抜けます。
PostGraphileの作業
本記事ではPostGraphileをPostgreSQLと同じLinuxホストにインストールします。
このページを参考にして、PostGraphileをインストール&起動します。
Dockerを使う方法もあります。インストール
npm install -g postgraphile起動コマンド例
postgraphile --connection postgres://postgres:secret@localhost/sample_db --port 15000 --schema public --export-schema-graphql ~/schema.graphql --cors起動画面
本記事ではBlazorアプリでのCORSエラーを避けるために、単に「--cors」オプションを付けてPostGraphileを起動しましたが、本番環境では安全な方法でCORSエラーを回避してください。
postgraphileコマンドを起動するだけで、PostgreSQLのスキーマを読み取ってGraphQLエンドポイントを自動生成してくれます。
とても楽で、これもノーコードと言えるかもしれません。起動画面によれば、URLは
- GraphQLエンドポイント:http://localhost:15000/graphql
- GraphiQL:http://localhost:15000/graphiql
となっています。
本記事では、このLinuxホストのIPアドレスは「192.168.1.7」です。
GraphQLエンドポイントのURLの「localhost」を「192.168.1.7」に書き換えて、後ほどBlazorアプリで使用します。ここでブラウザからGraphiQLにアクセスして、クエリーを発行したりドキュメントを見たりしてみましょう。
クエリー例
query allWeatherForecasts { allWeatherForecasts { nodes { id dt temperatureC summary } } }ブラウザ画面
レスポンス
{ "data": { "allWeatherForecasts": { "nodes": [ { "id": 1, "dt": "2000-01-01", "temperatureC": 7, "summary": "Hot" }, { "id": 2, "dt": "2000-01-02", "temperatureC": -16, "summary": "Cool" }, { "id": 3, "dt": "2000-01-03", "temperatureC": 17, "summary": "Hot" }, (中略) { "id": 150, "dt": "2000-05-29", "temperatureC": 14, "summary": "Freezing" } ] } } }psqlからSELECT文を実行したときはid列の降順で表示されましたが、今回のレスポンスを見ると昇順になっていますね。
この順番は気にしないことにして、先に進みます。BlazorアプリのデータソースをjsonファイルからPostGraphileに置き換える
Visual Studioでの作業に戻ります。
ファイル削除:wwwroot\sample-data\weather.json
Blazorアプリのデータソースは「wwwroot\sample-data\weather.json」でしたが、もう使用しませんのでsample-dataディレクトリごと削除します。
削除後のファイルは以下の通り。パッケージのインストール
NuGetで以下の3パッケージをインストールします。
ファイル新規作成:Shared/WeatherForecast.cs
GraphQL APIのレスポンスデータを格納するデータ構造を作成します。
Sharedディレクトリ配下に「WeatherForecast.cs」を追加します。以下の内容にします。
Shared/WeatherForecast.csusing System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace SamplePostGraphile.Shared { // クエリー // query allWeatherForecasts { // allWeatherForecasts { // nodes { // id // dt // temperatureC // summary // } // } // } public class WeatherForecast { public int Id { get; set; } public DateTime Dt { get; set; } private double _tempC; public double TemperatureC { get { return _tempC; } set { _tempC = value; } } public double TemperatureF { get { return 32 + (_tempC / 0.5556); } set { _tempC = (value - 32) * 0.5556; } } public string Summary { get; set; } public WeatherForecast() { Dt = DateTime.Now.Date; } } // レスポンス例 // { // "data": { // "allWeatherForecasts": { // "nodes": [ // { // "id": 1, // "dt": "2000-01-01", // "temperatureC": 7, // "summary": "Hot" // }, // { // "id": 2, // "dt": "2000-01-02", // "temperatureC": -16, // "summary": "Cool" // }, // // (中略) // // ] // } // } // } public class AllWeatherForecastsResponse { public AllWeatherForecastsContent allWeatherForecasts { get; set; } public class AllWeatherForecastsContent { public List<WeatherForecast> Nodes { get; set; } } } }ファイル新規作成:Services/WeatherForecastService.cs
GraphQLクエリーを発行して、そのレスポンスからデータを取り出してリターンするメソッドを持つクラスを作成します。
本記事では、CRUDのうちR(Read)のみ実装しました。
プロジェクト配下に「Services」というディレクトリを作成します。Servicesディレクトリ配下に「WeatherForecastService.cs」を追加します。
以下の内容にします。
SamplePostGraphile/Services/WeatherForecastService.csusing SamplePostGraphile.Shared; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using GraphQL.Client.Http; using GraphQL.Client.Serializer.Newtonsoft; using GraphQL; namespace SamplePostGraphile.Services { public class WeatherForecastService { // 行儀が良くないですが、今回はここにGraphQLエンドポイントのURLを書いてしまいます private const string graphql_http = "http://192.168.1.7/15000/graphql"; public async Task<List<WeatherForecast>> GetForecastListAsync() { using var graphQLClient = new GraphQLHttpClient(graphql_http, new NewtonsoftJsonSerializer()); var allWeatherForecasts = new GraphQLRequest { Query = @" query allWeatherForecasts { allWeatherForecasts { nodes { id dt temperatureC summary } } } ", OperationName = "allWeatherForecasts", }; var graphQLResponse = await graphQLClient.SendQueryAsync<AllWeatherForecastsResponse>(allWeatherForecasts); return graphQLResponse.Data.allWeatherForecasts.Nodes; } //public async Task UpdateForecastAsync(WeatherForecast forecastToUpdate) //{ // 未実装 //} //public async Task DeleteForecastAsync(WeatherForecast forecastToRemove) //{ // 未実装 //} //public async Task InsertForecastAsync(WeatherForecast forecastToInsert) //{ // 未実装 //} } }変更:Program.cs
プロジェクト内でWeatherForecastServiceクラスを使えるようにします。
変更内容は以下の通りです。Program.cs+ using SamplePostGraphile.Services; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.Net.Http; using System.Text; using System.Threading.Tasks; namespace SamplePostGraphile { public class Program { public static async Task Main(string[] args) { var builder = WebAssemblyHostBuilder.CreateDefault(args); builder.RootComponents.Add<App>("app"); builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }); + builder.Services.AddScoped<WeatherForecastService>(); await builder.Build().RunAsync(); } } }変更:Pages/FetchData.razor
データソースをjsonファイルからPostGraphileに置き換えるようにソースコードを変更します。
変更内容は以下の通りです。Pages/FetchData.razor@page "/fetchdata" - @inject HttpClient Http + @using SamplePostGraphile.Shared + @using SamplePostGraphile.Services + @inject WeatherForecastService ForecastService <h1>Weather forecast</h1> - <p>This component demonstrates fetching data from the server.</p> + <p>This component demonstrates fetching data from the postgraphile server.</p> @if (forecasts == null) { <p><em>Loading...</em></p> } else { <table class="table"> <thead> <tr> <th>Date</th> <th>Temp. (C)</th> <th>Temp. (F)</th> <th>Summary</th> </tr> </thead> <tbody> @foreach (var forecast in forecasts) { <tr> - <td>@forecast.Date.ToShortDateString()</td> + <td>@forecast.Dt.ToShortDateString()</td> <td>@forecast.TemperatureC</td> <td>@forecast.TemperatureF</td> <td>@forecast.Summary</td> </tr> } </tbody> </table> } @code { - private WeatherForecast[] forecasts; + List<WeatherForecast> forecasts { get; set; } protected override async Task OnInitializedAsync() { - forecasts = await Http.GetFromJsonAsync<WeatherForecast[]>("sample-data/weather.json"); + await GetForecasts(); } + async Task GetForecasts() + { + forecasts = await ForecastService.GetForecastListAsync(); + } - public class WeatherForecast - { - public DateTime Date { get; set; } - - public int TemperatureC { get; set; } - - public string Summary { get; set; } - - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); - } }ビルドして動かしてみます。
左メニューから「Fetch data」を選択すると、以下のように右側にPostgreSQLデータベースから取得したデータが一覧表示されました。以上でデータソースの置き換えは完了です。
この時点でgit commitしました。git add -A git commit -m "(1)データソースをweather.jsonからPostGraphileに変更します"ここまでのソースコードは、Telerikのコンポーネントがなくてもビルド/実行できます。
Telerik UI for BlazorのGridコンポーネントを使ってデータを一覧表示する
これ以降は、Telerikのプロダクトがインストールされた環境で作業します。
プロジェクトをTelerik UI for Blazorのアプリケーションにコンバートする
以下のように、Visual Studioの拡張機能メニューからTelerikアプリケーションにコンバートします。
NuGetパッケージの管理画面で、Telerik.UI.for.Blazorがインストールされたことを確認します。
コンバート完了時点で、一旦git commitしました。
git add -A git commit -m "(2)プロジェクトをTelerikアプリケーションにコンバートします"ファイル新規作成:Pages/Grid.razor
TelerikのGridコンポーネントを使用して、データを一覧表示するページを作成します。
Pagesディレクトリ配下に「Grid.razor」を追加します。以下の内容にします。
Pages/Grid.razor@page "/grid" @using SamplePostGraphile.Shared @using SamplePostGraphile.Services @inject WeatherForecastService ForecastService <div class="container-fluid"> <div class='row my-4'> <div class='col-12 col-lg-9 border-right'> <TelerikGrid Data="@forecasts" Height="550px" FilterMode="@GridFilterMode.FilterMenu" Sortable="true" Pageable="true" PageSize="20" Groupable="true" Resizable="true" Reorderable="true" OnUpdate="@UpdateHandler" OnDelete="@DeleteHandler" OnCreate="@CreateHandler" EditMode="@GridEditMode.Inline"> <GridColumns> <GridColumn Field="Id" Title="Id" Width="100px" Editable="false" Groupable="false" /> <GridColumn Field="Dt" Title="Date" Width="220px" DisplayFormat="{0:dddd, dd MMM yyyy}" /> <GridColumn Field="TemperatureC" Title="Temp. C" Width="100px" DisplayFormat="{0:N1}" /> <GridColumn Field="TemperatureF" Title="Temp. F" Width="100px" DisplayFormat="{0:N1}" /> <GridColumn Field="Summary" /> <GridCommandColumn Width="200px" Resizable="false"> <GridCommandButton Command="Save" Icon="@IconName.Save" ShowInEdit="true">Update</GridCommandButton> <GridCommandButton Command="Edit" Icon="@IconName.Edit" Primary="true">Edit</GridCommandButton> <GridCommandButton Command="Delete" Icon="@IconName.Delete">Delete</GridCommandButton> <GridCommandButton Command="Cancel" Icon="@IconName.Cancel" ShowInEdit="true">Cancel</GridCommandButton> </GridCommandColumn> </GridColumns> <GridToolBar> <GridCommandButton Command="Add" Icon="@IconName.Plus" Primary="true">Add Forecast</GridCommandButton> <GridCommandButton Command="ExcelExport" Icon="@IconName.FileExcel">Export to Excel</GridCommandButton> </GridToolBar> <GridExport> <GridExcelExport FileName="weather-forecasts" AllPages="true" /> </GridExport> </TelerikGrid> </div> <div class='col-12 col-lg-3 mt-3 mt-lg-0'> <h3>Telerik UI for Blazor Grid</h3> </div> </div> </div> @code { List<WeatherForecast> forecasts { get; set; } protected override async Task OnInitializedAsync() { await GetForecasts(); } async Task GetForecasts() { forecasts = await ForecastService.GetForecastListAsync(); } public async Task DeleteHandler(GridCommandEventArgs args) { //WeatherForecast currItem = args.Item as WeatherForecast; //await ForecastService.DeleteForecastAsync(currItem); //await GetForecasts(); } public async Task CreateHandler(GridCommandEventArgs args) { //WeatherForecast currItem = args.Item as WeatherForecast; //await ForecastService.InsertForecastAsync(currItem); //await GetForecasts(); } public async Task UpdateHandler(GridCommandEventArgs args) { //WeatherForecast currItem = args.Item as WeatherForecast; //await ForecastService.UpdateForecastAsync(currItem); //await GetForecasts(); } }変更:Shared/NavMenu.razor
実行時の左メニューにGridを追加します。
変更内容は以下の通りです。Shared/NavMenu.razor<div class="top-row pl-4 navbar navbar-dark"> <a class="navbar-brand" href="">SamplePostGraphile</a> <button class="navbar-toggler" @onclick="ToggleNavMenu"> <span class="navbar-toggler-icon"></span> </button> </div> <div class="@NavMenuCssClass" @onclick="ToggleNavMenu"> <ul class="nav flex-column"> <li class="nav-item px-3"> <NavLink class="nav-link" href="" Match="NavLinkMatch.All"> <span class="oi oi-home" aria-hidden="true"></span> Home </NavLink> </li> <li class="nav-item px-3"> <NavLink class="nav-link" href="counter"> <span class="oi oi-plus" aria-hidden="true"></span> Counter </NavLink> </li> <li class="nav-item px-3"> <NavLink class="nav-link" href="fetchdata"> <span class="oi oi-list-rich" aria-hidden="true"></span> Fetch data </NavLink> </li> + <li class="nav-item px-3"> + <NavLink class="nav-link" href="grid"> + <span class="oi oi-grid-four-up" aria-hidden="true"></span> Grid + </NavLink> + </li> </ul> </div> @code { private bool collapseNavMenu = true; private string NavMenuCssClass => collapseNavMenu ? "collapse" : null; private void ToggleNavMenu() { collapseNavMenu = !collapseNavMenu; } }ビルドして動かしてみます。
以下のように左メニューに「Grid」が追加されました。これを選択すると、右側にTelerikのGridコンポーネントでデータが一覧表示されました。CRUDのうちRしか実装していませんが、試しに任意の行のEditボタンを押してDate列の右端をクリックしてみます。以下のようにカレンダー入力が出てきました。
以上で作業が完了しましたので、git commitしました。
git add -A git commit -m "(3)Grid.razorページを追加します"GitHubにもpushしました。
今後
TelerikのGridコンポーネントは機能がリッチだそうですので、深堀りしてみたいですね。
サードパーティーのコンポーネントに習熟すれば、ノーコードに劣らないスピード感でアプリを開発できそうです。
むしろ、数多あるNoCodeから適切なものを選ぶ→NoCodeで開発する→場合によってはYesCodeで作り直す、というステップを踏むより負担が少ない気がします。以上です。
- 投稿日:2021-03-02T18:30:48+09:00
Xamarin.Androidで動的に追加したコントロールの文字や背景の色を変える
最近、目標達成スケジュールというアプリを作りました。
これを作るにあたり、調べてもなかなか情報が見つからないことがあったので、同じようなところで困っている人のために共有しようと思います。間違った知識、非効率的な方法を紹介しているかもしれませんが、一時お付き合いください。
環境
windows10
VisualStudio2019
Xamarin.Androidコントロールを追加する
まずはコントロールを追加しましょう。
コントロールを追加する方法は、XMLファイルで定義したコントロールをCSファイルで利用する方法とあまり変わりません。まずレイアウトを作成し、そのレイアウトの中にコントロールを追加します。
var ll = new LinearLayout(this) { Orientation = Orientation.Vertical,//縦並びにする LayoutParameters = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MatchParent, ViewGroup.LayoutParams.MatchParent) }; var title = new TextView(this) { Text = "タイトル", LayoutParameters = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MatchParent, ViewGroup.LayoutParams.WrapContent) }; ll.AddView(title);//これでレイアウト内にtitleテキストが表示されるこれをSetContentViewの引数に指定するだけです。
LayoutParamsは縦横の大きさを指定します。MatchParentとWrapContent以外にも値を直接指定することができます。自分は常に画面サイズの何分の1を維持したいときなどに使っています。
コントロールの色を変える
こっちがこの記事のメインです。プログラムからコントロールを追加する方法はググれば簡単に見つかるのですが、探し方が悪かったのか語る必要もないほど常識的なことなのか、色を指定する方法がなかなか見つかりませんでした。同じところで迷っている人がもしいれば、参考にしてください。
var title = new TextView(this) { Text = "タイトル", LayoutParameters = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MatchParent, ViewGroup.LayoutParams.WrapContent) }; title.SetTextColor(Android.Graphics.Color.Argb(200, 200, 0, 0));//テキストの色 title.SetBackgroundColor(Android.Graphics.Color.Argb(200, 0, 200, 0));//背景の色 ll.AddView(title);これだけでOKです。
- 投稿日:2021-03-02T18:20:48+09:00
アルゴリズムの基礎と疑似コード
アルゴリズムとは
アルゴリズムは有効な入力に対して、計算能力によって求められた結果を有限な時間のうちに出力することができる、定義の明確な一連の手順のことです。それはつまり有効なアルゴリズムはどのプログラミング言語においても再現することができますし、逆にアルゴリズムを実際の計算機で再現することをプログラミングということもできるわけです。
擬似コード
擬似コードはプログラミング言語ごとの特性などを無視して普遍的な理解を共有するためのものでアルゴリズムを抽象化するものです。正しく書かれた有効な擬似コードはどの言語においても再現できます。
擬似コードの記法
擬似コードの記法は様々あり、これは一例です。
記号 意味 V variable 変数 E Expression 式 B Boolean 真偽 N Integer Valued Statement 実数値を持つ式 S Statement 文 P Procedure 戻り値のない一連の手順 x Parameter 宣言された変数 <- Assignment 代入 A[i] Array 配列 簡単な使用例
//計算 (x + y) * 2 //条件分岐 if B S else T // 反復 do S while B // forを用いた反復 for v <- n to m do Sアルゴリズムを擬似コードで書く
例として、与えられた値の出現回数を数えるコードを書いてみましょう
ALGORITHM CountSameNum(A[0..n-1], p) count <- 0 for i <- 0 to n - 1 do if A[i] == p count <- count + 1 return countアルゴリズムの分析と効率性
多くの場合アルゴリズムは多くのデータ入力があるため計算能力とその効率の分析はとても重要です
アルゴリズムの効率は二種類あります
- 時間的効率
- 空間的効率
時間的効率は計算にかかる時間における効率性で入力量に対しての経過時間を分析するものです
それにたいして空間的効率は入力量が記録機器において占める量を分析するものです擬似コードにおける時間的効率は計算処理が行われる単位にコードを分割してそれを計上します
ALGORITHM CountSameNum(A[0..n-1], p) count <- 0 C1 for i <- 0 to n - 1 do if A[i] == p C2 count <- count + 1 C3 return count C4$T(n) = c1+(c2+c3)(n-1)+c4 $
もちろん擬似コードをプログラミング言語に落とし込んで処理時間を計算することができますが、それは実行する計算機の影響をうけます
using System; class Program { static void Main(string[] args) { var sw = new System.Diagnostics.Stopwatch(); int[] sampleArray = new int[8]{1,2,3,4,1,2,3,4}; int target = 1; sw.Start(); int result = CountSameNum(sampleArray, target); sw.Stop(); Console.WriteLine("result is " + result + " it took " + sw.ElapsedMilliseconds + " milliseconds"); } static int CountSameNum(int[] arr, int target){ int count = 0; for(int i = 0; i < arr.Length; i++){ if(arr[i] == target){ count = count + 1; } } return count; } }その他C#で書いたアルゴリズム
2進数から10進数へclass Program { static void Main(string[] args) { string binaryValue = Console.ReadLine(); Console.WriteLine(ConvertBinaryToDecimal(binaryValue)); } static int ConvertBinaryToDecimal(string binar) { char[] binaryArray = binar.ToCharArray(); Array.Reverse(binaryArray); int result = 0; for (int i = 0; i < binaryArray.Length; i++) { int binaryCharToInt = binaryArray[i] - '0'; result += (binaryCharToInt * ((int)Math.Pow(2, i))); } return result; } }配列の2つの要素の最小距離を求めるstatic int MinimumDistance(int[] IntList) { int dmin = int.MaxValue; for(int i = 0; i < IntList.Length; i++) { for(int j = 0; j < IntList.Length; j++) { if(j != i && Math.Abs(IntList[i] - IntList[j]) < dmin) { dmin = Math.Abs(IntList[i] - IntList[j]); } } } return dmin; }のちのち加筆していきます
- 投稿日:2021-03-02T15:41:10+09:00
CSVファイルを扱うのに便利な CSVHelper ver 25.0 の Getting Start
プログラム間でデータのやり取りする時に、まだまだ CSV を使うことが多くあります。そんな時、c# なら CSVHelper が非常に役立ちます。
ただ、バージョンアップのスピードが速く仕様の変更も多いので、現時点での最新版 25.0 でのサンプルを挙げておきます。
参照元は こちら です。入手方法
Visual Studio のパッケージマネージャーコンソールからインストールします。
PM> Install-Package CsvHelper.NET CLI Console の場合は、シェルでプロジェクトパスに移動し、次のコマンドを実行します。
> dotnet add package CsvHelper
CSV ファイルの読み込み
まずは、クラスマッピングを利用した CSV ファイルの読み込み方です。
読み込みサンプル1using System; using System.Globalization; using System.IO; using CSVHelper; namespace test { class Program { static void Main(string[] args) { using (var reader = new StreamReader("file.csv")) { using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture)) { // csv データが行毎に Foo クラスに格納され、IEnumerable<Foo> として // records に割り当てられます。 var records = csv.GetRecords<Foo>(); // records は IEnumerable なので、こんな使い方ができます。 foreach(var i in records) { Console.WriteLine(i.Id); } } } } public class Foo { public int Id { get; set; } public string Name { get; set; } } } }読み込み対象の CSV ファイルの中身は次の通りです。
file.csvId,Name 1,Taro 2,HanakoCSVHelper は、CSV ファイルの先頭行をヘッダとみなし、ヘッダ名から Foo のどのプロパティ名に格納するかを判断します。このため、次のような CSV ファイルでも読み込むことができます。
file2.csvName,Id Taro,1 Hanako,2CSVHelper では、ヘッダ名の大文字小文字を区別します。デフォルトは Pascal Case です。もし、Pascal Case でない場合、たとえば全て小文字の場合は、コンフィグで指定することで読み込みが可能になります。
読み込みサンプル2using System; using System.Globalization; using System.IO; using CSVHelper; namespace test { class Program { static void Main(string[] args) { var config = new CsvConfiguration(CultureInfo.InvariantCulture) { //読み取ったヘッダが小文字に変換されるように ToLower() を仕込みます。 PrepareHeaderForMatch = args => args.Header.ToLower(), }; using (var reader = new StreamReader("path\\to\\file.csv")) { using (var csv = new CsvReader(reader, config)) { // csv データが行毎に Foo クラスに格納され、IEnumerable<Foo> として // records に割り当てられます。 var records = csv.GetRecords<Foo>(); // records は IEnumerable なので、こんな使い方ができます。 foreach(var i in records) { Console.WriteLine(i.Id); } } } } public class Foo { public int Id { get; set; } public string Name { get; set; } } } }config の PrepareHeaderForMatch で ToLower() を仕込んでおくことで、次のような CSV を読み込むことができます。
file3.csvid,name 1,Taro 2,HanakoCSV にヘッダがない場合
CSV ファイルの1行目にヘッダがない場合は、次のように修正します。
configの修正var config = new CsvConfiguration(CultureInfo.InvariantCulture) { HasHeaderRecord = false, };列と格納先クラスのプロパティ名を属性を使って指定する
CSVファイルのヘッダと、格納用クラス(例では Foo)のプロパティ名を一致出来ないケースがあると思います。この場合は、格納用クラスのプロパティに属性を設定して指定することが出来ます。この際、CsvHelper.Configuration.Attributes を using で定義しておいてください。
また、Index で指定する場合、初番は 0 ですのでご注意下さい。Index属性で読み込み順を指定public class Foo { [Index(0)] public int Id { get; set; } [Index(1)] public string Name { get; set; } }Name属性でヘッダ名を指定public class Foo { [Name("id")] public int Id { get; set; } [Name("name")] public string Name { get; set; } }列と格納先クラスのプロパティ名をクラスマップを使って指定する
以前のバージョンからあった方法で、格納先クラスをベースにマップクラスを定義し、読み込み順序を定義します。サンプルでは、Foo に対応する FooMap を定義しています。
なお、ClassMap クラスは CsvHelper.Configuration に定義されていますので、using で宣言しておきます。読み込みサンプル3using System; using System.Globalization; using System.IO; using CSVHelper; using CsvHelper.Configuration; namespace test { class Program { static void Main(string[] args) { using (var reader = new StreamReader("path\\to\\file.csv")) { using (var csv = new CsvReader(reader, config)) { //クラスマップを使って読み込み順序を指定します csv.Context.RegisterClassMap<FooMap>(); // csv データが行毎に Foo クラスに格納され、IEnumerable<Foo> として // records に割り当てられます。 var records = csv.GetRecords<Foo>(); // records は IEnumerable なので、こんな使い方ができます。 foreach(var i in records) { Console.WriteLine(i.Id); } } } } public class Foo { public int Id { get; set; } public string Name { get; set; } } public class FooMap: ClassMap<Foo> { public FooMap() { Map(m => m.Id).Name("id"); Map(m => m.Name).Name("name"); } } } }1行読み込む毎に処理を行いたい
csv.GetRecords() を使うと CSV を丸ごと読み込むことができました。しかし、業務プログラムなんかだと、1行読み込む毎にコンソールに出力したり、チェックをかけたりすることが多いと思います。
この場合は、GetRecords() ではなく GetRecord() を使います。
読み込みサンプル4using System; using System.Globalization; using System.IO; using CSVHelper; namespace test { class Program { static void Main(string[] args) { using (var reader = new StreamReader("path\\to\\file.csv")) { using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture)) { //読み込み開始準備を行います csv.Read(); //ヘッダを読み込みます csv.ReadHeader(); //行毎に読み込みと処理を行います while (csv.Read()) { var record = csv.GetRecord<Foo>(); Console.WriteLine(record.Id); } } } } public class Foo { public int Id { get; set; } public string Name { get; set; } } } }CSV ファイルの出力
次に配列に格納されたデータを CSV ファイルに出力します。データは records にあるとします。records は Foo クラスの配列です。これを CSV ファイルに出力します。
書き込みサンプル1using System; using System.Collections.Generic; using System.Globalization; using System.IO; using CSVHelper; namespace test { class Program { static void Main(string[] args) { var records = new List<Foo> { new Foo { Id = 1, Name = "one" }, new Foo { Id = 2, Name = "two" }, }; using (var writer = new StreamWriter("file.csv")) { using (var csv = new CsvWriter(reader, CultureInfo.InvariantCulture)) { //この1行で保存ができる csv.WriteRecords(records); } } } public class Foo { public int Id { get; set; } public string Name { get; set; } } } }ヘッダを付けずに出力する
ヘッダなしの CSV ファイルを読み込む時と同様に、ヘッダを出力しないように config で指定し、WriterRecords()を実行します。
書き込みサンプル2using System; using System.Collections.Generic; using System.Globalization; using System.IO; using CSVHelper; namespace test { class Program { static void Main(string[] args) { var records = new List<Foo> { new Foo { Id = 1, Name = "one" }, new Foo { Id = 2, Name = "two" }, }; var config = new CsvConfiguration(CultureInfo.InvariantCulture) { //ヘッダを出力しないように指定 HasHeaderRecord = false, }; using (var writer = new StreamWriter("file.csv")) { using (var csv = new CsvWriter(reader, config)) { csv.WriteRecords(records); } } } public class Foo { public int Id { get; set; } public string Name { get; set; } } } }列と格納先クラスのプロパティ名を属性を使って指定する
こちらも上の読み込みの時と同様に、格納先クラス「Foo」に属性を指定します。Index 属性だと番号で指定できます。Name 属性だと、ヘッダ名で指定できます。Index 属性の場合のサンプルは次の通りです。
書き込みサンプル3using System; using System.Collections.Generic; using System.Globalization; using System.IO; using CsvHelper; using CsvHelper.Configuration; using CsvHelper.Configuration.Attributes; namespace test { class Program { static void Main(string[] args) { var records = new List<Foo> { new Foo { Id = 1, Name = "one" }, new Foo { Id = 2, Name = "two" }, }; using (var writer = new StreamWriter("file.csv")) { using (var csv = new CsvWriter(reader, config)) { csv.WriteRecords(records); } } } public class Foo { [Index(0)] public int Id { get; set; } [Index(1)] public string Name { get; set; } } } }列と格納先クラスのプロパティ名をクラスマップを使って指定する
こちらも読み込み時と同様です。ClassMap を使って Mapクラスを定義します。全データを出力するなら Index や Name 属性の方が便利でお勧めです。
読み込みと書き込みで列の位置が異なるなら、読み込み用のクラスマップと書き込み用のクラスマップを用意することで実現できます。書き込みサンプル4using System; using System.Collections.Generic; using System.Globalization; using System.IO; using CsvHelper; using CsvHelper.Configuration; namespace test { class Program { static void Main(string[] args) { var records ; using (var reader = new StreamReader("file.csv")) { using (var csv = new CsvReader(reader, CultureInfo.InvariantCulture)) { csv.Context.RegisterClassMap<ReadFooMap>(); records = csv.GetRecords<Foo>(); } } using (var writer = new StreamWriter("fileout.csv")) { var config = new CsvConfiguration(CultureInfo.InvariantCulture) { csv.Context.RegisterClassMap<WriteFooMap>(); csv.WriteRecords(records); } } } public class Foo { public int Id { get; set; } public string Name { get; set; } } public class ReadFooMap : ClassMap<Foo> { public ReadFooMap () { Map(m => m.Id).Name("id"); Map(m => m.Name).Name("name"); } } public class WriteFooMap : ClassMap<Foo> { public WriteFooMap() { Map(m => m.Name).Name("name"); Map(m => m.Id).Name("id"); } } } }1行出力する毎に処理を行いたい
WriteRecords() だと一気に CSV データを出力できました。行ごとに処理を行う場合は、WriteRecord() を使います。読み込みの場合と同じですね。
書き込みサンプル4using System; using System.Collections.Generic; using System.Globalization; using System.IO; using CsvHelper; using CsvHelper.Configuration; namespace test { class Program { static void Main(string[] args) { var records = new List<Foo> { new Foo { Id = 1, Name = "one" }, new Foo { Id = 2, Name = "two" }, }; using (var writer = new StreamWriter("file.csv")) { var config = new CsvConfiguration(CultureInfo.InvariantCulture) { csv.WriteHeader<Foo>(); csv.NextRecord(); foreach (var record in records) { csv.WriteRecord(record); csv.NextRecord(); } } } } public class Foo { public int Id { get; set; } public string Name { get; set; } } } }ここまでが Getting Start
CSVHelper の Getting Start はここまでです。
2種類の列レイアウトのある複雑な CSV を読んだり、データを bool 値として読み込むなど、様々な機能があります。また、読み込んだデータを配列ではなく、DataTable に格納する事もできます。DataTable に格納できれば、そのまま BulkCopy を使って SQL Server のに高速にインポートできます。
興味がわいた方は、下のリンクをたどってください。
- 投稿日:2021-03-02T14:21:48+09:00
Wallpaper Engineみたいに壁紙をUnityで作る
はじめに
今回は動く壁紙として有名なWallpapaer Engineのように、Unityで作ったプロジェクトを壁紙として表示する方法を書きます。
仕組み
壁紙のウィンドウハンドルを取得し、Unityのプロジェクトをそのウィンドウの子ウィンドウとして表示するだけです。
壁紙のウィンドウハンドル
c++で取得します。この記事ではc++でdllを作り、c#で呼び出します。
BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam) { HWND p = FindWindowEx(hwnd, NULL, L"SHELLDLL_DefView", NULL); HWND* ret = (HWND*)lParam; if (p) { *ret = FindWindowEx(NULL, hwnd, L"WorkerW", NULL); } return true; } HWND get_wallpaper_window() { HWND progman = FindWindow(L"ProgMan", NULL); SendMessageTimeout(progman, 0x052C, 0, 0, SMTO_NORMAL, 1000, nullptr); HWND wallpaper_hwnd = nullptr; EnumWindows(EnumWindowsProc, (LPARAM)&wallpaper_hwnd); return wallpaper_hwnd; }Unityを表示する
[DllImport("WallpaperDLL.dll")] //c++で作ったdllを呼び出す static extern IntPtr GetWallpaperHWND(); /*------------------------------------------*/ var path = ユニティーのexeのパス; var cmdline = $"-parentHWND {GetWallpaperHWND()}";//子ウィンドウとして起動 Process exe = Process.Start(path, cmdline);完成
二つ作ってみました。結構いい感じにできてよかったです。
ソース
DLL
最後に
結構簡単なのでぜひ試してみてください
参考
https://yotiky.hatenablog.com/entry/unity_uaal-wpf
https://stackoverflow.com/questions/56132584/draw-on-windows-10-wallpaper-in-c
- 投稿日:2021-03-02T14:21:48+09:00
Wallpaper Engineみたいな壁紙をUnityで作る
はじめに
今回は動く壁紙として有名なWallpapaer Engineのように、Unityで作ったプロジェクトを壁紙として表示する方法を書きます。
仕組み
壁紙のウィンドウハンドルを取得し、Unityのプロジェクトをそのウィンドウの子ウィンドウとして表示するだけです。
壁紙のウィンドウハンドル
c++で取得します。この記事ではc++でdllを作り、c#で呼び出します。
BOOL CALLBACK EnumWindowsProc(HWND hwnd, LPARAM lParam) { HWND p = FindWindowEx(hwnd, NULL, L"SHELLDLL_DefView", NULL); HWND* ret = (HWND*)lParam; if (p) { *ret = FindWindowEx(NULL, hwnd, L"WorkerW", NULL); } return true; } HWND get_wallpaper_window() { HWND progman = FindWindow(L"ProgMan", NULL); SendMessageTimeout(progman, 0x052C, 0, 0, SMTO_NORMAL, 1000, nullptr); HWND wallpaper_hwnd = nullptr; EnumWindows(EnumWindowsProc, (LPARAM)&wallpaper_hwnd); return wallpaper_hwnd; }Unityを表示する
[DllImport("WallpaperDLL.dll")] //c++で作ったdllを呼び出す static extern IntPtr GetWallpaperHWND(); /*------------------------------------------*/ var path = ユニティーのexeのパス; var cmdline = $"-parentHWND {GetWallpaperHWND()}";//子ウィンドウとして起動 Process exe = Process.Start(path, cmdline);完成
二つ作ってみました。結構いい感じにできてよかったです。
ソース
DLL
最後に
結構簡単なのでぜひ試してみてください
参考
https://yotiky.hatenablog.com/entry/unity_uaal-wpf
https://stackoverflow.com/questions/56132584/draw-on-windows-10-wallpaper-in-c
- 投稿日:2021-03-02T13:50:25+09:00
【C#】Mutexクラスを使ったプログラムの排他制御
はじめに
アプリケーションの多重起動を阻止したい場合はMutexクラスを使用すれば良い。
Mutexクラスを使うことで簡単に排他処理を作ることができる。サンプルプログラム
Program.csusing System; using System.Diagnostics; class Program { private static System.Threading.Mutex _Mutex; static void Main(string[] args) { if(MutexTest()) { Console.WriteLine("True 多重起動です。"); } else { Console.WriteLine("false 単体起動です。"); } Console.ReadKey(); } static bool MutexTest() { //ミューテックスクラスのインスタンス生成 _Mutex = new System.Threading.Mutex(false,"SubProcess"); //Mutexの所有権を要求 if (_Mutex.WaitOne(0, false) == false) return true; //プロセスを取得 string AppName = Process.GetCurrentProcess().MainModule.FileName; var ps = Process.GetProcessesByName(AppName); bool ProcessFlg = false; foreach (var item in ps) { ProcessFlg = true; break; } //起動済ならreturn if (ProcessFlg) return true; return false; } }出力結果(単体でプログラムを起動の場合)
false 単体起動です。出力結果(同プログラムを複数起動の場合、2つ目以降)
True 多重起動です。ちなみに、プログラム上でMutexクラスを解放する際はReleaseMutexメソッドを使用する。
_Mutex.ReleaseMutex();
- 投稿日:2021-03-02T10:36:45+09:00
[C#]await利用時の同期コンテキストと実行スレッドの動きについてコードを動かして見ていく
private async Task MethodAsync() { Print($"1:Before await. Thread Id: {Thread.CurrentThread.ManagedThreadId}"); await Task.Run(() => Print($"2:In task run. Thread Id: {Thread.CurrentThread.ManagedThreadId}")); Print($"3:After await. Thread Id: {Thread.CurrentThread.ManagedThreadId}"); }いきなりですが問題です。
上のコードには3つのPrint出力があり、それぞれの出力処理が行われる実行スレッドを出力します。
ではこのコードを実行した場合、それぞれの出力の実行スレッドはどうなるでしょうか?①1~3全て同じスレッドになる
②1~3全て別のスレッドになる
③1,2が同じスレッドで、3だけ別スレッド
④1,3が同じスレッドで、2だけ別スレッドただし、WPFアプリケーションでの実行+このメソッドはメインスレッドから呼ばれるとする。
この記事を読むと、この問題の答えは何か?なぜそうなるのか?が理解できます。
はじめに
C#の非同期処理について調べだすと、「同期コンテキストが存在する場合、awaitは処理後に自動でスレッドを戻してくれる」、という説明をよく見ます。
自分はその説明を読んだだけではしっかり意味がわからなかったので、実際にコードを書いてみてその辺りの動きを確認しようと思いました。
その確認した内容を、自分の備忘録、兼、他の人の理解の助けにもなるだろうと思いまとめたのがこの記事です。
またawait利用時の同期コンテキストがわかれば、副産物として
Task.Wait
,Task.Result
の利用が推奨されていない理由も理解しやすくるなるので、そちらについても書いています。想定してる読者
- C#の基本文法はわかっている
- async/await, Taskの存在ぐらいは知っている
- 記事の冒頭にある問題がわからなかった
環境
記事の中ででてくるコードはWPFで動かしています。
ただWindowsFormやUnityなどの"C#+GUI"のものなら理屈はほぼ同じなはずなので、その辺を普段使っている人にも読んでもらえると思います。※CUIアプリケーションでは、この記事に書いてある内容が一部通じないので注意してください。
- WPF (C#のGUIフレームワーク)
- .NET Core 3.1
同期コンテキストとは何か?
「同期コンテキスト ≒ 複数スレッドに跨る処理を安全に行うための仕組み」です。
非同期処理は、うっかりすると、デッドロックや再現性の低いバグを起こしてしまいます。
このようなバグはほとんどがマルチスレッドの処理が原因で起きるものです。
同期コンテキストは、スレッド間の処理の受け渡しなどを上手く管理してくれる仕組みです。つまり、同期コンテキストというのは、マルチスレッド処理が原因の不具合を避けC#の非同期処理を安全に使いやすくしてくれるための仕組みです。
C#では同期コンテキストの仕組みを扱うために
System.Threading.SynchronizationContext
クラスとうものが用意されています。実は、await
を利用している裏ではこのSynchronizationContext
が良い感じにスレッド間の動きを調整してくれているのです。awaitと同期コンテキスト
await
を使ったときの同期コンテキストがどうなっているか見ていきます。下のコードは、単純に
await
を使った場合について実行スレッドがどうなっているかを確かめるもので、記事の冒頭に書いた問題に、メソッドを呼び出すButton_Clickがついただけのコードです。
(画面上にButtonが一つ置いており、それを押すとButton_Clickが呼ばれるという状況です)private async void Button_Click(object sender, RoutedEventArgs e) { Print($"Button click. Thread Id: {Thread.CurrentThread.ManagedThreadId}"); await MethodAsync(); } private async Task MethodAsync() { Print($"Before await. Thread Id: {Thread.CurrentThread.ManagedThreadId}"); await Task.Run(() => Print($"In task run. Thread Id: {Thread.CurrentThread.ManagedThreadId}")); Print($"After await. Thread Id: {Thread.CurrentThread.ManagedThreadId}"); }実行結果Button click. Thread Id: 1 Before await. Thread Id: 1 In task run. Thread Id: 5 After await. Thread Id: 1
await
しているTask.Run()
の中では、スレッドIdが異なっています。逆にawait
の前後では、スレッドIdが一致しています。
(なので、記事冒頭の問題の答えは「④1,3が同じスレッドで、2だけ別スレッド」でした。)つまり、awaitには、「awaitする前後で実行スレッドを保存しておく機能」があります。
もっと正確にいうと、await
には「System.Threading.SynchronizationContext.Current
がnullでない場合に、await
の前後で実行スレッドを自動で保存してくれる機能」があります。詳しくは後述しますが、WPFなどではメインスレッドに対して自動で
SynchronizationContext.Current
がセットされています。そして上のコードではメインスレッド上でawait Task.Run()
が実行されているため、その後にスレッドが元に戻っています。自動でセットされている
SynchronizationContext.Current
を、意図的にnull
にした場合も見ておきましょう。SynchronizationContext.Currentがnullの場合private async void Button_Click(object sender, RoutedEventArgs e) { Print($"ButtonClick. Thread Id: {Thread.CurrentThread.ManagedThreadId}"); await MethodAsync(); } private async Task MethodAsync() { Print($"Before await. Thread Id: {Thread.CurrentThread.ManagedThreadId}"); // SynchronizationContext.Currentをnullに設定する。ここではメインスレッドに対して設定している。 SynchronizationContext.SetSynchronizationContext(null); await Task.Run(() => Print($"In task run. Thread Id: {Thread.CurrentThread.ManagedThreadId}")); Print($"After await. Thread Id: {Thread.CurrentThread.ManagedThreadId}"); }実行結果ButtonClick. Thread Id: 1 Before await. Thread Id: 1 In task run. Thread Id: 5 After await. Thread Id: 5
await
後にスレッドが元に戻らずTask.Run()
の中と同じスレッドIdとなっています。
これは、await
する前のスレッド(ここではメインスレッド)で、SynchronizationContext.Current
にnull
が代入されたためです。
※通常このような処理をする事はないと思います。ここでは説明のため、わざとnull
を代入しています。awaitにはその前後で実行スレッドを自動で保存してくれる機能があり、それは"SynchronizationContext.Current != null"の場合のみ働く。
この辺りの働きをさらに詳しく知りたければ、以下のページなどが参考になると思います。
そもそもSynchronizationContext.Currentはどこでセットされているのか
上でWPFなどではメインスレッドに対して自動で同期コンテキスト(
SynchronizationContext.Current
)が設定されていると書きました。
では、メインスレッドの同期コンテキストはどこで設定されているのでしょうか?WPFの場合は、
System.Windows.Threading.DispatcherSynchronizationContext
(System.Threading.SynchronizationContext
クラスを継承)が、メインスレッドのSynchronizationContext.Current
として自動で設定されています。WindowsFormやUnityの場合もそれぞれ適当なクラスがメインスレッドの同期コンテキストとして自動で設定されています。そのため、メインスレッドで
await
を使った場合には、自動でスレッドが元に戻るようになっています。CUIとGUIだと何故挙動が変わるか
環境の項目で、次のように書きました。
※CUIアプリケーションでは、この記事に書いてある内容が一部通じないので注意してください。
これは、上述したようにGUIアプリケーションではメインスレッド(UIスレッド)に対する同期コンテキストが自動で設定されているのに対し、CUIアプリケーションではそれが行われていないためです。
逆に言えば、根本的なawait
の働き自体にはGUIとCUIで差がありません。
CUIでも同期コンテキストが設定されていれば、await
後にスレッドが元に戻ります。await自体には、別スレッドに切り替える働きがないことに注意
ここまで見てきたコードで一つ勘違いしやすいポイントがあります。
それは、awaitには別スレッドに切り替える働きはないということです。上のコード例では、
await
している部分でスレッドIdが変化していますが、これはawait
の働きではなくTask.Run()
の働きによるものです。Task.Run()は、その引き数に与えられたデリゲートをスレッドプール上で実行します。この働きによって実行スレッドが変わっています。上のコードもよく見てもらうと、Button_Clickから
await
をつけてMethodAsyncが実行されていますが、Button_Click内と、MethodAsyncのawait
前の部分で実行スレッドが変化していません。スレッドプールについても一応簡単に説明しておきます。
スレッドは必要になったときにその都度新しくつくるより、最初にいくつかつくっておきそれを使いまわす方が効率が良いです。その使いまわしの仕組みがスレッドプールです。
C#にはTask.Run
などスレッドプールを簡単に使うために提供されている仕組みがあるので、基本的にスレッドプールも自分で直接操作する必要はなく、それらを使えば大丈夫です。
スレッドプールの働きなどについてもう少し知りたければ以下の記事がわかりやすいです。awaitと同期コンテキストの働きのまとめ
単純に
await
を利用した場合の同期コンテキストの動きについてまとめておきます。
await
には、その前後で実行スレッドを保存してくれる働きがある
- 正確には実行スレッドではなく同期コンテキストを保存している
- この働きは、
System.Threading.SynchronizationContext.Current
がnull
でない場合にのみ働く- WPFなどではメインスレッドに対して自動で実行コンテキスト(
SynchronizationContext.Current
)が設定されているawait
しただけでは、その処理の実行スレッドは変わらない
- 実行スレッドが変化するのは
Task.Run()
などの機能Task.Wait, Task.Resultが推奨でない理由
C#の非同期処理について調べると、「
Task.Wait
,Task.Result
は使ってはいけない」という記述がよく見られます。この理由は、await
を使う際の同期コンテキストの働きを知っていれば、容易に理解できます。
Task.Wait
,Task.Result
が推奨でない理由は、デッドロックが簡単に起きてしまうからです。
まず、Task.Wait
を使ってデッドロックが起きてしまう例を見てみます。
例の中で行っているのは、
- TaskをWaitすること
- WaitされているTaskの中ではawait Task.Run()が使うこと
だけです。
Task.Waitでデッドロックするパターンprivate void Button_Click(object sender, RoutedEventArgs e) { MethodWait(); } // awaitを使っていないのでasync不要。 private void MethodWait() { Print($"Before await. Thread Id: {Thread.CurrentThread.ManagedThreadId}"); var task = MethodAsync(); // 結果待ちをするためにスレッドをロックして他のスレッドから触れなくしてしまう。 task.Wait(); Print($"After wait. Thread Id: {Thread.CurrentThread.ManagedThreadId}"); } private async Task MethodAsync() { // 処理終了後に元のスレッドに戻そうとするが、元のスレッドがTask.Wait()によりロックされており戻せない。 await Task.Run(() => Print($"In task run. Thread Id: {Thread.CurrentThread.ManagedThreadId}")); }実行結果Before await. Thread Id: 1 In task run.Thread Id: 4 // (After wait.~は出力されずUIもフリーズする)MethodAsync()内の処理まで実行された後、task.Wait()の部分で処理がフリーズしてしまっています。
これは、以下のようにお互いの処理待ちをしてしまうからです。
- task.Wait側は、タスクの完了までスレッドをロックする
- await Task.Run()~側は、処理終了し元のスレッドに戻して完了させたいが、元のスレッドがロックされているので戻せず完了できない
Task.Run()
の働きにより別スレッドで実行されている。await
はその後スレッドを元に戻そうとする。- 元のスレッドに戻すとこまでやって、このTask(MethodAsync()の戻り値)は完了となる
このように、awaitが自動で行ってくれている同期コンテキストの保存と、Task.Wait(Task.Result)によるスレッドのロックが組み合わさることによりデッドロック起きてしまいます。
そのため、await
かTask.Wait
のどちらかの利用を避けたいところです。
ここで、await
は有用な仕組みのため、Task.Wait
(Task.Result
)の利用を抑えます。
これが、Task.Wait
,Task.Result
の利用が推奨されない理由です。(Task.Resultがやっていることは、Task.Wait+結果取り出しです。つまり、デッドロックが起きる理由はTask.Waitと同様なため、ここでは解説を省略します。)
呼び出し元から非同期化することによってWaitを使わない。
Task.Waitは利用したくないと書きましたが、その一番シンプルな代替方法はawaitを使うことです。
Task.Waitではなくawaitを使うprivate async void Button_Click(object sender, RoutedEventArgs e) { await MethodAwaitAsync(); } private async Task MethodAwaitAsync() { Print($"Before await. Thread Id: {Thread.CurrentThread.ManagedThreadId}"); var task = MethodAsync(); await task; Print($"After wait. Thread Id: {Thread.CurrentThread.ManagedThreadId}"); } private async Task MethodAsync() { await Task.Run(() => Print($"In task run. Thread Id: {Thread.CurrentThread.ManagedThreadId}")); }実行結果Before await. Thread Id: 1 In task run.Thread Id: 4 After wait. Thread Id: 1きちんとフリーズすることなく実行されました。
Task.Wait
の代わりにawait
で処理完了を待っています。また、await
を使うためにメソッドにasync
キーワードがついています。(async
を使う場合は戻り値をvoid
ではなくTask
あるいはTask<TResult>
としましょう。その唯一の例外は、上コードのButton_ClickのようにUIイベントにデリゲートを登録する場合だけです。)この例のように、
Task.Wait
ではなくawait
を使うようにしていくと、呼び出し元のメソッドもasync/await
を使う必要があり、自然と一連の処理が全て非同期メソッドとなっていきます。
「呼び出し元まで全て非同期メソッドにしていってよいのか?」と迷うかもしれませんが、呼び出し元から非同期コードで統一することはMicrosoftのベストプラクティスでも推奨されています。ぜひやりましょう。Waitを使いたいならConfigureAwaitを使う
また、
await
後に元のスレッドに戻そうとすることでデッドロックが起きるなら、その働きをなくすことでもデッドロックを防ぐことができます。
やり方は、await
しているTask
の後ろにConfigureAwait(false)
をつけるだけです。ConfigureAwait(false)を使ってデッドロックを防ぐprivate void Button_Click(object sender, RoutedEventArgs e) { MethodWait(); } // awaitを使っていないのでasync不要。 private void MethodWait() { Print($"Before await. Thread Id: {Thread.CurrentThread.ManagedThreadId}"); var task = MethodAsync(); // 結果待ちをするためにスレッドをロックして他のスレッドから触れなくしてしまう。 task.Wait(); Print($"After wait. Thread Id: {Thread.CurrentThread.ManagedThreadId}"); } private async Task MethodAsync() { await Task.Run(() => Print($"In task run. Thread Id: {Thread.CurrentThread.ManagedThreadId}")) .ConfigureAwait(false); // ConfigureAwait(false)の後なのでメインスレッドに戻らず実行される。 Print($"After configure await. Thread Id: {Thread.CurrentThread.ManagedThreadId}"); }実行結果Before await. Thread Id: 1 In task run.Thread Id: 4 After configure await. Thread Id: 4 After wait. Thread Id: 1詳しくは下で説明しますが、
ConfigureAwait(false)
を利用することで、await
後に元のスレッドに戻る働きをなくし、デッドロックを防ぐことができます。
(元のスレッドに戻らないのはConfigureAwait(false)
を使ったasync
メソッド内だけです。)基本的には上で紹介したように、そもそもTask.Wait(Task.Result)を使わずに全てawaitに置き換えることが理想です。
しかし、ライブラリ作成者と利用者が一致しない場合など、利用者側にそれを徹底できない場合があります。
そのため、同期コンテキストを保持する必要がない場合はConfigureAwait(false)をつけ、コンテキストに依存しないコードを書くようにしておくのが無難です。(特に誰が利用するかわからないコードを書く時は)Microsoftのベストプラクティスでも以下のように書かれています。
... 可能な場合は常に ConfigureAwait を使用すべきであるということになります。コンテキストに依存しないコードは、GUI アプリケーションのパフォーマンスを向上し、部分的に非同期のコードベースに取り組む際のデッドロックを回避するのに役立ちます。この指針の例外は、コンテキストが必要なメソッドです。
awaitで同期コンテキストを保持しない
上で、
Task.Wait
とawait
の併用によるデッドロックを避けるため、ConfigureAwait(false)
オプションを使うと書きました。
これは、オプションをつけることにより、同期コンテキストを意図的に保持しないという選択になります。
この章では、その「同期コンテキストを保持しない」というところについて、もう少し掘り下げていきます。※説明する人によっては、同期コンテキストを「保持しない」ではなく「キャプチャしない」、「拾わない」と表現している場合もありますが、同じ意味です。
ConfigureAwait(false)
まずは、先ほどもでてきた
ConfigureAwait(false)
についてです。
凄くシンプルにConfigureAwait(false)
を使った場合を見てみます。ConfigureAwait(false)をつけると実行コンテキストを拾わないprivate async void Button_Click(object sender, RoutedEventArgs e) { await MethodAsync(); } private async Task MethodAsync() { Print($"Before await. Thread Id: {Thread.CurrentThread.ManagedThreadId}"); await Task.Run(() => Print($"In task run. Thread Id: {Thread.CurrentThread.ManagedThreadId}")).ConfigureAwait(false); Print($"After await. Thread Id: {Thread.CurrentThread.ManagedThreadId}"); }実行結果Before await. Thread Id: 1 In task run.Thread Id: 5 After await. Thread Id: 5
await
しているTask
にConfigureAwait(false)
オプションをつけることで、await
後に実行スレッドがメインスレッドに戻らなくなっています。
このように、awaitするTaskにConfigureAwait(false)オプションをつけると実行コンテキストが保持されなくなります。
(実行コンテキストを拾わない、キャプチャしないと言ったりもします。)先ほども紹介したように、デッドロックなどを防ぐためには、このConfigureAwait(false)などを使って、実行コンテキストを保持しないことがとても重要になってきます。C#で非同期処理をするならぜひ覚えておきましょう。
ConfigureAwaitの注意事項
同期コンテキストを保持しなくなる
ConfigureAwait(false)
オプションですが、一点気を付けたい部分があります。
それは、あるasyncメソッド内で一度ConfigureAwait(false)
を使うと、もとの同期コンテキストを復活できないことです。
コード例で見てみましょう。ConfigureAwait(false)で同期コンテキストを捨てると復活できないprivate async void Button_Click(object sender, RoutedEventArgs e) { await MethodAsync(); } private async Task MethodAsync() { Print($"Before await. Thread Id: {Thread.CurrentThread.ManagedThreadId}"); await Task.Run(() => Print($"In task run1. Thread Id: {Thread.CurrentThread.ManagedThreadId}")).ConfigureAwait(false); Print($"After await1. Thread Id: {Thread.CurrentThread.ManagedThreadId}"); await Task.Run(() => Print($"In task run2. Thread Id: {Thread.CurrentThread.ManagedThreadId}")).ConfigureAwait(true); Print($"After await2. Thread Id: {Thread.CurrentThread.ManagedThreadId}"); }実行結果Before await. Thread Id: 1 In task run1.Thread Id: 4 After await1. Thread Id: 4 In task run2.Thread Id: 5 After await2. Thread Id: 5一つめのawait Task.Run()では
ConfigureAwait(false)
を使っているため、"After await1"のスレッドIdがメインスレッドと異なっているのは先ほどまでと同じです。
ただ、二つめのawait Task.Run()ではConfigureAwait(true)
としているのに、await後の処理がメインスレッドに戻っていません。asyncメソッド内に
await
が複数回ある場合、途中で同期コンテキストを捨ててしまって大丈夫か注意してください。ContinueWith
上で
ConfigureAwait(false)
を使うことによって同期コンテキストを保持しない方法について紹介しましたが、ContinueWith
オプションでも似たようなことができます。ContinueWithをオプションなしで使うprivate async void Button_Click(object sender, RoutedEventArgs e) { await MethodAsync(); } private async Task MethodAsync() { Print($"Before await. Thread Id: {Thread.CurrentThread.ManagedThreadId}"); await Task.Run(() => Print($"In task run. Thread Id: {Thread.CurrentThread.ManagedThreadId}")) .ContinueWith(_ => { Print($"Continue. Thread Id: {Thread.CurrentThread.ManagedThreadId}"); }, TaskScheduler.Default ); }実行結果Before await. Thread Id: 1 In task run.Thread Id: 5 Continue.Thread Id: 10
ContinueWith
の第二引数にTaskScheduler.Default
を渡すと、第一引き数で渡したデリゲートがスレッドプール上で実行されます。(ContinueWith
の第二引数を省略した場合でも同じようにスレッドプール上で実行されます。TaskScheduler.Default
が規定値なため)
ConfigureAwait(false)
と違って同期コンテキストを捨てているわけではないですが、このようにして、await
前のメインスレッドとは違うスレッドで実行することもできます。逆に
ContinueWith
を使って、await
前のスレッドに明示的に処理を戻すこともできます。ContinueWithをつかってawait前のスレッドに戻すprivate async void Button_Click(object sender, RoutedEventArgs e) { await MethodAsync(); } private async Task MethodAsync() { Print($"Before await. Thread Id: {Thread.CurrentThread.ManagedThreadId}"); var currentSynchronizationContextScheduler = TaskScheduler.FromCurrentSynchronizationContext(); await Task.Run(() => Print($"In task run. Thread Id: {Thread.CurrentThread.ManagedThreadId}")) .ContinueWith(_ => { Print($"Continue. Thread Id: {Thread.CurrentThread.ManagedThreadId}"); }, currentSynchronizationContextScheduler ); }実行結果Before await. Thread Id: 1 In task run.Thread Id: 4 Continue.Thread Id: 1
await
前に現在の同期コンテキストからTaskScheduler
をつくっておきContinueWith
の引数として渡すことで、await
前のスレッドで実行できていますね。
ContinueWith
では、このようにTaskScheduler
を使った柔軟なスレッドコントロールや、TaskContinuationOptions
を使った処理フローのコントロールが可能です。
同期コンテキストを捨てるだけならConfigureAwait(false)
で十分ですが、複雑な非同期処理フローが必要になる場合にはこちらを使うと良いでしょう。
- System.Threading.Tasks.Task.ContinueWith
- System.Threading.Tasks.TaskScheduler
- System.Threading.Tasks.TaskContinuationOptions
おわりに、まとめ
この記事では、
await
を使う際の同期コンテキストの働きについて、実際のコード例とともにまとめました。記事冒頭の問題の答えは、「④1,3が同じスレッドで、2だけ別スレッド」です。
その理由は、以下です。
- WPFではメインスレッド(UIスレッド)に対して同期コンテキスト(
SynchronizaitonContext.Current
)が自動で設定される- 同期コンテキストが設定されているスレッドで
await
を使った場合、await
以後の処理に戻る際に同期コンテキストを保持する(≒実行スレッドを元に戻してくれる)- Task.Run()での処理はスレッドプール上で行われる(なので2は別スレッドで出力される)
また、GUIアプリケーションで
Task.Wait
,Task.Result
の利用が推奨されない理由は以下です。
- 「
Task.Wait
(Task.Result
)のTaskの処理が完了するまでスレッドをロックする働き」と「await
の処理終了後に実行スレッドを元に戻して処理を完了させる働き」がぶつかり、デッドロックを起こすため参考文献
この記事は、await利用時の同期コンテキストの働きという点に注目してまとめました。
非同期処理をさらに理解して使いこなすには、複数の人の解説を読んで、色んな角度から見てみるのが良いと思いますので、自分が参考にさせてもらった記事のリンクをいくつか貼らせてもらいます。
- Taskを極めろ!async/await完全攻略
- Taskクラスを中心に非同期処理について解説された記事
- 非同期詳しくない人でもめちゃくちゃわかりやすい
- ufcpp
- 排他制御とか含めた非同期処理全般についてまとまってる。
- 同期コンテキストをテーマに書かれている部分はこちら
- kekyoの丼-できる!C#で非同期処理(Taskとasync-await)
- C#の非同期処理入門記事。
- 使い始めの人がやりがちな危ない処理について、"なぜ使っては駄目か"がわかりやすくまとまってる。
- 非同期処理を学びだす早い段階で呼んでおくとよさげ。
- kekyoの丼-.NET非同期処理(async-await)を制御する、様々な方法
- より実践的な非同期処理の使い方の話
- kekyoの丼-“パターンでわかる! .NET Coreの非同期処理” と楽屋ネタ
- より実践的な非同期処理の使い方の話②
- 動画とスライド
- C# 非同期、覚え書き。
- C#の非同期処理で使うクラスやメソッドがよくまとまっている
- 非同期処理の基本的なことがわかってから読むとよさげ
- Microsoftドキュメント-Async/Await非同期プログラミングのベスト プラクティス
- 投稿日:2021-03-02T09:59:40+09:00
簡単なタスクをAzure static web appsに載せてみる(HTML,C#)
前提
C#で書いた(ほぼ)スクリプトを特定の人がオンラインでできるようにWeb化したい。
ひとまず簡単にAzure FunctionとHTMLがあればいいか、と思ったらAzureにちょうどいいサービスができていたので、
それを使って構築してみる。Static Web Apps
https://docs.microsoft.com/ja-jp/azure/static-web-apps/overview
まずはこのへん見ましょう。
フロントエンドはJSとかシンプルなHTMLとか、バックAPIはFunction使われる。
ただしリポジトリはGithub限定で個人的にちょっと不便(Azureのレポはあかんのかい…)シンプルページをクローン
https://github.com/staticwebdev/vanilla-basic
このあたりからcloneする。
ただこのままだとルーティングがないので、routes.json
を作成する必要がある。ルート定義(routes.json)
https://docs.microsoft.com/ja-jp/azure/static-web-apps/routes
このへんなんだが、シンプルHTMLだと一覧に入ってなかったり。https://docs.microsoft.com/ja-jp/azure/static-web-apps/front-end-frameworks
この一覧参照シンプルHTMLならルートフォルダにそのまま入れる。
routes.json
の書き方はroutes 配列に出現する順序で実行されます。
これを読んでないとマニュアルのスニペットをそのまま貼っつけてハマった。。
routes.json{ "routes": [ { "route": "/profile", "allowedRoles": ["authenticated"] }, { "route": "/admin/*", "allowedRoles": ["administrator"] } ] }↑ちゃんと書くといわゆるこういう感じになる。(一要素だけ書いちゃってて動かなかった)
認証つける
特定の人だけに見せるページなら別途roleを割り当てる。
authenticated
のロールだとどんなアカウントでもひとまずログインした状態であれば見えてしまうので、
ちゃんとロール管理→招待、から対象のメールアドレスを入力しておく。(割り当てるroleはadminなどにすればいいかな)
※ここでロールのことが出てくる、routes.json
や認証の説明だけ読んでもわからんかった…APIを追加する
https://docs.microsoft.com/ja-jp/azure/static-web-apps/add-api
大体ドキュメントの通りなんだが、例ではnodeでC#だとどうするのかわかりにくい。
C#はいらない子?
(というかVSCodeの操作前提ってなんだよ…ともなった)けどちゃんとできるので、くじけずにやる。
APIフォルダにAzureFunction追加
api
フォルダを作成して、AzureFunctionプロジェクトをそこに作成する。(VSでもRiderでも大丈夫)
そいでAPIのコード書く(もちろんなんでもOK)
で、ちゃんと動くことを確認しておく。ただ、このままだと
function.json
がない。
自分で作るの?けど例も無いのにどうやって??
と思ったら、AzureFunctionの方のドキュメントに
https://docs.microsoft.com/ja-jp/visualstudio/mac/azure-functions-lab?view=vsmac-2019#exercise-4-working-with-functionjson
とあった。どうやらビルドした時に作成されているようだ。
それをルートの場所に持ってこよう。Github Actionは動くがビルドされない
これがまぁ困った。
最初はcsファイルと各jsonファイルしかコミットしてなかったが、それだとビルドができないらしい。
全然API動かん…と思ってgithubActionのログをみたら(詳細にログ見れます。動かないときは要チェック)
Error: Could not detect the language from repo.
と出ていて、ビルドも何もできていない様子だった。
https://docs.microsoft.com/ja-jp/azure/app-service/deploy-local-git
このへん見て解決。
ASP.NET Core : *.sln または *.csproj
という様に、csprojファイルも追加するとちゃんとビルドできた。所感
簡単にAPI付きのWebページを作れるのは便利。
認証もつけられるし。(BASIC認証もパッとできれば尚良)
ただAzure全体に言えることなんだが、ドキュメントがちょっとかゆいところに手が届かない感あり。
- 投稿日:2021-03-02T03:39:14+09:00
例のじゃんけん
はじめに
if
を使わずにじゃんけんします。
じゃんけんの結果はC#8の機能の switch式 を使って受け取ります。先駆者様
個人ブログとのクロスポストです。
勝敗判定
じゃんけんのルールとして、
->
の右側が強いものとすると、Rock
->Paper
->Scissors
->Rock
の関係性が成り立ち、3つの手を周期として勝敗が決まります。
ここで、Rock
を0、Paper
を1、Scissors
を2として、関係性をそれぞれ1の距離を持った有効グラフだとすると、自分の手と相手の手の距離が1では負け、距離が2では勝ち、距離が0(3)ではあいこのような表現することができます。このことから、
相手の手 - 自分の手
を計算することで距離を求めることができます。
しかし、自分の手が2で、相手の手が0だった場合、0 - 2 == -2
のように、距離が負数になってしまいます。このとき、じゃんけんは3つの手を周期としているため、自分の手や相手の手を3つ移動させたところで手は変わらず、勝敗は変わりません。つまり、距離に3を足したり、3で剰余を取ったとしても勝敗は変わりません。このことから、(相手の手 - 自分の手 + 3) % 3
とすることで、距離を0、1、2の3つにすることができ、負数と3以上になることを避けることができます。実装
Rps
(じゃんけん) クラスにHand
(グー、チョキ、パー) とResult
(引き分け、負け、勝ち) のenum
を定義することで、Battle
関数の引数と戻り値にそれぞれ意味を持たせます。C#の
enum
は、既定値としてint
の0をとり、要素が増えるにつれて1増えます。そのため、この場合は、Rock (0)
、Paper (1)
、Scissors (2)
のようにそれぞれ値を持つことができます。
Battle
関数は、自分の手と相手の手を渡すことで、Result.Draw
、Result.Lose
、Result.Win
のいずれかを返します。public static class Rps { public enum Hand { Rock, Paper, Scissors } public enum Result { Draw, Lose, Win } public static Result Battle(Hand own, Hand opponent) { var result = ((int)opponent - (int)own + 3) % 3; return (Result)result; } }呼び出し側では、例に倣ってそれぞれの絵文字を割り当てます。
switch式
を使うことで、Battle
が返す可能性のあるパターンに、あいこ
、あなたの負け
、あなたの勝ち
を割り当て、一致した要素をresult
として受け、自分の手、相手の手と一緒に表示します。_ => throw new ArgumentOutOfRangeException()
では、対象となる値以外の値として判定された際に該当するアームになります。public static class Program { public static void Main() { const string draw = "あいこ"; const string lose = "あなたの負け"; const string win = "あなたの勝ち"; var map = new Dictionary<Rps.Hand, string> { [Rps.Hand.Rock] = "✊", [Rps.Hand.Paper] = "?", [Rps.Hand.Scissors] = "✌" }; // var hands = new[] { Rps.Hand.Rock, Rps.Hand.Paper, Rps.Hand.Scissors }; var hands = Enum.GetValues(typeof(Rps.Hand)).Cast<Rps.Hand>().ToArray(); foreach (var own in hands) foreach (var opponent in hands) { // Draw, Lose, Winのままなら // var result = Rps.Battle(own, opponent).ToString(); var result = Rps.Battle(own, opponent) switch { Rps.Result.Draw => draw, Rps.Result.Lose => lose, Rps.Result.Win => win, _ => throw new ArgumentOutOfRangeException() }; Console.WriteLine($"{map[own]} vs {map[opponent]}: {result}"); } } }実行結果
- 投稿日:2021-03-02T00:14:56+09:00
VisualStudioでプロジェクトを追加する方法
はじめに
VisualStudioで作成済みのプロジェクトを追加し、エラーにならないための備忘録。
以下の順番で行う
①作成済みのプロジェクトを追加する
②アセンブリを参照する
③.Netのバージョンを一致させる①作成済みのプロジェクトを追加する
ソリューションファイルを右クリック > 追加 > 既存のプロジェクト を選択
②アセンブリを参照する
参照元のプロジェクトの 参照 を右クリックし、参照の追加 を選択
基本はこれで他のプロジェクトが参照できる。
だが、どうしてもビルドが通らず、以下のエラーが出る。error CS0246: 型または名前空間の名前 'Games' が見つかりませんでした (using ディレクティブまたはアセンブリ参照が指定されていることを確認してください)。そんな時は.Netのバージョンがプロジェクトで異なっていることがある。
③.Netのバージョンを一致させる
プロジェクトのバージョンは、プロジェクトを右クリックして、プロパティから確認できる。
対象のフレームワーク の.NET Frameworkのバージョンについて、参照元よりも参照先の方が新しいと、上手に参照できない。バージョンを一致させるか、参照先の方を古いバージョンに変更する必要がある。