24

编程语言中的 6 个有趣特性

 4 years ago
source link: https://www.infoq.cn/article/r4Io897HbGZvfhqSYPmy
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Java 是一门不断发展的语言,这是一件好事。然而,其他语言的一些特性也是值得研究的。语言的结构是人们思考问题的方式,也是人们设计解决方案的方式。学习或至少熟悉其他语言是借鉴其设计的好方法。

Java 是我学习的第一门语言并且是我专业使用的语言。它是我大约十五年以来的主要谋生手段。然而,它并不是我多年来学习和使用的唯一语言:例如,很久以前,我必须开发 JavaScript 代码来实现动态用户界面。当时,它被称为 DHTML ……几年前,我还自学了 Kotlin,并且从未停止过使用它。去年,在一家新公司工作时,我尝试了 Clojure,但没有成功。

在上述所有场景中,Java 仍然是我学习和评判其他语言的基准。以下是一些有趣的语言特性,我认为这些特性对于来自 Java 背景的人都颇具思想挑战性。

JavaScript:原型

JavaScript 是我和 Java 一起使用的第一种语言。尽管 JavaScript 已经发展这么多年了,但它有一个实现起来非常奇怪的常见特性:新对象的实例化。

在 Java 中,首先创建要一个

复制代码

publicclassPerson{

privatefinalStringname;
privatefinal LocalDate birthdate;

publicPerson(Stringname, LocalDate birthdate){
this.name = name;
this.birthdate = birthdate;
}

publicStringgetName(){
returnname;
}

publicLocalDategetBirthdate(){
returnbirthdate;
}
}

然后,就可以继续创建该类的 实例 了:

复制代码

varperson1 =newPerson("John Doe", LocalDate.now());
varperson2 =newPerson("Jane Doe", LocalDate.now());

JavaScript 与 Java 的语法非常相似:

复制代码

classPerson{
constructor(name, birthdate) {
this.name = name;
this.birthdate = birthdate;
}
}

letperson1 =newPerson("John Doe",Date.now());
letperson2 =newPerson("Jane Doe",Date.now());

相似之处到此为止。由于 JavaScript 具有动态特性,所以可以向现有实例中添加属性和函数。

复制代码

person1.debug=function(){
console.debug(this);
}

person1.debug();

但是,这些只能添加到某个实例中。其他实例会缺少这些补充属性或函数:

复制代码

person2.debug();// Throws TypeError: person2.debug is not a function

要将函数(或属性)添加到 所有实例 (无论是现在的还是将来的)中,都需要利用 原型 的概念:

复制代码

Person.prototype.debug= function() {
console.debug(this);
}

person1.debug();
person2.debug();

let person3 = new Person("Nicolas", Date.now());

person3.debug();

Kotlin:扩展函数 / 属性

几年前,我开始尝试着自学 Android。我发现这种体验对开发人员来说不太友好:当然,我了解它其中一个目标是尽可能减少内存占用,但这是以非常简洁的 API 为代价的。

我记得当时我必须调用带有很多参数的方法,其中大多数参数为 null 。在尝试寻找到一种方法来解决这个问题时,找到了 Kotlin 的扩展属性:带有默认参数。我后来停止了 Android 的学习,但仍继续使用 Kotlin。

我喜欢 Kotlin。很多人都称赞 Kotlin 的 null 安全性(null-safety)实现。但对我来说,我喜欢它,并不是因为它是 null 安全的,而是因为别的。

假设我们经常需要将字符串首字母改成大写。在 Java 中实现这一目的的方法是使用静态方法创建一个类:

复制代码

publicclassStringUtils{

publicstatic String capitalize(Stringstring) {
var character =string.substring(0,1).toUpperCase();
var rest =string.substring(1,string.length() -1).toLowerCase();
returncharacter + rest;
}
}

在早期,每个项目几乎都具有 StringUtils 和 DateUtils 类。幸运的是,现有的库提供了最常用的功能,例如 Apache Commons LangGuava 。然而,它们仍遵循相同的设计原则,即遵循基于静态方法的设计原则。这很糟糕,因为 Java 被认为是一种面向对象语言。不幸的是,静态方法不是面向对象的。

扩展函数 和属性的帮助下,Kotlin 允许将行为、状态分别添加到现有的类中。语法非常简单,并且与面向对象的方法完全兼容:

复制代码

funString.capitalize(): String {
valcharacter = substring(0,1).toUpperCase()
valrest = substring(1, length -1).toLowerCase()
returncharacter + rest
}

在编写 Kotlin 代码时,我经常使用这个。

在底层,Kotlin 编译器生成与 Java 代码类似的字节码。这仅仅是语法糖,但是从设计的角度来看,与 Java 代码相比,它是一个巨大的改进!

Go:隐式接口实现

在大多数面向对象语言(Java、Scala、Kotlin 等)中,类可以实现一个 契约 (也称为 接口 )。这样,客户端代码可以引用该接口,而无需关心任何特定的实现。

复制代码

publicinterface Shape {

floatarea();
floatperimeter();

defaultvoiddisplay(){
System.out.println(this);
System.out.println(perimeter());
System.out.println(area());
}
}
publicclassRectangleimplementsShape{

publicfinalfloatwidth;
publicfinalfloatheight;

publicRectangle(floatwidth,floatheight){
this.width=width;
this.height=height;
}

@Override
publicfloatarea(){
returnwidth*height;//(1)
}

@Override
publicfloatperimeter(){
return2*width+2*height;//(1)
}

publicstaticvoidmain(String... args){
varrect=newRectangle(2.0f,3.0f);
rect.display();
}
}

(1)处为了精确起见,应该使用 BigDecimal ,但这不是重点

重点是:由于 Rectangle 实现了 Shape,所以可以在 Rectangle 的任何实例上调用在 Shape 上定义的 display() 方法。

Go 不是一种面向对象语言:它没有类的概念。它提供了结构体,并且函数可以与这种结构体相关联。它还提供了接口,该接口可以使用结构体来实现。

然而,Java 实现接口的方式是 显式的 :Rectangle 类声明它实现了 Shape。相反,Go 的方式是隐式的。实现接口所有函数的结构体隐式地实现了该接口。

这可以转换为如下代码:

复制代码

packagemain

import(
"fmt"
)

typeshapeinterface{//(1)
area()float32
perimeter()float32
}

typerectanglestruct{//(2)
widthfloat32
heightfloat32
}

func(rect rectangle)area()float32{//(3)
returnrect.width * rect.height
}

func(rect rectangle)perimeter()float32{//(3)
return2* rect.width +2* rect.height
}
funcdisplay(shape shape){//(4)
fmt.Println(shape)
fmt.Println(shape.perimeter())
fmt.Println(shape.area())
}

funcmain(){
rect := rectangle{width:2, height:3}
display(rect)//(5)
}

(1)定义 shape 接口

(2)定义 rectangle 结构体

(3)将两个 shape 函数添加到 rectangle 中

(4)display() 方法只接收一个 shape 参数

(5)因为 rectangle 实现了 shape 的所有函数,并且由于是隐式实现的,所以 rect 也是一个 shape。因此,调用 display() 方法并将 rect 作为参数进行传递是完全合法的

Clojure:“依赖类型”

我之前的公司对 Clojure 投入了大量的资金。正因为如此,我努力学习过这门语言,甚至还写了 几篇文章 来总结我对它的理解。

Clojure 深受 LISP 的启发。因此,表达式用圆括号括起来,首先执行位于圆括号内部的方法。此外,Clojure 是一种动态类型语言:它们虽然有类型,但没有声明。

另一方面,该语言提供了基于契约的编程。可以指定前置条件和后置条件:它们在运行时计算。这些条件可以进行类型检查, 例如,检查参数是字符串还是布尔值等?甚至可以进行更进一步地检查,类似于 _dependent 类型:

在计算机科学和逻辑学中,依赖类型是其定义依赖于某个值的类型。“整数对”是一种类型。由于对值的依赖,“第二个大于第一个的整数对”也是依赖类型。

— 维基百科

https://en.wikipedia.org/wiki/Dependent_type

它在运行时强制执行,因此它不能被真正称为依赖类型。然而,这是我所接触过的语言中最接近依赖类型的一种了。

之前,我曾详细写过一篇关于依赖类型和基于契约编程的 文章

Elixir :模式匹配

一些语言吹嘘自己提供了模式匹配的特性。通常,模式匹配可用于计算变量,例如,在 Kotlin 中:

复制代码

varstatusCode: Int
val errorMessage =when(statusCode) {
401->"Unauthorized"
403->"Forbidden"
500->"Internal Server Error"
else->"Unrecognized Status Code"
}

这个用法是类固醇上(steroids)的 switch 语句。然而,一般来说,模式匹配的应用要广泛得多。在下面的代码片段中,首先检查常规 HTTP 状态错误码,如果没有找到,则默认设成更通用的错误信息:

复制代码

val errorMessage =when{
statusCode== 401 ->"Unauthorized"
statusCode== 403 ->"Forbidden"
statusCode- 400 < 100 ->"Client Error"
statusCode== 500 ->"Internal Server Error"
statusCode- 500 < 100 ->"Server Error"
else->"Unrecognized Status Code"
}

不过,它是有限制的。

Elixir 是一种在 Erlang OTP 上运行的动态类型语言,它将模式匹配提升到了一个全新的水平。Elixir 的模式匹配可用于简单的变量析构:

复制代码

{a,b, c} = {:hello,"world",42}

a 将被赋值成 :hello,b 被赋值成 “world”,c 被赋值成 42。

它还可以对集合进行更高级的析构:

复制代码

[head | tail] = [1,2,3]

head 被赋值成 1,tail 被赋值成 [2, 3]。

然而,对于函数重载来说,它甚至更是如此。作为一种函数式语言,Elixir 没有用于循环的关键字(for 或 while),循环需要使用递归来实现。

举个例子,我们使用递归来计算 List 的大小。在 Java 中,这是很容易的,因为有一个 size() 方法,但是 Elixir API 没有提供这样的功能。让我们用如下的伪代码来实现该功能,Elixir 也是采用这种递归的方法。

复制代码

publicintlengthOf(List<?>item){
return lengthOf(0,items);
}

privateintlengthOf(intsize, List<?>items){
if(items.isEmpty()) {
return size;
}else{
return lengthOf(size+ 1,items.remove(0));
}
}

几乎可以将它逐行的转换成 Elixir:

复制代码

def length_of(list),do: length_of(0,list)

defp length_of(size,list)do
if[]==listdo
size
else
[_|tail]=list//(1)
length_of(size+ 1,tail)
end
end

(1)变量析构的模式匹配。表头的值被赋值给 _ 变量,这意味着以后就无法引用它了,因为它没有用处了。

然而,如前所述,Elixir 模式匹配也适用于函数重载。因此,Elixir 的命名方式将是:

复制代码

deflist_len(list),do:list_len(0,list)

defplist_len(size, []),do: size//(1)
defplist_len(size,list)do//(2)
[_|tail]=list
list_len(size+ 1,tail)
end

(1)如果列表为空,则调用此方法

(2)否则调用此函数

注意,模式是按照声明的顺序进行评估的:在上面的代码段中, Elixir 首先评估具有空列表的函数,如果不匹配,才评估第二个函数,即列表不为空。如果要以相反的顺序声明函数,则每次都会对非空列表进行匹配操作。

Python:for 推导式

Python 是一种动态类型语言。与 Java 一样,Python 通过 for 关键字提供循环功能。下面的代码片段循环遍历集合中的所有项,并逐个打印它们。

复制代码

fornin[1,2,3,4,5]:
print(n)

要在新集合中收集所有项,可以先创建一个空集合,然后在循环中添加每个项到空集合中:

复制代码

numbers = []
fornin[1,2,3,4,5]:
numbers.append(n)
print(numbers)

然而,可以使用一个精美的 Python 特性: for 推导式(for comprehensions) 。虽然它与标准循环使用相同的 for 关键字,但是 for 推导式是一个能获得相同结果的函数式构造器。

复制代码

numbers = [nfornin[1,2,3,4,5]]
print(numbers)

上面片段的输出是 [1, 2, 3, 4, 5] 。

也可以转换每个项。例如,下面的代码段将计算每个项的平方:

复制代码

numbers = [n **2fornin[1,2,3,4,5]]
print(numbers)

输出是 [1, 4, 9, 16, 25]。

for 推导式的一个好处是能够使用条件语句。例如,下面的代码片段将只过滤偶数项,然后将其平方:

复制代码

numbers = [n **2fornin[1,2,3,4,5]ifn %2==0]
print(numbers)

输出是 [4, 16]。

最后,for 推导式允许使用笛卡尔积。

复制代码

numbers = [a:nfornin[1,2,3]forain['a','b']]
print(numbers)

它将会输出 [(‘a’, 1), (‘b’, 1), (‘a’, 2), (‘b’, 2), (‘a’, 3), (‘b’, 3)]。

以上的 for 推导式也被称为 列表推导式(list comprehensions) ,因为它们是为了创建新的列表而设计的。 Map 推导式(Map comprehension) 也是非常相似的,目的是为了创造 map。

原文链接:

https://blog.frankel.ch/six-interesting-features-programming-languages/


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK