tag:crieit.net,2005:https://crieit.net/tags/%E6%8B%A1%E5%BC%B5%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89/feed
「拡張メソッド」の記事 - Crieit
Crieitでタグ「拡張メソッド」に投稿された最近の記事
2022-06-06T18:50:19+09:00
https://crieit.net/tags/%E6%8B%A1%E5%BC%B5%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89/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>
あぱしょに
tag:crieit.net,2005:PublicArticle/17767
2021-11-17T20:22:26+09:00
2021-11-17T20:22:26+09:00
https://crieit.net/posts/if-expression-in-csharp
【C#】え!!C#でif式を?できらぁ!
<p>タイトルは正確ではありません。すいません。if式っぽく書ける条件判定関数的なものです。if式っぽく書きたかったので作ってみました。</p>
<p>C#の<code>if</code>は文であって式ではありません。式ではないので</p>
<pre><code class="csharp">var name = "ほげほげ君";
// こうは書けない
var exName1 = if (name.EndsWith("君")) {
return name;
}
else {
return name + "君";
}
// こう書く
var exName2 = "";
if (name.EndsWith("君"))
exName = name;
else
exName2 = name + "君";
</code></pre>
<p>みたいに書かなければいけません。人によるかと思いますが、これ読みにくくないですか?<code>exName2</code>への代入が2箇所あるので読んでて鬱陶しいです。何度もこういうコードが出てくると嫌です。</p>
<p>このくらいのコードであれば三項演算子で解決するという方法もあるでしょう。</p>
<pre><code class="csharp">var name = "ほげほげ君";
var newName = name.EndsWith("君") ? name : name + "君";
</code></pre>
<p>これであれば代入は1箇所ですし分かりやすいですね。しかし、次のようなケースではどうでしょう。</p>
<pre><code class="csharp">// 三項演算子
var newName = name.EndsWith("さん") ? Regex.Replace(name, "さん$", "君") : (name.EndsWith("君") ? name : name + "君");
// if文
var newName = "";
if (name.EndsWith("さん"))
newName = Regex.Replace(name, "さん$", "君");
else if (name.EndsWith("君"))
newName = name;
else
newName = name + "君";
</code></pre>
<p>最後が"さん"であれば"君"に変換するという場合は、三項演算子を使えば分かりにくくなり、<code>if</code> <code>else if</code> <code>else</code>では悪戯に長くなりますし結局代入箇所が分散していますね。もっとこう、スパっと書けないものでしょうか。ということでif式っぽいものを作りました。</p>
<pre><code class="csharp">// ex.1
var newName1 = Ext.If(name.EndsWith("さん"), Regex.Replace(name, "さん$", "君"))
.ElseIf(!name.EndsWith("君"), name + "君")
.Else(name);
</code></pre>
<p>あるいは</p>
<pre><code class="csharp">// ex.2
var newName2 = name.EndsWith("さん").Then(Regex.Replace(name, "さん$", "君"))
.ElseIf(!name.EndsWith("君"), name + "君")
.Else(name);
</code></pre>
<p>上記のように書くためのメソッドです。条件に対して欲しい結果をメソッドチェーンで書いていけるのでなんとなく読みやすいと思います。</p>
<p>まずは<strong>ex.1</strong>にある<code>If</code> <code>ElseIf</code> <code>Else</code>というメソッドがこちら。</p>
<pre><code class="csharp">public static Tuple<bool, T> If<T>(bool term, T value)
{
if (term)
{
// 条件が成立する場合はvalue
return new Tuple<bool, T>(true, value);
}
else
{
// 条件が成立しない場合はT型のdefault
return new Tuple<bool, T>(false, default);
}
}
public static Tuple<bool, T> ElseIf<T>(this Tuple<bool, T> prev, bool term, T value)
{
if (prev.Item1)
{
// 前の式が成立している場合はそれをそのまま返す
return prev;
}
else if (term)
{
// 条件が成立する場合はvalue
return new Tuple<bool, T>(true, value);
}
else
{
// 条件が成立しない場合はT型のdefault
return new Tuple<bool, T>(false, default);
}
}
public static T Else<T> Else(this Tuple<bool, T> prev, T value)
{
if (prev.Item1)
{
// 前の式が成立する場合は、その値を取り出して返す
return prev.Item2;
}
else
{
// 前の式が成立しない場合は規定値としてvalueを返す
return value;
}
}
</code></pre>
<p>if式を実現するにあたり、前の式の計算結果と値を受け取って引き継ぎつつ、最終的には値のみを返すという処理をする必要があります。そのため、<code>Tuple<bool, T></code>でそれらの情報を受け渡し、最終的に<code>Else</code>で<code>T</code>型の値部分のみを返しています。</p>
<p><code>ElseIf</code>と<code>Else</code>では<code>Tuple<bool, T></code>の拡張メソッドを使用しています。なので、このメソッドは<code>static</code>なクラスで宣言する必要があります。</p>
<p>続いて<strong>ex.2</strong>にある<code>Then</code>について。<strong>ex.2</strong>の<code>ElseIf</code>と<code>Else</code>は<strong>ex.1</strong>のものと同様です。</p>
<pre><code class="csharp">public static Tuple<bool, T> Then<T>(this bool term, T value)
{
if (term)
{
// 条件が成立する場合はvalue
return new Tuple<bool, T>(true, value);
}
else
{
// 条件が成立しない場合はT型のdefault
return new Tuple<bool, T>(false, default);
}
}
</code></pre>
<p>これも単純に<strong>ex.1</strong>の<code>If</code>の引数<code>term</code>を、引数ではなくメソッドチェーン的に取れるように<code>this</code>キーワードで拡張したメソッドになります。<code>this</code>キーワードにしている引数は明示的に指定することも可能なので、なんなら<strong>ex.1</strong>の<code>If</code>メソッドをこの<code>Then</code>メソッドで代用することも可能です。(条件を指定するならIf的な名前がいいと思って別に定義しました)</p>
<p><code>ElseIf</code>は必要に応じて省略することも、複数記述することもできます。</p>
<pre><code class="csharp">var point = GetPoint();
var score = Ext.If(point == 100, "S")
.ElseIf(point >= 80, "A")
.ElseIf(point >= 60, "B")
.ElseIf(point >= 40, "C")
.ElseIf(point >= 30, "D")
.Else("E");
</code></pre>
<p>このように書けます。if文でちまちま書いたり三項演算子でネストして書いていくよりはるかにわかりやすいと思います。</p>
<p><code>Func</code>や<code>Action</code>を受け取るよう拡張すれば、ラムダ式などを使ってより柔軟な表現が出来るようになると思います。たとえばActionだと、</p>
<pre><code class="csharp">public static bool Then(this bool term, Action action)
{
if (!term)
return false;
action();
return true;
}
public static bool ElseIf(this bool previous, bool term, Action action)
{
if (previous)
return true;
else if (!term)
return false;
action();
return true;
}
public static void Else(this bool previous, Action action)
{
if (previous)
return;
action();
}
// ***
var point = GetPoint();
(point == 100).Then(() => Console.WriteLine("S"))
.ElseIf((point >= 80), () => Console.WriteLine("A"))
.ElseIf((point >= 60), () => Console.WriteLine("B"))
.ElseIf((point >= 40), () => Console.WriteLine("C"))
.ElseIf((point >= 30), () => Console.WriteLine("D"))
.Else(() => Console.WriteLine("E"));
</code></pre>
<p>こんな感じで書けます。</p>
<p>ジェネリクスを使用していますが制約などはつけていませんし、より最適なコードもあるかもしれません。ある程度拡張性はあると思いますが、もっといいやり方あるよ!などがあれば、是非教えてください。</p>
あぱしょに
tag:crieit.net,2005:PublicArticle/16538
2021-01-05T20:44:40+09:00
2021-01-05T20:44:40+09:00
https://crieit.net/posts/csharp-extension-methods
【C#】拡張メソッド
<h1 id="序論"><a href="#%E5%BA%8F%E8%AB%96">序論</a></h1>
<p>C#には拡張メソッドという機能があって、まぁ知ってれば大変便利なのですが、弊社謹製レガシーライブラリはなんともまぁ前時代的な書き方をしてあり大変煩わしいしです。そのうえ誰もメンテしないから使いにくくて仕方がない、私がメンテしようにもテストコードすら書かれていないのでめんどくせぇ。というヘイト全開で拡張メソッドの素晴らしさを書いていこうと思います。弊社に届け、この思い☆</p>
<h1 id="拡張メソッド"><a href="#%E6%8B%A1%E5%BC%B5%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89">拡張メソッド</a></h1>
<p>C#における拡張メソッドは、<strong>あたかもそのクラスに元からあるかのように使用できるインスタンスメソッドを実装する</strong>という方法です。</p>
<p>たとえば、stringには<code>string.IsNullOrEmpty</code>というメソッドがあります。引数で渡された文字列が空白またはnullの場合にtrueを返してくれるというメソッドです。</p>
<pre><code class="csharp">string text = "";
Console.Write(string.IsNullOrEmpty(text));
// 結果 -> true
</code></pre>
<p>この書き方、なんか嫌じゃないですか?つまり、<code>text</code>というインスタンスが<code>IsNullOrEmpty</code>かどうかをいちいちstringのメソッド越しに聞くの嫌じゃないですか?子どもと話してて、お父さんお母さんに「何歳ですか?」って聞いてるみたいで嫌じゃないですか?子どもに聞いて子どもに答えてほしい。なのでそうしましょう。</p>
<pre><code class="csharp">public static class StringExtension
{
public static bool IsNullOrEmpty(this string text)
{
return string.IsNullOrEmpty(text);
}
}
</code></pre>
<p>上記のメソッドを用意することで、以下のように書けます。</p>
<pre><code class="csharp">string text = "";
Console.Write(text.IsNullOrEmpty());
// 結果 -> true
</code></pre>
<p>比べてみましょう。</p>
<pre><code class="csharp">string text = "";
Console.Write(string.IsNullOrEmpty(text)); // 拡張メソッドじゃないver
Console.Write(text.IsNullOrEmpty()); // 拡張メソッドver
</code></pre>
<p>あらまぁ見てくださいよこのコード量の違いを。素晴らしいですね。</p>
<p>あたかも、<code>text</code>というインスタンスの<code>IsNullOrEmpty</code>メソッドを呼び出しているようでしょう。</p>
<h1 id="拡張メソッド記述のポイント"><a href="#%E6%8B%A1%E5%BC%B5%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E8%A8%98%E8%BF%B0%E3%81%AE%E3%83%9D%E3%82%A4%E3%83%B3%E3%83%88">拡張メソッド記述のポイント</a></h1>
<p>拡張メソッドのポイントは3つ。</p>
<ol>
<li>staticクラス内に記載すること</li>
<li>staticメソッドにすること</li>
<li>第1引数の頭に<code>this</code>キーワードをつけること</li>
</ol>
<p>こうすることで、そのメソッドをインスタンスメソッドのように扱うことができます。</p>
<h1 id="コードがスッキリする"><a href="#%E3%82%B3%E3%83%BC%E3%83%89%E3%81%8C%E3%82%B9%E3%83%83%E3%82%AD%E3%83%AA%E3%81%99%E3%82%8B">コードがスッキリする</a></h1>
<p>例えば以下のようなコードがあるとしましょう。</p>
<pre><code class="csharp">string result = string.Join(",", Enumerable.Range(1, 20).Select(n => ((n % 3 == 0) || $"{n}".Contain("3")) ? "アホ" : $"{n}"));
// 結果 -> 1, 2, アホ, 4, 5, アホ, 7, 8, アホ, 10, 11, アホ, アホ, 14, アホ, 16, 17, アホ, 19, 20
</code></pre>
<p>上記コードは1から20のうち、<strong>3の倍数と3がつく数字の時は"アホ"を、それ以外はその数字を返すコレクションを<code>", "</code>で区切って連結する</strong>というものですが、このコードを読む時って</p>
<ol>
<li>まず<code>string.Join</code>があることで何かが区切って連結される</li>
<li>何が区切られるかを読む</li>
</ol>
<p>という順番で読むことになると思います。で、何が区切られるのかを読み始めたらめんどくさいコレクション操作をしていて嫌になりますね。</p>
<p>ですが、以下のような拡張メソッドが用意されていたらどうでしょう。</p>
<pre><code class="csharp">public static class GenericsExtensions
{
public static string JoinBy<T>(this IEnumerable<T> source, string separater)
{
// nullチェックは省略
return string.Join(separater, source);
}
}
</code></pre>
<p>これを使うと、以下のように書けます。</p>
<pre><code class="csharp">string result = Enumerable.Range(1, 20).Select(n => ((n % 3 == 0) || $"{n}".Contain("3")) ? "アホ" : $"{n}").JoinBy(",");
// 結果 -> 1, 2, アホ, 4, 5, アホ, 7, 8, アホ, 10, 11, アホ, アホ, 14, アホ, 16, 17, アホ, 19, 20
</code></pre>
<p>これならどうでしょう。対象の数字を"アホ"へ変換した後に、<code>", "</code>で区切って連結させると<strong>自然に</strong>読めますよね。</p>
<p>この自然に読めるということが超重要なんです。ただでさえ頭を使うことが多いので、不要なストレスを省くようにすべきだと思います。(別の話になりますが、LINQは頭の中のロジック通りに書けて、書いてある通りに自然に読めるので本当にすごい)</p>
<p>拡張メソッドを上手く活用できれば、可読性を上げることもできます。</p>
<p>おまけですが、以下のようなアホ判定拡張メソッドを用意するともっとスッキリすることでしょう。</p>
<pre><code class="csharp">public static class IntExtensions
{
public static bool IsMultipleOf(this int value, int @base)
{
return (value % @base == 0);
}
public static bool IsAho(this int value)
{
const int AhoTrigger = 3;
return value.IsMultipleOf(AhoTrigger) || $"{value}".Contains($"{AhoTrigger}");
}
}
// こう書ける
string result = Enumerable.Range(1, 20).Select(n => n.IsAho() ? "アホ" : $"{n}").JoinBy(", ");
// 結果 -> 1, 2, アホ, 4, 5, アホ, 7, 8, アホ, 10, 11, アホ, アホ, 14, アホ, 16, 17, アホ, 19, 20
</code></pre>
<h1 id="終わりに"><a href="#%E7%B5%82%E3%82%8F%E3%82%8A%E3%81%AB">終わりに</a></h1>
<p>拡張メソッドは上記の通りとてもメリットが大きく、一度実装してしまえば使う側は何も考えずに使える素晴らしい機能です。再利用性や拡張性も高いです。</p>
<p>とりあえずよく使う<code>string.IsNullOrEmpty</code>や<code>string.Format</code>などを拡張メソッドにするだけでもかなり便利になります。</p>
<p>これでみんな幸せになります、めでたしめでたし…と言いたいところですが全然めでたくないのが弊社です。私は日々弊社謹製レガシーライブラリと戦っています。ちくせう。</p>
あぱしょに