47

オプションの T の配列をオプションではない T の配列に変換できるようにする拡張機能を Array に書き込もうとしています。

たとえば、これは次のようなフリー関数として記述できます。

func removeAllNils(array: [T?]) -> [T] {
    return array
        .filter({ $0 != nil })   // remove nils, still a [T?]
        .map({ $0! })            // convert each element from a T? to a T
}

しかし、これを拡張機能として機能させることはできません。拡張機能はオプションの値の配列にのみ適用されることをコンパイラに伝えようとしています。これは私がこれまでに持っているものです:

extension Array {
    func filterNils<U, T: Optional<U>>() -> [U] {
        return filter({ $0 != nil }).map({ $0! })
    }
}

(コンパイルされません!)

4

7 に答える 7

80

Swift 2.0 の時点で、配列から nil 値をフィルター処理するために独自の拡張機能を記述する必要はありません。flatMap配列を平坦化し、 nil をフィルター処理する を使用できます。

let optionals : [String?] = ["a", "b", nil, "d"]
let nonOptionals = optionals.flatMap{$0}
print(nonOptionals)

版画:

[a, b, d]

ノート:

2 つのflatMap機能があります。

于 2015-08-11T18:21:28.220 に答える
64

TL;DR

スイフト4

を使用しarray.compactMap { $0 }ます。Apple はフレームワークを更新して、バグや混乱を引き起こさないようにしました。

スイフト3

array.flatMap { $0 }潜在的なバグや混乱を避けるために、nil を削除するために使用しないでください。array.removeNils()代わりに (以下の実装、Swift 3.0 用に更新)などの拡張メソッドを使用します。


ほとんどの場合は機能しますが、拡張機能array.flatMap { $0 }を使用する理由がいくつかあります。array.removeNils()

  • removeNils やりたいことを正確に説明します:値を削除しnilます。よく知らない人flatMapが調べる必要があり、調べたときによく注意すれば、次のポイントと同じ結論に達するでしょう。
  • flatMapには、2つのまったく異なることを行う 2 つの異なる実装があります。型チェックに基づいて、コンパイラはどちらが呼び出されるかを決定します。型推論が頻繁に使用されるため、これは Swift では非常に問題になる可能性があります。(たとえば、変数の実際の型を判断するには、複数のファイルを検査する必要がある場合があります。) リファクタリングにより、アプリが間違ったバージョンを呼び出し、見つけにくいバグにflatMapつながる可能性があります。
  • 2 つのまったく異なる機能があるため、この 2 つを簡単に混同flatMapできるため、理解がはるかに難しくなります。
  • flatMapはオプションではない配列 (例: ) で呼び出すことができるため、 からまで[Int]の配列をリファクタリングすると、コンパイラが警告しない呼び出しが誤って取り残される可能性があります。せいぜい自分自身を返すだけですが、最悪の場合、他の実装が実行され、バグにつながる可能性があります。[Int?][Int]flatMap { $0 }
  • Swift 3 では、戻り値の型を明示的にキャストしないと、コンパイラは間違ったバージョンを選択し、意図しない結果を引き起こします。(以下の Swift 3 セクションを参照)
  • 最後に、型チェック システムがオーバーロードされた関数のどれを呼び出すかを決定する必要があるため、コンパイラの速度が低下します。

要約すると、問題の関数には 2 つのバージョンがあり、残念ながらflatMap.

  1. ネスト レベルを削除してシーケンスをフラット化します (例: [[1, 2], [3]] -> [1, 2, 3])

    public struct Array<Element> : RandomAccessCollection, MutableCollection {
        /// Returns an array containing the concatenated results of calling the
        /// given transformation with each element of this sequence.
        ///
        /// Use this method to receive a single-level collection when your
        /// transformation produces a sequence or collection for each element.
        ///
        /// In this example, note the difference in the result of using `map` and
        /// `flatMap` with a transformation that returns an array.
        ///
        ///     let numbers = [1, 2, 3, 4]
        ///
        ///     let mapped = numbers.map { Array(count: $0, repeatedValue: $0) }
        ///     // [[1], [2, 2], [3, 3, 3], [4, 4, 4, 4]]
        ///
        ///     let flatMapped = numbers.flatMap { Array(count: $0, repeatedValue: $0) }
        ///     // [1, 2, 2, 3, 3, 3, 4, 4, 4, 4]
        ///
        /// In fact, `s.flatMap(transform)`  is equivalent to
        /// `Array(s.map(transform).joined())`.
        ///
        /// - Parameter transform: A closure that accepts an element of this
        ///   sequence as its argument and returns a sequence or collection.
        /// - Returns: The resulting flattened array.
        ///
        /// - Complexity: O(*m* + *n*), where *m* is the length of this sequence
        ///   and *n* is the length of the result.
        /// - SeeAlso: `joined()`, `map(_:)`
        public func flatMap<SegmentOfResult : Sequence>(_ transform: (Element) throws -> SegmentOfResult) rethrows -> [SegmentOfResult.Iterator.Element]
    }
    
  2. シーケンスから要素を削除します (例: [1, nil, 3] -> [1, 3])

    public struct Array<Element> : RandomAccessCollection, MutableCollection {
        /// Returns an array containing the non-`nil` results of calling the given
        /// transformation with each element of this sequence.
        ///
        /// Use this method to receive an array of nonoptional values when your
        /// transformation produces an optional value.
        ///
        /// In this example, note the difference in the result of using `map` and
        /// `flatMap` with a transformation that returns an optional `Int` value.
        ///
        ///     let possibleNumbers = ["1", "2", "three", "///4///", "5"]
        ///
        ///     let mapped: [Int?] = numbers.map { str in Int(str) }
        ///     // [1, 2, nil, nil, 5]
        ///
        ///     let flatMapped: [Int] = numbers.flatMap { str in Int(str) }
        ///     // [1, 2, 5]
        ///
        /// - Parameter transform: A closure that accepts an element of this
        ///   sequence as its argument and returns an optional value.
        /// - Returns: An array of the non-`nil` results of calling `transform`
        ///   with each element of the sequence.
        ///
        /// - Complexity: O(*m* + *n*), where *m* is the length of this sequence
        ///   and *n* is the length of the result.
        public func flatMap<ElementOfResult>(_ transform: (Element) throws -> ElementOfResult?) rethrows -> [ElementOfResult]
    }
    

{ $0 }#2は、として渡すことでnilを削除するために人々が使用するものtransformです。メソッドがマップを実行し、すべてのnil要素を除外するため、これは機能します。

「Apple はなぜ #2 を"に改名しなかったのか?」removeNils()と疑問に思うかもしれません。心に留めておくべきことの 1 つは、flatMapnil を削除するために使用することが #2 の唯一の使用法ではないということです。実際、どちらのバージョンもtransform関数を使用するため、上記の例よりもはるかに強力になる可能性があります。

たとえば、#1 は簡単に文字列の配列を個々の文字に分割 (フラット化) し、各文字を大文字にする (マップ) ことができます。

["abc", "d"].flatMap { $0.uppercaseString.characters } == ["A", "B", "C", "D"]

番号#2はすべての偶数を簡単に削除(平坦化)し、各数値に-1(マップ)を掛けることができます:

[1, 2, 3, 4, 5, 6].flatMap { ($0 % 2 == 0) ? nil : -$0 } == [-1, -3, -5]

(この最後の例では、明示的な型が記述されていないため、Xcode 7.3 が非常に長い時間スピンする可能性があることに注意してください。メソッドに異なる名前を付ける必要がある理由についてのさらなる証拠です。)

やみくもにsを使っflatMap { $0 }て削除する本当の危険は、 を呼び出すときではなく、 のようなものを呼び出すときに起こります。前者の場合、無害に呼び出し #2 を呼び出し、 を返します。後者の場合、同じことをすると思うかもしれませんが (値がないため無害に戻ります)、呼び出し #1 を使用しているため、実際には戻ります。nil[1, 2][[1], [2]][1, 2][[1], [2]]nil[1, 2]

flatMap { $0 }sを削除するために使用されるという事実は、Apple からのものではなくnil、Swiftコミュニティの 推奨事項のようです。おそらく、Apple がこの傾向に気付いた場合、最終的にremoveNils()機能または同様のものを提供するでしょう。

それまでは、独自のソリューションを考え出す必要があります。


解決

// Updated for Swift 3.0
protocol OptionalType {
    associatedtype Wrapped
    func map<U>(_ f: (Wrapped) throws -> U) rethrows -> U?
}

extension Optional: OptionalType {}

extension Sequence where Iterator.Element: OptionalType {
    func removeNils() -> [Iterator.Element.Wrapped] {
        var result: [Iterator.Element.Wrapped] = []
        for element in self {
            if let element = element.map({ $0 }) {
                result.append(element)
            }
        }
        return result
    }
}

(注: ... と混同しないでください。この投稿で説明しelement.mapたこととは何の関係もありません。ラップ解除できるオプションの型を取得するために関数を使用しています。この部分を省略すると、この部分が取得されます。構文エラー:「エラー: 条件付きバインディングの初期化子には、'Self.Generator.Element' ではなく、オプションの型が必要です。」どのように役立つかについての詳細は、非 nil をカウントするために SequenceType に拡張メソッドを追加することについて書いたこの回答を参照してください。 .)flatMapOptionalmapmap()

使用法

let a: [Int?] = [1, nil, 3]
a.removeNils() == [1, 3]

var myArray: [Int?] = [1, nil, 2]
assert(myArray.flatMap { $0 } == [1, 2], "Flat map works great when it's acting on an array of optionals.")
assert(myArray.removeNils() == [1, 2])

var myOtherArray: [Int] = [1, 2]
assert(myOtherArray.flatMap { $0 } == [1, 2], "However, it can still be invoked on non-optional arrays.")
assert(myOtherArray.removeNils() == [1, 2]) // syntax error: type 'Int' does not conform to protocol 'OptionalType'

var myBenignArray: [[Int]?] = [[1], [2, 3], [4]]
assert(myBenignArray.flatMap { $0 } == [[1], [2, 3], [4]], "Which can be dangerous when used on nested SequenceTypes such as arrays.")
assert(myBenignArray.removeNils() == [[1], [2, 3], [4]])

var myDangerousArray: [[Int]] = [[1], [2, 3], [4]]
assert(myDangerousArray.flatMap { $0 } == [1, 2, 3, 4], "If you forget a single '?' from the type, you'll get a completely different function invocation.")
assert(myDangerousArray.removeNils() == [[1], [2, 3], [4]]) // syntax error: type '[Int]' does not conform to protocol 'OptionalType'

[1, 2, 3, 4]( removeNils() が を返すことが期待されていたのに、最後のもので flatMap がどのように返されるかに注意してください[[1], [2, 3], [4]]。)


解決策は、リンクされている@fabbの回答に似ています。

ただし、いくつかの変更を加えました。

  • method には名前を付けませんでした。シーケンス型flattenのメソッドが既に存在flattenし、まったく異なるメソッドに同じ名前を付けたことが、そもそもこの混乱に陥った原因です。言うまでもなく、それよりも何をするかを誤解する方がはるかに簡単flattenですremoveNils
  • Tで新しいタイプを作成するのではなく、( ) を使用OptionalTypeするのと同じ名前を使用します。OptionalWrapped
  • 時間につながる を実行するmap{}.filter{}.map{}代わりにO(M + N)、配列を 1 回ループします。
  • flatMapから に移動するGenerator.Element代わりに を使用しGenerator.Element.Wrapped?ますmap。関数nil内で値を返す必要はないので、それで十分です。関数を回避することで、まったく異なる関数を持つ同じ名前のさらに別の (つまり 3 番目の) メソッドを混同することが難しくなります。mapmapflatMap

removeNilsvs.を使用することの 1 つの欠点flatMapは、型チェッカーがもう少しヒントを必要とする場合があることです。

[1, nil, 3].flatMap { $0 } // works
[1, nil, 3].removeNils() // syntax error: type of expression is ambiguous without more context

// but it's not all bad, since flatMap can have similar problems when a variable is used:
let a = [1, nil, 3] // syntax error: type of expression is ambiguous without more context
a.flatMap { $0 }
a.removeNils()

あまり調べていませんが、追加できるようです:

extension SequenceType {
  func removeNils() -> Self {
    return self
  }
}

オプションではない要素を含む配列でメソッドを呼び出せるようにしたい場合。これにより、大規模な名前変更 (例: flatMap { $0 }-> removeNils()) が容易になります。


自分自身への割り当ては、新しい変数への割り当てとは異なります?!

次のコードを見てください。

var a: [String?] = [nil, nil]

var b = a.flatMap{$0}
b // == []

a = a.flatMap{$0}
a // == [nil, nil]

驚くべきことに、にa = a.flatMap { $0 }割り当てた場合は nil を削除しません、 !に割り当てた場合は nil を削除します。私の推測では、これはオーバーロードされたものと関係があり、Swift が使用するつもりのないものを選択したことと関係があると思います。abflatMap

予想されるタイプにキャストすることで、問題を一時的に解決できます。

a = a.flatMap { $0 } as [String]
a // == []

しかし、これは忘れがちです。代わりに、removeNils()上記の方法を使用することをお勧めします。


アップデート

の(3)オーバーロードの少なくとも1つを非推奨にする提案があるようですflatMaphttps://github.com/apple/swift-evolution/blob/master/proposals/0187-introduce-filtermap.md

于 2016-07-24T02:08:44.923 に答える
44

一般的な構造体またはクラスに対して定義された型を制限することはできません。配列は任意の型で機能するように設計されているため、型のサブセットに対して機能するメソッドを追加することはできません。型制約は、ジェネリック型を宣言するときにのみ指定できます

必要なものを実現する唯一の方法は、グローバル関数または静的メソッドのいずれかを作成することです-後者の場合:

extension Array {
    static func filterNils(_ array: [Element?]) -> [Element] {
        return array.filter { $0 != nil }.map { $0! }
    }
}

var array:[Int?] = [1, nil, 2, 3, nil]

Array.filterNils(array)

または、単純にcompactMap(以前のflatMap) を使用して、すべての nil 値を削除するために使用できます。

[1, 2, nil, 4].compactMap { $0 } // Returns [1, 2, 4]
于 2015-01-28T11:17:07.583 に答える