C#のOfTypeメソッド相当の処理は、SwiftではflatMapで書ける、という話

  • 12
    いいね
  • 0
    コメント

【C#】Where(it => it is Xxx).Select(it => it as Xxx)って書かず、OfType使おう!【LINQ】 - Qiitaを見て、Swiftとの対比記事を書いてみようと思いました。

元記事では、C#でコレクションから特定の型のオブジェクトだけ取り出したい場合は、WhereSelectの組み合わせでも出来るものの、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>();
    }
}

(元記事では、WhereSelectisasという型比較系の処理が2回行われていたのが冗長に感じたので、SelectWhereの順に書き換えています)

Swift ではどう書く?

いくつかコード例書いていきます。

mapfilter (NG)

上のC#の例のSelectWhereに相当する処理であるmapfilterに単純に置き換えてみました。

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 }

mapOptional(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いただきました。

https://gist.github.com/neuecc/6cbec456dab4d0bb3c81976799b5fc6e

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でシンプル(SwiftflatMapと同等)に書けるようになって、ついでに拡張メソッドの実装もラムダ式では無くなるので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の間は色々改良あった)。