首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

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

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

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

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

JavaScript:原型

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

在Java中,首先创建要一个

public class Person {

  private final String name;
  private final LocalDate birthdate;

  public Person(String name, LocalDate birthdate) {
    this.name = name;
    this.birthdate = birthdate;
  }

  public String getName() {
    return name;
  }

  public LocalDate getBirthdate() {
    return birthdate;
  }
}

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

var person1 = new Person("John Doe", LocalDate.now());
var person2 = new Person("Jane Doe", LocalDate.now());

JavaScript与Java的语法非常相似:

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

let person1 = new Person("John Doe", Date.now());
let person2 = new Person("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中实现这一目的的方法是使用静态方法创建一个类:

public class StringUtils {

  public static String capitalize(String string) {
    var character = string.substring(0, 1).toUpperCase();
    var rest = string.substring(1, string.length() - 1).toLowerCase();
    return character + rest;
  }
}

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

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

fun String.capitalize(): String {
  val character = substring(0, 1).toUpperCase()
  val rest = substring(1, length - 1).toLowerCase()
  return character + rest
}

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

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

Go:隐式接口实现

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

public interface Shape {

  float area();
  float perimeter();

  default void display() {
    System.out.println(this);
    System.out.println(perimeter());
    System.out.println(area());
  }
}
public class Rectangle implements Shape {

  public final float width;
  public final float height;

  public Rectangle(float width, float height) {
    this.width = width;
    this.height = height;
  }
  
  @Override
  public float area() {
    return width * height;           //(1)
  }

  @Override
  public float perimeter() {
    return 2 * width + 2 * height;   //(1)
  }

  public static void main(String... args) {
    var rect = new Rectangle(2.0f, 3.0f);
    rect.display();
  }
}

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

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

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

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

这可以转换为如下代码:

package main

import (
 "fmt"
)

type shape interface {        //(1)               
 area() float32
 perimeter() float32
}

type rectangle struct {         //(2)             
 width float32
 height float32
}

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

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

func main() {
 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中:

var statusCode: 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 也是采用这种递归的方法。

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

private int lengthOf(int size, 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 [] == list do
    size
  else
    [_ | tail] = list           //(1)
    length_of(size + 1, tail)
  end
end

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

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

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

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

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

(2)否则调用此函数

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

Python:for推导式

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

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

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

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

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

numbers = [n for n in [1, 2, 3, 4, 5]]
print(numbers)

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

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

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

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

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

numbers = [n ** 2 for n in [1, 2, 3, 4, 5] if n % 2 == 0]
print(numbers)

输出是 [4, 16]。

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

numbers = [a:n for n in [1, 2, 3] for a in ['a', 'b']]
print(numbers)

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

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

  • 发表于:
  • 本文为 InfoQ 中文站特供稿件
  • 首发地址https://www.infoq.cn/article/r4Io897HbGZvfhqSYPmy
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券