最酷的编程语言特性
每种语言的示例程序
随着示例程序库的不断扩充,我经常会反思自己学到的东西。我尤其喜欢比较和对比各种编程语言及其特性。在亲自体验了 100 多种编程语言之后,我想分享一下我个人认为最酷炫的编程语言特性列表。
现在,这类文章通常都是用来吸引点击的。比如,我本来可以很容易地把这篇文章命名为“十大最酷的编程语言特性”——说实话,我也考虑过这个——但我觉得这篇文章更适合作为一份动态文档。换句话说,我计划随着对编程语言的了解越来越多,不断补充和修改。
所以,记得时不时回来看看,别忘了分享一下。总之,我们赶紧开始吧。
功能列表
为了您的方便,我提供了每个酷炫功能的链接列表。
在接下来的章节中,您将发现越来越多炫酷的编程功能供您参考。浏览完毕后,欢迎在评论区分享您最喜欢的功能。
扩展方法
扩展方法是我有机会探索的最酷的编程语言特性之一。我第一次听说扩展方法是在用 Kotlin 实现 Hello World 的时候。当然,对于这么简单的程序来说,它们完全没有必要,但我还是有机会了解它们的存在。
如果您不确定什么是扩展方法,它们允许我们在现有类中添加方法,而无需直接扩展该类。例如,假设我们非常喜欢 Java 中的 String 类,但我们认为该类可以从变异方法中受益。目前,我们创建变异方法的唯一方法是创建我们自己的扩展 String 的类:
public class StringPlusMutation extends String {
public String mutate() {
// Add mutation code
}
}
同时,在 Kotlin 中,我们要做的就是编写一个直接扩展 String 类的方法:
fun String.mutate():
// Add mutation code
现在,无论何时创建一个字符串,我们都可以对其调用 mutate,就好像 mutate 是 String 中原始公共方法集的一部分一样。
当然,我应该提一下扩展方法也有其缺点。例如,使用上面的例子,如果 String API 增加了一个 mutate 方法会发生什么?这将是一个棘手的 bug,但可以肯定地说,这种情况很少发生。
无论如何,我个人只会考虑使用扩展方法来进行快速原型设计。如果你使用了它们,请在评论区告诉我。
宏
我遇到的另一个最酷的编程语言特性是宏。我第一次发现宏是在 Rust 中玩Hello World 的时候。毕竟,Rust 中的打印功能就是用宏实现的。
对于那些不了解的人来说,宏实际上是一种元编程特性。更具体地说,宏允许我们通过向抽象语法树添加规则来直接修改语言。这些规则是通过模式匹配生成的。
话虽如此,上面的解释相当密集,所以让我们看一个 Rust 中的例子:
macro_rules! print {
($($arg:tt)*) => ($crate::io::_print(format_args!($($arg)*)));
}
这条规则直接取自Rust 源代码。如我们所见,print 宏有一个模式匹配的情况,它本质上接受任意数量的参数,并在打印之前尝试对其进行格式化。
如果模式匹配的语法令人困惑,那么学习一些正则表达式的知识可能是个好主意。正则表达式的语法与模式匹配的语法类似。
当然,正如你可能看到的,宏是一个非常复杂的功能。事实上,它们通常很难编写,调试起来也同样困难。这就是为什么即使在 Rust 的文档中,宏也被认为是不得已而为之的功能。
自动属性
除了宏和扩展方法之外,最酷的编程语言特性之一就是自动属性。我第一次接触自动属性是在 C# 中,当时我发现它只是一种语法糖。之后,我在《C# 中的 Hello World》一书中写了一些关于它的内容。
简单来说,自动属性只是面向对象编程语言中 getter 和 setter 的简写。例如,假设我们有一个 Person 类,我们希望该类包含一个 name 字段。毕竟,人是有名字的,所以给该类添加这个字段是合理的。让我们用 Java 来实现它:
public class Person {
private String name;
}
现在,如果我们想要设置 name 属性,我们可能需要编写一个公共方法来更新私有的 name 字段。通俗地说,我们称这些方法为 setter 方法,因为它们可以设置对象的属性。然而,正式的叫法是mutator 方法。
在 Java 中,以下创建了一个 mutator 方法:
public setName(String name) {
this.name = name;
}
我们已经写了六行代码,但还不能获取用户的姓名。为此,我们编写了一个 getter 或访问器方法:
public getName() {
return this.name;
}
在具有自动属性的语言中,我们可以完全删除这六行样板代码。例如,在 C# 中,以下类与我们刚刚在 Java 中创建的类相同:
public class Person
{
public string Name { get; set; }
}
利用自动属性,我们可以将想要公开的每个字段的六行代码减少为一行 - 太棒了!
可选链式调用
接下来是最酷的编程语言特性列表中的可选链。我第一次发现可选链是在用 Swift 编写 Hello World 的时候。当然,这个特性对于实现 Hello World 来说并不是那么有用,但它仍然是一个值得探索的有趣概念。
在真正解释可选链之前,我们必须先了解什么是可选值。在 Swift 中,变量不能为空。换句话说,变量不能存储 NIL 值,至少不能直接存储。这是一个很棒的特性,因为我们可以假设所有变量都包含某个值。
当然,有时变量需要为 NIL。幸运的是,Swift 通过一个名为可选类型的装箱功能提供了这一功能。可选类型允许用户将值包装在一个容器中,而该容器可以解开后显示为值或 NIL:
var printString: String?
printString = "Hello, World!"
print(printString!)
在这个例子中,我们声明了一个可选的字符串,并赋予它的值为“Hello, World!”。由于我们知道该变量存储的是字符串,所以我们可以无条件地解包该值并打印出来。当然,无条件解包通常是一种不好的做法,所以我只是为了展示可选值而展示它。
无论如何,可选链采用了可选的概念,并将其应用于方法调用和字段。例如,假设我们有一些很长的方法调用链:
important_char = commandline_input.split('-').get(5).charAt(7)
在这个例子中,我们获取一些命令行输入,并用连字符将其拆分。然后,我们获取第五个标记并提取第七个字符。如果任何一个方法调用失败,我们的程序就会崩溃。
使用可选链,我们实际上可以在链中的任何位置捕获 NIL 返回值,并优雅地失败。我们不会崩溃,而是会得到一个为 NIL 的 important_char 值。这比处理毁灭金字塔要好得多。
Lambda 表达式
如果不谈 Lambda 表达式,我们可能无法列出最酷炫的编程语言特性。公平地说,Lambda 表达式并非新概念(参见:Lisp 中的 Hello World)。事实上,它的历史比计算机还要悠久。即便如此,Lambda 表达式仍然出现在现代语言中——即使是像 Java 这样的成熟语言中的新特性。
说实话,我第一次听说 Lambda 表达式大概是在三四年前,当时我正在教 Java。那时候,我并不清楚 Lambda 表达式是什么,也几乎不感兴趣。
然而,几年后,我开始使用 Python,它有大量利用 lambda 表达式的开源库。所以,到了某个时候,我不得不使用它们。
如果你从未听说过 Lambda 表达式,你可能听说过匿名函数。为了简单起见,它们是同义词。细微的区别在于 Lambda 表达式可以用作数据。更具体地说,我们可以将 Lambda 表达式打包成一个变量,并将其作为数据进行操作:
increment = lambda x: x + 1
increment(5) # Returns 6
在这个例子中,我们实际上创建了一个函数,将其存储在一个变量中,并像其他函数一样调用它。实际上,我们甚至可以让一个函数返回一个函数,从而动态地生成函数:
def make_incrementor(n):
return lambda x: x + n
addFive = make_incrementor(5)
addFive(10) # Returns 15
现在,这很酷!
渐进式打字
如果你有过一些编程经验,那么你可能对两种主要的数据类型很熟悉:静态类型和动态类型。当然,不要将这些术语与显式类型和隐式类型,甚至强类型和弱类型混淆。这三对术语的含义各不相同。
对我来说,最酷的编程语言特性之一就是静态类型和动态类型的交集,称为渐进类型。简单来说,渐进类型是一种语言特性,它允许用户精确地决定何时需要静态类型,否则就使用动态类型。
在许多支持渐进类型的语言中,此特性通过类型注解体现出来。类型注解与你在显式类型语言中通常会看到的内容非常相似,但注解是可选的:
def divide(dividend: float, divisor: float) -> float:
return dividend / divisor
这里,我们有一个完整注释的 Python 函数,它指示了预期的输入和输出类型。但是,我们也可以轻松地删除注释:
def divide(dividend, divisor):
return dividend / divisor
现在,我相信无论哪种情况,我们都可以向此函数传递几乎任何内容。但是,第一种选择为静态类型检查提供了机会,可以将其集成到 IDE 和其他静态分析工具中。我认为这是双赢的。
虽然我选择用 Python 作为例子,但我第一次遇到渐进式输入问题还是在研究《Hack 中的 Hello World》这篇文章的时候。显然,Facebook 确实想改进 PHP 的输入系统。
不变性
另一个最酷的编程语言特性是不变性。我第一次发现不变性是在一门高级语言课程上。这门课上,我们布置了一项作业,要求比较和评估大约 10 种不同语言的特性。在这些特性中,其中之一就是值语义,它隐含了不变性。
当然,什么是不变性?事实上,不变性描述的是变量(或者更确切地说是常量),它们一旦创建就无法更改。在许多语言中,不变性通常体现在字符串中。这就是为什么在循环中使用连接通常被认为是一个坏主意:
my_string = ""
for i in range(10):
my_string += str(i) # Generates a new string every iteration
print(my_string) # Prints "0123456789"
事实上,你可能会注意到,正是出于这个原因,我在“每种语言的字符串反转”系列中从未使用过连接操作。当然,语言开发者都知道这些常见的陷阱,所以他们会很好地优化我们的错误。
无论如何,在我使用 Elm 玩 Hello World之前,我从未真正见过一种语言实现除了字符串之外的不可变性。在 Elm 中,变量就像数学中一样是不可变的,因此以下代码片段会引发错误:
a = 2
a = a - 5
这里的问题在于递归。首先,我们定义a
。然后,我们尝试将其重新定义a
为自身的函数。对于我们这些通常使用命令式语言的人来说,这种语法我们甚至不会注意到。但对于数学家来说,第二个等式就存在严重问题。
当然,不变性的优点在于,值一旦创建,你就可以信任它。你永远不必担心它会在你背后改变。
多重调度
与 Lambda 表达式类似,多重分派并非编程语言中的新特性,但却是最酷炫的功能之一。因为它允许我们做一些有趣的事情,比如在对象层次结构中为同一个方法提供不同的实现。
最近,我在写Julia 中的 Hello World文章时想起了这个功能。Julia 显然是一种类似于 Python 和 R 的数值分析语言,并且支持通用编程。然而,与 Python 不同的是,Julia 支持多重分派。
说到这里,我可能应该区分一下单次分派和多次分派。单次分派是一种编程语言特性,它允许用户在对象层次结构中以不同的方式定义相同的方法。例如,考虑以下内容:
pikachu.tackle(charmander);
charmander.tackle(pikachu);
这里,我们假设在某个 Pokémon 层级结构中有两个对象:pikachu 和 charmander。这两个对象都重写了某个通用的 tackle 方法。运行这段代码时,JVM 能够调度合适的 tackle 方法。
当然,单一调度语言的情况可能会很糟糕,因为方法选择对于调用者来说只是动态的。至于参数,我们只能依赖静态类型。
假设小火龙被实例化为一个通用的宝可梦。如果使用单次调度,皮卡丘将使用通用的宝可梦拦截方法,而不是小火龙特有的拦截方法。因此,皮卡丘可能会错过拦截。
使用多重调度,这几乎不成问题,因为方法选择过程发生在运行时。自然而然地,合适的拦截方法被发出,皮卡丘成功拦截。
如果我的解释有误,请在评论区告诉我。为了得到更清晰的解释,我推荐 Eli Bendersky 的文章《多语言者多重调度指南》。
解构
还记得我提到过模式匹配吗?解构(也称为可迭代对象拆包)是另一种模式匹配的形式,用于从数据集合中提取数据。请看以下 Python 示例:
start, *_ = [1, 4, 3, 8]
print(start) # Prints 1
print(_) # Prints [4, 3, 8]
在这个例子中,我们可以提取列表的第一个元素,而忽略其余元素。同样,我们也可以轻松地提取列表的最后一个元素:
*_, end = ["red", "blue", "green"]
print(end) # Prints "green"
事实上,通过模式匹配,我们可以从数据集中提取任何我们想要的内容,假设我们知道它的结构:
start, *_, (last_word_first_letter, *_) = ["Hi", "How", "are", "you?"]
print(last_word_first_letter) # Prints "y"
这绝对是编程语言中最酷的功能之一了。我们不用手动用索引提取数据,而是可以编写一个模式来匹配需要解包或解构的值。
在线测试
说实话,我之前一直不知道该怎么称呼这个功能,因为我不知道它有没有正式的名称。不过,内联测试是我见过的最酷的编程语言功能之一。
我第一次接触到内联测试是在 Pyret 中,这门语言专为编程教育而设计。在 Pyret 中,单元测试是基本语法的一部分。换句话说,我们无需导入任何库或构建任何套件即可运行测试。
相反,Pyret 在源代码中包含了几个用于测试的子句:
fun sum(l):
cases (List) l:
| empty => 0
| link(first, rest) => first + sum(rest)
end
where:
sum([list: ]) is 0
sum([list: 1, 2, 3]) is 6
end
这里,我们可以看到一个很棒的列表求和函数。该函数包含两种基本情况:空和非空。如果为空,函数返回 0。否则,函数执行求和。
到那时,大多数语言都已经完成了,测试也就成了事后才考虑的事情。然而,Pyret 并非如此。为了添加测试,我们添加了一个 where 子句。在本例中,我们测试了一个空列表和一个预期和为 6 的列表。
代码执行时,测试也会运行。然而,测试是非阻塞的,因此除非出现灾难性问题,代码将继续运行。
无论如何,单从可维护性方面来说,我喜欢这个功能。使用 Pyret 时,开发过程中绝不会忘记测试。
内联汇编程序
哦,你以为内联测试是唯一酷炫的内联功能吗?来认识一下内联汇编吧:这是我在用 D 语言编写 Hello World时第一次了解到的最酷炫的编程语言功能之一。
事实证明,内联汇编程序是一种编程语言特性,允许开发人员直接利用系统架构。当使用支持内联汇编程序的语言时,以下代码完全合法:
void *pc;
asm
{
pop EBX ;
mov pc[EBP], EBX ;
}
这里,我们看起来像是 C/C++ 和汇编语言的混合体,本质上就是这样。我们编写的代码可以直接与底层架构兼容,所以我们清楚地知道发生了什么。
我觉得这个特性真的很有趣,因为语言在有机会的时候往往会选择抽象。而使用 D,我们可以完全放弃内置功能,自己编写自定义的汇编级实现。
您最喜欢的功能是什么?
你最喜欢的功能没有入选最酷的编程语言功能列表吗?请在下方评论区留言告诉我,我会去看看!当然,如果你喜欢这篇文章,别忘了分享哦。我相信其他人也会喜欢的。
无论如何,感谢您抽出时间浏览我的作品。如果您喜欢,不妨考虑订阅 The Renegade Coder。我的大部分写作都是在那里进行的。事实上,这篇文章就是从该网站转发的,所以不妨看看。
鏂囩珷鏉ユ簮锛�https://dev.to/therenegadecoder/the-coolest-programming-language-features-1953