Go 函数是“一等公民”的理解

函数(function)作为现代编程语言的基本语法元素存在于支持各种范式(paradigm)的主流编程语言当中。无论是命令式语言 C、多范式通用编程语言 C++,还是面向对象编程语言 Java、Ruby,亦或是函数式语言 Haskell、动态脚本语言 Python、PHP、JavaScript,函数这一语法元素都是当仁不让的核心。

Go 语言以“成为新一代系统级语言”为目标而诞生,但在演化过程中,逐渐演化成了面向并发、契合现代硬件发展趋势的通用编程语言。Go 语言中没有那些典型的面向对象语言的语法,比如类、继承、对象等。Go 语言中的方法(method)本质上亦是函数的一个“变种”。因此,在 Go 语言中,函数是唯一一种基于特定输入、实现特定任务并可反馈任务执行结果的代码块。本质上我们可以说 Go 程序就是一组函数的集合。

和其他编程语言中的函数相比,Go 语言的函数具有如下特点:

  • 以“func”关键字开头;
  • 支持多返回值;
  • 支持具名返回值;
  • 支持递归调用;
  • 支持同类型的可变参数;
  • 支持 defer,实现函数优雅返回

更为关键的是函数在 Go 语言中属于“一等公民(first-class citizen)”。众所周知,并不是在所有编程语言中函数都是“一等公民”,本节中我就和大家一起来看看成为”一等公民“的函数都有哪些特质可以帮助我们写出优雅简洁的代码。

1. 什么是“一等公民”

关于什么是编程语言的“一等公民”,业界并没有教科书给出精准的定义。这里引用一下 wiki 发明人、C2 站点作者沃德·坎宁安(Ward Cunningham)对“一等公民”的诠释:

如果一门编程语言对某种语言元素的创建和使用没有限制,我们可以像对待值(value)一样对待这种语法元素,那么我们就称这种语法元素是这门编程语言的“一等公民”。拥有“一等公民”待遇的语法元素可以存储在变量中,可以作为函数传递给函数,可以在函数内部创建并可以作为返回值从函数返回。在动态类型语言中,语言运行时还支持对“一等公民”类型的检查。

基于上面关于“一等公民”的诠释,我们来看看 Go 语言的函数是如何满足上述条件而成为“一等公民”的。

1.1 正常创建

我们可以在源码顶层正常创建一个函数,如下面的函数 newPrinter:

1// $GOROOT/src/fmt/print.go 2func newPrinter() *pp { 3 p := ppFree.Get().(*pp) 4 p.panicking = false 5 p.erroring = false 6 p.wrapErrs = false 7 p.fmt.init(&p.buf) 8 return p 9}

1.2 在函数内创建

在 Go 语言中,我们可以在函数内定义一个新函数,如下面代码中在 hexdumpWords 函数内部定义的匿名函数(被赋值给变量 p1)。在 C/C++中我们无法实现这一点,这也是 C/C++语言中函数不是“一等公民”的原因之一。

1// $GOROOT/src/runtime/print.go 2func hexdumpWords(p, end uintptr, mark func(uintptr) byte) { 3 p1 := func(x uintptr) { 4 var buf [2 * sys.PtrSize]byte 5 for i := len(buf) - 1; i >= 0; i-- { 6 if x&0xF < 10 { 7 buf[i] = byte(x&0xF) + '0' 8 } else { 9 buf[i] = byte(x&0xF) - 10 + 'a' 10 } 11 x >>= 4 12 } 13 gwrite(buf[:]) 14 } 15 ... ... 16}

1.3 作为类型

我们可以使用函数来自定义类型,如下面代码中的 HandlerFunc、visitFunc 和 action:

1// $GOROOT/src/net/http/server.go 2type HandlerFunc func(ResponseWriter, *Request) 3 4// $GOROOT/src/sort/genzfunc.go 5type visitFunc func(ast.Node) ast.Visitor 6 7// codewalk: https://tip.golang.org/doc/codewalk/functions/ 8type action func(current score) (result score, turnIsOver bool)

1.4 存储到变量中

我们可以将定义好的函数存储到一个变量中,如下面代码中的 apply:

1// $GOROOT/src/runtime/vdso_linux.go 2func vdsoParseSymbols(info *vdsoInfo, version int32) { 3 if !info.valid { 4 return 5 } 6 7 apply := func(symIndex uint32, k vdsoSymbolKey) bool { 8 sym := &info.symtab[symIndex] 9 typ := _ELF_ST_TYPE(sym.st_info) 10 bind := _ELF_ST_BIND(sym.st_info) 11 12 ... ... 13 14 *k.ptr = info.loadOffset + uintptr(sym.st_value) 15 return true 16 } 17 ... ... 18}

1.5 作为参数传入函数

我们可以将函数作为参数传入函数,比如下面代码中函数 AfterFunc 的参数 f:

1$GOROOT/src/time/sleep.go 2 3func AfterFunc(d Duration, f func()) *Timer { 4 t := &Timer{ 5 r: runtimeTimer{ 6 when: when(d), 7 f: goFunc, 8 arg: f, 9 }, 10 } 11 startTimer(&t.r) 12 return t 13}

1.6 作为返回值从函数返回

函数还可以被作为返回值从函数返回,如下面代码中函数 makeCutsetFunc 的返回值就是一个函数:

1// $GOROOT/src/strings/strings.go 2func makeCutsetFunc(cutset string) func(rune) bool { 3 if len(cutset) == 1 && cutset[0] < utf8.RuneSelf { 4 return func(r rune) bool { 5 return r == rune(cutset[0]) 6 } 7 } 8 if as, isASCII := makeASCIISet(cutset); isASCII { 9 return func(r rune) bool { 10 return r < utf8.RuneSelf && as.contains(byte(r)) 11 } 12 } 13 return func(r rune) bool { return IndexRune(cutset, r) >= 0 } 14}

我们看到:就像沃德·坎宁安(Ward Cunningham)对“一等公民”的诠释中所说的那样,Go 中的函数可以像普通整型值那样被创建和使用。

除了上面那些例子,函数还可以被放入数组/切片/map 等结构中、可以像其他类型变量一样被赋值给 interface{}、甚至我们可以建立元素为函数的 channel,如下面例子:

1// function_as_first_class_citizen_1.go 2package main 3 4import "fmt" 5 6type binaryCalcFunc func(int, int) int 7 8func main() { 9 var i interface{} = binaryCalcFunc(func(x, y int) int { return x + y }) 10 c := make(chan func(int, int) int, 10) 11 fns := []binaryCalcFunc{ 12 func(x, y int) int { return x + y }, 13 func(x, y int) int { return x - y }, 14 func(x, y int) int { return x * y }, 15 func(x, y int) int { return x / y }, 16 func(x, y int) int { return x % y }, 17 } 18 19 c <- func(x, y int) int { 20 return x * y 21 } 22 23 fmt.Println(fns[0](5, 6)) 24 f := <-c 25 fmt.Println(f(7, 10)) 26 v, ok := i.(binaryCalcFunc) 27 if !ok { 28 fmt.Println("type assertion error") 29 return 30 } 31 32 fmt.Println(v(17, 7)) 33}

和 C/C++这类语言相比,作为“一等公民”的 Go 函数拥有难得的"灵活性"。接下来我们需要考虑如何使用 Go 函数才能发挥出它作为“一等公民”的最大效用。

2. 函数作为“一等公民”的特殊运用

1). 像整型变量那样对函数进行显式转型

Go 是类型安全的语言,Go 语言不允许隐式类型转换,因此下面的代码是无法通过编译的:

1var a int = 5 2var b int32 = 6 3fmt.Println(a + b) // 违法操作: a + b (不匹配的类型int和int32)

我们必须通过对上面代码进行显式的转型才能通过编译器的检查:

1var a int = 5 2var b int32 = 6 3fmt.Println(a + int(b)) // ok。输出11

函数是“一等公民”,对整型变量进行的操作也同样可以用在函数上面,即函数也可以被显式转型,并且这样的转型在特定的领域具有奇妙的作用。一个最为典型的示例就是 http.HandlerFunc 这个类型,我们来看一下例子:

1// function_as_first_class_citizen_2.go 2package main 3 4import ( 5 "fmt" 6 "net/http" 7) 8 9func greeting(w http.ResponseWriter, r *http.Request) { 10 fmt.Fprintf(w, "Welcome, Gopher!\n") 11} 12 13func main() { 14 http.ListenAndServe(":8080", http.HandlerFunc(greeting)) 15}

上述代码是我们日常最为常见的一个用 Go 构建的 Web Server 的例子。其工作机制很简单,当用户通过浏览器或类似 curl 这样的命令行工具访问 Web server 的 8080 端口时,会收到“Welcome, Gopher!”一行文字版应答。很多 Gopher 可能并未真正深入分析过这段代码,这里用到的恰恰是函数作为“一等公民”的特性,我们来看一下。

我们先来看一下 ListenAndServe 的源码:

1// $GOROOT/src/net/http/server.go 2func ListenAndServe(addr string, handler Handler) error { 3 server := &Server{Addr: addr, Handler: handler} 4 return server.ListenAndServe() 5}

ListenAndServe 会将来自客户端的 http 请求交给其第二个参数 handler 处理,而这里 handler 参数的类型 http.Handler 接口:

1// $GOROOT/src/net/http/server.go 2type Handler interface { 3 ServeHTTP(ResponseWriter, *Request) 4}

该接口仅有一个方法:ServeHTTP,其原型为:func(http.ResponseWriter, *http.Request)。这与我们自己定义的 http 请求处理函数 greeting 的原型是一致的。但是我们没法直接将 greeting 作为参数值传入,否则会报下面错误:

1func(http.ResponseWriter, *http.Request) does not implement http.Handler (missing ServeHTTP method)

即函数 greeting 并未实现接口 Handler 的方法,无法将其赋值给 Handler 类型的参数。

在代码中我们也并未直接将 greeting 传入 ListenAndServe,而是将 http.HandlerFunc(greeting)作为参数传给了 ListenAndServe。我们来看看 http.HandlerFunc 是什么?

1// $GOROOT/src/net/http/server.go 2 3type HandlerFunc func(ResponseWriter, *Request) 4 5// ServeHTTP calls f(w, r). 6func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) { 7 f(w, r) 8}

HandlerFunc 其实就是一个基于函数定义的新类型,它的底层类型为 func(ResponseWriter, *Request)。该类型有一个方法 ServeHTTP,继而实现了 Handler 接口。也就是说 http.HandlerFunc(greeting)这句代码的真正含义是将函数 greeting 显式转换为 HandlerFunc 类型,后者实现了 Handler 接口,满足 ListenAndServe 函数第二个参数的要求。

另外,之所以 http.HandlerFunc(greeting)这个语句可以通过编译器检查,正是因为 HandlerFunc 的底层类型正是 func(ResponseWriter, *Request),与 greeting 的原型是一致的。这和下面整型变量的转型原理并无二致:

1type MyInt int 2var x int = 5 3y := MyInt(x) // MyInt的底层类型为int,类比 HandlerFunc的底层类型为func(ResponseWriter, *Request)

为了充分理解这种显式转型的“技巧”,我们再来看一个简化后的例子:

1// function_as_first_class_citizen_3.go 2package main 3 4import "fmt" 5 6type BinaryAdder interface { 7 Add(int, int) int 8} 9 10type MyAdderFunc func(int, int) int 11 12func (f MyAdderFunc) Add(x, y int) int { 13 return f(x, y) 14} 15 16func MyAdd(x, y int) int { 17 return x + y 18} 19 20func main() { 21 var i BinaryAdder = MyAdderFunc(MyAdd) 22 fmt.Println(i.Add(5, 6)) 23}

和 Web server 那个例子一样,我们想将 MyAdd 这个函数赋值给 BinaryAdder 这个接口,直接赋值是不行的,我们需要一个底层函数类型与 MyAdd 一致的自定义类型的显式转换,这个自定义类型就是 MyAdderFunc,该类型实现了 BinaryAdder 接口,这样在经过 MyAdderFunc 的显式转型后,MyAdd 被赋值给了 BinaryAdder 的变量 i。这样通过 i 调用的 Add 方法实质上就是我们的 MyAdd 函数。

2) 函数式编程

Go 语言演进到如今,对多种编程范式或多或少都有支持。比如:对函数式编程的支持就得意于函数是“一等公民”的特质。虽然 Go 不推崇函数式编程,但有些时候应用一些函数式编程风格可以写出更加优雅、更简洁、更易维护的代码。

柯里化函数

我们先来看一种函数式编程的典型应用:柯里化函数(currying)。在计算机科学中,柯里化是把接受多个参数的函数变换成接受一个单一参数(原函数的第一个参数)的函数,并且返回接受余下的参数和返回结果的新函数的技术。这个技术以逻辑学家 Haskell Curry 命名。

定义总是拗口难懂,我们来用 Go 编写一个直观的柯里化函数的例子:

1// function_as_first_class_citizen_4.go 2package main 3 4import "fmt" 5 6func times(x, y int) int { 7 return x * y 8} 9 10func partialTimes(x int) func(int) int { 11 return func(y int) int { 12 return times(x, y) 13 } 14} 15 16func main() { 17 timesTwo := partialTimes(2) 18 timesThree := partialTimes(3) 19 timesFour := partialTimes(4) 20 fmt.Println(timesTwo(5)) 21 fmt.Println(timesThree(5)) 22 fmt.Println(timesFour(5)) 23}

运行这个例子:

1$ go run function_as_first_class_citizen_4.go 210 315 420

这里的柯里化是指将原接受两个参数的函数 times 转换为接受一个参数的 partialTimes 的过程。通过 partialTimes 函数构造的 timesTwo 将输入参数扩大为原先 2 倍、timesThree 将输入参数扩大为原先的 3 倍…。

这个例子利用了函数的几点性质:

  • 在函数中定义,通过返回值返回
  • 闭包

闭包是前面没有提到的 Go 函数支持的一个特性。 闭包是在函数内部定义的匿名函数,并且允许该匿名函数访问定义它的外部函数的作用域。本质上,闭包是将函数内部和函数外部连接起来的桥梁。

以上述示例来说,partialTimes 内部定义的匿名函数就是一个闭包,该匿名函数访问了其外部函数(partialTimes)的变量 x。这样当调用 partialTimes(2)时,partialTimes 实际上返回一个调用 times(2,y)的函数:

1timesTwo = func(y int) int { 2 return times(2, y) 3 }

函子(Functor)

函数式编程范式最让人”望而却步“的就是首先要了解一些抽象概念,比如上面的柯里化,再比如这里的函子(Functor)。什么是函子呢?具体来说,成为函子需要两个条件:

  • 函子本身是一个容器类型,以 Go 语言为例,这个容器可以是切片、map 甚至是 channel;
  • 光是容器还不够,该容器类型还需要实现一个方法,该方法接受一个函数类型参数,并在容器的每个元素上应用那个函数,得到一个新的函子,原函子容器内部的元素值不受到影响

我们还是用一个具体的示例来直观看一下吧:

1// function_as_first_class_citizen_5.go 2package main 3 4import ( 5 "fmt" 6) 7 8type IntSliceFunctor interface { 9 Fmap(fn func(int) int) IntSliceFunctor 10} 11 12type intSliceFunctorImpl struct { 13 ints []int 14} 15 16func (isf intSliceFunctorImpl) Fmap(fn func(int) int) IntSliceFunctor { 17 newInts := make([]int, len(isf.ints)) 18 for i, elt := range isf.ints { 19 retInt := fn(elt) 20 newInts[i] = retInt 21 } 22 return intSliceFunctorImpl{ints: newInts} 23} 24 25func NewIntSliceFunctor(slice []int) IntSliceFunctor { 26 return intSliceFunctorImpl{ints: slice} 27} 28 29func main() { 30 // 原切片 31 intSlice := []int{1, 2, 3, 4} 32 fmt.Printf("init a functor from int slice: %#v\n", intSlice) 33 f := NewIntSliceFunctor(intSlice) 34 fmt.Printf("original functor: %+v\n", f) 35 36 mapperFunc1 := func(i int) int { 37 return i + 10 38 } 39 40 mapped1 := f.Fmap(mapperFunc1) 41 fmt.Printf("mapped functor1: %+v\n", mapped1) 42 43 mapperFunc2 := func(i int) int { 44 return i * 3 45 } 46 mapped2 := mapped1.Fmap(mapperFunc2) 47 fmt.Printf("mapped functor2: %+v\n", mapped2) 48 fmt.Printf("original functor: %+v\n", f) // 原functor没有改变 49 fmt.Printf("composite functor: %+v\n", f.Fmap(mapperFunc1).Fmap(mapperFunc2)) 50}

运行这段代码:

1$ go run function_as_first_class_citizen_5.go 2init a functor from int slice: []int{1, 2, 3, 4} 3original functor: {ints:[1 2 3 4]} 4mapped functor1: {ints:[11 12 13 14]} 5mapped functor2: {ints:[33 36 39 42]} 6original functor: {ints:[1 2 3 4]} 7composite functor: {ints:[33 36 39 42]}

分析这段代码:

  • 这里我们定义了一个 intSliceFunctorImpl 类型,用来作为 functor 的载体;
  • 我们把 functor 要实现的方法命名为 Fmap,intSliceFunctorImpl 类型实现了该方法。同时该方法也是 IntSliceFunctor 接口的唯一方法;可以看到在这个代码中真正的 functor 其实是 IntSliceFunctor,这符合 Go 的惯用法;
  • 我们定义了创建 IntSliceFunctor 的函数:NewIntSliceFunctor。通过该函数以及一个初始切片,我们可以实例化一个 functor;
  • 我们在 main 中定义了两个转换函数,并将这两个函数应用到上述 functor 实例;我们看到得到的新 functor 的内部容器元素值是在原容器的元素值经由转换函数转换后得到的;
  • 在最后,我们还可以对最初的 functor 实例连续(组合)应用转换函数,这让我们想到了数学课程中的函数组合;
  • 无论如何应用转换函数,原 functor 中容器内的元素值不受到影响。

functor 非常适合对容器集合元素做批量同构处理,而且代码也要比每次都对容器中的元素作循环处理要优雅简洁许多。但要想在 Go 中发挥 functor 的最大效能,还需要 Go 对泛型提供支持,否则我们就需要为每一种容器类型都实现一套对应的 Functor 机制。比如上面的示例仅支持元素类型为 int 的切片,如果元素类型换为 string 或元素类型依然为 int,但容器类型换为 map,我们还需要分别为之编写新的配套代码。

延续传递式(Continuation-passing Style)

函数式编程离不开递归,以求阶乘函数为例,我们可以轻易用递归方法写出一个实现:

1// function_as_first_class_citizen_6.go 2func factorial(n int) int { 3 if n == 1 { 4 return 1 5 } else { 6 return n * factorial(n-1) 7 } 8} 9 10func main() { 11 fmt.Printf("%d\n", factorial(5)) 12}

这是一个非常常规的求阶乘的实现思路,但是这种思路并未应用到函数作为”一等公民“的任何特质。函数式编程有一种被称为延续传递式(Continuation-passing Style,以下简称为 CPS)的编程风格可以充分运用函数作为”一等公民“的特质。

在 CPS 风格中,函数是不允许有返回值的。一个函数 A 应该将其想返回的值显式传给一个 continuation 函数(一般接受一个参数),而这个 continuation 函数自身是函数 A 的一个参数。概念太过抽象,我们用一个简单的例子来说明一下:

下面得 Max 函数的功能是返回两个参数值中较大的那个值:

1// function_as_first_class_citizen_7.go 2package main 3 4import "fmt" 5 6func Max(n int, m int) int { 7 if n > m { 8 return n 9 } else { 10 return m 11 } 12} 13 14func main() { 15 fmt.Printf("%d\n", Max(5, 6)) 16}

我们把 Max 函数看作是上面定义中的 A 函数在未 CPS 化之前的状态。接下来,我们来根据 CPS 的定义将其转换为 CPS 风格:

  • 首先我们去掉 Max 函数的返回值,并为其添加一个函数类型的参数 f(这个 f 就是定义中的 continuation 函数)
1func Max(n int, m int, f func(int)) 2

将返回结果传给 continuation 函数,即把 return 语句替换为对 f 函数的调用

1func Max(n int, m int, f func(int)) { 2 if n > m { 3 f(n) 4 } else { 5 f(m) 6 } 7}

完整的转换后的代码如下:

1// function_as_first_class_citizen_8.go 2package main 3 4import "fmt" 5 6func Max(n int, m int, f func(y int)) { 7 if n > m { 8 f(n) 9 } else { 10 f(m) 11 } 12 13} 14 15func main() { 16 Max(5, 6, func(y int) { fmt.Printf("%d\n", y) }) 17}

接下来,我们使用同样的方法将上面的阶乘实现转换为 CPS 风格。

  • 首先我们去掉 factorial 函数的返回值,并为其添加一个函数类型的参数 f(这个 f 也就是 CPS 定义中的 continuation 函数)
1func factorial(n int, f func(y int))
  • 接下来,将 factorial 实现中的返回结果传给 continuation 函数,即把 return 语句替换为对 f 函数的调用
1func factorial(n int, f func(int)) { 2 if n == 1 { 3 f(n) 4 } else { 5 factorial(n-1, func(y int) { f(n * y) }) 6 } 7}

由于原 else 分支有递归,因此我们需要把未完成的计算过程封装为一个新的函数 f’作为 factorial 递归调用的第二个参数,f’的参数 y 即为原 factorial(n-1)的计算结果,而 n * y 是要传递给 f 的,于是 f’这个函数的定义就为:func(y int) { f(n * y) }。

转换为 CPS 风格的阶乘函数的完整代码如下:

1// function_as_first_class_citizen_9.go 2package main 3 4import "fmt" 5 6func factorial(n int, f func(int)) { 7 if n == 1 { 8 f(1) //基本情况 9 } else { 10 factorial(n-1, func(y int) { f(n * y) }) 11 } 12} 13 14func main() { 15 factorial(5, func(y int) { fmt.Printf("%d\n", y) }) 16}

我们简单解析一下上述实例代码的执行过程(下面用伪代码阐释):

1f1 = func(y int) { fmt.Printf("%v\n", y) 2factorial(5, f1) 3 4f2 = func(y int) {f1(5 * y)} 5factorial(4, f2) 6 7f3 = func(y int) {f2(4 * y)} 8factorial(3, f3) 9 10f4 = func(y int) {f3(3 * y)} 11factorial(2, f4) 12 13f5 = func(y int) {f4(2 * y)} 14factorial(1, f5) 15 16f5(1) 17 18=> 19 20f5(1) 21= f4(2 * 1) 22= f3(3 * 2 * 1) 23= f2(4 * 3 * 2 * 1) 24= f1(5 * 4 * 3 * 2 * 1) 25= 120

读到这里很多朋友会提出心中的疑问:这种 CPS 风格虽然利用了函数作为”一等公民“的特质,但是其代码理解起来颇为困难,这种风格真的好吗?朋友们的担心是有道理的。这里对 CPS 风格的讲解其实是一个”反例“,目的就是告诉大家,尽管作为”一等公民“的函数给 Go 带来的强大的表达能力,但是如果选择了不适合的风格或者说为了函数式而进行函数式编程,那么就会出现代码难于理解,且代码执行效率不高的情况(CPS 需要语言支持尾递归优化,但 Go 目前并不支持)。

3. 小结

成为”一等公民“的函数极大增强了 Go 语言的表现力,我们可以像对待值变量那样对待函数,上述函数编程思想的运用就得益于此。

让自己习惯于函数是”一等公民“,请牢记本节要点:

  • Go 函数可以像变量值那样被赋值给变量、作为参数传递、作为返回值返回和在函数内部创建等;
  • 函数可以像变量那样被显式转型;
  • 基于函数特质,了解 Go 中的几种有用的函数式编程风格:柯里化、函子;
  • 不要为了符合特定风格而滥用函数特质。
点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

Swift 简介

Swift和ObjectiveC的主要区别1,编程范式Swift可以面向协议编程、函数式编程、面向对象编程。Swift语言引入了协议、协议的扩展、泛型等新特性,因此使用Swift语言可以很好地面向协议编程;Swift语言将函数和闭包提升为语言的一等公民,函数可以作为一个变量、可以作为其他函数的参数、作为其他函数的返回值等来传递,所以

Scala学习(一):Scala简介与Hello World!

一.Scala是什么Scala是一门多范式的编程语言,类似于Java。设计初衷是实现可伸缩的语言、并集成面向对象编程和函数式编程的各种特性。二.环境准备(以Windows下安装Scala为例)1.官网:http://www.scalalang.org/ 下载安装包:!image(https://static.osc