linq.js ver 1.2.0.0 - バグフィックスとEqualityComparer

バグフィックス

JavaScriptのオブジェクトはハッシュ。というから、ああ、はいはい、つまりDictionaryなんですね、と素で何の疑問も持たず、思っていました。キーには別に数字でもオブジェクトでもなんでも、そう、オブジェクトでも入れられると思っていて、平然とそれを大前提にしたコードをlinq.jsにも混ぜ込んでいました。集合演算系のものに。ありえないです。C#脳すぎる。

var hashTest = {};
var obj = { hoge: "fuga" }
var obj2 = { tako: "ika" }
hashTest[obj] = "tettette";
hashTest[obj2] = "totetotetote";
hashTest["[object Object]"] = "12345";
for (var key in hashTest)
    alert(hashTest[key]);

これは当然のようにアラートで出てくる値は"12345"だけです。JavaScriptのオブジェクトはキーが文字列限定のDictionary(C#脳ですみません)で、キーに文字列以外のものを突っ込んだ場合、自動的に文字列に変換される。だからオブジェクトは文字列に変換され、オブジェクトが文字列化される際は全て"[object Object]"になるから上書きされる、というお話。そんなわけで、今までのlinq.jsでは……

var seq = E.Range(1, 10).Select("{test:$}"); // {test:1},{test:2}...
var count = seq.Distinct().Count(); // 重複を除いたものの合計、当然10なのに?
alert(count); // 結果は1です、ごめんなさい

となっていました。これは酷い。ちなみに、何故かFirefoxでは10になります。IEやChromeは1なんですけどね。linq.jsの内部の動きから考えるに、Firefoxのほうが変な挙動なのは確かなのですが良くわかりません。ただ、ブラウザ間で互換の取れてない動きが出てしまう、というのもまた良くない話です。そんなわけで直しました。めでたしめでたし、のようでいて複雑な気分。というのも内部コードの実行効率が酷いことになってしまったからです。下のコードはkeyが含まれているかどうかを調べる関数の一部なのですが

if (Linq.HashSet.IsPrimitive(key))
{
    return this.PrimitiveContainer.hasOwnProperty(key);
}
else
{
    for (var i = 0; i < this.ObjectContainer.length; i++)
    {
        if (key === this.ObjectContainer[i]) return true;
    }
    return false;
}

最初にPrimitive、つまり数字や文字列であるかを判断し、プリミティブであればhasOwnPropertyでキーがあるかないかを判断。こっちは今まで通り。そしてそのままではキーとして使えないオブジェクトに対しては、線形探索で全部舐めて探してるんですね―。うはー、大量の配列に使った場合の遅さが怖すぎる。ただ、この辺のやり方はJavaScript Hashtableなんかも同じような内容なので、しょうがないみたいです、素のJavaScriptでは。ハッシュコードを算出するために重たい計算を挟めば、それはそれで本末転倒ですし。

EqualityComparer

JavaScriptのObjectは参照型、なので全ての値が同一であろうとも、別のオブジェクトであるのなら別だと判断される。しかし、さすがにそれでは実用性に乏しい。

// Before
var a1 = E.Repeat("dummy", 10).Select("{test:$}").Distinct().Count();  // 10
// After
var a2 = E.Repeat("dummy", 10).Select("{test:$}").Distinct("$.test").Count(); // 1

{test:"dummy"}を10個生成し、それを重複除去するとしたら、望む値は当然、1。そういった動作が出来るようにDistinctなどの集合演算系にキー指定が出来るようになりました。そういえばC#の匿名型はクラスなのに重複除去されるのは何でだろう、と思ったらちゃんと「すべてのプロパティが等しい場合のみ、等しい」ように作られているようです。さすが、ユーザーがどういう動作を望んでいるのか分かってる。

Contains, Distinct, Except, Intersect, SequenceEqual, UnionにcompareSelectorを追加、という形になります。比較関数にするかキーセレクターにするかで悩んだのですが、比較関数だと使用時の定義が面倒くさいので、実用性を考えて簡易的なもののほうが良いかな、と思いキーセレクターにしました。9割以上は同じものの比較ですよねー、という決めつけです。複数の値で比較したい時は適当に連結する、しかないです。塩でも振りながら文字列にして結合してください。お手軽でそこそこ確実な手を言えば、ToJSONメソッドを使ってJSONに変換するのも手です。

var seq = E.Range(1, 10).Select("{key:$<5,evenodd:$%2==0}");
seq.Distinct("E.Repeat($,1).ToJSON()").Count(); // 4

true:false,true:true,false:true,false:falseの4通りのどれかが10個なので、重複除去すると合計数は4つ。ところでそうそう、集合演算で線形探索とかフザけるなタコ、と思った場合はこのようにJSON化することで(値参照になりますが)回避できます。何かバッドノウハウみたいで素敵。ただ、そもそもオブジェクトにたいしてキー指定無しの集合演算自体が使う機会なんてほとんどないとは思いますが。

Profile

Yoshifumi Kawai

Cysharp, Inc
CEO/CTO

Microsoft MVP for Developer Technologies(C#)
April 2011
|
July 2023

Twitter:@neuecc GitHub:neuecc

Archive