20210302のC#に関する記事は11件です。

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アプリのテンプレートを選択します。

a01.png

プロジェクト名を「SamplePostGraphile」、ソリューション名を「SamplePostGraphile_sol」にしましたが、名前は何でも良いです。

a02.png

Blazor WebAssembly Appを選択します。今回はhttpsは外しました。

a03.png

以下のファイルが自動生成されました。
ソースコードを読むと、データソースとしてwwwroot\sample-data\weather.jsonが使用されています。

a04.png

このままビルドして動かしてみます。
左メニューから「Fetch data」を選択すると、以下のように右側にjsonファイルのデータが一覧表示されました。

a05.png

この時点でgit commitしました。
手順は、Visual Studioのgitメニューからgitリポジトリを作成し、GitHubにpushしました。
以下のコミットメッセージは、Visual Studioが自動生成したものです。

a55.png

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つだけですね。

a10.png

ER図メニューから「DDLを作成する」を選択します。

a11.png

RDBMS種類でPostgreSQLを選択し、DDL生成ボタンを押します。

a12.png

以下の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

a09.png

データベースを作成します。名前を「sample_db」にしましたが、何でも良いです。

CREATE DATABASE sample_db;

a13.png

カレントデータベースを、作成したsample_dbに切り替えます。

\c sample_db

a14.png

先ほどERモデリングツールが生成したDDLをpsqlにコピペして実行します。

a15.png

以下の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;

a16.png

以下のSELECT文を流して、データが作成されたか確認します。

SELECT
    *
FROM
    weather_forecasts;

以下のようにidの降順で表示されましたが、順番に意味はありません。

a17.png

psqlから抜けます。

a18.png

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

起動画面

a28.png

本記事ではBlazorアプリでのCORSエラーを避けるために、単に「--cors」オプションを付けてPostGraphileを起動しましたが、本番環境では安全な方法でCORSエラーを回避してください。

postgraphileコマンドを起動するだけで、PostgreSQLのスキーマを読み取ってGraphQLエンドポイントを自動生成してくれます。
とても楽で、これもノーコードと言えるかもしれません。

起動画面によれば、URLは

となっています。
本記事では、このLinuxホストのIPアドレスは「192.168.1.7」です。
GraphQLエンドポイントのURLの「localhost」を「192.168.1.7」に書き換えて、後ほどBlazorアプリで使用します。

ここでブラウザからGraphiQLにアクセスして、クエリーを発行したりドキュメントを見たりしてみましょう。

クエリー例


query allWeatherForecasts {
  allWeatherForecasts {
    nodes {
      id
      dt
      temperatureC
      summary
    }
  }
}

ブラウザ画面

a27.png

レスポンス

{
  "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ディレクトリごと削除します。
削除後のファイルは以下の通り。

a07.png

パッケージのインストール

NuGetで以下の3パッケージをインストールします。

a29.png

ファイル新規作成:Shared/WeatherForecast.cs

GraphQL APIのレスポンスデータを格納するデータ構造を作成します。
Sharedディレクトリ配下に「WeatherForecast.cs」を追加します。

a31.png

a32.png

以下の内容にします。

Shared/WeatherForecast.cs
using 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」というディレクトリを作成します。

a22.png

Servicesディレクトリ配下に「WeatherForecastService.cs」を追加します。

a23.png

以下の内容にします。

SamplePostGraphile/Services/WeatherForecastService.cs
using 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データベースから取得したデータが一覧表示されました。

a33.png

以上でデータソースの置き換えは完了です。
この時点で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アプリケーションにコンバートします。

a51.png

NuGetパッケージの管理画面で、Telerik.UI.for.Blazorがインストールされたことを確認します。

a52.png

コンバート完了時点で、一旦git commitしました。

git add -A
git commit -m "(2)プロジェクトをTelerikアプリケーションにコンバートします"

ファイル新規作成:Pages/Grid.razor

TelerikのGridコンポーネントを使用して、データを一覧表示するページを作成します。
Pagesディレクトリ配下に「Grid.razor」を追加します。

a42.png

a43.png

以下の内容にします。

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コンポーネントでデータが一覧表示されました。

a53.png

CRUDのうちRしか実装していませんが、試しに任意の行のEditボタンを押してDate列の右端をクリックしてみます。以下のようにカレンダー入力が出てきました。

a54.png

以上で作業が完了しましたので、git commitしました。

git add -A
git commit -m "(3)Grid.razorページを追加します"

GitHubにもpushしました。

a56.png

今後

TelerikのGridコンポーネントは機能がリッチだそうですので、深堀りしてみたいですね。
サードパーティーのコンポーネントに習熟すれば、ノーコードに劣らないスピード感でアプリを開発できそうです。
むしろ、数多あるNoCodeから適切なものを選ぶ→NoCodeで開発する→場合によってはYesCodeで作り直す、というステップを踏むより負担が少ない気がします。

以上です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

アルゴリズムの基礎と疑似コード

アルゴリズムとは

アルゴリズムは有効な入力に対して、計算能力によって求められた結果を有限な時間のうちに出力することができる、定義の明確な一連の手順のことです。それはつまり有効なアルゴリズムはどのプログラミング言語においても再現することができますし、逆にアルゴリズムを実際の計算機で再現することをプログラミングということもできるわけです。

擬似コード

擬似コードはプログラミング言語ごとの特性などを無視して普遍的な理解を共有するためのものでアルゴリズムを抽象化するものです。正しく書かれた有効な擬似コードはどの言語においても再現できます。

擬似コードの記法

擬似コードの記法は様々あり、これは一例です。

記号 意味
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;
        }

のちのち加筆していきます

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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 ファイルの読み込み方です。

読み込みサンプル1
using 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.csv
Id,Name
1,Taro
2,Hanako

CSVHelper は、CSV ファイルの先頭行をヘッダとみなし、ヘッダ名から Foo のどのプロパティ名に格納するかを判断します。このため、次のような CSV ファイルでも読み込むことができます。

file2.csv
Name,Id
Taro,1
Hanako,2

CSVHelper では、ヘッダ名の大文字小文字を区別します。デフォルトは Pascal Case です。もし、Pascal Case でない場合、たとえば全て小文字の場合は、コンフィグで指定することで読み込みが可能になります。

読み込みサンプル2
using 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.csv
id,name
1,Taro
2,Hanako

CSV にヘッダがない場合

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 で宣言しておきます。

読み込みサンプル3
using 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() を使います。

読み込みサンプル4
using 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 ファイルに出力します。

書き込みサンプル1
using 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()を実行します。

書き込みサンプル2
using 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 属性の場合のサンプルは次の通りです。

書き込みサンプル3
using 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 属性の方が便利でお勧めです。
 読み込みと書き込みで列の位置が異なるなら、読み込み用のクラスマップと書き込み用のクラスマップを用意することで実現できます。

書き込みサンプル4
using 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() を使います。読み込みの場合と同じですね。

書き込みサンプル4
using 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 のに高速にインポートできます。

興味がわいた方は、下のリンクをたどってください。

CSVHelper の Example のページ

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

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

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【C#】Mutexクラスを使ったプログラムの排他制御

はじめに

アプリケーションの多重起動を阻止したい場合はMutexクラスを使用すれば良い。
Mutexクラスを使うことで簡単に排他処理を作ることができる。

サンプルプログラム

Program.cs
using 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();
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[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.Currentnullが代入されたためです。
※通常このような処理をする事はないと思います。ここでは説明のため、わざとnullを代入しています。

awaitにはその前後で実行スレッドを自動で保存してくれる機能があり、それは"SynchronizationContext.Current != null"の場合のみ働く。

この辺りの働きをさらに詳しく知りたければ、以下のページなどが参考になると思います。

そもそもSynchronizationContext.Currentはどこでセットされているのか

上でWPFなどではメインスレッドに対して自動で同期コンテキスト(SynchronizationContext.Current)が設定されていると書きました。
では、メインスレッドの同期コンテキストはどこで設定されているのでしょうか?

WPFの場合は、System.Windows.Threading.DispatcherSynchronizationContextSystem.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.Currentnullでない場合にのみ働く
  • 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)によるスレッドのロックが組み合わさることによりデッドロック起きてしまいます。
そのため、awaitTask.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.Waitawaitの併用によるデッドロックを避けるため、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しているTaskConfigureAwait(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)で十分ですが、複雑な非同期処理フローが必要になる場合にはこちらを使うと良いでしょう。

おわりに、まとめ

この記事では、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利用時の同期コンテキストの働きという点に注目してまとめました。

非同期処理をさらに理解して使いこなすには、複数の人の解説を読んで、色んな角度から見てみるのが良いと思いますので、自分が参考にさせてもらった記事のリンクをいくつか貼らせてもらいます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

簡単なタスクを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全体に言えることなんだが、ドキュメントがちょっとかゆいところに手が届かない感あり。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

例のじゃんけん

はじめに

if を使わずにじゃんけんします。
じゃんけんの結果はC#8の機能の switch式 を使って受け取ります。

先駆者様

個人ブログとのクロスポストです。

勝敗判定

じゃんけんのルールとして、-> の右側が強いものとすると、 Rock -> Paper -> Scissors -> Rock の関係性が成り立ち、3つの手を周期として勝敗が決まります。
ここで、Rock を0、Paper を1、Scissors を2として、関係性をそれぞれ1の距離を持った有効グラフだとすると、自分の手と相手の手の距離が1では負け、距離が2では勝ち、距離が0(3)ではあいこのような表現することができます。

rps_dia.png

このことから、相手の手 - 自分の手 を計算することで距離を求めることができます。
しかし、自分の手が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.DrawResult.LoseResult.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}");
        }
    }
}

実行結果

rps.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

VisualStudioでプロジェクトを追加する方法

はじめに

VisualStudioで作成済みのプロジェクトを追加し、エラーにならないための備忘録。

以下の順番で行う
①作成済みのプロジェクトを追加する
②アセンブリを参照する
③.Netのバージョンを一致させる

①作成済みのプロジェクトを追加する

ソリューションファイルを右クリック > 追加 > 既存のプロジェクト を選択
image.png

追加したいプロジェクトの.csprojファイルを選択
image.png

プロジェクトが追加される
image.png

②アセンブリを参照する

参照元のプロジェクトの 参照 を右クリックし、参照の追加 を選択
image.png

参照先のプロジェクトにチェックを入れ、OKを選択する
image.png

基本はこれで他のプロジェクトが参照できる。
だが、どうしてもビルドが通らず、以下のエラーが出る。

error CS0246: 型または名前空間の名前 'Games' が見つかりませんでした (using ディレクティブまたはアセンブリ参照が指定されていることを確認してください)。

そんな時は.Netのバージョンがプロジェクトで異なっていることがある。

③.Netのバージョンを一致させる

プロジェクトのバージョンは、プロジェクトを右クリックして、プロパティから確認できる。
image.png

対象のフレームワーク の.NET Frameworkのバージョンについて、参照元よりも参照先の方が新しいと、上手に参照できない。バージョンを一致させるか、参照先の方を古いバージョンに変更する必要がある。
image.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む