ホーム 院内・スタッフ紹介 診療時間・アクセス フォトギャラリー
0574-27-6355

院長のメモ帖

2015年6月 2日 火曜日

Enumerable.Cast<TResult>メソッドの罠

前回、最近データー解析にはまっている話を書きましたが、その続きです。
自前で相関係数を計算するライブラリを作りました。

/blog/SourceCode/CorrelationCoefficient.cs.txt
/// <summary>
/// 二つの集合の相関係数を求める。二つのシーケンスの先頭から順番にペアとして要素を取り出し、要素数の多いシーケンスの残りの要素は無視される。
/// 計算式にはピアソンの積率相関係数を用いている。
/// 例外:     ArgumentNullException 引数がnullシーケンス。
///           DivideByZeroException 引数が空のシーケンス。
/// </summary>
/// <param name="List1">集合1</param>
/// <param name="List2">集合2</param>
/// <returns>相関係数</returns>
public static double CorrelationCoefficient(this IEnumerable<double> List1, IEnumerable<double> List2)
{
 if (List1 == null)
  throw new ArgumentNullException("引数List1がnullです。");
 if (List2 == null)
  throw new ArgumentNullException("引数List2がnullです。");
  var calcMember = List1.Zip(List2, (x, y) => new { x, y }).ToArray();
 //入力された二つのシーケンスを合流して、(x, y)の一対のデーターになった配列に変換する。
 //多い方の余った要素は無視される。
  if (calcMember.Length == 0)
 {
  //要素数がゼロなら例外発生。
  StringBuilder sb = new StringBuilder("計算する要素数がゼロです。");
  if (List1.Count() == 0)
   sb.Append("引数List1が空のシーケンスです。");
  if (List1.Count() == 0)
   sb.Append("引数List2が空のシーケンスです。");
  throw new DivideByZeroException(sb.ToString());
 }
 ///ピアソンの積率相関係数を計算
 /// SIGMA((xi-ax)(yi-ay)) / SQRT(SIGMA((xi-ax)^2) / SQRT(SIGMA((yi-ya)^2))

 var ax = calcMember.Select(r => r.x).Average();
 var ay = calcMember.Select(r => r.y).Average();
 var result = calcMember
 .Select(r => new { xi = r.x - ax, yi = r.y - ay })
 .Select(r => new { a = r.xi * r.yi, b = r.xi * r.xi, c = r.yi * r.yi })
 .Aggregate((t, i) => new { a = t.a + i.a, b = t.b + i.b, c = t.c + i.c });
 return result.a / Math.Sqrt(result.b) / Math.Sqrt(result.c);
 }

まあ、このメソッドは別に難しくもなんともなく普通に動くのですが、実際のデーターはint型なことが圧倒的に多いので、int型を受け取るオーバーロードを加えてみました。

public static double CorrelationCoefficient(this IEnumerable<int> List1, IEnumerable<int> List2)
  {
   return CorrelationCoefficient(List1.Cast<double>(), List2.Cast<double>());
  }

intからdoubleにキャストして型合わせをしただけのオーバーロードですが、実行時におもいっきりInvalidCastExceptionなぞ出てしまいました。Cast<T>メソッドは今までほぼ使ったことなかったのですが、前回の記事を書くときにLINQのおさらいをして存在を思い出したので、今回使ってみたところ思いっきりエラー出ました。

MSDNを調べた限り、OfType<T>メソッドとの違いはキャストできないときにnullを返すのか例外を出すのかだけの違いのようで、OfType<T>はいつも常用していて特に不都合を感じたことはありません。しかし、このソースでOfType<T>にしたところ、要素数がゼロになってDivideByZeroExceptionが出るようになっただけでした。

検証した限りではintからintの変換はobject型に格納していようがint型変数に格納していようが問題ないですが、違う値型同士では一切変換できないようです。クラス型の変換では全く問題が起こりませんでした。いやこれができないようではOfType使った今までのプログラム大半が動かないはずですから(笑)

違う値型への変換が全くできないと分かったので同じこと見つけた人いないかと思って検索した所、ありました。ここの解説によると、いったん入力値をobjectに変換してから目的の型に変換する仕様だから値型で機能しないそうです。このページにならってILSpyでCast<T>のコードを覗いてみますと


public static IEnumerable<TResult> Cast<TResult>(this IEnumerable source)
{
IEnumerable<TResult> enumerable = source as IEnumerable<TResult>;
if (enumerable != null)
{
  return enumerable;
}
if (source == null)
{
  throw Error.ArgumentNull("source");
}
return Enumerable.CastIterator<TResult>(source);
}

最初の太字のところが肝心の変換ですが、なんとas演算子でキャストしてるじゃないですか。as演算子のヘルプには思いっきり、「as  の演算子は参照の変換、null 許容変換とボックス化変換だけ実行します。 as  の演算子は他の変換を使用して、キャスト式を使用して実行する必要があるユーザー定義の変換など実行できません。」とかかれているわけで、当然ここはnullが帰ります。そこで最終行のCastIterator<T>メソッドへ飛ぶわけですが、

private static IEnumerable<TResult> CastIterator<TResult>(IEnumerable source)
{
foreach (object current in source)
{
  yield return (TResult)((object)current);
}
yield break;
}

というわけでintをobjectにボックス化してそれをdoubleにアンボクシング化を試みてますが、それは無理というもので、あえて書くとしたら太字のところは

(double)((int)((object)current))

ですよね...
例のコードは
public static double CorrelationCoefficient(this IEnumerable<int> List1, IEnumerable<int> List2)
  {
   return CorrelationCoefficient(List1.Select(r=>(double)r), List2.Select(r=>(double)r));
  }

とすることで決着がつきました。MSDNのページに違う値型への変換は例外発生するって一言書いてほしいな。キャストって名前も誤解招きやすいから、内部ではas演算子つかってますよってこともね。

投稿者 美濃加茂市のIT獣医師 近藤 博 | 記事URL | コメント(1)