在Swift中什么时候需要使用参数标签?

68

回答这个问题时,发现调用init需要参数标签。在Swift中这是很常见的。

class Foo {
    init(one: Int, two: String) { }
}

let foo = Foo(42, "Hello world") // Missing argument labels 'one:two:' in call

然而,有着陌生的力量在发挥作用:

extension Foo {
    func run(one: String, two: [Int]) { }
}

foo.run(one: "Goodbye", two: []) // Extraneous argument label 'one:' in call

在这里使用参数标签,需要明确声明。

我还没有看到文档中对所有这些进行详细说明的内容。哪些类型的类/实例/全局函数需要参数标签? Obj-C方法总是使用参数标签导出和导入吗?

6个回答

45
所有初始化方法都需要参数名:
var view = NSView(frame: NSRect(x: 10, y: 10, width: 50, height: 50))
class Foo {
    init(one: Int, two: String) { }
}

let foo = Foo(one: 42, two: "Hello world")

所有在对象上调用的方法都使用参数名,除第一个参数外:

extension Foo {
    func run(one: String, two: [Int]) { }
}
foo.run("Goodbye", two: [])

所有在Swift和Objective-C中的类函数都遵循相同的模式。您还可以显式添加外部名称。
extension Foo{
class func baz(one: Int, two: String){}
class func other(exOne one: Int,  exTwo two: String){}
}
Foo.baz(10, two:"str")
Foo.other(exOne: 20, exTwo:"str")

不是类函数的Swift函数不需要参数名,但你仍然可以显式添加它们:

func bar(one: Int, two: String){}
bar(1, "hello")

正如Bryan所说,这是为了使Swift方法在调用带有方法签名中参数名称的objective-c方法时能够更加容易理解。初始化方法包含第一个参数,因为Swift将初始化方法从objective-c的initWith:...更改为Class() ,因此第一个参数名称不再包含在方法名称中。

你的回答似乎是最完整的。你能添加一些外部文档的参考吗? - jtbandes
我想强制包含我的参数名称 ascending,以便调用站点更加明确。因此,我按照这个答案的指示,将方法签名设置为 sortByDate(ascending ascending: Bool)。Xcode建议我可以通过将 ascending ascending 更改为 #ascending 来简化此过程,我已经这样做了。我认为我们可以说Swift显然是一种“现代编程语言”,因为它采用了井号(对我来说一直都是“井号”)。现在我想在我的代码中添加一个 #stfu 参数。;-) - mharper
2
以下是《Swift编程语言》中的一句有用的引用:“然而,初始化器在它们的括号之前没有像函数和方法那样的标识功能名称。因此,初始化器参数的名称和类型在确定应该调用哪个初始化器方面起着特别重要的作用。由于这个原因,如果您没有自己提供外部名称,Swift会为初始化器中的每个参数提供自动外部名称。” - jtbandes
可能还需要补充一点,你可以使用 # 强制使用第一个参数标签,例如:func myFunc( #parameter: String) {},这样就必须像这样调用它:myFunc(parameter:"")myFunc("") 会显示错误,就像 @Robert 在他的回答中所示。 - SirRupertIII
为了更新之前的评论,"#"已经从Swift中删除。双倍使用'parameter'来使参数标签与参数名称相同 func myFunc(parameter parameter: String) {} - Mark Mckelvie
@jtbandes 感谢您提供的参考,这使得整个过程更加清晰明了,因为“init”具有非常通用的含义,这使得阅读变得困难... Swift 一般倾向于使代码更易读,在 ObjC 中,类方法使用 +,而在 Swift 中则使用 static。在 ObjC 中,您必须手动进行延迟实例化,在 Swift 中只需使用 lazy 关键字即可,在 ObjC 中,您必须使用 (NSMutableArray*) 进行强制转换,在 Swift 中则使用关键字 as,这使得所有内容都更易读。 - mfaani

35
从Swift 3.0开始,这一点再次发生了变化:所有方法、函数和初始化程序都需要为所有参数指定参数标签,除非您已经明确地选择使用外部名称_进行退出。这意味着像addChildViewController(_:)这样的方法现在应该写成这样:
func addChildViewController(_ childController: UIViewController)

这是作为Swift Evolution过程的一部分提出并获得批准的,并在SR-961中实现。


2
我本来想在这个问题上发布一个新的问题,但也许你可以回答:为什么有人想要省略参数标签?在我看来,似乎只是随意决定何时省略参数标签。 - tfantina
add(childController: UIViewController) 会不好吗?我们应该如何决定参数描述是函数名称的一部分还是参数标签? - Declan McKenna
在我的情况下,我有一个写入日志文件的类。与反复使用Log.write(text:“an error message”)相比,使用Log.write(“an error message”)要容易得多。可以将其与print()函数进行比较。在这种情况下,参数标签会使语法变得混乱(依我之见),并且似乎是多余的。 - bergy

33

Swift 3.0

在 Swift 3.0 中(预计于2016年晚些时候发布),默认行为很简单:

  • 所有方法的所有参数默认都有外部标签。

您可以在Swift API 设计准则中最简洁地找到这些规则。这种最新行为是在SE-0056,“建立包括第一个标签在内的所有参数的一致标签行为”中提出的,并在SR-961中实现。如下所述,可以更改默认行为,即“覆盖默认行为。”

Swift 2.2

在 Swift 2.2 中,语言对外部参数标签的默认存在已更改,现在更加简单。默认行为可以总结如下:

  • 方法和函数的第一个参数不应该有外部参数标签。
  • 方法和函数的其他参数应该有外部参数标签。
  • 初始化器的所有参数都应该有外部参数标签。

默认行为可以根据下面的"覆盖默认行为"进行更改。

示例

这些规则最好通过一个示例来说明:

func printAnimal(animal: String, legCount: Int) {
    let legNoun = legCount == 1 ? "leg" : "legs"
    print("\(animal) has \(legCount) \(legNoun)")
}

struct Player {
    let name: String
    let lives: Int

    init(name: String, lives: Int) {
        self.name = name
        self.lives = lives
    }

    func printCurrentScore(currentScore: Int, highScore: Int) {
        print("\(name)'s score is \(currentScore). Their high score is \(highScore)")
    }
}


// SWIFT 3.0
// In Swift 3.0, all argument labels must be included
printAnimal(animal: "Dog", legCount: 4)
let p = Player(name: "Riley", lives: 3)
p.printCurrentScore(currentScore: 50, highScore: 110)

// SWIFT 2.2
// In Swift 2.2, argument labels must be included or omitted in exactly the following way
// given the definition of the various objects.
printAnimal("Dog", legCount: 4)
let p = Player(name: "Riley", lives: 3)
p.printCurrentScore(50, highScore: 110)

// In Swift 2.2, none of the following will work
printAnimal(animal: "Dog", legCount: 4)  // Extraneous argument label 'animal:' in call
let q = Player("Riley", lives: 3)  // Missing argument label 'name:' in call
p.printCurrentScore(50, 110)  // Missing argument label 'highScore:' in call

覆盖默认行为

对于任何方法或函数的任何参数,您都可以偏离语言的默认设置,尽管样式指南正确地警告您不要这样做,除非有充分的理由。

要在通常没有外部参数标签的位置添加外部参数标签 - 仅适用于Swift 2.2,因为Swift 3.0将默认为将外部标签分配给每个参数 - 或更改外部参数标签 - 适用于两个版本 - 在本地参数标签之前编写所需的外部参数标签:

func printAnimal(theAnimal animal: String, legCount: Int) {
    let legNoun = legCount == 1 ? "leg" : "legs"
    print("\(animal) has \(legCount) \(legNoun)")
}

printAnimal(theAnimal: "Dog", legCount: 4)

如果通常情况下需要外部参数标签但你想要去掉它,可以使用特殊的外部参数标签 _

func printAnimal(animal: String, _ legCount: Int) {
    let legNoun = legCount == 1 ? "leg" : "legs"
    print("\(animal) has \(legCount) \(legNoun)")
}

// SWIFT 3.0
printAnimal(theAnimal: "Dog", 4)

// SWIFT 2.2
printAnimal("Dog", 4)

这些“默认覆盖”适用于任何方法或函数,包括初始化器。

1
非常感谢!既然您已经描述了规则,有没有可能给我们一个“为什么”呢?就是说,为什么?是只有我觉得这些语法规则完全没有任何意义吗? - Nostalg.io
2
嗯,真正的原因只能来自实施这些指南的核心Swift团队。一些想法:这些规则相当明显地告诉我们如何将Objective-C的方法名转换为Swift。此外,如果有关系的话,它们比Swift 2.0及以下版本更简单(以前,方法和函数的规则略有不同)。 - ravron
@gondo:你会很高兴看到,在Swift 3.0中,规则正在改变。请查看编辑答案末尾的注释。 - ravron
@RileyAvron 哎呀,也许应该将注释上移?毕竟,无论使用哪个版本,任何人都可以开始标记他们的第一个参数。当Swift 3完全发布后,新程序员将不需要改变他们的习惯。 - Gerald
@Gerald 好建议。我已经将答案重点放在了 Swift 3.0 特性上。 - ravron
显示剩余3条评论

12

这是我通过阅读相对稀少的文档和实际试验所能搜集到的信息:

  • 初始化方法总是需要标签。因为标签可以清楚地表明你要调用哪个初始化方法。否则,就会出现这种情况:

    FooBar(foos: 5)
    

    还有这个:

    FooBar(bars: 5)
    

    看起来完全相同:

    FooBar(5)
    

    在Swift中,只有init方法具有相同的名称但可能有不同参数的情况 - 这种情况在其他地方都不存在。这就是为什么...

  • 函数、方法等(除了init方法之外的任何内容)都会省略第一个标签 - 这是为了风格统一并且避免重复乏味。例如,而不是

    aDictionary.removeValueForKey(key: "four")
    

    我们有这个:

    aDictionary.removeValueForKey("four")
    

    仍需使用具有两个参数的函数,使其参数不含歧义且易于阅读。 因此,不要使用

    anArray.insert("zebras", 9)
    

    我们有一个更易于阅读的表单:

    anArray.insert("zebras", atIndex: 9)
    

看起来好多了。在我参加WWDC时,这被吹捧为Swift的一项功能:Java风格的现代化、简短的参数,而不会牺牲可读性。这也使得从Objective-C过渡变得容易,如Bryan Chen的回答所示。


2
实际上,函数默认情况下根本没有任何外部标签。 - radex
+1 好答案 - '第一个标签忽略特性'的唯一问题是像这样的方法:func insertLocation(lat : Double, lon : Double) { ... 然后被这样调用:insertLocation(2.5, lon: 168.3)。修复方法将是将其称为 insertLocationWithLat,但那样它就比需要的长。是否有任何方法可以禁用方法上的第一个标签省略特性? - Robert
有没有办法禁用方法中省略第一个标签的功能?-> 是的,使用 #,请参考我的答案。 - Robert

8

在调用方法时,您可以在标签之前使用#,使参数标签变为必需的。

例如:

func addLocation(latitude : Double, longitude : Double) { /*...*/ }
addLocation(125.0, -34.1) // Not clear

可以这样改进:
func addLocation(#latitude : Double, #longitude : Double) { /*...*/ }
addLocation(latitude: 125.0, longitude: -34.1) // Better

(来自WWDC 2014 - 416 - 构建现代框架,第15分钟)


Stack Overflow的语法高亮似乎存在一个bug。 - Robert
谢谢,"#"正是我要找的! - beetree

4
这只是让 ObjC 方法在 Swift 中看起来更好看。 文档

实例方法

方法的本地和外部参数名称

具体而言,Swift默认将方法中的第一个参数名赋予一个本地参数名,并且默认情况下将第二个及之后的参数名都赋予本地和外部参数名。这种约定与编写Objective-C方法时您熟悉的典型命名和调用约定相匹配,并且可以进行表达性强的方法调用,而无需限定您的参数名称。

...

上述默认行为意味着Swift中的方法定义采用与Objective-C相同的语法风格编写,并以自然、表达性强的方式进行调用。

自定义初始化

本地和外部参数名称

然而,初始化程序在其括号之前没有标识函数名称的功能,就像函数和方法一样。因此,初始化程序的参数名称和类型在确定应调用哪个初始化程序方面发挥了特别重要的作用。由于这一点,如果您未提供外部名称,则Swift为初始化程序中的每个参数提供自动外部名称

例如,对于这个ObjC类:

@interface Counter : NSObject

@property int count;

- (void)incrementBy:(int)amount numberOfTimes:(int)numberOfTimes;

@end

它用Swift编写的

class Counter {
    var count: Int = 0
    func incrementBy(amount: Int, numberOfTimes: Int) {
        count += amount * numberOfTimes
    }
}

调用 ObjC 版本

[counter incrementBy:10 numberOfTimes:2];

和 Swift 版本

counter.incrementBy(10, numberOfTimes:2)

你可以看到它们几乎相同


2
这似乎不是一个准确的比较。通常的 Objective-C 实现中,方法名会包含第一个参数 - incrementByAmount:(int)amount numberOfTimes:(int)numberOfTimes。我没有看到太多使用 Swift 的写法,如 incrementByAmount(amount: Int, numberOfTimes: Int) - Ben Packard

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