为什么在 Swift 中函数调用需要参数名?

67

我在一个类中有这个函数:

func multiply(factor1:Int, factor2:Int) -> Int{
    return factor1 * factor2
}

我尝试使用以下代码调用函数:

var multResult = calculator.multiply(9834, 2321)

问题在于编译器希望它看起来更像这样:

var multResult = calculator.multiply(9834, factor2: 2321)

为什么第一个会导致错误?


1
看起来编译器认为这个函数是一个方法。 - CrimsonChris
该类是否继承自Cocoa类(例如NSObject)?它是否标记为@objc?如果是,编译器会假定它可能从Objective-C中调用,因此其方法必须符合Objective-C的方法调用/命名约定。 - Ken Thomases
2
不,它是一个根类(没有超类),并且没有标记为@objc。 - 67cherries
可以将第二个参数名设为可选,只需在 factor2 前面加上下划线 _,例如 func multiply(factor1:Int, _ factor2:Int) {...} - Hlung
1
如果您不喜欢输入参数名称,可以将func multiply(factor1:Int, factor2:Int)替换为func multiply(factor1:Int, _ factor2:Int)。这不是黑客行为,而是官方语言语法。 - Jacob R
6个回答

116
Swift 2.0更新:现在函数的行为与方法完全相同,并且默认情况下:
  • 第一个参数没有外部名称;
  • 其他参数具有与内部名称相同的外部名称。

除此之外,以下规则仍然适用,只是# 简写语法现在已经消失了。


这里有一个更通用的解答:在类外定义真正的函数时和在类中定义方法时,函数的行为不同。此外,init方法有一条特殊规则。


函数

假设您定义了以下内容:

func multiply1(f1: Double, f2: Double) -> Double {
    return f1 * f2
}

参数名仅在函数内部本地有效,不能在调用函数时使用:

multiply1(10.0, 10.0)

如果想要在调用函数时强制使用命名参数,可以这样做。为每个参数声明添加它的外部名称前缀。这里,f1 的外部名称是 f1param,而对于 f2,我们使用简写方式,在其前面加上 # 以表示本地名称也将用作外部名称:

func multiply2(f1param f1: Double, #f2: Double) -> Double {
    return f1 * f2
}

那么,必须使用命名参数:

multiply2(f1param: 10.0, f2: 10.0)

方法

对于方法而言,情况有所不同。默认情况下,除了第一个参数之外的所有参数都被命名,正如你所发现的那样。假设我们有以下内容,并考虑 multiply1 方法:

class Calc {
    func multiply1(f1: Double, f2: Double) -> Double {
        return f1 * f2
    }
    func multiply2(f1param f1: Double, f2: Double) -> Double {
        return f1 * f2
    }
    func multiply3(f1: Double, _ f2: Double) -> Double {
        return f1 * f2
    }
}

然后,您必须使用第二个(和随后的,如果有)参数的名称:

let calc = Calc()
calc.multiply1(1.0, f2: 10.0)

您可以通过为第一个参数提供外部名称来强制使用命名参数,就像对函数一样(或者如果您想要使用与其本地名称相同的外部名称,可以在其本地名称前加上#)。然后,您必须使用它:

calc.multiply2(f1param: 10.0, f2: 10.0)

最后,你可以为其他参数声明一个外部名称_,表示你希望在调用方法时不使用命名参数,像这样:

calc.multiply3(10.0, 10.0)

互操作性注释: 如果您在 class Calc 前缀中添加 @objc 注释,则可以从 Objective-C 代码中使用它,并且它等效于此声明(请查看参数名称):

@interface Calc
- (double)multiply1:(double)f1 f2:(double)f2;
- (double)multiply2WithF1param:(double)f1 f2:(double)f2;
- (double)multiply3:(double)f1 :(double)f2;
@end

初始化方法

init方法的规则与其他方法略有不同,所有参数默认都有外部名称。例如,以下代码是有效的:

class Calc {
    init(start: Int) {}
    init(_ start: String) {}
}

let c1 = Calc(start: 6)
let c2 = Calc("6")

在这里,对于接受 Int 的重载,您必须指定 start:,但是对于接受 String 的重载,您必须省略它。

互操作性注意事项:此类将像这样导出到Objective-C:

@interface Calc
- (instancetype)initWithStart:(NSInteger)start __attribute__((objc_designated_initializer));
- (instancetype)init:(NSString *)start __attribute__((objc_designated_initializer));
@end

闭包

假设你定义了一个如下的闭包类型:

typealias FancyFunction = (f1: Double, f2: Double) -> Double

参数名称的行为与方法中的参数名称非常相似。在调用闭包时,除非您显式地将外部名称设置为'_',否则必须为参数提供名称。

例如,执行以下闭包:

fund doSomethingInteresting(withFunction: FancyFunction) {
    withFunction(f1: 1.0, f2: 3.0)
}

作为一个经验法则:即使你不喜欢它们,你也应该尽可能使用命名参数,特别是当两个参数具有相同的类型时,以消除歧义。我还会主张至少为所有的IntBoolean参数命名。

1
我已经添加了互操作性注释和“init”方法的规则。 - Jean-Philippe Pellet
2
很棒的解释。谢谢! - sivabudh
1
还有一件有趣的事情,如果您想在使用方法时显示第一个参数名称,则必须像这样定义它:func method(#firstParameter: Type, secondParameter: Type)这样做,当调用函数时,它将显示为:self.method(firstParameter: 3, secondParameter: 5)如果您不添加 #,它将显示为:self.method(3,secondParameter: 5) - diegomen
2
但是这个设计背后的理念是什么?它是试图防止我们混淆参数顺序吗?如果是这样,为什么函数没有这样的限制呢? - MK Yung
这比官方文档更好。感谢您提供了这个伟大的、出色的答案! - Bastian
显示剩余6条评论

4
在函数调用中,参数名被称为关键字名称,它们的起源可以追溯到Smalltalk语言。
类和对象经常从其他地方重复使用,或者是非常庞大复杂系统的一部分,很长一段时间内不会得到积极的维护关注。
在这些情况下,提高代码的清晰度和可读性非常重要,因为当开发人员处于截止日期压力下时,代码通常最终成为唯一的文档。
为每个参数赋予一个描述性的关键字名称使维护人员能够通过查看函数调用快速了解函数调用的目的,而无需深入研究函数代码本身。它使参数的隐含含义变得明确。
最新采用函数调用中参数关键字名称的语言是Rust (link) - 被描述为“运行速度极快、防止段错误并保证线程安全的系统编程语言”。
高稳定性系统需要更高的代码质量。关键字名称允许开发和维护团队更有机会避免并捕获发送错误参数或调用参数顺序不正确的错误。

Smalltalk程序员喜欢使用冗长和描述性的语言,而不是简洁和无意义的语言。他们可以这样做,因为他们的集成开发环境会为他们完成大部分此类输入。


这是一个很好的解释。关于语言设计,这并不能证明增加冗长性是有必要的,因为我们可以通过 Alt + 左键单击来查看参数名称和方法描述。 - Cosmin

2

由于您在示例代码中使用了calculator.multiply(),我认为这个函数是calculator对象的一个方法。

Swift从Objective-C继承了很多东西,这就是其中之一:

在Objective-C中,您会这样做(假设):

[calculator multiply:@9834 factor2:@2321];

在Swift中的等效写法为:(参考链接)

calculator.multiply(9834, factor2:2321);

我打算接受这个答案。我查看了文档,他们使用的示例比我的更有意义。 - 67cherries
2
对于没有使用过Objective C的人来说,这可能会很困惑 :) - Kokodoko
@Kokodoko 对于那些已经使用Objective-C工作了5年的人来说,这更加令人困惑 :P - Ankit Srivastava
尽管此页面上提供了所有的解释,但仍然不太合理...因为函数名已经包含了第一个参数,所以可以省略它吗?(在乘法示例中并没有这样做)。另外,如果Scratch省略第一个参数类型是因为它太令人困惑了,那么为什么他们不省略所有类型呢?这难道不会更加令人困惑吗?抱歉,我只是在发牢骚。 - Kokodoko

1
因为你的“multiply”函数是一个方法,就像Objective-c一样,方法中的参数是名称的一部分。
例如,你可以这样做。
class Calculator {

    func multiply(factor1:Int, factor2:Int) -> Int{
        return factor1 * factor2
    }

    func multiply(factor1:Int, factor2:Int, factor3:Int) -> Int{
        return factor1 * factor2 * factor3
    }

}

这里有两种不同的方法,分别为multiply(factor2)和multiply(factor2 factor3)。

这个规则只适用于方法,如果您像在类外声明函数一样声明它,则函数调用不需要参数名。


但这仍然无法解释为什么只有第一个参数名可以省略?我为什么不能为所有参数添加参数名以保持可读性?这似乎是来自旧版Objective C的经典代码,其中参数是函数名称的一部分。这导致函数名称非常长。如果我们想学习Swift,仍然不得不了解Objective C的工作方式,这相当奇怪。 - Kokodoko
第一个参数被省略了,因为Cocoa中方法的标准名称看起来像这样:func multiplyFactor1(factor1: Int, factor2:Int) {}因此,在调用该方法时,省略了第一个参数以避免重复使用它的名称。myCalculator.multiplyFactor1(2, facto2:2)myCalculator.multiplyFactor1(factor1: 2, facto2:2)更易读。 - Daniel

0
关于将不返回值的方法作为参数传递的说明:
func refresh(obj:Obj, _ method: (Obj)->Void = setValue) {
    method(element)
}
func setValue(obj:Obj){
    obj.value = "someValue"
}
refresh(someObj,setValue)

0

原因是历史的。这是在Smalltalk中的工作方式,并且它延续到了它的后代。Squeak,ScratchBlockly,Objective C和Swift。

儿童语言(Squeak,Scratch和Blockly)坚持这种方式,因为初学者往往会在arity和参数顺序方面遇到困难。这就是Smalltalk最初采用这种方式的原因。我不知道ObjC和Swift为什么决定采用这种约定,但他们确实这样做了。

Scratch example program


1
Squeak不是一个“儿童语言”。它是一个功能齐全的语言、虚拟机、GUI环境和IDE,人们已经使用它来实现像Scratch、E-Toys和Dr-Geo这样的适合儿童的系统,以及用于教授高中数学的数学函数可视化器。它还被用于实现:Seaside Web框架——一个摆脱了所有与用户在浏览器中使用“后退”和“前进”按钮有关问题的框架;DabbleDB;等等。 - Euan M

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