如何从任意类型中解包可选值?

61

假设有一个混合了可选和非可选值的 [Any] 类型的数组,例如:

let int:Int? = 1
let str:String? = "foo"

let values:[Any] = [int,2,str,"bar"]
我们如何提取Any类型中Optional的值(如果有),以便我们可以创建一个通用的打印函数,只打印出这些值。例如,此打印数组函数逐个打印每个元素:
func printArray(values:[Any]) {
    for i in 0..<values.count {
        println("value[\(i)] = \(values[i])")
    }
}

printArray(values)

输出结果将是:

value[0] = Optional(1)
value[1] = 2
value[2] = Optional("foo")
value[3] = bar
我们怎样才能改变它,使它只打印基础值,以便在其为Optional的情况下解包该值?例如:
value[0] = 1
value[1] = 2
value[2] = foo
value[3] = bar

更新进度...

当将参数更改为[Any?]时,它可以正常工作,例如:

let values:[Any?] = [int,2,str,"bar"]

func printArray(values:[Any?]) {
    for i in 0..<values.count {
        println("value[\(i)] = \(values[i]!)")
    }
}

printArray(values)

哪个将打印所需内容:

value[0] = 1
value[1] = 2
value[2] = foo
value[3] = bar

但仍然想看看如何从 Any 中展开 Optional,因为这是 MirrorType.value 返回的内容,使得提取 Optional 值变得困难,例如:

class Person {
    var id:Int = 1
    var name:String?
}

var person = Person()
person.name = "foo"

var mt:MirrorType = reflect(person)
for i in 0 ..< mt.count {
    let (name, pt) = mt[i]
    println("\(name) = \(pt.value)")
}

输出:

id = 1
name = Optional("foo")

当我需要:

id = 1
name = foo

1
是的,因为谜题措辞得当,我点了赞。当然你不会得到任何答案。 :) - matt
1
我已经发布了到目前为止的代码。也许有人可以帮忙跨过最后一道障碍。 - qwerty_so
这似乎不适用于所有情况。如果可选值在字典中,reflect() 将返回 disposition=Aggregate 而不是 Optional,计数为 0。:( 然而 valueType 显示了正确的类型。 - Zmey
这是一个重要的问题,但问题本身非常混乱,包含了许多例子和从答案中得出的进展。如果能将问题简化到基本情况,并将进展转化为答案,那就太好了。 - Maic López Sáenz
如果您有不同于已接受答案的解决方案,请将其作为“答案”发布,而不是放在问题中。谢谢。 - Eric Aya
显示剩余7条评论
10个回答

40

对于 Xcode 7 和 Swift 2:

func unwrap(any:Any) -> Any {

    let mi = Mirror(reflecting: any)
    if mi.displayStyle != .Optional {
        return any
    }

    if mi.children.count == 0 { return NSNull() }
    let (_, some) = mi.children.first!
    return some

}


let int:Int? = 1
let str:String? = "foo"
let null:Any? = nil
let values:[Any] = [unwrap(int),2,unwrap(str),"bar", unwrap(null)]

这将给你 [1, 2, "foo", "bar", {NSObject}]

NSNull()更改为nil,并将unwrap函数的返回值更改为Any? 将始终解开任何类型。


如果你将unwrap的返回值更改为Any?,你将始终得到一个Optional。 - Spriter
适用于Swift 3. :x - nemesis
在Swift 3上无法工作,在if mi.displayStyle != .Optional {这一行失败,出现错误Binary operator '!=' cannot be applied to operands of type 'Mirror.DisplayStyle?' and '_' - Miguel Guerreiro
3
针对 Swift 3,您需要将 if mi.displayStyle != .Optional { 更改为 if mi.displayStyle != .optional { - walkline
为了更好地理解Swift中的可选项,请查看此链接:http://www.ios360degree.com/swift-optional-and-non-optional-variables/ - Satish Azad

25

为了让一些人不用从答案和评论中拼凑出来,这里提供了一个包含"正常"方法和一些我认为是Swift 3的改进的答案,这些改进都是基于Xcode 8.2.1的。

使用反射

func unwrap<T>(_ any: T) -> Any
{
    let mirror = Mirror(reflecting: any)
    guard mirror.displayStyle == .optional, let first = mirror.children.first else {
        return any
    }
    return first.value
}

讨论

来自bubuxu的答案在Swift 3下无法编译通过。 正如walkline在他的评论中所建议的那样,将.Optional更改为.optional可以解决这个问题(请参见SE-0005Swift API设计指南)。

我认为这个解决方案可以改进的原因:

  • 我觉得返回NSNull()很奇怪。
  • 我认为返回带有返回类型Any?nil的替代方案也存在问题,因为它会把所有东西(包括非可选值)都变成可选值 (例如unwrap(any: 42)返回Optional(42))。
  • 当使用除Any值之外的任何内容调用unwrap(any:)时,Swift 3编译器会警告隐式地强制转换为Any

类似的想法适用于Sajjon的答案

我建议的解决方案解决了所有这些问题。但是,请注意,unwrap(_:)nil 作为类型Any返回,因此使用nil合并运算符不再起作用。 这意味着这只是将我认为第二个问题存在的问题转移了过来。但是对于我更感兴趣的反射用例,我发现这正是正确的做法。

在Optional上使用扩展

protocol OptionalProtocol {
    func isSome() -> Bool
    func unwrap() -> Any
}

extension Optional : OptionalProtocol {
    func isSome() -> Bool {
        switch self {
        case .none: return false
        case .some: return true
        }
    }

    func unwrap() -> Any {
        switch self {
        case .none: preconditionFailure("trying to unwrap nil")
        case .some(let unwrapped): return unwrapped
        }
    }
}

func unwrapUsingProtocol<T>(_ any: T) -> Any
{
    guard let optional = any as? OptionalProtocol, optional.isSome() else {
        return any
    }
    return optional.unwrap()
}

讨论

这基本上是 LopSae的解决方案 根据Swift 3更新而来。我还更改了先决条件失败消息并添加了unwrapUsingProtocol(_:)

用法

class Person {
    var id:Int = 1
    var name:String?
}

var person = Person()
person.name = "foo"

let mirror = Mirror(reflecting: person)
for child in mirror.children.filter({ $0.label != nil }) {
    print("\(child.label!) = \(unwrap(child.value))")
}

无论您使用unwrap()还是unwrapUsingProtocol(),这都将打印输出。
id = 1
name = foo

如果你正在寻找一种整齐地对齐输出的方法,请参考在Swift中使用制表符平均间隔描述字符串的方法。请注意保留HTML标签。

14

要检查一个Any变量是否是可选的协议可以用作无类型可选项

正如目前(Swift 2)不可能检查无类型可选项一样,将其转换为无类型可选项也不可能:

let anyType: Any.Type = Optional<String>.self
let anyThing: Any = Optional.Some("string")

anyType is Optional.Type // Causes error
let maybeString = anything as? Optional // Also causes error
// Argument for generic parameter 'Wrapped' could not be inferred

然而,提议的OptionalProtocol也可以用于提供访问可选值的无需泛型接口,甚至可以对其进行解包:

protocol OptionalProtocol {
    func isSome() -> Bool
    func unwrap() -> Any
}

extension Optional : OptionalProtocol {
    func isSome() -> Bool {
        switch self {
            case .None: return false
            case .Some: return true
        }
    }

    func unwrap() -> Any {
        switch self {
            // If a nil is unwrapped it will crash!
            case .None: preconditionFailure("nill unwrap")
            case .Some(let unwrapped): return unwrapped
        }
    }
}

// With this we can check if we have an optional
let maybeString: String? = "maybe"
let justString: String = "just"

maybeString is OptionalProtocol // true
justString is OptionalProtocol  // false

使用提供的方法,可以以相当自然的方式检查和访问选项,而无需进行不可能的Optional强制转换:

let values:[Any] = [
    Optional.Some(12),
    2,
    Optional<String>.None, // a "wrapped" nil for completeness
    Optional.Some("maybe"),
    "something"
]

for any in values {
    if let optional = any as? OptionalProtocol {
        if optional.isSome() {
            print(optional.unwrap())
        } else {
            // nil should not be unwrapped!
            print(optional)
        }
        continue
    }

    print(any)
}

将会打印出:

12
2
nil
maybe
something

这非常聪明。与反射相比,它的性能如何? - mrgrieves

8
对@thm进行轻微修改,以完全取消包装:
func unwrap<T>(_ any: T) -> Any {
    let mirror = Mirror(reflecting: any)
    guard mirror.displayStyle == .optional, let first = mirror.children.first else {
        return any
    }
    return unwrap(first.value)
}

7

我认为这是一种错误。

通常,要从 Any 中发现并提取特定类型,使用 as 进行向下转换是唯一支持的方法。但是:

let int:Int? = 1
let any:Any = int

switch any {
case let val as Optional<Int>: // < [!] cannot downcast from 'Any' to a more optional type 'Optional<Int>'
    print(val)
default:
    break
}

这意味着,没有支持的方法来做到这一点。 无论如何,显然您可以使用reflect来实现:
func printArray(values:[Any]) {
    for i in 0..<values.count {
        var val = values[i]

        var ref = reflect(val)
        // while `val` is Optional and has `Some` value
        while ref.disposition == .Optional && ref.count > 0 && ref[0].0 == "Some" {
            // replace `val` with unwrapped value
            val = ref[0].1.value;
            ref = reflect(val)
        }

        println("value[\(i)] = \(val)")
    }
}

let int:Int? = 1
let str:String? = "foo"

let values:[Any] = [int,2,str,"bar"]

printArray(values)

输出:

value[0] = 1
value[1] = 2
value[2] = foo
value[3] = bar

新增: 细微调整版本

func printArray(values:[Any]) {
    for i in 0..<values.count {

        var ref = reflect(values[i])
        // while `val` is Optional and has `Some` value
        while ref.disposition == .Optional && ref.count > 0 && ref[0].0 == "Some" {
            // Drill down to the Mirror of unwrapped value
            ref = ref[0].1
        }
        let val = ref.value

        println("value[\(i)] = \(val)")
    }
}

把它提取成一个函数:

func unwrapAny(val:Any) -> Any {
    var ref = reflect(val)
    while ref.disposition == .Optional && ref.count > 0 && ref[0].0 == "Some" {
        ref = ref[0].1
    }
    return ref.value
}

func printArray(values:[Any]) {
    for i in 0..<values.count {
        println("value[\(i)] = \(unwrapAny(values[i]))")
    }
}

在答案中添加了微调版本。 - rintaro
请问能否将这个转换为Swift 3? - RenniePet

1
不是完整的答案。归结起来就是这样:
let int:Int? = 1
let str:String? = "foo"

let values:[Any] = [int,2,str,"bar"]
func printArray(values:[Any]) {
  for i in 0..<values.count {
    let v = values[i]
    if _stdlib_demangleName(_stdlib_getTypeName(v)) == "Swift.Optional" {
      println("value[\(i)] = "it's optional: \(v)") // here I'm stuck
    }else {
      println("value[\(i)] = \(values[i])")
    }
  }
}

printArray(values)

FYI可以这样写得更好看一些:if "\(_stdlib_demangleName(_stdlib_getTypeName(v)))" == "Swift.Optional" {...} - mythz
当然,我考虑过这个问题。但是,尽管您可以使用stdlib方法来欺骗并查看某些内容是否为可选项,但您无法进行强制转换,因此无法解包。此外,我不会指望未经记录的stdlib方法会一直存在。最好将谜题留作谜题:语言中存在漏洞。 - matt
我们也可以通过以下方式更改它,以避免使用内部stdlib方法:if reflect(values[i]).disposition == MirrorDisposition.Optional { ... }。@matt现在很高兴能够让某些东西工作(如果可能的话?),并在正式API可用后再进行更改。 - mythz
@matt 是的,我在考虑创建一个通用的JSON序列化库,需要提取可选值进行序列化。 - mythz
@mythz,但我不明白为什么你不直接从[Any?]开始。问题解决了;现在一切都是可选的,一切都可以解包。这个解决方案对我来说很明显;这就是为什么我认为这个问题只是一个玩笑,一种戏弄Swift的方式。(事实上,我看到有人已经把这个作为答案了。) - matt
显示剩余5条评论

1
这个解决方案怎么样?我制作了一个通用版本的之前的答案。
fileprivate func unwrap<T>(value: Any)
  -> (unwraped:T?, isOriginalType:Bool) {

  let mirror = Mirror(reflecting: value)
  let isOrgType = mirror.subjectType == Optional<T>.self
  if mirror.displayStyle != .optional {
    return (value as? T, isOrgType)
  }
  guard let firstChild = mirror.children.first else {
    return (nil, isOrgType)
  }
  return (firstChild.value as? T, isOrgType)
}

let value: [Int]? = [0]
let value2: [Int]? = nil

let anyValue: Any = value
let anyValue2: Any = value2

let unwrappedResult:([Int]?, Bool)
  = unwrap(value: anyValue)    // ({[0]}, .1 true)
let unwrappedResult2:([Int]?, Bool)
  = unwrap(value: anyValue2)  // (nil, .1 true)
let unwrappedResult3:([UInt]?, Bool)
  = unwrap(value: anyValue)  // (nil, .1 false)
let unwrappedResult4:([NSNumber]?, Bool)
  = unwrap(value: anyValue)  ({[0]}, .1 false)

以下是 Playground 上的代码。

enter image description here


0

基于@bubuxu的解决方案,我们还可以:

func unwrap<T: Any>(any: T) -> T? {
    let mirror = Mirror(reflecting: any)
    guard mirror.displayStyle == .optional else { return any }
    guard let child = mirror.children.first else { return nil }
    return unwrap(any: child.value) as? T
}

但是在使用 unwrap 时,您需要使用 ?? nil 进行空值检查,就像在 foo 中所做的那样。

func foo<T>(_ maybeValue: T?) {
    if let value: T = unwrap(any: maybeValue) ?? nil {
        print(value)
    }
}

还是很整洁的!

(有人有 ?? nil 检查的解决方案吗?)


-1

不要让它变得太复杂,为什么不这样做:

let int:Int? = 1
let str:String? = "foo"

let values:[Any?] = [int,2,str,"bar"]

for var i:Int = 0; i < values.count; i++
{
    println("\(values[i]!)")
}

这将打印:

1
2
foo
bar


只有在您知道将会有一个值的情况下,这才是可以接受的,但使用可选项通常不是这种情况。 - Andrew Robinson
这很有趣。使用 Any? 实际上是将任何可选项解包并重新封装为 Any?。这意味着如果您执行 let any: Any = Optional.Some("maybe"),那么 any.dynamicType 将是 Optional<String>。但是,如果您执行 let maybeAny: Any? = Optional.Some("maybe"),那么 maybeAny.dynamicType 将是 Optional<protocol<>> - Maic López Sáenz

-3

根据在Swift 2.0中使用枚举情况模式,可能会像这样:

let pattern :[Int?]  = [nil, 332, 232,nil,55]
for case let number? in pattern {
   print(number)
}

输出: 332, 232, 55


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接