【C#】Where(it => it is Xxx).Select(it => it as Xxx)って書かず、OfType使おう!【LINQ】 - Qiitaを見て、Swiftとの対比記事を書いてみようと思いました。
元記事では、C#でコレクションから特定の型のオブジェクトだけ取り出したい場合は、Where
・Select
の組み合わせでも出来るものの、OfType
を使うと簡潔に書けて良い、ということでした。
元記事のコード例から、Unity依存排除してシンプル化するなどして、コード例を書き直してみました。
(Visual Studio for MacがあるとParallelsでWindows仮想マシン起動せずに済んで便利でした( ´・‿・`))
using System;
using System.Linq;
class Program
{
class Base { }
class A : Base { }
class B : Base { }
static void Main(string[] args)
{
var objects = new Base[] { new A(), new B() };
var arrayOfA = objects
.Select(x => x as A)
.Where(x => x != null)
.ToArray(); // → Aオブジェクト1つだけの配列となる
// 以下のように`OfType`を用いても同じ結果になるので、こう書く方がベター
var arrayOfA2 = objects.OfType<A>();
}
}
(元記事では、Where
→ Select
でis
・as
という型比較系の処理が2回行われていたのが冗長に感じたので、Select
→ Where
の順に書き換えています)
Swift ではどう書く?
いくつかコード例書いていきます。
map
→ filter
(NG)
上のC#の例のSelect
→ Where
に相当する処理であるmap
→ filter
に単純に置き換えてみました。
class Base { }
class A: Base {}
class B: Base {}
let objects: [Base] = [A(), B()]
let arrayOfA = objects
.map { $0 as? A }
.filter { $0 != nil }
結果は、[Optional(A)]
となり、求めていた[A]
ではなくNGです。
以下のように、filter
(is
でフィルター) → map
(フィルター済みなので一応as!
でもOK…)すると、結果は求めていた[A]
になりますが、イマイチなやり方ですよね。
let arrayOfA2 = objects
.filter { $0 is A }
.map { $0 as! A }
map
でOptional(A)
のArrayにしたあと、がんばってnil
を除去
reduce
でいわゆるflatten
ぽいことをしたコードが以下です。
let arrayOfA = objects
.map { $0 as? A }
.reduce([A]()) { sum, e in
if let e = e {
return sum + [e]
}
return sum
}
とりあえず結果は求めていた[A]
となりましたが、ちょっと処理が煩雑ですね。
flatMap
を使う
そこで、本命のflatMap
を使うと、こんなに簡単に書けてしまいます👏
let arrayOfA = objects.flatMap { $0 as? A }
flatMap
は、map
してflatten
(平坦化)の意味なので、まさしくやりたかったことに沿ったメソッドですね。
flatMap
では、Optional(A)
のArrayにして(map
)、さらにアンラップ(nilは除去)する処理(flatten
)が一度になされています。
SwiftにもofType
を定義
flatMap
でシンプルに書けるのであまり意味が無いですが、SwiftにもofType
を定義してみます。
extension Collection {
func ofType<T>() -> [T] {
return flatMap { $0 as? T }
}
}
すると、次のように、SwiftでもC#のofType
が使えるようになりました。
ただ、flatMap
をベタに書いた場合と記述量も読みやすさも変わらないので、基本的にはflatMap
をそのまま使えば良いと思います。
let arrayOfA = objects.ofType() as [A]
つまり、flatMap
という汎用メソッドがあるだけで、ofType
や類似の処理が簡潔に書ける、というキレイな世界になっています👏
(SwiftでofType
書くとしたら、上記のようにflatMap
ラップではなくベタに書いてflatMap
より高パフォーマンスなメソッドにしておくと存在価値ありそう)
C#ではflatMap
ぽいものは無いの?
SelectMany
で可能(SwiftのflatMap
とまったく同じでは無い)
少し記述が混み合いますが、SelectMany
で可能です。
var arrayOfA = objects.SelectMany(x =>
{
var a = x as A;
if (a == null)
{
return new A[0];
}
return new A[] { a };
}).ToArray();
nilの扱いが絡むとSwift
の方がシンプル・堅牢に書けることが多い
上の例ではSelectMany
のラムダ式が複雑になりましたが、以下のように[[1, 2], 3]
→ [1, 2, 3]
と変換するような単純なシーケンスの平坦化処理であれば、swift
とあまり差が無くなります。
(配列の初期化部分は少し面倒な記述になっていますが、SelectMany
の中身はシンプル)
C#:
var results = (new int[][] { new int[]{ 1, 2 }, new int[]{ 3 } }).SelectMany(x => x).ToArray();
Swift:
let results = [[1, 2], [3]].flatMap { $0 }
Swiftではオプショナルが言語仕様に組み込まれていて、さらにflatMap
が、ArrayもOptionalも同じ「入れ物」として抽象化しているので、「平坦化」処理の際、入れ子の配列を合体させるような感覚で、Optionalのアンラップが行われます。
C# でも拡張メソッド足すことでSelectMany
でシンプルに書けるように出来る
@neuecc さんからTweetいただきました。
flatMapの実装にそういうのがあるからって感じで言語仕様的に一緒だからってわけではない感。(しかしC#はnullableの考慮を足すのが面倒) https://t.co/cweyEGg5Y2 / “C#のOfTypeメソッド…” https://t.co/V6rrs1zCoW
— neuecc (@neuecc) November 25, 2016
元の文中には「言語仕様的に一緒」とは書かれてない(同じ「入れ物」として抽象化している、というのが正しい)ので、コメントはちょっと読み違え含むです、すみません。 https://t.co/ASzeq8TrKs
— neuecc (@neuecc) November 25, 2016
RxがTaskと融合するってのもIObservable.SelectManyにTaskのオーバーロード足しただけっていう話で、こういうのは見せ方が大事というのがある。全体的にそういう見せ方で統一されているから、そういうものだと扱える。そもそもLINQがそういうものですからね。
— neuecc (@neuecc) November 25, 2016
using System;
using System.Collections.Generic;
class Base { }
class A : Base { }
class B : Base { }
class Program
{
static void Main(string[] args)
{
var objects1 = new Base[] { new A(), new B() };
var objects2 = new int?[] { 1, 2, 100, null, 1000 };
var hoge = objects1.SelectMany(x => x as A);
var huga = objects2.SelectMany(x => x);
}
}
public static class FlatMapLikeExtensions
{
public static IEnumerable<U> SelectMany<T, U>(this IEnumerable<T> source, Func<T, U> selector)
where U : class
{
foreach (var item in source)
{
var v = selector(item);
if (v != null) yield return v;
}
}
public static IEnumerable<U> SelectMany<T, U>(this IEnumerable<T> source, Func<T, U?> selector)
where U : struct
{
foreach (var item in source)
{
var v = selector(item);
if (v.HasValue) yield return v.Value;
}
}
}
SelectManay
でシンプル(Swift
のflatMap
と同等)に書けるようになって、ついでに拡張メソッドの実装もラムダ式では無くなるのでyield
使えて http://qiita.com/mono0926/items/b8ff59938a60ddf93c6e#selectmanyで可能swiftのflatmapとまったく同じでは無い のコードより少しシンプルになっています。
この拡張メソッド足すと、C#でもOfType
いらないのでは🤔と思えてきちゃいます( ´・‿・`) (※1)
まったく同じ結果:
var hoge = objects.OfType<A>();
var fuga = objects.SelectMany(x => x as A);
(※1: ただしパフォーマンス的に違いがあったりはしそう。)
関連
関連として、以下の @koher さんの記事などがとても参考にあります。
1年ほど前の記事ですが、このあたりはSwift 2からほぼ変わっていないはずです(1→2の間は色々改良あった)。
-
Swift 2.0の新しいflatMapが便利過ぎる - Qiita
- 本記事書いた後に、そういえばこの記事あったなと思い出して読み返したら、似た内容でした( ´・‿・`)
-
ArrayとOptionalのmapは同じです - Qiita
- こういう話題に踏み込もうとしたものの、こちらの記事に充分詳しく書かれていて書く余地無かった( ´・‿・`)
- mapとflatMapという便利メソッドを理解する - Qiita