SelectManyでreturn nullはご法度 #linq #csharp 訂正あり

久しぶりのブログということで、細かいことを少しだけ。

編集:指摘に合わせてサンプルコードを初出に比べて一部修正してあります。
6/2:nullで例外を返すサンプルがnullを返さなくなっていたので修正

LINQに実装されている拡張メソッドの一つにSelectMany
というものがあり作用としては、Selectと同じ作用+配列の平坦化を行なってくれるという活用しどころの多い拡張メソッドの一つです。

具体例としては以下の様なテストコードのような動作をします。
(Stringの配列の配列を平坦化してstringの配列にしてくれます。)
※テストコードはMSTest+Chainning Assertionを利用

        [TestMethod]
        public void TestMethod1()
        {
            var values = new[] { new string[] { "a", "b","c"},
                                 new string[] { "d" }, new string[] { "e" } };

            values.SelectMany(x => x).Is("a","b","c","d","e");               
        }

こっからが本題なのですがそのSelectManyの中のラムダでnullを返すとNullReferenceExceptionが発行されます。

先ほどの例をベースに配列の中に”b”が含まれている時はnullを返す実装例を示します。
※テストコード中では例外が投げられることを期待しているためcatch句に入ったら成功としています。

        [TestMethod]
        public void TestMethod2()
        {
            var values = new[] { new string[] { "a", "b","c"},
                                 new string[] { "d" }, new string[] { "e" } };

            try
            {
                //SelectManyでnullをリターンすると落ちるはず。
                var outputValues = values.SelectMany(x => x.Contains("b") ? null : x).ToArray();
            }
            catch (Exception ex)
            {
                Assert.IsTrue(true);   
            }

        }

上記のような実装をした時、nullがreturnされるタイミングでNullReferenceExceptionが発行されます。

ので、上記のような要件の時は、以下のようにそもそも対象外のものを事前に除外しておくか、

        [TestMethod]
        public void TestMethod3()
        {
            var values = new[] { new string[] { "a", "b","c"},
                                 new string[] { "d" }, new string[] { "e" } };

            values.Where(x => !x.Contains("b")).SelectMany(x => x).Is( "d", "e" );
        }

以下のようにnew string[]{}を返すかする必要があります。

        [TestMethod]
        public void TestMethod4()
        {
            var values = new[] { new string[] { "a", "b","c"},
                                 new string[] { "d" }, new string[] { "e" } };

            values.SelectMany(x => x.Contains("b") ? Enumerable.Empty<string>():x).Is( "d", "e");
        }

まぁどちらかといえば、可能なら事前に除外しておくのが正しいと思います。

FacebookTwitterHatenaTumblr共有

#PHPで#LINQっぽい事ができるWrapperを作ってみた

LAMPな会社に来て1年半がたち、1ヶ月前にPHPデビューをしました。
で、やっぱりいろいろツライところがあったのと、風呂あがりに思い立ったのと、いろいろと触発されたところあって、PHPでLINQっぽく配列操作ができるWrapperを作ってみました。
実際に、最近恩恵に預かっているので作ってよかったなと思っています。

基本的に、where,select,reduce,groupBy,orderBy,join,zip,sum,averageが出来るようになっています。
詳細は、githubの以下のサイトを見てもらえばいいと思いますが、比較的速度を気にするようにしているので、中で処理するときのループ回数が少なくなるように設計してあります。

toVar()および、reduce()で配列が戻ってきます。

groupBy,orderBy,join,zip,sum,averageなどは、実際には一回実行して、新しくArrayWrapperでWrapしていますが、それ以外はできるだけ1回のループで処理が行われるようになっています。

ArrayWrapper for PHP

例えば、whereしてselectしてその結果を更に(意味ないけど)whereしたものを返すような書き方は以下のように出来ます。
(テストコードまんま貼り付けですが、$expectedが実行した結果だと思ってください。

    $target = new ArrayWrapper(
            array(
                array("key" => 1,"value" => 10),
                array("key" => 2,"value" => 11),
                array("key" => 3,"value" => 12),
                array("key" => 4,"value" => 13),
                array("key" => 5,"value" => 14)
            )
        );
 
    $actual = $target
            ->where(function($x){return $x["key"] > 2;})
            ->select(function($x){return array("K" => $x["key"],"V" => $x["value"] * 2);})
            ->where(function($x){return $x["K"] > 3;})
            ->toVar();
 
    $expected = array(
            array("K" => 4,"V" => 26),
            array("K" => 5,"V" => 28)
            );
        

例えば、groupByなんかは以下のように書けるようになっています。

$target = new ArrayWrapper(
        array(
            array("id" => 2,"value" => 10),
            array("id" => 2,"value" => 11),
            array("id" => 3,"value" => 12),
            array("id" => 3,"value" => 13),
            array("id" => 5,"value" => 14)  
        )
    );
 
//toVarで配列に戻す。
$actual = $target
        ->groupBy(array("id"))
        ->toVar();
 
//で結果、こうなる
$expected = array(
        array("keys" => array("id" => 2),"values" => array(
            array("id" => 2,"value" => 10),
            array("id" => 2,"value" => 11)
            )),
        array("keys" => array("id" => 3),"values" => array(
            array("id" => 3,"value" => 12),
            array("id" => 3,"value" => 13),
            )),
        array("keys" => array("id" => 5),"values" => array(
            array("id" => 5,"value" => 14)  
            ))
        );

joinもこんなふうに書きます。

    $leftArray = array(
                array("key" => 2,"name" => "Nasal Hair Cutter"),
                array("key" => 3,"name" => "scissors"),
                array("key" => 5,"name" => "knife")
            );
 
    $rightArray = array(
            array("id" => 1,"item_id" => 2,"value" => 10),
            array("id" => 2,"item_id" => 2,"value" => 20),
            array("id" => 3,"item_id" => 2,"value" => 30),
            array("id" => 4,"item_id" => 3,"value" => 40),
            array("id" => 5,"item_id" => 3,"value" => 50),
            array("id" => 6,"item_id" => 5,"value" => 60),
            array("id" => 7,"item_id" => 5,"value" => 70),
        );
 
    $target = new ArrayWrapper($leftArray);
 
    $actual = $target
            ->join($rightArray,
                    array("key"),
                    array("item_id"),
                    function ($leftValue,$rightValue)
                    {
 
                        return
                            array("item_id" => $rightValue["item_id"],
                                 "name" => $leftValue["name"],
                                 "value" => $rightValue["value"]);
                    })
            ->toVar();
 
    $expected = array(
                array("item_id" => 2,"name" => "Nasal Hair Cutter","value" => 10),
                array("item_id" => 2,"name" => "Nasal Hair Cutter","value" => 20),
                array("item_id" => 2,"name" => "Nasal Hair Cutter","value" => 30),
                array("item_id" => 3,"name" => "scissors","value" => 40),
                array("item_id" => 3,"name" => "scissors","value" => 50),
                array("item_id" => 5,"name" => "knife","value" => 60),
                array("item_id" => 5,"name" => "knife","value" => 70),
            );       

実際に、業務で使っていますが、groupBy、sum,orderByあたりはかなり重宝しています。

Singleメソッドを使うことで単一の要素を返せる事を知った

LINQを使って書くときに結果が必ず1件になることが分かっているのに今まで

var test = Enumerable.Range(1,10).Where(x => x == 5).Select(x => x);
Console.Write(test[0]);

とSelectメソッドを利用した上で、配列としてアクセスしていたが、MSDNを見て(というかインテリセンスでそれっぽいのを見つけてMSDNにアクセスして確認して)いたらSingleメソッドというものを見つけた。

どうも要素が一つに限定される場合はSelectの代わりにSingleメソッドを利用する事で「要素が一つの配列」としてではなく、単一の要素とする結果になるらしい。

という事で、

var test = Enumerable.Range(1,10).Where(x => x == 5).Single();
Console.Write(test);

と書いてみたら配列ではなく単一のint型の変数としてアクセスすることができた。

ちゃんと、いろいろと試してみないといけないなぁと思った。

シーケンスの 1 つの特定の要素を返します。

Queryable.Single メソッド (System.Linq)

追記:

コメント欄で教えて貰ったので、そちらを利用した場合のサンプルも作成してみました。

Singleメソッドの中に条件を記述できる。

var test = Enumerable.Range(1,10).Single(x => x == 5);
Console.Write(test);

明らかに、簡潔に記述できて楽ですね。

Whereであることを明示するか、Singleの中に記述するか迷うところではあります。

SignleOrDefaultメソッドを利用する。

シーケンスの 1 つの特定の要素を返します。そのような要素が見つからない場合は既定値を返します。

Queryable.SingleOrDefault メソッド (System.Linq)

var test = Enumerable.Range(1, 10).SingleOrDefault(x => x == 15);
if (test == 0)
{
MessageBox.Show("存在しない。");
}
else
{
MessageBox.Show(test.ToString());
}

こちらは、要素が存在しない時は、要素にあたる型の規定値を返します。

intの場合は0なので、上記のサンプルの場合0だったら要素が存在しなかったことになります。

0も範囲内に含まれているような状況だと判断に困りますが、Null許容型や参照型は規定値がNullなので、利用しやすいかもしれません。