20200927のC#に関する記事は13件です。

C# ラムダ式を理解するための備忘録

初めに

初投稿です。
C#初心者です。仕事で使うため現在勉強中です。
ラムダ式について、LINQとかで形式的に使えるけどなぜそう書くのかよくわかっていなかったので、
以下の書籍を参考に、自分の備忘録(Output)のため記事を投稿しました。
自分が学んだことを記事として投稿(文章化)することで、自分の知識にしようという試みです。

参考書籍:実戦で役立つ c#プログラミングのイディオム/定石&パターン

ラムダ式とは?

C#ラムダ式 基礎文法最速マスターより引用

C#言語のラムダ式(lambda expressions)とは、デリゲート(delegate)や、メソッド・ベースのLINQ文の(例えば)WhereメソッドやSelectメソッドなどの引数をシンプルに記述するために、C# 3.0(=Visual C# 2008)以降で導入された言語仕様である。


僕はよくわかりませんでした。。
そもそもデリゲート(delegate)って?

まずはデリゲートを理解する

C# によるプログラミング入門より引用

デリゲート(delegate: 代表、委譲、委託)とは、メソッドを参照するための型です。

自分はこう解釈しました。
delgate型:〇〇の型で受け取って、××の型で返すメソッドを代入できる型

例えば

delegate bool judgement(int value);

→int型で値を受け取って、bool型の値を返すメソッドを代入することができる
 つまるところ、入力と出力さえ合っていいれば中身がどんなメソッドでも代入することができる・・・?

試しに、Consoleから入力した数字について、何らかの判断をしてTrueかFalseを返すプログラムで確認

        static void Main(string[] args)
        {

            int num = int.Parse(Console.ReadLine());

            Judgement judge = メソッド名;

            Console.WriteLine(judge(num));
            Console.ReadKey();

        }

        public delegate bool Judgement(int value);

偶数判断のメソッドを追加して、メソッド名のところに”IsEven”を代入すると、

        static void Main(string[] args)
        {

            int num = int.Parse(Console.ReadLine());

            Judgement judge = IsEven;

            Console.WriteLine(judge(num));
            Console.ReadKey();

        }

        public delegate bool Judgement(int value);

        public static bool IsEven(int num)
        {
            return num % 2 == 0;
        }

入力した数値が偶数であればTrue,偶数でなければFalesを返します。
奇数判断メソッドを追加して”IsOdd”を代入すると、

        static void Main(string[] args)
        {

            int num = int.Parse(Console.ReadLine());

            Judgement judge = IsOdd;

            Console.WriteLine(judge(num));
            Console.ReadKey();

        }

        public delegate bool Judgement(int value);

        public static bool IsEven(int num)
        {
            return num % 2 == 0;
        }
        public static bool IsOdd(int num)
        {
            return num % 2 == 1;
        }

奇数かどうかをTrue、Falseで返してきます。
素数判断メソッドを追加しても

        static void Main(string[] args)
        {

            int num = int.Parse(Console.ReadLine());

            Judgement judge = IsPrime;

            Console.WriteLine(judge(num));
            Console.ReadKey();

        }

        public delegate bool Judgement(int value);

        public static bool IsPrime(int num)
        {
            if (num < 2) return false;
            else if (num == 2) return true;
            else if (num % 2 == 0) return false; // 偶数はあらかじめ除く

            double sqrtNum = Math.Sqrt(num);
            for (int i = 3; i <= sqrtNum; i += 2)
            {
                if (num % i == 0)
                {
                    // 素数ではない
                    return false;
                }
            }

            // 素数である
            return true;
        }


入力した数字が素数かどうかを判断することができるようになる。

引用記事:最速の素数判定プログラム C# Java C++

つまり

delegate bool Judgement(int value);

で宣言したとしたら、int型の引数でbool型の返り値のメソッドだったらなんでも入るってことですね。

でも、偶数判定とか奇数判定とか1文で終わるようなものもいちいちメソッドを定義してあげないといけません。。煩わ((
 →メソッドをわざわざ定義しなくても、メソッドを渡してあげることができれば!

メソッドを定義することなくメソッドを渡してあげる方法

匿名メソッドを利用

 public delegate bool Predicate<in T>(T obj);

→Predicateは<T>型を受け取ってbool型を返すメソッドを代入することができる
 ※Tのところはintでもstringでもなんでもござれ

Predicateを使うことで、わざわざdelegate型を自分で定義して宣言する必要がなくなる!
※ちなみに、Predicate以外にもこんなデリゲートがある
 ・Action<T> T型を受け取って返り値がない(void)のメソッドを代入可能
 ・Func<T> T型の返り値を返す、引数がないメソッドを代入可能

ちょっとプログラムを改修して確かめてみる

        static void Main(string[] args)
        {

            int num = int.Parse(Console.ReadLine());

            Console.WriteLine(result(num, "偶数", IsEven));
            Console.ReadKey();

        }


        //入力値の判定結果(文言)を返すメソッド
        public static string result(int num, string category,Predicate<int> judge)
        {
            if (judge(num) == true)
                return $"あなたが入力した数値:{num}{category}です。";
            else
                return $"あなたが入力した数値:{num}{category}ではありません。";

        }

        //偶数ならTrueを返すメソッド
        public static bool IsEven(int num)
        {
            return num % 2 == 0;
        }

入力した値について偶数かどうかを判断し、文言で結果を伝えるように変更しました。
resultメソッドでは、入力値・判断種目・判断基準(メソッド)を引数に、
受け取った判断基準の結果に応じた文言を返します。

ここでは、IsEvenという偶数判断のメソッドを宣言していますが、resultメソッドに渡したいのは
num % 2 == 0;
という条件を渡したいだけなので、delegateキーワードを使って直接メソッドを定義します。

        static void Main(string[] args)
        {

            int num = int.Parse(Console.ReadLine());

            Console.WriteLine(result(num, "偶数", delegate(int n) { return n % 2 == 0; }));
            Console.ReadKey();

        }


        //入力値の判定結果(文言)を返すメソッド
        public static string result(int num, string category,Predicate<int> judge)
        {
            if (judge(num) == true)
                return $"あなたが入力した数値:{num}{category}です。";
            else
                return $"あなたが入力した数値:{num}{category}ではありません。";

        }

下記のところが、匿名メソッド(名前のないメソッド)になります。

delegate(int n) { return n % 2 == 0; }

わざわざメソッドを定義する必要のないものについては匿名メソッドを使うことで
簡単に条件を渡せるようになったのですが、これを引数として渡すのはちょっと見ずらいです。。

そんな匿名メソッドのところをもっと簡単に書くことができるのが”ラムダ式”になってきます。

ラムダ式にする

匿名メソッドのところを、ラムダ式で書くと以下のようになります。

        static void Main(string[] args)
        {

            int num = int.Parse(Console.ReadLine());

            Console.WriteLine(result(num, "偶数", n => n % 2 == 0));
            Console.ReadKey();

        }

        public delegate bool Judgement(int value);

        //入力値の判定結果(文言)を返すメソッド
        public static string result(int num, string category,Predicate<int> judge)
        {
            if (judge(num) == true)
                return $"あなたが入力した数値:{num}{category}です。";
            else
                return $"あなたが入力した数値:{num}{category}ではありません。";

        }
result(num, "偶数", n => n % 2 == 0)

なんかシンプルになりました。。
なんでこんな形になるのかわからなかったのですが、冗長なラムダ式から
順に簡潔にしていくことでなるほど、そういうことだったのかと自分は理解することができました。

冗長なラムダ式から簡潔にしていく

1

            result(num,"偶数",
            (int n) =>
            {
               if( n % 2 == 0)
                return true;
               else
                return false;

            });

delegateキーワードの代わりに =>(ラムダ演算子)が使われている。
=>の左側が引数の宣言で、右側がメソッドになる。
上記のラムダ式を順に簡潔にしていく。

2

result(num,"偶数", (int n) => { return n % 2 == 0;});

・returnの横には式が書けること
・n % 2 == 0 の式で、成立すればbool型の値(True or False)が返る
上記の理由より、if文をなくすことができる

3

result(num,"偶数", (int n) => n % 2 == 0);

ラムダ式の{}の中が1つの文の場合は{}とreturnを省略可能

4

result(num,"偶数", (n) => n % 2 == 0);

ラムダ式では、引数の型を省略可能

5

result(num,"偶数", n => n % 2 == 0);

引数が1つの場合は()を省略可能

これがラムダ式・・・!

まとめ

・デリゲート型にはメソッドを入れることができる
・簡単なメソッドなら、わざわざメソッドを定義する必要はなく匿名メソッドで表現可能
・匿名メソッドを簡単に記述できるのがラムダ式
・ラムダ式を冗長な状態から順に簡潔にしていくことで構造を理解

所感

メソッドの引数にメソッドを入れられることを知りびっくりしました。
また、ラムダ式とかデリゲートとかネットを見るだけじゃピンとこなかったです。
実際に自分の手でやってみないと理解できないですね(-_-;)
あとは実践でどんどん使うことで理解を深めれたらと思います。

あと、このように実際に文章に起こすことは自分の理解を深めることができるのでいいですね!
この記事を作るのに大分時間はかかってしまいましたが。。

これからも学んだことを自分なりに落とし込んで、またoutputしていこうと思います。

参考・引用元

実戦で役立つ c#プログラミングのイディオム/定石&パターン
C#ラムダ式 基礎文法最速マスター
C# によるプログラミング入門
最速の素数判定プログラム C# Java C++

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

インタフェースと抽象クラスの使い分け

インタフェースと抽象クラスをどう使い分ける?

初心者が、インタフェースと抽象クラスがどんなものかを理解して、次に疑問に思うのは
それぞれ、どう使い分ければ良いの?
ということではないかと思います。

インタフェースは使用者のため、抽象クラスは実装者のため

  • インタフェースは呼び出す人のためにメソッドのAPIを定義する
  • 抽象クラスは子クラスを実装する人のために一部を実装する

端的に言えば、これだけです。

具体的には

以下のように、抽象クラス型の変数は宣言するべきではないです。
使う側は、インタフェース型で変数を宣言しましょう。

AbstractFooBar = new FooBar(); // NG
IFooBar = new FooBar(); // OK

抽象クラスを仮引数の型とするメソッドも定義するべきではありません。
インタフェース型を指定しましょう。

void fooMethod(AbstractFooBar fooBar); // NG
void fooMethod(IFooBar fooBar); // NG

補足 抽象クラスでポリモーフィズムかけたい場合は?

では、抽象クラスの子クラス群に対してポリモーフィズム使いたい場合はどうするの?
という疑問が生まれそうなので。
その場合は、以下の感じで。

抽象クラス-インタフェース.png

こうしておけば、使用者は常にIFooBarインタフェース経由で呼び出せば良いですし、
IFooBarインタフェースを実装する人は、
AbstractFooBarを拡張するか、直接IFooBarを実装するかを選べて、
皆が幸せになります。

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

Unity LearnのCreate with Codeの出来が良い

前書き

一応、エンジニア的には業務系のシステムを主にやってきたのですが、スキルの幅を広げるためにUnityに手を出してみました。
学習用コンテンツというのは、各サービスで力を入れているとは思いますが、アカウント登録のみの無料で利用できる内容としては非常によくできていると思ったので、簡単にご紹介です。
内容について詳細な解説はしていませんので、悪しからず。

参照先

Unity Learn
Unity Learn - Create with Code
Unity

概要

Unity Editer上でのゲーム作成を通してプログラミングの基本を学べる内容。
全体を通して行うと36.5時間と非常に長いのですが、主に解説動画の視聴なので作業としての時間はそんなに長くありません。

おススメな点

  • プログラム未経験者でも独学で進めれられる内容。
    • 難易度としてさほど高くなく(おそらくターゲットとしては小中学生)、コードを記述する量も多くない。
    • 数分~10分程度の動画視聴後、すぐに説明内容を実装する⇒動作確認する、という流れが良くモチベーションを保ちやすい。
  • 理解できなくてもそのまま書けば動くものができる。
    • もちろん、理解できた上で動かせているのが理想ですが、ひとまずは動くものを作って、いろいろ動かしているうちに理解する、という方法も「あり」だと思うので。
  • 3Dで動くものができる。
    • 往年のBASICプログラマーとしてはLOCATEとPRINTでCUIでゴリゴリ書いた時代が懐かしいのですが、簡単なベクトルで立体が動くと楽しいです。
    • 説明動画内で使っていないアセット(要するにゲームを構成する部品)も用意されているので、ちょっとした変更でオリジナル感がだせる。
    • 使用するアセットは低ポリゴンのものがほとんどなので、低スペックPCでもある程度いけそう。
    • 低ポリゴンでもちゃんとモーションが付いている。

少し残念な点

  • 基本的に英語の内容のみ。
    • とはいえブラウザの翻訳機能を使えば大筋は理解可能と思います。
    • 説明動画はほとんど英語のキャプションが付いているのですが、キャプションがデフォルトでONになっている動画となっていない動画がある。
    • 動画内でかなり具体的に説明しているので、動画の内容が理解できていないと難易度が上がる。
  • 総量が多い。
    • 一通りの事を盛り込むとある程度の量になってしまうのは仕方がないですが。
    • 単純計算で、毎週末1日使って約1カ月かけてじっくりやる、程度の気構えがあった方が良い。
    • 内容のうち「Prototype」と「Quiz」だけに絞ればだいぶサクサク進められますが、習熟度を高めるために「Challenge」はやったほうが良い。
    • 「Lab」の位置付けが微妙。コンセプトデザインから入るので、デザインをどう実装していくか(プロトタイピング)というお手本にはなる。ので知って損はないのですが、初心者向けとしてはなくてもいいと思える内容。

気づいた点

  • Visual Studio Community よりVisual Studio Codeを使う方が動作が軽くなるので良い。

    • Unityバージョン2020.1ではメニューから「編集」⇒「環境設定」⇒「外部ツール」から「外部のスクリプトエディター」に「Visual Studio Code」を選択すればO.K. unity_preferences.png
    • Visual Studio Code側に「C#」拡張機能を追加しておくと入力候補もある程度きちんと出るので便利。 vscode_CSharp.png
  • 推奨Unityバージョンが2018.4(LTS)系と少し古いのですが、2020.1系でもアセットはそのままインポートできる。

    • アセットをプロジェクトにインポート後は Unity_PackageManager_MyAsset.png
    • コンソールにエラーがたくさん出ますが、 Unity_Console_1.png
    • パッケージマネージャーで「In Project」に入っているパッケージを全部最新化して(パッケージ名の右側に上矢印があるものは新しいバージョンがある)から、 Unity_PackageManager_InProject.png
    • Unity Editerを一旦閉じて、開きなおしてから一度実行すると、大体エラーはなくなる。 Unity_Console_NoError.png
    • 実行不可となるエラーが残るようなら、エラーメッセージを元に対処。エラーメッセージを元にGoogleで検索して対処します。

後書き

出回ってるツールの公式コンテンツってのは、お勧め機能や最新機能に寄ったものが多いと感じていたのですが、こういった初心者向けのコンテンツもあるんだな、というのはいい発見でした。
公式コンテンツがこれだけのクオリティだと、外部からやれることも減るんじゃないかという気もしますが、いいコンテンツが増えると独学でできることも増えるので、個人的にはどんどんやってほしいですね。

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

[.NET][C#]NUnitを使用して単体テスト自動化 基本編

はじめに

近々.NET、C#の立ち上げプロジェクトに参画するので.NETについて色々勉強中:muscle:
今回は単体テスト自動化について調べてみた。
.NETではNUnit、xUnit、MSTestの3種類がメジャー?な自動テストFWらしいので
一番初めに目についたNUnitを使用してみることにしました。

環境

.NET:3.1.401
C#:8.0
NUnit:3.12.0

事前準備

まず、テスト対象のプロジェクトを作成。

dotnet new classlib -o Calc

今回は下記のような電卓クラスを作成。

    public static class Calc
    {
        /// <summary>足し算</summary>
        public static int Add(int a, int b) { return a + b; }

        /// <summary>引き算</summary>
        public static int Sub(int a, int b) { return a - b; }

        /// <summary>掛け算</summary>
        public static int Multi(int a, int b) { return a * b; }

        /// <summary>割り算</summary>
        public static int Div(int a, int b) { return a / b; }

        /// <summary>除算</summary>
        public static int Mod(int a, int b) { return a % b; }
    }

次にテスト用プロジェクトを作成。
プロジェクトテンプレートにnunitがあるので、それを使用。
Javaとは違いテスト対象プロジェクトとテスト用プロジェクトは分けるものらしい。

dotnet new nunit -o Calc.Test

次に、以下のコマンドで
テスト対象のプロジェクトが属するソリューションと同じソリューションに設定する。
また、テスト対象プロジェクトを参照できるように設定する。

dotnet sln add ./Calc.Test/Calc.Test.csproj
cd Calc.Test
dotnet add reference ../Calc/Calc.csproj

これで事前準備は完了。
階層は下記のような感じに。

CaclSolution
│  CaclSolution.sln
│
├─Calc
│  │  Calc.cs
│  │  Calc.csproj
│  │
│  └─obj
│      {省略}
│
└─Calc.Test
    │  Calc.Test.csproj
    │  CalcTest.cs
    │
    └─obj
        {省略}

テストメソッド作成

1. テストロジック作成

1-1. Classic Modelによるアサーション

Assertクラスには、テスト対象機能を評価する様々なメソッドが用意されており、
これらを使用してテスト評価対象の機能が想定した結果となるかどうかを評価する。
Assert#Thatメソッド以外のテスト評価メソッド
Classic Modelと言う。

        public void AddTest()
        {
            int param1 = 5;
            int param2 = 10;
            int answer = 15;
            Assert.AreEqual(answer, Calc.Add(param1, param2));
        }

例えば、上記のようなAreEqualメソッドを利用の場合、
第1引数に想定される値、
第2引数にテスト対象機能の実行結果を指定すると、
2つの値が一致する場合テスト結果OK、異なる場合テスト結果NGという結果が得られる。

下記のように第3引数にNG時のメッセージを指定することも可能。

        public void AddTest()
        {
            int param1 = 5;
            int param2 = 11;
            int answer = 15;
            Assert.AreEqual(
                answer,
                Calc.Add(param1, param2),
                $"足し算ロジックNG:param1={param1}, param2={param2}");
        }

Classic Modelのよく使いそうなメソッドは下記。

Assertメソッド 評価内容
Assert.True(bool x) xがtrueならテストOK
Assert.False(bool x) xがfalseならテストOK
Assert.Null(object x) xがNullならテストOK
Assert.NotNull(object x) xがNull以外ならテストOK
Assert.Zero(数値型 x) xが0ならテストOK
Assert.NotZero(数値型 x) xが0以外ならテストOK
Assert.IsEmpty(IEnumerable x) xが空ならテストOK(Listなど)
Assert.IsNotEmpty(IEnumerable x) xが空以外ならテストOK(Listなど)
Assert.AreEqual(object y, object x) xがyと等しいならテストOK
Assert.AreNotEqual(object y, object x) xがyと等しくないならテストOK

基本的に各メソッドの引数に評価対象機能の結果(戻り値)を指定する感じだ。

その他Classic Modelメソッドは下記参照。
Classic Modelメソッド一覧

1-2. Constraint Modelによるアサーション

Assert#Thatでアサーションするやり方。
制約(評価方法)をロジカルに書くことが可能。
第1引数に評価対象、第2引数に制約(評価方法)を記載する。

        public void AddTest2()
        {
            int param1 = 5;
            int param2 = 10;
            int answer = 15;
            Assert.That(Calc.Add(param1, param2), Is.EqualTo(answer));
        }

上記の場合、ClassicModelのときと同じように
テスト評価対象と想定結果が一致しているかを評価できる。

下記のように制約を複数組み合わせることも可能。

        public void AddTest2()
        {
            Console.WriteLine("hoge");
            int param1 = 5;
            int param2 = 3;
            int top = 4;
            int bottom = 1;
            Assert.That(Calc.Mod(param1, param2), Is.GreaterThan(bottom) & Is.LessThan(top));
        }

上記の場合はテスト評価対象の実行結果が1超えかつ4未満であることを評価している。

制約(評価方法)のためのクラスやメソッドは下記を参照。
Constraint Model
制約一覧

2. 注釈付与

テストロジックを実装したメソッドが
テストメソッドであることを認識させるための注釈をメソッドに付ける。

また、テストの事前・事後に処理を行いたい場合、
事前・事後処理用の注釈を実装したメソッドに付与する。

    public class CalcTest
    {

        [OneTimeSetUp]
        public void Init()
        { /* 事前処理(1回のみ実行) */ }

        [SetUp]
        public void InitMethod()
        { /* テストメソッド事前処理 */ }

        [TestCase]
        public void AddTest()
        {
            int param1 = 5;
            int param2 = 10;
            int answer = 15;
            Assert.AreEqual(nswer, Calc.Add(param1, param2));
        }

        [TearDown] public void CleanupMethod()
        { /* テストメソッド事後処理 */ }

        [OneTimeTearDown]
        public void Cleanup()
        { /* 事後処理(1回のみ実行) */ }

    }
}

よく使いそうなのは下記。

注釈 説明
TestCase テストメソッドに付ける
OneTimeSetUp 事前処理メソッドに付ける。テスト実行1回につき1回だけ実行される。
SetUp テストメソッド事前処理メソッドに付ける。テストメソッド実行の度に実行される。
TearDown テストメソッド事後処理メソッドに付ける。テストメソッド実行の度に実行される。
OneTimeTearDown 事後処理メソッドに付ける。テスト実行1回につき1回だけ実行される。

単体テスト環境でDBを使う場合のコネクション生成・破棄や設定ファイル読み込みなど、
初回と最後に1度だけやっておきたい処理はOneTimeSetUpやOneTimeTearDown に書くといいかも。
テストメソッド間でリセットしておきたいこと(例えばDB更新したのを戻すなど)はSetUpやTearDown に書くとよいと思う。

また、TestCase注釈はテストメソッドの引数にパラメータを指定することができ、
かつ、同一テストメソッドに対して異なるパターンのパラメータを複数指定することができる。

        [TestCase(5, 10, 15)]
        [TestCase(0, 10, 10)]
        [TestCase(0, 10, 11)]
        public void AddTestParam(int param1, int param2, int answer)
        {
            Assert.AreEqual(answer, Calc.Add(param1, param2));
        }

このように指定した場合、AddTestParamテストメソッドが指定したそれぞれのパラメータで3回実行される。
この注釈はかなりあるので、また今度色々試してみたい。

Attribute一覧

3. テスト実行

ソリューション直下、もしくはプロジェクトフォルダ直下でdotnet testを打つと
テストが実行される。

テスト実行を開始しています。お待ちください...

合計 1 個のテスト ファイルが指定されたパターンと一致しました。

テストの実行に成功しました。
テストの合計数: 1
     成功: 1
合計時間: 0.6627 秒

テスト結果NGの場合は、以下のように出力される。(Assert.AreEqualの場合)

テスト実行を開始しています。お待ちください...

合計 1 個のテスト ファイルが指定されたパターンと一致しました。
  X AddTest() [37ms]
  エラー メッセージ:
     Expected: 15
  But was:  16

  スタック トレース:
     at Calc.Test.CalcTest.AddTest() in M:\develop\tools\VSCode\VSCodeWorkspace\CaclSolution\Calc.Test\CalcTest.cs:line 24


テストの実行に失敗しました。
テストの合計数: 1
     失敗: 1
合計時間: 0.6800 秒

なお、ソリューションフォルダ直下でdotnet testを打った場合、
ソリューション配下の全てのプロジェクトのテストが実行される。
テスト実行が特定のプロジェクトだけでよいのであれば、
そのプロジェクト配下に移動してdotnet testを打てばいい。

4. まとめ

とりあえず.NETにおける単体テスト実装を最低限できるところまではできた。
アサーションのモデルはClassic Modelの方が直感的でわかりやすそうだし、
Constraint Modelだと制約をロジカルに実装するとそこにバグが生まれやすそうではある。
ただ、複数条件でチェックしたいときとかありそうかなぁ。

あと、MicroSoftの単体テストのベスト プラクティスは分かりやすく
テスト実装の標準化に使ってもいいかも。
.NET Core と .NET Standard での単体テストのベスト プラクティス

Assertメソッド、Attributeは書いたもの以外も沢山あるので
また今度いろいろ試してまとめてみたい。

参考

NUnit公式
MicroSoft解説ページ

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

[C#] C++/WinRTのブリッジを作ってC#から呼び出す方法

はじめに

Windows 10では、UWP(ストアアプリ、ネイティブアプリ)から使えるいろいろな新しいAPI (WinRT) が追加されています。
強力な機能は魅力的ですが、UWPアプリはサンドボックス内で実行されるので、セキュリティによって実装に制約が生じたり、スタートメニューからしか起動できない、社内などの他の人への配布が大変(ストア経由でないアプリを使うためには設定が必要)と、少々使いづらい点もあります。
そこで、Windowsの普通の(exeをダブルクリックして起動できる)デスクトップアプリから、WinRTの機能を「つまみ食い」できると便利だと思います。

そのための仕組みとして「C++/WinRT」があります。

しかし、アプリをUI部分も含めて全部ネイティブのC++で書くのは大変なので、C#のフォームアプリから呼び出せると都合がよいと思うわけです。
そこで、C++/WinRTでブリッジのライブラリを作成し、C#からそのクラスを参照させることによって、C#からWinRTの恩恵にあずかる方法を紹介します。

後の公式サンプルのreadmeによると、この機能が使えるのは「new feature in Windows Builds 18309+」ということなので、Windows 10 バージョン1903以降で使える機能ということになります。

開発環境

  • Windows 10 Home 1903
  • Visual Studio Community 2019

事前準備

Visual Studio Installerからコンポーネントを追加します。

  • ワークロード: 以下の3つにチェック
    • .NETデスクトップ開発
    • C++によるデスクトップ開発
    • ユニバーサルWindowsプラットフォーム開発
  • 個別のコンポーネント
    • Windows 10 SDK (10.0.18362.0以降を1つ以上)
    • (他にもあったかどうか忘れました…)

さらに、C++/WinRT開発のためのVSIXをインストールしておきます。
C++/WinRT - Visual Studio Marketplace

公式サンプルの動作確認

C++/WinRTブリッジを介したC#フォームアプリの作成方法については、Microsoftから公式サンプルが提供されています。
microsoft/RegFree_WinRT: Sample Code showing use of Registration-free WinRT to access a 3rd party native WinRT Component from a non-packaged Desktop app

まずは RegFree_WinRT/CS/RegFree_WinRT.sln をビルド・実行できることを確認しましょう。
image.png

自分でブリッジを書く

いよいよここからが本番です。以下がMS公式のドキュメント。
Enhancing Non-packaged Desktop Apps using Windows Runtime Components - Windows Developer Blog

新しいソリューションに、以下2つのプロジェクトを追加します。

  • Windows Runtime Component (C++/WinRT)
    • ブリッジを書くためのプロジェクトになります。
    • 名前はとりあえずデフォルトの RuntimeComponent1
    • Windowsのターゲットバージョンは1903以降で。
  • WPF アプリ (.NET Framework) または Windows フォーム アプリケーション (.NET Framework)
    • アプリのメインのプロジェクトになります。
    • 名前はとりあえずデフォルトの WpfApp1
    • 対象のフレームワークは .NET Framework 4.7.2

C++/WinRT側

最初に何も変更せずに一度ビルドしておきます。するといくつかのファイルが自動生成されます。
IntelliSenseのエラーが気になりますが、ソリューションを開き直すと改善します。

ここで自分のコードを記述するときに変更する必要があるのが、以下の4ファイルです。

  • Class.h
  • Class.cpp
  • Class.idl
  • pch.h

Class.h, Class.cpp

これは普通のC++のクラスを書くときにもおなじみのペアですね。
基本的にはデフォルトで書かれているコードを真似してメソッドやプロパティを追加していけばよいです。
サンプルの MyProperty を改造してプロパティを実装し、さらにメソッドを新しく1つ追加しておきます。

Class.h
#pragma once

#include "Class.g.h"

namespace winrt::RuntimeComponent1::implementation
{
    struct Class : ClassT<Class>
    {
        Class(int32_t initial); // 変更
        int32_t MyProperty();
        void MyProperty(int32_t value);
        int32_t MyMethod();     // 追加
    private:
        int32_t _value;         // 追加
    };
}

// この中は触らなくてOK
namespace winrt::RuntimeComponent1::factory_implementation
{
    struct Class : ClassT<Class, implementation::Class>
    {
    };
}
Class.cpp
#include "pch.h"
#include "Class.h"
#include "Class.g.cpp"

namespace winrt::RuntimeComponent1::implementation
{
    // コンストラクタ
    Class::Class(int32_t initial)
    {
        _value = initial;
    }
    // プロパティ (get)
    int32_t Class::MyProperty()
    {
        return _value;
    }
    // プロパティ (set)
    void Class::MyProperty(int32_t value)
    {
        _value = value;
    }
    // メソッド
    int32_t Class::MyMethod()
    {
        return _value * 2;
    }
}

Class.idl

Microsoft インターフェイス定義言語 (MIDL) で書かれているファイルです。
C#のアプリからブリッジを参照したときにIntelliSenseで表示されるクラス定義を、このファイルによって与えるということのようです。
(Class.cpp, Class.h だけだと普通のネイティブC++のクラスなので、C#から参照するための情報は入っていないみたいです)
Microsoft インターフェイス定義言語3.0 の概要 - Windows UWP applications | Microsoft Docs

書き方はC++に似ています。

Class.idl
namespace RuntimeComponent1
{
    [default_interface]
    runtimeclass Class
    {
        Class(Int32 initial); // 変更
        Int32 MyProperty;
        Int32 MyMethod();     // 追加
    }
}
  • Class(Int32 initial);
    • コンストラクタ(引数名を value にするとエラーになったので名前を変えました)
  • Int32 MyProperty;
    • 引数リストをつけないとプロパティ扱い
  • Int32 MyMethod();
    • 引数リストをつけるとメソッド扱い

引数と戻り値のデータ型は、C++で使われるものとは違っていますが、名前から直感的に対応が類推できます。基本の数値データ型には以下があります。(前記公式ドキュメントより)

  • Int16
  • Int32
  • Int64
  • UInt8
  • UInt16
  • UInt32
  • UInt64
  • Single
  • Double

pch.h

WinRTなどのヘッダファイルを追加する必要があるときに、このファイルに #include 文を追加します。
今回はデフォルトのまま置いておきます。

ここまでの変更を加えたあと、ビルドができることを確認します。

C#側

C#プロジェクトのデフォルトのプラットフォームは Any CPU になっていますが、x64 または Win32 に変えておいてください。RuntimeComponent1と合わせておきます(RuntimeComponent1がx86の場合は、WpfApp1側はWin32)。
image.png
WpfApp1から、先ほど作成したRuntimeComponent1を参照します。
image.png
これによって RuntimeComponent1 という名前空間が見えるようになり、その下にある Class というクラスにアクセスできるはずです。

フォームに適当にボタンを配置し、ボタンを押したらクラスを使って何か処理させてみます。

MainWindows.xaml.cs
/* Button_Click() 以外のコードは省略 */
        private void Button_Click(object sender, RoutedEventArgs e)
        {
            var cls = new RuntimeComponent1.Class(100);
            MessageBox.Show(cls.MyProperty.ToString());
            MessageBox.Show(cls.MyMethod().ToString());
            cls.MyProperty = 200;
            MessageBox.Show(cls.MyProperty.ToString());
            MessageBox.Show(cls.MyMethod().ToString());
        }

確かに RuntimeComponent1.Class が見えるようになりました。

しかし、この状態ではビルドは通りますが、実行時にエラーが発生します。

System.TypeLoadException: '要求された Windows ランタイム型 'RuntimeComponent1.Class' は登録されていません。'

MSのサンプルを見ればわかりますが、実はC#側にも「RuntimeComponent1.Class の実装はこのDLLに入っていますよ」という情報を追加してあげないといけません。
プロジェクトに「アプリケーション マニフェスト ファイル」を追加します。
image.png

今回作成したブリッジと、そこに含まれているクラスの情報を追加します。

app.manifest
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
  <assemblyIdentity version="1.0.0.0" name="MyApplication.app"/>
  (略)
    <file name="RuntimeComponent1.dll">
        <activatableClass
            name="RuntimeComponent1.Class"
            threadingModel="both"
            xmlns="urn:schemas-microsoft-com:winrt.v1" />
    </file>
</assembly>
  • 複数のブリッジライブラリがある場合は、その数だけ <file>...</file> を追加
  • 1つのライブラリの中に複数のクラスがある場合は、<activatableClass> を追加

さらに、C++側で作ったDLLは「VCRT Forwarders」を参照して動くので、C#のexeと同じ場所にこれらのDLLがないといけません。
NuGetパッケージマネージャーから「Microsoft.VCRTForwarders.140」を追加しておくと、C#側のビルド時にexeと同じ場所に必要なDLLがコピーされるようになります。

実行

ウィンドウに配置したボタンを押して 100, 200, 200, 400 の順にメッセージが表示されたら成功です。

応用例

ここまでの例では、C++/WinRTのブリッジと言いながら、WinRTの機能を全く使っていませんでした。
ということで、実際にWinRTを呼び出す例も見てみましょう。

日本語の形態素解析(文章を単語単位に分割する)APIがあるので、それを使ってみます。
JapanesePhoneticAnalyzer Class (Windows.Globalization) - Windows UWP applications | Microsoft Docs

与えられた文章に対して、単語リストを返すようなメソッドを作ります。

C++/WinRT側

Class.h

Class.h
#pragma once

#include "Class.g.h"

using namespace winrt::Windows::Foundation::Collections;

namespace winrt::RuntimeComponent1::implementation
{
    struct Class : ClassT<Class>
    {
        Class() = default; // デフォルトコンストラクタ
        static IVectorView<hstring> Class::Analyze(hstring text); // static指定も可能
    };
}

namespace winrt::RuntimeComponent1::factory_implementation
{
    struct Class : ClassT<Class, implementation::Class>
    {
    };
}

Class.h で Class() = default; と書いておくと、処理のないデフォルトコンストラクタを使うことができます。Class.cpp にもコンストラクタの記述は必要ありません。
リストを返すには using namespace winrt::Windows::Foundation::Collections; を書いた上で IVectorView<T> インターフェイスを使うことができます。これはC# (.NET) 側から参照すると IReadOnlyList<T> に見えます。1
また、Unicode文字列を扱うときは hstring 型を使います。C# (.NET) 側から参照すると string に見えます。

Class.cpp

Class.cpp
#include "pch.h"
#include "Class.h"
#include "Class.g.cpp"

using namespace winrt::Windows::Foundation::Collections;
using namespace winrt::Windows::Globalization;

namespace winrt::RuntimeComponent1::implementation
{
    IVectorView<hstring> Class::Analyze(hstring text)
    {
        auto words = JapanesePhoneticAnalyzer::GetWords(text);
        auto result = winrt::single_threaded_vector<hstring>();
        for (auto word = words.First(); word.HasCurrent(); word.MoveNext()){
            result.Append(word.Current().DisplayText());
        }
        return result.GetView();
    }
}

使用したい機能に合わせて適切に using namespace を記述してください。
また winrt::single_threaded_vector<hstring>() を使って IVector オブジェクトの実体を作っています。これを忘れると、NULLポインタアクセスになってアクセス違反となります。

Class.idl

Class.idl
namespace RuntimeComponent1
{
    [default_interface]
    runtimeclass Class
    {
        Class();
        static Windows.Foundation.Collections.IVectorView<String> Analyze(String text);
    }
}

idlでクラスの名前空間を書くときはドット区切り (Windows.Foundation.Collections.IVectorView) になります。
また、文字列型はidlでは String となります。

pch.h

pch.h
#pragma once
#include <unknwn.h>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Globalization.h>

使いたい機能に合わせてヘッダファイルの記述を追加します。

C#側

MainWindows.xaml.cs
        private void Button_Click(object sender, RoutedEventArgs e)
        {
            var result = RuntimeComponent1.Class.Analyze("本日は晴天なり");
            MessageBox.Show(string.Join(",", result));
        }

C#側のプロジェクトで、参照にC++側のプロジェクトを追加してビルド・実行し、「本日,は,晴天,なり」と単語単位で区切られたメッセージが出たら成功です。
image.png

身も蓋もない話

正直この程度だったらブリッジを作るまでもなくて、以下の方法でもっと手軽にできたりします。
[C#] デスクトップアプリ (WPF) から手軽にWinRT APIを活用しよう - Qiita

ただポインタやATLなど色々触り出すと、下手にC#で書くよりC++でブリッジを作ったほうがたぶん楽です。

トラブルシューティング

ブリッジクラスがC#側から見えない

C#側から参照にC++プロジェクトを追加していても見えない場合は、idlファイルの記述をご確認ください。
idlファイルを編集したら「リビルド」したほうが良いです。

エラー C2660 'winrt::RuntimeComponent1::implementation::Class::MyMethod': 関数に 1 個の引数を指定できません。

Class.idl に書いた名前と引数で呼び出せるメソッドが、Class.cpp, Class.h 側に定義されていないと思われます。(オーバーロードは可能ですが、idlの記述に不足がないようにご注意ください)

System.TypeLoadException: '要求された Windows ランタイム型 'RuntimeComponent1.Class' は登録されていません。'

app.manifest の <file><activatableClass> の記述が不足しているようです。

System.BadImageFormatException: ' は有効な Win32 アプリケーションではありません。 (HRESULT からの例外:0x800700C1)'

C++側とC#側のプラットフォーム設定が合っていないみたいです。x64同士、またはx86/Win32の組み合わせであることを確認してください。前述のように Any CPU はダメです。

System.IO.FileNotFoundException: '指定されたモジュールが見つかりません。 (HRESULT からの例外:0x8007007E)'

まずは RuntimeComponent1 のビルドが成功していて、C#側のプロジェクトで指定したexeの出力先と同じ場所に RuntimeComponent1.dll がコピーされているか確認してください。
もし RuntimeComponent1.dll があるのにこのエラーが出るときは、VCRT ForwardersのDLLが見つからないのが原因と思われます。

  • NuGetパッケージマネージャーの設定で、既定のパッケージ管理方法を「Packages.config」にする
  • C#側のプロジェクトに Microsoft.VCRTForwarders.140 を追加する(既に追加されている場合で、後からPackages.configの設定を変えたときは、VCRTForwardersを一度削除して再度追加する)

ビルドしたあとに bin\x64\Debug の下などに msvcp140vcruntime140 などの名前で始まるDLLが大量にコピーされたら成功です。
image.png

0x00007FF90CD1DD44 (RuntimeComponent1.dll) で例外がスローされました (WpfApp1.exe 内): 0xC0000005: 場所 0x0000000000000000 の読み取り中にアクセス違反が発生しました

IVector の初期化ができているかご確認ください。winrt::single_threaded_vector をお忘れなく。

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

「ポケモン」から学ぶ、プログラミング入門者向けの、クラスとメソッドの概念

はじめに

プログラミングを学ぶ方が壁にぶつかるポイントは、いくつかあります。
条件分岐と繰り返し。
配列。
そして、クラスとメソッド。

特にクラスとメソッドは、「概念は分かるけど、一体なにが便利なんだ!?」という風に思う方がほとんどだと思います。例えば、

  • 犬を表す、「Dog」クラス
  • 年齢、性別、名前というプロパティ
  • 歩く、「walk」メソッド
  • Dogクラスは設計書。実際にはその設計書をもとに、実体化(インスタンス化)する

image.png

・・・なんて言われても、「なんのこっちゃ!?」となるに決まっています。この説明だと、分かるわけがないんですね。
もう少し身近な例で、かつ用途が分かる例での解説が、必要になってくるわけです。
そこで今回は、誰もが一度はやったことがあるであろう、「ポケモン」を使用して、解説していきます。

注意

  • この記事は、「一度はクラス・メソッドを勉強したことがある人」「でも、あんまりよくわからなかった人」がターゲットです。まだクラス・メソッドを勉強したことがない人は、一度は教材などから、勉強することをオススメします。

  • この記事は、ポケモンをやったことがない方が読んでも、さっぱりな内容になってます。
    ポケモンをやったことがない方は、ぜひこちらのリンクに飛んでください。
    https://www.pokemon.co.jp/ex/sword_shield/
    https://www.pokemon.co.jp/ex/VCAMAP/

  • ポケモンは、基本的に初代仕様です。

  • なるべくわかりやすくするため、一部無理やりな実装部分があります。また、あえて説明していないところもいくつかあります。
    以下の内容より良い実装、リファクタリングできる場所は、非常に多数あります。細かい事は気にせず、ざっくりとした概念を捉えていただけると嬉しいです。

ポケモンをプログラミングするには?

突然ですが、「ポケモンをプログラミングしてください」と言われたとき、皆さんはどのように実装しますか?

もし、クラスをちゃんと理解していない方が頑張って実装しようとすると、以下のように書くんじゃないかと思います。

using System;

namespace Pokemon
{
    class Program
    {
        static void Main(string[] args)
        {
            // 手持ちポケモン作成
            string[] pokemons = new string[] { "ピカチュウ", "カイリュー", "ヤドラン", "ピジョン", "コダック", "コラッタ" };

            // 手持ちポケモンのタイプ。上の配列と同じ要素番号で管理。TODO:複合タイプ
            string[] types = new string[] { "でんき", "ドラゴン", "みず", "ひこう", "みず", "ノーマル" };

            // 手持ちポケモンのレベル。上の配列と同じ要素番号で管理。
            int[] levels = new int[] { 50, 62, 28, 57, 78, 18 };

            // 以下、諸々の処理を頑張って書く・・・
        }
    }
}

・・・もうこの時点で、どこかで必ず破綻するのは、火の中水の中草の中、あの子のスカートの中を見るよりも明らかですね。

では、どのように実装するのがいいでしょうか?一緒に考えていきましょう。

すべてのポケモンに共通する情報を考える

ポケモンは多種多様います。その数、全部で151匹です。(byおじいちゃん博士)

それだけたくさんのポケモンがいますが、すべてのポケモンが、共通してもつ情報があります。まずはそれについて、考えていきましょう。

すべてのポケモンが共通してもつ情報(一例1)

どんなポケモンの種類でも、以下のような情報は、共通で持っています。

  • 図鑑NO
  • 名前
  • ニックネーム
  • レベル
  • わざ1
  • わざ2
  • わざ3
  • わざ4
  • タイプ1
  • タイプ2

image.png

このような情報は、すべてのポケモンが持っています。例外はありません。レベルの持たないポケモンなんていませんよね。遊戯王じゃあるまいし。

この内容を実装すると、以下のようになります。

using System;
using System.Collections.Generic;
using System.Text;

namespace Pokemon
{
    public class Pokemon
    {
        /// <summary>
        /// 図鑑番号
        /// </summary>
        public string ZukanNo;

        /// <summary>
        /// 名前
        /// </summary>
        public string Name;

        /// <summary>
        /// ニックネーム
        /// </summary>
        public string Nickname;

        /// <summary>
        /// レベル
        /// </summary>
        public int Level;

        /// <summary>
        /// わざ1
        /// </summary>
        public string Waza1;

        /// <summary>
        /// わざ2
        /// </summary>
        public string Waza2;

        /// <summary>
        /// わざ3
        /// </summary>
        public string Waza3;

        /// <summary>
        /// わざ4
        /// </summary>
        public string Waza4;

        /// <summary>
        /// タイプ1
        /// </summary>
        public string Type1;

        /// <summary>
        /// タイプ2
        /// </summary>
        public string Type2;
    }
}

すべてのポケモンが共通してもつ情報(HP、攻撃力、素早さなど)

すべてのポケモンには、HP、攻撃力、素早さといった情報を持っています。バトルで使用するパラメータですね。
これらのパラメータも、同じようにすべてのポケモンが持っているのですが、このパラメータの持ち方は、ちょっとややこしいです。

ポケモンのHPなどのパラメータは、「種族値」「個体値」「レベル」といった要素(ステータス)で決まってきます。
レベルはさておき、種族値、個体値は以下の内容です。

  • 種族値:ポケモンの種類によって決まってくる値。「ピカチュウは攻撃力低いけど、カイリキーは攻撃力高い!」というのは、この「種族値」の値で決まる。
  • 個体値:同じポケモンの種類でも、その1体1体のポケモンによって、強さが変わる。「なんか、同じレベルなのに、ライチュウAより、ライチュウBの方が素早さが早いぞ!?」というのは、この「個体値」の値で決まる。

といったといった内容で、ゲームには表示されない、隠しパラメータになります。
(※本当は努力値とか性格とかも絡んでくるのですが、ここでは割愛します)

そのため、ゲーム画面で表示される「攻撃力:150」「素早さ:200」というステータスは、実際には「種族値」「個体値」「レベル」といった要素の計算式で、決まってきます。

※「???」となってしまった方がいるかもしれません。これらの内容は説明したい内容の本筋じゃないので、「攻撃力とかのステータスは、色んな値の組み合わせで決めるんだ!」とだけ理解してください。


これらの内容を、以下の図、コードでまとめてみます。※ステータスは、最大HP、攻撃力、素早さのみ記載してます。防御力、特殊は割愛してます。

image.png

using System;
using System.Collections.Generic;
using System.Text;

namespace Pokemon
{
    public class Pokemon
    {
        // 割愛

        /// <summary>
        /// 最大HP種族値
        /// </summary>
        protected int syuzokuMaxHP;

        /// <summary>
        /// 攻撃力種族値
        /// </summary>
        protected int syuzokuKougeki;

        /// <summary>
        /// 素早さ種族値
        /// </summary>
        protected int syuzokuSubayasa;

        /// <summary>
        /// 最大HP個体値
        /// </summary>
        protected int kotaichiMaxHP;

        /// <summary>
        /// 攻撃力個体値
        /// </summary>
        protected int kotaichiKougeki;

        /// <summary>
        /// 素早さ個体値
        /// </summary>
        protected int kotaichiSubayasa;


        /// <summary>
        /// 最大HP取得
        /// </summary>
        /// <returns></returns>
        public int GetMaxHP()
        {
            // この式はホンモノじゃないよ
            return (int)Math.Floor((decimal)(syuzokuMaxHP + kotaichiMaxHP) * 2 + (decimal)(Level / 100)) + Level + 10;
        }

        /// <summary>
        /// 攻撃力取得
        /// </summary>
        /// <returns></returns>
        public int GetKougeki()
        {
            return getStatus(syuzokuKougeki, kotaichiKougeki);
        }

        /// <summary>
        /// 素早さ取得
        /// </summary>
        /// <returns></returns>
        public int GetSubayasa()
        {
            return getStatus(syuzokuSubayasa, kotaichiSubayasa);
        }

        /// <summary>
        /// 種族値と個体値から、ステータスを取得(HP以外で使用)
        /// ※ホンモノじゃないよ
        /// </summary>
        /// <param name="syuzokuchi">種族値</param>
        /// <param name="kotaichi">個体値</param>
        /// <returns></returns>
        protected int getStatus(int syuzokuchi, int kotaichi)
        {
            return (int)Math.Floor((decimal)(syuzokuchi + kotaichi) * 2 + (decimal)(Level / 100)) + 5;
        }
    }
}

攻撃力などのステータスが、計算式で出されていますね。そう、これがメソッドです。
例えば「攻撃力をゲーム画面に表示する」という状況になったとき、このGetKougekiメソッドを実行します。それにより、それぞれのポケモンが持つ他の値を使用して、攻撃力を求めて画面表示するわけです。
この計算式は、基本的にどのポケモンでも共通して使用されます。

それぞれのポケモンが持つ情報

すべてのポケモンが共通してもつ情報を定義しました。
今度はそれぞれのポケモンの種類が持つ情報を定義したり、セットしていきます。

例えば、「プリン」というポケモンを考えていきます。

ちなみに僕は小学生時代、プリンが大好きすぎて、周りからドン引きされてました。ぴえん。

プリンというポケモンは、以下のような情報が固定で決まっています。

  • 図鑑NO:039
  • 名前:プリン
  • タイプ1:ノーマル
  • タイプ2:なし
  • 種族値HP:115
  • 種族値攻撃:45
  • 種族値防御:20
  • 種族値特殊:25
  • 種族値素早さ:20

いつ、どこで捕まえたプリンでも、この情報は固定です。
この「プリン」というポケモンを実装する場合、きっと以下のような、Purinクラスで、実装を行います。

using System;
using System.Collections.Generic;
using System.Text;

namespace Pokemon
{
    public class Purin : Pokemon 
    {
        /// <summary>
        /// 図鑑番号
        /// </summary>
        public string ZukanNo = "039";

        /// <summary>
        /// 名前
        /// </summary>
        public string Name = "プリン";

        /// <summary>
        /// タイプ1
        /// </summary>
        public string Type1 = "ノーマル";

        /// <summary>
        /// 最大HP種族値
        /// </summary>
        protected int syuzokuMaxHP = 115;

        /// <summary>
        /// 攻撃力種族値
        /// </summary>
        protected int syuzokuKougeki = 45;

        /// <summary>
        /// 素早さ種族値
        /// </summary>
        protected int syuzokuSubayasa = 20;

    }
}

「class Purin : Pokemon」という書き方をしています。
これが、継承です。上記で作成した「Pokemon」というクラスをベースに、Purin用のクラスを作成しているわけですね。

他にも、ライチュウ、カイリキーといったポケモンも、以下のような実装となるはずです。

using System;
using System.Collections.Generic;
using System.Text;

namespace Pokemon
{
    public class Raicyu : Pokemon 
    {
        /// <summary>
        /// 図鑑番号
        /// </summary>
        public string ZukanNo = "026";

        /// <summary>
        /// 名前
        /// </summary>
        public string Name = "ライチュウ";

        /// <summary>
        /// タイプ1
        /// </summary>
        public string Type1 = "でんき";

        /// <summary>
        /// 最大HP種族値
        /// </summary>
        protected int syuzokuMaxHP = 60;

        /// <summary>
        /// 攻撃力種族値
        /// </summary>
        protected int syuzokuKougeki = 90;

        /// <summary>
        /// 素早さ種族値
        /// </summary>
        protected int syuzokuSubayasa = 100;
    }


    public class Kairiki : Pokemon
    {
        /// <summary>
        /// 図鑑番号
        /// </summary>
        public string ZukanNo = "068";

        /// <summary>
        /// 名前
        /// </summary>
        public string Name = "カイリキー";

        /// <summary>
        /// タイプ1
        /// </summary>
        public string Type1 = "かくとう";

        /// <summary>
        /// 最大HP種族値
        /// </summary>
        protected int syuzokuMaxHP = 90;

        /// <summary>
        /// 攻撃力種族値
        /// </summary>
        protected int syuzokuKougeki = 130;

        /// <summary>
        /// 素早さ種族値
        /// </summary>
        protected int syuzokuSubayasa = 55;
    }
}


つまり、これらのクラスは、ポケモンの数だけ作成されていきます。
図に書くと、以下のようなものになります。

image.png

「とびだしてきた!」でインスタンス化

ここまでが、クラスの説明で、いわゆる「設計書」と呼ばれる部分です。
ここから、いよいよ実体化、インスタンス化していきます。

3番道路の草むらを歩いていると、草むらからプリンが飛び出してきます。
「あ! やせいの プリンが とびだしてきた!」
この瞬間、プリンが実体化します。
Purinクラスという設計書をもとに、実際にプリンが飛び出してくるわけです。

そして、このタイミングで、そのポケモンの「レベル」、「個体値」、「覚えている技」といった値が決定します。
レベルは、どの草むらで出会ったかによって、ある程度の範囲でランダムに決められます。
個体値の、一定の範囲でランダムに決められます。

これを実装すると、以下のようになるでしょうか。

////// Pokemon.cs
using System;
using System.Collections.Generic;
using System.Text;

namespace Pokemon
{
    public class Pokemon
    {
        /// <summary>
        /// コンストラクタ。とびだしてきた!時に実施される
        /// </summary>
        public Pokemon(int level)
        {
            ////// レベル、個体値、覚えている技を設定する
            // レベル
            Level = level;

            // 個体値。初代は0~15の範囲でランダムに決定する
            Random r = new System.Random();
            kotaichiMaxHP = r.Next(0, 16);
            kotaichiKougeki = r.Next(0, 16);
            kotaichiSubayasa = r.Next(0, 16);

            //// TODO:とびだしてきた時に覚えている技を作成
        }
    }
}


////// Purin.cs
using System;
using System.Collections.Generic;
using System.Text;

namespace Pokemon
{
    public class Purin : Pokemon
    {
        // コンストラクタ追加
        public Purin(int level) : base(level)
        {
        }
    }
}


////// Program.cs
using System;

namespace Pokemon
{
    class Program
    {
        static void Main(string[] args)
        {
            // やせいの プリンが とびだしてきた! ----------------------------------------------------

            // 1体目:レベル3のプリン
            Purin purin1 = new Purin(3);

            // 2体目:レベル5のプリン
            Purin purin2 = new Purin(5);

            // 3体目:レベル7のプリン
            Purin purin3 = new Purin(7);
        }
    }
}

Pokemonクラスのコンストラクタで、「とびだしてきた!」時の処理を実装しています。
Program.csで、3番道路でプリンがとびだしてきた時に、インスタンス化します。
そのタイミングで、初期レベル、個体値、覚えている技が決定します。

これらの値は、草むらで出会ってから、ずっと使用されます。
「あるタイミングで急にレベル1になる」とか、「あるタイミングでは急に攻撃力の個体値がめっちゃ強くなった!」ということは、基本的には無いわけです。

この「とびだしてきた!」をきっかけにして、どんどんポケモンがインスタンス化していきます。
Purinクラスという設計書をもとに、どんどんプリンが量産化されていきます。桃源郷ですね。
イメージとしては、以下のようになるでしょうか。

image.png

いろんなケースでクラスとメソッドを考えてみる

ここからは、もう少し別のケースを見て、クラスのメソッドを理解していきましょう。

技を覚える・忘れる

ポケモンバトルによってレベルが上がると、ポケモンは技を覚えていきます。
その技について、「手持ちの技は4つまで」というルールがあります。
そのため、以下のような流れとなります。

  • 現在のそのポケモンがもつ、技の数を確認する
  • 技が3つ以下であれば、その技を覚えさせる(インスタンス化された「わざ」プロパティにセットする)
  • 技が4つの場合、プレイヤーに技を忘れさせるか確認する
  • 忘れさせない場合、終了する
  • 忘れさせる場合、プレイヤーに忘れさせる技を選択させる
  • 選択した技を忘れさせる(削除する)
  • 新しく覚えた技をセットする

なので、「技を覚える」というメソッドを実装するとしたら、こんな内容になるはずです。

using System;
using System.Collections.Generic;
using System.Text;

namespace Pokemon
{
    public class Pokemon
    {
        /// <summary>
        /// 技を覚える
        /// </summary>
        /// <param name="wazaName"></param>
        public void AddWaza(string wazaName)
        {
            // 技を覚えている数が3つ以下の場合、技をセットして終了
            if(string.IsNullOrEmpty(Waza1) || string.IsNullOrEmpty(Waza2) || string.IsNullOrEmpty(Waza3) || string.IsNullOrEmpty(Waza4))
            {
                // setWazaメソッド:新しい技をセット
                setWaza(wazaName);
                return;
            }

            // 技が4つの場合、プレイヤーに技を忘れさせるか確認する
            // askForgetWazaメソッド:技を忘れさせるか確認
            // 忘れさせない場合、終了する
            if (!askForgetWaza(wazaName))
            {
                return;
            }

            // 忘れさせる場合、プレイヤーに忘れさせる技を選択させる
            // selectForgetWazaメソッド:忘れさせる技を選択して忘れさせる
            selectForgetWaza(wazaName);

            // setWazaメソッド:新しい技をセット
            setWaza(wazaName);
        }

        /// <summary>
        /// 新しい技をセット
        /// </summary>
        /// <param name="wazaName"></param>
        protected void setWaza(string wazaName)
        {
            if (string.IsNullOrEmpty(Waza1))
            {
                Waza1 = wazaName;
            }
            else if (string.IsNullOrEmpty(Waza2))
            {
                Waza2 = wazaName;
            }
            else if (string.IsNullOrEmpty(Waza3))
            {
                Waza3 = wazaName;
            }
            else if (string.IsNullOrEmpty(Waza4))
            {
                Waza4 = wazaName;
            }
            return;
        }
    }
}

「技」の実装方法

上記のコードで、「技」について触れました。string wazaNameとして、技名文字列として実装しています。
でも、本当に「技」情報を、文字列として実装して、いいのでしょうか?

「そのポケモンが覚えている技」というものを、単に「技の名前文字列」(ハイドロポンプとか、かえんほうしゃとか)で管理しようとすると、ポケモンバトルの際に、やっぱり破綻します。プログラミング始めたての方が陥る現象です。

ポケモンには、さまざまな種類の「技」があります。
そして、その「技」には、さまざまな共通の情報があります。例えば、以下のような内容です。

  • 名前
  • タイプ
  • 威力
  • PP
  • 攻撃・補助
  • 物理・特殊
  • 命中率
  • 追加効果

そして、例えば「はたく」という技は、以下のような効果があります。

  • 名前:はたく
  • タイプ:ノーマル
  • 威力:40
  • PP:35
  • 攻撃・補助:攻撃
  • 物理・特殊:物理
  • 命中率:100
  • 追加効果:なし

「うたう」という技は、以下のような効果があります。

  • 名前:うたう
  • タイプ:ノーマル
  • 威力:なし
  • PP:15
  • 攻撃・補助:補助
  • 物理・特殊:なし
  • 命中率:55
  • 追加効果:相手を眠らせる

そう、つまり何が言いたいかというと、ポケモンの種類と同様に、技についても、クラスで実装できるということです。

Wazaクラスを実装するのであれば、以下のようになるでしょうか。

using System;
using System.Collections.Generic;
using System.Text;

namespace Pokemon
{
    /// <summary>
    /// 技クラス
    /// </summary>
    public class Waza
    {
        /// <summary>
        /// 技名
        /// </summary>
        public string Name;

        /// <summary>
        /// タイプ
        /// </summary>
        public string Type;

        /// <summary>
        /// 威力
        /// </summary>
        public int Power;

        /// <summary>
        /// PP
        /// </summary>
        public int PP;

        /// <summary>
        /// 攻撃かどうか
        /// </summary>
        public bool IsAttack;

        /// <summary>
        /// 物理かどうか
        /// </summary>
        public bool IsButsuri;

        /// <summary>
        /// 命中率
        /// </summary>
        public int HitRate;
    }


    /// <summary>
    /// はたくクラス
    /// </summary>
    public class Hataku : Waza
    {
        /// <summary>
        /// 技名
        /// </summary>
        public string Name = "はたく";

        /// <summary>
        /// タイプ
        /// </summary>
        public string Type = "ノーマル";

        /// <summary>
        /// 威力
        /// </summary>
        public int Power = 40;

        /// <summary>
        /// PP
        /// </summary>
        public int PP = 35;

        /// <summary>
        /// 攻撃かどうか
        /// </summary>
        public bool IsAttack = true;

        /// <summary>
        /// 物理かどうか
        /// </summary>
        public bool IsButsuri = true;

        /// <summary>
        /// 命中率
        /// </summary>
        public int HitRate = 100;
    }


    /// <summary>
    /// うたうクラス
    /// </summary>
    public class Utau : Waza
    {
        /// <summary>
        /// 技名
        /// </summary>
        public string Name = "うたう";

        /// <summary>
        /// タイプ
        /// </summary>
        public string Type = "ノーマル";

        /// <summary>
        /// PP
        /// </summary>
        public int PP = 15;

        /// <summary>
        /// 攻撃かどうか
        /// </summary>
        public bool IsAttack = false;

        /// <summary>
        /// 物理かどうか
        /// </summary>
        public bool IsButsuri = false;

        /// <summary>
        /// 命中率
        /// </summary>
        public int HitRate = 55;
    }
}

ポケモン側の実装も、このようになります。わざ1~4が、string型から、Wazaクラスに変わりました。

using System;
using System.Collections.Generic;
using System.Text;

namespace Pokemon
{
    public class Pokemon
    {
        /// <summary>
        /// わざ1
        /// </summary>
        //public string Waza1;
        public Waza Waza1;

        /// <summary>
        /// わざ2
        /// </summary>
        //public string Waza2;
        public Waza Waza2;

        /// <summary>
        /// わざ3
        /// </summary>
        //public string Waza3;
        public Waza Waza3;

        /// <summary>
        /// わざ4
        /// </summary>
        //public string Waza4;
        public Waza Waza4;
    }
}

そのポケモンが持っている技の型も、Wazaクラスになるわけです。
こちらの方が、ポケモンバトルで技を使用する際に、圧倒的に使いやすくなります。
他にも、上記で書いた、「技を覚える」というメソッドも、Wazaクラスに変わることで、圧倒的に実装しやすくなるでしょう。

このように、「クラスで実装した方がいい」ものは、他にも非常に多数あります。
例を挙げれば、以下のようなものでしょうか。

  • タイプ
  • 技の追加効果(共通の追加効果は多数あるので)
  • トレーナー

これらの内容を、どう実装すればいいだろう?ということを、プログラミングはしなくても、設計して考えてみることをオススメします。

ヌケニンで学ぶオーバーライド

これまでの説明で、「攻撃力などのステータスは、種族値や個体値やレベルなどで計算して決まる」「計算式は、すべてのポケモンで共通している」と書きました。
しかし、初代のポケモンではないのですが、ここで例外となるポケモンがいます。「ヌケニン」というポケモンです。

ヌケニンは、「レベルや個体値などに関わらず、HPが常に1」というポケモンです。HP1なんですよ。とんでもないポケモンですね。

では、このヌケニンを実現するために、どのような実装を行えばいいでしょうか?
ヌケニンの実装には、「オーバーライド」という方法が非常に役立ちます。

using System;
using System.Collections.Generic;
using System.Text;

namespace Pokemon
{
    public class Nukenin : Pokemon
    {
        /// <summary>
        /// 最大HP取得。ヌケニン専用
        /// </summary>
        /// <returns></returns>
        public new int GetMaxHP()
        {
            // ヌケニンのHPは常に1
            return 1;
        }
    }
}

このように、NukeninクラスのGetHPメソッドで、"return 1;"だけする処理を記載します。
そうすることで、ゲーム画面やバトルでHPを使用するとき、全ポケモン共通の計算式を使用せず、「常に1」ということを実現できます。

これが、オーバーライドです。共通のものがあっても、独自の処理を実装できるので、非常に便利です。

このような「独自の処理」が必要なものは、ポケモンには多数ありそうです。
例えば、以下のようなものです。それらの処理をどうやって実装するか?という所を、考えてみると良さそうです。

  • わざ「フライングプレス」:「格闘」と「飛行」タイプの技として扱う
  • わざ「フリーズドライ」:氷タイプの技だけど、水タイプの技にも効果抜群となる
  • ポケモン「パッチール」の、見た目の模様:パッチールの模様は、全部で4,294,967,296通りある

まとめ:さらなる理解のために

以上で、ポケモンについての解説は終了です。
「よく分かった!」という人もいれば、「やっぱりさっぱり!」という人もいるかもしれません。

大切なのは、「自分で設計して、自分で実装してみる」ことです。まずは設計だけでも構いません。
そして、クラスやメソッドの実装の練習を行うためには、身近な、自分が愛着を持っている内容がオススメです。
「これらの内容を、自分でプログラミングするなら、どうやって設計するかな・・・?」ということを、自分で考えてみるのが、すごく大切です。
例を挙げれば、こんな内容でしょうか。

  • マリオの敵キャラ
  • メタルギアソリッドの武器
  • ときメモの各キャラのステータスや高感度
  • 遊戯王の各カード

そして、ここまで自分が書いたコードも、かなりの勢いで改善点があります。例えば、以下のような内容です。

  • 種族値やタイプは、各ポケモンで固定だけれども、今の実装だと、何らかの理由で上書きをすることができてしまう。なので、これらの値は読み取り専用で実装するのが良い。
  • 「やせいの ポケモンが とびだしてきた!」での、ポケモンのレベル・個体値・最初に覚える技の決定処理を、今はコンストラクタで実施している。しかし、コンストラクタで実装すると、例えば「セーブデータから『続きから』のデータを読み込んで、プリンの情報を取得する」という処理でも、この「とびだしてきた処理」が実行されてしまう。なので本当は、別の方法で「とびだしてきた処理」を実装した方が良い。

対戦機能、セーブデータから読み込み機能・・・といった機能を実装していくうちに、こういった問題にぶち当たることは、よくあります。そのたびにリファクタリングして、都度対応していく必要があります。

以前、入門者向けにブラックジャックの記事も書いたのですが、分かりやすい・取っつきやすい例から、自分で手を動かして、「あーでもないこーでもない」を繰り返し、理解・定着していくことが、何よりも重要です。

この記事を読んだ方が、少しでもクラスとメソッドについて理解が進んでくれていたら、すごく嬉しいです。

THE END

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

Update your C# in Unity ~ ローカル関数 ~

Unityにおいて古いC#しか使えない時代もありました。しかし、それは過去のことです。本稿執筆時の最新LTSであるUnity 2019.4ではC# 7.3がサポートされています。また、本稿執筆時の最新Beta版であるUnity 2020.2ではC# 8.0がサポート予定です。

長らくUnityで古いC#しか使えなかったことで、「C#にこんな機能あるのか?知らなかった!」となることがある方も多いのではないでしょうか?この「Update your C# in Unity」シリーズでは、「C#の比較的新しい機能をUnityでこんな風に使えるよ!」という紹介を行います。


言語機能名: ローカル関数
追加バージョン: C# 7.0でローカル関数、C# 8.0で静的ローカル関数
説明: 関数を他の関数の中に入れ子にし、定義できる


ローカル関数を使うことで、関数の中に入れ子で関数を定義することができます。

private関数はその型のメンバーからは呼び出すことができます。一方でローカル関数は、その関数の中でしか呼び出せない、より狭いスコープの関数です。スコープを非常に狭めたく、かつ処理に名前をつけたい場合、ローカル関数を検討してください。


ローカル関数の利用例はLINQライクな関数の実装です。

次のコードはLINQライクなMapメソッドの実装の、よくない例です。仮引数であるsourceやpredicateがnullの場合、関数呼び出しのタイミングでArgumentNullExceptionが投げられることを期待したいところです。しかし、この実装だと返り値の要素を、最初に列挙するタイミングまでArgumentNullException投げられません。これにより、実行時の思わぬ不具合の原因になってしまうことがあります。

public static class MyEnumerable
{
        public static IEnumerable<TResult> Map<TSource, TResult> (this IEnumerable<TSource> source, Func<TSource,TResult> selector)
        {
            if (source == null)
                throw new ArgumentNullException ("source");
            if (selector == null)
                throw new ArgumentNullException ("predicate");

            foreach (TSource element in source) {
                yield return selector (element);
            }
        }
    }
}

関数呼び出しのタイミングでArgumentNullExceptionが投げられることするためには、次のように二つの関数に分割する必要があります。

public static IEnumerable<TResult> Map<TSource, TResult> (this IEnumerable<TSource> source, Func<TSource,TResult> selector)
{
    if (source == null)
        throw new ArgumentNullException ("source");
    if (selector == null)
        throw new ArgumentNullException ("selector");

    return source.Map_ (selector);
}

private static IEnumerable<TResult> Map_<TSource, TResult> (this IEnumerable<TSource> source, Func<TSource,TResult> selector)
{
    foreach (TSource element in source) {
        yield return selector (element);
    }
}

これで期待通り関数呼び出しのタイミングでArgumentNullExceptionが投げられます。しかし、Mapからしか呼ばないMap_ができてしまいました。
これはローカル関数を利用することで、次にように改善することができます。

public static IEnumerable<TResult> Map<TSource, TResult> (this IEnumerable<TSource> source, Func<TSource,TResult> selector)
{
    if (source == null)
        throw new ArgumentNullException ("source");
    if (selector == null)
        throw new ArgumentNullException ("selector");

    return Impl (source, selector);

    IEnumerable<TResult> Impl(IEnumerable<TSource> source_, Func<TSource,TResult> selector_)
    {
        foreach (TSource element in source_) {
            yield return selector_ (element);
        }
    }
}

次のように、ローカル関数から外側の関数の変数をキャプチャすることもできます。

public static IEnumerable<TResult> Map<TSource, TResult> (this IEnumerable<TSource> source, Func<TSource,TResult> selector)
{
    if (source == null)
        throw new ArgumentNullException ("source");
    if (selector == null)
        throw new ArgumentNullException ("selector");

    return Impl ();

    // 外側の関数の引数や変数をキャプチャできる
    IEnumerable<TResult> Impl ()
    {
        foreach (TSource element in source) {
            yield return selector (element);
        }
    }
}

ローカル関数から外側の関数の変数をキャプチャすることが必要な場合や便利な場合もあります。
しかし、うっかり意図と違うキャプチャをしてしまい不具合の原因になることがあります。

これを解決するために、C# 8.0から静的ローカル関数が加わりました。
静的なローカル関数では、外側の関数の変数をキャプチャするとコンパイルエラーになります。
これにより、意図しない外側の関数の変数のキャプチャを防ぐことができます。
static修飾子をローカル関数につけることで静的なローカル関数になります。

public static IEnumerable<TResult> Map<TSource, TResult> (this IEnumerable<TSource> source, Func<TSource,TResult> selector)
{
    if (source == null)
        throw new ArgumentNullException ("source");
    if (selector == null)
        throw new ArgumentNullException ("selector");

    return Impl (source, selector);

    // C# 8.0から使える
    // staticがついていると静的ローカル関数
    // 外側の関数の引数や変数のキャプチャを許さない
    static IEnumerable<TResult> Impl(IEnumerable<TSource> source_, Func<TSource,TResult> selector_)
    {
        foreach (TSource element in source_) {
            yield return selector_ (element);
        }
    }
}

Unityでの利用例をあげます。

次のように、IEumeratorを返すLaunchImpl関数と、それを内部で呼び出しCoroutineを返すLaunch関数を定義していたとします。IEnumeratorを返すLaunchImpl関数は、Coroutineを返すLaunch関数からしか利用していません。StartCoroutineの仕様上、このように二つの関数を分離する必要があります。

using System.Collections;
using UnityEngine;

public class Launcher : MonoBehaviour
{
    public Coroutine Launch()
    {
        return StartCoroutine(LaunchImpl());
    }

    private IEnumerator LaunchImpl()
    {
        // 略
        yield break;
    }
}

これはローカル関数を使い次のように定義することもできます。

public class Launcher : MonoBehaviour
{
    public Coroutine Launch()
    {
        return StartCoroutine(LaunchImpl());

        static IEnumerator LaunchImpl()
        {
            // 略
            yield break;
        }
    }
}

ローカル関数を使うことで、関数のスコープと可視性を関数内のスコープだけに制限することができます。
private関数より狭いスコープで定義することができます。
スコープを非常に狭めたく、かつ処理に名前をつかたい場合、ローカル関数を検討してください。

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

ReactivePropertyの初期化が面倒なのでまとめて初期化してみる(Plain Object版)

M <-> VM をBindするのにプロパティ一個一個 ToReactiveProperty() していくのが面倒くさい

特に INotifyPropertyChanged を実装できない PlainObject の場合。

    public class ReactiveBinder<TSource, TTarget>
    {
        private readonly static Dictionary<string, Action<TSource, TTarget>> binderActions = new Dictionary<string, Action<TSource, TTarget>>();

        static ReactiveBinder()
        {
            BuildBinders();
        }

        public void Bind(TSource source, TTarget target)
        {
            foreach(var action in binderActions)
            {
                action.Value.Invoke(source, target);
            }
        }

        private static void BuildBinders()
        {
            var targetType = typeof(TTarget);
            var sourceType = typeof(TSource);
            MethodInfo fromObject = GetBinderMethod(targetType);

            var sourceParameter = Expression.Parameter(sourceType, "$source");
            var targetParameter = Expression.Parameter(targetType, "$destination");
            var modeParameter = Expression.Constant(ReactivePropertyMode.Default);
            var ignoreValidationErrorValue = Expression.Constant(false);

            foreach (var property in targetType.GetProperties())
            {
                var sourceProperty = sourceType.GetProperty(property.Name);
                var sourcePropertyAccessor = Expression.Property(
                    Expression.Convert(sourceParameter, sourceType),
                    property.Name);

                var propertySelector = Expression.Lambda(sourcePropertyAccessor, sourceParameter);

                var genericMethod = fromObject.MakeGenericMethod(new Type[] { sourceType, sourceProperty.PropertyType });
                var fromObjectCall = Expression.Call(genericMethod, sourceParameter, propertySelector, modeParameter, ignoreValidationErrorValue);

                var bindingProperty = Expression.Property(
                    targetParameter,
                    property);

                var assignment = Expression.Assign(bindingProperty, fromObjectCall);

                var lambda = Expression.Lambda<Action<TSource, TTarget>>(
                    assignment,
                    sourceParameter,
                    targetParameter
                    );

                binderActions.Add(property.Name, lambda.Compile());
            }
        }
        private static MethodInfo GetBinderMethod(Type targetType)
        {
            MethodInfo fromObject = null;
            Type[] paramTypes = new Type[] {
                targetType,
                typeof(int),
                typeof(ReactivePropertyMode),
                typeof(bool)
            };
            var flags = BindingFlags.Static | BindingFlags.Public;
            foreach (MethodInfo method in typeof(ReactiveProperty).GetMethods(flags))
            {

                if (method.IsGenericMethod && method.IsGenericMethodDefinition && method.ContainsGenericParameters)
                {
                    if (method.Name == "FromObject" && method.GetParameters().Length == paramTypes.Length)
                    {
                        fromObject = method;
                        break;
                    }
                }
            }
            return fromObject;
        }
    }

Refletion と Expressionを駆使してViewModel のプロパティに ReactiveProperty.FromObject() の呼び出しを代入していく。

Example

Model

    public class SampleModel
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public DateTime? UpdateDateTime { get; set; }
        public bool Flag { get; set; }
    }

ViewModel

    public class SampleItemViewModel : BindableBase, INotifyPropertyChanged
    {

        public ReactiveProperty<int> Id { get; set; }
        public ReactiveProperty<string> Name { get; set; }
        public ReactiveProperty<DateTime?> UpdateDateTime { get; set; }
        public ReactiveProperty<bool> Flag { get; set; }

        public SampleItemViewModel()
        {

        }

        public SampleItemViewModel(SampleModel model)
        {
            var binder = new ReactiveBinder<SampleModel, SampleItemViewModel>();
            binder.Bind(model, this);
        }
    }

注: FromObjectメソッドを取得するのにパラメータの数の一致だけ見てるけど、オーバーロードはパラメータの数ではなくてパラメータの型をチェックするはずなので破綻する可能性がある。

既にありがちなんだけどまあ Expression の練習と思って。
オブジェクト構築のコストなんぞ、知らん。

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

.NET Core 3.1 GUIテーマ

■はじめに

.NET Core 3.1Visual Studioでコントロールの見栄えを変えたり標準以外のコントロールを使ってみます。
000.png

キーワード:ライト/ダークテーマ, NumericUpDown, プログレスリング, ウォーターマーク, トグルスイッチ, InputBox

■環境

  • Windows 10
  • Visual Studio 2019 Version 16.7.4
  • .NET Core 3.1
  • MahApps.Metro 2.2.0

■ベース画面作成

こちら を参考に「準備」と「プロジェクトの作成」をしてください。
プロジェクト名はWpfAppThemeSampleにしました。

そして画面を以下のように作成します。

MainWindow.xaml
<Window
    x:Class="WpfAppThemeSample.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:local="clr-namespace:WpfAppThemeSample"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MainWindow"
    Width="800"
    Height="450"
    mc:Ignorable="d">

    <Window.Resources>
        <!--  タブの既定スタイル  -->
        <Style TargetType="TabItem">
            <Setter Property="Width" Value="120" />
        </Style>
        <!--  ボタンの既定スタイル  -->
        <Style TargetType="Button">
            <Setter Property="Margin" Value="8,8,8,0" />
        </Style>
        <!--  画面下部のボタンスタイル  -->
        <Style x:Key="FooterButton" TargetType="Button">
            <Setter Property="Width" Value="100" />
            <Setter Property="Margin" Value="8,0,0,0" />
        </Style>
        <!--  テキストボックスの既定スタイル  -->
        <Style TargetType="TextBox">
            <Setter Property="Margin" Value="8,8,8,0" />
        </Style>
    </Window.Resources>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition />
            <RowDefinition Height="Auto" />
        </Grid.RowDefinitions>

        <!--  テーマ切り替え  -->
        <GroupBox Margin="8" Header="テーマ">
            <StackPanel Orientation="Horizontal">
                <RadioButton
                    Margin="8"
                    Content="ライト"
                    IsChecked="True" />
                <RadioButton Margin="8" Content="ダーク" />
            </StackPanel>
        </GroupBox>

        <TabControl Grid.Row="1" Margin="8">
            <!--#region ■タブ1-->
            <TabItem Header="タブ1" IsSelected="True">
                <ScrollViewer>
                    <StackPanel>
                        <ListBox
                            Width="200"
                            Height="80"
                            Margin="8"
                            HorizontalAlignment="Left"
                            SelectedIndex="2">
                            <ListBoxItem Content="あいうえお" />
                            <ListBoxItem Content="かきくけこ" />
                            <ListBoxItem Content="さしすせそ" />
                            <ListBoxItem Content="たちつてと" />
                            <ListBoxItem Content="なにぬねの" />
                            <ListBoxItem Content="はひふへほ" />
                        </ListBox>
                        <CheckBox
                           Margin="8,8,8,0"
                           Content="チェックボックス"
                           IsChecked="True" />
                        <CheckBox Margin="8,8,8,0" Content="チェックボックス" />
                        <TextBox Text="" />
                        <TextBox Text="" />
                        <Button Content="ボタン" />
                        <Button Content="ボタン" />
                        <Button Content="ボタン" />
                        <Button Content="ボタン" />
                        <Button Content="ボタン" />
                        <Button Content="ボタン" />
                    </StackPanel>

                </ScrollViewer>
            </TabItem>
            <!--#endregion-->

            <!--#region ■タブ2-->
            <TabItem Header="タブ2">
                <StackPanel>
                    <TextBox />
                </StackPanel>
            </TabItem>
            <!--#endregion-->
        </TabControl>

        <StackPanel
            Grid.Row="2"
            Margin="8"
            HorizontalAlignment="Right"
            Orientation="Horizontal">
            <Button Content="InputBox" Style="{StaticResource FooterButton}" />
            <Button Content="ボタン2" Style="{StaticResource FooterButton}" />
            <Button Content="ボタン3" Style="{StaticResource FooterButton}" />
        </StackPanel>
    </Grid>
</Window>

実行してみます。
001.png

■デザインの変更

◇ライブラリのインストール

ソリューションエクスプローラーでプロジェクトを右クリック、メニューからNuGetパッケージの管理を選択します。
002.png

参照タブの検索ボックスにmetroと入力し、MahApps.Metroをインストールします。
003.png

◇App.xamlの修正

ResourceDictionaryを追加します。

App.xaml(修正後)
<Application
    x:Class="WpfAppThemeSample.App"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:WpfAppThemeSample"
    StartupUri="MainWindow.xaml">
    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Controls.xaml" />
                <ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Fonts.xaml" />
                <ResourceDictionary Source="pack://application:,,,/MahApps.Metro;component/Styles/Themes/Light.Blue.xaml" />
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources>
</Application>

◇MainWindow.xamlの修正

Windowmah:MetroWindowに変更し、
xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls"を追加します。

MainWindow.xaml(修正前)
<Window
    x:Class="WpfAppThemeSample.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:local="clr-namespace:WpfAppThemeSample"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MainWindow"
    Width="800"
    Height="450"
    mc:Ignorable="d">
MainWindow.xaml(修正後)
<mah:MetroWindow
    x:Class="WpfAppThemeSample.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:local="clr-namespace:WpfAppThemeSample"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls"
    Title="MainWindow"
    Width="800"
    Height="450"
    mc:Ignorable="d">

スタイル定義にBasedOnを追加します。

MainWindow.xaml(修正後)
        <!--  タブの既定スタイル  -->
        <Style BasedOn="{StaticResource MahApps.Styles.TabItem}" TargetType="TabItem">
            <Setter Property="Width" Value="120" />
        </Style>
        <!--  ボタンの既定スタイル  -->
        <Style BasedOn="{StaticResource MahApps.Styles.Button}" TargetType="Button">
            <Setter Property="Margin" Value="8,8,8,0" />
        </Style>
        <!--  画面下部のボタンスタイル  -->
        <Style
            x:Key="FooterButton"
            BasedOn="{StaticResource MahApps.Styles.Button}"
            TargetType="Button">
            <Setter Property="Width" Value="100" />
            <Setter Property="Margin" Value="8,0,0,0" />
        </Style>
        <!--  テキストボックスの既定スタイル  -->
        <Style BasedOn="{StaticResource MahApps.Styles.TextBox}" TargetType="TextBox">
            <Setter Property="Margin" Value="8,8,8,0" />
        </Style>

◇MainWindow.xaml.csの修正

MainWindow.xaml.csファイルを開き、継承元の記述を消します。
004.png

MainWindow.xaml.cs(修正前)
public partial class MainWindow : Window
MainWindow.xaml.cs(修正後)
public partial class MainWindow

実行してみます。
005.png
デザインが変わりました。

■大文字小文字

タイトルバーのMainWindowと下部ボタンのInputBoxが勝手に大文字に変えられています。
これを無効にして元々設定されたテキストの通りに表示するようにします。

mah:MetroWindowTitleCharacterCasing="Normal"を追加します。
FooterButtonのスタイル定義に<Setter Property="mah:ControlsHelper.ContentCharacterCasing" Value="Normal" />を追加します。

■ウィンドウスタイル設定(最大化ボタン、最小化ボタン)

mah:MetroWindow
ResizeMode="CanResizeWithGrip"(サイズ変更グリップ表示)、
ShowMaxRestoreButton="False"(最大化ボタン非表示)、
ShowMinButton="False"(最小化ボタン非表示)を追加します。

ここまででXamlは以下のようになっています。

<mah:MetroWindow
    x:Class="WpfAppThemeSample.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:local="clr-namespace:WpfAppThemeSample"
    xmlns:mah="http://metro.mahapps.com/winfx/xaml/controls"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MainWindow"
    Width="800"
    Height="450"
    ResizeMode="CanResizeWithGrip"
    ShowMaxRestoreButton="False"
    ShowMinButton="False"
    TitleCharacterCasing="Normal"
    mc:Ignorable="d">
<!--  画面下部のボタンスタイル  -->
<Style
    x:Key="FooterButton"
    BasedOn="{StaticResource MahApps.Styles.Button}"
    TargetType="Button">
    <Setter Property="Width" Value="100" />
    <Setter Property="Margin" Value="8,0,0,0" />
    <Setter Property="mah:ControlsHelper.ContentCharacterCasing" Value="Normal" />
</Style>

実行してみます。
タイトルバーとボタンの英字がちゃんと指定された通り、大文字小文字が区別されて表示されました。
最小化ボタンと最大化ボタンも消えました。
ウィンドウの右下にサイズ変更がしやすいようにグリップが表示されました。
006.png

■ダークテーマ

現在の白基調の配色と、黒基調の配色を切り替えられるようにします。
2つのラジオボタンにCheckedイベントの処理を設定します。

MainWindow.xaml
<!--  テーマ切り替え  -->
<GroupBox Margin="8" Header="テーマ">
    <StackPanel Orientation="Horizontal">
        <RadioButton
            Margin="8"
            Checked="ThemeRadioButton_Checked"
            Content="ライト"
            IsChecked="True" />
        <RadioButton
            Margin="8"
            Checked="ThemeRadioButton_Checked"
            Content="ダーク" />
    </StackPanel>
</GroupBox>
MainWindow.xaml.cs
using ControlzEx.Theming;
using System.Windows;
using System.Windows.Controls;

namespace WpfAppThemeSample
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow
    {
        public MainWindow()
        {
            InitializeComponent();
        }

        private void ThemeRadioButton_Checked(object sender, RoutedEventArgs e)
        {
            if (((RadioButton)sender).Content.ToString() == "ライト")
            {
                // ライトテーマを設定
                ThemeManager.Current.ChangeTheme(this, "Light.Blue");
            }
            else
            {
                // ダークテーマを設定
                ThemeManager.Current.ChangeTheme(this, "Dark.Blue");
            }
        }

    }
}

実行してみます。
ダークのラジオボタンを選択すると黒ベースの配色になりました。
007.png

■ウォーターマーク

プレースホルダとも言います。
テキストボックス未入力時に薄く説明文を表示します。
008.png

タブ2に配置していたテキストボックスを修正します。
mah:TextBoxHelper.Watermarkに設定した文字列が、テキストボックス空欄時に薄字で表示されます。
ついでにmah:TextBoxHelper.ClearTextButton="True"でテキスト内容をクリアする×ボタンが表示されます。

MainWindow.xaml
<TabItem Header="タブ2">
    <StackPanel>
        <TextBox
            mah:TextBoxHelper.ClearTextButton="True"
            mah:TextBoxHelper.Watermark="何か入力してください。"
            Text="" />
    </StackPanel>
</TabItem>

■NumericUpDown

数値入力に適したテキストボックスです。
直接入力することも、+, -ボタンで数値を増減することもできます。
009.png

タブ2の中のStackPanelに以下を追加します。

MainWindow.xaml
<mah:NumericUpDown
    Width="100"
    Margin="8"
    Maximum="999"
    Minimum="1"
    Value="10" />

■トグルスイッチ

On/Offするスイッチです。
(たまに●を一生懸命ドラッグしている人がいますが、クリックで切り替わります)
010.png

タブ2のStackPanelに追加します。

MainWindow.xaml
<mah:ToggleSwitch
    Margin="8"
    Header="何か処理"
    IsOn="False"
    OffContent="停止"
    OnContent="処理中"
    Toggled="ToggleSwitch_Toggled" />
MainWindow.xaml.cs
private void ToggleSwitch_Toggled(object sender, RoutedEventArgs e)
{

}

■プログレスリング

処理中表示に使う、●がぐるぐる回るやつです。
011.png

タブ2のStackPanelに追加します。

MainWindow.xaml
<mah:ProgressRing x:Name="ProgressR" IsActive="False" />

トグルスイッチ切り替え時の処理を記述します。
トグルスイッチがOnになったら処理中表示にするようにします。

MainWindow.xaml.cs
private void ToggleSwitch_Toggled(object sender, RoutedEventArgs e)
{
    ProgressR.IsActive = ((MahApps.Metro.Controls.ToggleSwitch)sender).IsOn;
}

012.png

■InputBox

文字入力するためのポップアップ画面です。
013.png

InputBoxボタンにClickイベントの処理を追加します。

MainWindow.xaml
<Button
    Click="InputBoxButton_Click"
    Content="InputBox"
    Style="{StaticResource FooterButton}" />
<Button Content="ボタン2" Style="{StaticResource FooterButton}" />
<Button Content="ボタン3" Style="{StaticResource FooterButton}" />

MahApps.Metro.Controls.Dialogsをusingしておきます。
InputBoxButton_Click定義にasyncを付け足してください。
ShowInputAsyncでInputBoxを表示します。

MainWindow.xaml.cs
using MahApps.Metro.Controls.Dialogs;


private async void InputBoxButton_Click(object sender, RoutedEventArgs e)
{
    var settings = new MetroDialogSettings() { DefaultText = "最初に表示しておく文字列" };
    string result = await this.ShowInputAsync(
        "タイトル", "何か入力してください。", settings);
    if (result == null)
    {
        // キャンセル
        return;
    }
    else
    {
        await this.ShowMessageAsync("タイトル", $"「{result}」が入力されました。");
    }
}

■タブ切り替え時にアニメーション

TabControlmah:MetroAnimatedTabControlに変更するとタブ切り替え時に中身がスライドして表示されるようになります。

MainWindow.xaml
<TabControl Grid.Row="1" Margin="8">
MainWindow.xaml
<mah:MetroAnimatedTabControl Grid.Row="1" Margin="8">
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

JavaScriptの基礎

JavaScriptの基礎を復習しました。

まだまだHTMLとCSSを組み合わせて、カレンダーを作ることを目指しています。

学習したコード

var firstName = 'myname';
var age = 32;
console.log(firstName + '' + ' is' + age);

var job, is front engineer;
job = 'driver';
is front engineer = false;

console.log(firstNa me + 'is a ' + age + ' year old ' + job +'.Is I front engineer? ' + is front engineer);
// Variable mutation
age = 'thirty two';
job ='office worker';

alert(firstName + 'is a ' + age + ' year old ' + job +'.Is I front engineer? ' + is front engineer);
 var lastName = prompt('What is my last name?');
 console.log(firstName + ' ' +lastName);

alertのメッセージをクリックして、 promptにlastName(苗字)を入力できる。

C#をJavaScriptに変換する。

var seireki=0;
        var heisei=0;
        for( seireki=1989; seireki<=2017; seireki++)
        {
            Console.log("西暦 " + seireki + "年は、");
            heisei = seireki - 1988;
            Console.log("平成 " + heisei + "年");

上のコードは、エラーが出るかもしれません。For, loopのコードを調べてみます。

今日の振り返り

JavaScriptの基礎固めして、カレンダー作りに必要なコードを覚える。

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

C#でC++のcinっぽいものを作ってみる

注意

この記事は昔はてなブログに書いた記事C#でC++のcinっぽいのに詳細説明を加筆し、細部を修正したものです。

TL; DR

  • C++ のcinと似た使い勝手のクラスをC#で再現してみました。
  • ただし入力は最初にまとめて読み込みます。
  • 最終的に以下のように標準入力が読めるようになります。

入力

5
John
100 foo 200

コード

static void Main(string[] args)
{
    // 標準入力から読み込み
    int N = cin;
    string name = cin;
    (int A, string str, int B) = cin;

    // 標準出力へ書き出し
    Console.WriteLine(N);
    Console.WriteLine(name);
    Console.WriteLine($"{A} {str} {B}");
}

出力(入力と全く同様)

5
John
100 foo 200

但し書き

  • 実用目的というよりかは「C#でもこういうことできるよ!」という思考実験・例示の目的で書いています。
  • 一般的なコードとして行儀の悪いことをしています。(型変換演算子が副作用を持つ)
  • cin周りの構文を完全に真似はしていません。
  • 仕様で保証されていない動作に依存しているかもしれません。(分解代入のあたりの評価順が保証されているのか調べていない)

C++のcinについて

C++ではcinを用いて空白・改行区切りのテキストデータを簡単に読み込むことができます。

先ほども例示した

5
John
100 foo 200

というデータは、

int N, A, B;
string name, str;

// 標準入力から読み込み
cin >> N;
cin >> name;
cin >> A >> str >> B;

// 標準出力へ書き出し
cout << N << endl;
cout << name << endl;
cout << A << " " << str << " " << B << endl;

というC++コードで読み込めます。
標準出力への書き出しのために用いているcoutについての説明は割愛し、cin周りのコードについて説明します。
cin >> N;では標準入力から数値を読み込んでおり、cin >> name; では標準入力から文字列を読み込んでいます。また、 cin >> A >> str >> B のように、コード1行で複数の値を読み込むこともできます。読み取られるデータは空白・改行に従って適切にトークン化されてから解釈されます。ここから考えるに、上のcinの用法が使い勝手が良い理由には以下のエッセンスがありそうです。

  • 入力を適切にトークン化してくれている。
  • メソッド呼び出しを書かずに値を取り出せる。
  • 変数の型に応じて数値としても文字列としても値を取り出せる。
  • 1行で複数の変数に値を取り出せる。

C#で実装

上記エッセンスを実現したCinクラスを作成します。

tokenに分割して読み込む機能

標準入力はクラスの初期化時にまとめて読み込むことにし、tokenに区切ってtokensプライベートフィールドに保存しておくこととします。tokensフィールドは先頭から一つづつ値を取り出していく用途で用いられるので、 Queueというデータ構造で持ちます。

class Cin {
    private Queue<string> tokens;

    public Cin() {
        string line;
        tokens = new Queue<string> ();
        while ((line = Console.ReadLine ()) != null) {
            foreach (var token in line.Split (' ')) {
                tokens.Enqueue (token);
            }
        }
    }
}

整数型として変数に読み込む

int N = cin; のような書き味を目指します。
Cinクラスのインスタンスcinからint型の値を取り出すために暗黙の型変換を活用(悪用?)します。

public static implicit operator int(Cin cin) => int.Parse(cin.tokens.Dequeue());

queueからtokenを一つ取り出し、intにパースした結果を返す型変換子を定義しています。この型変換は副作用を持っています。(Dequeueメソッドはqueueの先頭の値を返して、queueからその値を削除します。)行儀が悪いですが、行儀にはcinっぽい書き味の尊い犠牲になってもらい、今回は気にしないこととします。

ここまでで以下のようなコードが動くようになっています。

int a = cin;
int b = cin;
int c = cin;

トークンごとに整数型の値を読みだせるようになりました。書き手は変換メソッドを明示的に呼ぶ必要がありません。

文字列型として変数に読み込む

string str = cin; のような書き味を目指します。
実装は整数型の読み込みとほぼ変わりません。整数へのパースが不要な分、よりシンプルになっています。

public static implicit operator string(Cin cin) => cin.tokens.Dequeue();

ここまでで以下のようなコードが動きます。

int a = cin;
string str1 = cin;
int b = cin;

整数型か文字列型かを区別せずに、同一の = cin で値が取り出せています。

1行で複数の変数に値を取り出す

(int A, string str, int B) = cin;のような書き味を目指します。
csharp にはユーザー定義クラスによる分割代入を可能にするための機能があります。今回はこれを使用します。

(int a, string b) = cin を可能にするためには以下のdeconstructメソッドを定義すればよいです。

public void Deconstruct(out Cin o1, out Cin o2) => (o1, o2) = (this, this);

少し複雑なので順を追って説明します。まずo1, o2にはthisが代入されているので、(int a, string b) = cin(int a, string b)=(cin, cin)と同じになります。この左右のcinがそれぞれ先ほど定義した暗黙の型変換子によりint, stringに変換されます。
これと同様のdeconstructメソッドを実用しそうなタプルの要素数まで定義します。以下では要素数8のタプルまで定義しました。

public void Deconstruct(out Cin o1, out Cin o2) =>
    (o1, o2) = (this, this);
public void Deconstruct(out Cin o1, out Cin o2, out Cin o3) =>
    (o1, o2, o3) = (this, this, this);
public void Deconstruct(out Cin o1, out Cin o2, out Cin o3, out Cin o4) =>
    (o1, o2, o3, o4) = (this, this, this, this);
public void Deconstruct(out Cin o1, out Cin o2, out Cin o3, out Cin o4, out Cin o5) =>
    (o1, o2, o3, o4, o5) = (this, this, this, this, this);
public void Deconstruct(out Cin o1, out Cin o2, out Cin o3, out Cin o4, out Cin o5, out Cin o6) =>
    (o1, o2, o3, o4, o5, o6) = (this, this, this, this, this, this);
public void Deconstruct(out Cin o1, out Cin o2, out Cin o3, out Cin o4, out Cin o5, out Cin o6, out Cin o7) =>
    (o1, o2, o3, o4, o5, o6, o7) = (this, this, this, this, this, this, this);
public void Deconstruct(out Cin o1, out Cin o2, out Cin o3, out Cin o4, out Cin o5, out Cin o6, out Cin o7, out Cin o8) =>
    (o1, o2, o3, o4, o5, o6, o7, o8) = (this, this, this, this, this, this, this, this);

ここまでで以下のコードが動きます。

(int a, string b, int c) = cin;

1行で複数の変数に値を取り出せるようになりました。

まとめ

C#でもC++のcinと似た使い勝手のクラスを作成することができました。
今回のコードはここです。

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

Pythonで画像ファイルの仕分け(3)

まえがき

前回、削除対象リストのファイルを生成するところまで終わった。ファイルの内容はこんな感じだ。

\TMP\200104\delete.txt
C:\TMP\IMG_2607(1).jpg
C:\TMP\IMG_2608(1).jpg
\TMP\200105\delete.txt
C:\TMP\IMG_2610(1).jpg
C:\TMP\IMG_2610(2).jpg
C:\TMP\IMG_2611(1).jpg

各年月(yyyymm)フォルダに削除したいファイルのフルパスが一行ずつ書かれているdelete.txtがあるので、これを読みだして削除するのみである。まだ試験段階なのでテストフォルダを対象に処理を書いてみる。

Python3で書いてみる

Python : 3.8.3

DeleteAll.py
import glob
import os

if __name__ == "__main__":
    files = glob.glob("C:\\tmp\\**\\delete.txt", recursive=True)

    for file in files:
        with open(file, "r") as f:
            for line in f.readlines():
                os.remove(line.strip())

glob様様。readlines()で読むと末尾に'\n'がくっつくのは何故?(ちょっと邪魔)

C#で書いてみる

C# : 8.0(Microsoft Visual Studio Community 2019)

DeleteAll.cs
using System;
using System.IO;
using System.Text;

namespace DeleteAll
{
    class DeleteAll
    {
        static void Main()
        {
            var files = Directory.GetFiles(@"c:\tmp", "delete.txt", SearchOption.AllDirectories);

            foreach (var file in files)
            {
                foreach (var line in File.ReadAllLines(file, Encoding.GetEncoding("Shift-JIS")))
                {
                    File.Delete(line);
                }
            }
        }
    }
}

普段は思わないがこうしてPythonと並べてみると括弧がとても邪魔なものに見えてきた・・・

VB6で書いてみる(FileSystemObject使用)

VisualBasic : 6.0

DeleteAll.bas
Option Explicit

Private colFiles As Collection

Public Sub Main()
    Set colFiles = New Collection
    Call GetFiles("C:\TMP\")
    Call DeleteFiles(colFiles)
End Sub

Private Sub GetFiles(ByVal strSearchPath As String)
    Dim FSO As FileSystemObject

    Set FSO = New FileSystemObject

    Dim objSubFolder As Folder

    For Each objSubFolder In FSO.GetFolder(strSearchPath).SubFolders
        Call GetFiles(objSubFolder.Path)
    Next

    Dim objFile As file

    For Each objFile In FSO.GetFolder(strSearchPath).files
        If LCase(objFile.Name) = "delete.txt" Then
            Call colFiles.Add(FSO.BuildPath(strSearchPath, objFile.Name))
        End If
    Next
End Sub

Private Sub DeleteFiles(ByVal files As Collection)
    Dim FSO As FileSystemObject
    Dim vntFile As Variant

    Set FSO = New FileSystemObject

    For Each vntFile In files
        With FSO.GetFile(vntFile).OpenAsTextStream(ForReading)
            Do
                Kill .ReadLine()
            Loop Until (.AtEndOfLine())
        End With
    Next
End Sub

古式ゆかしきハンガリアン記法で。再帰検索を自前実装する必要があって大変面倒。これでFileSystemObjectを使わない縛りにでもしたら大事になります。GetFiles()内の変数宣言は登場する直前にDim宣言するよう書いていますが古いVB6ソースではメソッド冒頭でまとめて宣言を書く習わしです。多分C->VBと流れた人の流儀なのかと思いますがそんな風に書いてるソースに限って1メソッド500行とかになってるクソコードを良く見かけます。

バッチファイルで書いてみる

DeleteAll.bat
@echo off
cd \tmp
for /f "usebackq" %%i in (`dir /s /b delete.txt`) do (for /f %%j in (%%i) do del %%j)

バッチファイルのなんとシンプルなことか。代わりに書式は若干難解ではあるけれど。

あとがき

これにて本件完了。さて、次は何に取り組もうか。

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

C#向けのデータベース関連のテスト用ユーティリティを作った(DBUnitモドキ)

DBが絡むテスト

DBが絡むテストをするときにテストデータの投入やデータの検証を
テストコードをなるべく書かずにやりたいことがある。
特にテストコードを書きなれていないメンバがいる場合や
そこそこたくさんのテストデータを用意しなければいけない場合など。

テストデータと期待値を外部ファイルに作成してそれを読み込む形で
テストデータ投入と検証を行いたい、JavaだったらDBUnitがある、C#では?
NDbUnitなるプロダクトがあったりするのだけど、
データをXMLではなくCSVファイルまたはExcelファイルで作成したかったのと
スキーマ定義ファイル(?)なるものを作るのがめんどい、
ので実装してみることにした。

仕様

  • テスト前に投入するテストデータを前提データ、 検証に使用する期待値のデータを期待値データと呼ぶことにする
  • 前提データと期待値データはCSVで作成する (ExcelはOfficeのライセンスがないので。。。)
  • 前提データと期待値データはセット(データセットと呼ぶことにする) にしてディレクトリに格納する
  • データセットはテストIDで一意に識別される
  • データセットのディレクトリ名に規約を設けて特定のパターンの場合に データセットとして認識されるようにしておく、ディレクトリ名でテストIDを定義する
  • データセットの配置先ルートディレクトリを指定して、テストIDを指定して前提データ投入と検証を行う
  • 任意のクエリの実行結果も検証できるようにする
  • なるべくテーブル定義に依存しないようにする(必ずしもテーブル定義とあっている必要はない)

データセットはこんな感じでプロジェクトのルートや
ソリューションのルートに置いておく。

プロジェクトルート/
  testdata/
    .../ # ディレクトリ構造は自由にできるようにしておく
      T__SAMPLE1__SampleTest1/
        R__item.csv
        R__sales.csv
        E__sales.csv
        E__assert_sales.csv
      T__SAMPLE2__SampleTest2/
        ...

データセットのディレクトリ名規約: T__{テストID}__{コメント}
テストデータのファイル名規約: R__{テーブル名}
期待値データのファイル名規約: E__{テーブル名 or クエリ名}

成果物

GitHubで公開してる

https://github.com/singy15/DumbAssert

DumbAssert.csをプロジェクトに追加して使う

こんな感じになった

例えばこんなテーブルがDBにあるとする(SQLite想定)

CREATE TABLE "article" (
    "article_id" INTEGER NOT NULL,
    "name" TEXT NOT NULL,
    "content" TEXT NOT NULL,
    "published" TEXT NOT NULL,
    "tag" TEXT,
    "version" INTEGER NOT NULL
    PRIMARY KEY("article_id" AUTOINCREMENT)
);

データセットを用意する

project-root/
  testdata/
    T__A1__PublishArticle/
      R__article.csv
      E__article.csv
      E__assert_article.csv

前提データ、NULLは<NULL>で表現する(設定で変えられるようになってる ※後述)。

R__article.csv
article_id,name,content,published,tag,version
1,test1,content1,0,tag1,1
2,test2,content2,0,<NULL>,1

期待値データ、記述していないカラムは検証対象外になる。
ソート順は記述されたカラムの昇順になる。
この場合ORDER BY article_id asc, name asc, content asc, published asc, tag ascになる

E__article.csv
article_id,name,content,published,tag
1,test1,content1,0,tag1
2,test2,content2,1,<NULL>

期待値データ、任意のクエリの実行結果を検証することもできる。
ストアドファンクションやビューの検証にも使える。

E__assert_article.csv
@query{select article_id,published where article_id = 2 order by article_id}
article_id,published
2,1

テストコード、Prepareで前提データを投入、
Prepare時に読み込んだデータセットの期待値データがAssertで検証される

[Test]
public void TestSample1() 
{
    // データセットのルートディレクトリを指定(必須)
    DumbAssertConfig.TestDataBaseDir = /*データセットのルートディレクトリ*/;
    // エンコーディングを指定(デフォルトはUTF-8)
    DumbAssertConfig.Encoding = Encoding.GetEncoding("UTF-8");
    // 前提データ投入前にデータを削除(デフォルトはtrue)
    DumbAssertConfig.DeleteBeforeInsert = true;
    // NULLの代替文字列を指定(デフォルトは"<NULL>")
    DumbAssertConfig.NullString = "<NULL>";
    // DateTimeの文字列表現パターンを指定
    // ※ToStringのパラメタ(デフォルトは"yyyy-MM-dd HH:mm:ss.fff")
    DumbAssertConfig.DateTimePattern = "yyyy-MM-dd HH:mm:ss.fff";
    // 生成されるSQLのカラム名をダブルクォートでクォートするか(デフォルトはtrue)
    DumbAssertConfig.QuoteColumnName = true;
    // 改行コード
    //(デフォルトはEnvironment.NewLine ※WindowsならCRLF、Mac/LinuxならLF)
    DumbAssertConfig.NewLine = Environment.NewLine;
    using(IDbConnection conn = new SQLiteConnection(/*接続文字列*/)) {
        conn.Open();
        DumbAssert du = new DumbAssert(conn);
        du.Prepare("A1");
        ...
        DB操作を伴う処理
        ...
        du.Assert();
        conn.Close();
    }
}

既存のトランザクションを与えて前提データの投入と検証をすることができるので、
テスト後にロールバックしてデータを元に戻すということができる。

[Test]
public void TestUseExistingTransaction() 
{
    DumbAssertConfig.TestDataBaseDir = /*データセットのルートディレクトリ*/;
    using(IDbConnection conn = new SQLiteConnection(/*接続文字列*/)) {
        conn.Open();
        var tx = conn.BeginTransaction();
        DumbAssert du = new DumbAssert(conn, tx);
        du.Prepare("A1");
        ...
        DB操作を伴う処理
        ...
        du.Assert();
        tx.Rollback();
        conn.Close();
    }
}

前提データだけのデータセットを作って共通の前提データとして利用することもできる。

du.Prepare("Common1")
du.Prepare("A1");
...
DB操作を伴う処理
...
du.Assert();

特定のデータセットの期待値のみを検証することも可能。

du.Prepare("A1")
du.Prepare("A2");
...
DB操作を伴う処理
...
du.Assert("A1");

実装

CSVの読み込み

可能な限り依存を減らしたかったので.NETの標準機能だけでつくることにした。

using Microsoft.VisualBasic.FileIO;
...

TextFieldParser parser = 
    new TextFieldParser(filePath, DumbAssertConfig.Encoding);
parser.SetDelimiters(",");
this.Data = new List<string[]>();
while(!parser.EndOfData)
{
    this.Data.Add(parser.ReadFields());
}
...

データの検証

基本的にデータをADO.NETで取得してCSVの期待値と比較しているだけなのだけど、
文字列ならいいけど数値や日時、ブール型などをどう扱うかという問題については
全部文字列化して比較するという少々雑な方法をとっている。
シリアライザを設定してどのように文字列化するかを変更できるようになっている。
文字列と数値、日時以外はテストしてないので動くか微妙。。。
そのうちテストする。

public class DumbAssertSerializer
{
    public string Serialize(object value)
    {
        switch(value)
        {
            case null: return DumbAssertConfig.NullString;
            case Boolean val: return Serialize(val);
            case Byte val: return Serialize(val);
            case Char val: return Serialize(val);
            case DateTime val: return Serialize(val);
            ...
            default: return null;
        }
    }
    public string Serialize(Boolean value) { return value.ToString(); }
    public string Serialize(Byte value) { return value.ToString(); }
    public string Serialize(Char value) { return value.ToString(); }
    public string Serialize(DateTime value) { return value.ToString(DumbAssertConfig.DateTimePattern); }
    ...
}

public class DumbAssertConfig
{
    ...

    public static DumbAssertSerializer Serializer = new DumbAssertSerializer();

    public static string NullString = "<NULL>";

    ...

    public static string DateTimePattern = "yyyy-MM-dd HH:mm:ss.fff";
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む