20191204のC#に関する記事は14件です。

C# の REPL, スクリプティング環境の比較

この記事は、 C# その2 Advent Calendar 2019 の 12/4 の記事だ。
ギリギリの投稿で申し訳ない。

Qiita が怖いので、 普段は ブログCrieit, Qrunch に投稿しているのだが、 今回はせっかくの Advent Calendar なので、 Qiita 初投稿。
お手柔らかにお願いします。

Qiitaとブログの使い分け 、難しいよね。

追記 2019/12/06
ktz_alias 氏にコメントで教えてもらった JupyterLab がなかなか良かったので、その情報を追記した。

みんな C# の REPL に何を使ってる?

頭の中で正確にコードをコンパイルして動かせる人ならともかく、 私のようにコードを書くのが本職ではない人間にとっては、 コード片が意図通り動くかどうかを対話的に入力できる環境があるとありがたい。

また、細々した処理を行いたいとき、 プロジェクトを作成せずにサクッと実行できるとうれしいだろう。

皆は、どのような C# REPL 環境を使っているだろうか?

…そうだね、 Visual Studio の C# Interactive だね。

~~糸冬~~

 

…それで、終わってしまってはナンなので、 C# の REPL や スクリプト実行 ができるツールを紹介しつつ、簡単に比べてみたいと思う。

「他にもこんなものがあるよ!」 と言うのがあったら、是非コメントいただけると幸いだ。

Visual Studio C# Interactive

csharp_interactive.gif
Visual Studio に付属している、C# インタラクティブ。

正直、これが使えるならこれ一択という感じ。

他がぱっとしないのも、これが強すぎるからかもしれない。

最大の問題は、ライセンスだ。
Visual Studio Community があるので、 個人PC上で個人用途に限るなら何も問題はないが、企業や団体に所属したり、それらから受託する場合はそうはいかない。
せっかく Visual Studio 2017 Professional を買ってもらえても、 2019 が出た後は最新の C# が使えなくて悔しい思いを味わうことになる。

他にも、 ちょっとコードの実行を試したいだけだと、立ち上げるのが重量級過ぎるという問題もある。
(これは、貧弱すぎるスペックのマシンを使っているのが悪いというのもあるが。。)

また、現状は .NET Framework のみの提供で、 .NET Core 環境が無いというのも地味な欠点。
C# 8.0 以降、 .NET Core 3 (厳密には、 .NET Standard 2.1) 以降じゃないと使えない機能がぼちぼち出てきているので、これは意外と大きな問題である。

C# Interactive には、 Visual Studio を立ち上げないでもよいコンソール版 (csi.exe) が存在するのだが、こちらはコード補完が全くないので利便性が大きく落ちる。

Mono C# Shell csharp, gsharp

csharp.gif
gsharp.gif
Mono に付属している C# Shell の CUI版 と GUI版。

.NET Core が登場してからは何かと影が薄い Mono。

Xamarin や Unity のバックエンドとしてはまだまだ現役で使われているけれど、 .NET 5 では .NET Core と統合される予定。

そんな Mono には、付属の C# Shell が用意されている。

C# Interactive には遠く及ばないものの、簡単なコード補完もついているので、ちょっと動かす程度なら全然使える。
ただ、 型の補完が微妙だったり、 CaseSensitive な補完しかできなかったり(これは好みにもよるが)、 補完の候補が一部足りてなかったりと、 あまり期待しない方がよいレベル。。。

GUI 版は、 Mono 5 以降ほとんど更新されていないので、 補完のインターフェースが CUI版 よりもちょっと悪い。
一方で、 Plot 関数でグラフが描けたり、 画像を表示できたりといった、 使いどころが微妙な利点がある。

また、 Windows版 Mono には GUI 版である gsharp が入っていない。
このため、 WSL に Mono を入れて Xサーバ で GUI を動かすのが手っ取り早いのだが、 Ubuntu で 最新 (6.4系) の Mono の gsharp を入れようとすると、 パッケージの依存関係の不具合でインストールできない問題がある。
C# REPL GUI Shell, Mono gsharp を Ubuntu に入れようとすると発生するエラーを回避する | Aqua Ware つぶやきブログ

dnSpy

dnspy.png
お世話になっている人も多いだろう、 多機能で UI がステキな .NET の 逆コンパイラ・デバッガーである dnSpy

実はこの dnSpy にも C# Interactive が付属している。

シンタックスハイライトもきれいに効いて、とてもイケているのだが、大変残念ながらコード補完がないのだ。
一応かなり昔から作者による Issue は立っているのだが、 毎回マイルストーンが後ろにずらされ、最近ついにマイルストーンから外されてしまったので、対応は期待できなさそうだ。
Add C# REPL completion · Issue #197 · 0xd4d/dnSpy

.NET Core 版の dnSpy なら、 C# Interactive も .NET Core で動いているのが良ポイント。

ただ、残念ながら Linux などの 非Windows では提供されていない。

LINQPad

linqpad.png
LINQPad - The .NET Programmer's Playground

LINQ に限らず、 C# や VB.NET のコードの動作確認を行えるツールだ。

コードは書いたそばから自動的に実行されて結果が表示されるが、 REPL ではないため、毎回コードの先頭から実行される。
このため、使い勝手としては C# Script に近い。

データのダンプ機能が優れているので、簡単なデータ処理をさせるのには向いているかも知れない。

CS-Script

CS-Script | ECMA-compliant C# based scripting platform.

わりと C# が登場した (2002年) 直後くらい (2004年) から存在している、老舗スクリプティング環境。

スクリプトファイルを指定してコードを実行することはできるが、 どちらかというとアプリにライブラリとして組み込むスクリプトエンジンを提供することに主眼を置いているようだ。

他のほとんどの実装が Roslyn のスクリプティング機能で動いている一方、 CS-Script は独自のエンジンで動かしているようだ。
…とはいっても、最終的には .NET や Mono の Roslyn コンパイラでコンパイルされるわけだけども。

その為かどうかわからないけど、スクリプトの記述ルールが他とちょっと違う。

scriptcs

scriptcs.png
scriptcs - Write C# scripts in your favorite text editor

こちらも Roslyn がスクリプティング機能に対応する前の、割と昔からある老舗。

CS-Script とは異なり、コチラは Roslyn 登場後は Roslyn スクリプティング機能で動いているようだ。

スクリプトを実行したり、 REPL を起動できたりはするが、 コード補完はできないし、これといった特徴がない。。。

.NET Framework で動作するため、 .NET Standard 2.1 にも非対応。

dotnet script

dotnet-script.gif
filipw/dotnet-script: Run C# scripts from the .NET CLI.

C# スクリプト実行環境その3.

Visual Studio Code と連携することで、 デバッグや OmniSharp のコード補完のメリットが享受できる。
スクリプトを書いて実行するなら、個人的にはコレがオススメ。

また、 nuget と連携して依存ライブラリの解決が簡単にできるのも強み。

REPL はあるが、 コード補完はないのでオマケ程度と考えれば。

.NET Core with Jupyter Notebooks

jupyter.gif

@ktz_alias 氏にコメントで教えていただいた JupyterLab + .NET カーネル (.NET Core with Jupyter Notebooks) が、なかなか素晴らしい REPL 体験を提供してくれる。

型に基づいたコード補完が REPL で使えるのが最の高。

ローカルに Webサーバー 立ち上げてアクセスする仕組みなので、 Windows でなくても利用できる。

Preview 版と言いつつも、すでにだいぶ完成度が高いので、 「タブキー押さなくても、 "." を入力したときに補完が働いてほしい」 とか 「補完時には大文字と小文字を区別しないでほしい」とか、細かい部分が逆に気になり出してしまうほど。
今後の更新が楽しみである。

ドキュメントでは Jupyter インストールするために Anaconda 入れろとか書いてあるけれど、 Jupyter で .NET Core カーネルを実行する分には Python + pip で十分だ。
pip で Jupyter をインストールする場合、依存パッケージが膨大なので、venv で仮想環境に分けておいたほうが良い。
雑になるが、おおむね以下のような手順でインストール可能。

  1. .NET Core SDK 3.1 LTS または 2.1 LTS をインストール
  2. dotnet tool install -g dotnet-try で dotnet-try グローバルツールをインストール
  3. Python 3.7 あたり をインストール
  4. venv 環境を作成して、 pip で jupyterlab をいれて、 .net jupyter カーネルを入れて、 JupyterLab を起動

Windows なら以下のような感じ。

py -3.7 -m venv .\venv_jupyter
.\venv_jupyter\Scripts\activate
pip install jupyterlab
dotnet try jupyter install
jupyter lab

Ubuntu なら以下のような感じ。

python3 -m venv ./venv_jupyter
. ./venv_jupyter/bin/activate
pip install jupyterlab
dotnet try jupyter install
jupyter lab

私が試した限りにおいては、 Python 3.8 だとエラーが出て Jupyter が起動できなかった。

オマケ

C# じゃないけど、 .NET で REPL できるヤツら。

  • PowerShell
    • 言わずと知れた、 .NET 界の コマンドラインシェル&スクリプト。
      オプジェクトパイプラインという独特な仕様と PowerShell Core の登場によって、 Windows 外にも地味に浸食しはじめている。
      C# の動作確認ではなくて、 .NET ライブラリの動作確認を行いたいのなら、 PowerShell で十分だろう。
      C# と文法が若干似ているのも使いやすいポイント。 似ているのに所々全然違うので慣れるとかえってハマったりもするが。
      C# でできることは大体 PowerShell でもできるが、オーバーロードやジェネリックスの解決がちょっと苦手。
  • F#
    • 公式でまともな REPL (fsi.exe) が提供されているので、 こちらも .NET ライブラリの動作確認を行いたいなら代替になる。
      関数型言語で C# とはかけ離れた文法なので、知らんと全く読み書きできない。
  • IronPython
    • Python が得意ならコレも選択肢になるだろう。
      Python らしく、 REPL もそこそこ使える機能を持っている。
      .NET Core (.NET Standard 2.0) にも対応している。
      言語仕様が、 2020年1月1日 で命日を迎える 2.7 系なのが難点。
      現在開発中の IronPython3 では Python 3 に対応されるので、花開くかもしれない。

まとめ

ここまで挙げたものの特徴を表にしてみた。

種類 スクリプティング REPL .NET Standard 2.1 対応 Linux対応 コード補完 備考
C# Interactive × × 商用利用有償 Mac版ならある
Mono C# Shell GUI版 は WSL 経由
dnSpy × × ×
LINQPad × × × ×
CS-Script × ○? × 老舗
scriptcs × × ×
dotnet-script コード補完はスクリプト記述時のみ。 NuGet 連携が特徴。
JupyterLab
PowerShell
F#
IronPython 次期 2.7.10 で .NET Core 3.0 に対応予定
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Blazorにおけるフォームバリデーション手法のまとめ

概要

Blazorにおけるフォームバリデーションの手法に関して紹介します。
下記のようなログインフォームを例にして紹介します。

ファイル名

本記事のデモ(メニューのFormを選択)
ソースコード

前提

.NET Core SDK 3.1.100-preview3-014645
Microsoft.AspNetCore.Blazor 3.1.0-preview2.19528.8
Visual Studio 2019

WebAssembly版(Client版)を使用しています。

また、サンプルではUI要素としてMatBlazorを使用しています。
詳細は下記を参照してください。
https://qiita.com/nobu17/items/ecf2121f7bbb6bc5294b

MatBlazorを使わない場合、一般的なForm要素に置き換えてください。
MatTextField → InputTextもしくはinput
MatButton  → button (type="submit")

基本編

一番多く使う基本的なパターンの実装と説明を行います。

入力モデルの作成

まずは、入力画面にバインドするクラスを定義します。
その上で、各項目に対するバリデーション定義を追加します。
バリデーションを属性で表現するのは、ASP.NET MVC等でもおなじみな方法なので.NET開発者であれば見慣れたものかと思います。

LoginData.cs
    public class LoginData
    {
        [Required(ErrorMessage = "ユーザIDを入力してください。")]
        [StringLength(16, ErrorMessage = "ユーザIDが長すぎます。")]
        public string UserID { get; set; }

        [Required(ErrorMessage = "パスワードを入力してください。")]
        [StringLength(32, ErrorMessage = "パスワードが長すぎます。")]
        public string Password { get; set; }
    }

コードビハインドの作成

次にViewにバインドする、先ほどのLoginDataをメンバとして保持するクラスを作成します。
今回はコードビハインドでrazorコンポーネントとC#コードを分離して記載します。
コードビハインドに関しての詳細は、下記を参照してください。
https://qiita.com/nobu17/items/b7dc78db7beb1d833dc8

Form1ViewModel.cs
    public class Form1ViewModel : ComponentBase
    {
        public LoginData LoginData { get; set; } = new LoginData();

        public void Submit()
        {
            // do something
        }
    }

ビューの定義

コードビハインドとバインドする画面を作成します。

Form1.razor
@inherits MatTest.Models.Form.Form1ViewModel
<EditForm Model="@LoginData" OnValidSubmit="@Submit">
    <DataAnnotationsValidator />
    <ValidationSummary />

    <MatTextField FullWidth="true" Label="UserID" @bind-Value="@LoginData.UserID"></MatTextField> 
    <ValidationMessage For="@(() => LoginData.UserID)" />

    <MatTextField FullWidth="true" Label="Password" @bind-Value="@LoginData.Password" Type="password"></MatTextField>
    <ValidationMessage For="@(() => LoginData.Password)" />

    <MatButton Label="Login" Outlined="true" Type="submit">
</EditForm>

EditForm

フォームで入力する要素をこのタグで囲みます。
- Modelにフォームに入力するプロパティをバインドします。
- OnValidSubmitに入力が正常な場合の確定処理のメソッドをバインドします。
また、不正な入力で確定ボタン押下を検知したい場合には、OnInvalidSubmitイベントをバインドすることで検知できます。

DataAnnotationsValidator

先ほど入力データクラスに付与した属性(Requiredなど)のバリデーションを実施する場合に記載します。

ValidationSummary

バリデーションで発生したエラー内容を表示します。

vali_1.png

ValidationMessage

ValidationSummaryはすべてのエラー内容が表示されるため、
各入力項目に対して個別のバリデーションを表示したい場合に使用します。
For内にラムダでプロパティを指定します。

vali_2.png

カスタムの検証属性

独自のバリデーション属性を作成したい場合は、ValidationAttributeクラスを継承します。
これはBlazor固有というよりも.NETでは一般的に使われている手法です。

CustomeValidationAttribute.cs
    public class CustomeValidationAttribute : ValidationAttribute
    {
        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            var str = value as string;
            if (str != null && string.IsNullOrWhiteSpace(str))
            {
                return new ValidationResult("空白は無効です。", new[] { validationContext.MemberName });
            }
            return ValidationResult.Success;
        }
    }

IsValidをオーバーライドして対象のオブジェクトを検証します。

  • 正常の場合はValidationResult.Success戻す
  • 入力エラーの場合は、ValidationResult内にエラーメッセージとvalidationContext.MemberNameを入れて戻す

入力エラー時には下記のようにエラーメッセージが表示されます。

cus.PNG

応用編

基本をベースに、色々な場合を紹介します。

ネストしたクラスに対するバリデーション

DataAnnotationsValidatorはネストしたオブジェクトに対しては、機能しません。
現在はまだプレビュー版となりますが、下記の手順で可能となります。

モジュール追加

Nugetから下記モジュールを追加します。

Microsoft.AspNetCore.Blazor.DataAnnotations.Validation

属性追加

ネストしたクラスのプロパティに対して、ValidateComplexType属性を付与します。

NestedData.cs
    public class NestedData
    {
        [Required]
        [ValidateComplexType]
        public LoginData LoginData { get; set; } = new LoginData();
    }

バリデータの変更

DataAnnotationsValidatorの代わりにObjectGraphDataAnnotationsValidatorを使用します。

Form2.razor
@inherits MatTest.Models.Form.Form2ViewModel
<EditForm Model="@NestedData" OnValidSubmit="@Submit">
    <ObjectGraphDataAnnotationsValidator />
    <ValidationSummary />

    <MatTextField FullWidth="true" Label="UserID" @bind-Value="@NestedData.LoginData.UserID"></MatTextField> 
    <MatTextField FullWidth="true" Label="Password" @bind-Value="@NestedData.LoginData.Password" Type="password"></MatTextField>
    <MatButton Label="Login" Outlined="true" Type="submit">
</EditForm>

OSSモジュール(FluentValidation)を使ったカスタムバリデーション

OSSで提供されいてる機能で属性検証以外のバリデーションを作成できます。
FluentValidationといった.NET向けのバリデーションライブラリをBlazor対応させる方法が紹介されているのでそちらを参考に実装します。

https://chrissainty.com/using-fluentvalidation-for-forms-validation-in-razor-components/

パッケージの導入

NugetからFluentValidationをインストールします。

バリデータの作成

FluentValidationの作法に沿ったバリデーションクラスを作成します。

LoginDataValidator.cs
    public class LoginDataValidator : AbstractValidator<LoginData>
    {
        public LoginDataValidator()
        {
            RuleFor(p => p.UserID).NotEmpty().WithMessage("ログインIDを入力してください。");
            RuleFor(p => p.UserID).MaximumLength(10).WithMessage("ログインIDは10文字まで入力してください。");
            RuleFor(p => p.Password).NotEmpty().WithMessage("パスワードを入力してください。");
            RuleFor(p => p.Password).MaximumLength(10).WithMessage("パスワードは10文字まで入力してください。");
        }
    }

AbstractValidatorを継承したクラスを作成します。
(ジェネリクスにはバリデーション対象のクラスを指定)
コンストラクタ内でRuleForラムダで各メンバーのバリデーションを実装します。

コンポーネントの作成

作成したバリデータだけではBlazorではそのまま使えないため、Blazor側のバリデーションに対応させるためのコンポーネントを作成します。
BlazorにはバリデーションのためのEditContextといった仕組みが提供されており、その仕組み内でFluentValidationのバリデーションを行います。
EditContextの詳細に関しては割愛しますが、下記等が参考になります。
https://gunnarpeipman.com/blazor-form-validation/

掲載元(掲載時より、一部APIの仕様が変わっているのでその対応を行っています。AddRangeをAddに変更。参考)

FluentValidationValidator.cs
    public class FluentValidationValidator : ComponentBase
    {
        [CascadingParameter] EditContext CurrentEditContext { get; set; }

        protected override void OnInitialized()
        {
            if (CurrentEditContext == null)
            {
                throw new InvalidOperationException($"{nameof(FluentValidationValidator)} requires a cascading " +
                    $"parameter of type {nameof(EditContext)}. For example, you can use {nameof(FluentValidationValidator)} " +
                    $"inside an {nameof(EditForm)}.");
            }

            CurrentEditContext.AddFluentValidation();
        }
    }

    public static class EditContextFluentValidationExtensions
    {
        public static EditContext AddFluentValidation(this EditContext editContext)
        {
            if (editContext == null)
            {
                throw new ArgumentNullException(nameof(editContext));
            }

            var messages = new ValidationMessageStore(editContext);

            editContext.OnValidationRequested +=
                (sender, eventArgs) => ValidateModel((EditContext)sender, messages);

            editContext.OnFieldChanged +=
                (sender, eventArgs) => ValidateField(editContext, messages, eventArgs.FieldIdentifier);

            return editContext;
        }

        private static void ValidateModel(EditContext editContext, ValidationMessageStore messages)
        {
            var validator = GetValidatorForModel(editContext.Model);
            var validationResults = validator.Validate(editContext.Model);

            messages.Clear();
            foreach (var validationResult in validationResults.Errors)
            {
                messages.Add(editContext.Field(validationResult.PropertyName), validationResult.ErrorMessage);
            }

            editContext.NotifyValidationStateChanged();
        }

        private static void ValidateField(EditContext editContext, ValidationMessageStore messages, in FieldIdentifier fieldIdentifier)
        {
            var properties = new[] { fieldIdentifier.FieldName };
            var context = new ValidationContext(fieldIdentifier.Model, new PropertyChain(), new MemberNameValidatorSelector(properties));

            var validator = GetValidatorForModel(fieldIdentifier.Model);
            var validationResults = validator.Validate(context);

            messages.Clear(fieldIdentifier);
            // APIの仕様変更のため、Addに変更
            //messages.AddRange(fieldIdentifier, validationResults.Errors.Select(error => error.ErrorMessage));
            messages.Add(fieldIdentifier, validationResults.Errors.Select(error => error.ErrorMessage));

            editContext.NotifyValidationStateChanged();
        }

        private static IValidator GetValidatorForModel(object model)
        {
            var abstractValidatorType = typeof(AbstractValidator<>).MakeGenericType(model.GetType());
            var modelValidatorType = Assembly.GetExecutingAssembly().GetTypes().FirstOrDefault(t => t.IsSubclassOf(abstractValidatorType));
            var modelValidatorInstance = (IValidator)Activator.CreateInstance(modelValidatorType);

            return modelValidatorInstance;
        }
    }

実装

作成したバリデータコンポーネント(FluentValidationValidator)を配置します。

Form3.razor
<EditForm Model="@LoginData" OnValidSubmit="@HandleValidSubmit" class="mat-layout-grid-cell mat-layout-grid-cell-span-12">
    <FluentValidationValidator />
    <ValidationSummary />
    <MatTextField FullWidth="true" Label="UserID" @bind-Value="@LoginData.UserID"></MatTextField>
    <MatTextField FullWidth="true" Label="Password" @bind-Value="@LoginData.Password" Type="password"></MatTextField>
    <MatButton Label="Login" Outlined="true" Type="submit"></MatButton>
</EditForm>

まとめ

Blazorにおけるフォームバリデーション手法に関してまとめました。
バリデーション手法は従来の.NETの手法を踏襲しているため、親しみがある人も多いのではないでしょうか。

Blazorのその他の投稿記事

何点かBlazorに関して記事を書いていますので、良ければ見てみてください。

参考資料

https://docs.microsoft.com/ja-jp/aspnet/core/blazor/forms-validation?view=aspnetcore-3.0
https://gunnarpeipman.com/blazor-form-validation/
https://chrissainty.com/using-fluentvalidation-for-forms-validation-in-razor-components/
https://blazor-university.com/forms/writing-custom-validation/
http://blazorhelpwebsite.com/Blog/tabid/61/EntryId/4337/Blazor-Forms-and-Validation.aspx
https://remibou.github.io/Client-side-validation-with-Blazor-and-Data-Annotations/
https://dzone.com/articles/blazor-form-validation
https://itnext.io/blazor-forms-and-validation-418173350435

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

リンクスリングスのアウトゲーム設計

はじめに

サムザップ #2 Advent Calendar 2019 の12/3の記事です。

株式会社サムザップの尾崎です。Unityエンジニアです。

内容

リンクスリングスのアウトゲームの設計について紹介したいと思います。
また扱いやすいAPI(プログラムインターフェース)を目指しているのでそのコードを紹介します。

※ アウトゲームとはキャラクター選択画面など4v4バトルのゲーム本体以外の機能を指します
※ 紹介するコードはエラー処理を省いて記載してきます

リンクスリングスについて

公式サイト

https://linqsrings.jp/

画面イメージ

スクリーンショット 2019-12-04 午後9.25.20.png スクリーンショット 2019-12-04 午後9.32.08.png スクリーンショット 2019-12-04 午後9.26.49.png

linqs_sample.gif

ゲーム画面動画

https://www.youtube.com/watch?v=XFfSejixKfE

設計方針

  • 分かりやすいシンプルな構成
  • 使いやすいAPI
  • メンテナンスしやすい
  • 簡単に動作確認できる

主な採用技術

async/awaitUniTask

async/awaitはC#標準の非同期処理のための機能です。
コルーチンの代わりとして使っていて、画面遷移や通信やアニメーションなどの非同期系処理はasync/awaitに統一しています。
コールバックがなく読みやすいコードになっています。

Zenject

オブジェクト同士を参照させるのにZenjectを採用しています。
staticやシングルトンがなくなり、整理されたクラス関係を構築できました。

Pusher

アウトゲームでのリアルタイム通信のために採用しています。
マッチング、チャット、ゲーム内通知などに使用しています。
HTTPポーリングに比べて高速なレスポンスが得られています。
ちなみにインゲームではPhotonを採用しています。

プログラム構成

MVC(Model-View-Controller)パターンです。
MVVM、MVPと比較検証した結果、シンプルなMVCを採用しました。

Model

Model=データはxxxDataというクラスに定義しています。
データそのものとそれを扱うメソッドを持ちます。
サーバーから受け取ったjsonをC#オブジェクトにする役割もあります。

public partial class SomeData : ISerializationCallbackReceiver
{
    // サーバーから受け取ったintへのプロパティ。読み取り専用
    public int SomeCount => someCount;

    public enum SomeTypes
    {
        None,
        Type1,
        Type2
    }

    // サーバーから受け取ったstringをenumに変換
    public SomeTypes SomeType;

    // データを元に判定を行ったりするプロパティ
    public bool SomeUsefulProperty
    {
        get
        {
            ...
        }
    }

    // データ検索などを行うメソッド
    public int SomeUsefulMethod(SomeTypes type)
    {
        ...
        ...
    }

    public void OnAfterDeserialize()
    {
        // 文字列をenumに変換
        Enum.TryParse(someType, out SomeType);
    }

    public void OnBeforeSerialize() { }
}

// サーバーから受け取るjsonをデシリアライズするためのクラス
// 半自動生成
[Serializable]
public partial class SomeData
{
    [SerializeField]
    private int someCount;

    [SerializeField]
    private string someType;
}

Controller

画面を制御する部分です。
ModelとViewの橋渡しをします。
1画面につき1つのメインコントローラーを用意します。
複雑な画面ではメインコントローラー1つだとクラスが大きくなるので画面内の一部分を制御するサブコントローラーを作成します。

// 画面のメインコントローラー
public class SomeScene : MonoBehaviour, IAdditiveSceneTask
{
    // 画面遷移システム
    [Inject]
    private SceneLoader _sceneLoader;

    // View
    [SerializeField]
    private Text _text;

    // サブコントローラー
    [SerializeField]
    private SomeSubController _subController;

    // 画面遷移トゥイーン
    // インスペクタでリストにトゥイーンを登録するコンポーネントです
    [SerializeField]
    private Tweens _tweens;

    // 初期化
    private void Start()
    {
        _text.text = "";
    }

    // 画面遷移システムから画面開始時に呼び出される独自のコールバックです
    // IAdditiveSceneTaskを実装すると呼ばれます
    public async Task Activate()
    {
        /* 画面開始時の処理 */
        // 通信
        var someData = await WebRequest.Factory.SomeInfo(param).Send();

        // データをUIにセット
        _text.text = someData.name;

        // サブコントローラーの実行
        _subController.Execute();

        // UI出現アニメーション
        await _tweens.PlayInAnimations();
    }

    public async Task Inactivate()
    {
        /* 画面終了時の処理 */
        // UIを消すアニメーション
        await _tweens.PlayOutAnimations();

        // 各種アンロード
    }

    private void OnDestroy()
    {
        // 後処理
    }

    // ボタンが押されたときの処理
    // インスペクタでButtonコンポーネントから呼び出すように設定します
    public void OnClickButton()
    {
        // 例でバトルトップ画面に遷移
        // 画面はシーンをAdditiveロードする仕組み
        // 次シーンをロードしてActivate()を呼び出し、現在シーンのInactivateを呼び出します
        _sceneLoader.LoadSceneAdditive(ScenesEnum.BattleTop, false);
    }
}

View

Unity UIのCanvasやImage、ScrollRect、LayoutGroupなど見た目を制御するコンポーネントをViewコンポーネントと位置付けています。
それら見た目を制御するコンポーネントを組み合わせてHierarchyを構築してファイル化したSceneやPrefabがViewの扱いです。
基本的にはUnity UI標準コンポーネントを利用して、独自のViewコンポーネントを組み合わせています。
独自コンポーネントにはタブ、トゥイーン、スプライトアニメなど多数あります。
WebでいうHTMLのイメージです。

アウトゲームの機能

画面遷移

// 画面遷移のためのクラス
[Inject]
private SceneLoader _sceneLoader;

// シーンをAdditiveロード
_sceneLoader.LoadSceneAdditive(
    Scenes.SomeFunc,
    new SomeFuncScene.Arguments
    {
        TargetId = 1001
    }
);

ダイアログ (ポップアップウインドウ)

// ダイアログ開く
var dialog = await DialogLoader.Load<SomeDialog>();
dialog.Execute(param);
// ボタンが押されて閉じられるまで待つ
bool isOk = dialog.WaitClose();
if (isOk)
{
    // OKが押されたときの処理
}
public class SomeDialog : MonoBehaviour
{
    // ダイアログ共通処理コンポーネント
    [SerializeField]
    private DialogCommon _common;

    // OKボタンを押した?
    private bool _isOk = false;

    private void Awake()
    {
        // 初期化
        // 開く処理はDialogCommonによって自動的に行われます
    }

    public void Execute(int param)
    {
       // 引数を使った処理 
    }

    // OKボタンを押した
    public void OnClickOkButton()
    {
        _isOk = true;
        _common.Close();
    }

    // キャンセルボタンを押した
    public void OnClickCancelButton()
    {
        _common.Close();
    }

    // ボタンが押されてダイアログが閉じるまで待つ
    // 選択結果を返す
    public async Task<bool> WaitClose()
    {
        await Common.WaitClose();
        return _isOk;
    }
}

通信

try
{
    var webRequest = new WebRequest<SomeData>(APIType.SomeInfo, param);
    var responseData = await webRequest.Send();
}
catch (WebRequestException e)
{
    // 通信エラー時
}

リアルタイム通信

[Inject]
private IPusher _pusher;

await _pusher.Subscribe("channel_name",);
_pusher.Bind<SomeRealtimeData>("channel_name", "event_name", (someData) => {
    // サーバーからデータ受信したときの処理
    // 例. マッチングしたプレイヤーの情報を表示、チャットメッセージを表示
});

アセットバンドル

アセットバンドルシステムはIAssetBundleLoaderとして抽象化してサーバーからロードするクラスとローカルファイルからロードするクラスを切り替えられるようにしています。
Loadメソッドの第三引数ownerはGameObject型の引数でownerがDestroyされるとアセットバンドルもアンロードされる仕組みにしています。

// アセットバンドルロードシステム
[Inject]
private IAssetBundleLoader _assetBundleLoader;

var prefab = await _assetBundleLoader.Load<GameObject>(assetBundleName, assetName, owner);

設計で気をつけていること

コンポーネント指向

Unityの設計に習いコンポーネント指向で開発しています。
小さい機能を実現するコンポーネントを組み合わせて大きな機能を作ります。
コンポーネントが充実してくると組み合わせて新しい機能を効率よく作れます。
コードを書く必要がなく、非エンジニアにも優しいです。

これはコンポーネントの組み合わせで作ったボタンです。
スクリーンショット 2019-11-18 午後3.21.51.png

各コンポーネントの役割です

  • xxxButton: 独自ボタン制御(小さなコントローラー。クリック時の画面遷移などを行う)
  • Image: 見た目
  • Button: ボタン
  • CanvasGroup: 透明度とクリック可否
  • SwitchSprite: Imageに割り当てるスプライトの切り替え
  • ScaleInTween: UI出現時のトゥイーンアニメ
  • ScaleOutTween: UI消失時のトゥイーンアニメ
  • ClickTween: クリック時のトゥイーンアニメ
  • Se: クリック時のSE再生

各種TweenやSeはボタン以外でも利用しています。

コンポーネント指向の逆はオブジェクト指向の継承だと思います。
継承で上記ボタンを作ると標準Button継承したCustomButtonを作成しその中でトゥイーンやSe再生を作り込むことになり、それらは再利用しにくいものになります。
また大規模プログラムで継承を多用すると基底クラスに不必要な機能が入って肥大化することが多いです。コンポーネントの組み合わせで作ることでコード重複が少なく、再利用性の高いプログラムになります。
リンクスでは使い所をわきまえて継承階層が深くならないようにしています。

依存性の注入

Zenjectを利用しています。
1つの実装に依存しない柔軟性のあるプログラムにしています。
複数の実装が必要のないものはinterfaceを定義せずにクラス1つにしています。

シングルトンは禁止しています。

テストしやすい環境

シーンやコンポーネントをテストしやすくしています。
例えばシーンではバトル後の結果画面は正規フローだとログイン、マッチング、バトルを経るため動作確認までにとても手間がかかります。
バトル結果画面のシーンを開いた状態でUnity再生するとダミーデータで動作させて素早く確認できるようにしています。
コンポーネントはインスペクタにデバッグボタンを用意して確認しやすくしています。

UniRxオペレーターを多用しない

UniRxには多数のオペレーターが用意されていますが習得コストが高いと判断し、Whereなど超基本的なもののみを使うようにしています。
UniRxで使用しているのはSubject、ReactiveProperty、MicroCoroutineです。

  • Subject
    • C#標準eventの代わりに使用。解放が楽です。
  • ReactiveProperty
    • 値の変化を購読するときに使用しています。
  • MicroCoroutine
    • 高速なUpdate、コルーチンとして使用しています。

通信などの非同期処理にもRxを使わずasync/awaitかコルーチンを使っています。
手続き型で記述することで分かりやすくしています。

シングルトンを使用しない

シングルトンをアンチパターンと捉えて使用しないようにしています。
1つの実装に依存することになるのと、グローバル変数と同じく様々なところからアクセスされると分かりにくいコードになってしまうためです。

シーン構成

Unityのマルチーシーン機能を活用して1画面1シーンの構成にしています。
この構成にすることで作業分担しやすくなっています。
またシーンを開いて再生することで編集中画面の動作を素早く確認することもできます。

この画面はホーム画面でミッション画面を開きアイテム詳細ダイアログを開いた状態です。
スクリーンショット 2019-12-04 午後9.45.32.jpg
このときのHierarchyはこのようになっています。
スクリーンショット 2019-12-04 午後9.45.21.png

最後に

明日は @tomeitou さんの記事です。

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

【腹筋VR】Webエンジニアが「VRアプリ開発」やってみた→3日後

自己紹介

SESはWeb系じゃないけど、Web界隈のエンジニアです。VRの開発とかUnityとか何も知らない状態から3日でつくりました。

腹筋VR?

腹筋の回数をカウントしてくれるVRアプリです。いずれリリースできればと思います。
3日で動画のレベルまで形にできるんです。良くないですか? 3日間でやったことについて書きます。

やったこと

Oculus Goで開発しています。メルカリで1.8万で買いました。Oculus Questも近々購入予定。

環境構築

Udemyがサイバーマンデーセールだったので下記講座を購入しました

Unity 3D 超入門:UnityでVRゲームやエンドレスランなど4つのゲームを作ろう!
5つのゲームを作る講座もありますが、どう違うのか存じません

環境構築の記事もありますが、Vulkanを外す手順とか抜けてる記事が多いです。記事で何度かハマりましたが、上記の動画なら一発で環境構築できました。

注意事項
ただし、紹介した動画ではUnityの2017年版で環境構築させようとしてきます。インポートできないAssets(3Dモデルなどの素材)も多いので「2019.1」版でやりました。Gradleの設定だけはできなかったので飛ばしましたが、他の手順はそのままでビルドできました。

開発

Asset

環境構築が終わったらAssetを入れます。開発に必要な3Dモデル等を入れていく必要があります。最初にオススメしたUdemyの動画もいろいろな記事やサイトもOculus Integrationをオススメしてきます。しかし僕はOculus Integrationを即不採用にしました。

Oculusが公式で出している「Oculus Integration」を入れれば、Oculus Go向けのカメラ設定やコントローラの設定ができます。すぐにVR空間上に自分のコントローラを表示できるでしょう。ただし、メニュー画面の操作とかできません。

image.png

上記はプロトタイプの段階で作った3D空間のUI(メニュー画面)です。Oculus Goのコントローラが画面上に表示されるだけでメニューに表示されるボタンを押すことはできません。

下記のツイートの動画はもう少しブラッシュアップした版です。UI(メニュー画面)を操作するにはコントローラーからレーザーポインター等を出して、メニュー画面を操作する必要があるのです。動画序盤でコントローラーからでているレーザーポインターをやってくれているのはVIVE Input Utilityです。サンプルをみればすぐに下記のようなレーザーポインターを付けることができます。

角度の取得

動画を見ていただくとわかりますが腹筋するときは首が動きますね。顔が向いている角度で腹筋の回数をカウントします。UnityなのでC#です。

void Update () {
    Quaternion quaternion = this.transform.rotation;
    double x = quaternion.eulerAngles.x;
}

なんとこれだけで角度が取得できます。あとはプログラムをカメラにドラッグアンドドロップしてやれば、this(カメラ)の向いてる角度がxに入ります。Unity簡単ですね。

「真正面」を向いているときが0度、首を1度上に傾けると359度になるというルールがあります。

起き上がったとき=「カメラの向きが350度以上」
横になったとき =「カメラの向きが320度以下300度以上」

// 顔の角度
private const double up = 320.0;
private const double down = 350.0;
private const double limit = 300.0;

// 横になっているかどうか
private bool isDown = false;

// 腹筋回数
public int count = 0;

void Update () {

    // カメラの角度を取得
    Quaternion quaternion = this.transform.rotation;
    double x = quaternion.eulerAngles.x;

    // 300度未満なら何もしない
    if (x < limit) {return;}

    // 横になっていなかった かつ 320度未満300度以上
    if(isDown == false && x < up)
    {
        // 横になったと判定
        isDown = true;
        return;
    }

    // 横になっていた かつ 顔の角度が350度以上
    if(isDown == true && x > down)
    {
        // 横になっているかフラグをfalseにする
        isDown = false;
        count++; // 腹筋カウント
    }
}

このルールで腹筋のカウントをしています。単純でしょ?ヘドバンしないでね
C#の書き方いい加減すぎるか…

あとは…

ここまで腹筋VRを支える技術を説明しました。あとは最初に紹介した動画を見れば、開発に必要な知識を得られます。

最後に

なぜVRの開発を始めたのか、その動機が一番重要なので語らせてください。

最初はVRに失望していた

もともとVRには興味がありませんでした。学生時代に発売したOculus Riftの本体は高くないものの、「ゲーミングPC必須」でした。入門するだけで20~30万は軽く飛んでしまいます。これでは一般人には普及しない、と私は失望しました。当時スマホVRもコンテンツ数はとても少なかったのです。

XR燻製会の存在

それから数年間VRに対してアンテナを張っていませんでしたが、XR燻製会という煙的にもくもく会wイベントに参加しました。

僕は勢いで申し込んだのですが、内輪だけでやる予定のイベントだったのです。後日知ったのですが、「かんちゃんって誰だろう? 多分、他の参加者の知り合いなのかな」と当日まで思われていたそうです。

そこで燻製された美味しい食事とOculus GoやOculus QuestをはじめとするxR(VRやARの総称)コンテンツに触れることができました。「今はPCなくてもVRできる」という衝撃の事実を知りました。Oculus Goなら2万円、Oculus Questなら5万円でVRできるんです!すごい!

HoloLens ミートアップの参加

厳密にVRの勉強会ではないですが、ホロレンズミートアップに参加しました。xRの話が中心なので、VRの話も聞けました。具体的な業務利用の話やVRアプリの体験、とても新鮮でした。このイベントが終わってお金がたまった後にOculus Goポチりました。しばらく遊んだあと、開発を始めて3日で最初の動画の腹筋VRができたわけです。

何が言いたいのか

VRは安くなったことで「誰もが手が出せる」素晴らしいものになりました。
まだ【xR元年】と言われています。元年から次のステップに進むには【普及】が必要です。日本語コンテンツや開発に関するドキュメントは多くはありません。もっと開拓する人が必要だと感じました。

そこで遠目に見ているWeb界隈のあなたも例外ではありません。まずは、一緒にVR沼に浸かりませんか?

追記

NT札幌に参加したら、東京でもVRで腹筋するアプリを開発している人がいたと聞きました。Unityちゃんに踏んでもらえるとか…!その人と繋がりたい!

腹筋VRをリリースできたら、またリリースまでの記事書きます。

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

「大石泉すき」を公開鍵暗号標準(RSA)で暗号化/復号化する

この記事は「大石泉すき」アドベントカレンダー 4日目の記事となります。
4日目は、「大石泉すきを暗号化しよう!」とします。

何をしたいのか

「大石泉すき」という言葉を、一度暗号に直し、再度意味の通る文字に戻したい(復号したい)です。
今回は、公開鍵暗号を用いてこれを行っていこうと思います。

公開鍵暗号とは

仕組み自体は本筋ではないので、詳しくは下記の記事などを参照してください。

公開鍵暗号方式とは、暗号化と復号に別々の鍵を用いる暗号方式である。「非対称鍵暗号方式」とも呼ばれる。
~(中略)~
公開鍵暗号方式では、「暗号文を作り出す鍵」と「暗号文を元に戻す鍵」が異なる。暗号通信を行いたい人は、まず独自に2つの鍵のペアを作成する。同時に生成された一対の鍵のうち一方を公開鍵として公開し、他方を秘密鍵として厳重に管理する。送信者は受信者の公開鍵で暗号文を作成して送る。受信者は、自分の秘密鍵で受け取った暗号文を復号する。
引用元:@IT 公開鍵暗号方式とは

AliceとBobという名前の二者間の通信を例に出すと、

  • Bobは、Aliceに秘密の文字列(平文)を送りたい
  • Bobは、Aliceが公開している公開鍵(誰でも知ることが出来る)を使って、文字列を暗号化する
  • Aliceは、Bobから暗号化した文字列を受け取る
  • Aliceは、自分が持っている秘密鍵(Aliceしか知らない)で文字列を復号する
  • Aliceは、Bobから送られた秘密の文字列を受け取る

こんな風に、文字列の送受信が秘密に行えます。
(実際に利用される場合は、公開鍵が本当に正しいか、Aliceが所持しているかを証明する第三者機関が居たりしますが、今回は割愛します)

環境

今回、二者間の通信を表現するために、以下のような構成でソースコードを書きました。
C#で書く場合、AliceとBobでソリューションを分けてください。

Alice(メッセージ受信側)

  • 役割
    • HTTPサーバ上で公開鍵を公開する
    • 受信した暗号オブジェクトを復号する
  • 言語・フレームワーク
    • C# 8.0
    • ASP .net Core 3.0

Bob(メッセージ送信側)

  • 役割
    • Aliceが公開している公開鍵を入手し、秘密の文字列を暗号化する
    • 暗号化した文字列をAliceに送信する
  • 言語・フレームワーク
    • C# 8.0
    • .net Core 3.0 コンソールアプリケーション

実装

暗号化・復号化の実装

下記のライブラリを使いました。

使い方は下記の記事を参照。
更新日付を見る限り .net Framework用のライブラリを使っての紹介記事だと思われるが、.net Core用のライブラリでも同様の使い方で暗号化・復号化が可能。

使い方は大体こんな感じ。暗号文・復号文共にbyte[]配列で表される。

EncryptDecrypt.cs
        // 暗号化
        public static byte[] Encrypt(byte[]) bytes, string publickey)
        {
            // PEMフォーマットの公開鍵を読み込んで KeyParam を生成
            var publicKeyReader = new PemReader(new StringReader(publickey));
            var publicKeyParam = (AsymmetricKeyParameter)publicKeyReader.ReadObject();

            var RSA = new Pkcs1Encoding(new RsaEngine());
            // RSA暗号オブジェクトを初期化(第1引数trueは暗号化、falseは復号化)
            RSA.Init(true, publicKeyParam);

            // 暗号化対象のバイト列・長さを渡し、暗号化した結果のバイト列を受け取る
            byte[] encrypted = RSA.ProcessBlock(bytes, 0, bytes.Length);

            return encrypted;
        }

        // 復号化
        public byte[] Decrypto(byte[] cipher, string privateKey)
        {
            // PEMフォーマットの秘密鍵を読み込んで KeyParam を生成
            var privateKeyReader = new PemReader(new StringReader(privateKey));
            var privateKeyParam = (AsymmetricCipherKeyPair)privateKeyReader.ReadObject();

            var RSA = new Pkcs1Encoding(new RsaEngine());
            // RSA暗号オブジェクトを初期化(第1引数trueは暗号化、falseは復号化)
            RSA.Init(false, privateKeyParam.Private);

            // 復号化対象のバイト列・長さを渡し、復号化した結果のバイト列を受け取る
            var decrypto = RSA.ProcessBlock(cipher, 0, cipher.Length);
            return decrypto;
        }

BouncyCastle入手方法

  • ソリューションエクスプローラから「依存関係」を右クリック
  • 「NuGet パッケージの管理」をクリック > 「参照」をクリック
  • 「BouncyCastle.NetCore」を検索欄に入力し、出てきたものをインストール
    • 「BouncyCastle」は .net Framework用なので注意

鍵ペアの生成

動確検証用の公開鍵/暗号鍵のペアは、このサイトで生成しました。
https://travistidwell.com/jsencrypt/demo/index.html

個別実装

メッセージ受信側(HTTPサーバ)

RsaRemoteController.cs
        /// <summary>
        /// 暗号byte[]配列の復号
        /// </summary>
        /// <returns></returns>
        [HttpPost("")]
        public async System.Threading.Tasks.Task<string> DecryptoAsync()
        {
            byte[] encrypto;
            using (var ms = new MemoryStream(2048))
            {
                await Request.Body.CopyToAsync(ms);
                encrypto = ms.ToArray();  // returns base64 encoded string JSON result
            }

            var cert = new Cert();
            var decryptoByte = cert.Decrypto(encrypto, Cert.PRIVATE_KEY);

            // ログ
            _logger.LogInformation($"Decrypto [{Encoding.UTF8.GetString( decryptoByte )}]");
            return Encoding.UTF8.GetString( decryptoByte );
        }

        /// <summary>
        /// 公開鍵
        /// </summary>
        /// <returns></returns>
        [HttpGet("Alice/cert")]
        public string GetCert()
        {
            return Cert.PUBLIC_KEY;
        }
  • {ルートパス}/Alice/certに、Getリクエスト:公開鍵を生のstring型で返却する
  • ルートパスに、bodyに生のbyte配列(暗号文)を添付しPostリクエスト:リクエストのbyte配列を復号化したbyte配列を、文字列に変換する。
    • 今回は、どのように変換したのか分かるように、stringで結果を返却する実装にした
RsaRemoteController.cs
    class Cert
    {
        // 実際の運用時はハードコーディングせず、セキュアな場所に保存し逐一読み込むこと
        // Generate by https://travistidwell.com/jsencrypt/demo/index.html
        internal static readonly string PUBLIC_KEY = @"(略)";

        // 実際の運用時はハードコーディングせず、セキュアな場所に保存し逐一読み込むこと
        // Generate by https://travistidwell.com/jsencrypt/demo/index.html
        internal static readonly string PRIVATE_KEY =  @"(略)";

        internal Pkcs1Encoding RSA { get; }

        public Cert()
        {
            RSA = new Pkcs1Encoding(new RsaEngine());
        }

        /// <summary>
        /// 対称鍵暗号で暗号文を復号する
        /// </summary>
        /// <param name="cipher">平文の文字列</param>
        /// <param name="privatekey">秘密鍵</param>
        /// <returns>復号された文字列</returns>
        public byte[] Decrypto(byte[] cipher, string privateKey)
        {
            // PEMフォーマットの秘密鍵を読み込んで KeyParam を生成
            var privateKeyReader = new PemReader(new StringReader(privateKey));
            var privateKeyParam = (AsymmetricCipherKeyPair)privateKeyReader.ReadObject();

            var RSA = new Pkcs1Encoding(new RsaEngine());
            // RSA暗号オブジェクトを初期化(第1引数trueは暗号化、falseは復号化)
            RSA.Init(false, privateKeyParam.Private);

            // 復号化対象のバイト列・長さを渡し、復号化した結果のバイト列を受け取る
            var decrypto = RSA.ProcessBlock(cipher, 0, cipher.Length);
            return decrypto;
        }
    }
  • 単純な復号化処理。公開鍵・秘密鍵は絶対にハードコーディングしないこと

メッセージ送信側(コンソールアプリ)

Bob.cs
        static void Main()
        {
            // HttpClientを使うための準備。今回はあまり関係ない
            // HTTPConnectionFactoryを使うため、DI設定を行う
            var serviceCollection = new ServiceCollection()
                .AddHttpClient()                                         // IHttpClientFactoryの依存設定
                .AddSingleton<IHttpConnection, HttpConnectionSample>()    // IHTTPConnectionの依存設定
                .BuildServiceProvider();

            // DI設定済みのIHttpConnectionを実装したクラスを取得
            var connector = serviceCollection.GetService<IHttpConnection>();


            // 大石泉すき
            string plainText = "大石泉すき";
            Console.WriteLine($"PlainText\r\n{plainText}\r\n");

            // サーバから公開鍵を取得する
            // SendGetメソッドの中身はただのHttpClient.GetAsyncです
            var publicKey = connector.SendGet($"https://{メッセージ受信側HTTPサーバのIP:Port}/Alice/cert").Result;

            // RSA暗号標準オブジェクト(PKCS#1)を生成
            var rsa = new Pkcs1Encoding(new RsaEngine());

            // 暗号化
            var encrypted = Encrypt(plainText, publicKey, rsa);

            // byte配列は化けるのでBase64でエンコードしておく
            Console.WriteLine($"Encrypted(Base64 Encoded)\r\n{Convert.ToBase64String(encrypted)}\r\n");

            // 暗号文(配列)を復号化するべく、サーバに暗号文を送信
            // SendPostメソッドの中身はただのHttpClient.PostAsyncです
            var decrypted = connector.SendPost($"https://{メッセージ受信側HTTPサーバのIP:Port}/rsaremote/", encrypted).Result;

            // サーバで復号化した結果を表示
            Console.WriteLine($"Decrypted\r\n{decrypted}\r\n");
        }
  • 公開鍵取ってきてーの暗号化してーの復号化してもらいーののコントローラクラス
  • 「大石泉すき」を知っているのはBobだけ。暗号化してAliceに伝わるだろうか。
Bob.cs
        /// <summary>
        /// 公開鍵で文字列を暗号化する
        /// </summary>
        /// <param name="text">平文の文字列</param>
        /// <param name="publickey">Pem形式の公開鍵</param>
        /// <returns>暗号化されたByte</returns>
        public static byte[] Encrypt(string text, string publickey, Pkcs1Encoding rsa)
        {
            var bytes = Encoding.UTF8.GetBytes(text);

            // PEMフォーマットの公開鍵を読み込んで KeyParam を生成
            var publicKeyReader = new PemReader(new StringReader(publickey));
            var publicKeyParam = (AsymmetricKeyParameter)publicKeyReader.ReadObject();

            // RSA暗号オブジェクトを初期化(第1引数 true は「暗号化」を示す)
            rsa.Init(true, publicKeyParam);

            // 対象のバイト列を渡し暗号化した結果のバイト列を受け取る
            byte[] encrypted = rsa.ProcessBlock(bytes, 0, bytes.Length);

            return encrypted;
        }
  • 単純な暗号化処理。公開鍵・秘密鍵は今回クライアントは持っていない

動作確認

サーバ側を起動させた状態で、クライアント側を実行

クライアント側
PlainText
大石泉すき

Encrypted(Base64 Encoded)
W2joxjgxL+Q6CtCYaSGCzpx4fJYspCb7KRI/2Ddlnt//70o0R/039Hx6R2fywqCEF0Q21MqpF4/BbjzDM8lAKJgPEIFx5Gp2kYBO08B6bjYdrhSPgIeWEIj7ulwZPO4TD+G5bGwrZn/ogapQfUbTY748B49h1/d4t0IowxRartc=

Decrypted
大石泉すき
サーバ側
RsaServer.Controllers.RsaRemoteController: Information: Decrypto [大石泉すき]

参考資料

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

「大石泉すき」を公開鍵暗号標準(RSA)で暗号化/復号する

この記事は「大石泉すき」アドベントカレンダー 4日目の記事となります。
4日目は、「大石泉すきを暗号化しよう!」とします。

2019/12/04 21:37追記
当初「復号」を「復号化」と記載していたため、修正いたしました。ご指摘いただき、ありがとうございます。

何をしたいのか

「大石泉すき」という言葉を、一度暗号に直し、再度意味の通る文字に戻したい(復号したい)です。
今回は、公開鍵暗号を用いてこれを行っていこうと思います。

公開鍵暗号とは

仕組み自体は本筋ではないので、詳しくは下記の記事などを参照してください。

公開鍵暗号方式とは、暗号化と復号に別々の鍵を用いる暗号方式である。「非対称鍵暗号方式」とも呼ばれる。
~(中略)~
公開鍵暗号方式では、「暗号文を作り出す鍵」と「暗号文を元に戻す鍵」が異なる。暗号通信を行いたい人は、まず独自に2つの鍵のペアを作成する。同時に生成された一対の鍵のうち一方を公開鍵として公開し、他方を秘密鍵として厳重に管理する。送信者は受信者の公開鍵で暗号文を作成して送る。受信者は、自分の秘密鍵で受け取った暗号文を復号する。
引用元:@IT 公開鍵暗号方式とは

AliceとBobという名前の二者間の通信を例に出すと、

  • Bobは、Aliceに秘密の文字列(平文)を送りたい
  • Bobは、Aliceが公開している公開鍵(誰でも知ることが出来る)を使って、文字列を暗号化する
  • Aliceは、Bobから暗号化した文字列を受け取る
  • Aliceは、自分が持っている秘密鍵(Aliceしか知らない)で文字列を復号する
  • Aliceは、Bobから送られた秘密の文字列を受け取る

こんな風に、文字列の送受信が秘密に行えます。
(実際に利用される場合は、公開鍵が本当に正しいか、Aliceが所持しているかを証明する第三者機関が居たりしますが、今回は割愛します)

環境

今回、二者間の通信を表現するために、以下のような構成でソースコードを書きました。
C#で書く場合、AliceとBobでソリューションを分けてください。

Alice(メッセージ受信側)

  • 役割
    • HTTPサーバ上で公開鍵を公開する
    • 受信した暗号オブジェクトを復号する
  • 言語・フレームワーク
    • C# 8.0
    • ASP .net Core 3.0

Bob(メッセージ送信側)

  • 役割
    • Aliceが公開している公開鍵を入手し、秘密の文字列を暗号化する
    • 暗号化した文字列をAliceに送信する
  • 言語・フレームワーク
    • C# 8.0
    • .net Core 3.0 コンソールアプリケーション

実装

暗号化・復号の実装

下記のライブラリを使いました。

使い方は下記の記事を参照。
更新日付を見る限り .net Framework用のライブラリを使っての紹介記事だと思われるが、.net Core用のライブラリでも同様の使い方で暗号化・復号が可能。

使い方は大体こんな感じ。暗号文・復号文共にbyte[]配列で表される。

EncryptDecrypt.cs
        // 暗号化
        public static byte[] Encrypt(byte[]) bytes, string publickey)
        {
            // PEMフォーマットの公開鍵を読み込んで KeyParam を生成
            var publicKeyReader = new PemReader(new StringReader(publickey));
            var publicKeyParam = (AsymmetricKeyParameter)publicKeyReader.ReadObject();

            var RSA = new Pkcs1Encoding(new RsaEngine());
            // RSA暗号オブジェクトを初期化(第1引数trueは暗号化、falseは復号)
            RSA.Init(true, publicKeyParam);

            // 暗号化対象のバイト列・長さを渡し、暗号化した結果のバイト列を受け取る
            byte[] encrypted = RSA.ProcessBlock(bytes, 0, bytes.Length);

            return encrypted;
        }

        // 復号
        public byte[] Decrypto(byte[] cipher, string privateKey)
        {
            // PEMフォーマットの秘密鍵を読み込んで KeyParam を生成
            var privateKeyReader = new PemReader(new StringReader(privateKey));
            var privateKeyParam = (AsymmetricCipherKeyPair)privateKeyReader.ReadObject();

            var RSA = new Pkcs1Encoding(new RsaEngine());
            // RSA暗号オブジェクトを初期化(第1引数trueは暗号化、falseは復号)
            RSA.Init(false, privateKeyParam.Private);

            // 復号対象のバイト列・長さを渡し、復号した結果のバイト列を受け取る
            var decrypto = RSA.ProcessBlock(cipher, 0, cipher.Length);
            return decrypto;
        }

BouncyCastle入手方法

  • ソリューションエクスプローラから「依存関係」を右クリック
  • 「NuGet パッケージの管理」をクリック > 「参照」をクリック
  • 「BouncyCastle.NetCore」を検索欄に入力し、出てきたものをインストール
    • 「BouncyCastle」は .net Framework用なので注意

鍵ペアの生成

動確検証用の公開鍵/暗号鍵のペアは、このサイトで生成しました。
https://travistidwell.com/jsencrypt/demo/index.html

個別実装

メッセージ受信側(HTTPサーバ)

RsaRemoteController.cs
        /// <summary>
        /// 暗号byte[]配列の復号
        /// </summary>
        /// <returns></returns>
        [HttpPost("")]
        public async System.Threading.Tasks.Task<string> DecryptoAsync()
        {
            byte[] encrypto;
            using (var ms = new MemoryStream(2048))
            {
                await Request.Body.CopyToAsync(ms);
                encrypto = ms.ToArray();  // returns base64 encoded string JSON result
            }

            var cert = new Cert();
            var decryptoByte = cert.Decrypto(encrypto, Cert.PRIVATE_KEY);

            // ログ
            _logger.LogInformation($"Decrypto [{Encoding.UTF8.GetString( decryptoByte )}]");
            return Encoding.UTF8.GetString( decryptoByte );
        }

        /// <summary>
        /// 公開鍵
        /// </summary>
        /// <returns></returns>
        [HttpGet("Alice/cert")]
        public string GetCert()
        {
            return Cert.PUBLIC_KEY;
        }
  • {ルートパス}/Alice/certに、Getリクエスト:公開鍵を生のstring型で返却する
  • ルートパスに、bodyに生のbyte配列(暗号文)を添付しPostリクエスト:リクエストのbyte配列を復号したbyte配列を、文字列に変換する。
    • 今回は、どのように変換したのか分かるように、stringで結果を返却する実装にした
RsaRemoteController.cs
    class Cert
    {
        // 実際の運用時はハードコーディングせず、セキュアな場所に保存し逐一読み込むこと
        // Generate by https://travistidwell.com/jsencrypt/demo/index.html
        internal static readonly string PUBLIC_KEY = @"(略)";

        // 実際の運用時はハードコーディングせず、セキュアな場所に保存し逐一読み込むこと
        // Generate by https://travistidwell.com/jsencrypt/demo/index.html
        internal static readonly string PRIVATE_KEY =  @"(略)";

        internal Pkcs1Encoding RSA { get; }

        public Cert()
        {
            RSA = new Pkcs1Encoding(new RsaEngine());
        }

        /// <summary>
        /// 対称鍵暗号で暗号文を復号する
        /// </summary>
        /// <param name="cipher">平文の文字列</param>
        /// <param name="privatekey">秘密鍵</param>
        /// <returns>復号された文字列</returns>
        public byte[] Decrypto(byte[] cipher, string privateKey)
        {
            // PEMフォーマットの秘密鍵を読み込んで KeyParam を生成
            var privateKeyReader = new PemReader(new StringReader(privateKey));
            var privateKeyParam = (AsymmetricCipherKeyPair)privateKeyReader.ReadObject();

            var RSA = new Pkcs1Encoding(new RsaEngine());
            // RSA暗号オブジェクトを初期化(第1引数trueは暗号化、falseは復号)
            RSA.Init(false, privateKeyParam.Private);

            // 復号対象のバイト列・長さを渡し、復号した結果のバイト列を受け取る
            var decrypto = RSA.ProcessBlock(cipher, 0, cipher.Length);
            return decrypto;
        }
    }
  • 単純な復号処理。公開鍵・秘密鍵は絶対にハードコーディングしないこと

メッセージ送信側(コンソールアプリ)

Bob.cs
        static void Main()
        {
            // HttpClientを使うための準備。今回はあまり関係ない
            // HTTPConnectionFactoryを使うため、DI設定を行う
            var serviceCollection = new ServiceCollection()
                .AddHttpClient()                                         // IHttpClientFactoryの依存設定
                .AddSingleton<IHttpConnection, HttpConnectionSample>()    // IHTTPConnectionの依存設定
                .BuildServiceProvider();

            // DI設定済みのIHttpConnectionを実装したクラスを取得
            var connector = serviceCollection.GetService<IHttpConnection>();


            // 大石泉すき
            string plainText = "大石泉すき";
            Console.WriteLine($"PlainText\r\n{plainText}\r\n");

            // サーバから公開鍵を取得する
            // SendGetメソッドの中身はただのHttpClient.GetAsyncです
            var publicKey = connector.SendGet($"https://{メッセージ受信側HTTPサーバのIP:Port}/Alice/cert").Result;

            // RSA暗号標準オブジェクト(PKCS#1)を生成
            var rsa = new Pkcs1Encoding(new RsaEngine());

            // 暗号化
            var encrypted = Encrypt(plainText, publicKey, rsa);

            // byte配列は化けるのでBase64でエンコードしておく
            Console.WriteLine($"Encrypted(Base64 Encoded)\r\n{Convert.ToBase64String(encrypted)}\r\n");

            // 暗号文(配列)を復号するべく、サーバに暗号文を送信
            // SendPostメソッドの中身はただのHttpClient.PostAsyncです
            var decrypted = connector.SendPost($"https://{メッセージ受信側HTTPサーバのIP:Port}/rsaremote/", encrypted).Result;

            // サーバで復号した結果を表示
            Console.WriteLine($"Decrypted\r\n{decrypted}\r\n");
        }
  • 公開鍵取ってきてーの暗号化してーの復号してもらいーののコントローラクラス
  • 「大石泉すき」を知っているのはBobだけ。暗号化してAliceに伝わるだろうか。
Bob.cs
        /// <summary>
        /// 公開鍵で文字列を暗号化する
        /// </summary>
        /// <param name="text">平文の文字列</param>
        /// <param name="publickey">Pem形式の公開鍵</param>
        /// <returns>暗号化されたByte</returns>
        public static byte[] Encrypt(string text, string publickey, Pkcs1Encoding rsa)
        {
            var bytes = Encoding.UTF8.GetBytes(text);

            // PEMフォーマットの公開鍵を読み込んで KeyParam を生成
            var publicKeyReader = new PemReader(new StringReader(publickey));
            var publicKeyParam = (AsymmetricKeyParameter)publicKeyReader.ReadObject();

            // RSA暗号オブジェクトを初期化(第1引数 true は「暗号化」を示す)
            rsa.Init(true, publicKeyParam);

            // 対象のバイト列を渡し暗号化した結果のバイト列を受け取る
            byte[] encrypted = rsa.ProcessBlock(bytes, 0, bytes.Length);

            return encrypted;
        }
  • 単純な暗号化処理。公開鍵・秘密鍵は今回クライアントは持っていない

動作確認

サーバ側を起動させた状態で、クライアント側を実行

クライアント側
PlainText
大石泉すき

Encrypted(Base64 Encoded)
W2joxjgxL+Q6CtCYaSGCzpx4fJYspCb7KRI/2Ddlnt//70o0R/039Hx6R2fywqCEF0Q21MqpF4/BbjzDM8lAKJgPEIFx5Gp2kYBO08B6bjYdrhSPgIeWEIj7ulwZPO4TD+G5bGwrZn/ogapQfUbTY748B49h1/d4t0IowxRartc=

Decrypted
大石泉すき
サーバ側
RsaServer.Controllers.RsaRemoteController: Information: Decrypto [大石泉すき]

参考資料

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

【C#】トップ

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

UniRx を用いた PlayerPrefs との値のやりとりをする汎用 BaseModel

どうやって使うの?何に使うの?

PlayerPrefs を使って、ゲームのデータを永続化したりするのに、
BaseModel を継承した Model を作って、やりとりを簡潔にしましょう。
というもの。

以下がその BaseModel。

やってることは
1. Register メソッドで登録された ReactiveProperty を監視、値の変更があったら PlayerPrefs に保存
2. 次回 Awake 時に PlayerPrefs の情報と、 Register された情報をもとに、インスタンスと、そのデータを復元

BaseModel のコードの後に、その使用例を載せておきます。

BaseModel
using System.Collections.Generic;
using UnityEngine;

using System.Linq;

using UniRx;

namespace Models
{
    abstract public class BaseModel : MonoBehaviour
    {
        public class Model
        {
            public int id;

            public List<FloatReactiveProperty> floatAttrs = new List<FloatReactiveProperty>();
            public List<IntReactiveProperty> intAttrs = new List<IntReactiveProperty>();
            public List<StringReactiveProperty> stringAttrs = new List<StringReactiveProperty>();

            protected void Register(IntReactiveProperty attr)
            {
                intAttrs.Add(attr);
            }
            protected void Register(FloatReactiveProperty attr)
            {
                floatAttrs.Add(attr);
            }
            protected void Register(StringReactiveProperty attr)
            {
                stringAttrs.Add(attr);
            }

            virtual protected void InitInstance() { }

            virtual protected void RegisterAttributes() { }

            protected string modelName
            {
                get { return this.GetType().ToString().Split(new char[] { '+' })[0]; }
            }

            public Model()
            {
                InitInstance();
                RegisterAttributes();

                floatAttrs.ForEach(a =>
                {
                    a.AsObservable()
                        .Skip(1)
                        .Do(_ => Debug.Log(_))
                        .Do(value => PlayerPrefs.SetFloat(modelName + floatAttrs.IndexOf(a).ToString() + "float" + id, value))
                        .Subscribe(_ => PlayerPrefs.Save());
                });
                intAttrs.ForEach(a =>
                {
                    a.AsObservable()
                        .Skip(1)
                        .Do(_ => Debug.Log(_))
                        .Do(value => PlayerPrefs.SetInt(modelName + intAttrs.IndexOf(a).ToString() + "int" + id, value))
                        .Subscribe(_ => PlayerPrefs.Save());
                });
                stringAttrs.ForEach(a =>
                {
                    a.AsObservable()
                        .Skip(1)
                        .Do(_ => Debug.Log(_))
                        .Do(value => PlayerPrefs.SetString(modelName + stringAttrs.IndexOf(a).ToString() + "string" + id, value))
                        .Subscribe(_ => PlayerPrefs.Save());
                });

                PlayerPrefs.SetInt(modelName + "count", id + 1);
            }
        }

        virtual protected Model Instantiate()
        {
            return new Model();
        }

        public void Awake()
        {
            string modelName = this.GetType().ToString();

            int instanceCount = PlayerPrefs.HasKey(modelName + "count") ? PlayerPrefs.GetInt(modelName + "count") : 0;
            Enumerable.Range(0, instanceCount).ToList().ForEach(i =>
            {
                Model instance = Instantiate();

                instance.floatAttrs.ForEach(a =>
                {
                    a.Value = PlayerPrefs.GetFloat(modelName + instance.floatAttrs.IndexOf(a).ToString() + "float" + instance.id);
                });
                instance.intAttrs.ForEach(a =>
                {
                    a.Value = PlayerPrefs.GetInt(modelName + instance.intAttrs.IndexOf(a).ToString() + "int" + instance.id);
                });
                instance.stringAttrs.ForEach(a =>
                {
                    a.Value = PlayerPrefs.GetString(modelName + instance.stringAttrs.IndexOf(a).ToString() + "string" + instance.id);
                });
            });
        }
    }
}

使用例

Score(BaseModelの継承先)
using System.Linq;

using UniRx;

namespace Models
{
    public class Score : BaseModel
    {
        new public class Model : BaseModel.Model
        {
            public IntReactiveProperty score = new IntReactiveProperty();

            public bool isHighScore { get { return score.Value > 100; } }

            override protected void RegisterAttributes()
            {
                Register(score);
            }

            // 共通部分
            override protected void InitInstance()
            {
                id = instances.Count;
                instances.Add(this);
            }
        }

        static ReactiveCollection<Model> instances = new ReactiveCollection<Model>();

        public static ReactiveCollection<Model> All() { return new ReactiveCollection<Model>(instances); }
        public static int count { get { return All().Count; } }
        public static Model First() { return instances.First(); }

        override protected BaseModel.Model Instantiate()
        {
            return new Model();
        }

        new public void Awake()
        {
            base.Awake();

            if (All().Count == 0)
            {
                // new Model();
            }
        }
    }
}

Presenter
using UnityEngine;

using Models;

public class Presenter : MonoBehaviour
{
    void Start()
    {
        if (Score.count == 0)
        {
            Score.Model score = new Score.Model();
        }

        Debug.Log(Score.All().Count);
        Debug.Log(Score.First().score.Value);
    }
}

まだ改善の余地がたくさんあると思うので、まさかりください。
C# 全然わからん。

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

UniRx を用いた PlayerPref との値のやりとりをする汎用 BaseModel

どうやって使うの?何に使うの?

PlayerPref を使って、ゲームのデータを永続化したりするのに、
BaseModel を継承した Model を作って、やりとりを簡潔にしましょう。
というもの。

以下がその BaseModel。

やってることは
1. Register メソッドで登録された ReactiveProperty を監視、値の変更があったら PlayerPref に保存
2. 次回 Awake 時に PlayerPref の情報と、 Register された情報をもとに、インスタンスと、そのデータを復元

BaseModel のコードの後に、その使用例を載せておきます。

BaseModel
using System.Collections.Generic;
using UnityEngine;

using System.Linq;

using UniRx;

namespace Models
{
    abstract public class BaseModel : MonoBehaviour
    {
        public class Model
        {
            public int id;

            public List<FloatReactiveProperty> floatAttrs = new List<FloatReactiveProperty>();
            public List<IntReactiveProperty> intAttrs = new List<IntReactiveProperty>();
            public List<StringReactiveProperty> stringAttrs = new List<StringReactiveProperty>();

            protected void Register(IntReactiveProperty attr)
            {
                intAttrs.Add(attr);
            }
            protected void Register(FloatReactiveProperty attr)
            {
                floatAttrs.Add(attr);
            }
            protected void Register(StringReactiveProperty attr)
            {
                stringAttrs.Add(attr);
            }

            virtual protected void InitInstance() { }

            virtual protected void RegisterAttributes() { }

            protected string modelName
            {
                get { return this.GetType().ToString().Split(new char[] { '+' })[0]; }
            }

            public Model()
            {
                InitInstance();
                RegisterAttributes();

                floatAttrs.ForEach(a =>
                {
                    a.AsObservable()
                        .Skip(1)
                        .Do(_ => Debug.Log(_))
                        .Do(value => PlayerPrefs.SetFloat(modelName + floatAttrs.IndexOf(a).ToString() + "float" + id, value))
                        .Subscribe(_ => PlayerPrefs.Save());
                });
                intAttrs.ForEach(a =>
                {
                    a.AsObservable()
                        .Skip(1)
                        .Do(_ => Debug.Log(_))
                        .Do(value => PlayerPrefs.SetInt(modelName + intAttrs.IndexOf(a).ToString() + "int" + id, value))
                        .Subscribe(_ => PlayerPrefs.Save());
                });
                stringAttrs.ForEach(a =>
                {
                    a.AsObservable()
                        .Skip(1)
                        .Do(_ => Debug.Log(_))
                        .Do(value => PlayerPrefs.SetString(modelName + stringAttrs.IndexOf(a).ToString() + "string" + id, value))
                        .Subscribe(_ => PlayerPrefs.Save());
                });

                PlayerPrefs.SetInt(modelName + "count", id + 1);
            }
        }

        virtual protected Model Instantiate()
        {
            return new Model();
        }

        public void Awake()
        {
            string modelName = this.GetType().ToString();

            int instanceCount = PlayerPrefs.HasKey(modelName + "count") ? PlayerPrefs.GetInt(modelName + "count") : 0;
            Enumerable.Range(0, instanceCount).ToList().ForEach(i =>
            {
                Model instance = Instantiate();

                instance.floatAttrs.ForEach(a =>
                {
                    a.Value = PlayerPrefs.GetFloat(modelName + instance.floatAttrs.IndexOf(a).ToString() + "float" + instance.id);
                });
                instance.intAttrs.ForEach(a =>
                {
                    a.Value = PlayerPrefs.GetInt(modelName + instance.intAttrs.IndexOf(a).ToString() + "int" + instance.id);
                });
                instance.stringAttrs.ForEach(a =>
                {
                    a.Value = PlayerPrefs.GetString(modelName + instance.stringAttrs.IndexOf(a).ToString() + "string" + instance.id);
                });
            });
        }
    }
}

使用例

Score(BaseModelの継承先)
using System.Linq;

using UniRx;

namespace Models
{
    public class Score : BaseModel
    {
        new public class Model : BaseModel.Model
        {
            public IntReactiveProperty score = new IntReactiveProperty();

            public bool isHighScore { get { return score.Value > 100; } }

            override protected void RegisterAttributes()
            {
                Register(score);
            }

            // 共通部分
            override protected void InitInstance()
            {
                id = instances.Count;
                instances.Add(this);
            }
        }

        static ReactiveCollection<Model> instances = new ReactiveCollection<Model>();

        public static ReactiveCollection<Model> All() { return new ReactiveCollection<Model>(instances); }
        public static int count { get { return All().Count; } }
        public static Model First() { return instances.First(); }

        override protected BaseModel.Model Instantiate()
        {
            return new Model();
        }

        new public void Awake()
        {
            base.Awake();

            if (All().Count == 0)
            {
                // new Model();
            }
        }
    }
}

Presenter
using UnityEngine;

using Models;

public class Presenter : MonoBehaviour
{
    void Start()
    {
        if (Score.count == 0)
        {
            Score.Model score = new Score.Model();
        }

        Debug.Log(Score.All().Count);
        Debug.Log(Score.First().score.Value);
    }
}

まだ改善の余地がたくさんあると思うので、まさかりください。
C# 全然わからん。

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

IDEなしでC#のHelloWorld

VisualStudioなどのIDEなしでC#のHelloWorldやってみます。

好きなところにソースコードを用意します。

ファイル名: helloworld.cs

using System;
public static class HelloWorld
{
  public static void Main()
  {
    Console.WriteLine("Hello C#!");
  }
}

ソースコードをコンパイルしてexe形式にします。

C#の環境を確認

.NET Frameworkがインストールされていれば以下の場所にバージョンごとの.NET Frameworkが存在します。

32bit Windowsの場合
%windir%\Microsoft.NET\Framework

64bit Windowsの場合
%windir%\Microsoft.NET\Framework64

こんな感じ
image.png

バージョンのディレクトリの配下にcsc.exeというコンパイラが存在します。
image.png

コンパイルしてみる

> C:\Windows\Microsoft.NET\Framework64\v4.0.30319\csc.exe helloworld.cs
Microsoft (R) Visual C# Compiler version 4.8.3752.0
for C# 5
Copyright (C) Microsoft Corporation. All rights reserved.

This compiler is provided as part of the Microsoft (R) .NET Framework, but only supports language versions up to C# 5, which is no longer the latest version. For compilers that support newer versions of the C# programming language, see http://go.microsoft.com/fwlink/?LinkID=533240

image.png

コンパイルエラーはこんな感じに出ます。

helloworld.cs(6,35): error CS1002: ; が必要です。

exe実行してみる

> .\helloworld.exe
Hello C#!
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

初心者がC#でAtCoderデビューするためのVSProjectテンプレート

この記事は NSSOL Advent Calendar 2019 の4日目の記事です。

TL;DR

https://github.com/sekikatsu36/AtCoder
↑こちらをfork

概要

この記事では「C#使いの競技プログラミング未経験者が、C#で初AtCoderに挑むハードルを下げる」ことを目的に、ベースとなるVSプロジェクトの共有&紹介を行います。

コンテストを続けるための便利TIPSは他にも山ほどあるので、気になればぜひ調べててみてください。(末尾に参考URLを記載)

背景

AtCoderはいわゆる競技プログラミングコンテストのサービスです。
今更ですが、私も最近AtCoderを始めまして。(まだ茶色レベルの新米です)
初回にC# + Visual Studioで参戦した際、「事前にVSプロジェクトとか準備した方がいいんだろうなぁ」と思ってはいたのですが、それを怠ってぶっつけ本番で突撃して見事なまでに惨敗

何度か再戦し、最近それっぽいプロジェクトが出来上がったので、この場を借りて共有します。
「競プロをC#で!」みたいな記事は世に溢れてますが、VSプロジェクトは転がっていないようだったので、この期に配布。
私のような悔しい思いをする方が一人でも減ればと。

なんでC#?

C#が好きだから

その一言に尽きるんじゃないでしょうか。

一応上げると、以下のようなメリットがあります。

  • LINQが便利(ただしOrderByは使ってはならぬ。遅すぎ)
  • Visual Studioが神IDE
  • ジェネリックが使える

ただ、ぶっちゃけ、AtCoderでワザワザC#を選ぶ必要はないです。
C#には以下のようなデメリットがあるので、PythonやC++が得意ならそっちの方が良いと思います。

  • 競プロで便利なコレクション操作の一部が標準装備されていない(vs Python)
  • 遅い (vs C++)
  • 人口が少ない

できあがったもの

以下にあります。
適宜forkしてください。

https://github.com/sekikatsu36/AtCoder

使い方

A~Fまで、すべての問題が一プロジェクトに収まってるので、masterからブランチを切れば即コンテストを始められます。
ライブラリを作りたいときは、それ用のクラスを適当に追加すれば良いので、管理は楽だと思います。

コンテスト中にエントリクラスやライブラリに変更を加えた際は、そのファイルだけmasterにmergeすれば次のコンテストですぐ使えます。

git checkout abc146 -- .\AtCoder\Program.cs

問題を切り替えるときはProgram.cs内で呼び出すクラスを書き換える必要があります。
そこはイケてないのですが、良い方法が思いつかず。
まぁ何かしらの方法で切り替える必要はあると思うので、今はこれで妥協してます。
妙案あったらシェアいただけると嬉しいです。

中身の紹介

無限ループで繰り返し実行できる

まぁ言うまでもなく。
これくらいパパっと実装できるとは思いますが、事前に用意してあるとストレスが全然違います。
初心者が一番最初に欲しくなる機能だと思います。

スタートアップオブジェクトを指定して、コピペで提出できるようにする

すべての問題をそのまま一プロジェクトにまとめると、エントリポイントの問題が出てきます。
そのまま各問のクラスにMainメソッドを追加すると「エントリポイントが複数定義されてるぞ」とVisual Studioの怒りを買ってしまいます。
かといって、Main以外の名前にしてしまうと、コードをコピペして提出した時にAtCoderから「エントリポイントが見つからない」と心無いことを言われてしまいます。
プロジェクトを分けるのもコードを共通化しにくい。

image.png

そんな時はプロジェクトのプロパティから[アプリケーション]>[スタートアップオブジェクト]を設定しましょう。
複数のクラスでMainメソッドが定義されていても、これを指定すれば普通に実行できます。

各問の最初によくつかう標準入力処理を書いておく

AtCoderは、テストケースを標準入力から受け取る仕様です。
競プロ勢からすれば標準入出力なんて手慣れたものでしょうが、初心者は一瞬手が止まると思います。(標準入力とか普段使わないし)
出鼻を挫かれるのも何なので、最初から載せておきます。実際の問題に合わせて、追加・削除します。

// 文字列の入力
string s = Console.ReadLine();

// 整数の入力
long n = long.Parse(Console.ReadLine());

// 文字列配列の入力
string[] inputStrArray = Console.ReadLine().Split(' ');

// 整数配列の入力
long[] inputLongArray = Console.ReadLine().Split(' ').Select(i => long.Parse(i)).ToArray();

ちなみに、intを使ってメモリをケチる必要はないです。
これまで10回弱参加しましたが、メモリが問題になったことはほぼなかったので。
予期せぬ桁溢れの方がずっと怖いので、脳みそを止めてlongを使います。(メモリは溢れてから考えるスタイル)

なお、「入力を高速化」みたいなTIPSも世には転がってますが、最初はそこまで神経質になる必要はないかな、と。

各問の最後に標準出力処理を書いておく

AtCoderは、結果を標準出力から返す仕様です。
入力同様、出力処理も記載しておきます。

C#のConsole.WriteLineは遅いので、Mainの開始時にAutoFlushをfalseにセットして、

var sw = new System.IO.StreamWriter(Console.OpenStandardOutput()) { AutoFlush = false };
Console.SetOut(sw);

最後にFlush処理を行います。

Console.Out.Flush();

こうすれば少しだけ高速化できます。(と偉い人が言っていた。自分で速度計測したわけではないです orz)
結果の出力をFlushの前に書くことだけ意識的に。

オーバーフローを例外化する

初心者がやらかすミスの一つがオーバーフロー(桁溢れ)だと思います。
私も何度も痛い目を見ました。

前項の通りintを使わないことである程度回避できますが、それでも食らうときは食らうので、ローカル環境ではオーバーフローが発生したら例外が出るようにしましょう。
プロジェクトのプロパティで、[ビルド]>[詳細設計]>[演算のオーバーフロー及びアンダーフローのチェック]をONにします。

image.png

ライブラリを用意しておく(任意)

ライブラリを持っておくと、解くスピードがぐっと上がる可能性があります。
が、個人的に、

  • 事前に揃えておいても、初心者はどうせ使いどころが分からない
  • 一度自分で実装しようとしてみた方が勉強になる

という理由から、特にはじめは意識しなくてもいいと思います。

とはいえ、C#でAtCoderをやっていれば、今に以下のようなライブラリが欲しくなってくると思うので、「欲しいな」と思ったタイミングでライブラリ化しましょう。

  • 優先度付きキュー。合わせてダイクストラも。
  • UpperBound, LowerBound
  • gcd(最大公約数)
  • 素因数分解
  • nCr

使うときはLibを直接呼び出すのではなく、各問のクラスにコピペするよう注意。

その他TIPS

他にも、以下のようなことをやっておくと、便利かもしれません

  • コードスニペットを覚えておく https://qiita.com/Kosen-amai/items/248e44dff958be901a84
    • for/foreach/cwはよく使います
    • 他にもほしいものがあれば、Visual Studioを使っている利点をフル活用して、事前登録しておくよろし
  • 問題のURLを控えておく
    • コンテスト開始時はアクセスが集中するのでページ表示が遅いです
    • 問題の一覧 → A問題表示、と画面遷移すると若干待たされるので、最初からA問題に飛べるリンクを用意しておくと気が楽です(その数秒を争うレベルなのか?という疑問は捨て置く)
    • e.g. https://atcoder.jp/contests/abc141/tasks/abc141_a

参考リンク

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

VisualStudioの拡張機能をうまく活用すると人生が楽になる 【VisualStudio】【Unity】

gif.gif
floatの変数0.0fで初期化、stringはstring.Emptyで初期化...など
コードを書くときある程度プロジェクト毎にお約束のテンプレートがあったりすると思うのですが、何回も書くのが少し面倒になる時があります。

VisualStudioの拡張機能SnippetDesignerを利用すると自分の好きなコードのテンプレートが作れ、その問題が解決されます。 

実装手順

1.[ツール] ->[拡張機能と更新プログラム] ->[オンライン]からSnippetDesignerをインストール

snippet.PNG

2.テンプレート化したいコードを選択して右クリックからExportasSnippetを選択

prop.gif

3.最後に名前など書き換える必要のある部分を$$で囲みShotcut名を付ければ完成!

shotcut.PNG

使用した結果

gif2.gif

早い・・

よく使うコーディングに利用すれば大幅に効率を上げることが出来そうです。
プロジェクト内でしか利用できないショートカットを作ったとしても後で編集や削除が容易にできるのでどんどん活用していきましょう!

若者のhoge離れが始まっているようだがhogeで説明し続ける。

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

C#でWindows標準の音声認識で遊んでみた

Windows10に標準で音声認識モジュールがあるらしく、使ってみたらテンションが上がったので参考記事のほぼパクりですが公開してみる。

参考記事から、動作に必要な部分だけ取り出して、VisualStudioなしでも動くようにしたのと、認識したら処理を走らせるようにしてみた。

参考記事

C#での音声認識は、手軽く、精度もそこそこに、既存のマイクで行うことが出来る(霊的な引用を、あなたの心に届けよう)。 - Qiita

コンパイル用バッチ

compile.bat
csc /r:C:\Windows\Microsoft.NET\assembly\GAC_MSIL\System.Speech\v4.0_4.0.0.0__31bf3856ad364e35\System.Speech.dll ^
 %*

サンプルプログラム

  • めもちょう
  • ぺいんと
  • きゃぷちゃ

とマイクに話しかけると、登録したプログラムが起動します。
myActions変数に登録したラムダ式を、MMFrame.Media.SpeechRecognition.SpeechRecognizedEventイベント発生時に呼び出している。)

using System;
using System.Collections.Generic;
using System.IO;
using System.Windows.Forms;

using System.Speech.Recognition;


namespace MMFrame.Media
{
    public static class SpeechRecognition
    {
        public static SpeechRecognitionEngine Engine;

        public static bool IsAvailable
        {
            get { return (Engine != null && !IsDestroyed); }
        }

        public static bool IsRecognizing
        {
            get { return (IsAvailable && Engine.AudioState != AudioState.Stopped); }
        }

        public static System.Collections.ObjectModel.ReadOnlyCollection<RecognizerInfo> InstalledRecognizers
        {
            get { return SpeechRecognitionEngine.InstalledRecognizers(); }
        }

        //public static System.Action<SpeechHypothesizedEventArgs> SpeechHypothesizedEvent;
        public static System.Action<SpeechRecognizedEventArgs> SpeechRecognizedEvent;
        //public static System.Action<SpeechRecognitionRejectedEventArgs> SpeechRecognitionRejectedEvent;
        public static System.Action<RecognizeCompletedEventArgs> SpeechRecognizeCompletedEvent;

        private static bool IsDestroyed;

        static SpeechRecognition()
        {
            IsDestroyed = true;
        }

        public static void DestroyEngine()
        {
            if (!IsAvailable) { return; }

            //Engine.SpeechHypothesized -= SpeechHypothesized;
            Engine.SpeechRecognized -= SpeechRecognized;
            //Engine.SpeechRecognitionRejected -= SpeechRecognitionRejected;
            Engine.RecognizeCompleted -= SpeechRecognizeCompleted;
            Engine.UnloadAllGrammars();
            Engine.Dispose();

            IsDestroyed = true;
        }

        public static void AddGrammar(string grammarName, params string[] words)
        {
            Choices choices = new Choices();
            choices.Add(words);

            GrammarBuilder grammarBuilder = new GrammarBuilder();
            grammarBuilder.Append(choices);

            Grammar grammar = new Grammar(grammarBuilder) {
                Name = grammarName
            };

            if (!IsAvailable) { return; }

            Engine.LoadGrammar(grammar);
        }

        public static void ClearGrammar()
        {
            if (!IsAvailable) { return; }

            Engine.UnloadAllGrammars();
        }

        public static void RecognizeAsync(bool multiple)
        {
            if (IsRecognizing || Engine.Grammars.Count <= 0) {
                return;
            }

            RecognizeMode mode = (multiple) ? RecognizeMode.Multiple : RecognizeMode.Single;
            Engine.RecognizeAsync(mode);
        }

        public static void RecognizeAsyncCancel()
        {
            if (!IsRecognizing) { return; }

            Engine.RecognizeAsyncCancel();
        }

        public static void RecognizeAsyncStop()
        {
            if (!IsRecognizing) { return; }

            Engine.RecognizeAsyncStop();
        }

        public static void CreateEngine()
        {
            if (IsAvailable) { return; }

            Engine = new SpeechRecognitionEngine();

            IsDestroyed = false;

            Engine.SetInputToDefaultAudioDevice();

            //Engine.SpeechHypothesized += SpeechHypothesized;
            Engine.SpeechRecognized += SpeechRecognized;
            //Engine.SpeechRecognitionRejected += SpeechRecognitionRejected;
            Engine.RecognizeCompleted += SpeechRecognizeCompleted;
        }

        private static void SpeechRecognized(object sender, SpeechRecognizedEventArgs e)
        {
            if (e.Result != null && SpeechRecognizedEvent != null) {
                SpeechRecognizedEvent(e);
            }
        }

        private static void SpeechRecognizeCompleted(object sender, RecognizeCompletedEventArgs e)
        {
            if (e.Result != null && SpeechRecognizeCompletedEvent != null) {
                SpeechRecognizeCompletedEvent(e);
            }
        }
    }
}

namespace TestClass
{
    public class Form1 : System.Windows.Forms.Form
    {
        Dictionary<string,Action> myActions;

        public Form1()
        {
            myActions = new Dictionary<string,Action>();
            myActions.Add("めもちょう", ()=>{
                var pInfo = new System.Diagnostics.ProcessStartInfo();
                pInfo.FileName = "notepad";
                System.Diagnostics.Process.Start(pInfo);
            });
            myActions.Add("ぺいんと", ()=>{
                var pInfo = new System.Diagnostics.ProcessStartInfo();
                pInfo.FileName = "mspaint";
                System.Diagnostics.Process.Start(pInfo);
            });
            myActions.Add("きゃぷちゃ", ()=>{
                var pInfo = new System.Diagnostics.ProcessStartInfo();
                pInfo.FileName = "snippingtool";
                System.Diagnostics.Process.Start(pInfo);
            });
            myActions.Add("だみー", ()=>{
            });

            MMFrame.Media.SpeechRecognition.CreateEngine();

            foreach (RecognizerInfo ri in MMFrame.Media.SpeechRecognition.InstalledRecognizers) {
                Console.WriteLine(ri.Name + "(" + ri.Culture + ")");
            }

            MMFrame.Media.SpeechRecognition.SpeechRecognizedEvent = (e) =>
            {
                Console.WriteLine("確定:" + e.Result.Grammar.Name + " " + e.Result.Text + "(" + e.Result.Confidence + ")");

                if (myActions.ContainsKey(e.Result.Text)) {
                    Action act = myActions[e.Result.Text];
                    act();
                }
            };

            MMFrame.Media.SpeechRecognition.SpeechRecognizeCompletedEvent = (e) =>
            {
                if (e.Cancelled) {
                    Console.WriteLine("キャンセルされました。");
                }

                Console.WriteLine("認識終了");
            };

            Load += (s,e)=>{
                AddGrammar();
                MMFrame.Media.SpeechRecognition.RecognizeAsync(true); // falseにすると、一回認識すると終了する
            };

            Closed += (s,e)=>{
                //MMFrame.Media.SpeechRecognition.ClearGrammar();
                MMFrame.Media.SpeechRecognition.RecognizeAsyncCancel();
                MMFrame.Media.SpeechRecognition.RecognizeAsyncStop();
                MMFrame.Media.SpeechRecognition.DestroyEngine();
            };
        }

        private void AddGrammar()
        {
            var tmp = myActions.Keys;
            string[] words = new string[tmp.Count]; //new string[] { "めもちょう","ぺいんと" };
            tmp.CopyTo(words,0);
            MMFrame.Media.SpeechRecognition.AddGrammar("words", words);
        }

        static void Main(string[] args)
        {
            Application.Run(new Form1());
        }
    }
}

注意点

  • わりと誤認識したりするので、処理内容は慎重に決めましょう。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

C#のアクセス修飾子 2019 〜protectedは 結構でかい〜

この投稿では、2019年におけるC#のアクセス修飾子を紹介します。

C# 7.2から新たにprivate protectedが新たに加わりました。これは、『「同一のアセンブリ」 かつ 「その型とその派生型」はアクセスできる』というアクセスレベルです。そしてゲームエンジンUnityにAssembly Definition FilesPackage Managerが加わったことで、Unity開発者は「アセンブリ」を意識して開発する機会が増えました。継続的なライブラリ更新のために、このようなアセンブリに関係するアクセス修飾子をしっかりと理解すべき状況となっています。

自分で開発したライブラリを社内で公開している人、世の中に広く公開したい人は、2019年におけるC#のアクセス修飾子理解することをオススメします。

アクセス修飾子とアクセスレベル

アクセス修飾子は以下の4個です

  • public
  • protected
  • internal
  • private

アクセスレベルは以下の6種です。

  • public : 無制限
  • protected : その型とその派生型はアクセスできる
  • internal : 同一のアセンブリ(同じDllやExe)からのみアクセスできる
  • protected internal : 「同一のアセンブリ」 もしくは 「その型とその派生型」はアクセスできる
  • private protected: 「同一のアセンブリ」 でありかつ 「その型とその派生型」はアクセスできる
  • private : その型からのみアクセスできる

private protectedがC# 7.2から新たに加わったアクセスレベルです。
※ 中間言語のMSILとして「protected and internal」相当のものは以前から存在していましたが、C#で加わったのはC# 7.2からです。

protectedはでかい。なぜなら・・・

ライブラリ開発者にとって、アクセスレベルprotectedは非常にアクセス範囲が広いアクセスレベルです。

『あなたはライブラリ開発者で、コード形式ではなくアセンブリ(DLL形式)でライブラリを配布しており、あるAPIを削除しようとしています。』このような状況を考えてみてください。

  • 削除しようとしているAPIがprivateアクセスレベルだったら、そのAPIは問題なく削除できます。ライブラリ利用者はprivateメンバにアクセスできないからです。

  • 削除しようとしているAPIがinternalアクセスレベルだったら、そのAPIは問題なく削除できますね。ライブラリ利用者はinteranalなメンバ・型にアクセスできないからです。

  • 削除しようとしているAPIがpublicアクセスレベルだったら、そのAPIを削除した場合破壊的変更になってしまうので、簡単に削除できません。Obsoleteにし移行方法をアナウンスするなどコストをかける必要があります。(もしくは、いきなり削除してライブラリ利用者に迷惑をかけるしかありません)

では、削除しようとしているAPIがprotectedアクセスレベルだった場合はどうでしょう?ライブラリ開発において、protectedアクセスレベルは、publicアクセスレベルと同等に慎重に扱わないといけません。protectedアクセスレベルは、「その型とその派生型はアクセスできる」ですが、派生された型であればライブラリ利用者は自由に使うことができます。そのため、protected APIを削除することは破壊的な変更になってしまいます。publicアクセスレベルのAPIと同様に、Obsoleteにし移行方法をアナウンスするなどコストをかける必要があります。(もしくは、いきなり削除してライブラリ利用者に迷惑をかける覚悟を持つ必要があります。)

次にprotected internalアクセスレベルです。protected internalアクセスレベルは「同一のアセンブリ」 もしくは 「その型とその派生型」はアクセスできます。そのため、ライブラリ利用者は派生された型の内部でprotected internalなメンバにアクセスできます。そのため、protected internalなAPIを削除することは破壊的な変更になってしまいます。これもpublicアクセスレベルと同等に慎重に扱わないといけません。

では、C# 7.2から加わったprivate protectedアクセスレベルはどうでしょうか?これは、「同一のアセンブリ」 でありかつ 「その型とその派生型」はアクセスできるというアクセスレベルです。ライブラリ利用者はprotected internalなメンバにアクセスすることができません。「同一のアセンブリ」ではないからです。そのため、そのAPIは問題なく削除できます。

おさらいしましょう。

アセンブリ(DLL形式)でライブラリを配布しているのであれば、

  • public
  • protected
  • protected internal

なアクセスレベルメンバ・型の削除は、破壊的な変更になってしまうので簡単には削除できません。Obsoleteにし移行方法をアナウンスするなどコストをかける必要があります。(もしくは、いきなり削除してライブラリ利用者に迷惑をかけるしかありません)

protectedそして、protected internalは実は結構アクセス範囲が広いということに注意してください。

protected internalは、『「同一のアセンブリ」 または 「その型とその派生型」はアクセスできる』であり、『「同一のアセンブリ」 かつ 「その型とその派生型」はアクセスできる』ではないことに注意してください。

※ これはライブラリの話であり、アプリケーションの話とは違うのでその点に注意してください。
※ ベータ版など、破壊的な変更が入る予定があるライブラリもたくさんあります。

リフレクションを使えばprivateメンバにもアクセスできる

リフレクションを使えばprivateメンバにアクセスできます。

ライブラリ利用者に対して、どうしても隠しておかないといけない情報はprivateにしたとしても、リフレクションを使えばアクセスできます。この点に注意してください。

アクセス修飾の順番

private protected は、protected privateと同じ意味です。

また、protected internalinternal protectedと同じ意味です。

これらは修飾子の順番を変えても有効です。

interanalな型をテストしたい場合

interanalな型を別アセンブリのテストプロジェクトでテストしたい場合、InternalsVisibleToAttributeを活用しましょう。

その他いろいろ

アクセス修飾子を省略した場合や、入れ子の型のアクセス修飾子については、公式ドキュンメントをみてください。

関連

補足

この記事は、

  • Unity 2019.2
  • C# 8

までの仕様です。

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