【ちゃんと投げよう】あなたの作ったオレオレLINQメソッドは間違ってるかもしれない！【ArgumentNullException】

# はじめに

　C#最高ですよね！LINQ最高ですよね！！！

　さてそんなC#・LINQに対してですが、「LINQにこういうの欲しいな〜。無いからLINQみたいにIEnumerable\<TSource\>の拡張メソッドとして、IEnumerable\<TResult\>を返すメソッドを作ろう！」と「オレオレLINQメソッド」を作ったことがある人も結構いると思います。

　そんな「オレオレLINQメソッド」ですが、気をつけないと思わぬ落とし穴にはまるかもしれません。

　あなたの作った「オレオレLINQメソッド」、ちゃんと正しく`ArgumentNullException`投げますか？

# LINQの多くのメソッドは遅延実行

　LINQの多くのメソッドは遅延実行されます。

```csharp:
using System;
using System.Linq;
using System.Collections.Generic;

namespace YieldExample
{
	class MainClass
	{
		static int ShowAndReturn (int num)
		{
			Console.WriteLine ($"  in ShowAndReturn:{num}");
			return num;
		}

		public static void Main (string[] args)
		{
			int[] intArray = new int[]{ 3, 1, 4, 1, 5, 9, 2 };
			IEnumerable<int> intEnumerable = intArray.Select (ShowAndReturn);
			Console.WriteLine ("after select");
			foreach (int num in intEnumerable) {
				Console.WriteLine ($"    in foreach loop:{num}");
			}
		}
	}
}
```

　上記のコードの実行結果は次の通りです。

```csharp:
after select
  in ShowAndReturn:3
    in foreach loop:3
  in ShowAndReturn:1
    in foreach loop:1
  in ShowAndReturn:4
    in foreach loop:4
  in ShowAndReturn:1
    in foreach loop:1
  in ShowAndReturn:5
    in foreach loop:5
  in ShowAndReturn:9
    in foreach loop:9
  in ShowAndReturn:2
    in foreach loop:2
```

　上記の実行結果からわかるように、`Select`メソッドを呼び出しても、すぐに`intArray`の各要素に`ShowAndReturn`から作ったデリゲートが適用されるわけではありません。Selectメソッドの結果をforeachメソッドで列挙した時点など、必要になった時点で適用されます。

　LINQの多くのメソッドは遅延実行されますね。「オレオレLINQメソッド」を作られる方は、この点は把握されていると思います。(コード・プロジェクトの置き場は[こちら](https://github.com/RyotaMurohoshi/memos/blob/master/code_snippets/YieldExample/YieldExample/Program.cs))

# LINQの遅延実行されるメソッドの多くも、ArgumentNullExceptionはすぐ投げる

　さてプログラミングを書く上で良い場合だけ考えてはいけません。例外的な状況も考慮しないといけません。Selectメソッドの引数にnullを渡した場合を考えましょう。

　次のNUnitを用いたテストコードをみてください。次のテストは全てパスします。

```csharp:
using NUnit.Framework;
using System;
using System.Linq;
using System.Collections.Generic;

namespace EnumerableArgumentNullExceptionExample
{
	[TestFixture ()]
	public class TestLinq
	{
		[Test ()]
		public void TestSelect ()
		{
			IEnumerable<int> intEnumerable = new int[]{ 3, 1, 4, 1, 5, 9, 2 };
			Func<int, int> nullSelector = null;

			Assert.Catch<ArgumentNullException> (() => {
				intEnumerable.Select (nullSelector);
			});

			Assert.Catch<ArgumentNullException> (() => {
				foreach (int num in intEnumerable.Select (nullSelector)) {
					;
				}
			});
		}
	}
}
```

　特に次の箇所を見てください。

```csharp:
Assert.Catch<ArgumentNullException> (() => {
	intEnumerable.Select (nullSelector);
});
```

　`Select`メソッドは遅延実行されるのでしたね。上記のコードでは、foreach文などでSelectの結果は列挙されておらず評価されていませんね。しかし、`ArgumentNullException`が発生しています。もし引数がnullだった場合、その結果が列挙(評価)されずともメソッド呼び出しをした時点で、`ArgumentNullException`を投げることがこのコードからわかりますね。

# 間違ったオレオレLINQの例

　さて、「オレオレLINQ」メソッドを作ってみましょう。今回は`Select`と全く同じ機能を持つメソッドを、`Map`というメソッド名で作ってみましょう。

```csharp:
public static class MyEnumerable
{
		public static IEnumerable<TResult> Map<TSource, TResult> (this IEnumerable<TSource> source, Func<TSource,TResult> selector)
		{
			if (source == null)
				throw new ArgumentNullException ("source");
			if (selector == null)
				throw new ArgumentNullException ("predicate");

			foreach (TSource element in source) {
				yield return selector (element);
			}
		}
	}
}
```

　この投稿のタイトルは『【ちゃんと投げよう】あなたの作ったオレオレLINQメソッドは間違ってるかもしれない！【ArgumentNullException】』です。どうでしょうか？ちゃんと引数がnullだった際の処理も書かれていて、一見ちゃんとArgumentNullExceptionを投げそうですよね。

　Selectメソッドは遅延実行されるメソッドでしたが、引数がnullだった場合は評価される時点でなくて、メソッド呼び出した時点で`ArgumentNullException`を投げたのを思い出してください。`Map`メソッドも正しく`ArgumentNullException`を投げるかテストをみてみましょう。

```csharp:
namespace EnumerableArgumentNullExceptionExample.Bad
{
	[TestFixture ()]
	public class TestMyLinq
	{
		[Test ()]
		public void TestMap ()
		{
			IEnumerable<int> intEnumerable = new int[]{ 3, 1, 4, 1, 5, 9, 2 };
			Func<int, int> nullSelector = null;

			// //Next test cannot pass. Must add null handling exactlly.
			// Assert.Catch<ArgumentNullException> (() => {
			//	intEnumerable.Map (nullSelector);
			// });

			Assert.Catch<ArgumentNullException> (() => {
				foreach (int num in intEnumerable.Map (nullSelector)) {
					;
				}
			});
		}
	}
}
```

　上記のコードのコメントアウトした部分に注目してください。残念ながら、この箇所のコードはパスしません。


```csharp:
intEnumerable.Map (nullSelector);
```

　`Select`メソッドではメソッドが呼ばれた時点で`ArgumentNullException`が投げられました。しかし`Map`メソッドはnullが引数として渡された時点では`ArgumentNullException`を投げてくれません。

　どうすれば良いでしょうか？

# すぐArgumentNullExceptionを投げるように改善する

　C#の仕様的に先ほどの`Map`のコードでは、評価したタイミングで`ArgumentNullException`を投げてくれません。それでは、Selectメソッドのようにメソッドを呼び出した時点で`ArgumentNullException`を投げるように修正しましょう。

　修正版`Map`メソッドは次の通りです。

```
public static IEnumerable<TResult> Map<TSource, TResult> (this IEnumerable<TSource> source, Func<TSource,TResult> selector)
{
	if (source == null)
		throw new ArgumentNullException ("source");
	if (selector == null)
		throw new ArgumentNullException ("selector");

	return source.Map_ (selector);
}

public static IEnumerable<TResult> Map_<TSource, TResult> (this IEnumerable<TSource> source, Func<TSource,TResult> selector)
{
	foreach (TSource element in source) {
		yield return selector (element);
	}
}
```

　`null`チエックをする部分と別に、`yield`で列挙する箇所を別メソッドとして切り出しました。このように修正した`Map`メソッドは次のテストをパスすることができます。


```csharp:
namespace EnumerableArgumentNullExceptionExample
{
	[TestFixture ()]
	public class TestMyLinq
	{
		[Test ()]
		public void TestMap ()
		{
			IEnumerable<int> intEnumerable = new int[]{ 3, 1, 4, 1, 5, 9, 2 };
			Func<int, int> nullSelector = null;

			Assert.Catch<ArgumentNullException> (() => {
				intEnumerable.Map (nullSelector);
			});

			Assert.Catch<ArgumentNullException> (() => {
				foreach (int num in intEnumerable.Map (nullSelector)) {
					;
				}
			});
		}
	}
}
```

# さいごに

　「LINQにこういうの欲しいな〜。無いからLINQみたいにIEnumerable\<TSource\>の拡張メソッドとして、IEnumerable\<TResult\>を返すメソッドを作ろう！」と、「オレオレLINQメソッド」を作りたくなることがあると思います。

　しかし気をつけて実装をしないと、ちゃんと正しく`ArgumentNullException`投げてくれないかもしれません。`Select`や`Where`のように、メソッドを呼び出した時点で`ArgumentNullException`を投げないと、メソッドが遅延実行されるため、思いがけないタイミングで`ArgumentNullException`が発生して、デバックが大変になる可能性があります。

　`Select`や`Where`などのように、正しいタイミングで`ArgumentNullException`投げてくれないを投げるよう、気をつけて実装しましょう。

　本投稿のプロジェクトは[こちら](https://github.com/RyotaMurohoshi/memos/tree/master/code_snippets/EnumerableNullArgumentExceptionExample)

　また、余談ですが.NETのSelectはパフォーマンスをよくするために、本投稿のようなシンプルな実装にはなっていません。