ラベル ラムダ式 の投稿を表示しています。 すべての投稿を表示
ラベル ラムダ式 の投稿を表示しています。 すべての投稿を表示

2018年4月13日金曜日

ここで、ちょっと立ち止まってラムダ式の研究

とにかく最後まで進める為に、輪切り画像の生成に集中して先に進もうと思っていました。しかし、前回の記事を書いていて、ラムダ式に引っかかりがあって、ある程度理解できないと気が済まなくなりました。

一旦立ち止まって、ラムダ式の研究をしてみたいと思います。

お付き合いください。

たまにお世話になっている(とは言っても、勝手に拝見しているだけなんですけど)Qitaのアカウントrawr様の記事から、以下の例を考えてみます。

Listに入っている文字列の後ろに、何か決まった文字列を追加したいとき、foreach文を使って書くと以下のようになると思います。foreachが2回出て来ますが、最初のforeachは後ろに文字列を追加する処理、2番目に出てくるforeachは、処理結果をコンソールに出力するためのものです。

リスト1

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

namespace lamda
{
    class Program
    {
        static void Main(string[] args)
        {
            var list = new List<string>();  // Listクラスのオブジェクトとしてlistを生成
            list.Add("aaa");  // listの要素を追加していきます
            list.Add("bbb");
            list.Add("ccc");
            list.Add("ddd");
            list.Add("eee");
            list.Add("fff");
            list.Add("ggg");

          // 処理結果を格納する為のListクラスのオブジェクトresultを生成        
            var result = new List<string>();  

            foreach ( var s in list )
            {
                result.Add( s + ".hoge");
            }
            
            foreach ( var s in result )
            {
                Console.WriteLine( s );
            }
        }
    }
}
ここで出てくるListクラスは初めて出てくるものです。Listは以前、「CSVのデータを一旦変数に格納、型変換をして描画処理を行います」のエントリーで出て来たArrayListによく似た可変長配列を実現するためのクラスです。 
ArrayListとの違いはこちらのページに説明があります。できるだけListを使いましょう、とのこと。 
ArrayListでは、各要素の型はオブジェクト型で、どんな型のデータでも格納できるものの、使うときに必ずキャストする必要があるのに対して、Listではオブジェクトを生成するときに決まってしまうと書いてあります。使うときに必ずキャストしなければならないのは、ちょっと面倒ですし、各要素の型が要素ごとに違う、なんていう設計はしないとおもいますので、最初に適切に型が決まった方が使いやすそうです。
このブログで取り扱っている描画処理のプログラムに於いては、この辺の事情も良くわからずにコーディングしていたので、ArrayListを使用しており、それをそのまま載せています。

ここで、参考にさせていただいたQiitaの筆者様は、もう少し簡潔に書けないものかと考えられたわけですね。

そこで、Selectメソッドというメソッドに登場してもらいます。上のリストはもう少し簡潔に以下のように書くことが出来るようです。

リスト2

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

namespace lamda2
{
    class Program
    {
        static void Main(string[] args)
        {
            var list = new List<string>();
            list.Add("aaa");
            list.Add("bbb");
            list.Add("ccc");
            list.Add("ddd");
            list.Add("eee");
            list.Add("fff");
            list.Add("ggg");

            // データの格納先を生成するとともに、データの加工メソッドをSelectメソッドに渡す
            var result = list.Select( append ); 

            foreach( string s in result )
            {
                Console.WriteLine( s );
            }
        }

        private  static string append( string s )
        {
            return s + "hoge";
        }
    }
}

ここで、見慣れない書法が出て来ました。var result = list.Select( append );です。

list.Select()はメソッドですが、メソッドの引数としてメソッドの名前を指定しています。更にスッキリしなくなったので、色々調べ回ったところ、これはDelegateの一種で、Func<T, TResult>によるDelegateである、ということのようです。マイクロソフトの説明がここにあります。

メソッドとは、クラスに属する関数、もしくは手続きなので、メソッドに渡す引数は本来数値もしくは文字、文字列といった「値」です。しかし、今回のようにメソッドにメソッドを渡したい、というニーズがあり、このような拡張がなされたものと思います。今回の例では、Selectメソッドの中で、Listオブジェクトのメンバー全てに対して、メンバーを加工する方法をSelectメソッドに与えたいのです。

C#は進化し続ける高級言語のようで、あとから色々な仕様追加がなされていますね。勉強する側の立場からすると、言語仕様的にスッキリしなくてわかりにくい、という印象を受けます。

さて、リスト2で定義されてSelectメソッドに渡されているappendメソッドは、たった1行の小さなメソッドです。このぐらいのメソッドであれば、わざわざ別の場所で定義して使うよりも、もっと簡潔な書き方ができないか?ということで考えられたのが以下の書き方です。

var result = list.Select( (string s) => { return s + ".txt"; } );

こうやって、インライン展開して書くと、わざわざ別のところでメソッドを定義しなくてもよくなります。これがラムダ式なのだそうです。見慣れたラムダ式とは若干違いますね。

ここで、


  • 引数の型が自明(型推論できる)の場合は省略可能
  • メソッドが1文しかない場合は、{}やreturnを省略可能
  • 引数が1つしかない場合は()を省略可能


といったルールがあるようで、上記リストは

var result = list.Select( s => s + ".txt" );

と書き換えることが出来る、ということになります。これで、割によく見るラムダ式になりました。

これから、最初のリストは以下のように書き換えることが出来ます。

リスト3

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

namespace lamda3
{
    class Program
    {
        static void Main(string[] args)
        {
            var list = new List<string>();
            list.Add("aaa");
            list.Add("bbb");
            list.Add("ccc");
            list.Add("ddd");
            list.Add("eee");
            list.Add("fff");
            list.Add("ggg");

            var result = list.Select( s => s + "hoge" );

            foreach( string s in result )       
            {
                Console.WriteLine( s );
            }
        }
    }
}

リスト1に書いたforeach節は、この1行のラムダ式で書き表された、ということになります。

さて、ここからは余談になります。

ここで、var result = list.Select( s => s + "hoge" );なる式でresultに結果を受け取ると、resultはIEnumerable<T>型となります。これを、ToList()メソッドでList<T> 型に変換してあげると、foreach節をより短い記述にすることが出来ます。

リスト4

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

namespace lamda3
{
    class Program
    {
        static void Main(string[] args)
        {
            var list = new List<string>();
            list.Add("aaa");
            list.Add("bbb");
            list.Add("ccc");
            list.Add("ddd");
            list.Add("eee");
            list.Add("fff");
            list.Add("ggg");

            var result = list.Select( s => s + "hoge" ).ToList();

            result.ForEach( Console.WriteLine );
        }
    }
}

結果出力のforeach節も、1行で書き表されてしまいました。

今までの流れからすると、ラムダ式を使うとメソッドに手続きを渡すことが出来る、ということになります。ただ、手続きを受け取る側もそれに応じた書き方をする必要があるようです。それはまた別の機会に書こうと思います。

今回はここまでにします。


2018年4月3日火曜日

いよいよ輪切り画像の生成に取り組む

いよいよ、輪切り画像の生成に取り組みます。

輪切りする場所の座標を取得する


今まで何度かマウスカーソルの座標を取得し続ける、という動作を取り上げました。取得し続けるのはリアルタイム処理で難しそうですが、輪切りする場所の座標を取得する、という処理はそれ程難しくありません。pictureBoxの上でマウスボタンがクリックされたとき、そのクリックされた座標を取得すれば良いのです。

        /*
         * pictureboxの上でマウスをクリックしたときに発生するイベント
         */
        private void pictureBox1_Click(object sender, EventArgs e)
        {
            // クリックされたときのクライアント座標をsaveして使う。
            // mouse_x/mouse_yはマウスイベントが入ると更新されてしまうので、それを防ぐ
            int x_slice = mouse_x, y_slice = mouse_y;

            MessageBox.Show("Mouse clicked X= " + x_slice + " : Y= " + y_slice);
            // データの中にある座標で、クリックした座用に最も近いものを得る(Nearest拡張メソッド)
            MessageBox.Show("Slice by " + x_axis_dt_int.Nearest(x_slice));
           // drawSlice();

        }

この中で、mouse_xとmouse_yは、MouseMoveイベントが発生するたびにマウスカーソルの座標が代入されるグローバル変数です。

このメソッドをpictureBox_Clickイベントへのイベントハンドラとして登録すると、クリックされた時点でのマウスの座標をx_slice、y_sliceに取り込むことができます。

この時点では、輪切り画像を実際に描画するメソッドをまだ作成していなかったので、取得したマウスカーソルの座標をMessageBoxに表示するようにしました。

実はこれ、その後の学習で、イベントハンドラの引数のeの中に、イベントが発生したときのマウスカーソルの座標の情報が含まれることがわかりました。それを参照すれば、もっとシンプルで外の影響を受けないコードを書くことが出来ます。

今回のケースでは、学習したときそのままのコードを引用すると言うことで、最初に書いたコードをそのまま載せました。

最も近い数値を拾ってくる


マウスで輪切りするポイントの座標を拾うとき、拾った座標の数値と全く同じ数値がデータの中にあるとは限りません。データが全て整数でできていて、マウスポインターの座標も整数であれば、全く同じ数値がデータの中に存在する可能性はありますが、それは特殊なケースと考えておくのが後々無難です。

プログラムの詳細仕様を考えていて、この問題に突き当たりました。これを解決する為には、指定した数値に最も近い数値をデータの中から探す、という機能が必要になります。

色々と調べていたら、発見しました。コガネブログ様です。

下記が、最も近い値を拾ってくる、ということを実現するコードです。この中には、拡張メソッド、という技術と、ラムダ式という技術が入っています。


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

public static class IEnumerableExtensions
{
    public static int Nearest(this IEnumerable < int > self, int target)
    {
        var min = self.Min(c => Math.Abs(c - target));
        return self.First(c => Math.Abs(c - target) == min);
    }
}

呼び出す側では、こう言う書き方をします。

         private void drawSlice()
        {
            Bitmap img = new Bitmap(Constants.size_slicebox_x, Constants.size_slicebox_y);
            MessageBox.Show("Slice by "+x_axis.Nearest(mouse_x));
        }

呼び出す側では、まだスライス画像を実際に描くコードの実装ができていなかったので、指定したx座標に最も近いx座標をx_axisオブジェクトから探し出して、メッセージボックスに表示するようにしています。


拡張メソッドは、こちら(いつも助けていただいている「++C++ //未確認飛行 C様」に説明がありますが、何やら難しくてよくわかりません。

ラムダ式についても、同じ所に説明があります。これも難しいです。時間をかけて理解を深める必要があります。

もう一つこう言う説明もあります。Qiitaの「今更ですが、ラムダ式」という記事です。

特にラムダ式は記述の仕方が通常のC#のコードとあまりにも違って、理解が難しいので、今後研究して理解することにして、今はこういう物を入れておくと意図通りの動きをする、というレベルにとどめておきたいと思います。

今回の記事では、
  • 測定データからプロットを描画
  • 描画したプロットをクリックして輪切り画像を作成したい場所の座標を取得
  • 取得した座標に最も近いデータ(今回はX軸のデータのみ)をリストから拾う
  • 取得した座標をメッセージボックスに表示した後、輪切りする座標をメッセージボックスに表示する
ということを行っています。

前回の記事に対して、輪切り画像を作成したい場所の座標の取得と、取得した座標に最も近いデータをリストから拾う、という大きな2つの機能を追加しています。

プログラムリストを以下に示します。Form1のデザインは前回の記事と全く同じで、イベントの追加もないので、Form1.Designer.csは割愛します。