20190203のC#に関する記事は10件です。

C#でナンプレ(数独)を解くプログラムを書いてみた

ナンプレとは

ナンプレは、Number Placeの略で、9×9の正方形の枠内に、以下の規則にしたがって1〜9までの数字を入れていくパズルです。

  1. 縦9列のどの列にも1~9の数字がひとつずつはいる
  2. 横9行のどの行にも1~9の数字がひとつずつはいる
  3. 正方形内で区切られた3x3のブロック(全部で9つ)のどのブロックにも1~9の数字が1つずつはいる

C#でナンプレを解くプログラムを書く

このパズルを解くのにいわゆるバックトラック法といわれる手法で解を求めています。その中心となるクラスが、Solverクラスです。コードは、この記事の後半で掲載しています。

「マスに数字を置けるか?」とか「マスに数字を置く」などの操作は、すべてBoardクラスが担当していますので、Solverクラスは、とてもシンプルです。

ちなみに、バックトラック法としてはかなりオーソドックスなコードだと思います。特に高速化のための工夫などはしていません。それでも、そこそこの速度は出ています。最後に示したテストデータの場合は、ほぼ待ち時間なく解が求まります。

マスの空きが少ないブロック、行、列から攻めていくようにすれば、もう少し速度が上がると思いますが、そのコードを書くのが面倒なので手抜きしてます。

なお、すべての解を求めるようにしています。なので、解がものすごく多いデータを与えると、いつまでたっても終わらない可能性があります。

もし、一つだけ解を求めたい場合は、Mainメソッドで、

 var answer = sol.Solve(nums).Take(1);

のようにすれば、OKです。

データはファイルから読み取るようになっています。

C#のコード

Program.cs

データを読み込み、SolverクラスのSolveメソッドを呼び出し解(複数)を求めて、その結果を表示しています。

using System;
using System.Linq;
using System.IO;

namespace NumberPlace {
    class Program {
        static void Main(string[] args) {
            var nums = ReadData(@"data.txt");
            var sol = new Solver();
            var answer = sol.Solve(nums);
            foreach (var board in answer)
                board.Print();
        }

        // データを読み込む
        static int[,] ReadData(string path) {
            int[,] nums = new int[9, 9];
            var lines = File.ReadAllLines(path);
            for (int y = 0; y < 9; y++) {
                int x = 0;
                foreach (var n in lines[y].Split(',')) {
                    nums[x++, y] = int.Parse(n);
                }
            }
            return nums;
        }
    }
}

Solver.cs

空いている箇所に条件にあう数字があれば、その数字を置く、という処理を再帰的に繰り返しています。条件に合う数字がない場合には、その手は失敗なので、逆戻りして、別の数字を置くようにしています。

空いている場所はどこかとか、指定した数字が置けるかといった処理は、Boardクラスが担当しています。

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

namespace NumberPlace {
    class Solver {
        private Board _board;

        public IEnumerable<Board> Solve(int[,] mat) {
            _board = new Board(mat);
            return SolveInner();
        }

        private IEnumerable<Board> SolveInner() {
            if (_board.IsCompleted()) {
                // 完成したら、answerに答えを入れる。
                yield return _board.Clone();
            } else {
                // 空いている位置をひとつ取り出す
                var pos = _board.AlloLocations()
                                .Where(p => _board.IsVacant(p))
                                .First();
                // そこに、1-9 の数を置いてみる。
                for (int n = 1; n <= 9; n++) {
                    if (_board.CanPut(pos, n)) {
                        _board.Put(pos, n);
                        // 置けたので、再帰的に次の数を置いていく。
                        var answer = SolveInner();
                        foreach (var a in answer)
                            yield return a;
                        // 次の数を置くために、今置いた場所には 0 を入れなおす。
                        _board.Put(pos, 0);
                    }
                }
                // 1..9どれも駄目。つまり失敗->  呼び出し元に戻る
            }
        }
    }
}

Location.cs

位置を示すクラスです。

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

namespace NumberPlace {
    class Location {
        public int X { get; set; }
        public int Y { get; set; }

        public Location(int x, int y) {
            X = x;
            Y = y;
        }
    }
}

Board.cs

9×9の正方形を表すクラスで、この正方形に関わる様々なメソッドを定義しています。

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

namespace NumberPlace {

    class Board {
        private int[,] sheet;

        // コンストラクタ
        public Board(int[,] mat) {
            this.sheet = mat;
        }

        // コンストラクタ
        private Board() {
            sheet = new int[9, 9];
        }

        // クローン
        public Board Clone() {
            var nboard = new Board();
            nboard.sheet = (int[,])this.sheet.Clone();
            return nboard;
        }

        // チェックはしない CanPutを呼び出しているのが前提
        public void Put(Location pos, int num) {
            sheet[pos.X, pos.Y] = num;
        }

        // 置けるか
        public bool CanPut(Location pos, int num) {
            if (sheet[pos.X, pos.Y] != 0)
                return false;
            // 仮に置いてみる。
            sheet[pos.X, pos.Y] = num;
            try {
                return IsValid(pos);
            } finally {
                // 元に戻す
                sheet[pos.X, pos.Y] = 0;
            }
        }

        // 完成か
        public bool IsCompleted() {
            if (!Enumerable.Range(0, 9).All(nums => IsCompleted(VerticalNums(nums))))
                return false;
            if (!Enumerable.Range(0, 9).All(nums => IsCompleted(HorizontalNums(nums))))
                return false;
            return BoxLocations().All(pos => IsCompleted(BoxNums(pos)));
        }

        // 完成したか
        public bool IsCompleted(IEnumerable<int> nums) {
            return nums.Where(n => n >= 1).Distinct().Count() == 9;
        }

        // 空いているか
        public bool IsVacant(Location pos) {
            return sheet[pos.X, pos.Y] == 0;
        }

        // posの位置に関する配置は違反していないか
        public bool IsValid(Location pos) {
            return IsValid(VerticalNums(pos.X)) &&
                   IsValid(HorizontalNums(pos.Y)) &&
                   IsValid(BoxNums(pos));
        }

        // numsに重複はないか (つまり違反していないか)IsCompletedとは別。
        public bool IsValid(IEnumerable<int> nums) {
            return nums.Where(n => n >= 1).Distinct().Count() == nums.Where(n => n >= 1).Count();
        }

        // 9つある小さな四角形の左上のLocationを列挙
        public IEnumerable<Location> BoxLocations() {
            return from x in Enumerable.Range(0, 2)
                   from y in Enumerable.Range(0, 2)
                   select new Location(x * 3, y * 3);
        }

        // 注目している位置が含まれる3*3の領域の数を列挙
        public IEnumerable<int> BoxNums(Location pos) {
            return from x in Enumerable.Range(0, 3)
                   from y in Enumerable.Range(0, 3)
                   select sheet[pos.X / 3 * 3 + x, pos.Y / 3 * 3 + y];
        }

        // 注目している位置が含まれる縦一列の数を列挙
        public IEnumerable<int> VerticalNums(int x) {
            for (int y = 0; y < 9; y++) {
                yield return sheet[x, y];
            }
        }

        // 注目している位置が含まれる横一列の数を列挙
        public IEnumerable<int> HorizontalNums(int y) {
            for (int x = 0; x < 9; x++) {
                yield return sheet[x, y];
            }
        }

        // すべての位置を列挙する
        public IEnumerable<Location> AlloLocations() {
            return from x in Enumerable.Range(0, 9)
                   from y in Enumerable.Range(0, 9)
                   select new Location(x, y);
        }

        internal void Print() {
            for (int y = 0; y < 9; y++) {
                for (int x = 0; x < 9; x += 3) {
                    Console.Write(" {0} {1} {2}", sheet[x, y], sheet[x+1, y], sheet[x+2, y]);
                    if (x == 0 || x == 3)
                        Console.Write("|");
                }
                Console.WriteLine();
                if (y == 2 || y == 5)
                    Console.WriteLine("------+------+------");
            }
            Console.WriteLine();
        }
    }
}

実行結果

以下の示すデータを与えて、解いてみました。データの0は空いていることを示しています。
ここでは、あえて、複数の解を持つ問題を解かせてみました。

入力データ

0,2,0,0,7,5,0,1,0
1,0,0,0,0,4,5,0,8
0,5,6,8,0,0,2,0,0
8,1,0,0,0,0,7,0,0
9,0,0,0,0,0,0,0,3
0,0,2,0,0,0,0,4,5
0,0,9,0,0,1,4,3,0
3,0,1,7,0,0,0,0,9
0,7,0,3,9,0,0,2,0

 4 2 8| 9 7 5| 3 1 6
 1 9 3| 2 6 4| 5 7 8
 7 5 6| 8 1 3| 2 9 4
------+------+------
 8 1 5| 4 3 9| 7 6 2
 9 4 7| 5 2 6| 1 8 3
 6 3 2| 1 8 7| 9 4 5
------+------+------
 2 8 9| 6 5 1| 4 3 7
 3 6 1| 7 4 2| 8 5 9
 5 7 4| 3 9 8| 6 2 1

 4 2 8| 9 7 5| 3 1 6
 1 9 3| 6 2 4| 5 7 8
 7 5 6| 8 1 3| 2 9 4
------+------+------
 8 1 5| 4 3 9| 7 6 2
 9 4 7| 2 5 6| 1 8 3
 6 3 2| 1 8 7| 9 4 5
------+------+------
 2 8 9| 5 6 1| 4 3 7
 3 6 1| 7 4 2| 8 5 9
 5 7 4| 3 9 8| 6 2 1

2つの解が求まりました。

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

ViewModelから画面を閉じる処理

ViewModelから画面を閉じる処理

画面を閉じる処理を通して処理の追加方法の例を示します。
見やすいよう一部省略や改行位置を変更しています。

Action

View側の画面を閉じる処理。

using Prism.Interactivity.InteractionRequest;
using System.Windows;
using System.Windows.Interactivity;

namespace Sample.ViewUtilities {
    public class CloseWindowAction : TriggerAction<DependencyObject> {
        protected override void Invoke(object parameter) {
            if (parameter is InteractionRequestedEventArgs e) {
                Invoke(AssociatedObject, e);
            }
        }

        private void Invoke(DependencyObject sender,
            InteractionRequestedEventArgs e) {
            if (sender is Window window) {
                window.Close();
                e.Callback?.Invoke();
            }
        }
    }
}

ViewModel

Viewに画面を閉じる処理を指示。

public InteractionRequest<Notification> CloseWindowRequest { get; private set; }
public DelegateCommand CloseWindowCommand { get; private set; }

public SampleWindowViewModel() {
    CloseWindowRequest = new InteractionRequest<Notification>();
    CloseWindowCommand = new DelegateCommand(ExecuteCloseWindowCommand,
        CanExecuteCloseWindowCommand);
}

private bool CanExecuteCloseWindowCommand() {
    return true;
}

private void ExecuteCloseWindowCommand() {
    CloseWindowRequest.Raise(null);
}

XAML

xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:prism="http://www.codeplex.com/prism"
xmlns:ViewUtilities="clr-namespace:Sample.ViewUtilities"
・・・
<i:Interaction.Triggers>
  <prism:InteractionRequestTrigger
    SourceObject="{Binding CloseWindowRequest}">
    <ViewUtilities:CloseWindowAction/>
  </prism:InteractionRequestTrigger>
</i:Interaction.Triggers>
・・・
<Window.InputBindings>
  <KeyBinding Modifiers="Ctrl" Key="W"
    Command="{Binding CloseWindowCommand, Mode=OneWay}"/>
</Window.InputBindings>
・・・
<Menu>
  <MenuItem Header="ファイル(_F)">
    <MenuItem Header="閉じる(_X)"
      Command="{Binding CloseWindowCommand, Mode=OneWay}"/>
  </MenuItem>
</Menu>
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Enumで定義したものを多言語対応させたい

普通の多言語対応

多言語対応というと一般的には、リソースファイルを定義してそこから呼び出すという手段がとられると思います。

例えばProperties内のResources.resxで次のように定義したとします。

Resource.png

property.cs
// 日本語用リソースだと”4逓倍”、英語用だと”Multiple 4”
string str = Properties.Resources.Multi4 

各言語ごとにリソースファイル用意しておいて、切り替えることでstrに格納される文字列を変えることができるんですね。

一方でEnumでリソースを呼び出そうにも、宣言時には呼び出せません。

enum CountType
{
  Multi4,
  Multi2,
  Multi1
}

解決策

そこで拡張メソッドを使ってリソースファイルから各言語での表記を呼び出します。

public static string GetName(this CountType e)
{
  switch (e)
  {
     case CountType.Multi4:
       return Properties.Resources.Multi4;
     case CountType.Multi2:
       return Properties.Resources.Multi2;
     case CountType.Multi1:
       return Properties.Resources.Multi1;
     default:
       return "Undefined";
  }
}

使う際はGetName()メソッドを呼び出します。

foreach (CountType type in Enum.GetValues(typeof(CountType)))
{
  Combobox.Items.Add(type.GetName());
}

これで日本語表記できるようになりました。

Combobox.png

環境

Visual Studio 2017 Community
.Net Framework 4.6
C#

参考文献

WPF アプリの国際化 (多言語対応) と、実行中の動的な言語切り替え
WPF Runtime Localization

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

[C#]TypeSafeEnumパターンでenumに対するswitch-case, if-elseを撲滅する => 基底クラスの実装

enumとswitch-caseの例:四則演算

こんなenumがあったとする

        private enum CalcType
        {
            /// <summary>足し算</summary>
            Add,

            /// <summary>引き算</summary>
            Subtract,

            /// <summary>掛け算</summary>
            Multiple,

            /// <summary>割り算</summary>
            Divide,
        }

で、計算メソッドはこんな感じ

        private decimal Calc(decimal valueL, decimal valueR, CalcType type)
        {
            switch (type)
            {
                case CalcType.Add:
                    return valueL + valueR;
                case CalcType.Subtract:
                    return valueL - valueR;
                case CalcType.Multiple:
                    return valueL * valueR;
                case CalcType.Divide:
                    return valueL / valueR;
                default:
                    throw new ArgumentOutOfRangeException(nameof(type), type, null);
            }
        }

・・・switch-caseは美しくない。例えばGUIアプリだと計算そのもののほかにも"+", "-", "*", "/"といった記号を表示しないといけないかもしれない。その時にまたswitch-caseで分岐するのか?とかいろいろ不安。

そこでTypeSafeEnumパターン

ステップ1:基本

TypeSafeEnumとは何か?それはジョシュア・ブロックが「Effective Java」で~とかなんとか能書きはおいといて、コードやコード。Talk is cheapや。
先ほどの四則演算enumだったら下記のように実装する。

        /// <summary>四則計算(TypeSafeEnum)</summary>
        public class CalcTypeAsSimpleTypeSafeEnum
        {
            public static readonly CalcTypeAsSimpleTypeSafeEnum Add =
                new CalcTypeAsSimpleTypeSafeEnum("足し算", "+", (v1, v2) => v1 + v2);
            public static readonly CalcTypeAsSimpleTypeSafeEnum Subtract =
                new CalcTypeAsSimpleTypeSafeEnum("引き算", "-", (v1, v2) => v1 - v2);
            public static readonly CalcTypeAsSimpleTypeSafeEnum Multiple =
                new CalcTypeAsSimpleTypeSafeEnum("掛け算", "-", (v1, v2) => v1 * v2);
            public static readonly CalcTypeAsSimpleTypeSafeEnum Divide =
                new CalcTypeAsSimpleTypeSafeEnum("割り算", "-", (v1, v2) => v1 / v2);

            private CalcTypeAsSimpleTypeSafeEnum(string name, string symbol, Func<decimal, decimal, decimal> calcFunc)
            {
                _calcFunc = calcFunc ?? throw new ArgumentNullException(nameof(calcFunc));
                Name = name;
                Symbol = symbol;
            }

            /// <summary>計算関数</summary>
            private readonly Func<decimal, decimal, decimal> _calcFunc;

            /// <summary>名称</summary>
            public string Name { get; }

            /// <summary>記号</summary>
            public string Symbol { get; }

            /// <summary>計算を実施</summary>
            /// <param name="v1">値1</param>
            /// <param name="v2">値2</param>
            /// <returns>計算結果</returns>
            public decimal Calc(decimal v1, decimal v2) => _calcFunc(v1, v2);

            public override string ToString() { return $"{Name}[{Symbol}]"; }
        }

ポイント

  • コンストラクタはprivateにし、インスタンスはpublic static readonlyで定義する
    • (初心者には違和感があるかもしれない。。当時プログラマ3年生位だった自分は最初変な感じだと思った。でもすぐに慣れた。)
  • 単なるクラスなのでプロパティやメンバ変数を定義できる
    • 普通のenumでも属性を使ったら文字列持たせられるとかいうけど、個人的にめんどいと思うので、あまりやらない。(既存コードとの兼ね合いでやることもある。。)
  • クラスのインスタンス=定数でないのでswitch-caseは逆に使えなくなる

これにより先ほどのCalcメソッドはこのように簡略化される。

        private decimal Calc(decimal v1, decimal v2, CalcTypeAsSimpleTypeSafeEnum type)
        {
            if (type == null)
            {
                throw new ArgumentNullException(nameof(type));
            }

            return type.Calc(v1, v2);
        }

かくしてswitch-caseの駆逐に成功!

ステップ2:一般的な機能追加

さて、switch-caseの駆逐に成功したあとは使い勝手の向上について考える。

実際のenumの使い方のシナリオでは例えば

  • DBにenumのキーを保存し、また逆にDBからenumのキーを読みだして復元する

とか、

  • enumの値を列挙する

とかいうことがある。そのようなシナリオに対応するために以下のようにステップ1のコードに手を加える。

        /// <summary>四則計算(TypeSafeEnum)</summary>
        public class CalcTypeAsTypeSafeEnum
        {
            public static readonly CalcTypeAsTypeSafeEnum Add =
                new CalcTypeAsTypeSafeEnum(0,"足し算", "+", (v1, v2) => v1 + v2);
            public static readonly CalcTypeAsTypeSafeEnum Subtract =
                new CalcTypeAsTypeSafeEnum(1,"引き算", "-", (v1, v2) => v1 - v2);
            public static readonly CalcTypeAsTypeSafeEnum Multiple =
                new CalcTypeAsTypeSafeEnum(2,"掛け算", "-", (v1, v2) => v1 * v2);
            public static readonly CalcTypeAsTypeSafeEnum Divide =
                new CalcTypeAsTypeSafeEnum(3,"割り算", "-", (v1, v2) => v1 / v2);

            private CalcTypeAsTypeSafeEnum(int key, string name, string symbol, Func<decimal, decimal, decimal> calcFunc)
            {
                _calcFunc = calcFunc ?? throw new ArgumentNullException(nameof(calcFunc));
                Key = key;
                Name = name;
                Symbol = symbol;
                KeyValueDictionary.Add(key, this);
            }

            private static readonly Dictionary<int, CalcTypeAsTypeSafeEnum> KeyValueDictionary =
                new Dictionary<int, CalcTypeAsTypeSafeEnum>();

            /// <summary>計算関数</summary>
            private readonly Func<decimal, decimal, decimal> _calcFunc;

            /// <summary>キー</summary>
            public int Key { get; }

            /// <summary>名称</summary>
            public string Name { get; }

            /// <summary>記号</summary>
            public string Symbol { get; }

            /// <summary>全値を取得</summary>
            public static IReadOnlyCollection<CalcTypeAsTypeSafeEnum> Values => KeyValueDictionary.Values;

            /// <summary>キーからオブジェクトを取得</summary>
            /// <param name="key">The key.</param>
            /// <param name="defaultValue">取得できなかった時のデフォルト値</param>
            /// <returns>オブジェクト</returns>
            /// <exception cref="System.ArgumentNullException">defaultValue</exception>
            public static CalcTypeAsTypeSafeEnum FromKey(int key, CalcTypeAsTypeSafeEnum defaultValue = null)
                          => KeyValueDictionary.TryGetValue(key, out var ret) ? ret : defaultValue;


            /// <summary>計算を実施</summary>
            /// <param name="v1">値1</param>
            /// <param name="v2">値2</param>
            /// <returns>計算結果</returns>
            public decimal Calc(decimal v1, decimal v2) => _calcFunc(v1, v2);

            public override string ToString() { return $"{Name}[{Symbol}]"; }
        }

ポイント

  • メンバ変数にkeyを追加(enumの値に相当)
  • static なKeyValueDictionaryを定義し、コンストラクタでkey, 値(this=自分自身のオブジェクト)を登録する
    • これによりValuesで全値を列挙できる
    • FromKeyメソッドでキーから値を取得できる

ステップ3:一般的な機能を基底クラス(TypeSafeEnumBase<>)に持たせる

ステップ2の一般的な機能は毎回実装するのは面倒。ということで共通の基底クラスを作っておく。
いろいろ実装方法はあるが、自分は以下のような感じのものを使っている。

(※これを職場が変わるたびに作り直しているのが面倒だったのでWeb上に残そうと思ったのがこのエントリのモチベーション)

    /// <summary>TypeSafeEnumBase</summary>
    public class TypeSafeEnumBase<TSelf, TKey> where TSelf : TypeSafeEnumBase<TSelf, TKey>
    {
        /// <summary>Dictionary{Key, Value}</summary>
        private static readonly Dictionary<TKey, TSelf> KeyValueDictionary = new Dictionary<TKey, TSelf>();

        static TypeSafeEnumBase()
        {
            // touch myself to initialize values
            // without this, Count might be 0 on first time
            var value =(typeof(TSelf).GetFields(BindingFlags.GetField | BindingFlags.Public | BindingFlags.Static |
                                                BindingFlags.DeclaredOnly)
                .Where(f => !f.IsLiteral)).FirstOrDefault()?.GetValue(null);
        }

        /// <summary>Initializes a new instance of the <see cref="TypeSafeEnumBase{TSelf, TKey}" /> class.</summary>
        /// <param name="key">The key.</param>
        protected TypeSafeEnumBase(TKey key)
         : this(key, key.ToString())
        {
        }

        /// <summary>Initializes a new instance of the <see cref="TypeSafeEnumBase{TSelf, TKey}"/> class.</summary>
        /// <param name="key">The key.</param>
        /// <param name="name">The name.</param>
        protected TypeSafeEnumBase(TKey key, string name)
        {
            if (KeyValueDictionary.ContainsKey(key))
            {
                throw new ArgumentException($"The key [{key}] has been already registered.");
            }

            Key = key;
            Name = name;

            // Add {key, self} to dictionary
            KeyValueDictionary.Add(key, this as TSelf);
        }

        /// <summary>Key</summary>
        public TKey Key { get; }

        /// <summary>Name</summary>
        public string Name { get; }

        /// <summary>AllValues</summary>
        public static IReadOnlyCollection<TSelf> Values => KeyValueDictionary.Values;

        /// <summary>Count</summary>
        public static int Count => KeyValueDictionary.Count;

        /// <summary>Get value by key</summary>
        /// <param name="key">The key.</param>
        /// <param name="defaultValue">The default value.</param>
        /// <returns>value</returns>
        public static TSelf FromKey(TKey key, TSelf defaultValue = null) 
                      => KeyValueDictionary.TryGetValue(key, out var ret) ? ret : defaultValue;

        /// <summary>Converts to string.</summary>
        /// <returns>A <see cref="System.String" /> that represents this instance.</returns>
        public override string ToString() => $" {Name}";
    }

ポイント

  • Keyのほかによく使うNameを標準で持つ。(指定しない場合はNameはkey.ToString()となる)
  • ジェネリックの指定型はTypeSafeEnumBaseを継承した自身の型とkeyの型。自身の型を
  • staticコンストラクタで自分自身にアクセスしている。これをやらないと初回ValuesやCount(辞書)にアクセスしたときにまだ定義(Add, Subtract...)が生成されておらず、辞書が空っぽになっているので。。

で、先ほどのCalcTypeで実装するとこんな感じ

        /// <summary>四則計算(TypeSafeEnum)</summary>
        public class CalcTypeUsesTypeSafeEnumBase : TypeSafeEnumBase<CalcTypeUsesTypeSafeEnumBase, int>
        {
            public static readonly CalcTypeUsesTypeSafeEnumBase Add =
                new CalcTypeUsesTypeSafeEnumBase(0,"足し算", "+", (v1, v2) => v1 + v2);
            public static readonly CalcTypeUsesTypeSafeEnumBase Subtract =
                new CalcTypeUsesTypeSafeEnumBase(1,"引き算", "-", (v1, v2) => v1 - v2);
            public static readonly CalcTypeUsesTypeSafeEnumBase Multiple =
                new CalcTypeUsesTypeSafeEnumBase(2,"掛け算", "-", (v1, v2) => v1 * v2);
            public static readonly CalcTypeUsesTypeSafeEnumBase Divide =
                new CalcTypeUsesTypeSafeEnumBase(3,"割り算", "-", (v1, v2) => v1 / v2);

            private CalcTypeUsesTypeSafeEnumBase(int key, string name, string symbol, Func<decimal, decimal, decimal> calcFunc) 
                : base(key, name)
            {
                _calcFunc = calcFunc ?? throw new ArgumentNullException(nameof(calcFunc));
                Symbol = symbol;
            }
            /// <summary>計算関数</summary>
            private readonly Func<decimal, decimal, decimal> _calcFunc;

            /// <summary>記号</summary>
            public string Symbol { get; }

            /// <summary>計算を実施</summary>
            /// <param name="v1">値1</param>
            /// <param name="v2">値2</param>
            /// <returns>計算結果</returns>
            public decimal Calc(decimal v1, decimal v2) => _calcFunc(v1, v2);
        }

これでステップ2よりはだいぶスッキリ。

TypeSafeEnumBaseの他の使用例

もう少し実践的にわかりやすい例を示すために(嘘)、自分の好きなプログレバンドを実装してみよう。
(Hadi Hariri先生もKotolinの説明の時にプログレバンドPink Floyedを使ってたしぃ)
https://hhariri.wordpress.com/2013/11/18/refactoring-to-functional-reducing-and-flattening-lists/
↓引用

data class Track(val title: String, val durationInSeconds: Int)

val pinkFloyd = listOf(
        Album("The Dark Side of the Moon", 1973, 2, 1,
                listOf(Track("Speak to Me", 90),
                        Track("Breathe", 163),
                        Track("On he Run", 216),
                        Track("Time", 421),
                        Track("The Great Gig in the Sky", 276),
                        Track("Money", 382),
                        Track("Us and Them", 462),
                        Track("Any Color You Like", 205),
                        Track("Brain Damage", 228),
                        Track("Eclipse", 123)
                )
        ))
        // the rest omitted for brevity

TypeSafeEnumBaseの例:プログレバンド名と国籍

        /// <summary>ProgRockBands for TypeSafeEnumBase Demo</summary>
        private class ProgRockBands : TypeSafeEnumBase<ProgRockBands, int>
        {
            public static readonly ProgRockBands MoonSafari
                = new ProgRockBands(1, "Moon Safari", Country.Sweden);

            public static readonly ProgRockBands GentleGiant
                = new ProgRockBands(2, "Gentle Giant", Country.UK);

            public static readonly ProgRockBands Genesis
                = new ProgRockBands(3, "Genesis", Country.UK);

            public static readonly ProgRockBands CameliasGarden
                = new ProgRockBands(4, "Camelias Garden", Country.Italy);

            public static readonly ProgRockBands IQ
                = new ProgRockBands(5, "IQ", Country.UK);

            public static readonly ProgRockBands PFM
                = new ProgRockBands(6, "PFM", Country.Italy);

            public static readonly ProgRockBands YoninBayashi
                = new ProgRockBands(7, "四人囃子(Yonin Bayashi)", Country.Japan);

            public static readonly ProgRockBands TheFlowerKings
                = new ProgRockBands(8, "The Flower Kings", Country.Sweden);

            /// <summary>Initializes a new instance of the <see cref="ProgRockBands" /> class.</summary>
            /// <param name="key">The key.</param>
            /// <param name="name">The name.</param>
            /// <param name="country">The country.</param>
            private ProgRockBands(int key, string name, Country country)
                : base(key, name)
            {
                Country = country;
            }

            /// <summary>Country</summary>
            public Country Country { get; }
        }

        private enum Country
        {
            UK,
            Italy,
            Sweden,
            Japan
        }

上記クラスのテストコードは以下のような感じ(※要Chaining Assertion=>Nugetから取得)

    <summary>ProgRockBandsTest</summary>
    [TestClass]
    public class ProgRockBandsTest
    {
        /// <summary>CountTest</summary>
        [TestMethod]
        public void CountTest()
        {
            ProgRockBands.Count.Is(8);
        }

        /// <summary>Valueses the test.</summary>
        [TestMethod]
        public void ValuesTest()
        {
            // All
            ProgRockBands.Values.Is(
                ProgRockBands.MoonSafari,
                ProgRockBands.GentleGiant,
                ProgRockBands.Genesis,
                ProgRockBands.CameliasGarden,
                ProgRockBands.IQ,
                ProgRockBands.PFM,
                ProgRockBands.YoninBayashi,
                ProgRockBands.TheFlowerKings);

            // Filter by country
            ProgRockBands.Values.Where(v => v.Country == Country.Sweden)
                .Is(ProgRockBands.MoonSafari, ProgRockBands.TheFlowerKings);
            ProgRockBands.Values.Where(v => v.Country == Country.Italy)
                .Is(ProgRockBands.CameliasGarden, ProgRockBands.PFM);
            ProgRockBands.Values.Where(v => v.Country == Country.UK)
                .Is(ProgRockBands.GentleGiant, ProgRockBands.Genesis, ProgRockBands.IQ);
            ProgRockBands.Values.Where(v => v.Country == Country.Japan).Is(ProgRockBands.YoninBayashi);
        }

        /// <summary>FromKeyTest</summary>
        [TestMethod]
        public void FromKeyTest()
        {
            ProgRockBands.FromKey(1).Is(ProgRockBands.MoonSafari);
            ProgRockBands.FromKey(8).Is(ProgRockBands.TheFlowerKings);

            // no match value => default
            ProgRockBands.FromKey(9).IsNull();
            ProgRockBands.FromKey(10, ProgRockBands.YoninBayashi).Is(ProgRockBands.YoninBayashi);
        }
   }

それでは良いプログラミングライフをノシ

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

[C#]TypeSafeEnumパターンでenumに対するswitch-case, if-elseを駆逐する => 基底クラスの実装

enumとswitch-caseの例:四則演算

こんな四則演算のenumがあったとする

        private enum CalcType
        {
            /// <summary>足し算</summary>
            Add,

            /// <summary>引き算</summary>
            Subtract,

            /// <summary>掛け算</summary>
            Multiple,

            /// <summary>割り算</summary>
            Divide,
        }

で、計算メソッドはこんな感じ

        private decimal Calc(decimal valueL, decimal valueR, CalcType type)
        {
            switch (type)
            {
                case CalcType.Add:
                    return valueL + valueR;
                case CalcType.Subtract:
                    return valueL - valueR;
                case CalcType.Multiple:
                    return valueL * valueR;
                case CalcType.Divide:
                    return valueL / valueR;
                default:
                    throw new ArgumentOutOfRangeException(nameof(type), type, null);
            }
        }

使用方法(テストコード)はこんな感じ。(※要Chaining Assertion:Nugetから取得)

        [TestMethod]
        public void CalcTest()
        {
            // 足し算
            Calc(4, 5, CalcType.Add).Is(9);
            // 引き算
            Calc(4, 5, CalcType.Subtract).Is(-1);
            // 掛け算
            Calc(4, 5, CalcType.Multiple).Is(20);
            // 割り算
            Calc(4, 5, CalcType.Divide).Is((decimal) 0.8);
        }

・・・switch-caseは美しくない。例えばGUIアプリだと計算そのもののほかにも"+", "-", "*", "/"といった記号を表示しないといけないかもしれない。その時にまたswitch-caseで分岐するのか?とかいろいろ不安。

そこでTypeSafeEnumパターン

ステップ1:基本

TypeSafeEnumとは何か?それはジョシュア・ブロックが「Effective Java」で~とかなんとか能書きはおいといて、コードやコード。Talk is cheapや。
先ほどの四則演算enumだったら下記のように実装する。

        /// <summary>四則計算(TypeSafeEnum)</summary>
        public class CalcTypeAsSimpleTypeSafeEnum
        {
            public static readonly CalcTypeAsSimpleTypeSafeEnum Add =
                new CalcTypeAsSimpleTypeSafeEnum("足し算", "+", (v1, v2) => v1 + v2);
            public static readonly CalcTypeAsSimpleTypeSafeEnum Subtract =
                new CalcTypeAsSimpleTypeSafeEnum("引き算", "-", (v1, v2) => v1 - v2);
            public static readonly CalcTypeAsSimpleTypeSafeEnum Multiple =
                new CalcTypeAsSimpleTypeSafeEnum("掛け算", "*", (v1, v2) => v1 * v2);
            public static readonly CalcTypeAsSimpleTypeSafeEnum Divide =
                new CalcTypeAsSimpleTypeSafeEnum("割り算", "/", (v1, v2) => v1 / v2);

            private CalcTypeAsSimpleTypeSafeEnum(string name, string symbol, Func<decimal, decimal, decimal> calcFunc)
            {
                _calcFunc = calcFunc ?? throw new ArgumentNullException(nameof(calcFunc));
                Name = name;
                Symbol = symbol;
            }

            /// <summary>計算関数</summary>
            private readonly Func<decimal, decimal, decimal> _calcFunc;

            /// <summary>名称</summary>
            public string Name { get; }

            /// <summary>記号</summary>
            public string Symbol { get; }

            /// <summary>計算を実施</summary>
            /// <param name="v1">値1</param>
            /// <param name="v2">値2</param>
            /// <returns>計算結果</returns>
            public decimal Calc(decimal v1, decimal v2) => _calcFunc(v1, v2);

            public override string ToString() { return $"{Name}[{Symbol}]"; }
        }

ポイント

  • コンストラクタはprivateにし、インスタンスはpublic static readonlyで定義する
    • (初心者には違和感があるかもしれない。。当時プログラマ3年生位だった自分は最初変な感じだと思った。でもすぐに慣れた。)
  • 単なるクラスなのでプロパティやメンバ変数を定義できる
    • 普通のenumでも属性を使ったら文字列持たせられるとかいうけど、個人的にめんどいと思うので、あまりやらない。(既存コードとの兼ね合いでやることもある。。)
  • クラスのインスタンス=定数でないのでswitch-caseは逆に使えなくなる

これにより先ほどのCalcメソッドはこのように簡略化される。

        private decimal Calc(decimal v1, decimal v2, CalcTypeAsSimpleTypeSafeEnum type)
        {
            if (type == null)
            {
                throw new ArgumentNullException(nameof(type));
            }

            return type.Calc(v1, v2);
        }

かくしてswitch-caseの駆逐に成功!

ステップ2:一般的な機能追加

さて、switch-caseの駆逐に成功したあとは使い勝手の向上について考える。

実際のenumの使い方のシナリオでは例えば

  • DBにenumのキーを保存し、また逆にDBからenumのキーを読みだして復元する

とか、

  • enumの値を列挙する

とかいうことがある。そのようなシナリオに対応するために以下のようにステップ1のコードに手を加える。

        /// <summary>四則計算(TypeSafeEnum)</summary>
        public class CalcTypeAsTypeSafeEnum
        {
            public static readonly CalcTypeAsTypeSafeEnum Add =
                new CalcTypeAsTypeSafeEnum(0,"足し算", "+", (v1, v2) => v1 + v2);
            public static readonly CalcTypeAsTypeSafeEnum Subtract =
                new CalcTypeAsTypeSafeEnum(1,"引き算", "-", (v1, v2) => v1 - v2);
            public static readonly CalcTypeAsTypeSafeEnum Multiple =
                new CalcTypeAsTypeSafeEnum(2,"掛け算", "*", (v1, v2) => v1 * v2);
            public static readonly CalcTypeAsTypeSafeEnum Divide =
                new CalcTypeAsTypeSafeEnum(3,"割り算", "/", (v1, v2) => v1 / v2);

            private CalcTypeAsTypeSafeEnum(int key, string name, string symbol, Func<decimal, decimal, decimal> calcFunc)
            {
                _calcFunc = calcFunc ?? throw new ArgumentNullException(nameof(calcFunc));
                Key = key;
                Name = name;
                Symbol = symbol;
                KeyValueDictionary.Add(key, this);
            }

            private static readonly Dictionary<int, CalcTypeAsTypeSafeEnum> KeyValueDictionary =
                new Dictionary<int, CalcTypeAsTypeSafeEnum>();

            /// <summary>計算関数</summary>
            private readonly Func<decimal, decimal, decimal> _calcFunc;

            /// <summary>キー</summary>
            public int Key { get; }

            /// <summary>名称</summary>
            public string Name { get; }

            /// <summary>記号</summary>
            public string Symbol { get; }

            /// <summary>全値を取得</summary>
            public static IReadOnlyCollection<CalcTypeAsTypeSafeEnum> Values => KeyValueDictionary.Values;

            /// <summary>キーからオブジェクトを取得</summary>
            /// <param name="key">The key.</param>
            /// <param name="defaultValue">取得できなかった時のデフォルト値</param>
            /// <returns>オブジェクト</returns>
            /// <exception cref="System.ArgumentNullException">defaultValue</exception>
            public static CalcTypeAsTypeSafeEnum FromKey(int key, CalcTypeAsTypeSafeEnum defaultValue = null)
                          => KeyValueDictionary.TryGetValue(key, out var ret) ? ret : defaultValue;


            /// <summary>計算を実施</summary>
            /// <param name="v1">値1</param>
            /// <param name="v2">値2</param>
            /// <returns>計算結果</returns>
            public decimal Calc(decimal v1, decimal v2) => _calcFunc(v1, v2);

            public override string ToString() { return $"{Name}[{Symbol}]"; }
        }

ポイント

  • メンバ変数にkeyを追加(enumの値に相当)
  • static なKeyValueDictionaryを定義し、コンストラクタでkey, 値(this=自分自身のオブジェクト)を登録する
    • これによりValuesで全値を列挙できる
    • FromKeyメソッドでキーから値を取得できる

ステップ3:一般的な機能を基底クラス(TypeSafeEnumBase<>)に持たせる

ステップ2の一般的な機能は毎回実装するのは面倒。ということで共通の基底クラスを作っておく。
いろいろ実装方法はあるが、自分は以下のような感じのものを使っている。

(※これを職場が変わるたびに作り直しているのが面倒だったのでWeb上に残そうと思ったのがこのエントリのモチベーション)

    /// <summary>TypeSafeEnumBase</summary>
    public class TypeSafeEnumBase<TSelf, TKey> where TSelf : TypeSafeEnumBase<TSelf, TKey>
    {
        /// <summary>Dictionary{Key, Value}</summary>
        private static readonly Dictionary<TKey, TSelf> KeyValueDictionary = new Dictionary<TKey, TSelf>();

        static TypeSafeEnumBase()
        {
            // touch myself to initialize values
            // without this, Count might be 0 on first time
            var value =(typeof(TSelf).GetFields(BindingFlags.GetField | BindingFlags.Public | BindingFlags.Static |
                                                BindingFlags.DeclaredOnly)
                .Where(f => !f.IsLiteral)).FirstOrDefault()?.GetValue(null);
        }

        /// <summary>Initializes a new instance of the <see cref="TypeSafeEnumBase{TSelf, TKey}" /> class.</summary>
        /// <param name="key">The key.</param>
        protected TypeSafeEnumBase(TKey key)
         : this(key, key.ToString())
        {
        }

        /// <summary>Initializes a new instance of the <see cref="TypeSafeEnumBase{TSelf, TKey}"/> class.</summary>
        /// <param name="key">The key.</param>
        /// <param name="name">The name.</param>
        protected TypeSafeEnumBase(TKey key, string name)
        {
            if (KeyValueDictionary.ContainsKey(key))
            {
                throw new ArgumentException($"The key [{key}] has been already registered.");
            }

            Key = key;
            Name = name;

            // Add {key, self} to dictionary
            KeyValueDictionary.Add(key, this as TSelf);
        }

        /// <summary>Key</summary>
        public TKey Key { get; }

        /// <summary>Name</summary>
        public string Name { get; }

        /// <summary>AllValues</summary>
        public static IReadOnlyCollection<TSelf> Values => KeyValueDictionary.Values;

        /// <summary>Count</summary>
        public static int Count => KeyValueDictionary.Count;

        /// <summary>Get value by key</summary>
        /// <param name="key">The key.</param>
        /// <param name="defaultValue">The default value.</param>
        /// <returns>value</returns>
        public static TSelf FromKey(TKey key, TSelf defaultValue = null) 
                      => KeyValueDictionary.TryGetValue(key, out var ret) ? ret : defaultValue;

        /// <summary>Converts to string.</summary>
        /// <returns>A <see cref="System.String" /> that represents this instance.</returns>
        public override string ToString() => $" {Name}";
    }

ポイント

  • Keyのほかによく使うNameを標準で持つ。(指定しない場合はNameはkey.ToString()となる)
  • ジェネリックの指定型はTypeSafeEnumBaseを継承した自身の型とkeyの型。
  • staticコンストラクタで自分自身にアクセスしている。これをやらないと初回ValuesやCount(辞書)にアクセスしたときにまだ定義(Add, Subtract...)が生成されておらず、辞書が空っぽになっているので。。

で、先ほどのCalcTypeで実装するとこんな感じ

        /// <summary>四則計算(TypeSafeEnum)</summary>
        public class CalcTypeUsesTypeSafeEnumBase : TypeSafeEnumBase<CalcTypeUsesTypeSafeEnumBase, int>
        {
            public static readonly CalcTypeUsesTypeSafeEnumBase Add =
                new CalcTypeUsesTypeSafeEnumBase(0,"足し算", "+", (v1, v2) => v1 + v2);
            public static readonly CalcTypeUsesTypeSafeEnumBase Subtract =
                new CalcTypeUsesTypeSafeEnumBase(1,"引き算", "-", (v1, v2) => v1 - v2);
            public static readonly CalcTypeUsesTypeSafeEnumBase Multiple =
                new CalcTypeUsesTypeSafeEnumBase(2,"掛け算", "*", (v1, v2) => v1 * v2);
            public static readonly CalcTypeUsesTypeSafeEnumBase Divide =
                new CalcTypeUsesTypeSafeEnumBase(3,"割り算", "/", (v1, v2) => v1 / v2);

            private CalcTypeUsesTypeSafeEnumBase(int key, string name, string symbol, Func<decimal, decimal, decimal> calcFunc) 
                : base(key, name)
            {
                _calcFunc = calcFunc ?? throw new ArgumentNullException(nameof(calcFunc));
                Symbol = symbol;
            }
            /// <summary>計算関数</summary>
            private readonly Func<decimal, decimal, decimal> _calcFunc;

            /// <summary>記号</summary>
            public string Symbol { get; }

            /// <summary>計算を実施</summary>
            /// <param name="v1">値1</param>
            /// <param name="v2">値2</param>
            /// <returns>計算結果</returns>
            public decimal Calc(decimal v1, decimal v2) => _calcFunc(v1, v2);
        }

これでステップ2よりはだいぶスッキリ。

TypeSafeEnumBaseの他の使用例

もう少し実践的にわかりやすい例を示すために(嘘)、自分の好きなプログレバンドを実装してみよう。
(Hadi Hariri先生もKotolinの説明の時にプログレバンドPink Floyedを使ってたしぃ)
https://hhariri.wordpress.com/2013/11/18/refactoring-to-functional-reducing-and-flattening-lists/
↓引用

data class Track(val title: String, val durationInSeconds: Int)

val pinkFloyd = listOf(
        Album("The Dark Side of the Moon", 1973, 2, 1,
                listOf(Track("Speak to Me", 90),
                        Track("Breathe", 163),
                        Track("On he Run", 216),
                        Track("Time", 421),
                        Track("The Great Gig in the Sky", 276),
                        Track("Money", 382),
                        Track("Us and Them", 462),
                        Track("Any Color You Like", 205),
                        Track("Brain Damage", 228),
                        Track("Eclipse", 123)
                )
        ))
        // the rest omitted for brevity

TypeSafeEnumBaseの例:プログレバンド名と国籍

        /// <summary>ProgRockBands for TypeSafeEnumBase Demo</summary>
        private class ProgRockBands : TypeSafeEnumBase<ProgRockBands, int>
        {
            public static readonly ProgRockBands MoonSafari
                = new ProgRockBands(1, "Moon Safari", Country.Sweden);

            public static readonly ProgRockBands GentleGiant
                = new ProgRockBands(2, "Gentle Giant", Country.UK);

            public static readonly ProgRockBands Genesis
                = new ProgRockBands(3, "Genesis", Country.UK);

            public static readonly ProgRockBands CameliasGarden
                = new ProgRockBands(4, "Camelias Garden", Country.Italy);

            public static readonly ProgRockBands IQ
                = new ProgRockBands(5, "IQ", Country.UK);

            public static readonly ProgRockBands PFM
                = new ProgRockBands(6, "PFM", Country.Italy);

            public static readonly ProgRockBands YoninBayashi
                = new ProgRockBands(7, "四人囃子(Yonin Bayashi)", Country.Japan);

            public static readonly ProgRockBands TheFlowerKings
                = new ProgRockBands(8, "The Flower Kings", Country.Sweden);

            /// <summary>Initializes a new instance of the <see cref="ProgRockBands" /> class.</summary>
            /// <param name="key">The key.</param>
            /// <param name="name">The name.</param>
            /// <param name="country">The country.</param>
            private ProgRockBands(int key, string name, Country country)
                : base(key, name)
            {
                Country = country;
            }

            /// <summary>Country</summary>
            public Country Country { get; }
        }

        private enum Country
        {
            UK,
            Italy,
            Sweden,
            Japan
        }

上記クラスのテストコードは以下のような感じ(※要Chaining Assertion:Nugetから取得)

    <summary>ProgRockBandsTest</summary>
    [TestClass]
    public class ProgRockBandsTest
    {
        /// <summary>CountTest</summary>
        [TestMethod]
        public void CountTest()
        {
            ProgRockBands.Count.Is(8);
        }

        /// <summary>Valueses the test.</summary>
        [TestMethod]
        public void ValuesTest()
        {
            // All
            ProgRockBands.Values.Is(
                ProgRockBands.MoonSafari,
                ProgRockBands.GentleGiant,
                ProgRockBands.Genesis,
                ProgRockBands.CameliasGarden,
                ProgRockBands.IQ,
                ProgRockBands.PFM,
                ProgRockBands.YoninBayashi,
                ProgRockBands.TheFlowerKings);

            // Filter by country
            ProgRockBands.Values.Where(v => v.Country == Country.Sweden)
                .Is(ProgRockBands.MoonSafari, ProgRockBands.TheFlowerKings);
            ProgRockBands.Values.Where(v => v.Country == Country.Italy)
                .Is(ProgRockBands.CameliasGarden, ProgRockBands.PFM);
            ProgRockBands.Values.Where(v => v.Country == Country.UK)
                .Is(ProgRockBands.GentleGiant, ProgRockBands.Genesis, ProgRockBands.IQ);
            ProgRockBands.Values.Where(v => v.Country == Country.Japan).Is(ProgRockBands.YoninBayashi);
        }

        /// <summary>FromKeyTest</summary>
        [TestMethod]
        public void FromKeyTest()
        {
            ProgRockBands.FromKey(1).Is(ProgRockBands.MoonSafari);
            ProgRockBands.FromKey(8).Is(ProgRockBands.TheFlowerKings);

            // no match value => default
            ProgRockBands.FromKey(9).IsNull();
            ProgRockBands.FromKey(10, ProgRockBands.YoninBayashi).Is(ProgRockBands.YoninBayashi);
        }
   }

それでは良いプログラミングライフをノシ

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

LINQのToDictonaryを覚えた

ToDictionaryをMicrosoftのドキュメントで見てみると、

ToDictionary<TSource,TKey,TElement>(IEnumerable<TSource>, Func<TSource,TKey>, Func<TSource,TElement>)

って書いてある。が、俺には理解不能。

今回の記事の初期化

いかにも例題的なクラス

Employee.cs
class Employee
{
    /// <summary>
    /// 社員ID
    /// </summary>
    /// <remarks>プライマリキー</remarks>
    int Id { get; set; }

    /// <summary>
    /// 社員名
    /// </summary>
    string Name { get; set; }

    // 他のクラスメンバー略...
}

がある。

とある会社の全従業員をぶちこんだ、
List Employees; が存在する。

ここから、社員IDと社員名を列挙した、
Dictionary<int, string> EmployeeDictionary;
を作成する。
俺のコードをチェックする誰かを満足させるまでを書いた。

紆余曲折

最初はforで書いた

for(var i=0; i<Emploees.Count; i++)
{
    EmployeeDictionary.Add(Employees[i].Id, Employees[i].Name); 
}

次はforeachに書き直した

以前、
https://qiita.com/StaticProgramer/items/fab629ce838fb52ee814
で学んだ通り、まずforではアレだと言われるので、foreachに書き直す。

foreach (var employee in Employees)
{
    EmployeeDictionary.Add(employee.Id, employee.Name);
}

でも、これでは不合格だって言われたので。

KeyValuePair<int, string>を使えってことか?

// だめだ。よくわかんねぇ

悩むこと数分。

結論

そうか。LINQか。

EmployeeDictionary = Employees.ToDictionary(x => x.Id, x => x.Name);

1行で書けた。

番外編

苦闘中に書いた間違い作品。
てっきりKeyValuePairを使うんだと思ってたから。

// これじゃList<KeyValuePair> なんだぜ。。。
var dic = Employees.Select(x => { return new KeyValuePair<int, string>(x.Id, x.Name); }).Tolist();

// KeyにKeyは入ってるけど、ValueにKeyValuePairが入ってる気がした。
var dic2 = Employees.Select(x => { return new KeyValuePair<int, string>(x.Id, x.Name); }).ToDictionary(x => x.Key);
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

C#でSelenium,Appiumクライアントを開発したときの覚書

はじめに

C#でSeleniumおよびAppiumクライアントを開発したときの覚書です。

  • クライアント開発・動作環境
    OS : Windows10 Pro
    IDE : Visual Studio 2017

  • 使ったバージョン
    Selenium : 3.14
    Appium : 4.0.0.4-beta

参考ドキュメント

Selenium API Reference
チートシート
逆引きクイックリファレンス(※C#はないが何となく雰囲気参考になる)
appium dot-net-driver wiki

最初理解が難しかったポイント

  • WebDriver
    各種ブラウザを外部からAPIで制御するためのドライバ。ブラウザごと(エンジンごと)に異なるWebDriverが存在する。SeleniumはWebDriverに対する入力を仲介してくれる。

  • SeleniumからWebDriverへのアクセス方法
    アクセス方法は2種類あり、一つはクライアントからWebDriverに直接アクセスする方法。もう一つはSelenium Serverを介してWebDriverにアクセスする方法。
    前者の場合、ブラウザに対応するWebDriverのインスタンスを使う。後者の場合、RemoteWebDriverインスタンスを使う。
    RemoteWebDriverの場合、インスタンス作成時に引き渡すオプションでブラウザの種類を指定する。

  • DesiredCapabilities
    Selenium3ではDriverOptions(を継承した)インスタンスに変更されている。昔のバージョンの記事を見るとDesiredCapabilitiesの指定がいっぱいあってちょっと混乱する。
    インスタンス作成時のオプション指定みたいなもの。DriverOptionのプロパティに含まれていないCapability指定も存在する。
    しかしどのブラウザでどういったCapability指定が使用できるかの調べ方がいまだによくわからない。どこかに一覧ないかなぁ。。

  • WindowsのC#でクライアントを開発したときにMacのSafariやiPhoneのテストができるのか
    実機があればできる。Mac上にSelenium Standalone Server、Appium Serverを立てて、それを仲介してRemoteWebDriverで操作する。スクリーンショットもWindows側にリモートで取得できる。便利!

導入方法

Selenium

nugetパッケージ

パッケージマネージャーコンソール
Install-Package Selenium.WebDriver
Install-Package Selenium.RC
Install-Package Selenium.WebDriverBackedSelenium
Install-Package Selenium.Support
Install-Package DotNetSeleniumExtras.WaitHelpers
Install-Package DotNetSeleniumExtras.PageObjects
Install-Package Selenium.WebDriver.ChromeDriver
Install-Package Selenium.WebDriver.IEDriver
Install-Package Selenium.WebDriver.GeckoDriver
Install-Package Selenium.WebDriver.MicrosoftWebDriver

Appium

nugetパッケージ

パッケージマネージャーコンソール
Install-Package Appium.WebDriver -Version 4.0.0.4-beta

AppiumはSeleniumのラッパーであり、Selenium3.13以降にはAppium3.xでは対応しておらずAppium4.x以上が必要。現時点ではbeta版しかない。(2019/2/3時点)

その他細かい点

  • IEはハードコピーがうまく取得できない。(変に切れたり偏ったりする。謎。)

  • IEは拡大率100%にしておかないと起動時エラーになる。(オプションで無視して起動もできるが、ただでさえうまく取得できないハードコピーがさらにいまいちになる。)

  • IEはセキュリティ設定の保護モードのON/OFFがすべての設定(イントラ、インターネット、信頼済みサイト等)でどちらかに統一されている必要がある。
    混在していると何故かウインドウやエレメントが取得できない。

  • Javascriptで新規タブを作成する場合、ポップアップブロックがOFFになっている必要がある。

  • Firefoxはインストール後に一度起動してある程度いじってからでないとエラーになる。
    (具体的にどこをどうしたら大丈夫なのかは不明)

コードサンプル

メンバ定義や自分で作ったメソッドの定義を載せていないものもあるので考え方の参考程度に。

Driverインスタンスの作成

参考にさせて頂いた記事 : C#でSelenium2を使用して主要ブラウザを動かしてみた

WebDriverFactory.cs
using OpenQA.Selenium;
using OpenQA.Selenium.Appium;
using OpenQA.Selenium.Appium.Android;
using OpenQA.Selenium.Appium.Enums;
using OpenQA.Selenium.Appium.iOS;
using OpenQA.Selenium.Chrome;
using OpenQA.Selenium.Edge;
using OpenQA.Selenium.Firefox;
using OpenQA.Selenium.IE;
using OpenQA.Selenium.Remote;
using OpenQA.Selenium.Safari;

    static class WebDriverFactory
    {
        /// <summary>
        /// 対象のWebDriverのインスタンスを作成します。
        /// </summary>
        public static IWebDriver CreateInstance(AppSettings.BrowserName browserName)
        {
            try
            {
                switch (browserName)
                {
                    case AppSettings.BrowserName.None:
                        throw new ArgumentException(string.Format("Not Definition. BrowserName:{0}", browserName));

                    case AppSettings.BrowserName.Chrome:
                        ChromeOptions ChromeOption = new ChromeOptions();
                        ChromeDriverService ChromeDService = ChromeDriverService.CreateDefaultService();
                        return new ChromeDriver(ChromeDService, ChromeOption);

                    case AppSettings.BrowserName.Firefox:
                        FirefoxOptions FirefoxOption = new FirefoxOptions();
                        FirefoxDriverService FFDService = FirefoxDriverService.CreateDefaultService();
                        //Selenium3.12以前
                        //return new FirefoxDriver(FFDService, FirefoxOption,new TimeSpan(0,0,60));
                        //Selenium3.13以降
                        return new FirefoxDriver(FFDService, FirefoxOption);

                    case AppSettings.BrowserName.InternetExplorer:
                        InternetExplorerOptions IEOption = new InternetExplorerOptions();
                        //ズームは100%でないとスクリーンショットが切れる(これでも切れることがある)
                        IEOption.IgnoreZoomLevel = false;
                        //保護モードが混在していると要素が取得できない
                        IEOption.IntroduceInstabilityByIgnoringProtectedModeSettings = false ;
                        InternetExplorerDriverService IEService = InternetExplorerDriverService.CreateDefaultService("./", "IEDriverServer.exe");
                        return new InternetExplorerDriver(IEService, IEOption);

                    case AppSettings.BrowserName.Edge:
                        EdgeOptions EdgeOption = new EdgeOptions();
                        EdgeDriverService EdgeDService = EdgeDriverService.CreateDefaultService();
                        return new EdgeDriver(EdgeDService, EdgeOption);

                    default:
                        throw new ArgumentException(string.Format("Not Definition. BrowserName:{0}", browserName));
                }
            }
            catch (Exception)
            {
                //エラーが発生したらWebDriverを強制終了する
                ProcessHandler.KillWebDriverProcess();

                //エラーはそのまま上位に返す
                throw;
            }
        }


        /// <summary>
        /// 対象のリモートWebDriverインスタンスを作成します。
        /// </summary>
        /// <returns></returns>
        public static IWebDriver CreateRemoteInstance(AppSettings.RemoteBrowserName browserName, string ipAddress, string OsVersion, string DeviceName)
        {
            try
            {
                //Appium3以前
                //DesiredCapabilities capabilities = new DesiredCapabilities();

                //Appium4以降
                AppiumOptions capabilities = new AppiumOptions();


                switch (browserName)
                {
                    case AppSettings.RemoteBrowserName.None:
                        throw new ArgumentException(string.Format("Not Definition. BrowserName:{0}", browserName));

                    case AppSettings.RemoteBrowserName.Chrome:
                        ChromeOptions ChromeOption = new ChromeOptions();
                        return new RemoteWebDriver(new Uri("http://" + ipAddress + ":4444/wd/hub"), ChromeOption);

                    case AppSettings.RemoteBrowserName.Safari:
                        SafariOptions SafariOption = new SafariOptions();
                        return new RemoteWebDriver(new Uri("http://" + ipAddress + ":4444/wd/hub"), SafariOption);
                    case AppSettings.RemoteBrowserName.iOS:
                        //Appium3以前
                        //capabilities.SetCapability(MobileCapabilityType.PlatformName, "iOS");
                        //capabilities.SetCapability(MobileCapabilityType.PlatformVersion, OsVersion);
                        //capabilities.SetCapability(MobileCapabilityType.DeviceName, DeviceName);
                        //capabilities.SetCapability(MobileCapabilityType.BrowserName, "Safari");
                        //capabilities.SetCapability(MobileCapabilityType.Udid, "auto");
                        //capabilities.SetCapability(MobileCapabilityType.AutomationName, "XCUITest");

                        //Appium4以降
                        capabilities.PlatformName = "iOS";
                        capabilities.AddAdditionalCapability(MobileCapabilityType.PlatformVersion, OsVersion);
                        capabilities.AddAdditionalCapability(MobileCapabilityType.DeviceName, DeviceName);
                        capabilities.AddAdditionalCapability(MobileCapabilityType.BrowserName, "Safari");
                        capabilities.AddAdditionalCapability(MobileCapabilityType.Udid, "auto");
                        capabilities.AddAdditionalCapability(MobileCapabilityType.AutomationName, "XCUITest");


                        return new IOSDriver<IOSElement>(new Uri("http://" + ipAddress + ":4723/wd/hub"), capabilities);

                    case AppSettings.RemoteBrowserName.Android:
                        //Appium3以前
                        //capabilities.SetCapability(MobileCapabilityType.PlatformName ,"Android");
                        //capabilities.SetCapability(MobileCapabilityType.PlatformVersion, OsVersion);
                        //capabilities.SetCapability(MobileCapabilityType.DeviceName, DeviceName);
                        //capabilities.SetCapability(MobileCapabilityType.BrowserName, "Chrome");

                        //Appium4以降
                        capabilities.PlatformName = "Android";
                        capabilities.AddAdditionalCapability(MobileCapabilityType.PlatformVersion, OsVersion);
                        capabilities.AddAdditionalCapability(MobileCapabilityType.DeviceName, DeviceName);
                        capabilities.AddAdditionalCapability(MobileCapabilityType.BrowserName, "Chrome");


                        return new AndroidDriver<AppiumWebElement>(new Uri("http://" + ipAddress + ":4723/wd/hub"), capabilities);
                    default:
                        throw new ArgumentException(string.Format("Not Definition. BrowserName:{0}", browserName));
                }
            }
            catch (Exception)
            {
                //エラーが発生したらWebDriverを強制終了する
                ProcessHandler.KillWebDriverProcess();

                //エラーはそのまま上位に返す
                throw;
            }
        }
    }
}

セレクタの種類と名前を指定して要素の探索

        private delegate By ByLocaterDelegate(string nameToFind);

        private static Dictionary<string, ByLocaterDelegate> byLocaterList
            = new Dictionary<string, ByLocaterDelegate>()
            {
                {"classname",By.ClassName},
                {"cssselector",By.CssSelector},
                {"id",By.Id},
                {"linktext",By.LinkText},
                {"name",By.Name},
                {"partiallinktext",By.PartialLinkText},
                {"tagname",By.TagName},
                {"xpath",By.XPath},
            };

        /// <summary>
        /// 要素の種類と探索する要素名を指定してbyセレクタを返す
        /// </summary>
        /// <param name="elementType">要素の種類</param>
        /// <param name="nameToFind">探索する名称</param>
        /// <returns>byセレクタ</returns>
        private static By ByElementName(string elementType, string nameToFind)
        {
            if (byLocaterList.ContainsKey(elementType.ToLower()))
            {
                ByLocaterDelegate byLocater = byLocaterList[elementType.ToLower()];
                return byLocater(nameToFind);
            }
            else
            {
                //指定がない場合はIDとする
                ByLocaterDelegate byLocater = byLocaterList["id"];
                return byLocater(nameToFind);
            }
        }

呼び出し
        element = Driver.FindElement(ByElementName(elementType, nameToFind));

複数フレームの要素探索

要素が見つからなかったときに例外で返すのが個人的には好きじゃないです。
じゃあどう返せばいいのかと言われると難しいですが。nullよりは例外のほうがいいんですかね~。うーん。

※waitはWebDriverWaitインスタンス。(以降全部同じ)

        /// <summary>
        /// 複数フレーム内の要素探索を行う
        /// </summary>
        /// <param name="elementType">要素の種類</param>
        /// <param name="nameToFind">要素の名前</param>
        /// <exception cref="NoSuchElementException"></exception>
        private IWebElement FindElementInFrame(string elementType, string nameToFind)
        {
            //フレームがある場合、要素が見つかるまでフレーム間を移動する。
            List<IWebElement> frameElements = Driver.FindElements(By.TagName("frame")).ToList<IWebElement>();
            if (frameElements.Count > 0)
            {

                for (int i = 0; i < frameElements.Count; i++)
                {
                    try
                    {
                        //一旦親ウインドウに戻ってから次のフレームに移動しないとエラーになる
                        wait.Until(drv => drv.SwitchTo().DefaultContent());
                        wait.Until(drv => drv.SwitchTo().Frame(frameElements[i]));
                        element = Driver.FindElement(ByElementName(elementType, nameToFind));
                        break;
                    }
                    catch (NoSuchElementException)
                    {
                        continue;
                    }

                }
            }

            if (element == null)
            {
                throw new NoSuchElementException();
            }
            else
            {
                return element;
            }
        }

タイムアウトの設定

        /// <summary>
        /// WebDriverのInplicitTimeoutを設定する
        /// </summary>
        /// <param name="timeout">タイムアウト値(秒)</param>
        private bool SetTimeout(double timeout)
        {
            try
            {
                Driver.Manage().Timeouts().ImplicitWait = TimeSpan.FromSeconds(timeout);
            }
            catch (Exception ex) when (ex is ArgumentException || ex is OverflowException)
            {
                return false;
            }
            return true;
        }

スクリーンショットをbitmapで取得

        /// <summary>
        /// 取得したスクリーンショットをビットマップとして返す
        /// </summary>
        private Bitmap GetScreenshotAsBmp()
        {
            var byteImg = ((ITakesScreenshot)Driver).GetScreenshot().AsByteArray;
            Bitmap bmp;
            using (MemoryStream ms = new MemoryStream(byteImg))
            {
                bmp = new Bitmap(ms);
            }
            return bmp;
        }

Javascriptを使ってページロードを待つ

正しく動いているのかちょっと疑問ですが一応。
StackOverFlowから拾ってきた

        /// <summary>
        /// Javascriptを使ってページロードの完了を待つ
        /// </summary>
        private bool WaitPageLoad()
        {
            var javaScriptExecutor = Driver as IJavaScriptExecutor;
            Func<IWebDriver, bool> readyCondition = webDriver => (bool)(javaScriptExecutor
                    .ExecuteScript("return (document.readyState == 'complete')"));
            return wait.Until(readyCondition);
        }

指定した要素に移動する

モバイルのときにうまくいかない…。

        /// <summary>
        /// 指定した要素に移動する
        /// </summary>
        private void MoveToElement(IWebElement element)
        {
            //要素の位置までスクロールする
            wait.Until(Drv => ((IJavaScriptExecutor)Drv).ExecuteScript("arguments[0].scrollIntoView(false);return true;", element));

            try
            {
                Actions actions = new Actions(Driver);
                actions.MoveToElement(element);
                actions.Perform();
            }
            catch (Exception ex)
            {   //モバイルの場合は下記のエラーになるので移動しない
                if (!ex.Message.StartsWith("An unknown server-side error occurred while processing the command. Original error: Error Domain=com.facebook.WebDriverAgent Code=1 \"Unsupported origin type"))
                {
                    throw;
                }
            }
        }

画面全体のスクリーンショットを取得

スクロールで動的にメニューが変化する場合や上部に固定のメニューフレームがあるパターンには対応していません。

参考にさせて頂いた記事:
Chromeでも画面全体のキャプチャ―を取得する
【C#】画像の結合 (Util.ImageCombineVはこの記事をそのままコピペ)

            decimal innerH = 0;
            decimal innerW = 0;
            decimal scrollH = 0;
            innerH = Int32.Parse(jsDriver.ExecuteScript("return window.innerHeight").ToString());
            innerW = Int32.Parse(jsDriver.ExecuteScript("return window.innerWidth").ToString());
            scrollH = Int32.Parse(jsDriver.ExecuteScript("return document.documentElement.scrollHeight").ToString());

            string filePath = "スクリーンショットを保存したいパス";

            wait.Until(Drv => Drv.SwitchTo().DefaultContent());

            if (innerH > scrollH)
            {
                ((ITakesScreenshot)Driver).GetScreenshot().SaveAsFile(filePath, ScreenshotImageFormat.Png);
            }
            else
            {
                decimal repeat = Math.Ceiling((scrollH / innerH));
                decimal duplH = Math.Abs(scrollH - (innerH * repeat));

                List<Bitmap> screenshots = new List<Bitmap>();
                for (int i = 0; i < repeat; i++)
                {
                    wait.Until(Drv => ((IJavaScriptExecutor)Drv).ExecuteScript("window.scrollTo(0," + innerH * i + ");return true;"));
                    //ちょっと待たないとスクロールが間に合わない
                    Thread.Sleep(100);
                    screenshots.Add(GetScreenshotAsBmp());
                }

                //最後の画像は重複部分を切り取る
                Bitmap lastSc = screenshots.Last();

                //画像の縦横のサイズとJavascriptで取得した幅と高さから比率計算し、切り取るべきサイズを算出する
                decimal bitmapWidth = lastSc.Width;
                decimal bitmapHeight = lastSc.Height;

                decimal ratioWidth = innerW / bitmapWidth;
                decimal ratioHeight = innerH / bitmapHeight;

                int duplHeight = (int)Math.Round(duplH / ratioHeight, 0, MidpointRounding.AwayFromZero);
                int cutWidth = (int)Math.Round(innerW / ratioWidth, 0, MidpointRounding.AwayFromZero);
                int cutHeight = (int)Math.Round((innerH - duplH) / ratioHeight, 0, MidpointRounding.AwayFromZero);

                //算出したサイズで切り出す
                Rectangle rect = new Rectangle(0, duplHeight, cutWidth, cutHeight);
                screenshots.RemoveAt(screenshots.Count - 1);
                screenshots.Add(lastSc.Clone(rect, lastSc.PixelFormat));

                Bitmap screenshot = Util.ImageCombineV(screenshots.ToArray<Bitmap>());
                screenshot.Save(filePath);
            }

通常では変更できない要素の値を強制的に書き換える

            IWebElement element = Driver.FindElement(ByElementName(elementType, nameToFind));
            jsDriver.ExecuteScript("arguments[0].value = '" + Value + "';", element);

通常では変更できない要素のテキストを強制的に書き換える

            IWebElement element = Driver.FindElement(ByElementName(elementType, nameToFind));
            jsDriver.ExecuteScript("arguments[0].innerHTML = '" + Text + "';", element);
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

C++からC#のDLLを呼ぶ方法

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

LeapMotion+Unityでグー・チョキ・パーを認識する

はじめに

VRMの流行の兆しが見えたので推しの子とじゃんけんが出来るソフトでも作っておこうかな、と思ったのでLeapMotionでじゃんけんの手を検知する仕組みを作りました。知見共有。


制作

新しい方のLeapMotionSDKとUnityの知見は少な目な気がします…もしかしてもうLeapMotion界隈下火だったりします…?

LeapMotionSDKの準備

まずLeapMotion公式からUnity向けSDKをダウンロードしておきます。中にあったUnityPackageをProjectにインポートします。

Unity側の準備

Assets/LeapMotion/Core/Examples/Capsule Hands(Desktop)からLeapMotionControllerとHandModelsをコピペして自分のシーンに貼り付けます。コレさえあればとりあえず動くはずなので、試しに実行してみて手がトラッキングできればとりあえず成功です。

手の形を認識する

HandRSP.cs
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Leap;
using System.Linq;
public class HandRSP : MonoBehaviour {
    private Controller controller;
    private Finger[] fingers;
    private bool[] isGripFingers;
    public enum RSP
    {
        Rock, Scissors, Paper
    };
    public RSP rsp;
    // Use this for initialization
    void Start () {
        controller = new Controller();
        fingers = new Finger[5];
        isGripFingers = new bool[5];
    }

    // Update is called once per frame
    void Update () {
        Frame frame = controller.Frame();
        if(frame.Hands.Count != 0)
        {
            List<Hand> hand = frame.Hands;
            fingers = hand[0].Fingers.ToArray();
            isGripFingers = Array.ConvertAll(fingers, new Converter<Finger, bool>(i => i.IsExtended));
            Debug.Log(isGripFingers[0]+","+ isGripFingers[1] + "," + isGripFingers[2] + "," + isGripFingers[3] + "," + isGripFingers[4]);
            int extendedFingerCount = isGripFingers.Count(n => n == true);
            if(extendedFingerCount == 0)
            {
                rsp = RSP.Rock;
            }
            else if(extendedFingerCount < 4)
            {
                rsp = RSP.Scissors;

            }
            else
            {
                rsp = RSP.Paper;

            }

        }
    }
}

コレをシーン内の好きなオブジェクトにアタッチすればrspにグーチョキパーの認識結果が表示されます。

解説

usingステートメント

LeapMotionの機能を利用するにはusing Leap;のステートメントを利用します。

Controller

LeapMotionの制御はControllerで行い、これがメインのインターフェースとなります。

Frame

Controller.Frameには手の挙動の情報が含まれています。

Hand

Hand型には手の位置、角度や各指の情報が存在しています。Controller.Handsで認識されている全ての手についてのHandを返します。認識範囲内に一つも手がなければ当然要素数0になるので例外処理をしっかりしましょう。

Finger

各手についている指の情報を保有する型です。位置、指し示す向き、握っているか否かなどの情報を取得することができます。Hand.Fingersですべての指のFingerを取得することができます。また、各Fingerに対してFinger.IsExtendで指の開きを検知することができます。指が開いているときTrue、指が曲がっているときがFalseです。上記の実装例ではisGripFingers = Array.ConvertAll(fingers, new Converter<Finger, bool>(i => i.IsExtended));で全ての指の曲がり方のブール値を取得したのち、int extendedFingerCount = isGripFingers.Count(n => n == true);(LinQを利用しています)で曲がっている指の本数を取得しています。

RSP

曲がった指の本数を基準にグーチョキパーのいずれの手が出されているか判断します。RSPは手の種類をまとめたEnumで、今回は曲がった指の本数が5本でグー、4~2本でチョキ、1本以下でパーとしています。
キャプチャ164.PNG
キャプチャ165.PNG
キャプチャ163.PNG

問題点

取り敢えず雑に認識させることができましたが、チョキとグーの認識が結構厳しく、ちゃんと手を下に向けないとうまく認識してくれなかったりします。これについては部屋が明るいのとか僕の手がオタク特有のまっしろハンドなのとかいろいろ原因がありそうです。

解決のアイデア

まぁ別にガチじゃんけんするわけでもないしコンピュータ側の出す手に合わせてユーザーを勝たせる側に強めの認識補正をかけてあげればいいんじゃないでしょうか。勝てる手を出したのに認識ミスで負けるってのはプレイフィール最悪なのでそれだけは避ける方向で行くといいと思います。

最後に

ちょうどVroidHubSDKも頂けたのでこれを利用して好みのモデルとじゃんけんが出来るシステムを組んでいきたいです。やるぞ~~

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

はじめての非同期処理 call backは,送信機と受信機のイメージで.(OcuGo.Games)

非同期処理とは。

このボタンが押されたら、とか。

敵が3体倒されたら、とか。

一般論でいうと、この人から電話がきたら、とか。

自分のタイミングではなく、外部からのトリガーで処理を実行したい際に「非同期処理」を使います。

送り手と受け手がいる.

送り手: 条件を満たしました!
keitai_mukashi.png

受け手: 条件満たしたのね.連絡ありがと! 次の処理流そっと.
denwa_business_woman.png

callbackは System.Action型

C#では、System.Action型の Callback という関数が用意されています.
以下では、「スペースキーが3回押されたら」という条件をトリガーに、次の処理を流すという設定でコードを紹介します。

送り手側

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class CountSpace : MonoBehaviour {
    System.Action Callback;
    int spacenum = 0;
   // Update is called once per frame
    void Update()
    {    //スペースキーを押すとspacenumをカウントアップする.
        if (Input.GetKeyDown(KeyCode.Space)){
            this.spacenum++;
        }
          //スペースキーが3回押されたら
        if (spacenum == 3) {
            this.Callback();  //送信機を使用して連絡をする.
        }
     }

    //送信機の設置
    public void SetCallback(System.Action Callback) 
    {
        this.Callback = Callback; //
    }



受け手側

  public CountSpace A; //CountSpaceという classの Aという変数

    // Use this for initialization
    void Start () 
   {     //電話が来たらカッコ内の処理を流す.
        A.SetCallback(連絡を受け取ってから流したい処理);
    }

ここでは仮に変数名をAとしましたが,なんでも構いません.
イメージ的にはAさんからかかってきたらということですが,そのAさんが誰であるか限定する必要がないためです。

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