Swift函数

Function概述

Swift中的function和其余语言(譬如C)中的function组成部分差不多,都包括:返回类型(若无返回值,则为void)、函数名、形参、函数体(逻辑代码)。只是Swift中function的长相和其他语言不太一样,它把返回值放在定义行的尾部,如下是一个包含包含返回值、形参的function:

func welcome(name: String) -> String {
    let ret = "Welcome, " + name + "!"
    println(ret)
    return ret;
}

Function的返回值和参数

Function可以有多个返回值

相对于OC,Swift中有一个新成员叫Tuple,它的主要应用场景就是作为函数/方法的参数或返回值。当function的返回值是一个tuple类型时,就意味着函数/方法可以同时返回多个值(这在OC中一般使用NSDictionary或struct),定义和使用含有多个返回值的函数比较容易,如下:

func minMax(array: [Int]) -> (min: Int, max: Int) {
    var cMin = array[0]
    var cMax = array[0]
    for index in 1..<array.count {
        let value = array[index]
        if value < cMin {
            cMin = value
        } else if (value > cMax) {
            cMax = value
        }
    }
 return (cMin, cMax)
}                  

External Parameter Name

上文出现的几个函数实体都含有参数,各个参数都有自己的名字,但是这些参数名都只能在函数内部使用(C语言中叫这个为实参),在调用时,这些参数对于调用者来说是没有意义的,调用者只要保证传入的参数值和定义式中的值意义对应即可,譬如上文的welcome函数的调用式可以是welcome("张不坏"),minMax函数的调用式可以是let (xMin, xMax) = minMax([7,8,9,3,4,5])

但是,在调用函数时保持传入的参数次序和函数定义式中的参数次序的一致性是一件挺麻烦的事情,不够优雅,并且不直观,譬如对于这样的调用welcome("张不坏"),一段时间之后再看这行代码,会想『卧槽,welcome中的参数是干嘛的』;且参数越多,可读性越差,不得不去查看函数原型才能理解各个参数各自的用处;再回到OC上,会很快发现OC中根本不存在这样的问题,因为在OC中,参数名已经成为了函数名的一部分,非常直观(呃,虽然导致函数名很长),即便不看函数原型,我们也能直观知道每个参数的作用。

Swift继承了OC的优点(可能有人认为这是缺点,那么可以理解为这是OC给Swift带来的历史包袱),对welcome函数定义式稍作修改,我们在调用上述的welcome函数时就变成了welcome(name: "张不坏"),welcome定义式修改如下:

func welcome(name name: String) -> String {
    let ret = "Welcome, " + name + "!"
    println(ret)
    return ret;
}

其中,参数列表name name: String中的第一个nameexternal parameter name,第二个namelocal parameter name,抽象描述如下:

func someFunction(externalParameterName localParameterName: SomeType) {
// function body goes here, and can use localParameterName
// to refer to the argument value for that parameter
}

Swift要求,如果在函数/方法定义式中为某个参数定义了external parameter name,那么在调用时就必须使用它,也就是说,经过这么一折腾后,welcome的调用式不能再写成这样welcome("张不坏"),必须写成welcome(name: "张不坏"),一段时间之后回头再看,会心一笑:『嗯,这个参数是表示「名字」』。

Shorthand External Parameter Names

有时候,external parameter name和local parameter name是一样的,譬如上述的welcome函数,在这种情况下,有一种快捷的写法:参数名只用写一遍,然后在参数名前加上一个#作为前缀,这就告诉Swift为该参数使用相同的external parameter name和local parameter name。如下:

func welcome(#name: String) -> String {
    let ret = "Welcome, " + name + "!"
    println(ret)
    return ret;
}

到了这里就自然产生了一个问题:如何看待external parameter name呢?在什么场合下使用,在什么场合下不宜使用呢?希望以后能回答这个问题吧!

默认形参值

默认形参值并不是Swift的特性,在其他很多语言(譬如Python)中早已经用到了。可以为任意形参定义默认值以作为函数定义的一部分。如果定义了默认值,那么调用函数时就可以省略该形参。默认形参的定义原则一般是『把具有默认值的形参放置在形参列表末尾』,这样将确保所有函数调用都是用顺序相同的无默认值实参,并让每种情况下清晰地调用相同的函数。譬如:

// 连接两个字符串并返回
func join(string s1: String, toString s2: String, withJoiner joiner: String = " ") -> String {
    return s1 + joiner + s2
}

 上述join函数的第三个参数joiner具有默认值,其调用式如下:

// 不使用默认参数
let newStr1 = join(string: "张", toString: "不坏", withJoiner: "-")
// 使用默认参数
let newStr2 = join(string: "张", toString: "不坏")

 

在对待『具有默认值的形参』时,Swift认为:

In most cases, it is useful to provide an external name of any parameter with a default value. This ensures that the argument for that parameter is clear in purpose if a value provided when the function is called.

简而言之,Swift认为,一般的形参是否有external parameter name它不管,但是对于具有默认值的形参,它认为具有一个external parameter name是非常有必要的,因为这样会让逻辑更清晰。也因此,你若不手动为『具有默认值的形参』指明external parameter name,Swift会自动帮你补上(补上一个与local parameter name一样的external parameter name)。

个人认为,Swift说得有道理!
P.S:试想一下,当函数中有多个『具有默认值的形参』但它们都没有external parameter name时会发生什么?

可变形参

一个可变形参可接受零个或多个指定类型的值。Swift的function中定义形参简单得令人发指,只需要在形参的类型后面插入...即可。关于函数的可变形参,说明如下:

  • 一个函数最多只能有一个可变形参
  • 可变形参可以对应任意多个输入值,但这些输入值必须和指定的类型一致;
  • 传递至可变形参的值在函数body里是以适当形式的数组存在的;
  • 当一个函数既包括可变形参又包括具有默认值的形参时,参数列表的参数排列必须是普通形参 -- 具有默认值的形参 -- 可变形参

如下:

func welcome(header: String = "welcome", #names: String...) {
    var str = header + ","
    for name in names {
        str += " "
        str += name
    }
    println(str)
}
welcome(names:"张无忌", "张不坏", "张全蛋")
// 输出:welcome, 张无忌, 张不坏, 张全蛋        

虽然Swift文档没有说明,但经过个人使用体验发现,当函数中同时含有可变形参具有默认值的形参时,最好为可变参数指明external parameter name,否则可能会产生一些问题。

常数形参和变量形参

函数的形参默认是常量。即任何试图在函数body内改变函数形参的值的行为都会引发编译错误。

P.S:根据我的理解,函数处理形参都会有一个拷贝的过程,对于值类型,进行值拷贝,对于引用类型,进行引用拷贝;但无论如何,默认情况下拷贝得到的都是『常量副本』;

但是有时候,函数有一个形参值的『变量副本』是非常有用的。您可以指定一个或多个形参作为变量形参,从而避免在函数内部为自己定义一个新的变量,具体做法是在参数名称前用关键字var定义变量参数,如下:

func addPrefix(var string: String, prefix: String) -> String {
    string = prefix + string
    return string
}
 
// 调用addPrefix
let str1 = "不坏"
var str2 = addPrefix(str1, "张")
println(str1) // 输出:不坏
println(str2) // 输出:张不坏

In-Out形参

上文讨论了『变量形参』,『变量形参』的本质是对传入的参数进行拷贝(至于值拷贝还是引用拷贝先不谈),得到的拷贝是变量值,也就是它是可以修改的,可以为它赋上别的值,也只是在函数内部修改,不会改变形参本身的值。

class Student: NSObject {
    var name: String?
    init(name: String) {
        self.name = name
    }
}
 
func changeName(var student: Student) {
    student = Student(name: "no-one") // student是一个变量(指针),可以修改
}
let jason = Student(name: "Jason") changeName(jason) println("name of the student: \(jason.name!)") // 输出: name of the student: Jason // jason没有改变

但有时候我们希望jason被修改,处理完后它不再是原来的对象;

这就要引入in-out形参,在定义形参时添加inout关键字即可指明in-out形参,in-out形参有一个传递至函数的值,由函数修改,并从函数返回来替换原来的值。

值得一提的是,in-out参数不能有默认值,如果标记参数为inout,则该参数不能被同时标记为var或let;并且只能传递一个变量作为in-out形参对应的实参。不能传递一个常量或者字面量作为实参,因为常量和字面量不能被修改。此外,当把变量作为实参传递给in-out形参时,需要在变量前添加&符号,以表明它可以被函数修改。

举个栗子,定义一个函数,用来产生NSError对象:

// 产生一个NSError对象
func generateError(inout error: NSError?) {
    error = NSError(domain: "test-domain", code: 404, userInfo: nil)
}
 
class ViewController: UIViewController {
 
    override func viewDidLoad() {
        super.viewDidLoad()
 
        // 调用generateError
        var error: NSError?
        generateError(&error)
 
        if error != nil {
        println("error domain: \(error!.domain)")
        // 输出:error domain: test-domain
        }
    }
}            

是不是有种熟悉的感觉呢?呵呵,这类似的代码在OC中太常见了,如果用OC将上述代码写一遍,则是这样的:

// 产生一个NSError对象
void generateError(NSError **error) {
    *error = [NSError errorWithDomain:@"test-domain" code:404 userInfo:nil];
}
 
- (void)viewDidLoad {
 
    [super viewDidLoad];
    NSError *error;
    generateError(&error);
    if (error) {
        NSLog(@"error domain: %@", error.domain);
        // 输出:error domain: test-domain
    }
}        

通过这个示例,应该可以明白Swift中in-out形参的存在意义了吧!

Function类型

每一个函数都有特定的类型,函数的类型由函数的形参和返回类型决定,如下有两个函数:

func addTwoInts(a: Int, b: Int) -> Int {
    return a + b
}
 
func multiplyTwoInts(c: Int, d: Int) -> Int {
    return c * d
}

显然,这两个函数的形参和返回值完全一样,都是(Int, Int)->Int,所以可以说这两个函数是一个类型的参数。

返回值类型一样还比较容易理解,形参一样如何理解呢?

形参类型

(a: Int, b: Int)(c: Int, d: Int)显然属于同类型形参了;那么(#a: Int, #b: Int)(#c: Int, #d: Int)呢?示例演示表明:Swift认为它们仍然是同一种类型,仍然是(Int, Int)->Int类型,如下:

// 两个整数相加
func addTwoInts(#a: Int, #b: Int) -> Int {
    return a + b
}
// 两个整数相乘
func multiplyTwoInts(#c: Int, #d: Int) -> Int {
    return c * d
}
 
// 测试
let aFunction1:(Int, Int)->Int = addTwoInts
let result1 = aFunction1(4, 5)
 
let aFunction2:(Int, Int)->Int = multiplyTwoInts
let result2 = aFunction2(4, 5)
 
println("result1 = \(result1)") // 输出:result1 = 9
println("result2 = \(result2)") // 输出:result2 = 20

我比较疑惑的是Swift是如何处理函数重名问题的。同一个作用域下的函数重名的情况可能有两种:

  • 函数名相同,但类型不同;
  • 函数名相同,且类型相同;

『名字相同,但类型不同』的重名函数

// 定义三个函数,它们的『函数名相同,但类型不同』
 
// 1号sayHello
func sayHello() {
    println("Hello")
}
 
// 2号sayHello
func sayHello() -> Bool {
    println("Hello")
    return true
}
 
// 3号sayHello
func sayHello(name: String) {
    println("Hello, \(name)")
}

如此定义不会有任何编译器错误,Swift是允许『名字相同,但类型不同』的重名函数存在同一个作用域内的;

不过调用『名字相同,但类型不同』的函数有一点点问题,上面的3号sayHello函数相对来说比较清晰,执行sayHello("张不坏")这条语句时Swift只会去调用3号sayHello;但对于1号sayHello2号sayHello函数的调用就不是那么清晰了,执行sayHello()时Swift会调用哪个呢?1号sayHello还是2号sayHello?编译器也不知道,所以会产生这样的编译错误:Ambiguous use of 'sayHello'。就没有办法了吗?No,有办法,换个姿势即可:

let sayHello1: (Void)->Void = sayHello
let sayHello2: (Void)->Bool = sayHello
// 调用1号sayHello
sayHello1()
// 调用2号sayHello
let ret = sayHello2()

但这无非是给用户(函数使用者)带来麻烦。所以笔者认为,虽然Swift对函数定义的名称限制比较少,但在同一作用域的定义函数时,尽量不要定义『调用式可能相同』的重名函数,否则会给用户造成困惑。

『名字相同,且类型相同』的重名函数

显然,func sayHello(name: String) {}func sayHello(welcomeWord: String) {}这两个函数是没办法在同一个作用域同时定义的,但是func sayHello(name: String) {}却可以和func sayHello(#name2: String) {}同时出现在一个作用域。如下:

// 3号sayHello
func sayHello(name: String) {
    println("Hello, \(name)")
}
 
// 4号sayHello
func sayHello(#name2: String) {
    println("Hello,")
}

虽然,3号sayHello4号sayHello的函数类型都是(String)->void,但是考虑到它们的调用式不同,Swift也允许它们同时出现在同一个作用域内,但这依然带来了一些问题,譬如如下这几行代码无法通过编译:

let sayHello3:(String)->Void = sayHello
sayHello("张不坏") // 调用「3号sayHello」

这几行代码同样会产生编译错误:Ambiguous use of 'sayHello'

总结:虽然Swift对函数命名非常宽容,但在定义重名函数时一定要注意Ambiguous use of 'xxoo方法'这个问题。

使用函数类型

在Swift中可以像任何其他类型一样使用『函数类型』,譬如可以定义一个常量或变量为『函数类型』(其实上文中已经多次使用了『函数类型』)。

使用「函数类型」定义变量

举一个稍微复杂一点的栗子,当函数类型中含有可变参数时:

// 5号sayHello
func sayHello(welcomeWord: String = "Hello", toWho names: String...) {
    var words = welcomeWord
    for name in names {
        words += ", "
        words += name
    }
    println(words)
}
// 使用「5号sayHello」
let sayHello5: (String, String...) -> Void = sayHello
sayHello5("早上好", "张无忌", "张不坏", "张全蛋")
// 输出:早上好,张无忌,张不坏,张全蛋    

 除此之外,还可以:

  • 使用「函数类型」修饰形参;
  • 使用「函数类型」修饰返回值;

这个比较简单,暂时没有啥使用上的困惑,暂时略过吧,以后有疑问再补充!

除此之外,Swift中还有「嵌套函数」这个概念,这个概念在很多其他方法中也存在,使用比较简单,只是尚且不清楚在Swift中有啥「最佳实践」?

posted @ 2015-06-29 17:47  脸大皮厚歌  阅读(377)  评论(0编辑  收藏  举报