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");
        }

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

Share