tag:crieit.net,2005:https://crieit.net/tags/LINQ/feed
「LINQ」の記事 - Crieit
Crieitでタグ「LINQ」に投稿された最近の記事
2022-06-06T18:50:19+09:00
https://crieit.net/tags/LINQ/feed
tag:crieit.net,2005:PublicArticle/18209
2022-06-06T18:50:19+09:00
2022-06-06T18:50:19+09:00
https://crieit.net/posts/wrap-linq-methods
【C#】LINQ のメソッド名が SQL っぽいからラップする
<h2 id="SQL チックなメソッド名"><a href="#SQL+%E3%83%81%E3%83%83%E3%82%AF%E3%81%AA%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E5%90%8D">SQL チックなメソッド名</a></h2>
<p>C# の <em>LINQ to Object</em>(以下、単に LINQ と記載) はとても便利なんですけど、メソッド名が SQL っぽくて、いまいち直感的ではない上にダサいです。例として、JavaScript と比較してみましょう。<br />
<div class="table-responsive"><table>
<thead>
<tr>
<th>LINQ でのメソッド名</th>
<th>JavaScript のメソッド名</th>
</tr>
</thead>
<tbody>
<tr>
<td>Select</td>
<td>map</td>
</tr>
<tr>
<td>Where</td>
<td>filter</td>
</tr>
<tr>
<td>OrderBy</td>
<td>sort</td>
</tr>
</tbody>
</table></div></p>
<p>以下は、実際に利用する例です。<br />
JavaScript ではこう。</p>
<pre><code class="javascript">const hoge = [10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0];
const fuga = hoge.filter(x => x % 2 === 0).map(x => x * 2).sort();
</code></pre>
<p>C# ではこう。</p>
<pre><code class="csharp">var hoge = new[] { 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 };
var fuga = hoge.Where(x => x % 2 == 0).Select(x => x * 2).OrderBy(x => x).ToArray();
</code></pre>
<p>もちろん慣れてくれば <code>Select</code> だろうが <code>Where</code> だろうがスラスラ読めるのですが、慣れていなければ引っかかるかもしれません。一方、<code>map</code>, <code>filter</code>, <code>sort</code> というのは直感的で意味がわかりやすいです。</p>
<h2 id="ラップする"><a href="#%E3%83%A9%E3%83%83%E3%83%97%E3%81%99%E3%82%8B">ラップする</a></h2>
<p>ということで、ラッパーメソッドで名前を変えます。</p>
<h3><code>Select</code> => <code>Map</code></h3>
<pre><code class="csharp">public static IEnumerable<TResult> Map<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> func)
=> source.Select(x => func(x));
public static IEnumerable<TResult> Map<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, int, TResult> func)
=> source.Select((x, index) => func(x, index));
</code></pre>
<h3><code>Where</code> => <code>Filter</code></h3>
<pre><code class="csharp">public static IEnumerable<T> Filter<T>(this IEnumerable<T> source, Func<bool> func)
=> source.Where(x => func());
public static IEnumerable<T> Filter<T>(this IEnumerable<T> source, Func<T, bool> func)
=> source.Where(x => func(x));
public static IEnumerable<T> Filter<T>(this IEnumerable<T> source, bool func)
=> source.Where(x => func);
</code></pre>
<h3><code>OrderBy</code> => <code>Sort</code></h3>
<pre><code class="csharp">public static IOrderedEnumerable<TSource> Sort<TSource, TKey>(this IEnumerable<TSource> sources, Func<TSource, TKey> keySelector, IComparer<TKey> comparer)
=> sources.OrderBy(keySelector, comparer);
public static IOrderedEnumerable<TSource> Sort<TSource, TKey>(this IEnumerable<TSource> sources, Func<TSource, TKey> keySelector)
=> sources.OrderBy(keySelector);
public static IOrderedEnumerable<TSource> SortByDesc<TSource, TKey>(this IEnumerable<TSource> sources, Func<TSource, TKey> keySelector, IComparer<TKey> comparer)
=> sources.OrderByDescending(keySelector, comparer);
public static IOrderedEnumerable<TSource> SortByDesc<TSource, TKey>(this IEnumerable<TSource> sources, Func<TSource, TKey> keySelector)
=> sources.OrderByDescending(keySelector);
</code></pre>
<p>ソートに関しては、元の <code>OrderBy</code> が、昇順か降順かでメソッドが別れています。昇順のソートが <code>OrderBy</code>、降順のソートが <code>OrderByDescending</code> なので、それぞれ <code>Sort</code>、 <code>SortByDesc</code> というメソッド名でラップしましたが、ここは昇順か降順かをオプション引数で切り替える <code>Sort</code> という名前のメソッドにしても良いでしょう。</p>
<p>「昇順か降順かをメソッド名だけで判断できる」という利点はありますが、同じようなことが別の名前のメソッドで定義されているのも、個人的には気持ち悪いと感じます。</p>
<p>同様に、<code>ThenBy</code> と <code>ThenByDescending</code> も <code>Then</code> というメソッドでラップしてみます。</p>
<pre><code class="csharp">public static IOrderedEnumerable<TSource> Sort<TSource, TKey>(this IEnumerable<TSource> sources, Func<TSource, TKey> keySelector, IComparer<TKey> comparer, bool byDesc)
=> byDesc ? sources.OrderByDescending(keySelector, comparer)
: sources.OrderBy(keySelector, comparer);
public static IOrderedEnumerable<TSource> Sort<TSource, TKey>(this IEnumerable<TSource> sources, Func<TSource, TKey> keySelector, bool byDesc)
=> byDesc ? sources.OrderByDescending(keySelector)
: sources.OrderBy(keySelector);
public static IOrderedEnumerable<TSource> Then<TSource, TKey>(this IOrderedEnumerable<TSource> sources, Func<TSource, TKey> keySelector, IComparer<TKey> comparer, bool byDesc)
=> byDesc ? sources.ThenByDescending(keySelector, comparer)
: sources.ThenBy(keySelector, comparer);
public static IOrderedEnumerable<TSource> Then<TSource, TKey>(this IOrderedEnumerable<TSource> sources, Func<TSource, TKey> keySelector, bool byDesc)
=> byDesc ? sources.ThenByDescending(keySelector)
: sources.ThenBy(keySelector);
</code></pre>
<p>オプション引数 <code>byDesc</code> を設定しました。これにより、次のようにコードを書けます。</p>
<pre><code class="csharp">// ラップ前
var hoge = GetEmployee.OrderBy(s => s.Age).ThenByDesc(x => x.Salary);
// ラップ後
var hoge = GetEmployee.Sort(s => s.Age).Then(s => s.Salary, byDesc: true);
</code></pre>
<p>もちろん引数名の <code>byDesc</code> を省略することもできますが、理由があり記載しています。理由については、以下の記事で解説しているので、よかったらご覧ください。</p>
<p><a href="https://crieit.net/posts/dare-to-write-argument-names-patterns">【C#】あえて引数名を書くパターン</a></p>
<h2 id="比較"><a href="#%E6%AF%94%E8%BC%83">比較</a></h2>
<p>メソッド名をラップしてみることで、冒頭のコードは次のように書けるようになりました。</p>
<pre><code class="csharp">// 変更前
var hoge = new[] { 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 };
var fuga = hoge.Where(x => x % 2 == 0).Select(x => x * 2).OrderBy(x => x).ToArray();
// 変更後
var hoge = new[] { 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0 };
var fuga = hoge.Filter(x => x % 2 == 0).Map(x => x * 2).Sort(x => x).ToArray();
</code></pre>
<p>LINQ にあまり慣れていない人でも、よりスッと入ってきやすいコードになったのではないでしょうか。</p>
あぱしょに