编程语言特性:函数

栏目: Python · 发布时间: 6年前

内容简介:标题为啥用函数不用方法?方法和函数的区别在我看来,无非是方法多了一个参数,该参数指代方法的宿主,所以方法是函数的延伸,一个特例。编程语言的函数原型来自于数学,源远流长,在广大人群中已经根深蒂固,而且它的表意也是非常明确的,有未知数,视同输入,有函数值,视同输出。总而言之,我偏好函数。函数本身是一个虚拟的抽象概念,而我们现在做的事就是在抽象中进一步抽象,也就是抽象的抽象。在我看来,抽象是简化的同义词,都是试图提取重要的共性,忽略不重要的细节。抽象的产物的我们一般用模型指代,所以整个流程我称之为建模。这个特性

标题为啥用函数不用方法?方法和函数的区别在我看来,无非是方法多了一个参数,该参数指代方法的宿主,所以方法是函数的延伸,一个特例。编程语言的函数原型来自于数学,源远流长,在广大人群中已经根深蒂固,而且它的表意也是非常明确的,有未知数,视同输入,有函数值,视同输出。总而言之,我偏好函数。

2. 建模

函数本身是一个虚拟的抽象概念,而我们现在做的事就是在抽象中进一步抽象,也就是抽象的抽象。在我看来,抽象是简化的同义词,都是试图提取重要的共性,忽略不重要的细节。抽象的产物的我们一般用模型指代,所以整个流程我称之为建模。

2.1 函数的特性

2.1.1 都具有输入输出

这个特性已经被 程序员 烂熟于心,以至于在这里显得有些多余,正是因为他的普适,而更加说明这是不可或缺的一个重要组成元素。

输入是已知的,而输出是未知的,是我们需要求索的答案。函数经常被看做是一个黑盒子,我们把输入放进去,输出就会自动出来,而黑盒子里是什么,如果你只是一个使用者,则无需关心。这种方式极大的简化函数调用者的开发难度。

graph LR
输入-->函数
函数-->输出
复制代码

这里,可能有人说了,有些函数没有输出,“这里没有输出”意指函数没有返回值,有些编程语称也把这些没有返回值的函数称为过程,但是我这里的输出再次重申为不仅限于函数返回值的输出,而包括其他形式的输出,比如函数副作用,产生对外部状态的改变的行为也统称为输出,如果一个函数封闭式的内部改变,只进不出,虽不能否定其存在,但是从实用角度来讲毫无意义。输入同理,也不应该理解为狭义的编程语言函数的参数列表,而应该理解为外部状态的输入。

通常输入的具体实现是一个参数列表,该参数列表至少包含每个参数的变量名称和类型,如果是弱类型语言,类型也可省略;或者再不济,函数内部也至少提供访问到参数的路径,这样连整个参数列表都可以省略,比如 js 中函数内部内置变量 arguments 就可以访问到调用者所有实际参数,而不需要任何形式参数的声明;再或者这个函数没有参数列表,但这不代表,它没有输入,它有隐式输入,例如函数代码可以访问到某些外部状态,比如获取当前时间,我们也认为这是输入的一种其他形式 。

而输出不需要任何名称标识,但不是绝对的,比如 go 语言提供了返回值名称声明,方便了在函数多返回值时内部直接赋值,所以只能说返回标识不是必要的。强类型语言可能会需要提供返回类型声明,而弱类型语言连返回类型都可以省略。

接下来,我们来看看不同编程语言的一个函数应该长啥样。

go

package main

import "fmt"

func main() {
    result := foo("medivh")
    fmt.Println("result: " + result);
}

func foo(name string) string {
    fmt.Println("hello " + name)
    return name
}
复制代码

python

def foo(name):
    print("hello " + name)
    return name;

result = foo("medivh");
print("result: " + result)
复制代码

javascript

function foo(name) {
    console.log('hello ' + name);
    return name;
}

var result = foo("medivh")
console.log("result:", result);
复制代码

java

class Main {
    public static void main(String[] args) {
        String result = foo("medivh");
        System.out.println("result: " + result);
    }

    static String foo(String name) {
        System.out.println("hello " + name);
        return name;
    }

复制代码

c#

using System;

class Program
{
    static void Main(string[] args)
    {
        string result = Foo("medivh");
        Console.WriteLine("result: " + result);
    }

    static string Foo(string name)
    {
        Console.WriteLine("hello " + name);
        return name;
    }
}
复制代码

以上代码都是绝对可运行的,本人亲测!同时安利一个好用的工具,可以在本地搭建服务运行多种编程语言代码,它的名字是 nodebook 。我这里的代码即是使用该 工具 测试运行完成。

我们可以观察 foo 函数的定义,基本上所有编程语言语法都大同小异,都有参数列表和函数名称构成函数签名,强类型语言普遍有返回类型声明,而弱类型语言普遍没有返回类型声明,因为多此一举。那么这么多的表现形式,我们该怎么分析?记住下面这句话:

记剑意,不要记剑招。

剑意是其本质,是种子,可以诞生无穷无尽的剑招。 所以这里的剑意就是一个函数一定要有访问输入和产生输出的机制。

2.1.2 重载

事实证明重载不是必要的,例如 go 就没有重载,我不知道是 go 没来得及实现,还是设计构想中就没有重载这项,不过现在使用 go 语言编程的人也没有说因为缺少这一特性而而导致严重的问题出现,顶多是不习惯,不顺手。

何为重载?重载就是复用相同的函数名称,但是参数列表不同,进而函数签名不同,从而提供可靠的差异性。要知道一句名言,我说的,如下:

编程最难的事是命名,几乎占编程 80% 的时间。

为啥命名这么难,因为一个名字与其使用环境有着强关联,名字就是背后复杂逻辑简化的代名词,你能想出好的名字,也就意味着你理解背后的逻辑。所以名字只是一个表征,反映了你对程序的理解程度。我相信对于一个你十分熟悉的程序编写,命名的效率应该会很快。但是名字也是稀缺资源,同一个命名空间下,名字是用一个少一个,好用的名字不多,你只能在几乎有限的名字池中筛选一个最合适的。而对于同一功能不同版本,命名更是难上加难,重载非常完美的解决这个问题,只要参数列表不同,重用相同的函数名称是可能的。

重载的缺点是啥?是由参数列表的微小差异带来的近似混淆。由于参数列表差异微小,致使调用者稍不留神,就编写错误的调用代码。还有就是兼容类型的处理,特别强调隐式转换带来的坑。

还有,弱类型语言大多数是没有重载的,因为参数列表没有强类型制造差异,导致程序执行时无法识别具体调用的目标是谁,当然也有意外, python3 通过特有的机制去实现重载,比如使用装饰器 functools.singledispatch 。而强类型语言重载基本上是标配。具体编程语言重载示例如下所示:

java

class Main {
    public static void main(String[] args) {
        foo();
        foo("medivh");
    }

    static void foo() {
        System.out.println("hello world");
    }

    static void foo(String name) {
        System.out.println("hello " + name);
    }
}
复制代码

c#

using System;

class Program
{
    static void Main(string[] args)
    {
        Foo();
        Foo("medivh");
        Console.ReadKey();
    }

    static void Foo()
    {
        Console.WriteLine("hello world");
    }

    static void Foo(string name)
    {
        Console.WriteLine("hello " + name);
    }
}
复制代码

2.1.3 闭包

闭包是什么?闭包是一种机制,一种行为,是函数使用方式的进一步发明与创造,它可以让函数携带状态。

我们知道,影响函数行为有两大因数,一是函数的参数列表,二是外部状态的访问,例如函访问全局作用域变量。但是函数本身是没有状态的,所以它只能依据前面提的两大因数的而产生行为变化。

那闭包做了什么,它可以让一个函数内置状态,也就是说闭包让函数携带了状态,依据这些状态,它的行为很可能相应发生变化,这种携带状态的函数可以称之为运行时函数。

闭包只能发生在函数内部,所谓的“闭”是指封闭在某一函数内部而且不外泄的意思,一般情况下是函数返回一个被构建好的函数,这个被构建出来的函数携带了构建函数产生的状态信息,即使是构建函数返回值之后,这些状态依然被保留下来,可谓是如影随行,除非函数引用为零,这些状态信息所占用的内存才会被释放掉。

闭包代码如下所示:

javascript

function fooConstructor(status){
    return function foo() {
        console.log("status: " + status);
    };
}

var foo = fooConstructor(47)
foo()
复制代码

python3

def fooConstructor(status):
    def foo():
        print("status: " + str(status))
    return foo

foo = fooConstructor(47)
foo()
复制代码

2.1.4 位置参数、可变参数、默认参数和可选参数

关于参数列表这里还有很多花样可以折腾,虽然增加了理解的复杂度,但是方便了函数调用者的使用。下面一一介绍。

位置参数是按照位置来传参的函数参数声明,它是必传参数,也就是严格按照顺序一一传入,少一个也不行,这种情况想必大家都一清二楚,是最常见的参数形式。示例代码如下:

go

package main

import "fmt"

func main() {
    foo("a", "b")
}

func foo(arg1 string, arg2 string) {
    fmt.Printf("argument list: arg1 = %s, arg2 = %s\n", arg1, arg2)
}
复制代码

可变参数顾名思义,也就是可以有不确定数量的参数声明,为了不产生二义性,它只能放在固定参数后面,通常在函数内部,以类数组的方式去访问。示例代码如下:

go

package main

import "fmt"

func main() {
    foo("a", "b", "c")
}

func foo(arg1 string, someArgs ... string) {
    fmt.Printf("argument list: arg1 = %s, arg2 = %s\n", arg1, someArgs)
}
复制代码

默认参数意为在函数调用之前会有默认值赋给参数变量,如果调用者,没传这个参数,则内部访问到就是默认参数。示例代码如下:

c#

using System;

class Program
{
    static void Main(string[] args)
    {
        Foo("medivh");
        Console.ReadKey();
    }

    static void Foo(string name, int age = 18)
    {
        Console.WriteLine("My name is " + name + ", and my age is " + age + " years old.");
    }
}
复制代码

可选参数意指可以传递也可以不传递的参数声明,细分可以划分为两种类型,一种称之为可变命名参数,也就是调用时可以通过指定名字赋值的参数声明,它可以在固定参数后以任意顺序指定,另一种称之为可选位置参数,它是以固定顺序传参的参数声明,它可以省略末尾的,但是不能省略中间或者排在前面的可选位置参数。可选参数通常会和默认参数配合使用,示例代码将以 Dart 举例,因为他的语法在我看来比较优雅,代码如下:

dart

void main() {
    foo("medivh", age:24);
    foo2("medivh", "female", 17);
}

// 可选命名参数
void foo(String name, {String sex = "male", int age, String hobby="code"}) {
    print("name: " + name + ", sex: " + sex + ", age: " + age.toString() + ", hobby: " + hobby);
}

// 可选位置参数
void foo2(String name, [String sex = "male", int age, String hobby="code"]) {
    print("name: " + name + ", sex: " + sex + ", age: " + age.toString() + ", hobby: " + hobby);
}
复制代码

3 总结

函数是编程语言不可或缺的模块,使用它通常是为了封装代码以达到复用的目的。这里为函数建造出的模型覆盖了多种编程语言函数的共有特性,这些特性是值得反复推敲,也可思考替代某一种特性的其他实现方式,只不过,沉淀至今,更好的替代方案会越来越难找寻了。


以上所述就是小编给大家介绍的《编程语言特性:函数》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Compilers

Compilers

Alfred V. Aho、Monica S. Lam、Ravi Sethi、Jeffrey D. Ullman / Addison Wesley / 2006-9-10 / USD 186.80

This book provides the foundation for understanding the theory and pracitce of compilers. Revised and updated, it reflects the current state of compilation. Every chapter has been completely revised ......一起来看看 《Compilers》 这本书的介绍吧!

MD5 加密
MD5 加密

MD5 加密工具

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器

html转js在线工具
html转js在线工具

html转js在线工具