编程语言特性:函数

栏目: 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 总结

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


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

查看所有标签

猜你喜欢:

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

深度探索C++对象模型

深度探索C++对象模型

[美] Stanley B. Lippman / 侯捷 / 华中科技大学出版社 / 2001-5 / 54.00元

这本书探索“对象导向程序所支持的C++对象模型”下的程序行为。对于“对象导向性质之基础实现技术”以及“各种性质背后的隐含利益交换”提供一个清楚的认识。检验由程序变形所带来的效率冲击。提供丰富的程序范例、图片,以及对象导向观念和底层对象模型之间的效率测量。一起来看看 《深度探索C++对象模型》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

MD5 加密
MD5 加密

MD5 加密工具

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

html转js在线工具