将单个参数强制传递到多可选参数函数中

4

考虑下面的函数:

func myFunction(completion: (data: Data?, error: Error?) -> ()) { }

我目前的要求是completion只能接受data值或者error值,但不允许两个同时出现。其中一个必须为nil。

虽然可以将两个值都保留为可选,之后再进行解包和检查,但我认为更好的方法是编译器能够提示开发人员不能同时设置两个值。

从另一个角度来看,知道它们中的一个将总是被设置为someValue可能会更有用。

这样,您就可以保证获得一个errordata,无需担心处理它们都是nil的情况。

目前是否有一种方法可以做到这一点?


我认为你正在提出一种语言和类型系统的扩展。据我所知,在Swift签名中无法关联参数。 - Raphael
3个回答

8

2
作为结果枚举的替代方案,你应该研究一下 promises 框架。有很多这样的框架,但我喜欢 PromiseKit
func myFunction() -> Promise<Data> { }

这是它的使用方法。
obj.myFunction().then { data -> Void in
    // Use data, it's guaranteed to be there.
} .catch { error in
    // Handle the error
}

1
你希望Swift能做一些它不能做的事情。就我所知,Swift没有union类型。如果你需要类似的功能,可能需要查看函数式或函数式启发的语言。
< p > < em >插曲: 如果两种类型都是结构体或类(即不是协议),则可以使用此模式:
protocol DataOrError {}
extension Data: DataOrError {}
extension ErrorClass: DataOrError {}

假设没有其他协议实现者,那么你现在拥有了接近联合类型的东西。你也可以对类型进行switch-case操作:

switch doe {
    case let e as Error: print("Error \(e)")
    case let d as Data: print("Data \(d)")
    default: assert(false, "thou shalt not implement my dummy!")
}

无论如何,我们可以使用带有关联值的枚举来模拟联合类型(在某些限制下)。这可以作为一种低配版的联合类型。
像这样定义一个枚举:
enum DataOrError {
    case Data(Data)
    case Error(Error)
}

现在你可以在任何需要(确切地)有一个DataError的地方使用DataOrError,并且对于最多一个,可以使用DataOrError?
在调用站点,您会得到类似于这样的东西:
extension String: Error {} // low-effort errors, don't judge me!
func myFunction(completion: (DataOrError) -> ()) {
    completion(.Data(Data(bytes: [1,2,3,4,5])))
    completion(.Error("this won't work!"))
}

And callee-site:

var myCompletion = { (doe: DataOrError) in
    switch doe {
        case .Data(let d): print("Data \(d)")
        case .Error(let e): print("Error \(e)")
    }
}

myFunction(completion: myCompletion)
// > Data 5 bytes
// > Error this won't work!

设计说明:如果您有多种类型要与Error进行OR运算,您可能正在寻找另一种方向的泛化。在这种情况下,即使您牺牲了良好的语法,明确的包装器也可能是一个好的解决方案。

struct TalkativeOptional<T> {
    private(set) var rawValue: T?
    private(set) var error: Error?

    init(value: T) {
        self.rawValue = value
    }

    init(error: Error) {
        self.error = error
    }
}

请注意这两个属性中只有一个可以是非nil的。还有两种组合方式;您可以通过选择初始化程序来控制您想要的组合方式。
调用示例:
func convert(_ number: String) -> TalkativeOptional<Int> {
    if let int = Int(number) {
        return TalkativeOptional(value: int)
    } else {
        return TalkativeOptional(error: "'\(number)' not a valid integer!")
    }
}

示例调用站点:

var a = convert("dsa2e2")

if let val = a.rawValue {
    print("a + 1 = \(val + 1)")
} else { // we know by design that error is set!
    print("ERROR: \(a.error!)")
}

为了在第一个switch中绑定de,您需要使用let(例如:case let d as Data: print("Data \(d)"))。此外,对于枚举选项,您不需要使用default情况,因为编译器可以看到switch是穷尽的(这也是为什么枚举比协议更可取的原因之一)。还要注意,Swift约定使用lowerCamelCase枚举案例。最后(很抱歉给您带来这些),您的TalkativeOptional不能保证其属性中恰好有一个非空,因为其属性是可变的,因此强制解包可能存在潜在危险。 - Hamish
都对,谢谢!我自己还在学习中。1)关于 lowerCamelCase:这让我这个深受 Java 影响的人感到非常奇怪。2)关于 TalkativeOptional,很好的发现;我想到了 let 声明,但是如果我们将这样的值返回给某人,我们当然无法控制它。 - Raphael
1
没问题,欢迎加入Swift社区 :) 关于lowerCamelCase,它是根据命名类型为UpperCamelCase和其他所有东西均为小写字母的约定而来的(因此由于枚举值不是类型,所以它们是小写字母)(在此处记录)。至于您的第二点,您可以将TalkativeOptional的属性设置为let常量,因为它们在初始化时已经具有值,不需要被实现改变(尽管在init中仍需显式将其他属性设置为nil)。 - Hamish
我喜欢所有这些例子,最后一个是我最喜欢的,因为它将所有内容都很好地捆绑在一起,并且可以通过添加更多属性来扩展。完美地适合我的使用情况,所以谢谢你。 - Beau Nouvelle

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