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

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行で書き表されてしまいました。

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

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